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,612 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SCHEMA_VERSION = 1;
7
+ const ITERATIVE_MAX_ITERATIONS = 8;
8
+ const TIERS = new Set(['tier1', 'tier2', 'tier3', 'unclassified']);
9
+ const ROUTING_STATUSES = new Set(['ok', 'degraded']);
10
+ const STRATEGIES = new Set([
11
+ 'direct-single',
12
+ 'iterative-single',
13
+ 'fable-staged',
14
+ 'fable-parallel',
15
+ 'fable-multi-agent-workspace',
16
+ ]);
17
+ const STRATEGY_SELECTIONS = new Set(['shadow', 'selected', 'deferred']);
18
+ const CAPABILITY_STATES = new Set(['available', 'unavailable', 'unknown']);
19
+ const MODEL_CAPABILITY_STATES = new Set(['available', 'partial', 'unavailable', 'unknown']);
20
+ const PROHIBITIONS = new Set(['fable', 'subagents', 'parallel', 'workspace', 'memory', 'ensemble']);
21
+
22
+ function uniqueReasonCodes(values) {
23
+ return Array.from(new Set((values || []).filter(Boolean)));
24
+ }
25
+
26
+ function normalizeTierLabel(label) {
27
+ const text = String(label || '');
28
+ if (/^Tier\s*1\b/i.test(text)) return 'tier1';
29
+ if (/^Tier\s*2\b/i.test(text)) return 'tier2';
30
+ if (/^Tier\s*3\b/i.test(text)) return 'tier3';
31
+ return 'unclassified';
32
+ }
33
+
34
+ function normalizeCapability(value, model = false) {
35
+ const allowed = model ? MODEL_CAPABILITY_STATES : CAPABILITY_STATES;
36
+ return allowed.has(value) ? value : 'unknown';
37
+ }
38
+
39
+ function normalizeHostCapabilities(raw = {}) {
40
+ return {
41
+ subagents: normalizeCapability(raw.subagents),
42
+ parallelCalls: normalizeCapability(raw.parallelCalls),
43
+ hooks: normalizeCapability(raw.hooks),
44
+ state: normalizeCapability(raw.state),
45
+ modelAvailability: normalizeCapability(raw.modelAvailability, true),
46
+ };
47
+ }
48
+
49
+ function normalizeConstraints(raw = {}) {
50
+ const value = key => (raw[key] === undefined ? 'unspecified' : raw[key]);
51
+ return {
52
+ cost: value('cost'),
53
+ latency: value('latency'),
54
+ tokens: value('tokens'),
55
+ concurrency: value('concurrency'),
56
+ };
57
+ }
58
+
59
+ function normalizeExplicitRequest(raw = {}) {
60
+ const requestedStrategy = STRATEGIES.has(raw.strategy) ? raw.strategy : null;
61
+ const prohibitions = Array.isArray(raw.prohibitions)
62
+ ? raw.prohibitions.filter(value => PROHIBITIONS.has(value))
63
+ : [];
64
+ return {
65
+ fableModel: ['haiku', 'sonnet', 'opus'].includes(raw.fableModel) ? raw.fableModel : null,
66
+ strategy: requestedStrategy,
67
+ prohibitions: Array.from(new Set(prohibitions)),
68
+ };
69
+ }
70
+
71
+ function buildTaskShape(input = {}) {
72
+ const tier = TIERS.has(input.tier) ? input.tier : 'unclassified';
73
+ const signals = input.signals || {};
74
+ const hostCapabilities = normalizeHostCapabilities(input.hostCapabilities);
75
+ const constraints = normalizeConstraints(input.constraints);
76
+ const explicitRequest = normalizeExplicitRequest(input.explicitRequest || {
77
+ fableModel: input.requestedFableModel,
78
+ });
79
+
80
+ let scopeSize = 'unknown';
81
+ if (signals.trivialDocsEdit || signals.specificFile) scopeSize = 'single-file';
82
+ else if (signals.macroScope) scopeSize = 'repository-wide';
83
+ else if (signals.multipleTasks) scopeSize = 'multi-file';
84
+
85
+ let dependencyGraph = 'unknown';
86
+ if (signals.cyclicDependencies) dependencyGraph = 'cyclic';
87
+ else if (signals.independentWorkstreams) dependencyGraph = 'independent';
88
+ else if (signals.dependentStages || tier === 'tier3') dependencyGraph = 'sequential';
89
+
90
+ let writeSetOverlap = 'unknown';
91
+ if (signals.sharedWrite) writeSetOverlap = 'overlap';
92
+ else if (signals.readOnly) writeSetOverlap = 'read-only';
93
+ else if (signals.disjointWrites) writeSetOverlap = 'disjoint';
94
+
95
+ let domainDiversity = 'unknown';
96
+ if (signals.crossDomain || signals.reusableSpecialists) domainDiversity = 'multi-domain';
97
+ else if (tier === 'tier1' || tier === 'tier2') domainDiversity = 'single-domain';
98
+
99
+ let expectedDuration = 'unknown';
100
+ if (signals.multiSession) expectedDuration = 'multi-session';
101
+ else if (tier === 'tier3') expectedDuration = 'multi-turn';
102
+ else if (tier === 'tier1' || tier === 'tier2') expectedDuration = 'one-turn';
103
+
104
+ let reversibility = 'unknown';
105
+ if (signals.externalSideEffect) reversibility = 'external-side-effect';
106
+ else if (signals.irreversibleAction) reversibility = 'irreversible';
107
+
108
+ let uncertainty = 'unknown';
109
+ if (signals.highUncertainty) uncertainty = 'high';
110
+ else if (tier === 'tier1') uncertainty = 'low';
111
+ else if (tier === 'tier2') uncertainty = 'medium';
112
+
113
+ let verificationRequirement = 'unknown';
114
+ if (tier === 'tier3' || signals.highRisk) verificationRequirement = 'independent';
115
+ else if (tier === 'tier2') verificationRequirement = 'objective';
116
+ else if (tier === 'tier1') verificationRequirement = 'basic';
117
+
118
+ let estimatedStages = null;
119
+ if (Number.isInteger(signals.estimatedStages) && signals.estimatedStages > 0) estimatedStages = signals.estimatedStages;
120
+ else if (tier === 'tier3') estimatedStages = 2;
121
+ else if (tier === 'tier1' || tier === 'tier2') estimatedStages = 1;
122
+
123
+ return {
124
+ schemaVersion: SCHEMA_VERSION,
125
+ scopeSize,
126
+ estimatedStages,
127
+ dependencyGraph,
128
+ writeSetOverlap,
129
+ domainDiversity,
130
+ expectedDuration,
131
+ reversibility,
132
+ uncertainty,
133
+ verificationRequirement,
134
+ hostCapabilities,
135
+ constraints,
136
+ explicitRequest,
137
+ observedSignals: {
138
+ macroScope: Boolean(signals.macroScope),
139
+ trivialDocsEdit: Boolean(signals.trivialDocsEdit),
140
+ tier3Keyword: Boolean(signals.tier3Keyword),
141
+ tier2Keyword: Boolean(signals.tier2Keyword),
142
+ multipleTasks: Boolean(signals.multipleTasks),
143
+ multipleSentences: Boolean(signals.multipleSentences),
144
+ specificFile: Boolean(signals.specificFile),
145
+ question: Boolean(signals.question),
146
+ independentWorkstreams: Boolean(signals.independentWorkstreams),
147
+ readOnly: Boolean(signals.readOnly),
148
+ disjointWrites: Boolean(signals.disjointWrites),
149
+ sharedWrite: Boolean(signals.sharedWrite),
150
+ dependentStages: Boolean(signals.dependentStages),
151
+ multiSession: Boolean(signals.multiSession),
152
+ reusableSpecialists: Boolean(signals.reusableSpecialists),
153
+ crossDomain: Boolean(signals.crossDomain),
154
+ highUncertainty: Boolean(signals.highUncertainty),
155
+ irreversibleAction: Boolean(signals.irreversibleAction),
156
+ externalSideEffect: Boolean(signals.externalSideEffect),
157
+ },
158
+ reasonCodes: uniqueReasonCodes(input.reasonCodes),
159
+ };
160
+ }
161
+
162
+ function requestedStrategyIsProhibited(strategy, prohibitions) {
163
+ if (!strategy) return false;
164
+ if (strategy.startsWith('fable-') && prohibitions.includes('fable')) return true;
165
+ if (strategy === 'fable-parallel' && prohibitions.includes('parallel')) return true;
166
+ if (strategy === 'fable-multi-agent-workspace' && prohibitions.includes('workspace')) return true;
167
+ if ((strategy === 'fable-parallel' || strategy === 'fable-multi-agent-workspace') && prohibitions.includes('subagents')) return true;
168
+ return false;
169
+ }
170
+
171
+ function hasValidatedParallelScope(taskShape) {
172
+ return taskShape.dependencyGraph === 'independent' &&
173
+ (taskShape.writeSetOverlap === 'read-only' || taskShape.writeSetOverlap === 'disjoint');
174
+ }
175
+
176
+ function selectWorkflowStrategy(input = {}) {
177
+ const tier = TIERS.has(input.tier) ? input.tier : 'unclassified';
178
+ const routingStatus = ROUTING_STATUSES.has(input.routingStatus) ? input.routingStatus : 'degraded';
179
+ const taskShape = input.taskShape || buildTaskShape({ tier });
180
+ const explicit = taskShape.explicitRequest || normalizeExplicitRequest();
181
+ const prohibitions = explicit.prohibitions || [];
182
+ const reasonCodes = [];
183
+ const fallback = {
184
+ onMissingSubagents: 'visible-inline-or-stop',
185
+ disposition: 'none',
186
+ mode: 'none',
187
+ reasonCodes: [],
188
+ };
189
+
190
+ let strategy = null;
191
+ let strategySelection = 'deferred';
192
+
193
+ if (explicit.strategy) {
194
+ strategy = explicit.strategy;
195
+ strategySelection = 'selected';
196
+ reasonCodes.push('explicit-strategy-request');
197
+ if (requestedStrategyIsProhibited(strategy, prohibitions)) {
198
+ fallback.disposition = 'blocked';
199
+ fallback.mode = 'blocked';
200
+ fallback.reasonCodes.push('explicit-request-conflicts-with-prohibition');
201
+ } else if (strategy === 'fable-parallel' && !hasValidatedParallelScope(taskShape)) {
202
+ strategy = 'fable-staged';
203
+ fallback.disposition = 'reduced';
204
+ fallback.mode = 'serialized';
205
+ fallback.reasonCodes.push(
206
+ taskShape.writeSetOverlap === 'overlap'
207
+ ? 'parallel-write-set-overlap'
208
+ : 'parallel-preconditions-unproven',
209
+ );
210
+ }
211
+ } else if (routingStatus !== 'ok') {
212
+ reasonCodes.push('routing-degraded-strategy-deferred');
213
+ } else if (tier === 'unclassified') {
214
+ reasonCodes.push('unclassified-strategy-deferred');
215
+ } else if (tier === 'tier1') {
216
+ strategy = 'direct-single';
217
+ strategySelection = 'selected';
218
+ reasonCodes.push('tier1-bounded-single-pass');
219
+ } else if (tier === 'tier2') {
220
+ strategy = 'iterative-single';
221
+ strategySelection = 'selected';
222
+ reasonCodes.push('tier2-iterative-default');
223
+ } else if (taskShape.expectedDuration === 'multi-session' && taskShape.observedSignals.reusableSpecialists) {
224
+ strategy = 'fable-multi-agent-workspace';
225
+ strategySelection = 'selected';
226
+ reasonCodes.push('durable-reusable-specialists');
227
+ } else if (hasValidatedParallelScope(taskShape)) {
228
+ strategy = 'fable-parallel';
229
+ strategySelection = 'selected';
230
+ reasonCodes.push('independent-scopes-validated');
231
+ } else {
232
+ strategy = 'fable-staged';
233
+ strategySelection = 'selected';
234
+ reasonCodes.push('tier3-dependent-or-unproven-parallelism');
235
+ }
236
+
237
+ if (strategy && fallback.disposition !== 'blocked') {
238
+ if (strategy.startsWith('fable-') && prohibitions.includes('fable')) {
239
+ strategy = 'iterative-single';
240
+ fallback.disposition = 'reduced';
241
+ fallback.mode = 'inline';
242
+ fallback.reasonCodes.push('user-prohibited-fable');
243
+ }
244
+
245
+ if (strategy === 'fable-multi-agent-workspace' && prohibitions.includes('workspace')) {
246
+ strategy = 'fable-staged';
247
+ fallback.disposition = 'reduced';
248
+ fallback.mode = 'inline';
249
+ fallback.reasonCodes.push('user-prohibited-workspace');
250
+ }
251
+
252
+ if (strategy === 'fable-parallel' && prohibitions.includes('parallel')) {
253
+ strategy = 'fable-staged';
254
+ fallback.disposition = 'reduced';
255
+ fallback.mode = 'serialized';
256
+ fallback.reasonCodes.push('user-prohibited-parallelism');
257
+ }
258
+
259
+ const concurrency = Number(taskShape.constraints && taskShape.constraints.concurrency);
260
+ if (strategy === 'fable-parallel' && Number.isFinite(concurrency) && concurrency <= 1) {
261
+ strategy = 'fable-staged';
262
+ fallback.disposition = 'reduced';
263
+ fallback.mode = 'serialized';
264
+ fallback.reasonCodes.push('concurrency-budget-serializes');
265
+ }
266
+
267
+ if (strategy === 'fable-parallel' && taskShape.hostCapabilities.parallelCalls === 'unavailable') {
268
+ strategy = 'fable-staged';
269
+ fallback.disposition = 'reduced';
270
+ fallback.mode = 'serialized';
271
+ fallback.reasonCodes.push('parallel-capability-unavailable');
272
+ }
273
+
274
+ if (strategy === 'fable-parallel' && (prohibitions.includes('subagents') || taskShape.hostCapabilities.subagents === 'unavailable')) {
275
+ strategy = 'fable-staged';
276
+ fallback.disposition = 'reduced';
277
+ fallback.mode = 'inline';
278
+ fallback.reasonCodes.push(prohibitions.includes('subagents') ? 'user-prohibited-subagents' : 'subagents-unavailable');
279
+ }
280
+
281
+ if (strategy === 'fable-multi-agent-workspace') {
282
+ if (taskShape.hostCapabilities.state === 'unavailable') {
283
+ fallback.disposition = 'blocked';
284
+ fallback.mode = 'blocked';
285
+ fallback.reasonCodes.push('workspace-state-unavailable');
286
+ }
287
+ if (prohibitions.includes('subagents') || taskShape.hostCapabilities.subagents === 'unavailable') {
288
+ fallback.disposition = 'blocked';
289
+ fallback.mode = 'blocked';
290
+ fallback.reasonCodes.push(prohibitions.includes('subagents') ? 'user-prohibited-subagents' : 'subagents-unavailable');
291
+ }
292
+ } else if (strategy === 'fable-staged' && (prohibitions.includes('subagents') || taskShape.hostCapabilities.subagents === 'unavailable')) {
293
+ fallback.disposition = 'reduced';
294
+ fallback.mode = 'inline';
295
+ fallback.reasonCodes.push(prohibitions.includes('subagents') ? 'user-prohibited-subagents' : 'subagents-unavailable');
296
+ }
297
+
298
+ if (explicit.fableModel && taskShape.hostCapabilities.modelAvailability === 'unavailable') {
299
+ fallback.disposition = 'blocked';
300
+ fallback.mode = 'blocked';
301
+ fallback.reasonCodes.push('requested-model-capability-unavailable');
302
+ }
303
+ }
304
+
305
+ return {
306
+ strategy,
307
+ strategySelection,
308
+ reasonCodes: uniqueReasonCodes(reasonCodes),
309
+ fallback: {
310
+ ...fallback,
311
+ reasonCodes: uniqueReasonCodes(fallback.reasonCodes),
312
+ },
313
+ };
314
+ }
315
+
316
+ function planMetadataForStrategy(strategy) {
317
+ const base = {
318
+ patterns: ['router'],
319
+ requiredInvariants: ['scope-lock', 'verify-before-claim', 'replan-after-repeated-failure'],
320
+ suggestedSkills: [],
321
+ maxIterations: null,
322
+ parallelAllowed: null,
323
+ parallelReasonCodes: [],
324
+ workspaceRequired: null,
325
+ workspaceReasonCodes: [],
326
+ memoryRead: 'none',
327
+ memoryWrite: 'none',
328
+ verificationMode: 'verify-before-claim',
329
+ verificationIndependent: null,
330
+ };
331
+
332
+ if (strategy === 'direct-single') {
333
+ return {
334
+ ...base,
335
+ patterns: ['router', 'augmented-llm'],
336
+ parallelAllowed: false,
337
+ parallelReasonCodes: ['single-pass-topology'],
338
+ workspaceRequired: false,
339
+ workspaceReasonCodes: ['bounded-one-turn-work'],
340
+ verificationIndependent: false,
341
+ };
342
+ }
343
+
344
+ if (strategy === 'iterative-single') {
345
+ return {
346
+ ...base,
347
+ patterns: ['router', 'react'],
348
+ requiredInvariants: [...base.requiredInvariants, 'loop-budget', 'objective-verification'],
349
+ suggestedSkills: ['tdd', 'verification-loop'],
350
+ maxIterations: ITERATIVE_MAX_ITERATIONS,
351
+ parallelAllowed: false,
352
+ parallelReasonCodes: ['single-agent-loop'],
353
+ workspaceRequired: false,
354
+ workspaceReasonCodes: ['single-agent-loop'],
355
+ verificationMode: 'objective',
356
+ verificationIndependent: false,
357
+ };
358
+ }
359
+
360
+ if (strategy === 'fable-staged') {
361
+ return {
362
+ ...base,
363
+ patterns: ['router', 'planner-executor', 'evaluator-optimizer'],
364
+ requiredInvariants: [...base.requiredInvariants, 'stage-contracts', 'cold-verification'],
365
+ suggestedSkills: ['fable-mode', 'fable-discipline', 'verification-loop'],
366
+ parallelAllowed: false,
367
+ parallelReasonCodes: ['dependent-or-unproven-independent-stages'],
368
+ workspaceRequired: false,
369
+ workspaceReasonCodes: ['no-durable-workspace-requirement'],
370
+ verificationMode: 'cold-verifier',
371
+ verificationIndependent: true,
372
+ };
373
+ }
374
+
375
+ if (strategy === 'fable-parallel') {
376
+ return {
377
+ ...base,
378
+ patterns: ['router', 'planner-executor', 'evaluator-optimizer'],
379
+ requiredInvariants: [...base.requiredInvariants, 'stage-contracts', 'cold-verification', 'parallel-scope-contract', 'synthesis-barrier'],
380
+ suggestedSkills: ['fable-mode', 'fable-discipline', 'verification-loop'],
381
+ parallelAllowed: true,
382
+ parallelReasonCodes: ['independent-read-only-or-disjoint-scopes'],
383
+ workspaceRequired: false,
384
+ workspaceReasonCodes: ['no-durable-workspace-requirement'],
385
+ verificationMode: 'cold-verifier',
386
+ verificationIndependent: true,
387
+ };
388
+ }
389
+
390
+ if (strategy === 'fable-multi-agent-workspace') {
391
+ return {
392
+ ...base,
393
+ patterns: ['router', 'planner-executor', 'evaluator-optimizer', 'multi-agent-memory'],
394
+ requiredInvariants: [...base.requiredInvariants, 'stage-contracts', 'cold-verification', 'handoff-contracts', 'workspace-state'],
395
+ suggestedSkills: ['multi-agent-workspace', 'fable-mode', 'fable-discipline', 'verification-loop'],
396
+ parallelAllowed: false,
397
+ parallelReasonCodes: ['workspace-orchestrator-controls-concurrency'],
398
+ workspaceRequired: true,
399
+ workspaceReasonCodes: ['durable-reusable-specialists'],
400
+ memoryRead: 'workspace-index',
401
+ memoryWrite: 'propose',
402
+ verificationMode: 'cold-verifier',
403
+ verificationIndependent: true,
404
+ };
405
+ }
406
+
407
+ return base;
408
+ }
409
+
410
+ function buildWorkflowPlan(input = {}) {
411
+ const tier = TIERS.has(input.tier) ? input.tier : 'unclassified';
412
+ const routingStatus = ROUTING_STATUSES.has(input.routingStatus) ? input.routingStatus : 'degraded';
413
+ const taskShape = input.taskShape || buildTaskShape({ tier });
414
+ const selection = selectWorkflowStrategy({ tier, routingStatus, taskShape });
415
+ const meta = planMetadataForStrategy(selection.strategy);
416
+ const actionGateReasonCodes = uniqueReasonCodes(input.actionGateReasonCodes);
417
+ const actionGateRequired = actionGateReasonCodes.length > 0;
418
+
419
+ const requiredInvariants = [...meta.requiredInvariants];
420
+ const patterns = [...meta.patterns];
421
+ if (actionGateRequired) {
422
+ requiredInvariants.push('pre-action-approval');
423
+ patterns.push('verifier-gated');
424
+ }
425
+
426
+ const fallback = {
427
+ ...selection.fallback,
428
+ reasonCodes: uniqueReasonCodes(selection.fallback.reasonCodes),
429
+ };
430
+ if (actionGateRequired && taskShape.hostCapabilities.hooks === 'unavailable') {
431
+ fallback.disposition = 'blocked';
432
+ fallback.mode = 'blocked';
433
+ fallback.reasonCodes = uniqueReasonCodes([...fallback.reasonCodes, 'action-gate-hook-unavailable']);
434
+ }
435
+
436
+ const reasonCodes = uniqueReasonCodes([
437
+ ...(input.reasonCodes || []),
438
+ ...selection.reasonCodes,
439
+ ...fallback.reasonCodes,
440
+ ...actionGateReasonCodes,
441
+ ]);
442
+
443
+ return {
444
+ schemaVersion: SCHEMA_VERSION,
445
+ routingStatus,
446
+ tier,
447
+ strategy: selection.strategy,
448
+ strategySelection: selection.strategySelection,
449
+ patterns: Array.from(new Set(patterns)),
450
+ requiredInvariants: Array.from(new Set(requiredInvariants)),
451
+ suggestedSkills: Array.from(new Set(meta.suggestedSkills)),
452
+ actionGate: {
453
+ required: actionGateRequired,
454
+ reasonCodes: actionGateReasonCodes,
455
+ approver: actionGateRequired ? 'rule-or-human' : null,
456
+ disposition: actionGateRequired ? 'pending-approval' : null,
457
+ },
458
+ limits: {
459
+ maxIterations: meta.maxIterations,
460
+ maxRevisionRounds: 2,
461
+ maxWorkers: 'fable-orchestrator-cap',
462
+ },
463
+ parallelism: {
464
+ allowed: meta.parallelAllowed,
465
+ requires: ['declared-dependsOn', 'disjoint-writeSet'],
466
+ reasonCodes: uniqueReasonCodes(meta.parallelReasonCodes),
467
+ },
468
+ workspace: {
469
+ required: meta.workspaceRequired,
470
+ reasonCodes: uniqueReasonCodes(meta.workspaceReasonCodes),
471
+ },
472
+ memory: {
473
+ read: meta.memoryRead,
474
+ scope: 'task-relevant-only',
475
+ write: meta.memoryWrite,
476
+ },
477
+ verification: {
478
+ mode: meta.verificationMode,
479
+ independent: meta.verificationIndependent,
480
+ },
481
+ ensemble: null,
482
+ modelSelection: {
483
+ requested: taskShape.explicitRequest.fableModel,
484
+ policy: 'defer-to-fable-selector',
485
+ },
486
+ fallback,
487
+ reasonCodes,
488
+ };
489
+ }
490
+
491
+ function buildRouterContract(input = {}) {
492
+ const tier = normalizeTierLabel(input.recommendedTier);
493
+ const reasonCodes = uniqueReasonCodes(input.reasonCodes);
494
+ const explicitRequest = normalizeExplicitRequest(input.explicitRequest || {
495
+ fableModel: input.requestedFableModel,
496
+ });
497
+ const taskShape = buildTaskShape({
498
+ tier,
499
+ signals: input.signals,
500
+ hostCapabilities: input.hostCapabilities,
501
+ constraints: input.constraints,
502
+ explicitRequest,
503
+ reasonCodes,
504
+ });
505
+ const workflowPlan = buildWorkflowPlan({
506
+ routingStatus: input.routingStatus,
507
+ tier,
508
+ taskShape,
509
+ actionGateReasonCodes: input.actionGateReasonCodes,
510
+ reasonCodes,
511
+ });
512
+
513
+ return {
514
+ schemaVersion: SCHEMA_VERSION,
515
+ routingStatus: workflowPlan.routingStatus,
516
+ classification: {
517
+ tier,
518
+ label: input.recommendedTier || 'Unclassified',
519
+ rationale: input.rationale || 'No classification rationale available.',
520
+ reasonCodes,
521
+ },
522
+ taskShape,
523
+ workflowPlan,
524
+ };
525
+ }
526
+
527
+ function createDegradedRouterContract(reasonCode, message) {
528
+ return buildRouterContract({
529
+ routingStatus: 'degraded',
530
+ recommendedTier: 'Unclassified',
531
+ rationale: message || 'Structured router contract unavailable.',
532
+ reasonCodes: [reasonCode || 'router-contract-unavailable'],
533
+ signals: {},
534
+ requestedFableModel: null,
535
+ });
536
+ }
537
+
538
+ function validateWorkflowPlan(plan) {
539
+ const errors = [];
540
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
541
+ return { valid: false, errors: ['workflow plan must be an object'] };
542
+ }
543
+ if (plan.schemaVersion !== SCHEMA_VERSION) errors.push('unsupported schemaVersion');
544
+ if (!ROUTING_STATUSES.has(plan.routingStatus)) errors.push('invalid routingStatus');
545
+ if (!TIERS.has(plan.tier)) errors.push('invalid tier');
546
+ if (plan.strategy !== null && !STRATEGIES.has(plan.strategy)) errors.push('invalid strategy');
547
+ if (!STRATEGY_SELECTIONS.has(plan.strategySelection)) errors.push('invalid strategySelection');
548
+ if (plan.strategySelection === 'selected' && plan.strategy === null) errors.push('selected strategy must not be null');
549
+ if (plan.strategySelection === 'deferred' && plan.strategy !== null) errors.push('deferred strategy must be null');
550
+ if (!Array.isArray(plan.requiredInvariants)) errors.push('requiredInvariants must be an array');
551
+ if (!Array.isArray(plan.suggestedSkills)) errors.push('suggestedSkills must be an array');
552
+ if (!plan.actionGate || typeof plan.actionGate.required !== 'boolean') errors.push('actionGate.required must be boolean');
553
+ if (plan.actionGate && plan.actionGate.required && plan.actionGate.disposition !== 'pending-approval') errors.push('required actionGate must be pending-approval before execution');
554
+ if (!plan.limits || !Object.prototype.hasOwnProperty.call(plan.limits, 'maxIterations')) errors.push('limits.maxIterations is required');
555
+ if (plan.strategy === 'iterative-single' && (!Number.isInteger(plan.limits.maxIterations) || plan.limits.maxIterations < 1)) errors.push('iterative-single requires maxIterations');
556
+ if (plan.strategy === 'fable-parallel' && (!plan.parallelism || plan.parallelism.allowed !== true)) errors.push('fable-parallel requires parallelism.allowed=true');
557
+ if (!plan.fallback || !['none', 'reduced', 'blocked'].includes(plan.fallback.disposition)) errors.push('fallback.disposition is invalid');
558
+ if (!Array.isArray(plan.reasonCodes)) errors.push('reasonCodes must be an array');
559
+ return { valid: errors.length === 0, errors };
560
+ }
561
+
562
+ function validateRouterContract(contract) {
563
+ const errors = [];
564
+ if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {
565
+ return { valid: false, errors: ['router contract must be an object'] };
566
+ }
567
+ if (contract.schemaVersion !== SCHEMA_VERSION) errors.push('unsupported contract schemaVersion');
568
+ if (!contract.classification || !TIERS.has(contract.classification.tier)) errors.push('classification.tier is invalid');
569
+ if (!contract.taskShape || contract.taskShape.schemaVersion !== SCHEMA_VERSION) errors.push('taskShape schemaVersion is invalid');
570
+ const workflowValidation = validateWorkflowPlan(contract.workflowPlan);
571
+ errors.push(...workflowValidation.errors.map(error => `workflowPlan: ${error}`));
572
+ if (
573
+ contract.workflowPlan &&
574
+ contract.workflowPlan.strategy === 'fable-parallel' &&
575
+ contract.workflowPlan.fallback &&
576
+ contract.workflowPlan.fallback.disposition !== 'blocked'
577
+ ) {
578
+ if (contract.taskShape.dependencyGraph !== 'independent') errors.push('fable-parallel requires independent dependencyGraph');
579
+ if (!['disjoint', 'read-only'].includes(contract.taskShape.writeSetOverlap)) errors.push('fable-parallel requires disjoint/read-only writeSet');
580
+ }
581
+ return { valid: errors.length === 0, errors };
582
+ }
583
+
584
+ function writeRouterContract(contractPath, contract) {
585
+ if (!contractPath) return;
586
+ const validation = validateRouterContract(contract);
587
+ if (!validation.valid) {
588
+ throw new Error(`Refusing to write invalid router contract: ${validation.errors.join('; ')}`);
589
+ }
590
+ const dir = path.dirname(contractPath);
591
+ fs.mkdirSync(dir, { recursive: true });
592
+ const tempPath = `${contractPath}.${process.pid}.tmp`;
593
+ fs.writeFileSync(tempPath, `${JSON.stringify(contract)}\n`, 'utf8');
594
+ fs.renameSync(tempPath, contractPath);
595
+ }
596
+
597
+ module.exports = {
598
+ SCHEMA_VERSION,
599
+ ITERATIVE_MAX_ITERATIONS,
600
+ buildRouterContract,
601
+ buildTaskShape,
602
+ buildWorkflowPlan,
603
+ createDegradedRouterContract,
604
+ normalizeConstraints,
605
+ normalizeExplicitRequest,
606
+ normalizeHostCapabilities,
607
+ normalizeTierLabel,
608
+ selectWorkflowStrategy,
609
+ validateRouterContract,
610
+ validateWorkflowPlan,
611
+ writeRouterContract,
612
+ };