harness-everything 0.30.9 → 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 (150) 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 +34 -0
  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/live-host/codex-2026-10-13/README.md +55 -0
  9. package/benchmarks/results/live-host/codex-2026-10-13/observation-records.json +88 -0
  10. package/benchmarks/results/pr-303-review-2026-10-13/README.md +81 -0
  11. package/benchmarks/results/pr-303-review-2026-10-13/ci/run.json +1330 -0
  12. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-clean-env.log +1 -0
  13. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/mechanism-35-inherited-flag.log +34 -0
  14. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-captured.json +46 -0
  15. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate-diagnosis.md +13 -0
  16. package/benchmarks/results/pr-303-review-2026-10-13/diagnostics/verify-gate.json +46 -0
  17. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/summary.json +57 -0
  18. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-collision.log +8 -0
  19. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-consistency.log +624 -0
  20. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-docs-capabilities.log +131 -0
  21. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-platform-compatibility.log +9 -0
  22. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-openai.log +6 -0
  23. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-plugin-submission.log +5 -0
  24. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-references.log +6 -0
  25. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-release.log +73 -0
  26. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-repo-contract.log +5 -0
  27. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-invariants.log +785 -0
  28. package/benchmarks/results/pr-303-review-2026-10-13/gates/contracts/test-routing-skills.log +44 -0
  29. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/summary.json +12 -0
  30. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-consistency.log +624 -0
  31. package/benchmarks/results/pr-303-review-2026-10-13/gates/final-fast/test-release.log +73 -0
  32. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/summary.json +79 -0
  33. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mechanism.log +1737 -0
  34. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test-mutations.log +16 -0
  35. package/benchmarks/results/pr-303-review-2026-10-13/gates/heavy/test.log +3297 -0
  36. package/benchmarks/results/pr-303-review-2026-10-13/manifest.json +319 -0
  37. package/benchmarks/results/pr-303-review-2026-10-13/regression/binding-resolution-green.log +36 -0
  38. package/benchmarks/results/pr-303-review-2026-10-13/regression/determinism.json +174 -0
  39. package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-evidence.json +314 -0
  40. package/benchmarks/results/pr-303-review-2026-10-13/regression/quality-report.json +638 -0
  41. package/benchmarks/results/pr-303-review-2026-10-13/regression/red.log +240 -0
  42. package/benchmarks/results/pr-303-review-2026-10-13/regression/untyped-fable-probe.json +22 -0
  43. package/benchmarks/results/pr-303-review-2026-10-13/regression/workflow-obligations-green.log +254 -0
  44. package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-clean-env-fixed.log +1 -0
  45. package/benchmarks/results/pr-303-review-2026-10-13/verification/mechanism-35-inherited-flag-fixed.log +1 -0
  46. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.meta.json +8 -0
  47. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stderr.log +90 -0
  48. package/benchmarks/results/pr-303-review-2026-10-13/verification/nested-test.stdout.log +3186 -0
  49. package/benchmarks/results/pr-303-review-2026-10-13/verification/verify-gate-final.json +46 -0
  50. package/bin/cli.js +1 -1
  51. package/ci/doc-capability-consistency.test.js +11 -1
  52. package/ci/invariant-routing.test.js +7 -4
  53. package/ci/mechanism-2ag-router-worktree-contract.test.js +2 -2
  54. package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +2 -1
  55. package/ci/mechanism-2al-codex-hooks-compat.test.js +19 -0
  56. package/ci/mechanism-2am-observations-session-import.test.js +221 -0
  57. package/ci/mechanism-2an-workflow-obligations.test.js +352 -3
  58. package/ci/mechanism-2ao-plugin-sync.test.js +129 -4
  59. package/ci/mechanism-2aq-fable-stage-bindings.test.js +168 -0
  60. package/ci/mechanism-2ar-binding-resolution.test.js +211 -0
  61. package/ci/mechanism-2w-platform-compatibility.test.js +12 -0
  62. package/ci/mechanism-34-observations.test.js +64 -0
  63. package/ci/mechanism-35-verification-contract.test.js +1 -1
  64. package/ci/mutation-check.js +2 -0
  65. package/ci/mutation-loader.js +4 -0
  66. package/ci/openai-plugin-package.test.js +7 -5
  67. package/ci/prompt-contract-regressions.test.js +7 -6
  68. package/ci/router-workflow-plan.test.js +109 -14
  69. package/ci/skill-routing-check.js +44 -15
  70. package/ci/workflow-plan-consumers.test.js +41 -1
  71. package/docs/architecture.md +10 -10
  72. package/docs/enforcement-locks.md +1 -1
  73. package/docs/mechanism-first-skill-mesh.md +12 -11
  74. package/docs/openai-plugin.md +1 -1
  75. package/docs/philosophy.md +7 -6
  76. package/docs/platform-capabilities.md +14 -12
  77. package/docs/platform-compatibility.json +5 -5
  78. package/docs/routing.md +16 -17
  79. package/docs/system-one-observations.md +36 -9
  80. package/docs/system-one-routing.md +10 -7
  81. package/docs/system-one-suggestion-gates.md +5 -5
  82. package/docs/workflow-runtime.md +6 -2
  83. package/docs/workflows/harness-everything.md +14 -14
  84. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  85. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  86. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  87. package/fable-mode/CONTRACT-FORMAT.md +4 -1
  88. package/fable-mode/SKILL.md +6 -8
  89. package/fable-mode/agents/fable-orchestrator.md +10 -3
  90. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  91. package/fable-mode/fable-haiku/SKILL.md +1 -1
  92. package/fable-mode/fable-opus/SKILL.md +1 -1
  93. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  94. package/fable-mode/scripts/workflow-plan-consumer.js +101 -2
  95. package/harness-everything/SKILL.md +8 -7
  96. package/harness-everything/references/router-workflow-plan.md +11 -6
  97. package/harness-everything/references/triage-and-tiers.md +21 -23
  98. package/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
  99. package/harness-everything/scripts/kernel-router-core.js +52 -55
  100. package/harness-everything/scripts/kernel-router.js +48 -5
  101. package/harness-everything/scripts/router-contract-core.js +7 -7
  102. package/harness-everything/scripts/router-contract.js +1 -3
  103. package/harness-everything/scripts/routing-keywords.json +417 -121
  104. package/harness-everything/scripts/tier-router.js +64 -113
  105. package/hooks/scripts/contract-test.js +29 -3
  106. package/hooks/scripts/lib/binding-resolver.js +76 -0
  107. package/hooks/scripts/lib/fable-contracts.js +1 -1
  108. package/hooks/scripts/lib/observations.js +88 -12
  109. package/hooks/scripts/lib/workflow-obligations.js +292 -7
  110. package/hooks/scripts/lib/workflow-runtime.js +8 -1
  111. package/hooks/scripts/workflow-disposition.js +116 -2
  112. package/opencode-plugin/plugin.json +1 -1
  113. package/package.json +1 -1
  114. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  115. package/plugins/harness-everything/hooks/scripts/contract-test.js +29 -3
  116. package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +76 -0
  117. package/plugins/harness-everything/hooks/scripts/lib/fable-contracts.js +1 -1
  118. package/plugins/harness-everything/hooks/scripts/lib/observations.js +88 -12
  119. package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +292 -7
  120. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +8 -1
  121. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +116 -2
  122. package/plugins/harness-everything/plugin.json +1 -1
  123. package/plugins/harness-everything/scripts/lib/advisory-text.js +2 -2
  124. package/plugins/harness-everything/scripts/lib/generated-skills.js +78 -0
  125. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +4 -1
  126. package/plugins/harness-everything/skills/fable-mode/SKILL.md +6 -8
  127. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +10 -3
  128. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  129. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  130. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  131. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  132. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +101 -2
  133. package/plugins/harness-everything/skills/harness-everything/SKILL.md +8 -7
  134. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +11 -6
  135. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +21 -23
  136. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +2 -0
  137. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +52 -55
  138. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +48 -5
  139. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +7 -7
  140. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +1 -3
  141. package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +417 -121
  142. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +64 -113
  143. package/sbom.cdx.json +7 -7
  144. package/scripts/codex-hooks-compat.js +39 -14
  145. package/scripts/lib/advisory-text.js +2 -2
  146. package/scripts/lib/generated-skills.js +78 -0
  147. package/scripts/plugin-sync.js +100 -29
  148. package/scripts/system-one-observations-export.js +29 -2
  149. package/scripts/system-one-observations-import-codex-sessions.js +307 -0
  150. package/scripts/system-one-observations-review.js +11 -1
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const ROOT = path.resolve(__dirname, '..');
11
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-fable-stage-bindings-'));
12
+ const stateHome = path.join(tempRoot, 'state-home');
13
+ const workspace = path.join(tempRoot, 'workspace');
14
+ fs.mkdirSync(path.join(workspace, '.git'), { recursive: true });
15
+ fs.mkdirSync(path.join(workspace, 'tdd'), { recursive: true });
16
+ fs.mkdirSync(path.join(workspace, 'docs'), { recursive: true });
17
+ fs.writeFileSync(path.join(workspace, 'tdd', 'SKILL.md'), '# TDD\n');
18
+ fs.writeFileSync(path.join(workspace, 'docs', 'architecture.md'), '# Architecture\n');
19
+ process.env.HARNESS_STATE_HOME = stateHome;
20
+
21
+ const { prepareRun } = require(path.join(ROOT, 'fable-mode', 'scripts', 'workflow-plan-consumer.js'));
22
+ const { getSessionDir } = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'harness-state.js'));
23
+ const controller = path.join(ROOT, 'hooks', 'scripts', 'workflow-disposition.js');
24
+ const contractTest = path.join(ROOT, 'hooks', 'scripts', 'contract-test.js');
25
+ const stopGate = path.join(ROOT, 'hooks', 'scripts', 'workflow-stop-gate.js');
26
+ const workflowGate = path.join(ROOT, 'hooks', 'scripts', 'workflow-gate.js');
27
+ const kernelRouter = path.join(ROOT, 'harness-everything', 'scripts', 'kernel-router.js');
28
+ const sessionId = 'issue297-fable-bindings';
29
+ const workflowId = 'workflow-issue297-fable';
30
+ const plan = { workflowPlan: { strategySelection: 'selected', strategy: 'fable-staged', tier: 'tier3' } };
31
+ const run = prepareRun({
32
+ routerContract: plan,
33
+ workspaceRoot: workspace,
34
+ runId: 'issue297-stage-bindings',
35
+ sessionId,
36
+ workflowId,
37
+ stages: [{
38
+ stageId: 'implement', goal: 'implement a bounded change', agent: 'fable-worker-sonnet', task: 'use declared guidance and verify',
39
+ dependsOn: [], writeSet: ['src/change.js'], inputs: [], expectedOutputs: ['src/change.js'],
40
+ requiredBindings: [{ id: 'tdd', path: 'tdd/SKILL.md' }],
41
+ optionalBindings: [{ id: 'architecture-reference', path: 'docs/architecture.md' }],
42
+ checkCommand: 'node fixture-check.js', passCondition: 'exit 0',
43
+ }, {
44
+ stageId: 'verify', goal: 'verify the implementation', agent: 'fable-verifier', task: 'independently check the implementation',
45
+ dependsOn: ['implement'], writeSet: [], inputs: ['src/change.js'], expectedOutputs: ['verification evidence'],
46
+ requiredBindings: [{ id: 'verification-loop', path: 'verification-loop/SKILL.md' }],
47
+ checkCommand: 'node verifier-check.js', passCondition: 'exit 0',
48
+ }],
49
+ });
50
+
51
+ const sessionDir = getSessionDir(workspace, sessionId);
52
+ fs.writeFileSync(path.join(sessionDir, 'workflow-run.json'), JSON.stringify({
53
+ schemaVersion: 2, sessionId, workflowId, state: 'running', strategy: 'fable-staged', tier: 'tier3', revision: 1,
54
+ workflowPlan: { strategySelection: 'selected', strategy: 'fable-staged', tier: 'tier3', verification: { independent: false } },
55
+ runId: run.runId, escapes: [],
56
+ }, null, 2));
57
+
58
+ function runNode(script, args, payload, cwd = workspace) {
59
+ return spawnSync(process.execPath, [script, ...(args || [])], {
60
+ cwd,
61
+ encoding: 'utf8',
62
+ input: payload ? JSON.stringify(payload) : undefined,
63
+ env: { ...process.env, HARNESS_STATE_HOME: stateHome },
64
+ });
65
+ }
66
+ function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
67
+ function check(condition, message) {
68
+ if (!condition) throw new Error('FAIL ' + message);
69
+ console.log(' PASS ' + message);
70
+ }
71
+ function runCheck() {
72
+ return runNode(contractTest, [], {
73
+ tool_name: 'Bash', session_id: sessionId, cwd: workspace, worker_id: 'worker-1',
74
+ tool_input: { command: 'node fixture-check.js' },
75
+ tool_response: { stdout: 'fixture check passed', exitCode: 0 },
76
+ });
77
+ }
78
+
79
+ try {
80
+ console.log('=== #297 Fable active-stage bindings ===');
81
+ const firstCheck = runCheck();
82
+ check(firstCheck.status === 0, 'post-tool check hook remains fail-open');
83
+ const contractFile = path.join(run.runRoot, 'contracts', 'implement.json');
84
+ let contract = readJson(contractFile);
85
+ check(contract.status === 'binding-unresolved' && contract.unresolvedBindings.join(',') === 'tdd,architecture-reference',
86
+ 'a successful check does not pass a stage while its declared bindings remain unresolved');
87
+ const firstEvidence = readJson(path.join(run.runRoot, 'evidence', 'implement.json'));
88
+ check(firstEvidence.checkStatus === 'pass' && firstEvidence.status === 'binding-unresolved',
89
+ 'check success and unresolved binding status are recorded separately');
90
+ const notification = runNode(kernelRouter, [], {
91
+ hook_event_name: 'UserPromptSubmit', session_id: sessionId, cwd: workspace,
92
+ prompt: '<task-notification><task-id>issue297-stage-reminder</task-id><status>completed</status></task-notification>',
93
+ });
94
+ check(notification.status === 0 && notification.stdout.includes('Active Fable stage: implement'), 'retained Fable notification shows the current stage');
95
+ check(notification.stdout.includes('tdd/SKILL.md') && notification.stdout.includes('docs/architecture.md'), 'retained Fable notification shows only current-stage bindings');
96
+ check(!notification.stdout.includes('verification-loop/SKILL.md') && !notification.stdout.includes('ROUTER WORKFLOW PLAN (JSON)'),
97
+ 'retained Fable notification hides future bindings and avoids repeating the full plan');
98
+
99
+ const ordinaryTool = runNode(workflowGate, [], {
100
+ tool_name: 'Write', session_id: sessionId, cwd: workspace,
101
+ tool_input: { file_path: path.join(workspace, 'src', 'change.js'), content: 'change' },
102
+ });
103
+ check(ordinaryTool.status === 0, 'unresolved Fable bindings do not hard-block ordinary mutation tools');
104
+ const stop = runNode(stopGate, [], { hook_event_name: 'Stop', session_id: sessionId, cwd: workspace });
105
+ check(stop.status === 0, 'unresolved Fable bindings do not hard-block Stop');
106
+ const workflow = readJson(path.join(sessionDir, 'workflow-run.json'));
107
+ check(workflow.state === 'running' && workflow.unresolved.some(value => value.startsWith('implement:binding-unresolved:')),
108
+ 'Stop exposes the binding gap without locking the workflow');
109
+
110
+ const loaded = runNode(controller, [
111
+ 'stage-binding', '--session-id', sessionId, '--stage-id', 'implement', '--binding-id', 'tdd',
112
+ '--disposition', 'loaded', '--evidence', 'Read tdd/SKILL.md before implementation.',
113
+ ]);
114
+ check(loaded.status === 0 && JSON.parse(loaded.stdout).binding.status === 'loaded', 'active required stage binding can be explicitly resolved');
115
+ contract = readJson(contractFile);
116
+ check(contract.status === 'binding-unresolved', 'resolving one binding does not pass the stage while an optional binding is undecided');
117
+
118
+ const notNeeded = runNode(controller, [
119
+ 'stage-binding', '--session-id', sessionId, '--stage-id', 'implement', '--binding-id', 'architecture-reference',
120
+ '--disposition', 'not-needed', '--evidence', 'The implementation does not touch documented architecture.',
121
+ ]);
122
+ check(notNeeded.status === 0 && JSON.parse(notNeeded.stdout).binding.status === 'not-needed', 'optional binding can be explicitly declined with evidence');
123
+ const finalCheck = runCheck();
124
+ check(finalCheck.status === 0, 'stage check remains callable after binding resolution');
125
+ check(finalCheck.stdout.includes('verification-loop/SKILL.md'), 'successful stage check discloses the newly dependency-ready verifier binding');
126
+ contract = readJson(contractFile);
127
+ check(contract.status === 'pass' && contract.unresolvedBindings.length === 0, 'stage passes only after check and binding dispositions both resolve');
128
+ const finalEvidence = readJson(path.join(run.runRoot, 'evidence', 'implement.json'));
129
+ check(finalEvidence.status === 'pass' && finalEvidence.exitCode === 0, 'passing stage retains correlated check evidence');
130
+ const implementContract = readJson(contractFile);
131
+ const sameFile = (left, right) => fs.realpathSync(left).toLowerCase() === fs.realpathSync(right).toLowerCase();
132
+ check(implementContract.requiredBindings[0].availability === 'available' &&
133
+ sameFile(implementContract.requiredBindings[0].resolvedPath, path.join(ROOT, 'tdd', 'SKILL.md')),
134
+ 'packaged skill binding resolves to the installed Harness skill at run preparation');
135
+ check(implementContract.optionalBindings[0].availability === 'available' &&
136
+ sameFile(implementContract.optionalBindings[0].resolvedPath, path.join(workspace, 'docs', 'architecture.md')),
137
+ 'workspace reference binding resolves to the workspace file, not the Harness copy');
138
+
139
+ const ghostSession = 'pr303-fable-missing-binding';
140
+ const ghostRun = prepareRun({
141
+ routerContract: plan, workspaceRoot: workspace, runId: 'pr303-missing-stage-binding', sessionId: ghostSession, workflowId,
142
+ stages: [{
143
+ stageId: 'ghost', goal: 'use a missing reference', agent: 'fable-worker-sonnet', task: 'load a reference that does not exist',
144
+ dependsOn: [], writeSet: ['src/ghost.js'], inputs: [], expectedOutputs: ['src/ghost.js'],
145
+ requiredBindings: [{ id: 'ghost-reference', path: 'docs/pr303-ghost.md' }],
146
+ checkCommand: 'node ghost-check.js', passCondition: 'exit 0',
147
+ }],
148
+ });
149
+ const ghostContractFile = path.join(ghostRun.runRoot, 'contracts', 'ghost.json');
150
+ check(readJson(ghostContractFile).requiredBindings[0].availability === 'missing',
151
+ 'a declared stage path with no file is recorded as missing, not available');
152
+ fs.writeFileSync(path.join(getSessionDir(workspace, ghostSession), 'workflow-run.json'), JSON.stringify({
153
+ schemaVersion: 2, sessionId: ghostSession, workflowId, state: 'running', strategy: 'fable-staged', tier: 'tier3', revision: 1,
154
+ workflowPlan: { strategySelection: 'selected', strategy: 'fable-staged', tier: 'tier3', verification: { independent: false } },
155
+ runId: ghostRun.runId, escapes: [],
156
+ }, null, 2));
157
+ const ghostLoad = runNode(controller, [
158
+ 'stage-binding', '--session-id', ghostSession, '--stage-id', 'ghost', '--binding-id', 'ghost-reference',
159
+ '--disposition', 'loaded', '--evidence', 'claimed to read the ghost reference',
160
+ ]);
161
+ const ghostBinding = readJson(ghostContractFile).requiredBindings[0];
162
+ check(ghostLoad.status === 0 && ghostBinding.status === 'unavailable' && ghostBinding.reasonCode === 'binding-path-missing',
163
+ 'a missing stage binding cannot be recorded as loaded');
164
+ console.log('PASS: #297 Fable stage binding gate mechanics');
165
+ } finally {
166
+ if (!tempRoot.startsWith(path.join(os.tmpdir(), 'harness-fable-stage-bindings-'))) throw new Error('unsafe fixture cleanup');
167
+ fs.rmSync(tempRoot, { recursive: true, force: true });
168
+ }
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // PR #303 review: binding availability must come from a real readable file
5
+ // under the workspace or an installed skill root, and self-evolved skills stay
6
+ // discoverable as metadata-only ids until their declaring step is active.
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ const ROOT = path.resolve(__dirname, '..');
13
+ const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-binding-resolution-'));
14
+ const fakeHome = path.join(stateHome, 'home');
15
+ fs.mkdirSync(fakeHome, { recursive: true });
16
+ delete process.env.CLAUDE_PLUGIN_ROOT;
17
+ delete process.env.PLUGIN_ROOT;
18
+ process.env.HARNESS_STATE_HOME = stateHome;
19
+ process.env.HOME = fakeHome;
20
+ process.env.USERPROFILE = fakeHome;
21
+
22
+ const kernel = path.join(ROOT, 'harness-everything/scripts/kernel-router.js');
23
+ const tierRouter = path.join(ROOT, 'harness-everything/scripts/tier-router.js');
24
+ const controller = path.join(ROOT, 'hooks/scripts/workflow-disposition.js');
25
+
26
+ let failed = 0;
27
+ function check(condition, message) {
28
+ if (condition) console.log(' PASS ' + message);
29
+ else { console.error(' FAIL ' + message); failed++; }
30
+ }
31
+ function run(script, args, workspace, input = null) {
32
+ return spawnSync(process.execPath, [script, ...args], {
33
+ cwd: workspace,
34
+ encoding: 'utf8',
35
+ input: input === null ? undefined : JSON.stringify(input),
36
+ env: { ...process.env, HARNESS_WORKSPACE_ROOT: workspace },
37
+ });
38
+ }
39
+ function responseOf(result) {
40
+ try { return JSON.parse(result.stdout || ''); } catch (_) { return null; }
41
+ }
42
+ function sessionFile(workspace, sessionId, payload, name) {
43
+ const { getSessionDir } = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js'));
44
+ return path.join(getSessionDir(workspace, sessionId, payload), name);
45
+ }
46
+ function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
47
+ function samePath(left, right) {
48
+ if (!left || !right) return false;
49
+ const real = value => { try { return fs.realpathSync(value); } catch (_) { return path.resolve(value); } };
50
+ return real(left).toLowerCase() === real(right).toLowerCase();
51
+ }
52
+
53
+ console.log('=== PR #303 binding resolution ===');
54
+
55
+ // 1. Declared paths: syntax alone is not availability.
56
+ const missingSession = 'pr303-missing-binding';
57
+ const missingPayload = { session_id: missingSession, cwd: ROOT, prompt: 'Fix this checkout bug and add a regression test.' };
58
+ check(run(kernel, [], ROOT, missingPayload).status === 0, 'Tier 2 route persists for declared-path coverage');
59
+ const missingRequirements = JSON.stringify([
60
+ {
61
+ id: 'req-change', stepType: 'behavior-change', summary: 'Correct the checkout behavior', acceptance: 'the behavior is corrected',
62
+ requiredBindings: [{ id: 'domain-reference', path: 'docs/pr303-missing-reference.md' }],
63
+ optionalBindings: [{ id: 'routing-reference', path: 'docs/routing.md' }],
64
+ },
65
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify the change', acceptance: 'verification evidence is recorded' },
66
+ ]);
67
+ check(run(controller, [
68
+ 'plan', '--session-id', missingSession, '--requirements-json', missingRequirements,
69
+ '--strategy', 'iterative-single', '--evidence', 'behavior change then verification',
70
+ ], ROOT).status === 0, 'plan with a missing declared path is still recorded');
71
+ const obligationsFile = sessionFile(ROOT, missingSession, missingPayload, 'workflow-obligations.json');
72
+ let state = readJson(obligationsFile);
73
+ const missingBinding = state.requirements[0].requiredBindings.find(item => item.id === 'domain-reference');
74
+ const presentBinding = state.requirements[0].optionalBindings.find(item => item.id === 'routing-reference');
75
+ check(missingBinding.availability === 'missing' && missingBinding.resolvedPath === null,
76
+ 'a declared path with no file is recorded as missing, not available');
77
+ check(presentBinding.availability === 'available' && samePath(presentBinding.resolvedPath, path.join(ROOT, 'docs/routing.md')),
78
+ 'a declared path that exists resolves to its readable file');
79
+ check(run(controller, ['start', '--session-id', missingSession], ROOT).status === 0, 'workflow starts with a visible missing binding');
80
+ const missingLoad = run(controller, [
81
+ 'binding', '--session-id', missingSession, '--step-id', 'req-change', '--binding-id', 'domain-reference',
82
+ '--disposition', 'loaded', '--evidence', 'claimed to read the reference',
83
+ ], ROOT);
84
+ check(missingLoad.status !== 0, 'a missing required binding cannot be marked loaded');
85
+ state = readJson(obligationsFile);
86
+ const afterLoad = state.requirements[0].requiredBindings.find(item => item.id === 'domain-reference');
87
+ check(afterLoad.status === 'unavailable' && afterLoad.reasonCode === 'binding-path-missing',
88
+ 'the failed load leaves the required binding visibly unavailable');
89
+ check(run(controller, [
90
+ 'step', '--session-id', missingSession, '--step-id', 'req-change', '--disposition', 'pass', '--evidence', 'done',
91
+ ], ROOT).status !== 0, 'the step cannot pass while its required binding is unresolved');
92
+ check(run(controller, [
93
+ 'binding', '--session-id', missingSession, '--step-id', 'req-change', '--binding-id', 'routing-reference',
94
+ '--disposition', 'loaded', '--evidence', 'read the routing reference',
95
+ ], ROOT).status === 0, 'an existing optional binding can be loaded');
96
+
97
+ // 2. Deployed plugin layouts resolve built-in skill bindings.
98
+ const emptyWorkspace = fs.mkdtempSync(path.join(stateHome, 'empty-workspace-'));
99
+ const canonicalResolver = require(path.join(ROOT, 'hooks/scripts/lib/binding-resolver.js'));
100
+ const canonical = canonicalResolver.resolveBinding({ id: 'tdd', path: 'tdd/SKILL.md' }, emptyWorkspace);
101
+ check(canonical.availability === 'available' && samePath(canonical.file, path.join(ROOT, 'tdd/SKILL.md')),
102
+ 'repository/Claude plugin layout resolves tdd/SKILL.md at the package root');
103
+ const codexResolverPath = path.join(ROOT, 'plugins/harness-everything/hooks/scripts/lib/binding-resolver.js');
104
+ check(fs.existsSync(codexResolverPath), 'Codex plugin package ships the binding resolver');
105
+ if (fs.existsSync(codexResolverPath)) {
106
+ const packaged = require(codexResolverPath).resolveBinding({ id: 'tdd', path: 'tdd/SKILL.md' }, emptyWorkspace);
107
+ check(packaged.availability === 'available' &&
108
+ samePath(packaged.file, path.join(ROOT, 'plugins/harness-everything/skills/tdd/SKILL.md')),
109
+ 'Codex plugin layout resolves tdd/SKILL.md under its skills/ root');
110
+ }
111
+ const absent = canonicalResolver.resolveBinding({ id: 'nowhere', path: 'nowhere/SKILL.md' }, emptyWorkspace);
112
+ check(absent.availability === 'missing', 'a path absent from workspace and package roots is missing');
113
+
114
+ // 2b. A workspace reference never falls back to a same-named Harness file.
115
+ check(fs.existsSync(path.join(ROOT, 'docs/architecture.md')), 'fixture precondition: Harness ships docs/architecture.md');
116
+ const collision = canonicalResolver.resolveBinding({ id: 'architecture-reference', path: 'docs/architecture.md' }, emptyWorkspace);
117
+ check(collision.availability === 'missing' && collision.file === null,
118
+ 'a missing workspace reference does not resolve to the Harness package copy');
119
+ const packagedSkill = canonicalResolver.resolveBinding({ id: 'verification-loop', path: 'verification-loop/SKILL.md' }, emptyWorkspace);
120
+ check(packagedSkill.availability === 'available', 'a skill id naming its own SKILL.md still resolves from the package');
121
+ const disguised = canonicalResolver.resolveBinding({ id: 'tdd', path: 'docs/architecture.md' }, emptyWorkspace);
122
+ check(disguised.availability === 'missing', 'a registered id at an unregistered path is treated as a workspace reference');
123
+
124
+ const collisionSession = 'pr303-collision-binding';
125
+ const collisionPayload = { session_id: collisionSession, cwd: emptyWorkspace, prompt: 'Fix this checkout bug and add a regression test.' };
126
+ check(run(kernel, [], emptyWorkspace, collisionPayload).status === 0, 'Tier 2 route persists in a client workspace');
127
+ check(run(controller, [
128
+ 'plan', '--session-id', collisionSession, '--requirements-json', JSON.stringify([
129
+ { id: 'req-change', stepType: 'behavior-change', summary: 'Change behavior', acceptance: 'changed',
130
+ requiredBindings: [{ id: 'architecture-reference', path: 'docs/architecture.md' }] },
131
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify', acceptance: 'verified' },
132
+ ]),
133
+ '--strategy', 'iterative-single', '--evidence', 'client workspace without its own architecture doc',
134
+ ], emptyWorkspace).status === 0, 'client plan declares a workspace reference that collides with a Harness file');
135
+ const collisionState = readJson(sessionFile(emptyWorkspace, collisionSession, collisionPayload, 'workflow-obligations.json'));
136
+ check(collisionState.requirements[0].requiredBindings.find(item => item.id === 'architecture-reference').availability === 'missing',
137
+ 'step flow records the colliding workspace reference as missing');
138
+ check(run(controller, ['start', '--session-id', collisionSession], emptyWorkspace).status === 0, 'client workflow starts');
139
+ check(run(controller, [
140
+ 'binding', '--session-id', collisionSession, '--step-id', 'req-change', '--binding-id', 'architecture-reference',
141
+ '--disposition', 'loaded', '--evidence', 'claimed to read the architecture doc',
142
+ ], emptyWorkspace).status !== 0, 'step flow cannot load the Harness copy as the workspace reference');
143
+
144
+ const { prepareRun } = require(path.join(ROOT, 'fable-mode/scripts/workflow-plan-consumer.js'));
145
+ const stageRun = prepareRun({
146
+ routerContract: { workflowPlan: { strategySelection: 'selected', strategy: 'fable-staged', tier: 'tier3' } },
147
+ workspaceRoot: emptyWorkspace, runId: 'pr303-collision-stage', sessionId: 'pr303-collision-stage',
148
+ stages: [{
149
+ stageId: 'design', goal: 'design', agent: 'fable-worker-sonnet', task: 'use the architecture doc',
150
+ dependsOn: [], writeSet: [], inputs: [], expectedOutputs: ['notes'],
151
+ requiredBindings: [{ id: 'architecture-reference', path: 'docs/architecture.md' }],
152
+ checkCommand: 'node check.js', passCondition: 'exit 0',
153
+ }],
154
+ });
155
+ const stageBinding = readJson(path.join(stageRun.runRoot, 'contracts', 'design.json')).requiredBindings[0];
156
+ check(stageBinding.availability === 'missing' && stageBinding.resolvedPath === null,
157
+ 'stage flow records the colliding workspace reference as missing');
158
+
159
+ // 3. Self-evolved skills: ids at routing time, file only at the active step.
160
+ const workspace = fs.mkdtempSync(path.join(stateHome, 'generated-workspace-'));
161
+ const generatedDir = path.join(workspace, '.claude', 'harness-everything', 'skills', 'generated', 'flaky-retry-lesson');
162
+ fs.mkdirSync(generatedDir, { recursive: true });
163
+ fs.writeFileSync(path.join(generatedDir, 'SKILL.md'), '---\nname: flaky-retry-lesson\n---\n# Flaky retry lesson\n');
164
+ fs.writeFileSync(path.join(workspace, '.claude', 'harness-everything', 'manifest.json'), JSON.stringify({
165
+ package: 'harness-everything',
166
+ generated: [{ id: 'flaky-retry-lesson', dirPath: generatedDir, description: 'Retry lesson', triggers: ['flaky', 'retry'] }],
167
+ }));
168
+
169
+ const prompt = 'Fix the flaky retry handling in the payment client and add a regression test.';
170
+ const routed = run(tierRouter, [prompt], workspace);
171
+ check(routed.status === 0, 'router runs against a workspace with a generated-skill manifest');
172
+ check(routed.stdout.includes('SELF-EVOLVED SKILL SIGNALS (STEP INPUT ONLY): flaky-retry-lesson'),
173
+ 'matching self-evolved skill surfaces as a metadata-only id');
174
+ check(!routed.stdout.includes(generatedDir) && !routed.stdout.includes('flaky-retry-lesson/SKILL.md'),
175
+ 'router does not disclose the generated skill path or text');
176
+ const unmatched = run(tierRouter, ['Rename the payment client constant.'], workspace);
177
+ check(!unmatched.stdout.includes('SELF-EVOLVED SKILL SIGNALS'), 'unrelated prompts do not surface the generated skill');
178
+
179
+ const genSession = 'pr303-generated-binding';
180
+ const genPayload = { session_id: genSession, cwd: workspace, prompt };
181
+ check(run(kernel, [], workspace, genPayload).status === 0, 'Tier 2 route persists in the generated-skill workspace');
182
+ const genRequirements = JSON.stringify([
183
+ { id: 'req-fix', stepType: 'behavior-change', summary: 'Fix retry handling', acceptance: 'flaky retry no longer fails' },
184
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify retry handling', acceptance: 'verification evidence recorded', optionalBindings: ['flaky-retry-lesson'] },
185
+ ]);
186
+ check(run(controller, [
187
+ 'plan', '--session-id', genSession, '--requirements-json', genRequirements,
188
+ '--strategy', 'iterative-single', '--evidence', 'fix then verify with the retry lesson',
189
+ ], workspace).status === 0, 'a step can declare the generated skill id as a binding');
190
+ const genStart = responseOf(run(controller, ['start', '--session-id', genSession], workspace)) || {};
191
+ check(genStart.activeStep?.id === 'req-fix' && !JSON.stringify(genStart).includes('flaky-retry-lesson'),
192
+ 'the generated binding is not disclosed before its step is active');
193
+ check(run(controller, [
194
+ 'binding', '--session-id', genSession, '--step-id', 'req-fix', '--binding-id', 'tdd',
195
+ '--disposition', 'loaded', '--evidence', 'read the TDD contract',
196
+ ], workspace).status === 0, 'built-in TDD binding resolves from the package root outside the Harness repo');
197
+ const advanced = responseOf(run(controller, [
198
+ 'step', '--session-id', genSession, '--step-id', 'req-fix', '--disposition', 'pass', '--evidence', 'retry fix complete',
199
+ ], workspace)) || {};
200
+ const generatedBinding = advanced.activeStep?.optionalBindings?.find(item => item.id === 'flaky-retry-lesson');
201
+ check(advanced.activeStep?.id === 'req-verify' && generatedBinding?.availability === 'available' &&
202
+ samePath(generatedBinding.resolvedPath, path.join(generatedDir, 'SKILL.md')),
203
+ 'the generated binding becomes available with its SKILL.md once its step is active');
204
+ check(run(controller, [
205
+ 'binding', '--session-id', genSession, '--step-id', 'req-verify', '--binding-id', 'flaky-retry-lesson',
206
+ '--disposition', 'loaded', '--evidence', 'read the retry lesson',
207
+ ], workspace).status === 0, 'the active generated binding can be loaded');
208
+
209
+ fs.rmSync(stateHome, { recursive: true, force: true });
210
+ console.log('\n' + (failed === 0 ? 'PASS' : 'FAIL') + ': PR #303 binding resolution (' + failed + ' failures)');
211
+ process.exit(failed === 0 ? 0 : 1);
@@ -85,6 +85,18 @@ for (const platform of matrix.platforms) {
85
85
  assert.ok(compliance.totalEdits > 0);
86
86
  assert.ok(fs.statSync(path.join(session, 'edit-state.json')).size > 0);
87
87
  assert.ok(fs.statSync(path.join(session, 'zoom-out-report.md')).size > 0);
88
+ } else if (platform.id === 'codex') {
89
+ const capability = platform.capabilities.liveHostVerification;
90
+ assert.strictEqual(capability.status, 'Partial');
91
+ assert.strictEqual(capability.evidence, 'benchmarks/results/live-host/codex-2026-10-13/README.md');
92
+ const artifactRoot = path.dirname(path.join(ROOT, capability.evidence));
93
+ for (const file of ['README.md', 'observation-records.json']) {
94
+ assert.ok(fs.statSync(path.join(artifactRoot, file)).size > 0, `Missing Codex evidence: ${file}`);
95
+ }
96
+ const evidence = JSON.parse(fs.readFileSync(path.join(artifactRoot, 'observation-records.json'), 'utf8'));
97
+ assert.strictEqual(evidence.format, 'harness-codex-live-observation-evidence-v1');
98
+ assert.strictEqual(evidence.turns.length, 2, 'Codex artifact must preserve both consecutive turns');
99
+ assert.ok(evidence.turns.every(turn => turn.host === 'codex' && turn.selfReportReason === null));
88
100
  } else {
89
101
  assert.strictEqual(platform.capabilities.liveHostVerification.status, 'Unknown', `${platform.id}: no preserved live evidence`);
90
102
  }
