infinicode 2.3.2 → 2.3.4

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 (43) hide show
  1. package/.opencode/plugins/mesh-commands.tsx +139 -0
  2. package/dist/cli.js +2 -1
  3. package/dist/commands/mcp.js +2 -0
  4. package/dist/commands/run.js +100 -3
  5. package/dist/commands/serve.d.ts +1 -0
  6. package/dist/commands/serve.js +36 -1
  7. package/dist/kernel/capability-map.d.ts +18 -0
  8. package/dist/kernel/capability-map.js +84 -0
  9. package/dist/kernel/checkpoint-engine.js +9 -0
  10. package/dist/kernel/command-system.d.ts +3 -0
  11. package/dist/kernel/command-system.js +315 -10
  12. package/dist/kernel/config-schema.d.ts +11 -0
  13. package/dist/kernel/config-schema.js +1 -0
  14. package/dist/kernel/federation/event-bridge.js +3 -1
  15. package/dist/kernel/federation/federation.d.ts +10 -0
  16. package/dist/kernel/federation/federation.js +29 -9
  17. package/dist/kernel/federation/transport-http.d.ts +3 -0
  18. package/dist/kernel/federation/transport-http.js +25 -0
  19. package/dist/kernel/free-providers.d.ts +23 -0
  20. package/dist/kernel/free-providers.js +73 -1
  21. package/dist/kernel/index.d.ts +4 -0
  22. package/dist/kernel/index.js +3 -0
  23. package/dist/kernel/kernel.d.ts +12 -0
  24. package/dist/kernel/kernel.js +29 -2
  25. package/dist/kernel/mission-engine.js +14 -0
  26. package/dist/kernel/orchestrator.d.ts +3 -0
  27. package/dist/kernel/orchestrator.js +74 -2
  28. package/dist/kernel/planner.d.ts +42 -0
  29. package/dist/kernel/planner.js +174 -0
  30. package/dist/kernel/recovery-manager.d.ts +9 -0
  31. package/dist/kernel/recovery-manager.js +64 -3
  32. package/dist/kernel/router.d.ts +12 -1
  33. package/dist/kernel/router.js +36 -1
  34. package/dist/kernel/scheduler.d.ts +17 -0
  35. package/dist/kernel/scheduler.js +97 -1
  36. package/dist/kernel/setup.d.ts +9 -0
  37. package/dist/kernel/setup.js +38 -0
  38. package/dist/kernel/types.d.ts +65 -2
  39. package/dist/kernel/verification-engine.js +83 -27
  40. package/dist/kernel/verification-exec.d.ts +14 -0
  41. package/dist/kernel/verification-exec.js +65 -0
  42. package/dist/kernel/worker-runtime.js +3 -2
  43. package/package.json +1 -1
@@ -32,6 +32,10 @@ export type { GeminiProviderOptions } from './providers/gemini-provider.js';
32
32
  export { ALL_BUILTIN_WORKERS, createResearchWorker, createCodingWorker, createArchitectureWorker, createReviewWorker, createDocumentationWorker, createTranslationWorker, createTerminalWorker, createVerificationWorker, createVisionWorker, createFrontendWorker, createBrowserWorker, } from './workers/builtin-workers.js';
33
33
  export { KernelCommandRegistry, DEFAULT_COMMANDS } from './command-system.js';
34
34
  export type { KernelCommand, CommandContext } from './command-system.js';
35
+ export { MissionPlanner } from './planner.js';
36
+ export type { MissionPlannerDeps, PlanOptions } from './planner.js';
37
+ export { normalizeCapabilities, CANONICAL_CAPABILITIES } from './capability-map.js';
38
+ export { phaseBenchmarkFloor, phaseLabel } from './scheduler.js';
35
39
  export * from './federation/index.js';
36
40
  export { createMcpServer, serveMcpStdio } from './mcp/index.js';
37
41
  export type { McpServerDeps } from './mcp/index.js';
@@ -24,6 +24,9 @@ export { OpenAICompatibleProvider } from './providers/openai-compatible-provider
24
24
  export { GeminiProvider } from './providers/gemini-provider.js';
