Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions .claude/commands/pharn-build.md

Large diffs are not rendered by default.

41 changes: 27 additions & 14 deletions .claude/commands/pharn-dev-verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,43 @@ mkdir -p .pharn/pharn-dev-verify
npm test > /dev/null 2>&1; t=$? # the hermetic suite (incl. the feature's own *.test.*)
node .dev/floor/validate.mjs . > /dev/null 2>&1; v=$? # the structural floor — must be GREEN
npm run lint > /dev/null 2>&1; l=$? # eslint clean
npm run format:check > /dev/null 2>&1; f=$? # prettier clean — whole-repo (L9: track full `npm run check`)
npm run lint:md > /dev/null 2>&1; lm=$? # markdownlint clean — whole-repo (L9)
# per committed eval pair the feature ships (see below) — one structural:<expected> gate each:
node .dev/floor/check-structural.mjs <expected.json> <actual.json> . > /dev/null 2>&1; s=$?
# assemble → .pharn/pharn-dev-verify/results.json, one entry per gate actually run:
printf '{"test":%d,"validate":%d,"lint":%d,"structural:%s":%d}' "$t" "$v" "$l" "<expected.json>" "$s" \
printf '{"test":%d,"validate":%d,"lint":%d,"format:check":%d,"lint:md":%d,"structural:%s":%d}' \
"$t" "$v" "$l" "$f" "$lm" "<expected.json>" "$s" \
> .pharn/pharn-dev-verify/results.json
```

- **The gates are the existing checks — `/pharn-dev-verify` invents none** (`npm test`, `.dev/floor/validate.mjs`,
`.dev/floor/check-structural.mjs`, `npm run lint`). It orchestrates them; it does not reimplement checking
logic.
`.dev/floor/check-structural.mjs`, `npm run lint`, `npm run format:check`, `npm run lint:md`). It orchestrates
them; it does not reimplement checking logic. The `format:check` + `lint:md` + `lint` + `test` set is exactly
the repo's `npm run check` aggregate, so the verdict **tracks the full `npm run check`** — closing L9's
style-gate coverage hole **at verify** (an increment's own markdown style is caught here, not only at the full
`npm run check` / CI; `.dev/memory-bank/lessons-learned.md` L9 — cited, not restated, P4).
- **`structural:<expected>` — one gate per committed eval pair the feature ships,** discovered by
convention (P5 — membership, not classification): each `<cap>/evals/expected/*.json` with its committed
actual `findings.json` (the emission contract of `pharn-contracts/finding-shape.md` — cited, not
restated, P4). Today the one pair is `pharn-review/trust-fence/evals/expected/expected-injection-comment.json`
↔ `.dev/features/trust-fence/findings.json`. A feature shipping **no** eval-actual pair simply has **no**
`structural:*` gate (absent from the map) — exactly as `/pharn-dev-regress` handles it.
- **The core gates are stdlib-only** (`node --test`, `validate`, `check-structural`); `lint` needs the
dev devDeps already present in the working tree (no `npm ci` — `/pharn-dev-verify` runs only at HEAD, never in a
detached worktree).
- **Granularity (honest, not a silent gap — P7):** `test` / `validate` / `lint` are **whole-repo** (they
re-run the full suite with the feature present — the most honest "is it green with this in it"); the
**feature-specific** correctness signal is the `structural:*` gate over the feature's own evals plus the
feature's own `*.test.*` collected by `npm test`. The verdict is exactly as good as that deterministic
suite — never more (P0/P7).
- **The core gates are stdlib-only** (`node --test`, `validate`, `check-structural`); `lint` / `format:check` /
`lint:md` need the dev devDeps already present in the working tree (no `npm ci` — `/pharn-dev-verify` runs only
at HEAD, never in a detached worktree, so the style gates are cheap).
- **Granularity (honest, not a silent gap — P7):** `test` / `validate` / `lint` / `format:check` / `lint:md`
are **whole-repo** (they re-run the full suite/style over the repo with the feature present — the most honest
"is it green with this in it", so verify PASS requires the **whole** repo clean, not just the increment's
files); the **feature-specific** correctness signal is the `structural:*` gate over the feature's own evals
plus the feature's own `*.test.*` collected by `npm test`. The verdict is exactly as good as that
deterministic suite — never more (P0/P7).
- **The gate SET is advisory orchestration (two clocks, kept honest — L9, P0).** `check-verify.mjs` (the FLOOR
verdict) is generic over gate keys — it computes `PASS iff every gate exit 0` over **whatever** map this
command assembles. So the floor verdict mechanically covers `format:check` + `lint:md`, but **which** gates
are in the map is this command's **advisory** composition — there is no floor or test lock that the two style
gates STAY in the set. L9's remedy therefore lives in this orchestration layer (exactly where L9 places it),
not in a new floor primitive; do not read "verify runs the style gates" as floor-locked.

## Step 2 — ADVISORY layer: the verifier plug-in slot (LLM judgment — annotates, never gates)

Expand Down Expand Up @@ -157,7 +170,7 @@ Write, in order (re-scoping per artifact, per Step 0's caveat):
```json
{
"feature": "<name>",
"gates": { "test": 0, "validate": 0, "lint": 0, "structural:<expected>": 0 },
"gates": { "test": 0, "validate": 0, "lint": 0, "format:check": 0, "lint:md": 0, "structural:<expected>": 0 },
"verdict": "PASS",
"failing_gates": [],
"verifiers": { "registered": 0, "findings": [] }
Expand Down Expand Up @@ -245,8 +258,8 @@ verifiers today, no such free-text is produced yet; the boundary is in place for

## Live integration (manual when verifiers exist; the floor verdict is hermetically tested)

With **zero verifiers**, `/pharn-dev-verify` runs only stdlib gates + `npm run lint` and makes **no `claude -p`
call** — runnable in CI-like conditions. When a verifier is added it needs `claude -p` (tokens, auth) and
With **zero verifiers**, `/pharn-dev-verify` runs only stdlib gates + `npm run lint` / `format:check` / `lint:md`
and makes **no `claude -p` call** — runnable in CI-like conditions. When a verifier is added it needs `claude -p` (tokens, auth) and
is run **by hand**, like `/pharn-dev-eval`. The deterministic proof of the **verdict** logic is
`.dev/floor/check-verify.test.mjs` (pre-recorded `{gate:exit}` fixtures, **no** `claude -p`), which `npm test`
auto-collects via its `**/*.test.mjs` glob. This file is a command `.md` (not `*.test.mjs`), so `npm
Expand Down
25 changes: 23 additions & 2 deletions .claude/commands/pharn-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,20 @@ spec_content_hash: <the SPEC's pinned hash, copied verbatim> # fix #4 — carrie

