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.
- 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 -35
- package/CHANGELOG.md +17 -0
- package/README.md +2 -2
- 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 +87 -74
- 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 +9 -6
- package/ci/workflow-execution-contract.test.js +60 -0
- package/docs/architecture.md +111 -114
- package/docs/mechanism-first-skill-mesh.md +92 -103
- package/docs/openai-plugin.md +1 -1
- package/docs/philosophy.md +40 -33
- package/docs/platform-capabilities.md +2 -2
- package/docs/reviews/pr-142-issue-131.md +89 -0
- package/docs/routing.md +61 -124
- 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 +11 -10
- package/harness-everything/references/triage-and-tiers.md +76 -107
- package/harness-everything/scripts/kernel-router-core.js +263 -0
- package/harness-everything/scripts/kernel-router.js +97 -225
- 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 +51 -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 +11 -10
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +76 -107
- 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 -225
- 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 +51 -54
- package/using-git-worktrees/SKILL.md +14 -16
- package/using-git-worktrees/references/workflow-details.md +44 -85
|
@@ -1,526 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Runtime policy wrapper around the Phase-2 router contract. Keep the original
|
|
4
|
+
// topology selector stable in router-contract-core.js, then add lifecycle
|
|
5
|
+
// invariants that are orthogonal to strategy selection.
|
|
6
|
+
const core = require('./router-contract-core');
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
};
|
|
8
|
+
function isMajorWorkflow(plan) {
|
|
9
|
+
return Boolean(plan && (plan.tier === 'tier3' || String(plan.strategy || '').startsWith('fable-')));
|
|
314
10
|
}
|
|
315
11
|
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
12
|
+
function augmentWorkflowPlan(plan) {
|
|
13
|
+
if (!plan || !isMajorWorkflow(plan)) return plan;
|
|
14
|
+
const requiredInvariants = Array.isArray(plan.requiredInvariants) ? [...plan.requiredInvariants] : [];
|
|
15
|
+
const suggestedSkills = Array.isArray(plan.suggestedSkills) ? [...plan.suggestedSkills] : [];
|
|
16
|
+
if (!requiredInvariants.includes('isolated-worktree-before-mutation')) requiredInvariants.push('isolated-worktree-before-mutation');
|
|
17
|
+
if (!suggestedSkills.includes('using-git-worktrees')) suggestedSkills.push('using-git-worktrees');
|
|
18
|
+
return { ...plan, requiredInvariants, suggestedSkills, mutationIsolation: {
|
|
19
|
+
required: true, mechanism: 'git-worktree', transition: 'before-first-mutation', onUnavailable: 'blocked',
|
|
20
|
+
} };
|
|
408
21
|
}
|
|
409
22
|
|
|
410
23
|
function buildWorkflowPlan(input = {}) {
|
|
411
|
-
|
|
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
|
-
};
|
|
24
|
+
return augmentWorkflowPlan(core.buildWorkflowPlan(input));
|
|
489
25
|
}
|
|
490
26
|
|
|
491
27
|
function buildRouterContract(input = {}) {
|
|
492
|
-
const
|
|
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
|
-
|
|
28
|
+
const contract = core.buildRouterContract(input);
|
|
513
29
|
return {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
classification: {
|
|
517
|
-
tier,
|
|
518
|
-
label: input.recommendedTier || 'Unclassified',
|
|
519
|
-
rationale: input.rationale || 'No classification rationale available.',
|
|
520
|
-
reasonCodes,
|
|
521
|
-
},
|
|
522
|
-
taskShape,
|
|
523
|
-
workflowPlan,
|
|
30
|
+
...contract,
|
|
31
|
+
workflowPlan: augmentWorkflowPlan(contract.workflowPlan),
|
|
524
32
|
};
|
|
525
33
|
}
|
|
526
34
|
|
|
@@ -535,78 +43,10 @@ function createDegradedRouterContract(reasonCode, message) {
|
|
|
535
43
|
});
|
|
536
44
|
}
|
|
537
45
|
|
|
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
46
|
module.exports = {
|
|
598
|
-
|
|
599
|
-
|
|
47
|
+
...core,
|
|
48
|
+
augmentWorkflowPlan,
|
|
600
49
|
buildRouterContract,
|
|
601
|
-
buildTaskShape,
|
|
602
50
|
buildWorkflowPlan,
|
|
603
51
|
createDegradedRouterContract,
|
|
604
|
-
normalizeConstraints,
|
|
605
|
-
normalizeExplicitRequest,
|
|
606
|
-
normalizeHostCapabilities,
|
|
607
|
-
normalizeTierLabel,
|
|
608
|
-
selectWorkflowStrategy,
|
|
609
|
-
validateRouterContract,
|
|
610
|
-
validateWorkflowPlan,
|
|
611
|
-
writeRouterContract,
|
|
612
52
|
};
|
package/hooks/hooks.json
CHANGED
|
@@ -21,11 +21,22 @@
|
|
|
21
21
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/harness-everything/scripts/kernel-router.js\""
|
|
22
22
|
}
|
|
23
23
|
],
|
|
24
|
-
"description": "Establish Harness routing invariants before host/domain skill selection",
|
|
24
|
+
"description": "Establish Harness routing invariants and an active workflow execution contract before host/domain skill selection",
|
|
25
25
|
"id": "harness:pre:kernel-router"
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"PreToolUse": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash|PowerShell|Edit|Write|apply_patch",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/workflow-gate.js\""
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"description": "Enforce mandatory workflow entry and Tier 3 Git-worktree mutation isolation",
|
|
38
|
+
"id": "harness:pre:workflow-gate"
|
|
39
|
+
},
|
|
29
40
|
{
|
|
30
41
|
"matcher": "Bash|PowerShell",
|
|
31
42
|
"hooks": [
|
|
@@ -104,6 +115,16 @@
|
|
|
104
115
|
"description": "Close any unexecuted action-gate approvals as rejected for this turn",
|
|
105
116
|
"id": "harness:stop:action-gate-audit"
|
|
106
117
|
},
|
|
118
|
+
{
|
|
119
|
+
"hooks": [
|
|
120
|
+
{
|
|
121
|
+
"type": "command",
|
|
122
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/workflow-stop-gate.js\""
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"description": "Prevent completion while a selected Fable workflow has not reached verified stage resolution",
|
|
126
|
+
"id": "harness:stop:workflow-completion-gate"
|
|
127
|
+
},
|
|
107
128
|
{
|
|
108
129
|
"hooks": [
|
|
109
130
|
{
|