25
25
  export { ALL_BUILTIN_WORKERS, createResearchWorker, createCodingWorker, createArchitectureWorker, createReviewWorker, createDocumentationWorker, createTranslationWorker, createTerminalWorker, createVerificationWorker, createVisionWorker, createFrontendWorker, createBrowserWorker, } from './workers/builtin-workers.js';
26
26
  export { KernelCommandRegistry, DEFAULT_COMMANDS } from './command-system.js';
27
+ export { MissionPlanner } from './planner.js';
28
+ export { normalizeCapabilities, CANONICAL_CAPABILITIES } from './capability-map.js';
29
+ export { phaseBenchmarkFloor, phaseLabel } from './scheduler.js';
27
30
  // Federation — neutral device mesh (peers, telemetry, config sync, MOLTFED interop)
28
31
  export * from './federation/index.js';
29
32
  // MCP — north-facing control surface (spawn/dispatch/role/voice/map over the mesh)
@@ -20,6 +20,8 @@ import { PolicyEngine } from './policies.js';
20
20
  import { VerificationEngine } from './verification-engine.js';
21
21
  import { RecoveryManager } from './recovery-manager.js';
22
22
  import { KernelCommandRegistry } from './command-system.js';
23
+ import { MissionPlanner } from './planner.js';
24
+ import type { Federation } from './federation/federation.js';
23
25
  import type { CommandResult, EventHandler, KernelEvent, KernelEventType, KernelLike, Logger, Mission, MissionInput, Plugin, Policy, ProviderInterface, WorkerDefinition, MissionStatus } from './types.js';
