harness-everything 0.7.0 → 0.8.1

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 (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +12 -2
  4. package/AGENTS.md +36 -33
  5. package/CHANGELOG.md +23 -0
  6. package/README.md +9 -8
  7. package/benchmarks/results/reviews/pr142-verification.json +95 -0
  8. package/ci/action-gate.test.js +1 -1
  9. package/ci/doc-capability-consistency.test.js +3 -0
  10. package/ci/invariant-routing.test.js +105 -34
  11. package/ci/mechanism-2af-worktree-isolation.test.js +152 -0
  12. package/ci/mechanism-2ag-router-worktree-contract.test.js +44 -0
  13. package/ci/mechanism-2ah-workflow-lifecycle.test.js +140 -0
  14. package/ci/openai-plugin-package.test.js +14 -7
  15. package/ci/workflow-execution-contract.test.js +60 -0
  16. package/docs/architecture.md +112 -106
  17. package/docs/mechanism-first-skill-mesh.md +93 -99
  18. package/docs/openai-plugin.md +1 -1
  19. package/docs/philosophy.md +40 -30
  20. package/docs/platform-capabilities.md +10 -2
  21. package/docs/reviews/pr-142-issue-131.md +89 -0
  22. package/docs/routing.md +68 -103
  23. package/docs/workflow-runtime.md +109 -0
  24. package/fable-mode/CONTRACT-FORMAT.md +3 -1
  25. package/fable-mode/SKILL.md +1 -1
  26. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  27. package/fable-mode/fable-haiku/SKILL.md +1 -1
  28. package/fable-mode/fable-opus/SKILL.md +1 -1
  29. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  30. package/fable-mode/scripts/workflow-plan-consumer.js +3 -1
  31. package/harness-everything/SKILL.md +17 -23
  32. package/harness-everything/references/triage-and-tiers.md +81 -98
  33. package/harness-everything/scripts/kernel-router-core.js +263 -0
  34. package/harness-everything/scripts/kernel-router.js +97 -212
  35. package/harness-everything/scripts/router-contract-core.js +612 -0
  36. package/harness-everything/scripts/router-contract.js +21 -581
  37. package/hooks/hooks.json +22 -1
  38. package/hooks/scripts/contract-test.js +24 -2
  39. package/hooks/scripts/lib/workflow-isolation.js +102 -0
  40. package/hooks/scripts/lib/workflow-runtime.js +102 -0
  41. package/hooks/scripts/state-persist.js +1 -0
  42. package/hooks/scripts/workflow-disposition.js +97 -0
  43. package/hooks/scripts/workflow-gate.js +53 -0
  44. package/hooks/scripts/workflow-stop-gate.js +44 -0
  45. package/opencode-plugin/plugin.json +1 -1
  46. package/package.json +2 -2
  47. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  48. package/plugins/harness-everything/hooks/hooks.json +20 -1
  49. package/plugins/harness-everything/hooks/scripts/contract-test.js +24 -2
  50. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +102 -0
  51. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +102 -0
  52. package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -0
  53. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +97 -0
  54. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +53 -0
  55. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +44 -0
  56. package/plugins/harness-everything/plugin.json +1 -1
  57. package/plugins/harness-everything/scripts/lib/advisory-text.js +53 -54
  58. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +3 -1
  59. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  60. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  61. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  62. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  63. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  64. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +3 -1
  65. package/plugins/harness-everything/skills/harness-everything/SKILL.md +17 -23
  66. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +81 -98
  67. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +263 -0
  68. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +97 -212
  69. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +612 -0
  70. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +21 -581
  71. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +14 -16
  72. package/plugins/harness-everything/skills/using-git-worktrees/references/workflow-details.md +44 -85
  73. package/scripts/lib/advisory-text.js +53 -54
  74. package/using-git-worktrees/SKILL.md +14 -16
  75. package/using-git-worktrees/references/workflow-details.md +44 -85
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const {
8
+ createDegradedRouterContract,
9
+ validateRouterContract,
10
+ } = require('./router-contract');
11
+ const { applyEnsemblePolicy } = require('./ensemble-policy');
12
+
13
+ /**
14
+ * Harness Kernel Router
15
+ *
16
+ * tier-router remains the human-readable classifier/guide detector. The
17
+ * kernel consumes its versioned side-channel JSON contract instead of
18
+ * scraping human-readable stdout. Phase 2 selects an execution topology, but
19
+ * the router still only emits a plan: executors retain responsibility for
20
+ * spawning agents, creating workspaces, running tools, and enforcing gates.
21
+ * Phase 4 attaches ensemble-review only as a post-selection modifier.
22
+ */
23
+
24
+ const INVARIANT_TEXT = {
25
+ 'scope-lock': 'Route before execution: stay inside the authorized task/repository scope.',
26
+ 'verify-before-claim': 'Verify before claim: completion claims require objective evidence appropriate to the change.',
27
+ 'replan-after-repeated-failure': 'Re-plan on repetition: after 3 same-signature failures, stop micro-retrying and zoom out/re-diagnose.',
28
+ 'evaluate-suggestions-before-skip': 'Evaluate before skip: read each suggested skill\'s complete SKILL.md entry/basic flow before omitting it.',
29
+ 'loop-budget': 'Stop iterative-single when its explicit iteration budget is exhausted.',
30
+ 'objective-verification': 'Use an objective check for iterative work; self-critique alone is not verification.',
31
+ 'stage-contracts': 'Fable stages must have explicit stage contracts and pass conditions.',
32
+ 'cold-verification': 'Fable delivery requires a cold/independent verifier where specified.',
33
+ 'parallel-scope-contract': 'Parallel work requires declared independent scopes and non-overlapping/read-only writes.',
34
+ 'synthesis-barrier': 'Do not claim parallel completion until all workstreams reach the synthesis barrier.',
35
+ 'handoff-contracts': 'Multi-agent workspace work uses orchestrator-owned handoff contracts; workers do not form a peer mesh.',
36
+ 'workspace-state': 'Durable workspace state remains scoped to the workspace and existing state conventions.',
37
+ 'pre-action-approval': 'Irreversible/external side effects require approval before the exact payload executes.',
38
+ 'preserve-disagreement': 'Ensemble synthesis must retain unresolved minority positions and evidence gaps.',
39
+ 'independent-ensemble-verifier': 'Ensemble delivery requires a verifier independent from the candidate identities; agreement alone is not proof.',
40
+ 'isolated-worktree-before-mutation': 'Major engineering must mutate only inside a verified Git worktree; unavailable isolation means blocked.',
41
+ };
42
+
43
+ const SKILL_TEXT = {
44
+ 'tdd': 'tdd: useful for behavioral changes where executable tests can drive the implementation.',
45
+ 'verification-loop': 'verification-loop: useful for systematic build/lint/test/diff evidence before delivery.',
46
+ 'fable-mode': 'fable-mode / fable-discipline: useful for macro planning or deliberate multi-agent decomposition.',
47
+ 'fable-discipline': null,
48
+ 'multi-agent-workspace': 'multi-agent-workspace: useful when durable bounded delegation, handoffs, or workspace memory are required.',
49
+ };
50
+
51
+ function sanitizeClassifierOutput(stdout) {
52
+ const lines = String(stdout || '').split(/\r?\n/);
53
+ const filtered = [];
54
+ let skipLegacyContinuation = false;
55
+
56
+ for (const line of lines) {
57
+ if (line.includes('=> BASE EXECUTION LOOP:')) {
58
+ skipLegacyContinuation = true;
59
+ continue;
60
+ }
61
+ if (skipLegacyContinuation && line.includes('Track exactly ONE item in-progress')) {
62
+ skipLegacyContinuation = false;
63
+ continue;
64
+ }
65
+ skipLegacyContinuation = false;
66
+ filtered.push(line);
67
+ }
68
+
69
+ return filtered.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
70
+ }
71
+
72
+ function readStructuredContract(contractPath) {
73
+ try {
74
+ const parsed = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
75
+ const validation = validateRouterContract(parsed);
76
+ if (!validation.valid) {
77
+ return createDegradedRouterContract(
78
+ 'router-contract-invalid',
79
+ `Structured router contract failed validation: ${validation.errors.join('; ')}`,
80
+ );
81
+ }
82
+ return parsed;
83
+ } catch (err) {
84
+ return createDegradedRouterContract(
85
+ 'router-contract-unavailable',
86
+ `Structured router contract unavailable: ${err.message}`,
87
+ );
88
+ }
89
+ }
90
+
91
+ function resolvePrompt(prompt, stdinPayload) {
92
+ if (prompt) return prompt;
93
+ if (!stdinPayload) return '';
94
+ try {
95
+ const payload = JSON.parse(stdinPayload);
96
+ return typeof payload.prompt === 'string' ? payload.prompt : '';
97
+ } catch (_) {
98
+ return '';
99
+ }
100
+ }
101
+
102
+ function printWorkflowPlan(plan) {
103
+ const visible = {
104
+ schemaVersion: plan.schemaVersion,
105
+ routingStatus: plan.routingStatus,
106
+ tier: plan.tier,
107
+ strategy: plan.strategy,
108
+ strategySelection: plan.strategySelection,
109
+ actionGate: plan.actionGate,
110
+ limits: plan.limits,
111
+ parallelism: plan.parallelism,
112
+ workspace: plan.workspace,
113
+ memory: plan.memory,
114
+ verification: plan.verification,
115
+ ensemble: plan.ensemble,
116
+ fallback: plan.fallback,
117
+ reasonCodes: plan.reasonCodes,
118
+ requiredInvariants: plan.requiredInvariants,
119
+ suggestedSkills: plan.suggestedSkills,
120
+ mutationIsolation: plan.mutationIsolation,
121
+ };
122
+ console.log(`\n=> ROUTER WORKFLOW PLAN (JSON): ${JSON.stringify(visible)}`);
123
+ }
124
+
125
+ function uniqueSuggestedSkills(plan) {
126
+ if (!Array.isArray(plan.suggestedSkills)) return [];
127
+ return [...new Set(plan.suggestedSkills.filter(skill => typeof skill === 'string' && skill.trim()))];
128
+ }
129
+
130
+ function enforceSuggestionEvaluation(plan) {
131
+ if (uniqueSuggestedSkills(plan).length === 0) return;
132
+ if (!Array.isArray(plan.requiredInvariants)) plan.requiredInvariants = [];
133
+ if (!plan.requiredInvariants.includes('evaluate-suggestions-before-skip')) {
134
+ plan.requiredInvariants.push('evaluate-suggestions-before-skip');
135
+ }
136
+ }
137
+
138
+ function displayTier(tier) {
139
+ if (tier === 'tier1') return 'Tier 1';
140
+ if (tier === 'tier2') return 'Tier 2';
141
+ if (tier === 'tier3') return 'Tier 3';
142
+ return tier || 'unclassified';
143
+ }
144
+
145
+ function printRoutingCheckpoint(plan) {
146
+ const invariants = Array.isArray(plan.requiredInvariants) ? plan.requiredInvariants : [];
147
+ const suggestions = uniqueSuggestedSkills(plan);
148
+
149
+ console.log('\n=> HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE):');
150
+ console.log(` - Tier: ${displayTier(plan.tier)}`);
151
+ console.log(` - Strategy: ${plan.strategy || 'deferred'}`);
152
+ console.log(` - Required invariants: ${invariants.length ? invariants.join(', ') : 'none'}`);
153
+ console.log(` - Suggested skills: ${suggestions.length ? suggestions.join(', ') : 'none'}`);
154
+ if (suggestions.length > 0) {
155
+ console.log(' - Suggestion evaluation: MANDATORY. Before skipping any listed skill, read its complete SKILL.md entry and evaluate USE FOR, DO NOT USE FOR, workflow/basic flow, and hard rules.');
156
+ console.log(' - Skip evidence: do not reject from only the skill name, description, router summary, or a generic "routine/common task" judgement. If the entry cannot be resolved/read, mark it unresolved/unavailable rather than skipped.');
157
+ console.log(' - Suggestion disposition: individual skill applicability is conditional; the selected workflow is mandatory. Using one suggestion does not waive read-before-skip for other omitted suggestions. If all are not-applicable, state one brief flow-grounded reason.');
158
+ }
159
+ }
160
+
161
+ function printKernelContract(plan) {
162
+ console.log('\n=> REQUIRED HARNESS INVARIANTS:');
163
+ for (const invariant of plan.requiredInvariants || []) {
164
+ console.log(` - ${invariant}: ${INVARIANT_TEXT[invariant] || 'Required by the selected workflow plan.'}`);
165
+ }
166
+
167
+ console.log('\n=> WORKFLOW SKILLS (EVALUATE APPLICABILITY — SELECTED WORKFLOW IS MANDATORY):');
168
+ if (Array.isArray(plan.suggestedSkills) && plan.suggestedSkills.length > 0) {
169
+ const emitted = new Set();
170
+ for (const skill of plan.suggestedSkills) {
171
+ if (emitted.has(skill)) continue;
172
+ const text = Object.prototype.hasOwnProperty.call(SKILL_TEXT, skill) ? SKILL_TEXT[skill] : `${skill}: evaluate applicability inside the selected topology.`;
173
+ if (text) console.log(` - ${text}`);
174
+ emitted.add(skill);
175
+ if (skill === 'fable-mode') emitted.add('fable-discipline');
176
+ }
177
+ console.log(' - Read-before-skip: evaluate every suggested skill entry before omission; adopting one suggestion does not waive evaluation of the others.');
178
+ if (plan.strategy && plan.strategy.startsWith('fable-')) {
179
+ console.log(' - Fable is not a universal pipeline, but a selected Fable topology must be entered and resolved before completion.');
180
+ }
181
+ } else if (plan.strategy === 'direct-single') {
182
+ console.log(' - No domain skill was suggested. Prefer the bounded direct path and load a focused skill only when it adds value.');
183
+ } else if (plan.strategySelection === 'deferred') {
184
+ console.log(' - Strategy is deferred/unclassified. Do not infer triviality; choose the smallest justified approach from task evidence.');
185
+ } else {
186
+ console.log(' - No additional skill suggestion from the workflow plan.');
187
+ }
188
+
189
+ if (plan.ensemble) {
190
+ console.log(`\n=> ENSEMBLE REVIEW: bounded to ${plan.ensemble.maxCandidates} candidates; synthesis=${plan.ensemble.synthesis}; verifier=${plan.ensemble.verifier}. Preserve minority positions and do not claim improvement without paired evidence.`);
191
+ }
192
+
193
+ if (plan.actionGate && plan.actionGate.required) {
194
+ console.log(`\n=> ACTION GATE REQUIRED: ${plan.actionGate.reasonCodes.join(', ')}; disposition=${plan.actionGate.disposition}. The router only declares this requirement; execution must not bypass approval.`);
195
+ }
196
+
197
+ if (plan.fallback && plan.fallback.disposition !== 'none') {
198
+ console.log(`\n=> ROUTING FALLBACK: ${plan.fallback.disposition}/${plan.fallback.mode}; ${plan.fallback.reasonCodes.join(', ')}.`);
199
+ }
200
+
201
+ if (plan.routingStatus === 'degraded') {
202
+ console.log('\n=> ROUTING DEGRADATION: Structured routing is degraded. Keep the strategy deferred unless independent evidence supports a route; do not silently downgrade to Tier 1/direct execution.');
203
+ }
204
+
205
+ console.log('\n=> ORCHESTRATION POLICY: Mandatory applicable workflow. The selected topology is an execution contract; the model controls HOW to satisfy it. Escape requires explicit uncovered scope and evidence; covered obligations remain mandatory.');
206
+ }
207
+
208
+ function route(prompt, stdinPayload) {
209
+ const classifierPath = path.join(__dirname, 'tier-router.js');
210
+ const contractPath = path.join(os.tmpdir(), `harness-router-${process.pid}-${crypto.randomUUID()}.json`);
211
+ const args = prompt ? [classifierPath, prompt] : [classifierPath];
212
+ const result = spawnSync(process.execPath, args, {
213
+ input: prompt ? undefined : stdinPayload,
214
+ encoding: 'utf8',
215
+ env: {
216
+ ...process.env,
217
+ HARNESS_ROUTER_CONTRACT_PATH: contractPath,
218
+ },
219
+ });
220
+
221
+ if (result.error) {
222
+ console.error(`[Harness Kernel] Failed to execute tier-router.js: ${result.error.message}`);
223
+ process.exit(1);
224
+ }
225
+
226
+ const contract = readStructuredContract(contractPath);
227
+ applyEnsemblePolicy(contract, resolvePrompt(prompt, stdinPayload));
228
+ enforceSuggestionEvaluation(contract.workflowPlan);
229
+ try {
230
+ fs.rmSync(contractPath, { force: true });
231
+ } catch (err) {
232
+ // Temporary contract cleanup is best effort only.
233
+ }
234
+
235
+ return { contract, sanitized: sanitizeClassifierOutput(result.stdout), stderr: result.stderr, status: result.status };
236
+ }
237
+
238
+ function run(prompt, stdinPayload) {
239
+ const result = route(prompt, stdinPayload);
240
+ if (result.sanitized) console.log(result.sanitized);
241
+ if (result.stderr) process.stderr.write(result.stderr);
242
+ printWorkflowPlan(result.contract.workflowPlan);
243
+ printRoutingCheckpoint(result.contract.workflowPlan);
244
+ printKernelContract(result.contract.workflowPlan);
245
+
246
+ if (result.status !== 0) process.exit(result.status === null ? 1 : result.status);
247
+ }
248
+
249
+ if (require.main === module) {
250
+ const prompt = process.argv.slice(2).join(' ');
251
+ if (prompt) {
252
+ run(prompt, null);
253
+ } else if (process.stdin.isTTY) {
254
+ run('', null);
255
+ } else {
256
+ let input = '';
257
+ process.stdin.setEncoding('utf8');
258
+ process.stdin.on('data', chunk => { input += chunk; });
259
+ process.stdin.on('end', () => run('', input));
260
+ }
261
+ }
262
+
263
+ module.exports = { route, printWorkflowPlan, printRoutingCheckpoint, printKernelContract };
@@ -1,239 +1,124 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
3
+
2
4
  const crypto = require('crypto');
3
5
  const fs = require('fs');
4
- const os = require('os');
5
6
  const path = require('path');
6
- const { spawnSync } = require('child_process');
7
- const {
8
- createDegradedRouterContract,
9
- validateRouterContract,
10
- } = require('./router-contract');
11
- const { applyEnsemblePolicy } = require('./ensemble-policy');
12
-
13
- /**
14
- * Harness Kernel Router
15
- *
16
- * tier-router remains the human-readable classifier/guide detector. The
17
- * kernel consumes its versioned side-channel JSON contract instead of
18
- * scraping human-readable stdout. Phase 2 selects an execution topology, but
19
- * the router still only emits a plan: executors retain responsibility for
20
- * spawning agents, creating workspaces, running tools, and enforcing gates.
21
- * Phase 4 attaches ensemble-review only as a post-selection modifier.
22
- */
23
-
24
- const INVARIANT_TEXT = {
25
- 'scope-lock': 'Route before execution: stay inside the authorized task/repository scope.',
26
- 'verify-before-claim': 'Verify before claim: completion claims require objective evidence appropriate to the change.',
27
- 'replan-after-repeated-failure': 'Re-plan on repetition: after 3 same-signature failures, stop micro-retrying and zoom out/re-diagnose.',
28
- 'loop-budget': 'Stop iterative-single when its explicit iteration budget is exhausted.',
29
- 'objective-verification': 'Use an objective check for iterative work; self-critique alone is not verification.',
30
- 'stage-contracts': 'Fable stages must have explicit stage contracts and pass conditions.',
31
- 'cold-verification': 'Fable delivery requires a cold/independent verifier where specified.',
32
- 'parallel-scope-contract': 'Parallel work requires declared independent scopes and non-overlapping/read-only writes.',
33
- 'synthesis-barrier': 'Do not claim parallel completion until all workstreams reach the synthesis barrier.',
34
- 'handoff-contracts': 'Multi-agent workspace work uses orchestrator-owned handoff contracts; workers do not form a peer mesh.',
35
- 'workspace-state': 'Durable workspace state remains scoped to the workspace and existing state conventions.',
36
- 'pre-action-approval': 'Irreversible/external side effects require approval before the exact payload executes.',
37
- 'preserve-disagreement': 'Ensemble synthesis must retain unresolved minority positions and evidence gaps.',
38
- 'independent-ensemble-verifier': 'Ensemble delivery requires a verifier independent from the candidate identities; agreement alone is not proof.',
39
- };
40
-
41
- const SKILL_TEXT = {
42
- 'tdd': 'tdd: useful for behavioral changes where executable tests can drive the implementation.',
43
- 'verification-loop': 'verification-loop: useful for systematic build/lint/test/diff evidence before delivery.',
44
- 'fable-mode': 'fable-mode / fable-discipline: useful for macro planning or deliberate multi-agent decomposition.',
45
- 'fable-discipline': null,
46
- 'multi-agent-workspace': 'multi-agent-workspace: useful when durable bounded delegation, handoffs, or workspace memory are required.',
47
- };
7
+ const core = require('./kernel-router-core');
48
8
 
49
- function sanitizeClassifierOutput(stdout) {
50
- const lines = String(stdout || '').split(/\r?\n/);
51
- const filtered = [];
52
- let skipLegacyContinuation = false;
53
-
54
- for (const line of lines) {
55
- if (line.includes('=> BASE EXECUTION LOOP:')) {
56
- skipLegacyContinuation = true;
57
- continue;
58
- }
59
- if (skipLegacyContinuation && line.includes('Track exactly ONE item in-progress')) {
60
- skipLegacyContinuation = false;
61
- continue;
62
- }
63
- skipLegacyContinuation = false;
64
- filtered.push(line);
9
+ function loadRuntime() {
10
+ for (const prefix of ['../..', '../../..']) {
11
+ const candidate = path.resolve(__dirname, prefix, 'hooks/scripts/lib/workflow-runtime.js');
12
+ if (fs.existsSync(candidate)) return { runtime: require(candidate), hooksRoot: path.dirname(path.dirname(candidate)) };
65
13
  }
66
-
67
- return filtered.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
14
+ throw new Error('workflow runtime unavailable in this installation');
68
15
  }
69
16
 
70
- function readStructuredContract(contractPath) {
71
- try {
72
- const parsed = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
73
- const validation = validateRouterContract(parsed);
74
- if (!validation.valid) {
75
- return createDegradedRouterContract(
76
- 'router-contract-invalid',
77
- `Structured router contract failed validation: ${validation.errors.join('; ')}`,
78
- );
17
+ function persistWorkflow(plan, payload, prompt) {
18
+ if (!payload || !(payload.session_id || payload.sessionId)) return null;
19
+ const { runtime, hooksRoot } = loadRuntime();
20
+ const context = runtime.loadWorkflow(payload);
21
+ const { writeCurrentSession } = require(path.join(hooksRoot, 'lib/harness-state'));
22
+ writeCurrentSession(context.root, context.sessionId);
23
+ if (context.workflow && runtime.OPEN_STATES.has(context.workflow.state)) {
24
+ if (!context.workflow.workflowPlan) throw new Error('legacy unresolved workflow needs explicit migration; refusing to replace its obligations');
25
+ // Steering/status prompts cannot drop an unresolved execution contract.
26
+ // Stronger new work is retained as a pending route until explicit replan.
27
+ if (runtime.isMajorWorkflow(plan) && !runtime.isMajorWorkflow(context.workflow)) {
28
+ context.workflow.state = 'blocked';
29
+ context.workflow.pendingPlan = plan;
30
+ context.workflow.blockReason = 'stronger-route-requires-replan';
31
+ context.workflow.mutationIsolation = plan.mutationIsolation;
79
32
  }
80
- return parsed;
81
- } catch (err) {
82
- return createDegradedRouterContract(
83
- 'router-contract-unavailable',
84
- `Structured router contract unavailable: ${err.message}`,
85
- );
86
- }
87
- }
88
-
89
- function resolvePrompt(prompt, stdinPayload) {
90
- if (prompt) return prompt;
91
- if (!stdinPayload) return '';
92
- try {
93
- const payload = JSON.parse(stdinPayload);
94
- return typeof payload.prompt === 'string' ? payload.prompt : '';
95
- } catch (_) {
96
- return '';
33
+ context.workflow.lastPromptHash = crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24);
34
+ runtime.saveWorkflow(context);
35
+ return { ...context, hooksRoot, retained: true };
97
36
  }
98
- }
99
-
100
- function printWorkflowPlan(plan) {
101
- const visible = {
102
- schemaVersion: plan.schemaVersion,
103
- routingStatus: plan.routingStatus,
37
+ const selected = plan.strategySelection === 'selected' && Boolean(plan.strategy);
38
+ const now = Date.now();
39
+ context.workflow = {
40
+ schemaVersion: 2,
41
+ workflowId: crypto.randomUUID(),
42
+ createdAt: new Date(now).toISOString(),
43
+ createdAtMs: now,
44
+ sessionId: context.sessionId,
45
+ promptHash: crypto.createHash('sha256').update(prompt).digest('hex').slice(0, 24),
104
46
  tier: plan.tier,
105
47
  strategy: plan.strategy,
106
48
  strategySelection: plan.strategySelection,
107
- actionGate: plan.actionGate,
108
- limits: plan.limits,
109
- parallelism: plan.parallelism,
110
- workspace: plan.workspace,
111
- memory: plan.memory,
49
+ requiredInvariants: plan.requiredInvariants || [],
50
+ suggestedSkills: plan.suggestedSkills || [],
112
51
  verification: plan.verification,
113
- ensemble: plan.ensemble,
114
- fallback: plan.fallback,
115
- reasonCodes: plan.reasonCodes,
52
+ mutationIsolation: plan.mutationIsolation || { required: false },
53
+ workflowPlan: plan,
54
+ state: plan.fallback?.disposition === 'blocked' ? 'blocked' : selected ? 'pending' : 'deferred',
55
+ runId: null,
56
+ revision: 0,
57
+ escapes: [],
58
+ enforcement: {
59
+ mode: 'mandatory-applicable-workflow',
60
+ escapePolicy: 'scoped-evidence-only',
61
+ reasoningPolicy: 'model-controls-how',
62
+ },
116
63
  };
117
- console.log(`\n=> ROUTER WORKFLOW PLAN (JSON): ${JSON.stringify(visible)}`);
118
- }
119
-
120
- function uniqueSuggestedSkills(plan) {
121
- if (!Array.isArray(plan.suggestedSkills)) return [];
122
- return [...new Set(plan.suggestedSkills.filter(skill => typeof skill === 'string' && skill.trim()))];
123
- }
124
-
125
- function displayTier(tier) {
126
- if (tier === 'tier1') return 'Tier 1';
127
- if (tier === 'tier2') return 'Tier 2';
128
- if (tier === 'tier3') return 'Tier 3';
129
- return tier || 'unclassified';
64
+ runtime.saveWorkflow(context);
65
+ return { ...context, hooksRoot, retained: false };
130
66
  }
131
67
 
132
- function printRoutingCheckpoint(plan) {
133
- const invariants = Array.isArray(plan.requiredInvariants) ? plan.requiredInvariants : [];
134
- const suggestions = uniqueSuggestedSkills(plan);
135
-
136
- console.log('\n=> HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE):');
137
- console.log(` - Tier: ${displayTier(plan.tier)}`);
138
- console.log(` - Strategy: ${plan.strategy || 'deferred'}`);
139
- console.log(` - Required invariants: ${invariants.length ? invariants.join(', ') : 'none'}`);
140
- console.log(` - Suggested skills: ${suggestions.length ? suggestions.join(', ') : 'none'}`);
141
- if (suggestions.length > 0) {
142
- console.log(' - Suggestion disposition: suggestions are advisory; use any that add value. If all are skipped, state one brief reason in the first visible progress/update message.');
68
+ function run(raw) {
69
+ const promptArg = process.argv.slice(2).join(' ');
70
+ let payload = null;
71
+ if (raw.trim()) {
72
+ try { payload = JSON.parse(raw); } catch (_) { console.error('[Workflow Router] Invalid hook payload; lifecycle persistence unavailable.'); }
143
73
  }
144
- }
145
-
146
- function printKernelContract(plan) {
147
- console.log('\n=> REQUIRED HARNESS INVARIANTS:');
148
- for (const invariant of plan.requiredInvariants || []) {
149
- console.log(` - ${invariant}: ${INVARIANT_TEXT[invariant] || 'Required by the selected workflow plan.'}`);
74
+ const prompt = promptArg || (typeof payload?.prompt === 'string' ? payload.prompt : '');
75
+ const result = core.route(promptArg, raw || null);
76
+ let plan = result.contract.workflowPlan;
77
+ let persisted = null;
78
+ let failure = null;
79
+ try {
80
+ persisted = persistWorkflow(plan, payload, prompt);
81
+ if (persisted?.retained) plan = persisted.workflow.workflowPlan;
82
+ } catch (error) { failure = error; }
83
+ if (result.sanitized) console.log(result.sanitized);
84
+ if (result.stderr) process.stderr.write(result.stderr);
85
+ if (persisted?.retained) console.log('\n=> Retaining unresolved workflow from earlier prompts; the checkpoint below is the active execution contract.');
86
+ core.printWorkflowPlan(plan);
87
+ core.printRoutingCheckpoint(plan);
88
+ core.printKernelContract(plan);
89
+ console.log('\n=> WORKFLOW EXECUTION CONTRACT (MANDATORY WHEN SELECTED):');
90
+ console.log(' - State: ' + (persisted?.workflow.state || (plan.strategy ? 'unpersisted' : 'deferred')));
91
+ console.log(' - Selected workflow: ' + (plan.strategy || 'deferred'));
92
+ if (plan.strategySelection === 'selected') {
93
+ console.log(' - Execute the selected workflow to resolution. Reasoning and implementation remain flexible inside it.');
94
+ console.log(' - Completion requires the workflow obligations and objective verification to resolve.');
95
+ console.log(' - Generic simple/routine/already-clear reasons cannot waive the selected workflow.');
96
+ console.log(' - Tier 3 / Fable source and artifact mutations require Git worktree isolation; unavailable isolation means BLOCKED.');
97
+ console.log(' - Escape is limited to workflow-uncovered-scope or host-capability-unavailable, with stage, scope and evidence; other obligations remain mandatory.');
150
98
  }
151
-
152
- console.log('\n=> SUGGESTED SKILLS (ADVISORY — no fixed workflow order):');
153
- if (Array.isArray(plan.suggestedSkills) && plan.suggestedSkills.length > 0) {
154
- const emitted = new Set();
155
- for (const skill of plan.suggestedSkills) {
156
- if (emitted.has(skill)) continue;
157
- const text = Object.prototype.hasOwnProperty.call(SKILL_TEXT, skill) ? SKILL_TEXT[skill] : `${skill}: advisory for the selected topology.`;
158
- if (text) console.log(` - ${text}`);
159
- emitted.add(skill);
160
- if (skill === 'fable-mode') emitted.add('fable-discipline');
161
- }
162
- if (plan.strategy && plan.strategy.startsWith('fable-')) {
163
- console.log(' - Use these selectively; Tier 3/Fable does not create a universal skill pipeline.');
164
- }
165
- } else if (plan.strategy === 'direct-single') {
166
- console.log(' - No mandatory domain skill. Prefer the bounded direct path and load a focused skill only when it adds value.');
167
- } else if (plan.strategySelection === 'deferred') {
168
- console.log(' - Strategy is deferred/unclassified. Do not infer triviality; choose the smallest justified approach from task evidence.');
99
+ if (persisted) {
100
+ const controller = path.join(persisted.hooksRoot, 'workflow-disposition.js');
101
+ console.log(' - Workflow id: ' + persisted.workflow.workflowId);
102
+ console.log(' - Stage specification: ' + path.join(persisted.sessionDir, 'workflow-stages.json'));
103
+ console.log(' - Enter/replan: node "' + controller + '" start --session-id "' + persisted.sessionId + '"');
104
+ console.log(' - Escape one declared stage: node "' + controller + '" escape --session-id "' + persisted.sessionId + '" --stage-id "<id>" --reason-code workflow-uncovered-scope --scope "<uncovered scope>" --evidence "<evidence>"');
169
105
  } else {
170
- console.log(' - No additional skill suggestion from the workflow plan.');
171
- }
172
-
173
- if (plan.ensemble) {
174
- console.log(`\n=> ENSEMBLE REVIEW: bounded to ${plan.ensemble.maxCandidates} candidates; synthesis=${plan.ensemble.synthesis}; verifier=${plan.ensemble.verifier}. Preserve minority positions and do not claim improvement without paired evidence.`);
175
- }
176
-
177
- if (plan.actionGate && plan.actionGate.required) {
178
- console.log(`\n=> ACTION GATE REQUIRED: ${plan.actionGate.reasonCodes.join(', ')}; disposition=${plan.actionGate.disposition}. The router only declares this requirement; execution must not bypass approval.`);
179
- }
180
-
181
- if (plan.fallback && plan.fallback.disposition !== 'none') {
182
- console.log(`\n=> ROUTING FALLBACK: ${plan.fallback.disposition}/${plan.fallback.mode}; ${plan.fallback.reasonCodes.join(', ')}.`);
183
- }
184
-
185
- if (plan.routingStatus === 'degraded') {
186
- console.log('\n=> ROUTING DEGRADATION: Structured routing is degraded. Keep the strategy deferred unless independent evidence supports a route; do not silently downgrade to Tier 1/direct execution.');
187
- }
188
-
189
- console.log('\n=> ORCHESTRATION POLICY: Do not enforce workflow order. Enforce workflow invariants from the plan; suggested skills remain advisory. The router plans; execution components execute.');
190
- }
191
-
192
- function run(prompt, stdinPayload) {
193
- const classifierPath = path.join(__dirname, 'tier-router.js');
194
- const contractPath = path.join(os.tmpdir(), `harness-router-${process.pid}-${crypto.randomUUID()}.json`);
195
- const args = prompt ? [classifierPath, prompt] : [classifierPath];
196
- const result = spawnSync(process.execPath, args, {
197
- input: prompt ? undefined : stdinPayload,
198
- encoding: 'utf8',
199
- env: {
200
- ...process.env,
201
- HARNESS_ROUTER_CONTRACT_PATH: contractPath,
202
- },
203
- });
204
-
205
- if (result.error) {
206
- console.error(`[Harness Kernel] Failed to execute tier-router.js: ${result.error.message}`);
207
- process.exit(1);
208
- }
209
-
210
- const sanitized = sanitizeClassifierOutput(result.stdout);
211
- if (sanitized) console.log(sanitized);
212
- if (result.stderr) process.stderr.write(result.stderr);
213
-
214
- const contract = readStructuredContract(contractPath);
215
- applyEnsemblePolicy(contract, resolvePrompt(prompt, stdinPayload));
216
- try {
217
- fs.rmSync(contractPath, { force: true });
218
- } catch (err) {
219
- // Temporary contract cleanup is best effort only.
106
+ console.log(' - Runtime state was not persisted; host enforcement is unavailable/unknown, not proven hard enforcement.');
220
107
  }
221
-
222
- printWorkflowPlan(contract.workflowPlan);
223
- printRoutingCheckpoint(contract.workflowPlan);
224
- printKernelContract(contract.workflowPlan);
225
-
226
- if (result.status !== 0) process.exit(result.status === null ? 1 : result.status);
108
+ if (failure) {
109
+ console.error('[Workflow Router] ' + failure.message);
110
+ process.exitCode = 2;
111
+ } else if (result.status !== 0) process.exitCode = result.status || 1;
227
112
  }
228
113
 
229
- const prompt = process.argv.slice(2).join(' ');
230
- if (prompt) {
231
- run(prompt, null);
232
- } else if (process.stdin.isTTY) {
233
- run('', null);
234
- } else {
114
+ if (process.argv.length > 2 || process.stdin.isTTY) run('');
115
+ else {
235
116
  let input = '';
117
+ let finished = false;
118
+ const finish = () => { if (finished) return; finished = true; clearTimeout(timeout); run(input); };
119
+ const timeout = setTimeout(finish, 500);
236
120
  process.stdin.setEncoding('utf8');
237
121
  process.stdin.on('data', chunk => { input += chunk; });
238
- process.stdin.on('end', () => run('', input));
122
+ process.stdin.on('end', finish);
123
+ process.stdin.on('error', finish);
239
124
  }