infinicode 2.3.2 → 2.3.3

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 (39) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/commands/mcp.js +2 -0
  3. package/dist/commands/serve.d.ts +1 -0
  4. package/dist/commands/serve.js +36 -1
  5. package/dist/kernel/capability-map.d.ts +18 -0
  6. package/dist/kernel/capability-map.js +84 -0
  7. package/dist/kernel/checkpoint-engine.js +9 -0
  8. package/dist/kernel/command-system.d.ts +3 -0
  9. package/dist/kernel/command-system.js +315 -10
  10. package/dist/kernel/config-schema.d.ts +11 -0
  11. package/dist/kernel/config-schema.js +1 -0
  12. package/dist/kernel/federation/event-bridge.js +3 -1
  13. package/dist/kernel/federation/federation.d.ts +10 -0
  14. package/dist/kernel/federation/federation.js +28 -9
  15. package/dist/kernel/free-providers.d.ts +23 -0
  16. package/dist/kernel/free-providers.js +73 -1
  17. package/dist/kernel/index.d.ts +4 -0
  18. package/dist/kernel/index.js +3 -0
  19. package/dist/kernel/kernel.d.ts +12 -0
  20. package/dist/kernel/kernel.js +29 -2
  21. package/dist/kernel/mission-engine.js +14 -0
  22. package/dist/kernel/orchestrator.d.ts +3 -0
  23. package/dist/kernel/orchestrator.js +74 -2
  24. package/dist/kernel/planner.d.ts +42 -0
  25. package/dist/kernel/planner.js +174 -0
  26. package/dist/kernel/recovery-manager.d.ts +9 -0
  27. package/dist/kernel/recovery-manager.js +64 -3
  28. package/dist/kernel/router.d.ts +12 -1
  29. package/dist/kernel/router.js +36 -1
  30. package/dist/kernel/scheduler.d.ts +17 -0
  31. package/dist/kernel/scheduler.js +97 -1
  32. package/dist/kernel/setup.d.ts +9 -0
  33. package/dist/kernel/setup.js +38 -0
  34. package/dist/kernel/types.d.ts +65 -2
  35. package/dist/kernel/verification-engine.js +83 -27
  36. package/dist/kernel/verification-exec.d.ts +14 -0
  37. package/dist/kernel/verification-exec.js +65 -0
  38. package/dist/kernel/worker-runtime.js +3 -2
  39. package/package.json +1 -1