<the implementation strategy derived from the approved intent — ADVISORY model work>

## Steps / Files
## Steps

- <a concrete step or file to change>
- <a concrete implementation step — ADVISORY prose>
- <…>

## Files

- `<path/to/file>` — <what this file does / what changes>
- `<…>`

### Explicitly not touched

- `<reused/or/excluded/path>` — <reused / shelled / out of scope; never edited>

## Acceptance mapping

- <each SPEC Acceptance Criterion> → <how this plan satisfies it>
Expand All @@ -147,6 +156,18 @@ spec_content_hash: <the SPEC's pinned hash, copied verbatim> # fix #4 — carrie
- <anything to flag for the human / the next stage>
```

> **`## Files` is the PARSEABLE writes-scope (not prose).** `/pharn-build` derives its fix #7
> writes-scope from **this** section via `set-writes-scope.cjs --from-plan` — cite that contract
> (its `## Files` extractor) + `ARCHITECTURE.md §6`, do not restate (P4). Three rules keep it
> parseable: (1) the heading is exactly `## Files`; (2) each authorized path is a list item whose
> **leading token is a back-tick path** — ``- `path/to/file` — <what changes>``; (3) to **exclude** a
> path, put it under the `### Explicitly not touched` **subsection** (the setter stops at that
> heading) — **never** inline as ``- `path` — not touched`` (an inline-marked item still enters
> scope). Keep an unfilled placeholder in **angle-brackets** (`` `<path>` ``) so an un-filled
> `## Files` **fails closed** at the setter — a bare word like `` `path` `` would wrongly parse as a
> real scope path. The `## Steps` above is **advisory prose**; only `## Files` back-tick paths become
> the build's scope, and `/pharn-build` writes nothing outside them (fix #7).

`/pharn-plan` does **one** thing — it lands **one** plan derived from an approved spec. It does **not**
chain to `/pharn-grill` or `/pharn-build` (later stages). **End your turn.**

Expand Down
91 changes: 91 additions & 0 deletions .claude/hooks/set-writes-scope.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const { join } = require("node:path");

const SETTER = join(__dirname, "set-writes-scope.cjs");
const PLAN_CMD = join(__dirname, "..", "commands", "pharn-dev-plan.md");
const PLAN_PRODUCT_CMD = join(__dirname, "..", "commands", "pharn-plan.md");

