harness-everything 0.30.10 → 0.31.0

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 (134) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +1 -1
  4. package/CHANGELOG.md +24 -4
  5. package/README.md +8 -9
  6. package/VERIFICATION.md +1 -1
  7. package/benchmarks/results/live-host/claude-2026-10-08-issue-297/README.md +27 -0
  8. package/benchmarks/results/pr-303-review-2026-10-13/README.md +81 -0
  9. package/benchmarks/results/pr-303-review-2026-10-13/ci/run.json +1330 -0
  10. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-clean-env.log +1 -0
  11. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-inherited-flag.log +34 -0
  12. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-captured.json +46 -0
  13. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-diagnosis.md +13 -0
  14. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate.json +46 -0
  15. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/summary.json +57 -0
  16. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-collision.log +8 -0
  17. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-consistency.log +624 -0
  18. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-docs-capabilities.log +131 -0
  19. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-platform-compatibility.log +9 -0
  20. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-openai.log +6 -0
  21. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-submission.log +5 -0
  22. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-references.log +6 -0
  23. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-release.log +73 -0
  24. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-repo-contract.log +5 -0
  25. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-invariants.log +785 -0
  26. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-skills.log +44 -0
  27. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/summary.json +12 -0
  28. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-consistency.log +624 -0
  29. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-release.log +73 -0
  30. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/summary.json +79 -0
  31. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mechanism.log +1737 -0
  32. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mutations.log +16 -0
  33. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test.log +3297 -0
  34. package/benchmarks/results/pr-303-review-2026-10-13/manifest.json +319 -0
  35. package/benchmarks/results/pr-303-review-2026-10-13/regression/binding-resolution-green.log +36 -0
  36. package/benchmarks/results/pr-303-review-2026-10-13/regression/determinism.json +174 -0
  37. package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-evidence.json +314 -0
  38. package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-report.json +638 -0
  39. package/benchmarks/results/pr-303-review-2026-10-13/regression/red.log +240 -0
  40. package/benchmarks/results/pr-303-review-2026-10-13/regression/untyped-fable-probe.json +22 -0
  41. package/benchmarks/results/pr-303-review-2026-10-13/regression/workflow-obligations-green.log +254 -0
  42. package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-clean-env-fixed.log +1 -0
  43. package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-inherited-flag-fixed.log +1 -0
  44. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.meta.json +8 -0
  45. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stderr.log +90 -0
  46. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stdout.log +3186 -0
  47. package/benchmarks/results/pr-303-review-2026-10-13/verification/verify-gate-final.json +46 -0
  48. package/bin/cli.js +1 -1
  49. package/ci/invariant-routing.test.js +7 -4
  50. package/ci/mechanism-2ag-router-worktree-contract.test.js +2 -2
  51. package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +2 -1
  52. package/ci/mechanism-2an-workflow-obligations.test.js +352 -3
  53. package/ci/mechanism-2aq-fable-stage-bindings.test.js +168 -0
  54. package/ci/mechanism-2ar-binding-resolution.test.js +211 -0
  55. package/ci/mechanism-35-verification-contract.test.js +1 -1
  56. package/ci/mutation-check.js +2 -0
  57. package/ci/mutation-loader.js +4 -0
  58. package/ci/openai-plugin-package.test.js +7 -5
  59. package/ci/prompt-contract-regressions.test.js +7 -6
  60. package/ci/router-workflow-plan.test.js +109 -14
  61. package/ci/skill-routing-check.js +44 -15
  62. package/ci/workflow-plan-consumers.test.js +41 -1
  63. package/docs/architecture.md +10 -10
  64. package/docs/enforcement-locks.md +1 -1
  65. package/docs/mechanism-first-skill-mesh.md +12 -11
  66. package/docs/openai-plugin.md +1 -1
  67. package/docs/philosophy.md +7 -6
  68. package/docs/platform-capabilities.md +7 -5
  69. package/docs/routing.md +16 -17
  70. package/docs/system-one-observations.md +1 -1
  71. package/docs/system-one-routing.md +10 -7
  72. package/docs/system-one-suggestion-gates.md +5 -5
  73. package/docs/workflow-runtime.md +6 -2
  74. package/docs/workflows/harness-everything.md +14 -14
  75. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  76. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  77. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  78. package/fable-mode/CONTRACT-FORMAT.md +4 -1
  79. package/fable-mode/SKILL.md +6 -8
  80. package/fable-mode/agents/fable-orchestrator.md +10 -3
  81. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  82. package/fable-mode/fable-haiku/SKILL.md +1 -1
  83. package/fable-mode/fable-opus/SKILL.md +1 -1
  84. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  85. package/fable-mode/scripts/workflow-plan-consumer.js +101 -2
  86. package/harness-everything/SKILL.md +8 -7
  87. package/harness-everything/references/router-workflow-plan.md +11 -6
  88. package/harness-everything/references/triage-and-tiers.md +21 -23
  89. package/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
  90. package/harness-everything/scripts/kernel-router-core.js +52 -55
  91. package/harness-everything/scripts/kernel-router.js +48 -5
  92. package/harness-everything/scripts/router-contract-core.js +7 -7
  93. package/harness-everything/scripts/router-contract.js +1 -3
  94. package/harness-everything/scripts/routing-keywords.json +417 -121
  95. package/harness-everything/scripts/tier-router.js +64 -113
  96. package/hooks/scripts/contract-test.js +29 -3
  97. package/hooks/scripts/lib/binding-resolver.js +76 -0
  98. package/hooks/scripts/lib/fable-contracts.js +1 -1
  99. package/hooks/scripts/lib/workflow-obligations.js +292 -7
  100. package/hooks/scripts/lib/workflow-runtime.js +8 -1
  101. package/hooks/scripts/workflow-disposition.js +116 -2
  102. package/opencode-plugin/plugin.json +1 -1
  103. package/package.json +1 -1
  104. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  105. package/plugins/harness-everything/hooks/scripts/contract-test.js +29 -3
  106. package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +76 -0
  107. package/plugins/harness-everything/hooks/scripts/lib/fable-contracts.js +1 -1
  108. package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +292 -7
  109. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +8 -1
  110. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +116 -2
  111. package/plugins/harness-everything/plugin.json +1 -1
  112. package/plugins/harness-everything/scripts/lib/advisory-text.js +2 -2
  113. package/plugins/harness-everything/scripts/lib/generated-skills.js +78 -0
  114. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +4 -1
  115. package/plugins/harness-everything/skills/fable-mode/SKILL.md +6 -8
  116. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +10 -3
  117. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  118. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  119. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  120. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  121. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +101 -2
  122. package/plugins/harness-everything/skills/harness-everything/SKILL.md +8 -7
  123. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +11 -6
  124. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +21 -23
  125. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
  126. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +52 -55
  127. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +48 -5
  128. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +7 -7
  129. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +1 -3
  130. package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +417 -121
  131. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +64 -113
  132. package/sbom.cdx.json +7 -7
  133. package/scripts/lib/advisory-text.js +2 -2
  134. package/scripts/lib/generated-skills.js +78 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "status": "UNCHECKED_DISCOVERED",