@@ -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
  /**
@@ -41,6 +41,34 @@ export class CapabilityRouter {
41
41
  benchmarkScore: c.benchmarkScore,
42
42
  }));
43
43
  }
44
+ /** Public benchmark estimate for a model (0–100) — used by recovery/escalation. */
45
+ benchmarkFor(model) {
46
+ return this.estimateBenchmark(model);
47
+ }
48
+ /**
49
+ * Pick a strictly MORE capable model than the current one for the same
50
+ * capabilities — the verification-failure escalation ladder. Ranks candidates
51
+ * whose benchmark exceeds the current model's, preferring the highest
52
+ * benchmark (tie-broken by overall score). Returns null at the ceiling.
53
+ */
54
+ escalate(request, currentProviderId, currentModelId) {
55
+ const current = request.availableModels.find(m => m.providerId === currentProviderId && m.id === currentModelId);
56
+ const currentBench = current ? this.estimateBenchmark(current) : (request.minBenchmark ?? 0);
57
+ const candidates = this.scoreCandidates(request)
58
+ .filter(c => !(c.providerId === currentProviderId && c.modelId === currentModelId))
59
+ .filter(c => c.benchmarkScore > currentBench + 1)
60
+ .sort((a, b) => b.benchmarkScore - a.benchmarkScore || b.score - a.score);
61
+ const top = candidates[0];
62
+ if (!top)
63
+ return null;
64
+ return {
65
+ providerId: top.providerId,
66
+ modelId: top.modelId,
67
+ score: top.score,
68
+ benchmarkScore: top.benchmarkScore,
69
+ reason: `escalate ↑bench ${currentBench}→${top.benchmarkScore}: ${top.reason}`,
70
+ };
71
+ }
44
72
  scoreCandidates(request) {
45
73
  const { capabilities, preferences, policy, availableProviders, availableModels } = request;
46
74
  const mode = (policy.routing?.mode ?? 'balanced');
@@ -86,6 +114,12 @@ export class CapabilityRouter {
86
114
  // Heavy penalty when the model can't fit the estimated request — keeps it
87
115
  // as a last resort but routes to a capable model when one exists.
88
116
  const capacityPenalty = needTokens > 0 && effectiveLimit < needTokens ? 6 : 0;
117
+ // Phase/escalation floor: models below the requested benchmark are pushed
118
+ // down (proportional, not hard-excluded) so demanding phases climb to a
119
+ // capable model and the escalation ladder can keep ascending.
120
+ const floorPenalty = request.minBenchmark && benchmark < request.minBenchmark
121
+ ? (request.minBenchmark - benchmark) / 5
122
+ : 0;
89
123
  const score = capScore * weights.capability +
90
124
  benchScore * weights.benchmark +
91
125
  reliability * weights.reliability +
@@ -96,7 +130,8 @@ export class CapabilityRouter {
96
130
  preferredBonus +
97
131
  fastApiBonus -
98
132
  costPenalty -
99
- capacityPenalty;
133
+ capacityPenalty -
134
+ floorPenalty;
100
135
  scored.push({
101
136
  providerId: provider.id,
102
137
  modelId: model.id,
@@ -8,6 +8,13 @@ export declare class Scheduler {
8
8
  objectives: Objective[];
9
9
  tasks: Task[];
10
10
  };
11
+ /**
12
+ * Build one objective per phase. Capabilities are normalized (natural names
13
+ * allowed), each phase's tasks depend on ALL tasks of the phases it dependsOn
14
+ * (so phases run in order but tasks within a phase can parallelize), and each
15
+ * phase gets its own model benchmark floor → different models per phase.
16
+ */
17
+ private planPhases;
11
18
  createTask(missionId: string, objectiveId: string, description: string, capabilities: Capability[], dependencies: string[], input: TaskInput): Task;
12
19
  /**
13
20
  * Returns the next batch of tasks that are ready to execute:
@@ -28,3 +35,13 @@ export declare class Scheduler {
28
35
  reconcileObjectives(mission: Mission): void;
29
36
  private aggregateCapabilities;
30
37
  }
38
+ /**
39
+ * Per-phase model benchmark floor (0–100). Demanding phases (architecture,
40
+ * verification, review, reasoning-heavy) demand a more capable model; lighter
41
+ * phases (docs, translation, plain search) leave the floor open so the router
42
+ * can pick a cheaper/faster model. This is what lets one large mission spread
43
+ * across models by aspect instead of one-shotting with a single model.
44
+ */
45
+ export declare function phaseBenchmarkFloor(caps: Capability[]): number;
46
+ /** Human-readable phase label from an objective's dominant capability. */
47
+ export declare function phaseLabel(caps: Capability[]): string;
@@ -5,6 +5,7 @@
5
5
  * Supports: sequential, parallel, dependency graphs
6
6
  */
7
7
  import { nanoid } from 'nanoid';
8
+ import { normalizeCapabilities } from './capability-map.js';
8
9
  export class Scheduler {
9
10
  eventBus;
10
11
  logger;
@@ -13,27 +14,81 @@ export class Scheduler {
13
14
  this.logger = logger;
14
15
  }
15
16
  planMission(missionId, input) {
17
+ // Phase-based plan (each phase → one objective, with cross-phase deps).
18
+ if (input.phases && input.phases.length > 0) {
19
+ return this.planPhases(missionId, input.phases);
20
+ }
16
21
  const taskInputs = input.tasks ?? [];
17
22
  const objectiveId = nanoid(10);
18
23
  const tasks = [];
19
24
  for (const ti of taskInputs) {
20
25
  tasks.push(this.createTask(missionId, objectiveId, ti.description, ti.capabilities, ti.dependencies ?? [], ti.input));
21
26
  }
27
+ const objCaps = this.aggregateCapabilities(tasks);
22
28
  const objective = {
23
29
  id: objectiveId,
24
30
  missionId,
25
31
  description: input.goal,
26
- capabilities: this.aggregateCapabilities(tasks),
32
+ capabilities: objCaps,
27
33
  acceptanceCriteria: [],
28
34
  taskIds: tasks.map(t => t.id),
29
35
  status: tasks.length > 0 ? 'PENDING' : 'COMPLETED',
30
36
  createdAt: Date.now(),
37
+ phase: phaseLabel(objCaps),
38
+ minBenchmark: tasks.reduce((max, t) => Math.max(max, t.minBenchmark ?? 0), 0) || undefined,
31
39
  };
32
40
  this.logger.info(`Mission planned: ${missionId} with ${tasks.length} task(s) across 1 objective`);
33
41
  return { objectives: [objective], tasks };
34
42
  }
43
+ /**
44
+ * Build one objective per phase. Capabilities are normalized (natural names
45
+ * allowed), each phase's tasks depend on ALL tasks of the phases it dependsOn
46
+ * (so phases run in order but tasks within a phase can parallelize), and each
47
+ * phase gets its own model benchmark floor → different models per phase.
48
+ */
49
+ planPhases(missionId, phases) {
50
+ const objectives = [];
51
+ const tasks = [];
52
+ const phaseTaskIds = new Map(); // phase name (lower) → task ids
53
+ for (const phase of phases) {
54
+ const objectiveId = nanoid(10);
55
+ const phaseCaps = normalizeCapabilities(phase.capabilities);
56
+ const deps = (phase.dependsOn ?? []).flatMap(n => phaseTaskIds.get(n.trim().toLowerCase()) ?? []);
57
+ const taskInputs = phase.tasks && phase.tasks.length > 0
58
+ ? phase.tasks
59
+ : [{ description: phase.description, capabilities: phaseCaps, input: { prompt: phase.description } }];
60
+ const phaseTasks = taskInputs.map(ti => {
61
+ const tCaps = ti.capabilities && ti.capabilities.length > 0
62
+ ? normalizeCapabilities(ti.capabilities)
63
+ : phaseCaps;
64
+ const task = this.createTask(missionId, objectiveId, ti.description, tCaps, [...deps, ...(ti.dependencies ?? [])], ti.input ?? { prompt: ti.description });
65
+ // Explicit phase floor overrides the capability-derived one.
66
+ if (phase.minBenchmark)
67
+ task.minBenchmark = phase.minBenchmark;
68
+ return task;
69
+ });
70
+ tasks.push(...phaseTasks);
71
+ phaseTaskIds.set(phase.name.trim().toLowerCase(), phaseTasks.map(t => t.id));
72
+ const derivedFloor = phaseTasks.reduce((max, t) => Math.max(max, t.minBenchmark ?? 0), 0);
73
+ objectives.push({
74
+ id: objectiveId,
75
+ missionId,
76
+ description: phase.description,
77
+ capabilities: phaseCaps,
78
+ acceptanceCriteria: phase.acceptanceCriteria ?? [],
79
+ taskIds: phaseTasks.map(t => t.id),
80
+ status: phaseTasks.length > 0 ? 'PENDING' : 'COMPLETED',
81
+ createdAt: Date.now(),
82
+ phase: phase.name,
83
+ minBenchmark: (phase.minBenchmark ?? derivedFloor) || undefined,
84
+ });
85
+ }
86
+ this.logger.info(`Mission planned: ${missionId} with ${tasks.length} task(s) across ${objectives.length} phase(s): ${phases.map(p => p.name).join(' → ')}`);
87
+ return { objectives, tasks };
88
+ }
35
89
  createTask(missionId, objectiveId, description, capabilities, dependencies, input) {
36
90
  const id = nanoid(10);
91
+ const floor = phaseBenchmarkFloor(capabilities);
37
92
  const task = {
38
93
  id,
39
94
  objectiveId,
@@ -46,6 +101,7 @@ export class Scheduler {
46
101
  attempts: 0,
47
102
  maxAttempts: 3,
48
103
  createdAt: Date.now(),
104
+ minBenchmark: floor > 0 ? floor : undefined,
49
105
  };
50
106
  this.eventBus.emit({ type: 'TASK_QUEUED', timestamp: Date.now(), missionId, objectiveId, taskId: id, data: { description } });
51
107
  return task;
@@ -145,3 +201,43 @@ export class Scheduler {
145
201
  return [...set];
146
202
  }
147
203
  }
204
+ /**
205
+ * Per-phase model benchmark floor (0–100). Demanding phases (architecture,
206
+ * verification, review, reasoning-heavy) demand a more capable model; lighter
207
+ * phases (docs, translation, plain search) leave the floor open so the router
208
+ * can pick a cheaper/faster model. This is what lets one large mission spread
209
+ * across models by aspect instead of one-shotting with a single model.
210
+ */
211
+ export function phaseBenchmarkFloor(caps) {
212
+ const has = (c) => caps.includes(c);
213
+ if (has('architecture') || has('planning'))
214
+ return 85;
215
+ if (has('verification') || has('review'))
216
+ return 84;
217
+ if (has('coding') || has('frontend'))
218
+ return 78;
219
+ if (has('vision') || has('ocr'))
220
+ return 74;
221
+ if (has('reasoning'))
222
+ return 80;
223
+ return 0; // search / crawl / summarize / documentation / translation → open
224
+ }
225
+ /** Human-readable phase label from an objective's dominant capability. */
226
+ export function phaseLabel(caps) {
227
+ const has = (c) => caps.includes(c);
228
+ if (has('architecture') || has('planning'))
229
+ return 'architecture';
230
+ if (has('frontend'))
231
+ return 'frontend';
232
+ if (has('coding'))
233
+ return 'implementation';
234
+ if (has('verification') || has('review'))
235
+ return 'verification';
236
+ if (has('vision') || has('ocr'))
237
+ return 'vision';
238
+ if (has('search') || has('crawl'))
239
+ return 'research';
240
+ if (has('documentation') || has('translation'))
241
+ return 'documentation';
242
+ return 'general';
243
+ }
@@ -15,6 +15,15 @@ export type { InfinicodeConfig, CloudProviderConfig, WorkerModelPref } from './c
15
15
  * count of enabled providers registered.
16
16
  */
17
17
  export declare function registerCloudProviders(kernel: Kernel, cloudProviders: CloudProviderConfig[]): number;
18
+ /**
19
+ * Register (or re-register) local OpenAI-compatible providers (LM Studio, vLLM,
20
+ * SGLang, llama.cpp) on a live kernel from config. Providers register with
21
+ * type 'local' and require NO API key — they are never skipped for a missing
22
+ * key the way cloud providers can be, and the local server does not need to be
23
+ * running (unreachable ones are marked unhealthy by the health check). Returns
24
+ * the count of enabled providers registered.
25
+ */
26
+ export declare function registerLocalProviders(kernel: Kernel, localProviders: CloudProviderConfig[]): number;
18
27
  export declare function buildKernelFromConfig(config: Conf<InfinicodeConfig>, options?: {
19
28
  logger?: Logger;
20
29
  }): Kernel;
@@ -27,6 +27,35 @@ export function registerCloudProviders(kernel, cloudProviders) {
27
27
  const options = {
28
28
  id: cfg.id,
29
29
  name: cfg.name,
30
+ type: cfg.type,
31
+ baseURL: cfg.baseURL,
32
+ apiKey: cfg.apiKey,
33
+ headerName: cfg.headerName,
34
+ knownModels: preset?.knownModels ?? [],
35
+ };
36
+ kernel.registerProvider(cfg.id, new OpenAICompatibleProvider(options));
37
+ n++;
38
+ }
39
+ return n;
40
+ }
41
+ /**
42
+ * Register (or re-register) local OpenAI-compatible providers (LM Studio, vLLM,
43
+ * SGLang, llama.cpp) on a live kernel from config. Providers register with
44
+ * type 'local' and require NO API key — they are never skipped for a missing
45
+ * key the way cloud providers can be, and the local server does not need to be
46
+ * running (unreachable ones are marked unhealthy by the health check). Returns
47
+ * the count of enabled providers registered.
48
+ */
49
+ export function registerLocalProviders(kernel, localProviders) {
50
+ let n = 0;
51
+ for (const cfg of localProviders) {
52
+ if (!cfg.enabled)
53
+ continue;
54
+ const preset = getProviderPreset(cfg.id);
55
+ const options = {
56
+ id: cfg.id,
57
+ name: cfg.name,
58
+ type: 'local',
30
59
  baseURL: cfg.baseURL,
31
60
  apiKey: cfg.apiKey,
32
61
  headerName: cfg.headerName,
@@ -52,6 +81,15 @@ export function buildKernelFromConfig(config, options) {
52
81
  kernel.registerProvider('ollama', new OllamaProvider(ollamaOptions));
53
82
  // Register cloud providers
54
83
  registerCloudProviders(kernel, config.get('cloudProviders') ?? []);
84
+ // Register local OpenAI-compatible providers (LM Studio, vLLM, SGLang,
85
+ // llama.cpp) the user has configured in `localProviders`. No API key is
86
+ // required; an unreachable server is marked unhealthy by the health check.
87
+ // Only configured ones are registered — this avoids probing dead localhost
88
+ // ports on every machine. The presets (`localProviderConfigs()` in
89
+ // free-providers.ts / the setup wizard) make enabling one a one-liner.
90
+ const localProviders = config.get('localProviders') ?? [];
91
+ if (localProviders.length > 0)
92
+ registerLocalProviders(kernel, localProviders);
55
93
  // Apply per-worker model pins — the worker runtime honors them when routing.
56
94
  const workerModels = config.get('workerModels') ?? {};
57
95
  if (workerModels && Object.keys(workerModels).length > 0) {