@@ -58,6 +58,45 @@ test('commands are classified without keeping their text', () => {
58
58
  assert.strictEqual(lib.classifyCommand('cd x && git push'), 'git', 'the first recognizable segment decides');
59
59
  });
60
60
 
61
+ test('detailed appends distinguish duplicates and bounded lock timeouts', () => {
62
+ const s = sandbox();
63
+ const record = {
64
+ id: 'append-detail-one',
65
+ host: 'codex',
66
+ turn: 1,
67
+ writer: { sessionId: 'append-detail-session' },
68
+ contentSha256: 'append-detail-content-one',
69
+ };
70
+ const options = { dedupeById: true, dedupeByTurn: true };
71
+ assert.deepStrictEqual(lib.appendRecordDetailed(s.store, record, { prompt: 'first' }, options), { status: 'appended' });
72
+ assert.strictEqual(lib.appendRecord(s.store, record, { prompt: 'first' }, options), false,
73
+ 'the legacy boolean wrapper still reports a duplicate as false');
74
+ assert.deepStrictEqual(lib.appendRecordDetailed(s.store, {
75
+ ...record,
76
+ id: 'append-detail-ambiguous',
77
+ captureMethod: 'session-backfill',
78
+ writer: { sessionId: 'append-detail-session', turnId: 'turn-one' },
79
+ contentSha256: 'different-content-for-legacy-turn',
80
+ }, { prompt: 'uncertain match' }, options), { status: 'ambiguous' },
81
+ 'a partial legacy live-turn match is checked under the append lock and remains distinct from an exact duplicate');
82
+
83
+ const lockPath = path.join(s.store, '.index.lock');
84
+ fs.writeFileSync(lockPath, 'held by deterministic fixture');
85
+ const blocked = lib.appendRecordDetailed(s.store, {
86
+ ...record,
87
+ id: 'append-detail-two',
88
+ turn: 2,
89
+ writer: { sessionId: 'append-detail-session', turnId: 'turn-two' },
90
+ contentSha256: 'append-detail-content-two',
91
+ }, { prompt: 'second' }, { ...options, lockTimeoutMs: 25 });
92
+ assert.deepStrictEqual(blocked, { status: 'lock-timeout' }, 'lock contention is distinct from a duplicate');
93
+ assert.strictEqual(lib.markSessionBackfillBehaviorUnobserved(s.store, { lockTimeoutMs: 25 }), null,
94
+ 'migration reports lock acquisition failure consistently');
95
+ assert.strictEqual(JSON.parse(fs.readFileSync(path.join(s.store, 'observations-index.json'), 'utf8')).records.length, 1,
96
+ 'a timed out append leaves no durable index record');
97
+ fs.unlinkSync(lockPath);
98
+ });
99
+
61
100
  test('tier comes from the self-report, owner review overrides, behavior only flags contradictions', () => {
62
101
  assert.strictEqual(typeof lib.breadthTier, 'undefined', 'no file-count tier rule');
63
102
  assert.deepStrictEqual(lib.labelTier({ tier: 'tier2' }, null), { tier: 'tier2', source: 'self-report' });
@@ -222,6 +261,31 @@ test('exporter derives labels, splits by time and reports agreement', () => {
222
261
  assert(!fs.readFileSync(path.join(out, 'export-report.json'), 'utf8').includes('parser'), 'the report holds no text');
223
262
  });
224
263
 
264
+ test('exporter can select post-cutoff rows with self-reported labels for training', () => {
265
+ const s = sandbox();
266
+ writeFixtureStore(s);
267
+ const indexFile = path.join(s.store, 'observations-index.json');
268
+ const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
269
+ index.records[0].observedAt = '2026-09-26T23:59:59.000Z';
270
+ index.records.push({ ...index.records[1], id: 'missing-self-report', selfReport: null });
271
+ fs.writeFileSync(indexFile, JSON.stringify(index));
272
+ const out = path.join(s.dir, 'filtered-export');
273
+ const r = spawnSync('node', [EXPORTER, '--store', s.store, '--out', out, '--since', '2026-09-27', '--self-report-only'], {
274
+ encoding: 'utf8',
275
+ env: s.env,
276
+ });
277
+ assert.strictEqual(r.status, 0, r.stderr);
278
+ const prompts = fs.readFileSync(path.join(out, 'prompts-observed.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
279
+ const labels = fs.readFileSync(path.join(out, 'labels-observed.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
280
+ assert.strictEqual(prompts.length, 3);
281
+ assert.strictEqual(labels.length, 3);
282
+ assert(labels.every(label => label.validity && label.intents.length >= 0), 'only self-reported turns are included');
283
+ const report = JSON.parse(fs.readFileSync(path.join(out, 'export-report.json'), 'utf8'));
284
+ assert.strictEqual(report.since, '2026-09-27');
285
+ assert.strictEqual(report.selfReportOnly, true);
286
+ assert.strictEqual(report.skippedBeforeSince, 1);
287
+ assert.strictEqual(report.skippedUnlabeled, 1);
288
+ });
225
289
  test('review page lists disagreements blind and owner decisions override', () => {
226
290
  const s = sandbox();
227
291
  writeFixtureStore(s);
@@ -16,7 +16,7 @@ function run(command, args, cwd, options = {}) {
16
16
  const result = spawnSync(command, args, {
17
17
  cwd,
18
18
  encoding: 'utf8',
19
- env: { ...process.env, ...(options.env || {}) },
19
+ env: { ...process.env, HARNESS_SKIP_PROJECT_CHECKS: '', ...(options.env || {}) },
20
20
  timeout: options.timeout || 30000,
21
21
  });
22
22
  if (!options.allowFailure && result.status !== 0) {
@@ -17,6 +17,7 @@ const checks = [
17
17
  ['unbound workspace identity', 'scripts/lib/workspace.js', 'ci/mechanism-2u-issue-42-regressions.test.js'],
18
18
  ['worktree submodule reachability', 'using-git-worktrees/scripts/submodule-reachability.js', 'ci/mechanism-34-worktree-submodule-reachability.test.js'],
19
19
  ['verification contract precedence', 'harness-everything/scripts/verify-gate.js', 'ci/mechanism-35-verification-contract.test.js'],
20
+ ['positive skill-eval relevance', 'harness-everything/scripts/tier-router.js', 'ci/skill-routing-check.js'],
20
21
  ];
21
22
 
22
23
  let failures = 0;
@@ -36,6 +37,7 @@ for (const [label, target, testFile] of checks) {
36
37
  'scripts/lib/workspace.js': 'workspace-identity',
37
38
  'using-git-worktrees/scripts/submodule-reachability.js': 'submodule-reachability',
38
39
  'harness-everything/scripts/verify-gate.js': 'verify-contract-precedence',
40
+ 'harness-everything/scripts/tier-router.js': 'knowledge-signal-matcher',
39
41
  }[target],
40
42
  HARNESS_MUTATION_TARGET: path.join(ROOT, target),
41
43
  NODE_OPTIONS: `--require=${loader}`,
@@ -48,6 +48,10 @@ const mutations = {
48
48
  ' finalize(contract ? contractPlan(contract) : discoveredPlan());',
49
49
  ' finalize(discoveredPlan()); // mutation: ignore the authoritative project contract'
50
50
  ),
51
+ 'knowledge-signal-matcher': (source) => source.replace(
52
+ " if (matched && typeof group.id === 'string') {",
53
+ " if (matched && typeof group.id === 'string' && group.id !== 'tdd-test') { // mutation: remove the tdd-test matcher"
54
+ ),
51
55
  };
52
56
 
53
57
  if (target && mutation && mutations[mutation]) {
@@ -190,9 +190,10 @@ assert.strictEqual(second.status, 0, second.stderr);
190
190
  assert.strictEqual(first.stdout, second.stdout, 'identical routing input must produce identical output');
191
191
  assert.match(first.stdout, /RECOMMENDED TIER:\s*Tier 2/i);
192
192
  assert.match(first.stdout, /REQUIRED HARNESS INVARIANTS/);
193
- assert.match(first.stdout, /WORKFLOW SKILLS \(APPLICABILITY MUST BE RESOLVED\)/);
194
- assert.match(first.stdout, /evaluate-suggestions-before-skip/);
195
- assert.match(first.stdout, /read its complete SKILL\.md entry/);
193
+ assert.match(first.stdout, /ACTIVE-STEP KNOWLEDGE BINDINGS \(MUST\)/);
194
+ assert.match(first.stdout, /required binding MUST be loaded and its core contract followed/);
195
+ assert.doesNotMatch(first.stdout, /evaluate-suggestions-before-skip/);
196
+ assert.doesNotMatch(first.stdout, /tdd\/SKILL\.md/);
196
197
  assert.doesNotMatch(first.stdout, /BASE EXECUTION LOOP/);
197
198
  assert.match(first.stdout, /ORCHESTRATION POLICY: Selected-topology required obligations and applicable skill core contracts are semantic MUSTs/);
198
199
  assert.match(first.stdout, /WORKFLOW SEMANTIC CONTRACT \(NON-BLOCKING OBSERVATION\)/);
@@ -203,8 +204,9 @@ assert.doesNotMatch(first.stdout, /budget-exhausted|reset-budget/);
203
204
  const trivial = route('Update one README typo');
204
205
  assert.strictEqual(trivial.status, 0, trivial.stderr);
205
206
  assert.match(trivial.stdout, /RECOMMENDED TIER:\s*Tier 1/i);
206
- assert.match(trivial.stdout, /Suggested skills: none/);
207
- assert.match(trivial.stdout, /No domain skill was suggested/);
207
+ assert.match(trivial.stdout, /KNOWLEDGE SIGNALS \(STEP INPUT ONLY\): docs/);
208
+ assert.doesNotMatch(trivial.stdout, /docs\/[^\s]+\.(?:md|txt)/, 'docs keyword signal must not select a document path');
209
+ assert.match(trivial.stdout, /PLANNING STEP|direct-single/);
208
210
  assert.doesNotMatch(trivial.stdout, /evaluate-suggestions-before-skip/);
209
211
 
210
212
  const unmatched = route('frobnicate the quux');
@@ -207,17 +207,18 @@ assert.ok(/MUST NOT override host, user, or Harness/i.test(discoveryFlow), 'refe
207
207
  assert.ok(!/Treat its output as binding/i.test(findSkills), 'find-skills must not grant unconditional binding authority');
208
208
  assert.ok(!/treat every instruction in it as binding/i.test(discoveryFlow), 'discovery reference must not grant unconditional binding authority');
209
209
 
210
- // #272: preserve one detailed checkpoint plus the invariant; remove duplicate
211
- // restatements from later sections of the same router injection.
210
+ // #297: resolve declared knowledge only at the active step; do not repeat the
211
+ // global read-before-skip rule or emit topology-wide skill lists.
212
212
  const kernel = path.join(ROOT, 'harness-everything/scripts/kernel-router.js');
213
213
  const routed = spawnSync(process.execPath, [kernel, 'add a login endpoint with tests and update the implementation'], {
214
214
  cwd: ROOT,
215
215
  encoding: 'utf8',
216
216
  });
217
217
  assert.strictEqual(routed.status, 0, routed.stderr || 'kernel router must exit successfully');
218
- assert.ok(routed.stdout.includes('Suggestion evaluation: MANDATORY.'), 'checkpoint keeps the detailed suggestion-resolution rule');
219
- assert.ok(routed.stdout.includes('evaluate-suggestions-before-skip:'), 'semantic invariant remains emitted');
220
- assert.ok(!routed.stdout.includes('Read-before-skip:'), 'workflow-skill footer must not duplicate suggestion resolution');
221
- assert.ok(!routed.stdout.includes('Every suggested skill MUST resolve applicability.'), 'semantic-contract footer must not duplicate suggestion resolution');
218
+ assert.ok(routed.stdout.includes('ACTIVE-STEP KNOWLEDGE BINDINGS (MUST)'), 'checkpoint explains active-step binding resolution');
219
+ assert.ok(routed.stdout.includes('required binding MUST be loaded and its core contract followed'), 'active required bindings retain core-contract obligations');
220
+ assert.ok(!routed.stdout.includes('evaluate-suggestions-before-skip'), 'router removes the global read-before-skip invariant');
221
+ assert.ok(!routed.stdout.includes('tdd/SKILL.md'), 'router does not disclose a skill before its requirement step is active');
222
+ assert.ok(!routed.stdout.includes('RECOMMENDED KNOWLEDGE GUIDES'), 'router does not push keyword guide lists');
222
223
 
223
224
  console.log('PASS: prompt contract regressions');