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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +12 -2
- package/AGENTS.md +36 -33
- package/CHANGELOG.md +23 -0
- package/README.md +9 -8
- package/benchmarks/results/reviews/pr142-verification.json +95 -0
- package/ci/action-gate.test.js +1 -1
- package/ci/doc-capability-consistency.test.js +3 -0
- package/ci/invariant-routing.test.js +105 -34
- package/ci/mechanism-2af-worktree-isolation.test.js +152 -0
- package/ci/mechanism-2ag-router-worktree-contract.test.js +44 -0
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +140 -0
- package/ci/openai-plugin-package.test.js +14 -7
- package/ci/workflow-execution-contract.test.js +60 -0
- package/docs/architecture.md +112 -106
- package/docs/mechanism-first-skill-mesh.md +93 -99
- package/docs/openai-plugin.md +1 -1
- package/docs/philosophy.md +40 -30
- package/docs/platform-capabilities.md +10 -2
- package/docs/reviews/pr-142-issue-131.md +89 -0
- package/docs/routing.md +68 -103
- package/docs/workflow-runtime.md +109 -0
- package/fable-mode/CONTRACT-FORMAT.md +3 -1
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +3 -1
- package/harness-everything/SKILL.md +17 -23
- package/harness-everything/references/triage-and-tiers.md +81 -98
- package/harness-everything/scripts/kernel-router-core.js +263 -0
- package/harness-everything/scripts/kernel-router.js +97 -212
- package/harness-everything/scripts/router-contract-core.js +612 -0
- package/harness-everything/scripts/router-contract.js +21 -581
- package/hooks/hooks.json +22 -1
- package/hooks/scripts/contract-test.js +24 -2
- package/hooks/scripts/lib/workflow-isolation.js +102 -0
- package/hooks/scripts/lib/workflow-runtime.js +102 -0
- package/hooks/scripts/state-persist.js +1 -0
- package/hooks/scripts/workflow-disposition.js +97 -0
- package/hooks/scripts/workflow-gate.js +53 -0
- package/hooks/scripts/workflow-stop-gate.js +44 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +20 -1
- package/plugins/harness-everything/hooks/scripts/contract-test.js +24 -2
- package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +102 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +102 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -0
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +97 -0
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +53 -0
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +44 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +53 -54
- package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +3 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +3 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +17 -23
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +81 -98
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +263 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +97 -212
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +612 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +21 -581
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +14 -16
- package/plugins/harness-everything/skills/using-git-worktrees/references/workflow-details.md +44 -85
- package/scripts/lib/advisory-text.js +53 -54
- package/using-git-worktrees/SKILL.md +14 -16
- 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
|
+
};
|