From 9d50bc43c75ec493eabe76101fd4df5eae534d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Galarowicz?= Date: Mon, 28 Sep 2026 19:50:11 +0200 Subject: [PATCH] feat(floor): /pharn-verify reuses equivalent REGRESS/HEAD gate executions within one delivery run (6.34.0) Inside one /pharn-loop or /pharn-ship run, /pharn-verify records a gate's result from the completed execution /pharn-regress's HEAD side already made of it, without spawning the gate again, when tested code proves every bound input is equal (the execution identity: command, ordered files, cwd realpath, --timeout-ms, git HEAD, the PHARN-added env var name, the tree fingerprint). Style gates, AC level gates and reconcile are never reused. The source is offered only through a git-dir record regress publishes. Reused entries are ran:false, reason:"reused", with provenance; every miss runs the gate as before. Co-Authored-By: Claude Opus 5.5 --- .claude/commands/pharn-verify.md | 4 + .dev/features/verify-head-gate-reuse/GRILL.md | 87 ++++ .../verify-head-gate-reuse/MEASUREMENT.md | 61 +++ .dev/features/verify-head-gate-reuse/PLAN.md | 298 +++++++++++ .../verify-head-gate-reuse/REGRESSION.md | 29 ++ .../features/verify-head-gate-reuse/REVIEW.md | 98 ++++ .dev/features/verify-head-gate-reuse/SHIP.md | 38 ++ .../features/verify-head-gate-reuse/VERIFY.md | 25 + .../verify-head-gate-reuse/measure.mjs | 227 ++++++++ .../regression-report.json | 51 ++ .../verify-head-gate-reuse/verify-report.json | 22 + .dev/floor/command-hygiene.test.mjs | 8 +- CHANGELOG.md | 61 +++ CLAUDE.md | 33 +- README.md | 4 +- SKILLS_VERSION | 2 +- pharn/floor/gate-reuse-core.mjs | 217 ++++++++ pharn/floor/gate-reuse-core.test.mjs | 424 +++++++++++++++ pharn/floor/gate-run-core.mjs | 68 ++- pharn/floor/gate-run-core.test.mjs | 124 +++++ pharn/floor/head-reuse-offer.mjs | 158 ++++++ pharn/floor/head-reuse-offer.test.mjs | 229 ++++++++ pharn/floor/regress-base-reuse.mjs | 14 +- pharn/floor/render-verify.mjs | 19 + pharn/floor/render-verify.test.mjs | 38 ++ pharn/floor/run-gates.mjs | 256 +++++++-- pharn/floor/run-gates.test.mjs | 493 ++++++++++++++++++ pharn/floor/stage-regress.mjs | 16 + pharn/floor/stage-regress.test.mjs | 41 ++ pharn/floor/stage-verify-core.mjs | 53 +- pharn/floor/stage-verify-core.test.mjs | 85 ++- pharn/floor/stage-verify.mjs | 49 +- pharn/floor/stage-verify.test.mjs | 265 +++++++++- pharn/pharn-contracts/gate-run-record.md | 62 ++- pharn/pharn-contracts/verify-report.md | 30 ++ 35 files changed, 3596 insertions(+), 93 deletions(-) create mode 100644 .dev/features/verify-head-gate-reuse/GRILL.md create mode 100644 .dev/features/verify-head-gate-reuse/MEASUREMENT.md create mode 100644 .dev/features/verify-head-gate-reuse/PLAN.md create mode 100644 .dev/features/verify-head-gate-reuse/REGRESSION.md create mode 100644 .dev/features/verify-head-gate-reuse/REVIEW.md create mode 100644 .dev/features/verify-head-gate-reuse/SHIP.md create mode 100644 .dev/features/verify-head-gate-reuse/VERIFY.md create mode 100644 .dev/features/verify-head-gate-reuse/measure.mjs create mode 100644 .dev/features/verify-head-gate-reuse/regression-report.json create mode 100644 .dev/features/verify-head-gate-reuse/verify-report.json create mode 100644 pharn/floor/gate-reuse-core.mjs create mode 100644 pharn/floor/gate-reuse-core.test.mjs create mode 100644 pharn/floor/head-reuse-offer.mjs create mode 100644 pharn/floor/head-reuse-offer.test.mjs diff --git a/.claude/commands/pharn-verify.md b/.claude/commands/pharn-verify.md index 00c31a17..fe284421 100644 --- a/.claude/commands/pharn-verify.md +++ b/.claude/commands/pharn-verify.md @@ -66,6 +66,10 @@ test-infra` SPEC gets only the weaker BOOTSTRAP evidence (each level's gate ran - **The absolute-threshold residual:** the verdict asks "are ALL gates green NOW?", not "did anything flip?". A pre-existing red gate the feature did not cause also fails verify, by design — `/pharn-regress` is the stage that excludes pre-existing failures. The feature-specific signal is the `structural:*` gates over the feature's evals. +- **The reuse residual (6.34.0):** inside a `/pharn-loop` or `/pharn-ship` run, a gate's result may be REUSED from that + run's `/pharn-regress` HEAD execution over the same tree and execution identity, not re-run here; the report's + `gate_reuse` block names each. That the result equals a fresh run is advisory, and this verify then gives no second + sample of a flaky gate (`pharn/floor/gate-reuse-core.mjs`). ## Step 0 — Resolve ``, then set the writes-scope (fix #7, fail-closed) diff --git a/.dev/features/verify-head-gate-reuse/GRILL.md b/.dev/features/verify-head-gate-reuse/GRILL.md new file mode 100644 index 00000000..3ffb0ab4 --- /dev/null +++ b/.dev/features/verify-head-gate-reuse/GRILL.md @@ -0,0 +1,87 @@ +# GRILL — verify-head-gate-reuse + +Plan: `.dev/features/verify-head-gate-reuse/PLAN.md`. Spec-hash check: `node .dev/floor/hash-doc.mjs +pharn/ARCHITECTURE.md` → `d831d30d399a37dc403080072763d13383de6f6f31875e7e8cb4eadeb642f4f4`, equal to the plan's +`spec_content_hash` (no drift). **Step 1b (FLOOR):** `node pharn/floor/check-plan-lessons.mjs +.dev/features/verify-head-gate-reuse/PLAN.md .dev/memory-bank/lessons-learned.md` → exit **0** (`GREEN — applied_lessons: +L24, L29, L34, L35, L41, L43, L45, L54, L59, L60, L62, L65 … all 12 cited id(s) resolve … and are referenced in the plan +body`). + +**How this grill was run (recorded, advisory).** An independent, read-only Opus agent with a fresh context interrogated +the plan against the live code (it wrote nothing and ran no setter). It noted that the build had already started in the +worktree and spot-checked the in-progress diff where a finding bore on it. The orchestrator wrote this file from its +report. The agent's free text below inherits the plan's untrusted tag and is quoted as DATA (P2). The 13 registered +grillers (`node pharn/floor/count-grillers.mjs .`) were applied by axis; a11y, i18n, privacy and migrations do not apply +(no UI, no user-facing strings beyond fixed render lines, no personal data, no data migration). + +## Findings + +```yaml +- type: FINDING + rule_id: "P0" + severity: blocking + file: ".dev/features/verify-head-gate-reuse/PLAN.md:35" + problem: "Style gates are planned reusable, but a whole-tree style gate reads files the fingerprint excludes, and those files differ between the regress HEAD run and verify: REGRESSION.md/regression-report.json are written after the head drain, and a previous iteration's VERIFY.md/verify-report.json exist at regress time and are removed by verify's fresh start." + evidence: "worktree-fingerprint.mjs EXCLUDED_ARTIFACTS — 'POST-BUILD artifacts: written at or after regress, so a change cannot alter what a gate judged'; stage-regress.mjs renders REGRESSION.md after drain-head." +- type: FINDING + rule_id: "P0" + severity: blocking + file: ".dev/features/verify-head-gate-reuse/PLAN.md:238" + problem: "Under /pharn-ship --quick and /pharn-loop --quick no regress runs but the run marker is open, so the build agent can Write .pharn/pharn-regress/head/stamp.json and its logs (ALWAYS .pharn/**) after the marker; marker + mtime acceptance then offers a planted source, and verify would record gate exits nobody ran." + evidence: "enforce-writes-scope.cjs: const ALWAYS = ['.pharn/**']; the plan's L65 line claims regress clears head/ first, which quick mode never runs." +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/verify-head-gate-reuse/PLAN.md:104" + problem: "validateStamp would admit ANY id as reused, including reconcile and the AC level gates." + evidence: "the in-progress reusedRunDefect checks stage/shape but no id." +- type: FINDING + rule_id: "P0" + severity: important + file: ".dev/features/verify-head-gate-reuse/PLAN.md:200" + problem: "'Recorded only when its identity equals the live one' is floor at decision time only; nothing reading the verify stamp later re-derives it, and existing text says the map's values are exit codes the runner recorded from the listed argv." + evidence: "gate-run-core.mjs header lines 27-31; CLAUDE.md run-gates paragraph; gate-run-record.md 'What a validating stamp PROVES'." +- type: FINDING + rule_id: "P0" + severity: important + file: ".claude/commands/pharn-verify.md:56" + problem: "The command's claims, and LIMITS.md ('/pharn-verify re-runs the project's own gates'), overstate a reused gate; the new residual (no independent second sample of a flaky gate) is unstated; the command's byte ceiling leaves 916 bytes." + evidence: "pharn-verify.md 17,516 bytes vs COMMAND_BYTE_CEILINGS 18,432; LIMITS.md is human-only." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/verify-head-gate-reuse/PLAN.md:61" + problem: "Git HEAD (and the index) are outside the identity; a gate that reads git state is unbound." + evidence: "worktree-fingerprint.mjs: HEAD is deliberately NOT part of the digest." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/verify-head-gate-reuse/PLAN.md:215" + problem: "The budget check runs before run --next knows an entry is a HIT, so reuse saves processes and wall-clock, not Bash calls or model turns; the measurement must use the pinned --budget-ms." + evidence: "stage-runtime.mjs drainGates: budget.may() precedes the spawn; 540000/570000 allow a second slow step only within 30 s." +- type: FINDING + rule_id: "P0" + severity: minor + file: ".dev/features/verify-head-gate-reuse/PLAN.md:98" + problem: "results_sha256 === null means 'no REGULAR results file', not 'wrote nothing' (a directory, link or FIFO there also hashes null); e2e reads build output, so a reused build leaves e2e reading the head run's git-ignored output; the usage strings and CLAUDE.md lines need the new flag." + evidence: "run-gates.mjs sha256RegularFile returns null for a non-regular file; gate-run-core.mjs E2E_SET comment." +``` + +## Checked, no issue (the agent's own list) + +cwd is the invoking directory on both sides, so its realpath is stable; leaving the value of `PHARN_TEST_RESULTS`, `seq` +and `` out of the identity is correct; `timeout_ms` in the identity does not cause misses (both pinned 540000); a +crash between the log copy and the state write re-decides the entry (spawnGate truncates the logs, `clearResultsPath` +runs first); a freshness RERUN of verify misses by fingerprint and a `/pharn-ship` Step 2b retry re-runs regress, which +clears `head/`; the BASE-reuse increment is unaffected (`baseSpecFrom`/`specPart` project only `{id, shell, argv, +files}`); `check-verify`, `check-loop-fresh` C/E/F/G/J and `ac-gate-core`'s `ranAsPinned` do not branch on `ran` for a +non-level gate, and J re-hashes the copied logs under verify's own names; the stage-verify fixture closure is +regex-derived, so new literal `./x.mjs` imports are covered. + +## Verdict (ADVISORY) + +**NOT READY as planned — 2 blocking, 3 important, 3 minor.** The orchestrator accepts every finding and amends the plan +before the build continues (`PLAN.md`, "Grill amendments"): style gates join the never-reused set; the source is bound +by a record in the git dir that `/pharn-regress` publishes for the current run and verify requires; validateStamp +refuses a reused non-reusable id; git HEAD joins the identity; the wording and the residuals are corrected. The Step 1b +floor verdict above is GREEN and is not folded into these counts. diff --git a/.dev/features/verify-head-gate-reuse/MEASUREMENT.md b/.dev/features/verify-head-gate-reuse/MEASUREMENT.md new file mode 100644 index 00000000..d433d19b --- /dev/null +++ b/.dev/features/verify-head-gate-reuse/MEASUREMENT.md @@ -0,0 +1,61 @@ +# MEASUREMENT — verify-head-gate-reuse (2026-09-28) + +Measured on the maintainer's machine (darwin, Node 24), with `.dev/features/verify-head-gate-reuse/measure.mjs`, 3 +repetitions per variant, the machine otherwise idle. Each repetition builds a FRESH fixture repo carrying the floor +under test as its own `pharn/floor/` (an install's layout) and the write guards, opens a `/pharn-loop` run marker, +anchors the reconcile epoch as the build does, makes the build's edit, then runs `stage-regress.mjs` and +`stage-verify.mjs` with the pinned flags (`--timeout-ms 540000 --budget-ms 570000`; regress also `--base +--no-install`). + +**The fixture.** Four gates: `test` (node:test, one test, 0.3 s), `lint` (0.8 s), `typecheck` (1 s), `build` (1 s). +Every gate appends one line naming itself and the stage it ran under (read from `PHARN_TEST_RESULTS`, whose value is +that gate's own path under the stage's ``) to a counter file outside the repo. The build edit touches +`eslint.config.mjs`, so regress runs its style gate too — the widest overlap a discovered set has. The COUNTS are the +result; the wall-clock shows what the counts save on this fixture's shape, not what any real project saves. + +- **before** — `main`'s floor at `2cf0e85` (`git archive 2cf0e85 pharn/floor`); +- **after** — this branch's floor. + +## Counts (identical in all 3 repetitions) + +| variant | regress/HEAD processes | regress/BASE processes | verify processes | verify entries reused | total gate processes | +| ------- | ---------------------: | ---------------------: | -------------------------------- | -----------------------: | -------------------: | +| before | 4 | 4 | 4 (test, lint, typecheck, build) | 0 | 12 | +| after | 4 | 4 | **2** (test, lint) | **2** (typecheck, build) | **10** | + +`reconcile` (PHARN's own gate, always last at verify and never reused) is not counted above; it runs in both variants. +`test` runs at both stages because it is never the same execution (regress: the outside-scope subset; verify: the +whole suite) and is an AC level gate; `lint` runs at both because style gates are never reused (grill B1). In the +common case where the build touches no style config, regress skips `lint` by design, so regress spawns 3 + 3 and +verify still spawns the same 2 — the reused pair is the same. + +Both variants' verdicts are `PASS` in every repetition, and the after-variant's `verify-report.json` reads +`gate_reuse.reused = [typecheck, build]`. + +## Wall-clock (ms, per repetition) + +| variant | regress (whole stage) | verify (whole stage) | +| ------- | --------------------- | -------------------- | +| before | 9822 / 9827 / 9763 | 4815 / 4833 / 4885 | +| after | 9738 / 9755 / 9843 | 2506 / 2465 / 2508 | + +- **Verify drops by ~2.35 s (−49%) on this fixture** — the two reused gates' own time (2 × 1 s) plus two `npm run` + start-ups. Everything else verify does is unchanged: the chain check, the pairs, the verifier count, the + completeness capture, `test`, `lint`, `reconcile`, two fingerprints per SPAWNED gate, the verdict and the render. +- **Regress is unchanged within noise** (−89 ms to +80 ms): it publishes one small offer record once its HEAD stamp is + final, and records an identity per run. +- **The offer decision's own cost, timed in-process over the finished fixture (20 calls), is 16.9–17.8 ms**: one `git +rev-parse --absolute-git-dir` spawn, reading the markers, the offer and the head stamp and hashing them, plus one + `findReusable`. It runs once per verify invocation. The per-entry cost inside `run --next` (re-reading and hashing + the source stamp, the decision, reading, hashing and writing two logs) is inside the verify wall time above and + replaces a gate spawn plus its post-gate fingerprint. + +## Not measured, and not claimed + +- **Tokens.** The stages are deterministic code; the orchestrator's calls per verify are unchanged, and a reused entry + still spends one budgeted `run --next` call (so reuse saves processes and wall-clock inside an invocation, never a + Bash call or a model turn). +- **A real project's saving.** It scales with the duration of the project's `typecheck` / `type-check` / `build` (and + any structural pair both stages run). The fixture's 1 s gates stand in for minutes in a real project. +- **How often a real run offers.** Only inside a `/pharn-loop` or `/pharn-ship` run whose regress published an offer + and whose tree did not move between regress and verify; every other verify runs every gate, as before. diff --git a/.dev/features/verify-head-gate-reuse/PLAN.md b/.dev/features/verify-head-gate-reuse/PLAN.md new file mode 100644 index 00000000..71bff9c6 --- /dev/null +++ b/.dev/features/verify-head-gate-reuse/PLAN.md @@ -0,0 +1,298 @@ +# PLAN — verify-head-gate-reuse: VERIFY reuses equivalent REGRESS/HEAD gate executions + +- spec_content_hash: d831d30d399a37dc403080072763d13383de6f6f31875e7e8cb4eadeb642f4f4 +- applied_lessons: [L24, L29, L34, L35, L41, L43, L45, L54, L59, L60, L62, L65] +- increment: when this delivery run's `/pharn-regress` HEAD side already completed a gate execution whose execution identity (command, files, cwd, timeout, PHARN-added env, tree fingerprint) equals the one `/pharn-verify` now needs, the verify runner records that completed result — with its provenance — instead of spawning the process again; every other gate runs exactly as today. +- layer(s): pharn-floor (product), pharn-contracts (two contracts, additive), product `.claude/commands/pharn-verify.md` (one claims bullet) +- constitution_refs: [P0, P2, P3, P4, P5, P6, P7] + +## Why (P7) + +**The trigger is the maintainer's explicit direction** — this run's prompt ("PR 3: Reuse Equivalent HEAD Gate +Executions"), recorded as such (P5), not a failure this plan re-derives. The benefit claimed is the one measured +(`MEASUREMENT.md`): gate processes and wall-clock per verify. No token saving is claimed. + +## Confirmed current behavior (read this run, `main` at `2cf0e85`, 6.33.0) + +- `/pharn-regress` (`stage-regress.mjs`) runs its HEAD side with `run-gates.mjs init --stage regress --side head` + **at the invoking directory** (no `--cwd`), then `run --next` per entry; the finalized stamp stays at + `.pharn/pharn-regress/head/stamp.json` (REGRESS_PATHS.head) with every gate's `.out`/`.err` beside it, until the + next regress fresh start clears it (`clearScratchKeepingBaseEvidence` keeps only `base-gates/`). +- `/pharn-verify` (`stage-verify.mjs`) then runs `run-gates.mjs init --stage verify` at the same directory; its fresh + start clears `.pharn/pharn-verify/`; every entry is spawned by `run --next` (`spawnGate`). +- `/pharn-loop` and `/pharn-ship` run regress then verify inline, both with `--timeout-ms 540000`, neither with + `--gates`. `check-loop-fresh.mjs` G already requires `regress head fingerprint.final == verify fingerprint.init`. +- A run entry's execution inputs, read from `spawnGate`: `shell` (→ `/bin/sh -c ' "$@"' sh `) or + `argv` (+ `--` + files), the ordered `files`, `cwd`, `stdin` ignored, a new process group, the inherited + environment plus ONE PHARN-added variable (`PHARN_TEST_RESULTS` = that gate's own results path under ``), and + the `--timeout-ms` kill timer. The tree each gate judged is `fp_before` (worktree-fingerprint `ALGO`). + +### Discovery matrix — gates that can occur in both REGRESS/HEAD and VERIFY + +| gate | regress/head execution | verify execution | equivalent? | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `test` | `npm run test -- ` (files = `scope.outside_tests`); **not executed** (`ran:false no-files`) when none | `npm run test`, files `[]` (whole suite); the AC gate reads its per-test record from verify's own `` | **no** — files differ, and the AC gate needs verify's own results file → never reusable | +| `lint`, `format:check`, `lint:md` | `npm run ` — but **skipped by default** (`--skip-style` unless a style config is in `inside`) | `npm run ` | **yes** when regress ran it (a style-config-touching change); else absent → run | +| `typecheck`, `type-check` | `npm run ` | `npm run ` | **yes** (same tree, same identity) | +| `build` | `npm run build` | `npm run build` | **yes** unless it mutated the tree (`mutated:true` → never reusable) | +| `test:e2e`, `e2e` | never discovered at regress (E2E rule) | `npm run `, per-test record read by the AC gate | **no** — absent from regress; also never reusable (AC level gate) | +| `structural:` | only OUTSIDE-feature pairs (`outside_eval_pairs`) | only pairs in PLAN-declared capability dirs (EVAL_PAIR_RULE) | **yes** only if the same `` lands in both (argv identical); normally disjoint | +| explicit `--gates` token | `/bin/sh -c`; `test` gets the outside files appended | `/bin/sh -c`, no files | **yes** iff shell text and files are identical | +| `reconcile` | absent | injected last, judges the whole verify window | **no** — verify-only, must run live | +| `completeness` (aux, not a gate) | absent | captured at verify init | not a gate; never reused | + +Same id is never the equivalence test: `test` has the same id on both sides and different files. + +## The design + +### 1. Execution identity — `pharn/floor/gate-reuse-core.mjs` (NEW, pure) + +`executionIdentity({shell, argv, files, cwdAbs, timeoutMs, fingerprintAlgo, fpBefore})` = sha256 of the canonical +JSON of `{schema: "gate-execution-identity/1", shell, argv, files, cwd, stdin: "ignore", env_added: [RESULTS_ENV], +timeout_ms, fingerprint: {algo, before}}` (fixed key order). Every component is an input `spawnGate` actually uses: + +- **in**: shell / argv / ordered files (what runs), `cwd` as the realpath the gate ran in (where), `timeout_ms` + (the kill timer), the fingerprint algo + `fp_before` digest (the tree judged), the NAME of the one PHARN-added + variable, the identity schema. +- **out, each on purpose**: the gate `id` (a label — it is used only as the lookup key, see §3), `seq`, the stage / + side, ``, and the VALUE of `PHARN_TEST_RESULTS` (a per-stage path by construction; §3 requires the source to + have written nothing there). **Not bound, named**: the inherited environment and the machine (the BASE-reuse + bound, `regress-base-reuse-core.mjs`), and git-ignored content the fingerprint does not see (`node_modules/`). + +`run-gates.mjs run --next` records the identity on **every** run it executes as an optional additive +`runs[].identity_sha256` (hex64), so the HEAD side carries it without any change to `stage-regress.mjs`. + +### 2. The source — only this delivery run's REGRESS/HEAD stamp (`stage-verify.mjs`, phase `init`) + +`stage-verify.mjs` passes `--reuse-stamp .pharn/pharn-regress/head/stamp.json` to `run-gates.mjs init` only when +`gate-reuse-core.mjs acceptReuseSource` accepts, over inputs read this invocation: + +- exactly one open `/pharn-loop` or `/pharn-ship` run marker for the feature — the BASE-reuse rule reused, not + copied (`regress-base-reuse.mjs readMarkers` + `regress-base-reuse-core.mjs deliveryRunIdentity`, L35); +- the head stamp is a regular file (containment-walked, `O_NOFOLLOW`: `readInProject`) whose mtime is **not before** + the marker's (it was produced inside this run — a stamp an earlier run left is refused). + +Otherwise no `--reuse-stamp` is passed: every gate runs, exactly as today. Never an error, never a question. + +### 3. The per-entry decision — `run-gates.mjs run --next` (verify only) + +`init --reuse-stamp ` (refused for any stage but `verify`) reads the file once (regular, `O_NOFOLLOW`), and +records `reuse: {stamp, stamp_sha256}` in the in-progress record — or `reuse: null` when it is unreadable (a MISS, +never a refusal). `run --next`, for the next entry, after its usual `fp_before`: + +1. compute the live identity (§1); +2. if `reuse` is set, `findReusable` (pure) over the source re-read NOW: bytes' sha256 == `reuse.stamp_sha256`; the + stamp validates (`validateStamp`, stage `regress`, side `head`, this feature); a source run with the same `id` + exists and is ELIGIBLE — + - `ran === true`, `reason === null` (never a no-files entry, never a reused one — no chains); + - `timed_out === false`, `mutated === false` (`fp_before === fp_after`); + - `exit` an integer in `0..125` — a COMPLETED process exit, green **or red** (126/127 are spawn failures, + ≥128 a signal, 124-with-timeout is excluded by `timed_out`); + - `results_sha256 === null` (the field present and null: it wrote no per-test file); + - the id is not in `NON_REUSABLE_IDS` = ∪ `LEVEL_GATES` (`test`, `test:e2e`, `e2e` — the AC gate reads their + per-test records from verify's own `` and requires the pinned run) ∪ `{reconcile}`; + - `shell`, `argv`, `files` equal the verify entry's, and `identity_sha256` equals the live identity (so the + recorded fp_before equals the live tree, and algo, cwd and timeout agree); +3. the source logs are read (`O_NOFOLLOW`, regular, capped) and must hash to the recorded `stdout_sha256` / + `stderr_sha256`; they are copied into verify's `` under verify's own `logBasename(seq, id)`; +4. HIT → the run is recorded with `ran: false`, `reason: "reused"`, `reused: {stage, side, seq, stamp_sha256}`, the + source's `exit` / `stdout_sha256` / `stderr_sha256`, `timed_out: false`, `mutated: false`, + `fp_before = fp_after = live`, `results_sha256: null`, `identity_sha256` — **no process is spawned**; + any failure at any step → MISS → the gate is spawned exactly as today. + +`ran: false` is the truthful value: VERIFY did not run it. `validateStamp` admits `ran: false` only with +`reason: "no-files"` (unchanged) or `reason: "reused"` + a well-formed `reused` block + `identity_sha256`, and the +runner's finalize refusal (`entry-not-run`) admits the same. `SCHEMA` stays `gate-run-record/1` — additive, like +`results_sha256` (6.15.0); a pre-6.34.0 floor reads a reused entry as `entry-not-run`, a LAPSE (re-run), fail-closed. + +### 4. What does NOT change + +The required gate set (`resolveSet`), `aux.completeness`, the AC gate, `check-verify.mjs`, `check-regress.mjs`, +`stage-regress.mjs`, `check-loop-fresh.mjs` (J re-hashes the copied logs, E re-derives from the stamp, F/G compare +fingerprints — all unchanged and all pass over a reused entry), the lock, the drain/budget/resume (one `run --next` +per entry, the same atomic state write, the same lock — a crash before the state write re-decides the entry), the +BASE-reuse increment. VERIFY coverage is always `resolveSet`'s: REGRESS only offers results. + +### 5. The report and the render + +`stage-verify.mjs` reads its own finalized stamp and adds an ADDITIVE, ADVISORY block to `verify-report.json`: +`gate_reuse: {reused: [{id, stage, side, seq}]}` (empty list when nothing was reused) — `MERGED_KEYS` gains it. +`VERIFY.md` says which gate results were reused and that they were not re-executed here. No verdict reads it. + +## Files + +- `pharn/floor/gate-reuse-core.mjs` — NEW: execution identity, eligibility, source acceptance, closed miss set — pure +- `pharn/floor/gate-reuse-core.test.mjs` — NEW: identity negative controls per component, eligibility per rule +- `pharn/floor/gate-run-core.mjs` — validateStamp: optional `identity_sha256`; `reason: "reused"` + `reused` block +- `pharn/floor/gate-run-core.test.mjs` — the new validateStamp rules, with controls +- `pharn/floor/run-gates.mjs` — `init --reuse-stamp`; identity on every run; the reuse path in `run --next` +- `pharn/floor/run-gates.test.mjs` — reuse at the runner: spawn-count proof, MISS paths, crash/resume +- `pharn/floor/stage-verify.mjs` — source acceptance at init; the `gate_reuse` block +- `pharn/floor/stage-verify-core.mjs` — `gateReuseBlock`, `composeReport` gains it, `MERGED_KEYS` +- `pharn/floor/stage-verify-core.test.mjs` — the block and the merged key +- `pharn/floor/render-verify.mjs` — render the reused gates +- `pharn/floor/render-verify.test.mjs` — the render line +- `pharn/floor/stage-verify.test.mjs` — scenarios A–H, fresh-vs-reuse equivalence, freshness after reuse +- `pharn/pharn-contracts/gate-run-record.md` — `identity_sha256`, reused runs (additive) +- `pharn/pharn-contracts/verify-report.md` — the additive `gate_reuse` block +- `.claude/commands/pharn-verify.md` — one claims bullet: a gate result may be reused, named in the report +- `.dev/features/verify-head-gate-reuse/measure.mjs` — NEW: before/after process counts and wall-clock +- `.dev/features/verify-head-gate-reuse/MEASUREMENT.md` — NEW: the measured numbers +- `.dev/features/verify-head-gate-reuse/PLAN.md` — this plan +- `CHANGELOG.md` — `[6.34.0]` +- `SKILLS_VERSION` — 6.34.0 (minor: a new shipped capability) +- `README.md` — the version badge +- `CLAUDE.md` — the floor command list gains the reuse paragraph +- `docs/capabilities/**` — regenerated by `npm run docs:generate` if the catalog moves +- `README.md` CURRENT-STATE region — regenerated likewise +- `pharn/floor/head-reuse-offer.mjs` — NEW (grill B2): publish / discard / read the git-dir offer record +- `pharn/floor/head-reuse-offer.test.mjs` — NEW: publication rules, the ★ HOOK test on the record path +- `pharn/floor/regress-base-reuse.mjs` — `gitDirFile(name)` exported, `recordPath` built on it (one owner) +- `pharn/floor/stage-regress.mjs` — discard the offer at the fresh start, publish it once the HEAD side is finalized +- `pharn/floor/stage-regress.test.mjs` — the offer's lifecycle through the real script +- `.dev/floor/command-hygiene.test.mjs` — apparatus: its ✧ pin on `composeReport`'s spread gains the `gate_reuse` block + +## Contracts satisfied + +- `pharn/pharn-contracts/gate-run-record.md` — extended additively; a validating stamp still proves what it proved, + and a reused entry's exit is the recorded exit of the named source execution. +- `pharn/pharn-contracts/verify-report.md` — one additive advisory block, read by no verdict. +- `pharn/pharn-contracts/stage-exit.md` — unchanged (no new exit, question or reason code). + +## Evals to write (P1) + +No capability (`role:`) is added; the floor modules ship with tests (below), the repo's convention for floor code. + +## Tests and negative controls (the build must deliver each) + +Scenarios, each through the REAL `stage-verify.mjs` (and `stage-regress.mjs` where named), with a spawn counter in +the fixture's gate scripts (a line appended to a counter file OUTSIDE the tree), so "not spawned" is measured, never +read off `reused: true`: + +- **A** equivalent gate: regress/head runs `typecheck`; verify reuses it; the counter shows ONE `typecheck` run; the + verify stamp validates, the entry is `ran:false reason:reused` with provenance, `check-verify --stamp` agrees. +- **B** same id, different command: `--gates "…::typecheck"` differing text → MISS, counter shows two runs. +- **C** different files: `test` subset at regress vs full at verify → run (also NON_REUSABLE). +- **D** regress skipped a verify gate (default style skip) → `lint` runs at verify. +- **E** verify-only gate (`test:e2e`) → runs. +- **F** changed tree after regress → MISS for every gate. +- **G** partial overlap: `typecheck` + `build` HIT, `lint` + `test` + `reconcile` run; the stamp is complete. +- **H** unusable source (absent, no marker, stale marker, earlier-run stamp, corrupted stamp, a log edited, a + symlinked stamp) → every gate runs, `done`, same verdict, no new failure. +- **Fresh-vs-reuse equivalence (mandatory)**: the same fixture and tree through verify without and with the source: + equal `resolveSet` coverage (stamp `required` + run ids), equal exit map, verdict, `completeness`, `ac_gate`, and a + `check-loop-fresh.mjs` run over each that reads FRESH. +- **Negative controls per identity component** (L29/L60): shell, argv, each file, file order, cwd, timeout, fp algo, + fp digest → each alone flips HIT to MISS; **irrelevant metadata** (seq position, ``, stage label, the + `PHARN_TEST_RESULTS` value) → still HIT. +- **Eligibility controls**: timed_out, mutated, exit 126/127/≥128, results_sha256 non-null or absent, ran:false, + reason:reused, NON_REUSABLE id, no identity recorded → MISS; exit 1 (a completed red) → HIT and verify FAIL. +- **Resume**: a crash after the logs are copied but before the state write re-decides the entry once (no duplicate + run entry); a stamp is never finalized with an unrecorded entry. +- **validateStamp**: a reused entry without its block, with a malformed block, with `ran:true`, or without + `identity_sha256` → refused; the old no-files rule unchanged. +- **BASE-reuse interaction**: the stage-regress BASE-reuse suite still passes unchanged (identity_sha256 on runs does + not enter `baseSpecFrom` / `evidenceRequirement`). + +## Measurement + +`measure.mjs