harness-everything 0.8.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 -35
  5. package/CHANGELOG.md +17 -0
  6. package/README.md +2 -2
  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 +87 -74
  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 +9 -6
  15. package/ci/workflow-execution-contract.test.js +60 -0
  16. package/docs/architecture.md +111 -114
  17. package/docs/mechanism-first-skill-mesh.md +92 -103
  18. package/docs/openai-plugin.md +1 -1
  19. package/docs/philosophy.md +40 -33
  20. package/docs/platform-capabilities.md +2 -2
  21. package/docs/reviews/pr-142-issue-131.md +89 -0
  22. package/docs/routing.md +61 -124
  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 +11 -10
  32. package/harness-everything/references/triage-and-tiers.md +76 -107
  33. package/harness-everything/scripts/kernel-router-core.js +263 -0
  34. package/harness-everything/scripts/kernel-router.js +97 -225
  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 +51 -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 +11 -10
  66. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +76 -107
  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 -225
  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 +51 -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,252 +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
- '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
- };
41
-
42
- const SKILL_TEXT = {
43
- 'tdd': 'tdd: useful for behavioral changes where executable tests can drive the implementation.',
44
- 'verification-loop': 'verification-loop: useful for systematic build/lint/test/diff evidence before delivery.',
45
- 'fable-mode': 'fable-mode / fable-discipline: useful for macro planning or deliberate multi-agent decomposition.',
46
- 'fable-discipline': null,
47
- 'multi-agent-workspace': 'multi-agent-workspace: useful when durable bounded delegation, handoffs, or workspace memory are required.',
48
- };
7
+ const core = require('./kernel-router-core');
49
8
 
50
- function sanitizeClassifierOutput(stdout) {
51
- const lines = String(stdout || '').split(/\r?\n/);
52
- const filtered = [];
53
- let skipLegacyContinuation = false;
54
-
55
- for (const line of lines) {
56
- if (line.includes('=> BASE EXECUTION LOOP:')) {
57
- skipLegacyContinuation = true;
58
- continue;
59
- }
60
- if (skipLegacyContinuation && line.includes('Track exactly ONE item in-progress')) {
61
- skipLegacyContinuation = false;
62
- continue;
63
- }
64
- skipLegacyContinuation = false;
65
- 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)) };
66
13
  }
67
-
68
- return filtered.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
14
+ throw new Error('workflow runtime unavailable in this installation');
69
15
  }
70
16
 
71
- function readStructuredContract(contractPath) {
72
- try {
73
- const parsed = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
74
- const validation = validateRouterContract(parsed);
75
- if (!validation.valid) {
76
- return createDegradedRouterContract(
77
- 'router-contract-invalid',
78
- `Structured router contract failed validation: ${validation.errors.join('; ')}`,
79
- );
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;
80
32
  }
81
- return parsed;
82
- } catch (err) {
83
- return createDegradedRouterContract(
84
- 'router-contract-unavailable',
85
- `Structured router contract unavailable: ${err.message}`,
86
- );
87
- }
88
- }
89
-
90
- function resolvePrompt(prompt, stdinPayload) {
91
- if (prompt) return prompt;
92
- if (!stdinPayload) return '';
93
- try {
94
- const payload = JSON.parse(stdinPayload);
95
- return typeof payload.prompt === 'string' ? payload.prompt : '';
96
- } catch (_) {
97
- 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 };
98
36
  }
99
- }
100
-
101
- function printWorkflowPlan(plan) {
102
- const visible = {
103
- schemaVersion: plan.schemaVersion,
104
- 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),
105
46
  tier: plan.tier,
106
47
  strategy: plan.strategy,
107
48
  strategySelection: plan.strategySelection,
108
- actionGate: plan.actionGate,
109
- limits: plan.limits,
110
- parallelism: plan.parallelism,
111
- workspace: plan.workspace,
112
- memory: plan.memory,
49
+ requiredInvariants: plan.requiredInvariants || [],
50
+ suggestedSkills: plan.suggestedSkills || [],
113
51
  verification: plan.verification,
114
- ensemble: plan.ensemble,
115
- fallback: plan.fallback,
116
- 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
+ },
117
63
  };
118
- console.log(`\n=> ROUTER WORKFLOW PLAN (JSON): ${JSON.stringify(visible)}`);
64
+ runtime.saveWorkflow(context);
65
+ return { ...context, hooksRoot, retained: false };
119
66
  }
120
67
 