24
26
  export interface KernelOptions {
25
27
  logger?: Logger;
@@ -42,8 +44,18 @@ export declare class Kernel implements KernelLike {
42
44
  readonly logger: Logger;
43
45
  /** Canonical OpenKernel slash-command surface (/status, /workers, /goal …). */
44
46
  readonly commands: KernelCommandRegistry;
47
+ /** Decomposes a goal into phases when a mission is run with `autoPlan`. */
48
+ readonly planner: MissionPlanner;
49
+ /**
50
+ * The mesh node this kernel is part of, once `infinicode serve`/`mcp` wires it
51
+ * in via {@link attachFederation}. Undefined when running stand-alone. Node
52
+ * commands (/nodes, /node, /activity …) read it off the command context.
53
+ */
54
+ federation?: Federation;
45
55
  private missions;
46
56
  constructor(options?: KernelOptions);
57
+ /** Attach the federation mesh node so node/mesh slash-commands can reach it. */
58
+ attachFederation(federation: Federation): void;
47
59
  execute(input: MissionInput): Promise<Mission>;
48
60
  pause(missionId: string): Promise<void>;
49
61
  resume(missionId: string): Promise<Mission>;
@@ -22,6 +22,7 @@ import { RecoveryManager } from './recovery-manager.js';
22
22
  import { ConsoleLogger } from './logger.js';
23
23
  import { ALL_BUILTIN_WORKERS } from './workers/builtin-workers.js';
24
24
  import { KernelCommandRegistry } from './command-system.js';
25
+ import { MissionPlanner } from './planner.js';
25
26
  export class Kernel {
26
27
  eventBus;
27
28
  providerManager;
@@ -38,6 +39,14 @@ export class Kernel {
38
39
  logger;
39
40
  /** Canonical OpenKernel slash-command surface (/status, /workers, /goal …). */
40
41
  commands;
42
+ /** Decomposes a goal into phases when a mission is run with `autoPlan`. */
43
+ planner;
44
+ /**
45
+ * The mesh node this kernel is part of, once `infinicode serve`/`mcp` wires it
46
+ * in via {@link attachFederation}. Undefined when running stand-alone. Node
47
+ * commands (/nodes, /node, /activity …) read it off the command context.
48
+ */
49
+ federation;
41
50
  missions = new Map();
42
51
  constructor(options = {}) {
43
52
  this.logger = options.logger ?? new ConsoleLogger();
@@ -89,17 +98,35 @@ export class Kernel {
89
98
  });
90
99
  this.plugins = new PluginManager(this.eventBus, this.logger);
91
100
  this.commands = new KernelCommandRegistry(this);
101
+ this.planner = new MissionPlanner({
102
+ providerManager: this.providerManager,
103
+ router: this.router,
104
+ logger: this.logger,
105
+ });
92
106
  if (options.loadBuiltinWorkers ?? true) {
93
107
  for (const handler of ALL_BUILTIN_WORKERS) {
94
108
  this.workerRuntime.registerHandler(handler);
95
109
  }
96
110
  }
97
111
  }
112
+ /** Attach the federation mesh node so node/mesh slash-commands can reach it. */
113
+ attachFederation(federation) {
114
+ this.federation = federation;
115
+ }
98
116
  async execute(input) {
99
- const mission = this.missionEngine.create(input);
117
+ // Auto-planning: decompose a bare goal into phases before execution when
118
+ // asked and no tasks/phases were supplied. Each phase → its own objective +
119
+ // model tier (see MissionPlanner + scheduler.planPhases).
120
+ let effective = input;
121
+ if (input.autoPlan && !input.tasks?.length && !input.phases?.length) {
122
+ const policy = this.policies.resolveRef(input.policy).policy;
123
+ const phases = await this.planner.plan(input.goal, { description: input.description, policy });
124
+ effective = { ...input, phases };
125
+ }
126
+ const mission = this.missionEngine.create(effective);
100
127
  this.missions.set(mission.id, mission);
101
128
  try {
102
- await this.orchestrator.run(mission, input);
129
+ await this.orchestrator.run(mission, effective);
103
130
  }
104
131
  catch (err) {
105
132
  const message = err instanceof Error ? err.message : String(err);
@@ -38,6 +38,20 @@ export class MissionEngine {
38
38
  mission.tasks = tasks;
39
39
  this.transition(mission, 'READY');
40
40
  this.deps.logger.info(`Mission planned: ${mission.id} — ${tasks.length} task(s)`);
41
+ // Broadcast the phase breakdown so harnesses (InfiniBot mesh map) can render
42
+ // the plan + each phase's capabilities and model tier.
43
+ this.emit(mission, 'PLAN_CREATED', {
44
+ missionId: mission.id,
45
+ goal: mission.goal,
46
+ phases: objectives.map(o => ({
47
+ id: o.id,
48
+ phase: o.phase,
49
+ description: o.description,
50
+ capabilities: o.capabilities,
51
+ minBenchmark: o.minBenchmark,
52
+ taskCount: o.taskIds.length,
53
+ })),
54
+ });
41
55
  return mission;
42
56
  }
43
57
  start(mission) {
@@ -36,6 +36,9 @@ export declare class Orchestrator {
36
36
  private taskOverrides;
37
37
  /** Per-task forced worker type override set by the recovery manager. */
38
38
  private taskWorkerOverrides;
39
+ /** How many times each task has been replanned (bounds the replan loop). */
40
+ private taskReplans;
41
+ private maxReplans;
39
42
  constructor(deps: OrchestratorDeps);
40
43
  run(mission: Mission, input: MissionInput): Promise<Mission>;
41
44
  private runTasks;
@@ -7,6 +7,9 @@ export class Orchestrator {
7
7
  taskOverrides = new Map();
8
8
  /** Per-task forced worker type override set by the recovery manager. */
9
9
  taskWorkerOverrides = new Map();
10
+ /** How many times each task has been replanned (bounds the replan loop). */
11
+ taskReplans = new Map();
12
+ maxReplans = 2;
10
13
  constructor(deps) {
11
14
  this.deps = deps;
12
15
  }
@@ -68,6 +71,15 @@ export class Orchestrator {
68
71
  }
69
72
  }
70
73
  async executeTask(mission, task) {
74
+ // Per-phase model assignment: inherit this task's objective (phase) benchmark
75
+ // floor so demanding phases (architecture, verification) route to a more
76
+ // capable model tier than light phases — instead of one model for the whole
77
+ // mission. Escalation may later raise this floor further.
78
+ if (task.minBenchmark === undefined) {
79
+ const objective = mission.objectives.find(o => o.id === task.objectiveId);
80
+ if (objective?.minBenchmark)
81
+ task.minBenchmark = objective.minBenchmark;
82
+ }
71
83
  const overrideType = this.taskWorkerOverrides.get(task.id);
72
84
  let workerType = overrideType ?? this.deps.workerRuntime.matchWorkerType(task.capabilities);
73
85
  if (!workerType) {
@@ -90,7 +102,8 @@ export class Orchestrator {
90
102
  // Per-task verification via the Verification Engine pipeline.
91
103
  const vresult = await this.deps.verification.verify(mission, task, mission.policy.policy);
92
104
  if (!vresult.passed) {
93
- // Treat as a verification-failure recovery case.
105
+ // Count verification misses repeated misses escalate the model tier.
106
+ task.verificationFailures = (task.verificationFailures ?? 0) + 1;
94
107
  await this.handleRecovery(mission, task, `verification failed: ${vresult.summary}`, instance);
95
108
  }
96
109
  }
@@ -112,6 +125,7 @@ export class Orchestrator {
112
125
  policy: mission.policy.policy,
113
126
  currentProviderId: instance.providerId,
114
127
  currentModelId: instance.modelId,
128
+ verificationFailures: task.verificationFailures,
115
129
  });
116
130
  const maxAttempts = mission.policy.policy.retry?.maxAttempts ?? 3;
117
131
  switch (decision.action) {
@@ -144,6 +158,35 @@ export class Orchestrator {
144
158
  task.error = message;
145
159
  break;
146
160
  }
161
+ case 'escalate-model': {
162
+ // Climb the benchmark ladder to a more capable model and raise this
163
+ // task's routing floor so it keeps escalating on further failures.
164
+ if (decision.rotateToProviderId && decision.rotateToModelId) {
165
+ this.taskOverrides.set(task.id, {
166
+ providerId: decision.rotateToProviderId,
167
+ modelId: decision.rotateToModelId,
168
+ });
169
+ if (decision.escalateToBenchmark) {
170
+ task.minBenchmark = Math.max(task.minBenchmark ?? 0, decision.escalateToBenchmark);
171
+ }
172
+ this.deps.eventBus.emit({
173
+ type: 'MODEL_CHANGED',
174
+ timestamp: Date.now(),
175
+ missionId: mission.id,
176
+ taskId: task.id,
177
+ data: {
178
+ from: { providerId: instance.providerId, modelId: instance.modelId },
179
+ to: { providerId: decision.rotateToProviderId, modelId: decision.rotateToModelId },
180
+ reason: decision.reason,
181
+ escalated: true,
182
+ minBenchmark: task.minBenchmark,
183
+ },
184
+ });
185
+ }
186
+ task.status = 'PENDING';
187
+ task.error = message;
188
+ break;
189
+ }
147
190
  case 'spawn-different-worker': {
148
191
  if (decision.newWorkerType) {
149
192
  this.taskWorkerOverrides.set(task.id, decision.newWorkerType);
@@ -153,7 +196,36 @@ export class Orchestrator {
153
196
  task.error = message;
154
197
  break;
155
198
  }
156
- case 'replan':
199
+ case 'replan': {
200
+ // Real replan: checkpoint, then restart the task with a CLEAN slate —
201
+ // drop the provider/model + worker overrides so it re-routes from
202
+ // scratch, and reset the attempt/verification counters. Bounded by
203
+ // maxReplans so an unsatisfiable task can't loop forever.
204
+ const replans = (this.taskReplans.get(task.id) ?? 0) + 1;
205
+ this.taskReplans.set(task.id, replans);
206
+ await this.deps.checkpoints.save(mission, this.collectWorkerStates(mission.id), 'crash');
207
+ if (replans > this.maxReplans) {
208
+ task.status = 'FAILED';
209
+ task.error = `${message} (replan limit reached)`;
210
+ this.deps.eventBus.emit({
211
+ type: 'TASK_FAILED',
212
+ timestamp: Date.now(),
213
+ missionId: mission.id,
214
+ taskId: task.id,
215
+ workerId: instance.id,
216
+ data: { error: task.error, attempts: task.attempts, replans },
217
+ });
218
+ break;
219
+ }
220
+ this.taskOverrides.delete(task.id);
221
+ this.taskWorkerOverrides.delete(task.id);
222
+ task.verificationFailures = 0;
223
+ task.attempts = 0;
224
+ task.status = 'PENDING';
225
+ task.error = message;
226
+ this.deps.logger.info(`Task ${task.id}: replan #${replans} — fresh routing + counters`);
227
+ break;
228
+ }
157
229
  case 'checkpoint': {
158
230
  await this.deps.checkpoints.save(mission, this.collectWorkerStates(mission.id), 'crash');
159
231
  task.status = 'PENDING';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * OpenKernel — Mission Planner
3
+ *
4
+ * Decomposes a one-line goal into a sequence of PHASES (each becomes an
5
+ * objective), so a large build spreads across the right models by aspect
6
+ * (architecture → implementation → verification) instead of one model
7
+ * one-shotting the whole thing. Each phase carries the capabilities it needs;
8
+ * the scheduler turns those into a per-phase model benchmark floor.
9
+ *
10
+ * LLM-driven when a capable provider is available, with a deterministic
11
+ * heuristic fallback so planning never blocks on model availability.
12
+ */
13
+ import type { Logger, MissionPhaseInput, Policy } from './types.js';
14
+ import type { ProviderManager } from './provider-manager.js';
15
+ import type { CapabilityRouter } from './router.js';
16
+ import { normalizeCapabilities } from './capability-map.js';
17
+ export interface MissionPlannerDeps {
18
+ providerManager: ProviderManager;
19
+ router: CapabilityRouter;
20
+ logger: Logger;
21
+ }
22
+ export interface PlanOptions {
23
+ description?: string;
24
+ policy?: Policy;
25
+ /** Max phases to request (default 6). */
26
+ maxPhases?: number;
27
+ }
28
+ export declare class MissionPlanner {
29
+ private deps;
30
+ constructor(deps: MissionPlannerDeps);
31
+ plan(goal: string, opts?: PlanOptions): Promise<MissionPhaseInput[]>;
32
+ private planWithLlm;
33
+ /** Extract + validate a phase array from a model's (possibly noisy) response. */
34
+ private parsePhases;
35
+ /**
36
+ * Deterministic fallback plan. Always yields buildable phases; adds a frontend
37
+ * phase for UI-shaped goals. Capabilities are natural — the scheduler
38
+ * normalizes them and derives each phase's model tier.
39
+ */
40
+ private heuristicPhases;
41
+ }
42
+ export { normalizeCapabilities };
@@ -0,0 +1,174 @@
1
+ import { CANONICAL_CAPABILITIES, normalizeCapabilities } from './capability-map.js';
2
+ export class MissionPlanner {
3
+ deps;
4
+ constructor(deps) {
5
+ this.deps = deps;
6
+ }
7
+ async plan(goal, opts = {}) {
8
+ const llm = await this.planWithLlm(goal, opts).catch(err => {
9
+ this.deps.logger.warn(`[planner] LLM planning failed: ${err instanceof Error ? err.message : String(err)}`);
10
+ return null;
11
+ });
12
+ if (llm && llm.length) {
13
+ this.deps.logger.info(`[planner] LLM decomposed goal into ${llm.length} phase(s): ${llm.map(p => p.name).join(' → ')}`);
14
+ return llm;
15
+ }
16
+ const heuristic = this.heuristicPhases(goal);
17
+ this.deps.logger.info(`[planner] heuristic plan: ${heuristic.map(p => p.name).join(' → ')}`);
18
+ return heuristic;
19
+ }
20
+ async planWithLlm(goal, opts) {
21
+ const providers = (await this.deps.providerManager.getHealthyProviders()).map(p => p.info);
22
+ const models = await this.deps.providerManager.getAvailableModels();
23
+ if (providers.length === 0 || models.length === 0)
24
+ return null;
25
+ // Route the planning call to a capable reasoning model.
26
+ const decision = await this.deps.router.route({
27
+ capabilities: ['planning', 'reasoning'],
28
+ preferences: { reasoning: 85 },
29
+ policy: opts.policy ?? { name: 'balanced', routing: { mode: 'balanced' } },
30
+ availableProviders: providers,
31
+ availableModels: models,
32
+ minBenchmark: 82,
33
+ });
34
+ if (!decision)
35
+ return null;
36
+ const provider = this.deps.providerManager.get(decision.providerId);
37
+ if (!provider)
38
+ return null;
39
+ const maxPhases = opts.maxPhases ?? 6;
40
+ const request = {
41
+ model: decision.modelId,
42
+ systemPrompt: 'You are a senior software project planner for an execution kernel. Break the goal into 2–' +
43
+ `${maxPhases} sequential PHASES. Reply with ONLY minified JSON, no prose, of the form: ` +
44
+ '{"phases":[{"name":"...","description":"...","capabilities":["..."],"dependsOn":["<earlier phase name>"],"acceptanceCriteria":["..."]}]}. ' +
45
+ 'Order phases so later ones dependsOn earlier ones. Prefer capability names from this list where they fit: ' +
46
+ CANONICAL_CAPABILITIES.join(', ') +
47
+ '. You may also use natural terms like backend, ui, tests, devops. Keep it concise and buildable.',
48
+ prompt: `Goal: ${goal}${opts.description ? `\nContext: ${opts.description}` : ''}\n\nPhased plan (JSON):`,
49
+ maxTokens: 1200,
50
+ temperature: 0.2,
51
+ };
52
+ const result = await provider.complete(request);
53
+ const parsed = this.parsePhases(result.content);
54
+ if (!parsed || parsed.length === 0)
55
+ return null;
56
+ return parsed.slice(0, maxPhases);
57
+ }
58
+ /** Extract + validate a phase array from a model's (possibly noisy) response. */
59
+ parsePhases(text) {
60
+ const json = extractJson(text);
61
+ if (!json)
62
+ return null;
63
+ let raw;
64
+ try {
65
+ raw = JSON.parse(json);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ const list = Array.isArray(raw)
71
+ ? raw
72
+ : Array.isArray(raw?.phases)
73
+ ? raw.phases
74
+ : null;
75
+ if (!list)
76
+ return null;
77
+ const phases = [];
78
+ const seen = new Set();
79
+ for (const item of list) {
80
+ if (!item || typeof item !== 'object')
81
+ continue;
82
+ const p = item;
83
+ const name = String(p.name ?? p.phase ?? `phase-${phases.length + 1}`).trim().slice(0, 40);
84
+ if (!name || seen.has(name.toLowerCase()))
85
+ continue;
86
+ seen.add(name.toLowerCase());
87
+ const description = String(p.description ?? name).trim();
88
+ const capabilities = Array.isArray(p.capabilities) ? p.capabilities.map(String) : undefined;
89
+ const dependsOn = Array.isArray(p.dependsOn) ? p.dependsOn.map(String) : undefined;
90
+ const acceptanceCriteria = Array.isArray(p.acceptanceCriteria)
91
+ ? p.acceptanceCriteria.map(String)
92
+ : undefined;
93
+ phases.push({ name, description, capabilities, dependsOn, acceptanceCriteria });
94
+ }
95
+ return phases.length ? phases : null;
96
+ }
97
+ /**
98
+ * Deterministic fallback plan. Always yields buildable phases; adds a frontend
99
+ * phase for UI-shaped goals. Capabilities are natural — the scheduler
100
+ * normalizes them and derives each phase's model tier.
101
+ */
102
+ heuristicPhases(goal) {
103
+ const g = goal.toLowerCase();
104
+ const isUi = /\b(ui|ux|frontend|front-end|web|website|page|css|react|vue|svelte|design|dashboard|landing)\b/.test(g);
105
+ const phases = [
106
+ {
107
+ name: 'architecture',
108
+ description: `Plan the architecture, approach, and file/component breakdown for: ${goal}`,
109
+ capabilities: ['architecture', 'planning', 'reasoning'],
110
+ acceptanceCriteria: ['A concrete plan / component breakdown exists'],
111
+ },
112
+ {
113
+ name: 'implementation',
114
+ description: `Implement the core of: ${goal}`,
115
+ capabilities: ['coding', 'filesystem', 'terminal', 'reasoning'],
116
+ dependsOn: ['architecture'],
117
+ acceptanceCriteria: ['Core functionality is implemented'],
118
+ },
119
+ ];
120
+ if (isUi) {
121
+ phases.push({
122
+ name: 'frontend',
123
+ description: `Build the user interface for: ${goal}`,
124
+ capabilities: ['frontend', 'design', 'accessibility'],
125
+ dependsOn: ['architecture'],
126
+ acceptanceCriteria: ['A polished, responsive, accessible UI'],
127
+ });
128
+ }
129
+ phases.push({
130
+ name: 'verification',
131
+ description: `Test, review, and verify the implementation of: ${goal}`,
132
+ capabilities: ['verification', 'review', 'reasoning'],
133
+ dependsOn: isUi ? ['implementation', 'frontend'] : ['implementation'],
134
+ acceptanceCriteria: ['Builds, tests pass, and the goal is met'],
135
+ });
136
+ return phases;
137
+ }
138
+ }
139
+ /** Pull the first balanced JSON object/array out of a text blob. */
140
+ function extractJson(text) {
141
+ const trimmed = text.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
142
+ const start = trimmed.search(/[[{]/);
143
+ if (start === -1)
144
+ return null;
145
+ const open = trimmed[start];
146
+ const close = open === '{' ? '}' : ']';
147
+ let depth = 0;
148
+ let inStr = false;
149
+ let esc = false;
150
+ for (let i = start; i < trimmed.length; i++) {
151
+ const ch = trimmed[i];
152
+ if (inStr) {
153
+ if (esc)
154
+ esc = false;
155
+ else if (ch === '\\')
156
+ esc = true;
157
+ else if (ch === '"')
158
+ inStr = false;
159
+ continue;
160
+ }
161
+ if (ch === '"')
162
+ inStr = true;
163
+ else if (ch === open)
164
+ depth++;
165
+ else if (ch === close) {
166
+ depth--;
167
+ if (depth === 0)
168
+ return trimmed.slice(start, i + 1);
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+ // Re-export so harnesses can normalize capabilities the same way.
174
+ export { normalizeCapabilities };
@@ -27,6 +27,8 @@ export interface RecoveryContext {
27
27
  policy: Policy;
28
28
  currentProviderId?: string;
29
29
  currentModelId?: string;
30
+ /** How many times this task has failed verification — drives model escalation. */
31
+ verificationFailures?: number;
30
32
  }
31
33
  export declare class RecoveryManager {
32
34
  private deps;
@@ -34,5 +36,12 @@ export declare class RecoveryManager {
34
36
  classify(error: string): FailureType;
35
37
  decide(ctx: RecoveryContext): Promise<RecoveryDecision>;
36
38
  private findAlternateProvider;
39
+ /**
40
+ * Find a strictly MORE capable model than the one that just failed
41
+ * verification — the escalation ladder. Ranks across all healthy providers by
42
+ * benchmark and returns the best above the current model's tier, or null at
43
+ * the ceiling.
44
+ */
45
+ private findMoreCapableModel;
37
46
  private findAlternateWorker;
38
47
  }
@@ -33,6 +33,7 @@ export class RecoveryManager {
33
33
  let rotateToProviderId;
34
34
  let rotateToModelId;
35
35
  let newWorkerType;
36
+ let escalateToBenchmark;
36
37
  switch (failureType) {
37
38
  case '429':
38
39
  case 'provider-down': {
@@ -61,9 +62,22 @@ export class RecoveryManager {
61
62
  break;
62
63
  }
63
64
  case 'hallucination':
64
- case 'tool-failure':
65
65
  case 'verification-failure': {
66
- if (belowLimit) {
66
+ // Quality failures usually mean the model isn't strong enough. After the
67
+ // first verification miss, climb the benchmark ladder to a MORE capable
68
+ // model before anything else; only swap worker type if we're at the top.
69
+ const vFails = ctx.verificationFailures ?? 0;
70
+ // Escalate after MULTIPLE verification misses — the first miss retries or
71
+ // swaps worker; the second climbs to a more capable model.
72
+ const better = vFails >= 2 ? await this.findMoreCapableModel(ctx) : null;
73
+ if (better) {
74
+ action = 'escalate-model';
75
+ reason = `${failureType}: escalate → ${better.providerId}/${better.modelId} (bench ${better.benchmarkScore})`;
76
+ rotateToProviderId = better.providerId;
77
+ rotateToModelId = better.modelId;
78
+ escalateToBenchmark = better.benchmarkScore;
79
+ }
80
+ else if (belowLimit) {
67
81
  const altType = this.findAlternateWorker(ctx);
68
82
  if (altType) {
69
83
  action = 'spawn-different-worker';
@@ -77,7 +91,26 @@ export class RecoveryManager {
77
91
  }
78
92
  else {
79
93
  action = 'replan';
80
- reason = `${failureType}: attempts exhausted → replan`;
94
+ reason = `${failureType}: attempts exhausted, at model ceiling → replan`;
95
+ }
96
+ break;
97
+ }
98
+ case 'tool-failure': {
99
+ if (belowLimit) {
100
+ const altType = this.findAlternateWorker(ctx);
101
+ if (altType) {
102
+ action = 'spawn-different-worker';
103
+ reason = `tool-failure: spawn ${altType} worker`;
104
+ newWorkerType = altType;
105
+ }
106
+ else {
107
+ action = 'retry';
108
+ reason = `tool-failure: retry (attempt ${ctx.attempts}/${maxAttempts})`;
109
+ }
110
+ }
111
+ else {
112
+ action = 'replan';
113
+ reason = 'tool-failure: attempts exhausted → replan';
81
114
  }
82
115
  break;
83
116
  }
@@ -102,6 +135,7 @@ export class RecoveryManager {
102
135
  rotateToProviderId,
103
136
  rotateToModelId,
104
137
  newWorkerType,
138
+ escalateToBenchmark,
105
139
  };
106
140
  this.deps.eventBus.emit({
107
141
  type: 'RECOVERY',
@@ -135,6 +169,33 @@ export class RecoveryManager {
135
169
  return null;
136
170
  return { providerId: ranked[0].providerId, modelId: ranked[0].modelId };
137
171
  }
172
+ /**
173
+ * Find a strictly MORE capable model than the one that just failed
174
+ * verification — the escalation ladder. Ranks across all healthy providers by
175
+ * benchmark and returns the best above the current model's tier, or null at
176
+ * the ceiling.
177
+ */
178
+ async findMoreCapableModel(ctx) {
179
+ const providers = (await this.deps.providerManager.getHealthyProviders()).map(p => p.info);
180
+ const models = await this.deps.providerManager.getAvailableModels();
181
+ if (providers.length === 0 || models.length === 0)
182
+ return null;
183
+ const escalated = this.deps.router.escalate({
184
+ capabilities: ctx.task.capabilities,
185
+ preferences: { reasoning: 85 },
186
+ policy: ctx.policy,
187
+ availableProviders: providers,
188
+ availableModels: models,
189
+ minBenchmark: ctx.task.minBenchmark,
190
+ }, ctx.currentProviderId, ctx.currentModelId);
191
+ if (!escalated)
192
+ return null;
193
+ return {
194
+ providerId: escalated.providerId,
195
+ modelId: escalated.modelId,
196
+ benchmarkScore: escalated.benchmarkScore,
197
+ };
198
+ }
138
199
  findAlternateWorker(ctx) {
139
200
  const current = this.deps.workerRuntime.getHandler(this.deps.workerRuntime.matchWorkerType(ctx.task.capabilities) ?? 'coding');
140
201
  if (!current)
@@ -12,7 +12,7 @@
12
12
  * Every policy auto-routes across all healthy providers. The single exception
13
13
  * is the `offline` mode, which is LOCKED to local providers.
14
14
  */
15
- import type { ProviderInfo, RoutingDecision, RoutingRequest } from './types.js';
15
+ import type { ProviderInfo, ProviderModel, RoutingDecision, RoutingRequest } from './types.js';
16
16
  import type { Logger } from './types.js';
17
17
  export declare class CapabilityRouter {
18
18
  private logger;
@@ -22,6 +22,17 @@ export declare class CapabilityRouter {
22
22
  providerInfo: ProviderInfo;
23
23
  benchmarkScore: number;
24
24
  }>;
25
+ /** Public benchmark estimate for a model (0–100) — used by recovery/escalation. */
26
+ benchmarkFor(model: ProviderModel): number;
27
+ /**
28
+ * Pick a strictly MORE capable model than the current one for the same
29
+ * capabilities — the verification-failure escalation ladder. Ranks candidates
30
+ * whose benchmark exceeds the current model's, preferring the highest
31
+ * benchmark (tie-broken by overall score). Returns null at the ceiling.
32
+ */
33
+ escalate(request: RoutingRequest, currentProviderId?: string, currentModelId?: string): (RoutingDecision & {
34
+ benchmarkScore: number;
35
+ }) | null;
25
36
  private scoreCandidates;
26
37
  private capabilityScore;
27
38
  /**