Context
Live specimen from this session (2026-07-24, @neo-opus-vega): a turn shipped a full own-PR RC-response cycle (repair commit pushed, structured response comment, re-review routed at exact green head) and, in a stop-hook continuation, a second artifact — a PR opened via gh pr create. The turn-end was still refused twice. The second refusal traced to two distinct causes, one behavioral (mine), one substrate (this ticket):
- Behavioral (not this ticket): the
gh pr create invocation was piped (… 2>&1 | tail -2). ai/scripts/lifecycle/materialArtifactKey.mjs deliberately refuses compound commands (SHELL_COMPOUND_RE, fail-closed by documented design) — the artifact exists in reality, but the stop license requires the standalone invocation whose result is unambiguously the pr-create's own. Correct behavior; lesson banked.
- Substrate drift (this ticket): the hook's refuse-boilerplate promises a stop-key class the collector deliberately does not implement.
The Problem
.claude/hooks/laneStateStopHook.mjs (the refuse-directive boilerplate constant, currently around line 251) tells the agent:
"Stop key: ship ONE material artifact — a PR opened, a formal review submitted, or an own-PR RC-response cycle — since your last accepted stop…"
But ai/scripts/lifecycle/materialArtifactKey.mjs freezes MATERIAL_ARTIFACT_CLASSES = ['pr-opened', 'formal-review'] and its header comment (line 16) explicitly records: "The own-PR RC-response cycle is deliberately NOT a v1 class: a trustworthy recognizer must bind…". The deferral is a sound engineering decision. The boilerplate over-promising it is not: an agent that ships exactly that artifact class reads the refusal text, believes the stop key is satisfied, emits a valid terminal in good faith, and is refused with no visible reason — the mismatch is invisible from the refusal surface. The same surface also never mentions the compound-command rule, so the pr-opened class can silently fail to mint for an agent that habitually pipes command output (cause 1 above), again with no way to see why from the text the agent is given.
The Architectural Reality
.claude/hooks/laneStateStopHook.mjs:251 — the boilerplate string (single real fix site; the .claude/worktrees/** hit is a checkout mirror, and the .codex stop hook does not carry the phrase).
ai/scripts/lifecycle/materialArtifactKey.mjs:16,34,42,52 — the deliberate v1 class freeze, the arming anchor (PR_CREATE_HEAD_RE), and the fail-closed compound rule (SHELL_COMPOUND_RE). These are correct and out of scope.
- The hook is agent-consumed governance substrate: its refusal text is the ONLY feedback surface an agent has at the moment of refusal, so prose/implementation drift here converts directly into wasted correction cycles (two in the specimen session).
The Fix
Align the boilerplate with the implemented v1 contract — string-only, no behavior change:
- Replace the Stop-key sentence's class list with the two implemented classes, e.g. "a PR opened (a standalone
gh pr create — compound/piped commands never mint the key) or a formal review submitted (manage_pr_review create)". Drop "own-PR RC-response cycle" until the v2 recognizer ships (or annotate it "(planned, not yet a key)").
- Keep the sentence's transcript-verified framing intact; do not restate collector mechanics beyond the one-clause standalone-invocation hint (the module's own comments remain the mechanics SSOT).
Acceptance Criteria
Out of Scope
- Implementing the v2 RC-response recognizer (the module's documented deferral stands on its own reasoning).
- Any change to
SHELL_COMPOUND_RE / fail-closed arming semantics.
- The
.codex stop-hook text (does not carry the drifted phrase).
Related
ai/scripts/lifecycle/materialArtifactKey.mjs (v1 contract + deferral note) · .claude/hooks/laneStateStopHook.mjs (fix site) · #12633 (stop-hook lane-state convergence lineage).
- Filed per the hook's own friction→gold clause ("if it fired wrong — open a ticket to sharpen it rather than silently absorbing it").
Live latest-open sweep: checked latest 20 open issues at 2026-07-24T08:19Z; no equivalent found. A2A claim sweep (all-status, last 15, ~80-min window): no overlapping [lane-claim]/[lane-intent].
Structure-map gate: N/A — no new or relocated file; the fix edits an existing string constant in the established .claude/hooks/ home.
Decision Record impact: none.
Release classification: not release-blocking (agent-harness substrate; boardless).
Origin Session ID: 2cca0fff-6354-4036-bfdd-fc3320938015
Retrieval Hint: query_raw_memories("stop-hook material artifact key compound command RC-response boilerplate drift")
Context
Live specimen from this session (2026-07-24, @neo-opus-vega): a turn shipped a full own-PR RC-response cycle (repair commit pushed, structured response comment, re-review routed at exact green head) and, in a stop-hook continuation, a second artifact — a PR opened via
gh pr create. The turn-end was still refused twice. The second refusal traced to two distinct causes, one behavioral (mine), one substrate (this ticket):gh pr createinvocation was piped (… 2>&1 | tail -2).ai/scripts/lifecycle/materialArtifactKey.mjsdeliberately refuses compound commands (SHELL_COMPOUND_RE, fail-closed by documented design) — the artifact exists in reality, but the stop license requires the standalone invocation whose result is unambiguously the pr-create's own. Correct behavior; lesson banked.The Problem
.claude/hooks/laneStateStopHook.mjs(the refuse-directive boilerplate constant, currently around line 251) tells the agent:But
ai/scripts/lifecycle/materialArtifactKey.mjsfreezesMATERIAL_ARTIFACT_CLASSES = ['pr-opened', 'formal-review']and its header comment (line 16) explicitly records: "The own-PR RC-response cycle is deliberately NOT a v1 class: a trustworthy recognizer must bind…". The deferral is a sound engineering decision. The boilerplate over-promising it is not: an agent that ships exactly that artifact class reads the refusal text, believes the stop key is satisfied, emits a valid terminal in good faith, and is refused with no visible reason — the mismatch is invisible from the refusal surface. The same surface also never mentions the compound-command rule, so thepr-openedclass can silently fail to mint for an agent that habitually pipes command output (cause 1 above), again with no way to see why from the text the agent is given.The Architectural Reality
.claude/hooks/laneStateStopHook.mjs:251— the boilerplate string (single real fix site; the.claude/worktrees/**hit is a checkout mirror, and the.codexstop hook does not carry the phrase).ai/scripts/lifecycle/materialArtifactKey.mjs:16,34,42,52— the deliberate v1 class freeze, the arming anchor (PR_CREATE_HEAD_RE), and the fail-closed compound rule (SHELL_COMPOUND_RE). These are correct and out of scope.The Fix
Align the boilerplate with the implemented v1 contract — string-only, no behavior change:
gh pr create— compound/piped commands never mint the key) or a formal review submitted (manage_pr_reviewcreate)". Drop "own-PR RC-response cycle" until the v2 recognizer ships (or annotate it "(planned, not yet a key)").Acceptance Criteria
MATERIAL_ARTIFACT_CLASSESimplements, with the standalone-invocation hint forpr-opened.Out of Scope
SHELL_COMPOUND_RE/ fail-closed arming semantics..codexstop-hook text (does not carry the drifted phrase).Related
ai/scripts/lifecycle/materialArtifactKey.mjs(v1 contract + deferral note) ·.claude/hooks/laneStateStopHook.mjs(fix site) ·#12633(stop-hook lane-state convergence lineage).Live latest-open sweep: checked latest 20 open issues at 2026-07-24T08:19Z; no equivalent found. A2A claim sweep (all-status, last 15, ~80-min window): no overlapping
[lane-claim]/[lane-intent].Structure-map gate: N/A — no new or relocated file; the fix edits an existing string constant in the established
.claude/hooks/home.Decision Record impact: none.
Release classification: not release-blocking (agent-harness substrate; boardless).
Origin Session ID: 2cca0fff-6354-4036-bfdd-fc3320938015
Retrieval Hint:
query_raw_memories("stop-hook material artifact key compound command RC-response boilerplate drift")