@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +21 -58
  2. package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
  3. package/bin/__tests__/codex-hooks.test.js +382 -0
  4. package/bin/__tests__/codex-native.test.js +579 -80
  5. package/bin/__tests__/develop-contract.test.js +155 -76
  6. package/bin/__tests__/installer-safety.test.js +19 -154
  7. package/bin/__tests__/package-inventory.test.js +494 -26
  8. package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
  9. package/bin/__tests__/validator-grounding.test.js +11 -11
  10. package/bin/install.js +2 -7
  11. package/bin/lib/context.js +0 -45
  12. package/bin/lib/copy-utils.js +1 -1
  13. package/bin/lib/i18n.js +0 -3
  14. package/bin/lib/manifest.js +1 -1
  15. package/bin/phases/claude-runtime.js +1 -3
  16. package/bin/phases/codex-runtime.js +4 -2
  17. package/bin/phases/copy-payload.js +3 -15
  18. package/bin/phases/post-install.js +5 -17
  19. package/bin/phases/root-config.js +2 -3
  20. package/bin/phases/select-platform.js +2 -5
  21. package/bin/phases/summary.js +0 -1
  22. package/package.json +2 -5
  23. package/src/claude/CLAUDE.md +19 -6
  24. package/src/claude/agents/project-manager.md +41 -9
  25. package/src/claude/agents/spec-maker.md +82 -155
  26. package/src/claude/gitignore +1 -1
  27. package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
  28. package/src/claude/hooks/__tests__/state.test.js +249 -3
  29. package/src/claude/hooks/lib/context.cjs +1 -1
  30. package/src/claude/hooks/lib/detect.cjs +1 -1
  31. package/src/claude/hooks/spec-gate.cjs +44 -9
  32. package/src/claude/hooks/spec-state.cjs +51 -13
  33. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  34. package/src/claude/migration-manifest.json +17 -3
  35. package/src/claude/rules/manage-docs.md +43 -24
  36. package/src/claude/rules/state-sync.md +57 -62
  37. package/src/claude/rules/workflow.md +30 -13
  38. package/src/claude/scripts/change-firewall.cjs +2 -2
  39. package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
  40. package/src/claude/scripts/spec-receipt.cjs +196 -1
  41. package/src/claude/scripts/spec-resolver.cjs +428 -0
  42. package/src/claude/skills/develop/SKILL.md +89 -125
  43. package/src/claude/skills/develop/references/parallel-waves.md +75 -162
  44. package/src/claude/skills/develop/references/quality-gate.md +61 -121
  45. package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
  46. package/src/claude/skills/question/SKILL.md +1 -1
  47. package/src/claude/skills/specs/SKILL.md +152 -204
  48. package/src/claude/skills/specs/references/review.md +124 -243
  49. package/src/claude/skills/specs/references/templates.md +202 -0
  50. package/src/claude/skills/sync/SKILL.md +66 -90
  51. package/src/claude/skills/sync/references/sync-protocols.md +62 -83
  52. package/src/codex/AGENTS.md +20 -7
  53. package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
  54. package/src/codex/hooks/lib/spec-utils.cjs +25 -0
  55. package/src/codex/hooks/spec-gate.cjs +46 -16
  56. package/src/codex/hooks/spec-state.cjs +54 -16
  57. package/src/codex/rules/state-sync.md +57 -61
  58. package/src/common/AGENTS.md +8 -1
  59. package/bin/__tests__/opencode-spec-gate.test.js +0 -799
  60. package/bin/lib/opencode-install.js +0 -899
  61. package/bin/phases/opencode-runtime.js +0 -38
  62. package/src/claude/skills/specs/references/archive-workflow.md +0 -74
  63. package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
  64. package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
  65. package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
  66. package/src/claude/skills/specs/references/research-strategy.md +0 -107
  67. package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
  68. package/src/claude/skills/specs/references/translation-mirror.md +0 -83
  69. package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
  70. package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
  71. package/src/claude/skills/specs/rules/design-principles.md +0 -138
  72. package/src/claude/skills/specs/rules/design-review.md +0 -55
  73. package/src/claude/skills/specs/rules/ears-format.md +0 -42
  74. package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
  75. package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
  76. package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
  77. package/src/opencode/AGENTS.md +0 -41
  78. package/src/opencode/plugins/docs-sync.ts +0 -175
  79. package/src/opencode/plugins/inspect-block.ts +0 -164
  80. package/src/opencode/plugins/privacy-block.ts +0 -182
  81. package/src/opencode/plugins/rules.ts +0 -155
  82. package/src/opencode/plugins/session.ts +0 -179
  83. package/src/opencode/plugins/spec-gate.ts +0 -457
  84. package/src/opencode/plugins/spec-state.ts +0 -311
  85. package/src/opencode/plugins/state.ts +0 -253
  86. package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
  87. package/src/opencode/plugins/usage.ts +0 -178
