harness-everything 0.8.0 → 0.9.0

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 (102) 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 +51 -0
  6. package/README.md +2 -2
  7. package/benchmarks/results/reviews/pr142-verification.json +95 -0
  8. package/ci/action-gate.test.js +83 -3
  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/phase4-ensemble-memory.test.js +128 -10
  16. package/ci/router-workflow-plan.test.js +23 -0
  17. package/ci/workflow-execution-contract.test.js +133 -0
  18. package/ci/workflow-plan-consumers.test.js +18 -0
  19. package/docs/architecture.md +111 -114
  20. package/docs/mechanism-first-skill-mesh.md +92 -103
  21. package/docs/openai-plugin.md +1 -1
  22. package/docs/philosophy.md +40 -33
  23. package/docs/platform-capabilities.md +2 -2
  24. package/docs/reviews/pr-142-issue-131.md +89 -0
  25. package/docs/routing.md +61 -124
  26. package/docs/workflow-runtime.md +109 -0
  27. package/fable-mode/CONTRACT-FORMAT.md +3 -1
  28. package/fable-mode/SKILL.md +1 -1
  29. package/fable-mode/agents/fable-orchestrator.md +9 -7
  30. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  31. package/fable-mode/fable-haiku/SKILL.md +1 -1
  32. package/fable-mode/fable-opus/SKILL.md +1 -1
  33. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  34. package/fable-mode/scripts/workflow-plan-consumer.js +13 -8
  35. package/harness-everything/SKILL.md +11 -10
  36. package/harness-everything/references/triage-and-tiers.md +76 -107
  37. package/harness-everything/schemas/router-task-shape.schema.json +4 -2
  38. package/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
  39. package/harness-everything/scripts/kernel-router-core.js +265 -0
  40. package/harness-everything/scripts/kernel-router.js +118 -223
  41. package/harness-everything/scripts/router-contract-core.js +633 -0
  42. package/harness-everything/scripts/router-contract.js +21 -581
  43. package/harness-everything/scripts/tier-router.js +19 -1
  44. package/hooks/hooks.json +25 -4
  45. package/hooks/scripts/action-gate.js +111 -2
  46. package/hooks/scripts/contract-test.js +27 -2
  47. package/hooks/scripts/lib/workflow-isolation.js +102 -0
  48. package/hooks/scripts/lib/workflow-runtime.js +254 -0
  49. package/hooks/scripts/state-persist.js +1 -0
  50. package/hooks/scripts/subagent-scope-guard.js +20 -3
  51. package/hooks/scripts/workflow-disposition.js +104 -0
  52. package/hooks/scripts/workflow-gate.js +56 -0
  53. package/hooks/scripts/workflow-stop-gate.js +44 -0
  54. package/multi-agent-workspace/SKILL.md +2 -2
  55. package/multi-agent-workspace/scripts/index_memory.js +107 -1
  56. package/opencode-plugin/plugin.json +1 -1
  57. package/package.json +2 -2
  58. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  59. package/plugins/harness-everything/hooks/hooks.json +44 -1
  60. package/plugins/harness-everything/hooks/scripts/action-gate.js +111 -2
  61. package/plugins/harness-everything/hooks/scripts/contract-test.js +27 -2
  62. package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +102 -0
  63. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +254 -0
  64. package/plugins/harness-everything/hooks/scripts/state-persist.js +1 -0
  65. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +20 -3
  66. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +104 -0
  67. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +56 -0
  68. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +44 -0
  69. package/plugins/harness-everything/plugin.json +1 -1
  70. package/plugins/harness-everything/scripts/lib/advisory-text.js +51 -54
  71. package/plugins/harness-everything/skills/fable-mode/CONTRACT-FORMAT.md +3 -1
  72. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  73. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +9 -7
  74. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  75. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  76. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  77. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  78. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +13 -8
  79. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -10
  80. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +76 -107
  81. package/plugins/harness-everything/skills/harness-everything/schemas/router-task-shape.schema.json +4 -2
  82. package/plugins/harness-everything/skills/harness-everything/schemas/router-workflow-plan.schema.json +3 -2
  83. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +265 -0
  84. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +118 -223
  85. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +633 -0
  86. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +21 -581
  87. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +19 -1
  88. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +2 -2
  89. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/index_memory.js +107 -1
  90. package/plugins/harness-everything/skills/self-evolve/SKILL.md +6 -6
  91. package/plugins/harness-everything/skills/self-evolve/references/memory-resolution.md +22 -33
  92. package/plugins/harness-everything/skills/self-evolve/scripts/persist-memory.js +336 -57
  93. package/plugins/harness-everything/skills/self-evolve/scripts/self-regression.js +1 -0
  94. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +14 -16
  95. package/plugins/harness-everything/skills/using-git-worktrees/references/workflow-details.md +44 -85
  96. package/scripts/lib/advisory-text.js +51 -54
  97. package/self-evolve/SKILL.md +6 -6
  98. package/self-evolve/references/memory-resolution.md +22 -33
  99. package/self-evolve/scripts/persist-memory.js +336 -57
  100. package/self-evolve/scripts/self-regression.js +1 -0
  101. package/using-git-worktrees/SKILL.md +14 -16
  102. package/using-git-worktrees/references/workflow-details.md +44 -85