121
- function uniqueSuggestedSkills(plan) {
122
- if (!Array.isArray(plan.suggestedSkills)) return [];
123
- return [...new Set(plan.suggestedSkills.filter(skill => typeof skill === 'string' && skill.trim()))];
124
- }
125
-
126
- function enforceSuggestionEvaluation(plan) {
127
- if (uniqueSuggestedSkills(plan).length === 0) return;
128
- if (!Array.isArray(plan.requiredInvariants)) plan.requiredInvariants = [];
129
- if (!plan.requiredInvariants.includes('evaluate-suggestions-before-skip')) {
130
- plan.requiredInvariants.push('evaluate-suggestions-before-skip');
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.'); }
131
73
  }
132
- }
133
-
134
- function displayTier(tier) {
135
- if (tier === 'tier1') return 'Tier 1';
136
- if (tier === 'tier2') return 'Tier 2';
137
- if (tier === 'tier3') return 'Tier 3';
138
- return tier || 'unclassified';
139
- }
140
-
141
- function printRoutingCheckpoint(plan) {
142
- const invariants = Array.isArray(plan.requiredInvariants) ? plan.requiredInvariants : [];
143
- const suggestions = uniqueSuggestedSkills(plan);
144
-
145
- console.log('\n=> HARNESS ROUTING CHECKPOINT (REQUIRED VISIBLE STATE):');
146
- console.log(` - Tier: ${displayTier(plan.tier)}`);
147
- console.log(` - Strategy: ${plan.strategy || 'deferred'}`);
148
- console.log(` - Required invariants: ${invariants.length ? invariants.join(', ') : 'none'}`);
149
- console.log(` - Suggested skills: ${suggestions.length ? suggestions.join(', ') : 'none'}`);
150
- if (suggestions.length > 0) {
151
- 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.');
152
- 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.');
153
- console.log(' - Suggestion disposition: execution is advisory after evaluation. Using one suggestion does not waive read-before-skip for other omitted suggestions. If all are skipped after evaluation, state one brief reason grounded in the evaluated flows in the first visible progress/update message.');
154
- }
155
- }
156
-
157
- function printKernelContract(plan) {
158
- console.log('\n=> REQUIRED HARNESS INVARIANTS:');
159
- for (const invariant of plan.requiredInvariants || []) {
160
- 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.');
161
98
  }
162
-
163
- console.log('\n=> SUGGESTED SKILLS (MANDATORY EVALUATION — ADVISORY EXECUTION):');
164
- if (Array.isArray(plan.suggestedSkills) && plan.suggestedSkills.length > 0) {
165
- const emitted = new Set();
166
- for (const skill of plan.suggestedSkills) {
167
- if (emitted.has(skill)) continue;
168
- const text = Object.prototype.hasOwnProperty.call(SKILL_TEXT, skill) ? SKILL_TEXT[skill] : `${skill}: advisory for the selected topology.`;
169
- if (text) console.log(` - ${text}`);
170
- emitted.add(skill);
171
- if (skill === 'fable-mode') emitted.add('fable-discipline');
172
- }
173
- console.log(' - Read-before-skip: evaluate every suggested skill entry before omission; adopting one suggestion does not waive evaluation of the others.');
174
- if (plan.strategy && plan.strategy.startsWith('fable-')) {
175
- console.log(' - Use these selectively after evaluation; Tier 3/Fable does not create a universal skill pipeline.');
176
- }
177
- } else if (plan.strategy === 'direct-single') {
178
- console.log(' - No domain skill was suggested. Prefer the bounded direct path and load a focused skill only when it adds value.');
179
- } else if (plan.strategySelection === 'deferred') {
180
- 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>"');
181
105
  } else {
182
- console.log(' - No additional skill suggestion from the workflow plan.');
183
- }
184
-
185
- if (plan.ensemble) {
186
- 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.`);
187
- }
188
-
189
- if (plan.actionGate && plan.actionGate.required) {
190
- 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.`);
191
- }
192
-
193
- if (plan.fallback && plan.fallback.disposition !== 'none') {
194
- console.log(`\n=> ROUTING FALLBACK: ${plan.fallback.disposition}/${plan.fallback.mode}; ${plan.fallback.reasonCodes.join(', ')}.`);
195
- }
196
-
197
- if (plan.routingStatus === 'degraded') {
198
- 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.');
106
+ console.log(' - Runtime state was not persisted; host enforcement is unavailable/unknown, not proven hard enforcement.');
199
107
  }
200
-
201
- console.log('\n=> ORCHESTRATION POLICY: Do not enforce workflow order. Enforce workflow invariants from the plan. Suggested-skill evaluation is mandatory; execution remains advisory after evaluation. The router plans; execution components execute.');
202
- }
203
-
204
- function run(prompt, stdinPayload) {
205
- const classifierPath = path.join(__dirname, 'tier-router.js');
206
- const contractPath = path.join(os.tmpdir(), `harness-router-${process.pid}-${crypto.randomUUID()}.json`);
207
- const args = prompt ? [classifierPath, prompt] : [classifierPath];
208
- const result = spawnSync(process.execPath, args, {
209
- input: prompt ? undefined : stdinPayload,
210
- encoding: 'utf8',
211
- env: {
212
- ...process.env,
213
- HARNESS_ROUTER_CONTRACT_PATH: contractPath,
214
- },
215
- });
216
-
217
- if (result.error) {
218
- console.error(`[Harness Kernel] Failed to execute tier-router.js: ${result.error.message}`);
219
- process.exit(1);
220
- }
221
-
222
- const sanitized = sanitizeClassifierOutput(result.stdout);
223
- if (sanitized) console.log(sanitized);
224
- if (result.stderr) process.stderr.write(result.stderr);
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
- printWorkflowPlan(contract.workflowPlan);
236
- printRoutingCheckpoint(contract.workflowPlan);
237
- printKernelContract(contract.workflowPlan);
238
-
239
- 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;
240
112
  }
241
113
 
242
- const prompt = process.argv.slice(2).join(' ');
243
- if (prompt) {
244
- run(prompt, null);
245
- } else if (process.stdin.isTTY) {
246
- run('', null);
247
- } else {
114
+ if (process.argv.length > 2 || process.stdin.isTTY) run('');
115
+ else {
248
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);
249
120
  process.stdin.setEncoding('utf8');
250
121
  process.stdin.on('data', chunk => { input += chunk; });
251
- process.stdin.on('end', () => run('', input));
122
+ process.stdin.on('end', finish);
123
+ process.stdin.on('error', finish);
252
124
  }