@@ -1,175 +1,102 @@
1
1
  ---
2
2
  name: spec-maker
3
- description: "Specification architect for the smallest routed Specs v2.1 artifact graph."
4
- model: opus
5
- tools: Glob, Grep, Read, Edit, Write, Bash, WebFetch, WebSearch, TaskCreate, TaskGet, TaskUpdate, TaskList, SendMessage
3
+ description: Build a small process-first feature plan and flat executable task packets, then run evidence-backed adversarial review before implementation.
4
+ tools: Read, Grep, Glob, Bash, Write, Edit, AskUserQuestion
5
+ model: inherit
6
6
  ---
7
+ # Spec Maker — process-first planner
7
8
 
8
- # Spec Maker — semantic kernel v2.1 author
9
+ Turn a substantial feature request into the smallest durable packet that
10
+ removes product decisions from implementation. The output is Markdown under
11
+ `specs/<feature>/`; it is not an implementation and does not authorize work.
9
12
 
10
- Produce specification artifacts, never implementation. Read
11
- `.claude/skills/specs/SKILL.md` first; it is the canonical routing contract.
13
+ ## Inputs
12
14
 
13
- ## Artifact Contract (MANDATORY)
15
+ - the user's feature description;
16
+ - current repository instructions and code;
17
+ - existing plans or decisions explicitly placed in scope;
18
+ - the three human-gate decisions from `hapo:specs`.
14
19
 
15
- `spec.json` is machine semantic authority. Requirements, design, conditional
16
- research, and conditional task Markdown are human projections. Host state
17
- records authority/integrity only and never supplies product semantics.
18
- The canonical `semantic_model`, semantic review, and readiness are promoted
19
- atomically only by the explicit installed machine semantic-sync step,
20
- `spec-readiness.cjs`. Authors and reviewers never directly write or promote
21
- those finalizer-owned fields; the command owns their validated round-trip shape. Authors may persist legitimate scope/decision/topology product semantics through the canonical scaffold/sync paths.
20
+ If the change is clear, isolated, reversible, and one or two files, recommend
21
+ direct implementation instead of manufacturing a plan.
22
22
 
23
- ```bash
24
- # Claude Code
25
- node .claude/scripts/spec-readiness.cjs specs/<feature> --review-result <review.json>
26
- # Codex
27
- node .codex/scripts/spec-readiness.cjs specs/<feature> --review-result <review.json>
28
- ```
23
+ ## Required process
29
24
 