@@ -1,526 +1,34 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
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
- 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
- };
8
+ function isMajorWorkflow(plan) {
9
+ return Boolean(plan && (plan.tier === 'tier3' || String(plan.strategy || '').startsWith('fable-')));
314
10
  }
315
11
 
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;
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
- 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
- };
24
+ return augmentWorkflowPlan(core.buildWorkflowPlan(input));
489
25
  }
490
26
 
491
27
  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
-
28
+ const contract = core.buildRouterContract(input);
513
29
  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,
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
- SCHEMA_VERSION,
599
- ITERATIVE_MAX_ITERATIONS,
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
  };
@@ -74,6 +74,14 @@ function detectProhibitions(prompt) {
74
74
  return rules.filter(([, regex]) => regex.test(prompt)).map(([name]) => name);
75
75
  }
76
76
 
77
+ function detectMemoryPersistenceRequest(prompt) {
78
+ const text = String(prompt || '');
79
+ return /^\s*self-evolve\b/i.test(text) ||
80
+ /\b(?:run|use|invoke|execute)\s+(?:the\s+)?self-evolve\b/i.test(text) ||
81
+ /\b(?:persist|save|record|remember)\b.{0,40}\b(?:lesson|memory|rule|insight)\b/i.test(text) ||
82
+ /(?:執行|使用).{0,8}自我進化|(?:記住|保存|持久化|記錄).{0,20}(?:教訓|記憶|規則|經驗)/i.test(text);
83
+ }
84
+
77
85
  function detectActionGateReasons(prompt) {
78
86
  const reasons = [];
79
87
  const irreversible = [
@@ -226,6 +234,7 @@ function run(userPrompt, context) {
226
234
  console.log(`[Tier Routing Pre-check]`);
227
235
 
228
236
  const promptLower = userPrompt.toLowerCase();
237
+ const memoryPersistenceRequested = detectMemoryPersistenceRequest(userPrompt);
229
238
  const loadedConfig = loadRoutingConfig();
230
239
  const routingConfig = loadedConfig.config;
231
240
  const TIER3_KEYWORDS = routingConfig.tiers.tier3 || [];
@@ -251,7 +260,12 @@ function run(userPrompt, context) {
251
260
  let rationale = 'No structural/testing signals matched; unclassified is not equivalent to trivial.';
252
261
  const reasonCodes = [...loadedConfig.reasonCodes, 'no-classification-signal'];
253
262
 
254
- if (isTrivialDocsEdit) {
263
+ if (memoryPersistenceRequested && !hasMacroSignal && !hasTier3Keyword && !hasTier2Keyword) {
264
+ recommendedTier = 'Tier 1 (Trivial)';
265
+ rationale = 'Explicit bounded self-evolve memory persistence request.';
266
+ reasonCodes.splice(loadedConfig.reasonCodes.length);
267
+ addReason(reasonCodes, 'memory-persistence-requested');
268
+ } else if (isTrivialDocsEdit) {
255
269
  recommendedTier = 'Tier 1 (Trivial)';
256
270
  rationale = 'Single documentation typo detected - direct edit, no checklist required.';
257
271
  reasonCodes.splice(loadedConfig.reasonCodes.length);
@@ -409,11 +423,15 @@ function run(userPrompt, context) {
409
423
  highUncertainty,
410
424
  irreversibleAction,
411
425
  externalSideEffect,
426
+ memoryPersistenceRequested,
412
427
  highRisk: irreversibleAction || externalSideEffect,
413
428
  },
414
429
  });
415
430
 
416
431
  console.log(`\n=> WORKFLOW STRATEGY: ${contract.workflowPlan.strategy || 'deferred'}`);
432
+ if (contract.workflowPlan.memory.write !== 'none') {
433
+ console.log(`=> MEMORY WRITE: ${contract.workflowPlan.memory.write}`);
434
+ }
417
435
  if (contract.workflowPlan.actionGate.required) {
418
436
  console.log(`=> ACTION GATE: required (${contract.workflowPlan.actionGate.reasonCodes.join(', ')}); disposition=pending-approval`);
419
437
  }