function tmp() {
return fs.mkdtempSync(join(os.tmpdir(), "pharn-sws-"));
Expand All @@ -45,3 +46,93 @@ test("artifact-split lock: a ROOT features/<name> --target is REJECTED (pharn-de
assert.equal(r.status, 1);
assert.equal(fs.existsSync(join(cwd, ".pharn", "writes-scope.json")), false);
});

// --- fail-closed: --from-plan on a PLAN with no parseable `## Files` (the /pharn-build crux) ---
// /pharn-build sets its writes-scope via `set-writes-scope.cjs --from-plan PLAN.md`, which requires a
// `## Files` heading whose items lead with a back-tick path. The product /pharn-plan template currently
// emits a free-text `## Steps / Files` section instead — so the setter MUST fail-closed (exit 1, no scope
// written) rather than guess a scope from un-parseable prose. This pins that crux scenario (previously
// uncovered: every other --from-plan test feeds a present `## Files`).

test("--from-plan on a PLAN with no `## Files` heading (a free-text `## Steps / Files`) exits 1 and writes nothing (fail-closed)", () => {
const cwd = tmp();
const plan = join(cwd, "PLAN.md");
fs.writeFileSync(
plan,
["# PLAN — x", "", "## Steps / Files", "", "- a concrete step or file to change", "- another step", ""].join("\n")
);
const r = setter(cwd, "--from-plan", plan);
assert.equal(r.status, 1);
assert.equal(fs.existsSync(join(cwd, ".pharn", "writes-scope.json")), false);
});

// --- closing-the-loop: --from-plan SUCCEEDS on a PLAN in /pharn-plan's NEW emitted shape ---
// The inverse of the fail-closed test above (and of the /pharn-build crux). After the `plan-files-scope`
// increment, the product /pharn-plan template emits a parseable `## Files` (a `## Files` heading whose
// items lead with a back-tick path), splitting the old free-text `## Steps / Files`. This pins that a
// PLAN in that shape sets a scope = exactly its `## Files` back-tick paths — proving the product chain
// spec → plan → build can now derive a writes-scope. The fixture mirrors the template's section
// structure (## Approach / ## Steps / ## Files / ### Explicitly not touched / ## Acceptance mapping) so
// it pins the PRODUCER's shape, not an arbitrary parser-accepted one (cf. the producer-faithfulness test
// below, which runs the setter over the real pharn-plan.md template).

test("--from-plan on a /pharn-plan-shaped PLAN (## Files with back-tick paths) exits 0; scope = exactly the authorized paths", () => {
const cwd = tmp();
const plan = join(cwd, "PLAN.md");
fs.writeFileSync(
plan,
[
"---",
"spec_id: sample-feature",
"spec_content_hash: 0000000000000000000000000000000000000000000000000000000000000000",
"---",
"",
"## Approach",
"",
"Rework the widget pipeline; the public `src/should-not-leak.ts` API is not touched.",
"",
"## Steps", // advisory prose BEFORE ## Files — its back-tick paths must NOT enter scope
"",
"- `src/also-not-scope.ts` — a step that names a file in back-ticks above ## Files",
"- wire the new module into the pipeline",
"",
"## Files",
"",
"- `src/widget.ts` — the new widget module",
"- `src/widget.test.ts` — its unit tests",
"",
"### Explicitly not touched", // a heading → the setter stops here; these paths never enter scope
"",
"- `src/legacy.ts` — reused, never edited",
"",
"## Acceptance mapping",
"",
"- AC-1 → the widget renders",
"",
].join("\n")
);
const r = setter(cwd, "--from-plan", plan);
assert.equal(r.status, 0); // SUCCESS — the inverse of the fail-closed cases above
const rec = JSON.parse(fs.readFileSync(join(cwd, ".pharn", "writes-scope.json"), "utf8"));
// scope is EXACTLY the ## Files back-tick paths, in order …
assert.deepEqual(rec.scope, ["src/widget.ts", "src/widget.test.ts"]);
// … and excludes the `### Explicitly not touched` path (the #15 hardening, here for a product plan) …
assert.equal(rec.scope.includes("src/legacy.ts"), false);
// … and excludes a back-tick path that appeared in ## Steps / ## Approach BEFORE ## Files.
assert.equal(rec.scope.includes("src/also-not-scope.ts"), false);
assert.equal(rec.scope.includes("src/should-not-leak.ts"), false);
});