30
- Run it only after Markdown and the semantic review result are final. Any failure
31
- leaves the exact original `spec.json` bytes unchanged.
32
-
33
- Persist the canonical 2.1 workflow-policy input: `version`, `planning_depth`,
34
- `assurance_level`, `classified_minimum`, and normalized `risks`.
35
- `planning_depth` and `assurance_level` are independent. Compatibility lane
36
- labels are derived views, never authoring inputs.
37
-
38
- Any normalized risk makes the automatic assurance minimum Elevated. Strict is
39
- opt-in and requires an explicit user/project independent-audit requirement or a
40
- user-confirmed, scope-specific audit decision. Never infer Strict from a risk
41
- keyword, severity label, Full depth, or model preference.
42
-
43
- Run the artifact router before loading discovery guidance or creating files:
44
-
45
- | Depth | Durable output |
46
- |---|---|
47
- | `None` | no durable spec artifact |
48
- | `Compact` | bounded `spec.json`, `requirements.md`, and `design.md` core |
49
- | `Full` | the same core plus only design sections activated by scope |
50
-
51
- Research is conditional on unresolved material uncertainty, an external-current
52
- fact that must be grounded, or an explicit user request. Otherwise it is absent
53
- with no pointer, file, template load, researcher, web search, or placeholder.
54
- Tasks are conditional on typed ownership, dependency, transition, proof, or
55
- parallel boundaries. Full and Strict do not trigger either artifact.
56
-
57
- Never write `init.json`, `spec-state.json`, `hydration.md`, phase files, or
58
- shorthand task filenames into a feature directory.
59
-
60
- ## Authoring route
61
-
62
- 1. Resolve the active feature and scope; ambiguity fails closed.
63
- 2. Classify planning and assurance independently, then route artifacts.
64
- 3. Establish `scope_lock` before semantic authoring.
65
- 4. Author requirements and design; load discovery only after routing.
66
- 5. Materialize conditional research/tasks only when their trigger is proven.
67
- 6. Review the whole graph, synchronize projections, and run final-byte gates.
68
- 7. Ask the installed machine finalization path to evaluate readiness; do not
69
- write readiness or authority directly.
70
-
71
- Investigate and ground repository facts. Choose the simplest reversible
72
- engineering option and record its bounded assumption. Ask the user for product,
73
- scope, security, data, or irreversible choices. Persist only actual items in
74
- `spec.json.decisions`; unresolved `user_owned` items block readiness.
75
-
76
- Reclassify before persistence. A persisted same-feature baseline is monotonic
77
- and cannot be downgraded until a trusted issuer exists; another feature remains
78
- independent. Authoring states are exactly `draft`, `validated`, or `absent` and
79
- are not approvals.
80
-
81
- ## Requirements and design
82
-
83
- Requirements use numeric, testable `RN.M` criteria. Design defines behavior
84
- owners, six-column typed anchors, stable decisions/contracts/invariants, and one
85
- parser-visible Verification Definitions section. Each V definition covers exact
86
- criteria and decision refs, a method or inspection, expected result, concrete
87
- negative/failure case, and reachability/grounding expectation.
88
-
89
- For retention/lifecycle and API contracts, follow `rules/design-principles.md` (clock anchor/source/timezone/precision/comparator/inclusivity/boundary + counterexample; method/route/auth/headers/schema/response/error/idempotency).
90
-
91
- Repository scouting may ground design without producing research. If new
92
- material uncertainty appears, return to the router rather than silently adding
93
- an artifact. Never invent a product decision, source, anchor, or command.
94
-
95
- ## Conditional tasks
96
-
97
- When typed topology activates tasks, each task has exactly seven H2 sections:
98
- Outcome; Scope; Anchors and Ownership; Changes; Acceptance; Dependencies; and
99
- Verification Plan. The only ownership table is
100
- `ID | Type | Target | Role | Access | Action`.
101
-
102
- Typed `coordination.boundaries` are the sole ownership, dependency, transition,
103
- proof, and parallel authority. Legacy trigger fields, priority markers,
104
- related-file lists, or prose are read compatibility only. Each `RN.M` has one
105
- implementation owner. A proof verifier owns a separate proof criterion/artifact
106
- and does not repeat the subject's Acceptance claim. Planned verification is not
107
- execution evidence. Every executable implementation task must own a concrete
108
- test file or artifact anchor (`file` or `artifact` with `write` `create` or
109
- `modify`) or share proof through a typed `proof` boundary; a bare `npm test`
110
- string is not ownership. Keep `Direct` and `Compact` light: do not coerce a
111
- task when no typed boundary exists. `spec.json` is machine authority for finalizer-owned `semantic_model`, `validation.semantic_review`, and `ready_for_implementation`; authors may persist legitimate scope/decision/topology product semantics only through the canonical scaffold/sync paths and must never hand-edit the finalizer-owned fields.
112
-
113
- ## Review and readiness
114
-
115
- Inventory every blocker across the routed graph. For every `RN.M`, construct a
116
- concrete counterexample and trace it to real `D`/`I`/`C` and `V` definitions.
117
- For any retention or lifecycle rule, include a counterexample where the wrong
118
- eligibility clock would violate the policy. Keep presentation concise without
119
- limiting finding count. Two-review target is advisory: after two failed semantic rounds, pause for unresolved product/security/architecture uncertainty or repeated non-convergence; one bounded mechanical correction without weakening gates is allowed; budgets never override correctness. Specs-only never creates or updates docs and never
120
- fabricates execution proof; record doc impact as a brief recommendation only.
121
-
122
- Routine and Elevated have no reviewer ceremony. Strict requires an independent
123
- allowlisted reviewer capability observed by the host hook. The author cannot
124
- self-attest it; the guardrail is not host-attested evidence or a security
125
- boundary. If the event-capable host path is unavailable, pause once with that
126
- exact blocker; do not retry reviewer loops, downgrade, or simulate authority.
127
-
128
- For every durable spec, run final-byte validation and grounding through the
129
- installed commands, including:
130
-
131
- ```bash
132
- # Claude Code
133
- node .claude/scripts/validate-spec-output.cjs specs/<feature>
134
- # Codex
135
- node .codex/scripts/validate-spec-output.cjs specs/<feature>
136
- ```
25
+ ### 1. Inspect before asking
137
26
 
