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
@@ -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) {
@@ -19,6 +19,15 @@ export interface Objective {
19
19
  status: TaskStatus;
20
20
  createdAt: number;
21
21
  completedAt?: number;
22
+ /** Human phase label for large projects (e.g. "architecture", "frontend"). */
23
+ phase?: string;
24
+ /**
25
+ * Minimum model benchmark (0–100) this phase should route to. Lets the
26
+ * orchestrator assign a more capable model to demanding phases (architecture,
27
+ * verification) and a cheaper one to light phases (docs) — instead of one
28
+ * model for the whole mission. Flows into each task's routing floor.
29
+ */
30
+ minBenchmark?: number;
22
31
  }
23
32
  export interface Task {
24
33
  id: string;
@@ -39,6 +48,11 @@ export interface Task {
39
48
  startedAt?: number;
40
49
  completedAt?: number;
41
50
  error?: string;
51
+ /** Routing floor for this task/phase (0–100). Escalates upward on repeated
52
+ * verification failure so the task climbs to a more capable model. */
53
+ minBenchmark?: number;
54
+ /** How many times this task's output failed verification — drives model escalation. */
55
+ verificationFailures?: number;
42
56
  }
43
57
  export interface TaskInput {
44
58
  prompt: string;
@@ -258,6 +272,12 @@ export interface Checkpoint {
258
272
  workerStates: WorkerInstanceState[];
259
273
  timestamp: number;
260
274
  reason: 'task' | 'interval' | 'pause' | 'crash' | 'manual';
275
+ /** Recent event log for this mission — restored so long-running/crash recovery
276
+ * keeps its activity trail. */
277
+ logs?: KernelEvent[];
278
+ /** Free-form memory bag persisted with the checkpoint (mission.metadata plus
279
+ * any harness-supplied working memory). */
280
+ memory?: Record<string, unknown>;
261
281
  }
262
282
  export interface WorkerInstanceState {
263
283
  workerId: string;
@@ -267,7 +287,7 @@ export interface WorkerInstanceState {
267
287
  modelId?: string;
268
288
  partialOutput?: string;
269
289
  }
270
- export type KernelEventType = 'MISSION_STARTED' | 'MISSION_COMPLETED' | 'MISSION_FAILED' | 'MISSION_PAUSED' | 'MISSION_RESUMED' | 'MISSION_CANCELLED' | 'OBJECTIVE_STARTED' | 'OBJECTIVE_COMPLETED' | 'TASK_QUEUED' | 'TASK_STARTED' | 'TASK_COMPLETED' | 'TASK_FAILED' | 'WORKER_SPAWNED' | 'WORKER_EXITED' | 'ROUTING_DECISION' | 'MODEL_CHANGED' | 'PROVIDER_CHANGED' | 'CHECKPOINT' | 'VERIFY_SUCCESS' | 'VERIFY_FAILED' | 'RECOVERY' | 'PROVIDER_HEALTH' | 'NOTIFICATION' | 'LOG';
290
+ export type KernelEventType = 'MISSION_STARTED' | 'MISSION_COMPLETED' | 'MISSION_FAILED' | 'MISSION_PAUSED' | 'MISSION_RESUMED' | 'MISSION_CANCELLED' | 'PLAN_CREATED' | 'OBJECTIVE_STARTED' | 'OBJECTIVE_COMPLETED' | 'TASK_QUEUED' | 'TASK_STARTED' | 'TASK_COMPLETED' | 'TASK_FAILED' | 'WORKER_SPAWNED' | 'WORKER_EXITED' | 'ROUTING_DECISION' | 'MODEL_CHANGED' | 'PROVIDER_CHANGED' | 'CHECKPOINT' | 'VERIFY_SUCCESS' | 'VERIFY_FAILED' | 'RECOVERY' | 'PROVIDER_HEALTH' | 'NOTIFICATION' | 'LOG';
271
291
  export interface KernelEvent<T = unknown> {
272
292
  type: KernelEventType;
273
293
  timestamp: number;
@@ -290,6 +310,12 @@ export interface RoutingRequest {
290
310
  * preventing "request too large" failures on rate-limited tiers.
291
311
  */
292
312
  estimatedRequestTokens?: number;
313
+ /**
314
+ * Minimum acceptable model benchmark (0–100). Models below the floor are
315
+ * heavily penalized (not hard-excluded), so demanding phases route to capable
316
+ * models and escalation can climb the tier ladder.
317
+ */
318
+ minBenchmark?: number;
293
319
  }
294
320
  export interface RoutingDecision {
295
321
  providerId: string;
@@ -382,13 +408,16 @@ export interface LlmJudgeOptions {
382
408
  modelId?: string;
383
409
  }
384
410
  export type FailureType = '429' | 'timeout' | 'provider-down' | 'hallucination' | 'tool-failure' | 'verification-failure' | 'planning-failure' | 'browser-failure' | 'unknown';
385
- export type RecoveryAction = 'retry' | 'rotate-provider' | 'spawn-different-worker' | 'replan' | 'checkpoint' | 'continue' | 'abort';
411
+ export type RecoveryAction = 'retry' | 'rotate-provider' | 'escalate-model' | 'spawn-different-worker' | 'replan' | 'checkpoint' | 'continue' | 'abort';
386
412
  export interface RecoveryDecision {
387
413
  action: RecoveryAction;
388
414
  reason: string;
389
415
  rotateToProviderId?: string;
390
416
  rotateToModelId?: string;
391
417
  newWorkerType?: WorkerType;
418
+ /** For 'escalate-model': the benchmark of the more-capable target, so the
419
+ * orchestrator can raise the task's routing floor and keep climbing. */
420
+ escalateToBenchmark?: number;
392
421
  }
393
422
  export interface PluginManifest {
394
423
  name: string;
@@ -442,6 +471,21 @@ export interface MissionInput {
442
471
  policy?: PolicyName;
443
472
  workspace?: WorkspaceRef;
444
473
  tasks?: MissionTaskInput[];
474
+ /**
475
+ * Explicit phase breakdown (each phase → one objective). When present it wins
476
+ * over `tasks`. Harnesses (e.g. InfiniBot) can pass phases directly with
477
+ * capabilities written naturally ("backend", "ui", "tests") — they're
478
+ * normalized to canonical capabilities and each phase routes to its own model
479
+ * tier via its benchmark floor.
480
+ */
481
+ phases?: MissionPhaseInput[];
482
+ /**
483
+ * When true and neither `tasks` nor `phases` is given, the MissionPlanner
484
+ * decomposes `goal` into phases (LLM-driven, heuristic fallback) before
485
+ * execution — turning a one-line goal into architecture → implementation →
486
+ * verification phases, each with an appropriate model.
487
+ */
488
+ autoPlan?: boolean;
445
489
  metadata?: Record<string, unknown>;
446
490
  }
447
491
  export interface MissionTaskInput {
@@ -450,6 +494,25 @@ export interface MissionTaskInput {
450
494
  dependencies?: string[];
451
495
  input: TaskInput;
452
496
  }
497
+ /**
498
+ * A phase of a mission — becomes one Objective plus its tasks. Capabilities may
499
+ * be written naturally (natural language / synonyms) and are normalized; the
500
+ * phase's model tier is derived from them unless `minBenchmark` is set.
501
+ */
502
+ export interface MissionPhaseInput {
503
+ /** Short phase label, e.g. "architecture", "backend", "frontend", "tests". */
504
+ name: string;
505
+ description: string;
506
+ /** Needed capabilities — natural names allowed ("backend", "ui", "testing"). */
507
+ capabilities?: string[];
508
+ acceptanceCriteria?: string[];
509
+ /** Names of phases that must finish before this one starts. */
510
+ dependsOn?: string[];
511
+ /** Explicit tasks; if omitted the phase runs as a single task from its description. */
512
+ tasks?: MissionTaskInput[];
513
+ /** Explicit model benchmark floor (0–100); else derived from capabilities. */
514
+ minBenchmark?: number;
515
+ }
453
516
  export interface ProviderVerifyResult {
454
517
  ok: boolean;
455
518
  latencyMs: number;
@@ -1,4 +1,7 @@
1
1
  import { scoreFrontend } from './frontend-scoring.js';
2
+ import { runCommand, tail } from './verification-exec.js';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
2
5
  const PIPELINE_ORDER = [
3
6
  'objective',
4
7
  'compile',
@@ -169,38 +172,91 @@ function objectiveVerifier() {
169
172
  };
170
173
  }
171
174
  function compileVerifier() {
172
- return {
173
- stage: 'compile',
174
- enabled: ctx => hasArtifact(ctx.output, 'code') || hasArtifact(ctx.output, 'file'),
175
- async run(ctx) {
176
- const codeArtifacts = ctx.output.artifacts?.filter(a => a.type === 'code' || a.type === 'file') ?? [];
177
- if (codeArtifacts.length === 0) {
178
- return { name: 'compile', passed: true, message: 'no code artifacts' };
179
- }
180
- const hasSyntax = codeArtifacts.every(a => (a.content ?? '').trim().length > 0);
181
- return {
182
- name: 'compile',
183
- passed: hasSyntax,
184
- message: hasSyntax ? `${codeArtifacts.length} artifact(s) non-empty` : 'empty code artifact',
185
- };
186
- },
187
- };
175
+ return commandVerifier('compile', 'compile');
188
176
  }
189
177
  function testsVerifier() {
190
- return {
191
- stage: 'tests',
192
- enabled: () => false,
193
- async run() {
194
- return { name: 'tests', passed: true, message: 'test runner not configured' };
195
- },
196
- };
178
+ return commandVerifier('tests', 'test');
197
179
  }
198
180
  function lintVerifier() {
181
+ return commandVerifier('lint', 'lint');
182
+ }
183
+ /** package.json script names to auto-detect per stage, in priority order. */
184
+ const SCRIPT_CANDIDATES = {
185
+ compile: ['build', 'compile', 'typecheck'],
186
+ test: ['test'],
187
+ lint: ['lint'],
188
+ };
189
+ /** True when the task actually produced code/file artifacts to verify. */
190
+ function producedCode(output) {
191
+ return hasArtifact(output, 'code') || hasArtifact(output, 'file');
192
+ }
193
+ /** Read `<root>/package.json` scripts, or undefined if missing/invalid. */
194
+ function readScripts(root) {
195
+ try {
196
+ const raw = readFileSync(join(root, 'package.json'), 'utf8');
197
+ const pkg = JSON.parse(raw);
198
+ return pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : undefined;
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ }
204
+ /** Pull an explicit command for `kind` from task context, then mission metadata. */
205
+ function explicitCommand(ctx, kind) {
206
+ const sources = [ctx.task.input.context?.verify, ctx.mission.metadata?.verify];
207
+ for (const src of sources) {
208
+ if (src && typeof src === 'object') {
209
+ const cmd = src[kind];
210
+ if (typeof cmd === 'string' && cmd.trim().length > 0)
211
+ return cmd.trim();
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+ /**
217
+ * Resolve the command for a stage: explicit context/metadata command first,
218
+ * else an auto-detected `npm run <script>` from the workspace package.json.
219
+ * Returns undefined when nothing can be resolved (stage stays disabled).
220
+ */
221
+ function resolveCommand(ctx, kind) {
222
+ const explicit = explicitCommand(ctx, kind);
223
+ if (explicit)
224
+ return explicit;
225
+ const scripts = readScripts(ctx.workspace.root);
226
+ if (!scripts)
227
+ return undefined;
228
+ for (const name of SCRIPT_CANDIDATES[kind]) {
229
+ if (typeof scripts[name] === 'string' && scripts[name].trim().length > 0) {
230
+ return `npm run ${name}`;
231
+ }
232
+ }
233
+ return undefined;
234
+ }
235
+ /**
236
+ * A verifier that runs a real project command (compile/tests/lint) in the
237
+ * mission workspace root and passes/fails on the process exit code. Enabled
238
+ * only when a command resolves AND the task produced code/file artifacts.
239
+ */
240
+ function commandVerifier(stage, kind) {
199
241
  return {
200
- stage: 'lint',
201
- enabled: () => false,
202
- async run() {
203
- return { name: 'lint', passed: true, message: 'linter not configured' };
242
+ stage,
243
+ enabled(ctx) {
244
+ return producedCode(ctx.output) && resolveCommand(ctx, kind) !== undefined;
245
+ },
246
+ async run(ctx) {
247
+ const command = resolveCommand(ctx, kind);
248
+ if (!command) {
249
+ // Should not happen (gated by enabled), but stay safe & non-blocking.
250
+ return { name: stage, passed: true, message: `no ${kind} command resolved` };
251
+ }
252
+ const result = await runCommand(command, ctx.workspace.root);
253
+ const passed = result.code === 0 && !result.timedOut;
254
+ const combined = tail(`${result.stdout}\n${result.stderr}`.trim());
255
+ const status = result.timedOut ? 'timed out' : `exit ${result.code}`;
256
+ const message = passed
257
+ ? `\`${command}\` passed (${status})`
258
+ : `\`${command}\` failed (${status})${combined ? `\n${combined}` : ''}`;
259
+ return { name: stage, passed, message };
204
260
  },
205
261
  };
206
262
  }
@@ -0,0 +1,14 @@
1
+ export interface ExecResult {
2
+ code: number | null;
3
+ stdout: string;
4
+ stderr: string;
5
+ timedOut: boolean;
6
+ }
7
+ /** Keep only the last `max` characters of a string. */
8
+ export declare function tail(text: string, max?: number): string;
9
+ /**
10
+ * Run `command` in `cwd` via a shell, capturing stdout/stderr. On Windows we
11
+ * use `shell: true` so npm scripts / .cmd shims resolve. Resolves (never
12
+ * rejects) with the exit code and captured output; kills the process on timeout.
13
+ */
14
+ export declare function runCommand(command: string, cwd: string, timeoutMs?: number): Promise<ExecResult>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * OpenKernel — Verification Exec Helper
3
+ *
4
+ * Runs a real project command (compile/tests/lint) in a workspace directory
5
+ * and reports pass/fail on the process exit code. Effect-free, no new deps —
6
+ * just node:child_process.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ const DEFAULT_TIMEOUT_MS = 180_000;
10
+ const MAX_TAIL_CHARS = 4000;
11
+ /** Keep only the last `max` characters of a string. */
12
+ export function tail(text, max = MAX_TAIL_CHARS) {
13
+ if (text.length <= max)
14
+ return text;
15
+ return `…${text.slice(text.length - max)}`;
16
+ }
17
+ /**
18
+ * Run `command` in `cwd` via a shell, capturing stdout/stderr. On Windows we
19
+ * use `shell: true` so npm scripts / .cmd shims resolve. Resolves (never
20
+ * rejects) with the exit code and captured output; kills the process on timeout.
21
+ */
22
+ export function runCommand(command, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
23
+ return new Promise(resolve => {
24
+ let stdout = '';
25
+ let stderr = '';
26
+ let timedOut = false;
27
+ let settled = false;
28
+ const child = spawn(command, {
29
+ cwd,
30
+ shell: true,
31
+ windowsHide: true,
32
+ });
33
+ const timer = setTimeout(() => {
34
+ timedOut = true;
35
+ try {
36
+ child.kill('SIGKILL');
37
+ }
38
+ catch {
39
+ /* already gone */
40
+ }
41
+ }, timeoutMs);
42
+ child.stdout?.on('data', (chunk) => {
43
+ stdout += chunk.toString();
44
+ if (stdout.length > MAX_TAIL_CHARS * 2)
45
+ stdout = tail(stdout, MAX_TAIL_CHARS * 2);
46
+ });
47
+ child.stderr?.on('data', (chunk) => {
48
+ stderr += chunk.toString();
49
+ if (stderr.length > MAX_TAIL_CHARS * 2)
50
+ stderr = tail(stderr, MAX_TAIL_CHARS * 2);
51
+ });
52
+ const finish = (code) => {
53
+ if (settled)
54
+ return;
55
+ settled = true;
56
+ clearTimeout(timer);
57
+ resolve({ code, stdout, stderr, timedOut });
58
+ };
59
+ child.on('error', err => {
60
+ stderr += `\n${err instanceof Error ? err.message : String(err)}`;
61
+ finish(1);
62
+ });
63
+ child.on('close', code => finish(code));
64
+ });
65
+ }
@@ -109,7 +109,7 @@ export class WorkerRuntime {
109
109
  instance.status = 'INITIALIZED';
110
110
  const plan = override
111
111
  ? this.lockedPlan(override, 'recovery override')
112
- : await this.planRoute(task.capabilities, handler.preferences, instance.type, estimateRequestTokens(task));
112
+ : await this.planRoute(task.capabilities, handler.preferences, instance.type, estimateRequestTokens(task), task.minBenchmark);
113
113
  // Surface the orchestrator's choice (ranked candidates + winner) so the
114
114
  // CLI / TUI can animate and highlight it.
115
115
  this.emitRouting(instance, task, plan);
@@ -189,7 +189,7 @@ export class WorkerRuntime {
189
189
  * router considered. Honors worker pins (a hard lock), otherwise defers to
190
190
  * the policy-driven capability router.
191
191
  */
192
- async planRoute(capabilities, preferences, workerType, estimatedRequestTokens) {
192
+ async planRoute(capabilities, preferences, workerType, estimatedRequestTokens, minBenchmark) {
193
193
  const providers = (await this.ctx.providerManager.getHealthyProviders()).map(p => p.info);
194
194
  const models = await this.ctx.providerManager.getAvailableModels();
195
195
  const policy = this.ctx.policies.get(this.defaultPolicyName);
@@ -214,6 +214,7 @@ export class WorkerRuntime {
214
214
  availableProviders: providers,
215
215
  availableModels: models,
216
216
  estimatedRequestTokens,
217
+ minBenchmark,
217
218
  });
218
219
  if (ranked.length === 0) {
219
220
  return { chosen: null, candidates: [], locked: false, mode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",