3
+ "reason": "1 verification candidate(s) detected but not executed",
4
+ "repoRoot": "<workspace>",
5
+ "roots": [
6
+ {
7
+ "path": "<workspace>",
8
+ "status": "UNCHECKED_DISCOVERED",
9
+ "reason": "1 verification candidate(s) detected but not executed",
10
+ "checks": [
11
+ {
12
+ "id": "package-test",
13
+ "run": "npm run test",
14
+ "cwd": "<workspace>",
15
+ "source": "package-auto",
16
+ "exitCode": 0,
17
+ "durationMs": 245096
18
+ }
19
+ ],
20
+ "candidates": [
21
+ {
22
+ "source": "git-hook",
23
+ "run": "git commit (pre-commit hook configured)",
24
+ "cwd": "<workspace>"
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "checks": [
30
+ {
31
+ "id": "package-test",
32
+ "run": "npm run test",
33
+ "cwd": "<workspace>",
34
+ "source": "package-auto",
35
+ "exitCode": 0,
36
+ "durationMs": 245096
37
+ }
38
+ ],
39
+ "candidates": [
40
+ {
41
+ "source": "git-hook",
42
+ "run": "git commit (pre-commit hook configured)",
43
+ "cwd": "<workspace>"
44
+ }
45
+ ]
46
+ }
package/bin/cli.js CHANGED
@@ -103,7 +103,7 @@ Commands:
103
103
  error instead of silently doing nothing.
104
104
  self-regression Run syntax and routing checks before committing changes (alias: test)
105
105
  next "<prompt>" Print the Harness Kernel routing recommendation for a prompt:
106
- tier/rationale + mandatory invariants + advisory skill suggestions.
106
+ tier/rationale + mandatory invariants + normalized knowledge signals.
107
107
  Hookless platforms can call this explicitly at the start of software
108
108
  work; hosts with UserPromptSubmit hooks receive it automatically.
109
109
  verify-install Compare installed Harness manifests and skill trees with
@@ -11,13 +11,15 @@ check(tier2.status===0,'kernel router exits successfully');
11
11
  check(/RECOMMENDED TIER:\s*Tier 2/i.test(tier2.stdout),'classifier result is preserved');
12
12
  check(tier2.stdout.includes('"strategy":"iterative-single"'),'Tier 2 selects iterative-single workflow');
13
13
  check(tier2.stdout.includes('loop-awareness:'),'iterative work exposes loop-awareness instead of a hard budget');
14
- check(tier2.stdout.includes('WORKFLOW SKILLS (APPLICABILITY MUST BE RESOLVED)'),'skill applicability is explicitly mandatory');
14
+ check(tier2.stdout.includes('ACTIVE-STEP KNOWLEDGE BINDINGS (MUST)'),'active-step binding resolution is explicitly mandatory');
15
15
  check(tier2.stdout.includes('ORCHESTRATION POLICY: Selected-topology required obligations and applicable skill core contracts are semantic MUSTs'),'orchestration policy preserves semantic MUST obligations');
16
16
  check(tier2.stdout.includes('WORKFLOW SEMANTIC CONTRACT (NON-BLOCKING OBSERVATION)'),'kernel separates semantic contract from non-blocking observation');
17
17
  check(tier2.stdout.includes('Tier 3 / Fable broad mutation MUST resolve isolation'),'worktree isolation disposition is a semantic MUST');
18
18
  check(!tier2.stdout.includes('SELECTED WORKFLOW IS GUIDANCE'),'old planning-guidance label is absent');
19
19
  check(!tier2.stdout.includes('unavailable isolation means BLOCKED'),'old worktree hard-block wording is absent');
20
- check(tier2.stdout.includes('evaluate-suggestions-before-skip'),'skill suggestions still require evaluation before omission');
20
+ check(tier2.stdout.includes('future-step bindings remain undisclosed'),'future step knowledge stays undisclosed');
21
+ check(tier2.stdout.includes('unknown/unavailable binding stays visible and unresolved'),'required binding gaps remain visible without adding a lock');
22
+ check(!tier2.stdout.includes('evaluate-suggestions-before-skip'),'global read-every-suggestion invariant is removed');
21
23
  check(tier2.stdout.includes('visible-status-updates'),'workflow plan carries mandatory user-visible status invariant');
22
24
  check(tier2.stdout.includes('USER-VISIBLE HARNESS STATUS CONTRACT (MUST)'),'kernel emits the mandatory unified status contract');
23
25
  for(const field of ['### 🚦 Harness Status','- **Current:**','- **Read / Evidence:**','- **Next:**','- **Risk / Blocked:**']){
@@ -31,7 +33,8 @@ check(tier2.stdout.includes('semantic communication MUST, not a hard execution l
31
33
  const tier3=spawnSync(process.execPath,[kernel,'audit the entire repository architecture and coordinate multiple modules'],{cwd:ROOT,encoding:'utf8'});
32
34
  check(tier3.status===0,'Tier 3 routing exits successfully');
33
35
  check(/RECOMMENDED TIER:\s*Tier 3/i.test(tier3.stdout),'macro task remains Tier 3');
34
- check(tier3.stdout.includes('using-git-worktrees'),'Tier 3 still routes the worktree skill');
36
+ check(tier3.stdout.includes('isolated-worktree-before-mutation'),'Tier 3 keeps the worktree isolation invariant');
37
+ check(!tier3.stdout.includes('using-git-worktrees/SKILL.md'),'Tier 3 does not push a global worktree skill path');
35
38
  check(tier3.stdout.includes('Tier 3 / Fable broad mutation MUST resolve isolation'),'Tier 3 requires an isolation disposition without a hard lock');
36
39
 
37
40
  const hs=read('harness-everything/SKILL.md');
@@ -60,7 +63,7 @@ const philosophy=read('docs/philosophy.md');
60
63
  check(/Contract Strength: MUST \/ SHOULD \/ MAY/i.test(philosophy),'philosophy defines canonical MUST/SHOULD/MAY strength');
61
64
  for(const phrase of [
62
65
  'Route before software/project execution',
63
- 'Read/evaluate every suggested skill before omission',
66
+ 'Resolve active required skill/reference bindings before their step',
64
67
  'Resolve selected-topology invariants/stages/checks/synthesis/verification',
65
68
  'Discover relevant environment/host facts',
66
69
  'Resolve Tier-3/Fable isolation before broad mutation',
@@ -16,7 +16,7 @@ const tier3 = router.buildRouterContract({
16
16
 
17
17
  assert.strictEqual(tier3.strategy, 'fable-staged');
18
18
  assert.ok(tier3.requiredInvariants.includes('isolated-worktree-before-mutation'), 'Tier 3 plan must carry mutation isolation invariant');
19
- assert.ok(tier3.suggestedSkills.includes('using-git-worktrees'), 'Tier 3 plan must surface the worktree workflow');
19
+ assert.strictEqual(tier3.suggestedSkills.length, 0, 'Tier 3 must not push a global worktree skill list');
20
20
  assert.strictEqual(tier3.workspace.required, false, 'Git worktree isolation must remain distinct from durable multi-agent workspace state');
21
21
  assert.strictEqual(tier3.mutationIsolation.onUnavailable, 'degraded', 'unavailable isolation records an explicit degraded fallback instead of a cognitive hard block');
22
22
 
@@ -29,7 +29,7 @@ const tier2 = router.buildRouterContract({
29
29
 
30
30
  assert.strictEqual(tier2.strategy, 'iterative-single');
31
31
  assert.ok(!tier2.requiredInvariants.includes('isolated-worktree-before-mutation'), 'Tier 2 must not be globally forced into major-workflow isolation');
32
- assert.ok(!tier2.suggestedSkills.includes('using-git-worktrees'), 'Tier 2 keeps worktree use task-dependent');
32
+ assert.strictEqual(tier2.suggestedSkills.length, 0, 'Tier 2 also has no topology-wide skill suggestions');
33
33
 
34
34
  const explicitFable = router.buildWorkflowPlan({
35
35
  routingStatus: 'ok',
@@ -189,7 +189,8 @@ try {
189
189
  });
190
190
  check(routed.status === 0, '#169 large-workspace Tier-2 fixture routes normally');
191
191
  const requirements = JSON.stringify([
192
- { id: 'req-fingerprint', summary: 'Exercise large-workspace shell admission', acceptance: 'the shell call remains available above the fingerprint observation cap' },
192
+ { id: 'req-fingerprint', stepType: 'behavior-change', summary: 'Exercise large-workspace shell admission', acceptance: 'the shell call remains available above the fingerprint observation cap' },
193
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify shell admission', acceptance: 'objective test evidence is recorded' },
193
194
  ]);
194
195
  const planned = node('hooks/scripts/workflow-disposition.js', undefined, [
195
196
  'plan', '--session-id', sessionId,
@@ -32,6 +32,9 @@ function run(script, args = [], input = null) {
32
32
  });
33
33
  }
34
34
  function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
35
+ function responseOf(result) {
36
+ try { return JSON.parse(result.stdout || ''); } catch (_) { return null; }
37
+ }
35
38
 
36
39
  console.log('=== Issue #85 requirements-first workflow contract ===');
37
40
 
@@ -72,9 +75,16 @@ check((workflow.unresolved || []).includes('decompose:pending'), 'decomposition
72
75
  check((workflow.unresolved || []).includes('compose:pending'), 'workflow-composition gap is visible');
73
76
 
74
77
  const requirements = JSON.stringify([
75
- { id: 'req-fix', summary: 'Correct the checkout failure', acceptance: 'the failing scenario succeeds', suggestedSkills: ['tdd'] },
76
- { id: 'req-regression', summary: 'Protect the behavior with a regression test', acceptance: 'the test fails before the fix and passes after it', suggestedSkills: ['tdd', 'verification-loop'] },
78
+ { id: 'req-fix', stepType: 'behavior-change', summary: 'Correct the checkout failure', acceptance: 'the failing scenario succeeds' },
79
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify the checkout change', acceptance: 'objective check evidence is recorded' },
80
+ ]);
81
+ const untypedRequirements = JSON.stringify([
82
+ { id: 'req-untyped', summary: 'Correct the checkout failure', acceptance: 'the failing scenario succeeds' },
77
83
  ]);
84
+ check(run(controller, [
85
+ 'plan', '--session-id', sessionId, '--requirements-json', untypedRequirements,
86
+ '--strategy', 'iterative-single', '--evidence', 'legacy untyped planning fixture',
87
+ ]).status !== 0, 'new single-agent plans cannot bypass active step typing');
78
88
  const planned = run(controller, [
79
89
  'plan', '--session-id', sessionId,
80
90
  '--requirements-json', requirements,
@@ -83,7 +93,7 @@ const planned = run(controller, [
83
93
  ]);
84
94
  check(planned.status === 0, 'requirements and workflow selection are recorded before execution');
85
95
  obligations = readJson(obligationFile);
86
- check(obligations.requirements.map(item => item.id).join(',') === 'req-fix,req-regression', 'requirement fragments are machine-readable');
96
+ check(obligations.requirements.map(item => item.id).join(',') === 'req-fix,req-verify', 'requirement fragments are machine-readable');
87
97
  check(obligations.workflowSelection.disposition === 'confirmed', 'workflow choice is explicitly confirmed after decomposition');
88
98
  check(obligations.obligations.find(item => item.id === 'decompose').status === 'pass', 'decompose obligation resolves from requirement fragments');
89
99
  check(obligations.obligations.find(item => item.id === 'compose').status === 'pass', 'compose obligation resolves from workflow-selection evidence');
@@ -111,6 +121,14 @@ check((workflow.unresolved || []).includes('verify:blocked'), 'blocked verificat
111
121
 
112
122
  check(run(controller, ['obligation', '--session-id', sessionId, '--obligation-id', 'verify', '--disposition', 'pass', '--evidence', 'npm test and focused regression passed']).status === 0,
113
123
  'verification can later resolve to pass with evidence');
124
+ check(run(controller, ['binding', '--session-id', sessionId, '--step-id', 'req-fix', '--binding-id', 'tdd', '--disposition', 'loaded', '--evidence', 'read and followed the TDD core contract before behavior changes']).status === 0,
125
+ 'behavior-change binding resolves before its active step passes');
126
+ check(run(controller, ['step', '--session-id', sessionId, '--step-id', 'req-fix', '--disposition', 'pass', '--evidence', 'behavior fix and regression case are complete']).status === 0,
127
+ 'behavior-change step advances after TDD binding resolution');
128
+ check(run(controller, ['binding', '--session-id', sessionId, '--step-id', 'req-verify', '--binding-id', 'verification-loop', '--disposition', 'loaded', '--evidence', 'read the verification contract for this step']).status === 0,
129
+ 'verification skill resolves only when the verification step becomes active');
130
+ check(run(controller, ['step', '--session-id', sessionId, '--step-id', 'req-verify', '--disposition', 'pass', '--evidence', 'objective checks completed and recorded']).status === 0,
131
+ 'verification step passes with objective evidence');
114
132
  check(run(statePersist, [], {
115
133
  session_id: sessionId, cwd: ROOT, hook_event_name: 'PostToolUse',
116
134
  tool_name: 'Bash', tool_input: { command: 'npm test' },
@@ -122,10 +140,339 @@ check(resolvedStop.status === 0, 'resolved workflow Stop remains fail-open');
122
140
  check(workflow.state === 'satisfied', 'planning + execution + verification allow satisfied');
123
141
  check((workflow.unresolved || []).length === 0, 'satisfied workflow has no unresolved obligations');
124
142
 
143
+ const stepSession = 'issue297-active-step-bindings';
144
+ const stepPayload = { session_id: stepSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
145
+ check(run(kernel, [], stepPayload).status === 0, 'Tier 2 route persists for active-step binding coverage');
146
+ const stepDir = getSessionDir(ROOT, stepSession, stepPayload);
147
+ const stepWorkflowFile = path.join(stepDir, 'workflow-run.json');
148
+ const stepObligationFile = path.join(stepDir, 'workflow-obligations.json');
149
+
150
+ console.log('\n=== Binding declaration validation ===');
151
+ let invalidBindingCase = 0;
152
+ for (const field of ['requiredBindings', 'optionalBindings']) {
153
+ for (const [valueName, value] of [
154
+ ['null', null],
155
+ ['object', { id: 'silently-dropped' }],
156
+ ['string', 'silently-dropped'],
157
+ ['boolean', false],
158
+ ['number', 0],
159
+ ]) {
160
+ for (const typed of [true, false]) {
161
+ invalidBindingCase++;
162
+ const sessionId = `issue303-invalid-binding-${invalidBindingCase}`;
163
+ const payload = { session_id: sessionId, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
164
+ const routed = run(kernel, [], payload);
165
+ check(routed.status === 0, `invalid ${field} ${valueName} fixture route persists (${typed ? 'typed' : 'untyped'})`);
166
+ const dir = getSessionDir(ROOT, sessionId, payload);
167
+ const workflowFile = path.join(dir, 'workflow-run.json');
168
+ const obligationFile = path.join(dir, 'workflow-obligations.json');
169
+ const workflowBefore = readJson(workflowFile);
170
+ const obligationsBefore = readJson(obligationFile);
171
+ const malformed = { id: 'req-review', summary: 'Review a change', acceptance: 'the change is reviewed', [field]: value };
172
+ const requirements = typed
173
+ ? [
174
+ { ...malformed, stepType: 'review' },
175
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify the change', acceptance: 'objective evidence is recorded' },
176
+ ]
177
+ : [malformed];
178
+ const result = run(controller, [
179
+ 'plan', '--session-id', sessionId, '--requirements-json', JSON.stringify(requirements),
180
+ '--strategy', 'iterative-single', '--evidence', 'validate binding declaration types',
181
+ ]);
182
+ check(result.status === 2, `plan exits 2 for ${field} ${valueName} (${typed ? 'typed' : 'untyped'} requirement)`);
183
+ check(result.stdout === '', `rejected ${field} ${valueName} plan writes no stdout (${typed ? 'typed' : 'untyped'})`);
184
+ check(new RegExp(field).test(result.stderr || ''), `rejection identifies ${field} for ${valueName} (${typed ? 'typed' : 'untyped'} requirement)`);
185
+ check(JSON.stringify(readJson(workflowFile)) === JSON.stringify(workflowBefore),
186
+ `rejected ${field} ${valueName} plan leaves workflow state unchanged (${typed ? 'typed' : 'untyped'})`);
187
+ check(JSON.stringify(readJson(obligationFile)) === JSON.stringify(obligationsBefore),
188
+ `rejected ${field} ${valueName} plan leaves obligation state unchanged (${typed ? 'typed' : 'untyped'})`);
189
+ }
190
+ }
191
+ }
192
+
193
+ const emptyBindingsSession = 'issue303-empty-binding-arrays';
194
+ const emptyBindingsPayload = { session_id: emptyBindingsSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
195
+ check(run(kernel, [], emptyBindingsPayload).status === 0, 'empty-array binding fixture route persists');
196
+ const emptyBindingsDir = getSessionDir(ROOT, emptyBindingsSession, emptyBindingsPayload);
197
+ const emptyBindingsRequirements = JSON.stringify([
198
+ { id: 'req-review-omitted', stepType: 'review', summary: 'Review a change', acceptance: 'the change is reviewed' },
199
+ { id: 'req-review-empty', stepType: 'review', summary: 'Review another change', acceptance: 'the second change is reviewed', requiredBindings: [], optionalBindings: [] },
200
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify both reviews', acceptance: 'objective evidence is recorded' },
201
+ ]);
202
+ const emptyBindingsPlan = run(controller, [
203
+ 'plan', '--session-id', emptyBindingsSession, '--requirements-json', emptyBindingsRequirements,
204
+ '--strategy', 'iterative-single', '--evidence', 'omitted and empty binding arrays are supported',
205
+ ]);
206
+ check(emptyBindingsPlan.status === 0, 'plan accepts omitted and explicit empty binding arrays');
207
+ const emptyBindingsState = readJson(path.join(emptyBindingsDir, 'workflow-obligations.json'));
208
+ check(emptyBindingsState.requirements[0].requiredBindings.length === 0 && emptyBindingsState.requirements[0].optionalBindings.length === 0,
209
+ 'omitted binding arrays remain empty after normalization');
210
+ check(emptyBindingsState.requirements[1].requiredBindings.length === 0 && emptyBindingsState.requirements[1].optionalBindings.length === 0,
211
+ 'explicit empty binding arrays remain empty after normalization');
212
+
213
+ const stepRequirements = JSON.stringify([
214
+ { id: 'req-review', stepType: 'review', summary: 'Inspect the existing checkout change', acceptance: 'the relevant diff has been reviewed', optionalBindings: ['security-review', 'review-guidance'] },
215
+ { id: 'req-change', stepType: 'behavior-change', summary: 'Correct the checkout behavior', acceptance: 'the reported behavior is corrected' },
216
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify the checkout change', acceptance: 'objective verification evidence is recorded' },
217
+ { id: 'req-commit', stepType: 'commit', summary: 'Commit the requested change', acceptance: 'the authorized commit is recorded' },
218
+ ]);
219
+ check(run(controller, [
220
+ 'plan', '--session-id', stepSession,
221
+ '--requirements-json', stepRequirements,
222
+ '--strategy', 'iterative-single',
223
+ '--evidence', 'the request contains review, behavior-change, verification, and commit steps',
224
+ ]).status === 0, 'normalized ordered steps are recorded in the existing requirements contract');
225
+ let stepState = readJson(stepObligationFile);
226
+ check(stepState.requirements.map(item => item.stepType || 'legacy').join(',') === 'review,behavior-change,verification,commit',
227
+ 'typed steps preserve request order');
228
+ check((stepState.requirements[1].requiredBindings || []).map(item => item.id).join(',') === 'tdd',
229
+ 'behavior-change step materializes required TDD binding');
230
+ check((stepState.requirements[2].requiredBindings || []).map(item => item.id).join(',') === 'verification-loop',
231
+ 'verification step owns its verification-loop binding');
232
+ check((stepState.requirements[3].requiredBindings || []).map(item => item.id).join(',') === 'git-commit',
233
+ 'commit step materializes required git-commit binding');
234
+ const stepsWithoutVerification = JSON.stringify([
235
+ { id: 'req-review-only', stepType: 'review', summary: 'Review the diff', acceptance: 'the diff is reviewed' },
236
+ ]);
237
+ check(run(controller, [
238
+ 'plan', '--session-id', stepSession, '--requirements-json', stepsWithoutVerification,
239
+ '--strategy', 'iterative-single', '--evidence', 'invalid fixture omits mandatory verification',
240
+ ]).status !== 0, 'typed-step plan cannot omit the mandatory verification step');
241
+
242
+ const stepStart = run(controller, ['start', '--session-id', stepSession]);
243
+ check(stepStart.status === 0, 'start accepts a confirmed typed-step workflow');
244
+ let stepResponse = responseOf(stepStart) || {};
245
+ check(stepResponse.activeStep?.id === 'req-review' && stepResponse.activeStep?.stepType === 'review',
246
+ 'start returns the first ordered step as active');
247
+ check(stepResponse.activeStepCommands?.resolveBinding.includes('--step-id "req-review"') &&
248
+ stepResponse.activeStepCommands?.passStep.includes('--step-id "req-review"'),
249
+ 'start returns concrete commands for resolving the active binding and passing the step');
250
+ check(stepResponse.activeStep?.requiredBindings?.length === 0 &&
251
+ stepResponse.activeStep?.optionalBindings?.map(item => item.id).join(',') === 'security-review,review-guidance',
252
+ 'review starts without TDD and surfaces only its optional binding');
253
+ check(stepResponse.activeStep?.optionalBindings?.[0]?.path === 'security-review/SKILL.md',
254
+ 'active optional binding includes its load path');
255
+ check(!JSON.stringify(stepResponse).includes('tdd') && !JSON.stringify(stepResponse).includes('git-commit'),
256
+ 'start does not disclose future-step bindings');
257
+ check(run(controller, [
258
+ 'binding', '--session-id', stepSession, '--step-id', 'req-commit', '--binding-id', 'git-commit',
259
+ '--disposition', 'loaded', '--evidence', 'premature future-step load attempt',
260
+ ]).status !== 0, 'future-step binding cannot be resolved before its step becomes active');
261
+ stepState = readJson(stepObligationFile);
262
+ check(stepState.activeStepId === 'req-review', 'active step is persisted in workflow-obligations state');
263
+ check(stepState.obligations.find(item => item.id === 'execute').suggestedSkills.length === 0 &&
264
+ stepState.obligations.find(item => item.id === 'verify').suggestedSkills.length === 0,
265
+ 'typed-step workflows do not reintroduce global suggested-skill pushes');
266
+ check(run(controller, [
267
+ 'step', '--session-id', stepSession, '--step-id', 'req-review', '--disposition', 'pass',
268
+ '--evidence', 'review finished without resolving optional bindings',
269
+ ]).status !== 0, 'active step cannot pass before surfaced optional bindings are resolved');
270
+
271
+ check(run(controller, [
272
+ 'binding', '--session-id', stepSession, '--step-id', 'req-review', '--binding-id', 'security-review',
273
+ '--disposition', 'loaded', '--evidence', 'loaded the security review checklist for this step',
274
+ ]).status === 0, 'active optional binding can be loaded with evidence');
275
+ check(run(controller, [
276
+ 'binding', '--session-id', stepSession, '--step-id', 'req-review', '--binding-id', 'review-guidance',
277
+ '--disposition', 'not-needed',
278
+ ]).status !== 0, 'optional binding cannot be marked not-needed without evidence');
279
+ check(run(controller, [
280
+ 'binding', '--session-id', stepSession, '--step-id', 'req-review', '--binding-id', 'review-guidance',
281
+ '--disposition', 'not-needed', '--evidence', 'the bounded diff review does not need the broader triage guide',
282
+ ]).status === 0, 'active optional binding can be marked not-needed with evidence');
283
+ const reviewAdvance = run(controller, [
284
+ 'step', '--session-id', stepSession, '--step-id', 'req-review', '--disposition', 'pass',
285
+ '--evidence', 'the relevant diff and acceptance criteria were reviewed',
286
+ ]);
287
+ const reviewResponse = responseOf(reviewAdvance) || {};
288
+ check(reviewAdvance.status === 0, 'review step can pass after its surfaced optional binding is resolved');
289
+ stepResponse = reviewResponse;
290
+ check(stepResponse.activeStep?.id === 'req-change' &&
291
+ stepResponse.activeStep?.requiredBindings?.map(item => item.id).join(',') === 'tdd',
292
+ 'review advance returns only the behavior-change step and its required TDD binding');
293
+ check(stepResponse.activeStep?.requiredBindings?.[0]?.path === 'tdd/SKILL.md' &&
294
+ !JSON.stringify(stepResponse).includes('verification-loop') && !JSON.stringify(stepResponse).includes('git-commit'),
295
+ 'behavior-change output includes its TDD path but keeps later bindings hidden');
296
+ check(run(controller, [
297
+ 'step', '--session-id', stepSession, '--step-id', 'req-change', '--disposition', 'pass',
298
+ '--evidence', 'behavior changed',
299
+ ]).status !== 0, 'behavior-change cannot pass before required TDD binding is resolved');
300
+ check(run(controller, [
301
+ 'binding', '--session-id', stepSession, '--step-id', 'req-change', '--binding-id', 'tdd',
302
+ '--disposition', 'loaded', '--evidence', 'read and followed the TDD core contract before changing behavior',
303
+ ]).status === 0, 'required TDD binding resolves with load evidence');
304
+ const changeAdvance = run(controller, [
305
+ 'step', '--session-id', stepSession, '--step-id', 'req-change', '--disposition', 'pass',
306
+ '--evidence', 'the checkout behavior and regression case are implemented',
307
+ ]);
308
+ const changeResponse = responseOf(changeAdvance) || {};
309
+ check(changeAdvance.status === 0 && changeResponse.activeStep?.id === 'req-verify',
310
+ 'resolved behavior-change advances to the mandatory verification step');
311
+ stepResponse = changeResponse;
312
+ check(stepResponse.activeStep?.requiredBindings?.map(item => item.id).join(',') === 'verification-loop' &&
313
+ !JSON.stringify(stepResponse).includes('git-commit'),
314
+ 'verification binding is disclosed only when verification becomes active');
315
+ check(run(controller, [
316
+ 'binding', '--session-id', stepSession, '--step-id', 'req-verify', '--binding-id', 'verification-loop',
317
+ '--disposition', 'loaded', '--evidence', 'read the verification-loop contract for this verification step',
318
+ ]).status === 0, 'verification binding resolves at its own step');
319
+ const verifyAdvance = run(controller, [
320
+ 'step', '--session-id', stepSession, '--step-id', 'req-verify', '--disposition', 'pass',
321
+ '--evidence', 'objective checks completed and their result is recorded',
322
+ ]);
323
+ const verifyResponse = responseOf(verifyAdvance) || {};
324
+ check(verifyAdvance.status === 0 && verifyResponse.activeStep?.id === 'req-commit',
325
+ 'verification step advances to commit only after its binding resolves');
326
+ stepResponse = verifyResponse;
327
+ check(stepResponse.activeStep?.requiredBindings?.map(item => item.id).join(',') === 'git-commit' &&
328
+ !JSON.stringify(stepResponse).includes('tdd') && !JSON.stringify(stepResponse).includes('verification-loop'),
329
+ 'commit binding is disclosed only when the commit step becomes active');
330
+ check(run(controller, [
331
+ 'binding', '--session-id', stepSession, '--step-id', 'req-commit', '--binding-id', 'git-commit',
332
+ '--disposition', 'loaded', '--evidence', 'read the commit authorization and staged-diff contract',
333
+ ]).status === 0, 'required git-commit binding resolves at the commit step');
334
+ const commitAdvance = run(controller, [
335
+ 'step', '--session-id', stepSession, '--step-id', 'req-commit', '--disposition', 'pass',
336
+ '--evidence', 'the explicitly requested commit was created and checked',
337
+ ]);
338
+ const commitResponse = responseOf(commitAdvance) || {};
339
+ check(commitAdvance.status === 0 && commitResponse.activeStep === null,
340
+ 'advancing the last step leaves no active-step bindings');
341
+ stepState = readJson(stepObligationFile);
342
+ check(stepState.obligations.find(item => item.id === 'execute').status === 'pass' &&
343
+ stepState.obligations.find(item => item.id === 'verify').status === 'pass',
344
+ 'completed typed steps resolve their aggregate execute and verify obligations with linked evidence');
345
+ const typedResolvedStop = run(stopGate, [], { session_id: stepSession, cwd: ROOT, hook_event_name: 'Stop' });
346
+ const typedWorkflow = readJson(stepWorkflowFile);
347
+ check(typedResolvedStop.status === 0 && typedWorkflow.state === 'satisfied',
348
+ 'all typed steps can satisfy the workflow without separate aggregate dispositions');
349
+ check((typedWorkflow.unresolved || []).length === 0, 'satisfied typed-step workflow has no unresolved obligations');
350
+
351
+ const verifyOnlySession = 'issue297-verification-only';
352
+ const verifyOnlyPayload = { session_id: verifyOnlySession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
353
+ check(run(kernel, [], verifyOnlyPayload).status === 0, 'verification-only workflow route persists');
354
+ const verifyOnlyObligationFile = path.join(getSessionDir(ROOT, verifyOnlySession, verifyOnlyPayload), 'workflow-obligations.json');
355
+ const verifyOnlyRequirements = JSON.stringify([
356
+ { id: 'req-verify-only', stepType: 'verification', summary: 'Verify existing behavior', acceptance: 'objective checks pass' },
357
+ ]);
358
+ check(run(controller, [
359
+ 'plan', '--session-id', verifyOnlySession, '--requirements-json', verifyOnlyRequirements,
360
+ '--strategy', 'iterative-single', '--evidence', 'the request verifies existing behavior without implementation work',
361
+ ]).status === 0, 'verification-only typed plan is accepted');
362
+ const verifyOnlyStart = run(controller, ['start', '--session-id', verifyOnlySession]);
363
+ check(verifyOnlyStart.status === 0 && responseOf(verifyOnlyStart)?.activeStep?.id === 'req-verify-only',
364
+ 'verification-only typed plan starts at its verification step');
365
+ let verifyOnlyState = readJson(verifyOnlyObligationFile);
366
+ check(verifyOnlyState.obligations.find(item => item.id === 'execute')?.status === 'pass' &&
367
+ verifyOnlyState.obligations.find(item => item.id === 'execute')?.evidence.includes('verification-only workflow'),
368
+ 'empty typed execution group resolves as explicit verification-only evidence');
369
+ check(run(controller, [
370
+ 'binding', '--session-id', verifyOnlySession, '--step-id', 'req-verify-only', '--binding-id', 'verification-loop',
371
+ '--disposition', 'loaded', '--evidence', 'loaded and followed the verification binding for the active step',
372
+ ]).status === 0, 'verification-only step resolves its required binding');
373
+ check(run(controller, [
374
+ 'step', '--session-id', verifyOnlySession, '--step-id', 'req-verify-only', '--disposition', 'pass',
375
+ '--evidence', 'the existing behavior passed its objective verification checks',
376
+ ]).status === 0, 'verification-only step passes with objective evidence');
377
+ const verifyOnlyStop = run(stopGate, [], { session_id: verifyOnlySession, cwd: ROOT, hook_event_name: 'Stop' });
378
+ const verifyOnlyWorkflow = readJson(path.join(getSessionDir(ROOT, verifyOnlySession, verifyOnlyPayload), 'workflow-run.json'));
379
+ verifyOnlyState = readJson(verifyOnlyObligationFile);
380
+ check(verifyOnlyStop.status === 0 && verifyOnlyWorkflow.state === 'satisfied',
381
+ 'verification-only workflow can satisfy without an execution step');
382
+ check(verifyOnlyState.obligations.find(item => item.id === 'verify')?.status === 'pass',
383
+ 'verification-only completion also resolves the aggregate verify obligation');
384
+
385
+ const unknownSession = 'issue297-unknown-binding';
386
+ const unknownPayload = { session_id: unknownSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
387
+ check(run(kernel, [], unknownPayload).status === 0, 'route persists for unknown-required-binding coverage');
388
+ const unknownDir = getSessionDir(ROOT, unknownSession, unknownPayload);
389
+ const unknownRequirements = JSON.stringify([
390
+ { id: 'req-unknown-review', stepType: 'review', summary: 'Review the unfamiliar domain change', acceptance: 'the unfamiliar behavior is assessed', requiredBindings: ['unregistered-review-skill'] },
391
+ { id: 'req-unknown-verify', stepType: 'verification', summary: 'Verify the reviewed change', acceptance: 'verification evidence exists' },
392
+ ]);
393
+ check(run(controller, [
394
+ 'plan', '--session-id', unknownSession, '--requirements-json', unknownRequirements,
395
+ '--strategy', 'iterative-single', '--evidence', 'a review-only capability was requested explicitly',
396
+ ]).status === 0, 'unknown required binding is accepted into the auditable contract');
397
+ const unknownStart = run(controller, ['start', '--session-id', unknownSession]);
398
+ check(unknownStart.status === 0, 'start surfaces rather than silently discarding an unknown required binding');
399
+ stepResponse = responseOf(unknownStart) || {};
400
+ check(stepResponse.activeStep?.requiredBindings?.[0]?.id === 'unregistered-review-skill' &&
401
+ stepResponse.activeStep?.requiredBindings?.[0]?.path === null &&
402
+ stepResponse.activeStep?.requiredBindings?.[0]?.status === 'pending',
403
+ 'unknown required binding remains visible as pending/unavailable');
404
+ check(run(controller, [
405
+ 'binding', '--session-id', unknownSession, '--step-id', 'req-unknown-review', '--binding-id', 'unregistered-review-skill',
406
+ '--disposition', 'loaded', '--evidence', 'attempted to load unknown skill',
407
+ ]).status !== 0, 'unknown required binding cannot be falsely marked loaded');
408
+ check(run(controller, [
409
+ 'step', '--session-id', unknownSession, '--step-id', 'req-unknown-review', '--disposition', 'pass',
410
+ '--evidence', 'review finished',
411
+ ]).status !== 0, 'step with unknown required binding cannot pass');
412
+ const unknownMutationReminder = run(workflowGate, [], {
413
+ session_id: unknownSession, cwd: ROOT, hook_event_name: 'PreToolUse',
414
+ tool_name: 'Write', tool_input: { file_path: path.join(ROOT, 'tmp-unknown-binding.txt'), content: 'x' },
415
+ });
416
+ check(unknownMutationReminder.status === 0, 'unknown required binding does not hard-block ordinary tools');
417
+ const unknownStop = run(stopGate, [], { session_id: unknownSession, cwd: ROOT, hook_event_name: 'Stop' });
418
+ const unknownWorkflow = readJson(path.join(unknownDir, 'workflow-run.json'));
419
+ check(unknownStop.status === 0 && unknownWorkflow.state === 'running',
420
+ 'missing binding keeps Stop fail-open while workflow remains unresolved');
421
+ check((unknownWorkflow.unresolved || []).some(item => item.includes('unregistered-review-skill')),
422
+ 'Stop reports the unresolved unknown binding');
423
+
424
+ const referenceSession = 'issue297-custom-reference-binding';
425
+ const referencePayload = { session_id: referenceSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
426
+ check(run(kernel, [], referencePayload).status === 0, 'custom-reference workflow route persists');
427
+ const referenceRequirements = JSON.stringify([
428
+ { id: 'req-reference-review', stepType: 'review', summary: 'Review the domain-specific behavior', acceptance: 'the domain rule is checked', requiredBindings: [{ id: 'domain-reference', path: 'docs\\philosophy.md' }] },
429
+ { id: 'req-reference-verify', stepType: 'verification', summary: 'Verify the domain change', acceptance: 'objective check evidence exists' },
430
+ ]);
431
+ check(run(controller, [
432
+ 'plan', '--session-id', referenceSession, '--requirements-json', referenceRequirements,
433
+ '--strategy', 'iterative-single', '--evidence', 'the review step requires one explicit domain reference',
434
+ ]).status === 0, 'step contracts accept an explicit skill/reference path');
435
+ const referenceStart = run(controller, ['start', '--session-id', referenceSession]);
436
+ stepResponse = responseOf(referenceStart) || {};
437
+ check(referenceStart.status === 0 && stepResponse.activeStep?.requiredBindings?.[0]?.path === 'docs/philosophy.md',
438
+ 'custom reference path is normalized and disclosed only at its active step');
439
+ check(run(controller, [
440
+ 'binding', '--session-id', referenceSession, '--step-id', 'req-reference-review', '--binding-id', 'domain-reference',
441
+ '--disposition', 'loaded', '--evidence', 'read docs/philosophy.md before completing the review',
442
+ ]).status === 0, 'explicit reference binding can resolve with evidence');
443
+ check(run(controller, [
444
+ 'step', '--session-id', referenceSession, '--step-id', 'req-reference-review', '--disposition', 'pass',
445
+ '--evidence', 'the relevant domain rule was applied in the review',
446
+ ]).status === 0, 'active step can pass after its explicit reference binding is resolved');
447
+ const unsafeReferenceRequirements = JSON.stringify([
448
+ { id: 'req-unsafe-reference', stepType: 'review', summary: 'Review a rule', acceptance: 'the rule is checked', requiredBindings: [{ id: 'unsafe-reference', path: '../private.md' }] },
449
+ { id: 'req-unsafe-verify', stepType: 'verification', summary: 'Verify the review', acceptance: 'objective check evidence exists' },
450
+ ]);
451
+ const unsafeReferenceSession = 'issue297-unsafe-reference-binding';
452
+ check(run(kernel, [], { session_id: unsafeReferenceSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' }).status === 0,
453
+ 'unsafe-reference workflow route persists');
454
+ check(run(controller, [
455
+ 'plan', '--session-id', unsafeReferenceSession, '--requirements-json', unsafeReferenceRequirements,
456
+ '--strategy', 'iterative-single', '--evidence', 'unsafe traversal path should be rejected',
457
+ ]).status !== 0, 'step bindings reject paths that traverse outside repository scope');
458
+ const driveReferenceSession = 'issue297-drive-reference-binding';
459
+ check(run(kernel, [], { session_id: driveReferenceSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' }).status === 0,
460
+ 'drive-reference workflow route persists');
461
+ const driveReferenceRequirements = JSON.stringify([
462
+ { id: 'req-drive-reference', stepType: 'review', summary: 'Review a rule', acceptance: 'the rule is checked', requiredBindings: [{ id: 'drive-reference', path: 'C:outside.md' }] },
463
+ { id: 'req-drive-verify', stepType: 'verification', summary: 'Verify the review', acceptance: 'objective check evidence exists' },
464
+ ]);
465
+ check(run(controller, [
466
+ 'plan', '--session-id', driveReferenceSession, '--requirements-json', driveReferenceRequirements,
467
+ '--strategy', 'iterative-single', '--evidence', 'drive-relative path should be rejected',
468
+ ]).status !== 0, 'step bindings reject Windows drive-relative paths');
469
+
125
470
  const mismatchSession = 'issue85-replan';
126
471
  const mismatchPayload = { session_id: mismatchSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
127
472
  check(run(kernel, [], mismatchPayload).status === 0, 'second Tier 2 route persists for replan discriminator');
128
473
  const mismatchDir = getSessionDir(ROOT, mismatchSession, mismatchPayload);
474
+ const initialSignals = readJson(path.join(mismatchDir, 'workflow-run.json')).workflowPlan.knowledgeSignals || [];
475
+ check(initialSignals.includes('tdd-test'), 'initial route records normalized knowledge signals');
129
476
  const mismatchPlan = run(controller, [
130
477
  'plan', '--session-id', mismatchSession,
131
478
  '--requirements-json', requirements,
@@ -140,6 +487,8 @@ check(mismatchObligations.workflowSelection.requestedStrategy === 'fable-staged'
140
487
  mismatchObligations.workflowSelection.confirmedStrategy === 'fable-staged',
141
488
  'requested and router-confirmed post-decomposition strategy are preserved');
142
489
  check(mismatchWorkflow.pendingPlan?.strategy === 'fable-staged', 'recomposed plan becomes the pending execution contract');
490
+ check(JSON.stringify(mismatchWorkflow.pendingPlan?.knowledgeSignals) === JSON.stringify(initialSignals),
491
+ 'strategy recomposition preserves the original knowledge signals');
143
492
  const mismatchStop = run(stopGate, [], { session_id: mismatchSession, cwd: ROOT, hook_event_name: 'Stop' });
144
493
  const mismatchAfterStop = readJson(path.join(mismatchDir, 'workflow-run.json'));
145
494
  check(mismatchStop.status === 0 && mismatchAfterStop.state === 'pending' &&