138
- Exit 0 proves only implemented deterministic checks. It does not prove semantic
139
- quality, approval, execution, lifecycle completion, or closeout. The installed
140
- machine authority may promote technical readiness only after every routed
141
- artifact, projection round-trip, semantic review, grounding check, and gate
142
- passes. The author never fabricates or directly writes that conclusion.
27
+ Find current entrypoints, reusable code, callers, tests, docs, and runtime
28
+ registration. Cite concrete `path:line` evidence. Mark facts that cannot be
29
+ verified as `[UNVERIFIED]` and state what would settle them.
143
30
 
144
- ## Lifecycle and handoff
31
+ ### 2. Open C1 once
145
32
 
146
- Canonical feature lifecycle is exactly `in_progress`, `paused`, `blocked`, or
147
- `done`. Technical readiness differs from closeout. Specs cannot produce `done`;
148
- final-state authority owns it after implementation closeout.
33
+ Summarize what exists, the minimum change set, and expansion signals. Ask the
34
+ user to EXPAND, KEEP, or CUT. Record the answer and explicit exclusions. Do not
35
+ repeat scope objections later unless new evidence invalidates the decision.
149
36
 
150
- Specs never invokes Develop. After machine-promoted readiness, report only the
151
- spec path and optional separate user invocation:
37
+ ### 3. Author the flat packet
38
+
39
+ Read `skills/specs/references/templates.md`, then create its marked flat packet:
152
40
 
153
41
  ```text
154
- Claude Code: /hapo:develop <feature>
155
- Codex: $hapo-develop <feature>
42
+ specs/<feature>/plan.md
43
+ specs/<feature>/task-01-<slug>.md
44
+ specs/<feature>/task-NN-<slug>.md
156
45
  ```
157
46
 
158
- Never suggest `/work`, `/code`, or an unnamed dispatch alias. `--auto` may
159
- complete authoring readiness through the same machine gates but still stops in
160
- Specs. On early stop, preserve the accurate unfinished lifecycle and name the
161
- exact decision or evidence needed to resume.
47
+ Keep tasks flat beside the plan. Each task owns one outcome, normally no more
48
+ than about five files, explicit acceptance IDs, dependencies, and a runnable
49
+ Verification Plan. Every criterion maps to at least one task and one proof.
50
+ Use EARS sentences for observable acceptance behavior.
51
+
52
+ Do not create implementation files, receipts, approval records, generated
53
+ registries, readiness claims, or empty supporting documents. Keep every new
54
+ task `Status: blocked` while C2 is open, and keep its `## Receipt` empty.
55
+
56
+ ### 4. Review from fresh context
57
+
58
+ Read `skills/specs/references/review.md`. Route fresh reviewers by capability
59
+ and give them only the plan packet plus repository access. Require a severity,
60
+ plan location, concrete failure, current `path:line` evidence, and smallest
61
+ repair for every finding.
62
+
63
+ Deduplicate and cap findings at 15. Open C2 so the user can accept, reject, or
64
+ revise each one. Apply only accepted changes and run the full consistency sweep
65
+ after every edit. Then derive every task state, not only the first candidate:
66
+ `pending` means semantically ready for the dependency-aware queue. Keep a task
67
+ `blocked` while a C1/C2 decision, accepted finding, or `UNKNOWN` closure remains
68
+ open. A named task dependency alone does not make it blocked; write dependencies
69
+ as exact flat task basenames and let the resolver derive the next pending task.
70
+ Move `blocked` to `pending` only when current evidence closes every non-dependency
71
+ blocker. Stop after two paper rounds; later findings need runtime evidence.
162
72
 
163
- ## Lazy references
73
+ ### 5. Hand off without dispatch
164
74
 