// --- producer-faithfulness: the REAL product /pharn-plan template fails closed (locks the placeholder
// style). The template's `## Files` example items are angle-bracket placeholders (`- `<path>``), which
// `isConcrete` rejects → the setter emits no scope and exits 1. This ties a test to the actual producer
// file: if the template ever regressed to a BARE-WORD example (`- `path``), that bare word would parse
// as a real scope path, the setter would exit 0, and THIS test would fail — catching the regression
// (the fail-closed-on-unfilled discipline the dev /pharn-dev-plan `<path>` placeholder also preserves).

test("--from-plan over the real pharn-plan.md template exits 1 (its `## Files` `<path>` placeholders fail closed)", () => {
const cwd = tmp();
const r = setter(cwd, "--from-plan", PLAN_PRODUCT_CMD);
assert.equal(r.status, 1);
assert.equal(fs.existsSync(join(cwd, ".pharn", "writes-scope.json")), false);
});
31 changes: 31 additions & 0 deletions .dev/features/build-caveat-sync/GRILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# GRILL — build-caveat-sync (advisory interrogation of PLAN.md)

**Plan:** `.dev/features/build-caveat-sync/PLAN.md` (pure doc-sync of the stale scope-source caveat in `pharn-build.md`). **OQ1** resolved → PRESERVE the fail-closed framing.
**Spec-hash check (content-hash floor primitive — surfaced, not blocking):** recomputed `sha256(ARCHITECTURE.md)` = `11cd9ad5983188623fe0931d13588c16435a5565888344e20669748947d1d969` — **matches** the plan's pin (`PLAN.md:3`). No drift.

> **This grill is ADVISORY end-to-end (P0).** No finding gates `/pharn-dev-build`. Enum-gated fields are my own assertions; free-text quotes the (untrusted) plan as DATA; `severity` is an advisory assignment (fix #3).

## Findings

### Axis P6 / P0 — completeness of the doc-sync (the caveat HEADING is also stale)

```yaml
- type: FINDING
rule_id: P6
severity: minor # advisory assignment (fix #3)
file: ".dev/features/build-caveat-sync/PLAN.md:26"
problem: "The plan rewrites the caveat's BODY and the inline example, but the caveat's HEADING still labels the gap 'a current, honest limit — LIMITS.md'; once /pharn-plan emits a parseable `## Files`, the gap is RESOLVED — it is no longer 'a current limit', so the LIMITS.md framing in the heading is itself stale and should be dropped/reframed too."
evidence: 'PLAN.md:26 — ''(1) Rewrite the "Scope-source caveat" blockquote (:103-107): replace the … framing …''. The heading at pharn-build.md:103 reads ''**Scope-source caveat (a current, honest limit — LIMITS.md).**'' Confirmed live: LIMITS.md carries NO specific entry for this gap (grep → no match), so dropping the LIMITS.md reference is clean — there is no LIMITS.md text to keep in sync.'
```

**For the build to weigh:** when rewriting the blockquote, also update the **heading** — the gap is resolved, so it is no longer "a current, honest limit". Reframe it as a resolved note (e.g. "Scope-source note (resolved — `plan-files-scope`)") and drop the `LIMITS.md` parenthetical (LIMITS.md has no matching entry to point at). The fail-closed point stays in the body.

## Prose summary

The plan is **correct, minimal, and complete**. The interrogation confirmed two things live: (1) `pharn-build.md:105` is the **only** remaining file that still calls `plan-files-scope` a pending follow-up (grep over the repo, excluding `.dev/features/` audit trails) — so the plan's one-file scope **fully covers** the stale reference; (2) `LIMITS.md` carries **no** stale entry for this gap, so there is no human-only trusted-doc cleanup hiding behind the caveat's `LIMITS.md` citation. The no-test decision is **P7-sound**: a pure prose doc-sync has no behavioral surface, and the real behavior the caveat describes is already pinned green by `set-writes-scope.test.cjs` (the closing-the-loop + fail-closed tests, from `plan-files-scope`). The guarantee audit honestly labels the change advisory (a doc correction) with the fail-closed behavior unchanged (floor).

One **minor** concern: the rewrite should also fix the caveat's **heading** ("a current, honest limit — LIMITS.md"), not just the body — the gap being resolved means it is no longer a current limit. Surfaced for the build.

## Verdict

**ADVISORY VERDICT: 1 concern raised (0 blocking-severity, 1 minor) — for the human to weigh before `/pharn-dev-build`.** Not a gate, not "grill passed": `/pharn-dev-build` is free to proceed; the deterministic backstops remain its own floor-gates (spec-hash drift fix #4; unresolved `## Open questions (HALT)` — already resolved) and `.dev/floor/validate.mjs`. The one finding is a small completeness refinement (fix the heading too), not a blocker.
Loading