feat(rfc-008): P3b-1 — relocate stop decision em-recall→enforce-contract.mjs#391
Conversation
…tract.mjs
R1 strong form: relocate em-recall.mjs's `--gate stop` handler verbatim into a new
enforcement-layer `scripts/enforce-contract.mjs` (byte-identical), and repoint
`stop-gate.sh` to it. The stop decision is purely marker-state (RFC-008:464), so this
slice reads NO contract/registry/config/events files — the contract-driven tier layer
(min(), _index.json cap lookup, config clamp, CLASS-C(a)) is inert for claude-code and
defers to P3b-2 with its install-runtime contract deploy + P4 config schema. em-recall
`--gate stop` stays until P3d.
- scripts/enforce-contract.mjs: pure `decideStop({repoRoot, sid})` + CLI; the 3 em-recall
exits (:182/:211/:216) translate to returns; console.log/process.exit only in the CLI.
- stop-gate.sh: repoint em-recall→enforce-contract; loud-fail port preserved (CLASS-C c).
- Fail-OPEN fix: isMain `import.meta.url === pathToFileURL(argv[1])` no-ops under a
symlinked install path (/var→/private/var) → stop gate allow-always. Fixed via
realpath-both; pinned by test C4 + the /var/folders hook fixture.
- tests: test-enforce-contract.mjs (44/0 — unit + 7-scenario parity vs em-recall on
stdout+exit+stderr+no-side-effect + CLI + symlink regression); test-stop-gate.sh 31/0
(fixture stages enforce-contract; L3.4 retargeted). validate-plan-marker-sites 2/2.
Plan review: codex ACCEPT (R2) + negative-scenario-planner (final). Code review:
negative-scenario-reviewer ACCEPT-with-FU. FU: structured-alert-probe.mjs same isMain
idiom (test-only); P3d retarget test-stop-gate Layer 1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lantiscooperdev
left a comment
There was a problem hiding this comment.
Review — RFC-008 P3b-1 (bot, substantive)
Reviewed the diff against em-recall.mjs:141-217 (the relocation source) and ran the suites locally.
Correctness
- Byte-identical relocation confirmed.
decideStop()matches em-recall's--gate stophandler verbatim — plan-pending strict 6-clause comparison,stopGateCarveOutApplies(repoRoot, sid), thepostDoneSize === 0check, the block-reason string, and the 3 exit→return translations (:182→null,:211→{decision,reason},:216→null). The 7-scenario parity suite asserts stdout + exit-code + stderr (modulo prefix) + no-marker-side-effect, all green. - Pure-function discipline:
console.log/process.exitappear only in the CLIif(isMain)wrapper;decideStopis side-effect-free. Correct.
Fail-open fix (the load-bearing catch)
The isMain realpath fix is correct and complete. Verified independently: under a symlinked install path the pre-fix idiom no-op'd → stop gate allow-always (fail-OPEN); the realpath-both check restores the block. The catch→false is the right direction — a script's argv[1] always exists when executing, so it can't mask a legitimate main invocation. No other fail-open path: a throw in decideStop/resolveRepoRoot propagates as non-zero exit → stop-gate.sh's || {block} envelope (loud-fail), not a silent allow.
Migration safety
- Loud-fail port symmetric (missing-binary + non-zero-exit, both branches). CLASS-C(c) satisfied.
- em-recall
--gate stopuntouched; both paths kept in parity until P3d. validate-plan-marker-sitesgreen — enforce-contract refs the plan marker by symbol, no new code-literal (no registry drift).
Tests
44/0 (enforce-contract) + 31/0 (stop-gate, incl. real hook integration) + 2/2 (validator). E2E via the /var/folders fake-home fixture exercises the production hook path and is what surfaced the fail-open bug.
Follow-ups (non-blocking)
- #390 — same isMain idiom in
structured-alert-probe.mjs(test-only today). - P3d — retarget
test-stop-gate.shLayer 1 off the to-be-deletedem-recall --gate stop.
Verdict: looks correct and well-covered. Deferring approval to the maintainer (UI).
…E fixture + wire test into CI CI caught a fixture-completeness drift: test-issue-146.mjs's mkE2EHome() runs the real stop-gate.sh hook (L4 runtime-E2E) through a fake canonical install, but staged only em-recall.mjs — so post-repoint the hook hit its loud-fail "enforce-contract.mjs not found" envelope and L4.1b (carve-out should allow) saw a block. Stage enforce-contract.mjs (its lib import closure was already copied). Local: 51/0. Also wire tests/test-enforce-contract.mjs into plan-marker-validate.yml (it ran in no CI workflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex PR-level review — ACCEPT (consensus, round 1)Cross-tool review via the second-opinion harness (episodes Codex independently verified:
Combined review trail for this PR: plan — codex R1 HOLD → R2 ACCEPT + negative-scenario-planner; code — negative-scenario-reviewer ACCEPT-with-FU; PR-level — codex ACCEPT. Deferring approval to the maintainer (UI). |
RFC-008 P3b-1 — relocate the
stopdecision em-recall → enforce-contract.mjsFirst sub-PR of the load-bearing P3b. Pure relocation (R1 strong form, mirroring P3a): move em-recall.mjs's
--gate stophandler verbatim into a new enforcement-layerscripts/enforce-contract.mjs, byte-identical, and repointstop-gate.shto it. The substrate's surviving enforcement code (em-recall--gate stop) is deleted in P3d.Scope: why pure relocation (no contract reads)
The workplan listed the full thin waist under P3b, but the only consumer migrated is
stop-gate.sh, which exercises only thestopevent — and the claude-code stop decision is purely marker-state (RFC-008:464, "thestopgate reads marker state, not command labels"). The contract-driven tier layer (effective_tier = min(),_index.jsoncapability lookup, config clamp, CLASS-C(a) fail-closed-on-unsupported) is inert for claude-code — min(STRONG,STRONG,identity)=STRONG→refuse_stop = today's behavior — and reading contract files from a globally-installed script would block-loud in every project but this one (install deploys onlypatterns/_index.json). So the tier layer defers to P3b-2, landing with its real dependencies (install-runtime contract deploy + the P4 config schema). P3d is unaffected.Changes
scripts/enforce-contract.mjs(new): puredecideStop({repoRoot, sid})+ thin CLI. The 3 em-recall exits (:182/:211/:216) translate toreturn;console.log/process.exitlive only in the CLI wrapper.stop-gate.sh: repoint em-recall → enforce-contract; both loud-fail envelopes preserved (CLASS-C c — missing/erroring binary blocks loud, never allow-always).isMaincheck (import.meta.url === pathToFileURL(argv[1])) no-ops under a symlinked install path (macOS/var→/private/var) → stop gate degrades to allow-always. em-recall had no isMain guard so was immune. Fixed via realpath-both; pinned by test C4 + the/var/foldershook fixture. Found during E2E, before review.Tests
node tests/test-enforce-contract.mjs→ 44/0 (decideStop unit; 7-scenario parity vsem-recall --gate stopon stdout + exit + stderr-modulo-prefix + no-marker-side-effect; CLI + symlink regression).bash tests/test-stop-gate.sh→ 31/0 (Layer-1 em-recall unit; Layer-2/3 hook→enforce-contract integration incl. retargeted L3.4 loud-fail; Layer-4 retime/rearm).node scripts/validate-plan-marker-sites.mjs→ 2/2.Review trail
Follow-ups
structured-alert-probe.mjs:138carries the same brittle isMain idiom (test-only today).test-stop-gate.shLayer 1 offem-recall --gate stopwhen that handler is deleted (inherent to P3d scope).🤖 Generated with Claude Code