165
- | Need | Load only then |
166
- |---|---|
167
- | acceptance syntax | `rules/ears-format.md` |
168
- | routed design | `rules/design-principles.md` and selected discovery rule |
169
- | activated research | research reference/template |
170
- | activated task topology | `rules/tasks-generation.md`, optional scoring rubric, task template |
171
- | final review | `references/review.md`, review and grounding rules |
75
+ Report the created files, C1 decision, accepted C2 findings, remaining
76
+ uncertainties, every task status, and the first pending task.
77
+ Do not start Develop or treat dispatchability as user authorization. The user
78
+ chooses when execution begins.
172
79
 
173
- Claude Code and Codex are the acceptance runtimes. Verify both installed
174
- projections and use native vocabulary on each surface. Report unresolved
175
- questions last.
80
+ ## Authoring constraints
81
+
82
+ - One mutable fact has one home; other files link to it.
83
+ - A task is not a bucket for several independent outcomes.
84
+ - Unknown outcomes become questions, not guessed requirements.
85
+ - Ambiguous rules receive two or three examples before C2.
86
+ - Ownership overlap or dependency uncertainty prevents a parallel claim.
87
+ - A command that cannot be run from the named work context is not verification.
88
+ - User approval and test proof are different facts; never infer either.
89
+ - Existing legacy specs remain untouched unless migration is explicitly asked.
90
+
91
+ ## Output
92
+
93
+ End with:
94
+
95
+ ```text
96
+ Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
97
+ Files: <plan and task paths>
98
+ C1: <decision>
99
+ C2: <accepted/rejected/revised counts>
100
+ Next: <first `Status: pending` task or blocker>
101
+ Unresolved questions: <none or concise list>
102
+ ```
@@ -1,4 +1,4 @@
1
- # CafeKit local runtime (.claude/ or .opencode/)
1
+ # CafeKit local runtime (.claude/)
2
2
  # Also ignored at project root by the installer. This file protects
3
3
  # force-adds (`git add -f`) and partial un-ignores of the runtime folder.
4
4
 
@@ -78,6 +78,44 @@ function bindFixtureReceipt(dir, value) {
78
78
  return value.replaceAll(VALID_BASE, context.base).replaceAll(VALID_HEAD, context.head);
79
79
  }
80
80
 
81
+ function workflowRuntimeContext(dir) {
82
+ return PROVENANCE.deriveRuntimeContext({
83
+ projectRoot: dir,
84
+ specsRoot: path.join(dir, 'specs'),
85
+ specFile: path.join(dir, 'specs', FEATURE, 'plan.md'),
86
+ featureName: FEATURE,
87
+ runtimeSession: 'test',
88
+ });
89
+ }
90
+
91
+ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test') {
92
+ const dir = tmpDir();
93
+ const featureDir = path.join(dir, 'specs', FEATURE);
94
+ fs.mkdirSync(featureDir, { recursive: true });
95
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
96
+ const context = workflowRuntimeContext(dir);
97
+ const task = [
98
+ '# Task 01: demo',
99
+ '',
100
+ 'Status: done',
101
+ '',
102
+ '## Dependencies',
103
+ '',
104
+ '- none',
105
+ '',
106
+ '## Verification Plan',
107
+ '',
108
+ `- Command: ${plannedCommand}`,
109
+ '',
110
+ ...receiptLines,
111
+ '',
112
+ ].join('\n')
113
+ .replaceAll(VALID_BASE, context.base)
114
+ .replaceAll(VALID_HEAD, context.head);
115
+ fs.writeFileSync(path.join(featureDir, 'task-01-demo.md'), task);
116
+ return dir;
117
+ }
118
+
81
119
  function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
82
120
  const featureDir = path.join(dir, 'specs', featureName);
83
121
  const specFile = path.join(featureDir, 'spec.json');
@@ -1241,3 +1279,203 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1241
1279
  fs.rmSync(dir, { recursive: true, force: true });
1242
1280
  }
1243
1281
  });
1282
+
1283
+ test('31. process-v3 done task without inline Receipt blocks', () => {
1284
+ const dir = makeWorkflowFixture();
1285
+ try {
1286
+ clearCache();
1287
+ const body = parseBlock(runHook({}, dir).stdout);
1288
+ assert.ok(body, 'missing process-v3 receipt should block');
1289
+ assert.strictEqual(body.decision, 'block');
1290
+ assert.match(body.reason, /missing_receipt/);
1291
+ assert.match(body.reason, /## Receipt/);
1292
+ } finally {
1293
+ fs.rmSync(dir, { recursive: true, force: true });
1294
+ }
1295
+ });
1296
+
1297
+ test('32. process-v3 requires Receipt heading and runtime-bound provenance', () => {
1298
+ const legacyHeading = makeWorkflowFixture([
1299
+ '## Evidence', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1300
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', 'pass', '```',
1301
+ ]);
1302
+ try {
1303
+ clearCache();
1304
+ const body = parseBlock(runHook({}, legacyHeading).stdout);
1305
+ assert.ok(body, 'legacy Evidence heading must not satisfy process-v3 receipt');
1306
+ assert.match(body.reason, /missing_receipt/);
1307
+ } finally {
1308
+ fs.rmSync(legacyHeading, { recursive: true, force: true });
1309
+ }
1310
+
1311
+ const stale = makeWorkflowFixture([
1312
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1313
+ `Base: ${VALID_BASE}`, `Head: ${VALID_BASE}`, '```text', 'pass', '```',
1314
+ ]);
1315
+ try {
1316
+ clearCache();
1317
+ const body = parseBlock(runHook({}, stale).stdout);
1318
+ assert.ok(body, 'stale process-v3 provenance should block');
1319
+ assert.match(body.reason, /provenance/);
1320
+ } finally {
1321
+ fs.rmSync(stale, { recursive: true, force: true });
1322
+ }
1323
+ });
1324
+
1325
+ test('32b. process-v3 ignores a Receipt heading and proof contained only in a fence', () => {
1326
+ const dir = makeWorkflowFixture([
1327
+ '~~~~markdown', '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1328
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', 'pass: 1', '```', '~~~~',
1329
+ ]);
1330
+ try {
1331
+ clearCache();
1332
+ const body = parseBlock(runHook({}, dir).stdout);
1333
+ assert.ok(body, 'fenced-only process-v3 Receipt must block');
1334
+ assert.strictEqual(body.decision, 'block');
1335
+ assert.match(body.reason, /missing_receipt/);
1336
+ } finally {
1337
+ fs.rmSync(dir, { recursive: true, force: true });
1338
+ }
1339
+ });
1340
+
1341
+ test('32c. process-v3 ignores canonical Receipt fields contained only in its output fence', () => {
1342
+ const dir = makeWorkflowFixture([
1343
+ '## Receipt', '', '```text', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1344
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, 'pass: 1', '```',
1345
+ ]);
1346
+ try {
1347
+ clearCache();
1348
+ const body = parseBlock(runHook({}, dir).stdout);
1349
+ assert.ok(body, 'fenced-only canonical fields must block');
1350
+ assert.strictEqual(body.decision, 'block');
1351
+ assert.match(body.reason, /verification_state/);
1352
+ assert.match(body.reason, /command/);
1353
+ assert.match(body.reason, /provenance/);
1354
+ } finally {
1355
+ fs.rmSync(dir, { recursive: true, force: true });
1356
+ }
1357
+ });
1358
+
1359
+ test('32d. process-v3 preserves explicit failures from the Receipt output fence', () => {
1360
+ const dir = makeWorkflowFixture([
1361
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1362
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
1363
+ 'exit code: 1', 'FAIL', '```',
1364
+ ]);
1365
+ try {
1366
+ clearCache();
1367
+ const body = parseBlock(runHook({}, dir).stdout);
1368
+ assert.ok(body, 'explicit fenced output failure must block');
1369
+ assert.strictEqual(body.decision, 'block');
1370
+ assert.match(body.reason, /verification_state/);
1371
+ assert.match(body.reason, /exit_result/);
1372
+ } finally {
1373
+ fs.rmSync(dir, { recursive: true, force: true });
1374
+ }
1375
+ });
1376
+
1377
+ test('32e. process-v3 rejects output whose closing fence is shorter than its opener', () => {
1378
+ const dir = makeWorkflowFixture([
1379
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1380
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '````text', '$ node --test',
1381
+ 'pass: 1', '```',
1382
+ ]);
1383
+ try {
1384
+ clearCache();
1385
+ const body = parseBlock(runHook({}, dir).stdout);
1386
+ assert.ok(body, 'malformed output fence must block');
1387
+ assert.strictEqual(body.decision, 'block');
1388
+ assert.match(body.reason, /command_output/);
1389
+ } finally {
1390
+ fs.rmSync(dir, { recursive: true, force: true });
1391
+ }
1392
+ });
1393
+
1394
+ test('33. process-v3 done task with canonical inline Receipt passes', () => {
1395
+ const dir = makeWorkflowFixture([
1396
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1397
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
1398
+ 'Status: pending', '## Receipt', 'pass: 1', '```',
1399
+ ]);
1400
+ try {
1401
+ clearCache();
1402
+ const result = runHook({}, dir);
1403
+ assert.strictEqual(result.code, 0, result.stderr);
1404
+ assert.strictEqual(result.stdout, '');
1405
+ } finally {
1406
+ fs.rmSync(dir, { recursive: true, force: true });
1407
+ }
1408
+ });
1409
+
1410
+ test('34. process-v3 Receipt command must match the exact Verification Plan command', () => {
1411
+ const dir = makeWorkflowFixture([
1412
+ '## Receipt', '', 'Verification: PASS', 'Command: true', 'Exit: 0',
1413
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ true', 'pass: 1', '```',
1414
+ ], 'pnpm test');
1415
+ try {
1416
+ clearCache();
1417
+ const body = parseBlock(runHook({}, dir).stdout);
1418
+ assert.ok(body, 'a substituted receipt command must block');
1419
+ assert.strictEqual(body.decision, 'block');
1420
+ assert.match(body.reason, /command_identity/);
1421
+ } finally {
1422
+ fs.rmSync(dir, { recursive: true, force: true });
1423
+ }
1424
+ });
1425
+
1426
+ test('35. process-v3 Stop ignores a completed packet when one packet remains active', () => {
1427
+ const dir = makeWorkflowFixture([
1428
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1429
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test', 'pass: 1', '```',
1430
+ ]);
1431
+ try {
1432
+ const activeDir = path.join(dir, 'specs', 'active');
1433
+ fs.mkdirSync(activeDir, { recursive: true });
1434
+ fs.writeFileSync(path.join(activeDir, 'plan.md'), '# Active plan\n');
1435
+ fs.writeFileSync(path.join(activeDir, 'task-01-active.md'), [
1436
+ '# Task 01: active', '', 'Status: pending', '',
1437
+ '## Dependencies', '', '- none', '',
1438
+ '## Verification Plan', '', '- Command: node --test', '',
1439
+ ].join('\n'));
1440
+ clearCache();
1441
+ const result = runHook({}, dir);
1442
+ assert.strictEqual(result.code, 0, result.stderr);
1443
+ assert.strictEqual(result.stdout, '');
1444
+ } finally {
1445
+ fs.rmSync(dir, { recursive: true, force: true });
1446
+ }
1447
+ });
1448
+
1449
+ test('36. process-v3 Stop accepts two completed packets with valid Receipts', () => {
1450
+ const dir = makeWorkflowFixture([
1451
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1452
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test', 'pass: 1', '```',
1453
+ ]);
1454
+ try {
1455
+ const otherDir = path.join(dir, 'specs', 'other');
1456
+ fs.mkdirSync(otherDir, { recursive: true });
1457
+ fs.writeFileSync(path.join(otherDir, 'plan.md'), '# Other plan\n');
1458
+ const context = PROVENANCE.deriveRuntimeContext({
1459
+ projectRoot: dir,
1460
+ specsRoot: path.join(dir, 'specs'),
1461
+ specFile: path.join(otherDir, 'plan.md'),
1462
+ featureName: 'other',
1463
+ runtimeSession: 'test',
1464
+ });
1465
+ fs.writeFileSync(path.join(otherDir, 'task-01-other.md'), [
1466
+ '# Task 01: other', '', 'Status: done', '',
1467
+ '## Dependencies', '', '- none', '',
1468
+ '## Verification Plan', '', '- Command: node --test', '',
1469
+ '## Receipt', '',
1470
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1471
+ `Base: ${context.base}`, `Head: ${context.head}`,
1472
+ '```text', '$ node --test', 'pass: 1', '```', '',
1473
+ ].join('\n'));
1474
+ clearCache();
1475
+ const result = runHook({}, dir);
1476
+ assert.strictEqual(result.code, 0, result.stderr);
1477
+ assert.strictEqual(result.stdout, '');
1478
+ } finally {
1479
+ fs.rmSync(dir, { recursive: true, force: true });
1480
+ }
1481
+ });