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
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { serve } from './commands/serve.js';
16
16
  import { mcpCommand } from './commands/mcp.js';
17
17
  import { installTui } from './commands/install-tui.js';
18
18
  import { DEFAULT_CONFIG } from './kernel/config-schema.js';
19
- const VERSION = '2.3.2';
19
+ const VERSION = '2.3.3';
20
20
  const config = new Conf({
21
21
  projectName: 'infinicode',
22
22
  defaults: DEFAULT_CONFIG,
@@ -291,6 +291,7 @@ program
291
291
  .option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
292
292
  .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
293
293
  .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
294
+ .option('--console', 'interactive slash-command console on the node (/nodes, /build, /activity …); on by default in a TTY')
294
295
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
295
296
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
296
297
  .option('--gateway-password <password>', 'auth password for the InfiniBot gateway')
@@ -72,6 +72,8 @@ export async function mcpCommand(config, opts) {
72
72
  logger: stderrLogger,
73
73
  options: { version, port, token, autoUpdate: fed.autoUpdate, applyConfig, autoPullConfig: role === 'satellite' },
74
74
  });
75
+ // Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
76
+ kernel.attachFederation(federation);
75
77
  // Hub/peer/relay: publish local providers + policy for satellites to source.
76
78
  const sharedFromConf = () => ({
77
79
  cloudProviders: config.get('cloudProviders'),
@@ -13,6 +13,7 @@ export interface ServeOptions {
13
13
  tag?: string;
14
14
  lan?: boolean;
15
15
  lanPort?: string;
16
+ console?: boolean;
16
17
  gateway?: string;
17
18
  gatewayToken?: string;
18
19
  gatewayPassword?: string;
@@ -77,6 +77,8 @@ export async function serve(config, opts) {
77
77
  autoPullConfig: role === 'satellite',
78
78
  },
79
79
  });
80
+ // Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
81
+ kernel.attachFederation(federation);
80
82
  // Hub/peer/relay: publish this machine's providers + policy so satellites can
81
83
  // auto-source them. Re-publish when the local config changes (live).
82
84
  const sharedFromConf = () => ({
@@ -133,7 +135,6 @@ export async function serve(config, opts) {
133
135
  connector.start();
134
136
  console.log(chalk.green(` ✓ linking to InfiniBot gateway ${opts.gateway}`));
135
137
  }
136
- console.log(chalk.dim(`\n mesh online — Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
137
138
  const shutdown = async () => {
138
139
  console.log(chalk.dim('\n stopping mesh…'));
139
140
  connector?.stop();
@@ -143,6 +144,40 @@ export async function serve(config, opts) {
143
144
  };
144
145
  process.on('SIGINT', () => void shutdown());
145
146
  process.on('SIGTERM', () => void shutdown());
147
+ // Interactive slash-command console on the live mesh node: type /nodes,
148
+ // /build <node> <goal>, /activity, /running, /follow <id>, etc. Enabled with
149
+ // --console (or automatically when stdin is a TTY).
150
+ if (opts.console ?? process.stdin.isTTY) {
151
+ const { createInterface } = await import('node:readline');
152
+ console.log(chalk.dim(`\n mesh online — slash-command console ready. Type /help or /nodes. Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
153
+ const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: chalk.cyan('mesh> ') });
154
+ rl.prompt();
155
+ rl.on('line', async (line) => {
156
+ const input = line.trim();
157
+ if (!input) {
158
+ rl.prompt();
159
+ return;
160
+ }
161
+ if (input === 'exit' || input === 'quit') {
162
+ await shutdown();
163
+ return;
164
+ }
165
+ rl.pause();
166
+ try {
167
+ const result = await kernel.command(input.startsWith('/') ? input : '/' + input);
168
+ console.log((result.ok ? chalk.reset : chalk.yellow)(result.text));
169
+ }
170
+ catch (err) {
171
+ console.log(chalk.red(err instanceof Error ? err.message : String(err)));
172
+ }
173
+ rl.resume();
174
+ rl.prompt();
175
+ });
176
+ rl.on('close', () => void shutdown());
177
+ }
178
+ else {
179
+ console.log(chalk.dim(`\n mesh online — Ctrl-C to stop. Peers: ${federation.peers().length}\n`));
180
+ }
146
181
  // Keep the process alive.
147
182
  await new Promise(() => { });
148
183
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * OpenKernel — Capability Normalization
3
+ *
4
+ * Harness-friendly capability passing. A phase (from a harness like InfiniBot,
5
+ * or from the LLM planner) can name what it needs in natural terms — "backend",
6
+ * "ui", "testing", "devops" — and this maps them onto the kernel's canonical
7
+ * capability vocabulary so the router/worker-matcher understand them. Unknown
8
+ * terms fall back to a fuzzy canonical match, then to `reasoning`.
9
+ */
10
+ import type { Capability } from './types.js';
11
+ /** The kernel's canonical capability vocabulary (mirrors the Capability union). */
12
+ export declare const CANONICAL_CAPABILITIES: Capability[];
13
+ /**
14
+ * Normalize natural capability names to canonical capabilities. Accepts canonical
15
+ * names as-is, maps known synonyms, fuzzy-matches by substring, and guarantees a
16
+ * non-empty result (falls back to `['reasoning']`).
17
+ */
18
+ export declare function normalizeCapabilities(input?: string[]): Capability[];
@@ -0,0 +1,84 @@
1
+ /** The kernel's canonical capability vocabulary (mirrors the Capability union). */
2
+ export const CANONICAL_CAPABILITIES = [
3
+ 'coding', 'browser', 'vision', 'terminal', 'planning', 'reasoning', 'filesystem',
4
+ 'search', 'crawl', 'ocr', 'review', 'summarize', 'citations', 'translation',
5
+ 'documentation', 'architecture', 'verification', 'frontend', 'design',
6
+ 'accessibility', 'dataviz', 'animation',
7
+ ];
8
+ /** Natural-language / domain terms → canonical capabilities. */
9
+ const SYNONYMS = {
10
+ // engineering domains
11
+ backend: ['coding', 'terminal', 'filesystem', 'reasoning'],
12
+ 'back-end': ['coding', 'terminal', 'filesystem', 'reasoning'],
13
+ server: ['coding', 'terminal', 'filesystem'],
14
+ api: ['coding', 'reasoning'],
15
+ ui: ['frontend', 'design'],
16
+ ux: ['frontend', 'design', 'accessibility'],
17
+ 'front-end': ['frontend', 'design'],
18
+ web: ['frontend', 'coding'],
19
+ fullstack: ['coding', 'frontend', 'filesystem'],
20
+ 'full-stack': ['coding', 'frontend', 'filesystem'],
21
+ mobile: ['frontend', 'coding'],
22
+ // testing / quality
23
+ test: ['verification', 'review'],
24
+ tests: ['verification', 'review'],
25
+ testing: ['verification', 'review'],
26
+ qa: ['verification', 'review'],
27
+ security: ['review', 'reasoning'],
28
+ audit: ['review', 'reasoning'],
29
+ // planning / design
30
+ architect: ['architecture', 'planning', 'reasoning'],
31
+ plan: ['planning', 'reasoning'],
32
+ spec: ['planning', 'documentation'],
33
+ // data / research
34
+ research: ['search', 'crawl', 'summarize'],
35
+ data: ['dataviz', 'reasoning'],
36
+ db: ['coding', 'filesystem'],
37
+ database: ['coding', 'filesystem'],
38
+ ml: ['reasoning', 'coding'],
39
+ ai: ['reasoning', 'coding'],
40
+ // ops / misc
41
+ refactor: ['coding', 'review'],
42
+ debug: ['coding', 'reasoning', 'terminal'],
43
+ deploy: ['terminal', 'filesystem'],
44
+ devops: ['terminal', 'filesystem', 'coding'],
45
+ infra: ['terminal', 'filesystem'],
46
+ docs: ['documentation', 'summarize'],
47
+ translate: ['translation'],
48
+ a11y: ['accessibility'],
49
+ chart: ['dataviz'],
50
+ charts: ['dataviz'],
51
+ animate: ['animation'],
52
+ scrape: ['crawl', 'search'],
53
+ };
54
+ /**
55
+ * Normalize natural capability names to canonical capabilities. Accepts canonical
56
+ * names as-is, maps known synonyms, fuzzy-matches by substring, and guarantees a
57
+ * non-empty result (falls back to `['reasoning']`).
58
+ */
59
+ export function normalizeCapabilities(input) {
60
+ const out = new Set();
61
+ const canon = new Set(CANONICAL_CAPABILITIES);
62
+ for (const raw of input ?? []) {
63
+ const key = String(raw).trim().toLowerCase();
64
+ if (!key)
65
+ continue;
66
+ if (canon.has(key)) {
67
+ out.add(key);
68
+ continue;
69
+ }
70
+ const mapped = SYNONYMS[key];
71
+ if (mapped) {
72
+ for (const c of mapped)
73
+ out.add(c);
74
+ continue;
75
+ }
76
+ // fuzzy: the term contains (or is contained by) a canonical token
77
+ const hit = CANONICAL_CAPABILITIES.find(c => key.includes(c) || c.includes(key));
78
+ if (hit)
79
+ out.add(hit);
80
+ }
81
+ if (out.size === 0)
82
+ out.add('reasoning');
83
+ return [...out];
84
+ }
@@ -28,6 +28,13 @@ export class CheckpointEngine {
28
28
  // cap per-mission checkpoint files to prevent disk exhaustion
29
29
  this.pruneMission(mission.id);
30
30
  const id = nanoid(12);
31
+ // Persist the mission's recent event log + working memory so a resumed or
32
+ // crash-recovered long-running mission keeps its activity trail and context.
33
+ const logs = this.eventBus
34
+ .getHistory()
35
+ .filter(e => e.missionId === mission.id)
36
+ .slice(-200);
37
+ const memory = mission.metadata ? structuredClone(mission.metadata) : undefined;
31
38
  const checkpoint = {
32
39
  id,
33
40
  missionId: mission.id,
@@ -35,6 +42,8 @@ export class CheckpointEngine {
35
42
  workerStates,
36
43
  timestamp: Date.now(),
37
44
  reason,
45
+ logs,
46
+ memory,
38
47
  };
39
48
  const file = join(this.dir, `${mission.id}_${id}.json`);
40
49
  writeFileSync(file, JSON.stringify(checkpoint, null, 2), 'utf-8');
@@ -12,8 +12,11 @@
12
12
  */
13
13
  import type { Kernel } from './kernel.js';
14
14
  import type { CommandResult } from './types.js';
15
+ import type { Federation } from './federation/federation.js';
15
16
  export interface CommandContext {
16
17
  kernel: Kernel;
18
+ /** The mesh node, when the kernel is running under `serve`/`mcp`. */
19
+ federation?: Federation;
17
20
  args: string[];
18
21
  raw: string;
19
22
  }
@@ -8,6 +8,61 @@ function pickMission(kernel, arg, status) {
8
8
  const pool = status ? missions.filter(m => m.status === status) : missions;
9
9
  return pool[pool.length - 1];
10
10
  }
11
+ const NO_MESH = 'Not part of a mesh. Start one with `infinicode serve` (or `infinicode mcp`).';
12
+ /** The command needs the mesh — return the federation or an error result. */
13
+ function needMesh(ctx) {
14
+ return ctx.federation ?? err(NO_MESH);
15
+ }
16
+ /**
17
+ * Resolve a node by display name (case-insensitive), nodeId, or nodeId prefix.
18
+ * "self"/"me"/"this" resolve to this node. Returns undefined if no unique match.
19
+ */
20
+ function resolveNode(fed, query) {
21
+ const nodes = fed.nodeStatuses();
22
+ const self = nodes[0]; // nodeStatuses() lists self first
23
+ const q = query.trim().toLowerCase();
24
+ if (q === 'self' || q === 'me' || q === 'this')
25
+ return self;
26
+ const exact = nodes.find(n => n.displayName.toLowerCase() === q || n.nodeId.toLowerCase() === q);
27
+ if (exact)
28
+ return exact;
29
+ const prefix = nodes.filter(n => n.nodeId.toLowerCase().startsWith(q) || n.displayName.toLowerCase().startsWith(q));
30
+ return prefix.length === 1 ? prefix[0] : undefined;
31
+ }
32
+ /** ISO HH:MM:SS from an epoch-ms timestamp. */
33
+ function clock(ts) {
34
+ return new Date(ts).toISOString().slice(11, 19);
35
+ }
36
+ /** "3s ago" / "2m ago" from an epoch-ms timestamp. */
37
+ function ago(ts) {
38
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
39
+ if (s < 60)
40
+ return `${s}s ago`;
41
+ if (s < 3600)
42
+ return `${Math.round(s / 60)}m ago`;
43
+ return `${Math.round(s / 3600)}h ago`;
44
+ }
45
+ /** One-line rendering of a mesh node status. */
46
+ function nodeLine(n, self = false) {
47
+ const dot = n.connected || self ? '●' : '○';
48
+ const load = typeof n.load === 'number' ? `${String(Math.round(n.load)).padStart(3)}%` : ' ?%';
49
+ const tag = self ? ' (this node)' : '';
50
+ return `${dot} ${n.displayName.padEnd(16)} ${n.role.padEnd(9)} v${(n.version ?? '?').padEnd(8)} load ${load} ${ago(n.lastSeenAt)}${tag}`;
51
+ }
52
+ /** Compact one-line summary of an activity stream frame. */
53
+ function frameLine(entry) {
54
+ const { f } = entry;
55
+ const kinds = { ev: 'event', hw: 'telem', hb: 'beat', nd: 'nodes', rt: 'route', log: 'log' };
56
+ const d = f.d;
57
+ let detail = '';
58
+ if (f.t === 'ev' && d)
59
+ detail = `${d.k ?? ''}${d.desc ? ' ' + d.desc : d.run ? ' ' + d.run : ''}`.trim();
60
+ else if (f.t === 'rt' && d)
61
+ detail = `${d.model ?? d.node ?? ''}`;
62
+ else if (f.t === 'log' && d)
63
+ detail = String(d.msg ?? d.line ?? '');
64
+ return `${clock(f.ts)} ${f.n.slice(0, 8).padEnd(8)} ${kinds[f.t].padEnd(6)} ${detail}`.trimEnd();
65
+ }
11
66
  export const DEFAULT_COMMANDS = [
12
67
  {
13
68
  name: 'help',
@@ -174,29 +229,278 @@ export const DEFAULT_COMMANDS = [
174
229
  {
175
230
  name: 'goal',
176
231
  aliases: ['g'],
177
- description: 'Start and run a new mission from a goal',
232
+ description: 'Plan a goal into phases and run it (each phase gets its own model tier)',
178
233
  usage: '/goal <text>',
179
234
  async run({ kernel, args }) {
180
235
  const goal = args.join(' ').trim();
181
236
  if (!goal)
182
237
  return err('Usage: /goal <text>');
183
- const mission = await kernel.execute({
184
- description: goal,
185
- goal,
186
- tasks: [{ description: goal, capabilities: ['reasoning', 'coding'], input: { prompt: goal } }],
187
- });
238
+ // autoPlan the MissionPlanner decomposes the goal into phases, each
239
+ // routed to an appropriate model tier by its capabilities.
240
+ const mission = await kernel.execute({ description: goal, goal, autoPlan: true });
188
241
  const done = mission.tasks.filter(t => t.status === 'COMPLETED').length;
242
+ const phases = mission.objectives
243
+ .map(o => `${o.phase ?? 'phase'}${o.minBenchmark ? ` ≥${o.minBenchmark}` : ''}`)
244
+ .join(' → ');
189
245
  const preview = mission.tasks
190
- .map(t => t.output?.content?.slice(0, 400))
246
+ .map(t => t.output?.content?.slice(0, 300))
191
247
  .filter(Boolean)
192
248
  .join('\n---\n');
193
249
  return {
194
250
  ok: mission.status === 'COMPLETED',
195
- text: `Mission ${mission.id} ${mission.status} (${done}/${mission.tasks.length})${preview ? '\n' + preview : ''}`,
251
+ text: `Mission ${mission.id} ${mission.status} (${done}/${mission.tasks.length})` +
252
+ `${phases ? `\nphases: ${phases}` : ''}${preview ? '\n' + preview : ''}`,
196
253
  data: { missionId: mission.id },
197
254
  };
198
255
  },
199
256
  },
257
+ {
258
+ name: 'mission',
259
+ aliases: ['mi'],
260
+ description: 'Detailed view of one mission (tasks + outputs)',
261
+ usage: '/mission [missionId|latest]',
262
+ run({ kernel, args }) {
263
+ const m = pickMission(kernel, args[0] ?? 'latest');
264
+ if (!m)
265
+ return err('No mission found. Start one with /goal <text>.');
266
+ const done = m.tasks.filter(t => t.status === 'COMPLETED').length;
267
+ const head = `${m.id} ${m.status} — ${m.name} (${done}/${m.tasks.length} done)`;
268
+ const tasks = m.tasks.map((t, i) => {
269
+ const out = t.output?.content?.replace(/\s+/g, ' ').slice(0, 120);
270
+ const e = t.error ? ` !${t.error.replace(/\s+/g, ' ').slice(0, 80)}` : '';
271
+ return ` ${String(i + 1).padStart(2)}. [${t.status.padEnd(9)}] ${t.description.slice(0, 60)}${out ? ' → ' + out : ''}${e}`;
272
+ });
273
+ return ok(`${head}\n${tasks.join('\n')}`);
274
+ },
275
+ },
276
+ {
277
+ name: 'nodes',
278
+ aliases: ['mesh', 'fleet'],
279
+ description: 'List mesh nodes: role, version, load, last seen',
280
+ run(ctx) {
281
+ const fed = needMesh(ctx);
282
+ if ('ok' in fed)
283
+ return fed;
284
+ const nodes = fed.nodeStatuses();
285
+ if (!nodes.length)
286
+ return ok('No nodes.');
287
+ const lines = nodes.map((n, i) => nodeLine(n, i === 0));
288
+ const connected = nodes.filter((n, i) => i === 0 || n.connected).length;
289
+ return ok(`${connected}/${nodes.length} node(s) online\n${lines.join('\n')}`);
290
+ },
291
+ },
292
+ {
293
+ name: 'node',
294
+ description: 'Inspect a node, run a command on it, or send it a task',
295
+ usage: '/node <name> [/command | task text]',
296
+ async run(ctx) {
297
+ const fed = needMesh(ctx);
298
+ if ('ok' in fed)
299
+ return fed;
300
+ const { kernel, args } = ctx;
301
+ const name = args[0];
302
+ if (!name)
303
+ return err('Usage: /node <name> [/command | task text]');
304
+ const node = resolveNode(fed, name);
305
+ if (!node)
306
+ return err(`No node matching "${name}". Try /nodes.`);
307
+ const isSelf = node.nodeId === fed.identity.nodeId;
308
+ const rest = args.slice(1).join(' ').trim();
309
+ // No payload → show the node's detail card.
310
+ if (!rest) {
311
+ const role = isSelf ? fed.getRole() : await fed.getRoleOf(node.nodeId).catch(() => null);
312
+ const caps = node.capabilities?.length ? [...new Set(node.capabilities)].join(', ') : '—';
313
+ const lines = [
314
+ nodeLine(node, isSelf),
315
+ ` id: ${node.nodeId}`,
316
+ ` role: ${node.role}${role?.role ? ` · agent "${role.role}"` : ''}`,
317
+ role?.architecture ? ` arch: ${role.architecture}` : '',
318
+ ` caps: ${caps}`,
319
+ ` cmds: ${node.commands?.length ?? 0} exposed`,
320
+ ].filter(Boolean);
321
+ return ok(lines.join('\n'));
322
+ }
323
+ // A slash-command → run it on that node.
324
+ if (rest.startsWith('/')) {
325
+ const data = isSelf ? await kernel.command(rest) : await fed.runCommandOn(node.nodeId, rest);
326
+ const text = data && typeof data === 'object' && 'text' in data
327
+ ? String(data.text)
328
+ : typeof data === 'string'
329
+ ? data
330
+ : JSON.stringify(data);
331
+ return ok(`[${node.displayName}] ${rest}\n${text}`);
332
+ }
333
+ // Otherwise → dispatch it as a task to that node.
334
+ if (isSelf) {
335
+ const mission = await kernel.execute({
336
+ description: rest,
337
+ goal: rest,
338
+ tasks: [{ description: rest, capabilities: ['reasoning', 'coding'], input: { prompt: rest } }],
339
+ });
340
+ const preview = mission.tasks.map(t => t.output?.content?.slice(0, 400)).filter(Boolean).join('\n---\n');
341
+ return ok(`[${node.displayName}] ${mission.status}\n${preview || '(no output)'}`);
342
+ }
343
+ const res = await fed.dispatch({ description: rest, capabilities: ['reasoning', 'coding'], prompt: rest }, { preferNodeId: node.nodeId }, { wait: true, timeoutMs: 180_000 });
344
+ if (!res)
345
+ return err(`Dispatch to ${node.displayName} failed (node unreachable or not capable).`);
346
+ const body = res.record.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
347
+ return ok(`[${node.displayName}] run ${res.runId} ${res.record.status}\n${body || '(no output)'}`);
348
+ },
349
+ },
350
+ {
351
+ name: 'dispatch',
352
+ aliases: ['d'],
353
+ description: 'Send a task to the best-fit node (compute-routed)',
354
+ usage: '/dispatch <task text>',
355
+ async run(ctx) {
356
+ const fed = needMesh(ctx);
357
+ if ('ok' in fed)
358
+ return fed;
359
+ const text = ctx.args.join(' ').trim();
360
+ if (!text)
361
+ return err('Usage: /dispatch <task text>');
362
+ const res = await fed.dispatch({ description: text, capabilities: ['reasoning', 'coding'], prompt: text }, {}, { wait: true, timeoutMs: 180_000 });
363
+ if (!res)
364
+ return err('No capable node available to take this task.');
365
+ const body = res.record.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
366
+ return ok(`dispatched to ${res.peerId} — run ${res.runId} ${res.record.status}\n${body || '(no output)'}`);
367
+ },
368
+ },
369
+ {
370
+ name: 'build',
371
+ aliases: ['b'],
372
+ description: 'Dispatch a PHASED build (auto-planned) to a node — non-blocking',
373
+ usage: '/build <node> <goal>',
374
+ async run(ctx) {
375
+ const fed = needMesh(ctx);
376
+ if ('ok' in fed)
377
+ return fed;
378
+ const name = ctx.args[0];
379
+ const goal = ctx.args.slice(1).join(' ').trim();
380
+ if (!name || !goal)
381
+ return err('Usage: /build <node> <goal>');
382
+ const node = resolveNode(fed, name);
383
+ if (!node)
384
+ return err(`No node matching "${name}". Try /nodes.`);
385
+ // Non-blocking: the node decomposes the goal into phases and runs them; we
386
+ // return a runId immediately and monitor via /activity + /running + /follow.
387
+ const res = await fed.dispatch({ description: goal.slice(0, 80), capabilities: ['architecture', 'coding', 'reasoning'], prompt: goal, autoPlan: true }, { preferNodeId: node.nodeId }, { wait: false });
388
+ if (!res)
389
+ return err(`Dispatch to ${node.displayName} failed (node unreachable or not capable).`);
390
+ return ok(`▶ phased build dispatched to ${node.displayName} — run ${res.runId}\n` +
391
+ ` monitor live: /activity\n in-flight: /running\n result: /follow ${res.runId}`);
392
+ },
393
+ },
394
+ {
395
+ name: 'follow',
396
+ aliases: ['f'],
397
+ description: 'Check a dispatched run\'s status + output',
398
+ usage: '/follow <runId>',
399
+ async run(ctx) {
400
+ const fed = needMesh(ctx);
401
+ if ('ok' in fed)
402
+ return fed;
403
+ const runId = ctx.args[0];
404
+ if (!runId)
405
+ return err('Usage: /follow <runId>');
406
+ const followed = await fed.followRun(runId);
407
+ if (!followed)
408
+ return err(`No run ${runId} (unknown or the holding node is gone).`);
409
+ const r = followed.record;
410
+ const body = r.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
411
+ return ok(`run ${r.runId} ${r.status}${r.error ? ' — ' + r.error : ''}\n${body || '(no output yet)'}`);
412
+ },
413
+ },
414
+ {
415
+ name: 'running',
416
+ aliases: ['ps'],
417
+ description: 'What is running now: missions, workers, mesh runs',
418
+ run(ctx) {
419
+ const { kernel, federation } = ctx;
420
+ const missions = kernel.listMissions().filter(m => m.status === 'RUNNING' || m.status === 'VERIFYING');
421
+ const workers = kernel.workerRuntime.getActive();
422
+ const sections = [];
423
+ sections.push(missions.length
424
+ ? `Missions (${missions.length}):\n` +
425
+ missions.map(m => ` ${m.id} ${m.status} — ${m.name} (${m.tasks.filter(t => t.status === 'RUNNING').length} task(s) running)`).join('\n')
426
+ : 'Missions: none running');
427
+ sections.push(workers.length
428
+ ? `Workers (${workers.length}):\n` + workers.map(w => ` ${w.type}:${w.status}`).join('\n')
429
+ : 'Workers: none active');
430
+ if (federation) {
431
+ const runs = federation.localRuns().filter(r => r.status === 'running');
432
+ const out = federation.outboundRuns();
433
+ sections.push(runs.length
434
+ ? `Mesh runs here (${runs.length}):\n` + runs.map(r => ` ${r.runId} — ${r.description.slice(0, 60)} (${ago(r.startedAt)})`).join('\n')
435
+ : 'Mesh runs here: none');
436
+ if (out.length)
437
+ sections.push(`Dispatched out (${out.length}):\n` + out.map(o => ` ${o.runId} → ${o.peerId}`).join('\n'));
438
+ }
439
+ return ok(sections.join('\n'));
440
+ },
441
+ },
442
+ {
443
+ name: 'activity',
444
+ aliases: ['act', 'feed'],
445
+ description: 'Recent activity across the mesh (or local events)',
446
+ usage: '/activity [count]',
447
+ run(ctx) {
448
+ const n = Math.max(1, Math.min(200, parseInt(ctx.args[0] ?? '20', 10) || 20));
449
+ if (ctx.federation) {
450
+ const frames = ctx.federation.recentFrames(n);
451
+ if (!frames.length)
452
+ return ok('No mesh activity yet.');
453
+ return ok(frames.map(frameLine).join('\n'));
454
+ }
455
+ // Stand-alone: fall back to the local event stream.
456
+ const hist = ctx.kernel.getEventHistory().slice(-n);
457
+ if (!hist.length)
458
+ return ok('No activity yet.');
459
+ return ok(hist
460
+ .map(e => `${clock(e.timestamp)} ${e.type}${e.missionId ? ' ' + e.missionId : ''}${e.taskId ? '/' + e.taskId : ''}`)
461
+ .join('\n'));
462
+ },
463
+ },
464
+ {
465
+ name: 'whoami',
466
+ aliases: ['me', 'id'],
467
+ description: 'This node: identity, role, and agent context',
468
+ run(ctx) {
469
+ if (!ctx.federation)
470
+ return ok('Stand-alone kernel (not in a mesh). Run `infinicode serve` to join one.');
471
+ const id = ctx.federation.identity;
472
+ const role = ctx.federation.getRole();
473
+ const peers = ctx.federation.peers().length;
474
+ const lines = [
475
+ `${id.displayName} (${id.role})`,
476
+ ` id: ${id.nodeId}`,
477
+ ` peers: ${peers} connected`,
478
+ role?.role ? ` agent: "${role.role}"` : '',
479
+ role?.architecture ? ` arch: ${role.architecture}` : '',
480
+ ].filter(Boolean);
481
+ return ok(lines.join('\n'));
482
+ },
483
+ },
484
+ {
485
+ name: 'role',
486
+ description: 'Show or set this node\'s persistent agent role/architecture',
487
+ usage: '/role [name] [architecture…]',
488
+ run(ctx) {
489
+ const fed = needMesh(ctx);
490
+ if ('ok' in fed)
491
+ return fed;
492
+ if (!ctx.args.length) {
493
+ const role = fed.getRole();
494
+ if (!role?.role)
495
+ return ok('No role set. Set one with /role <name> [architecture…].');
496
+ return ok(`role: "${role.role}"${role.architecture ? `\narch: ${role.architecture}` : ''}`);
497
+ }
498
+ const name = ctx.args[0];
499
+ const architecture = ctx.args.slice(1).join(' ') || undefined;
500
+ const saved = fed.setRole({ role: name, architecture });
501
+ return ok(`role set → "${saved.role}"${saved.architecture ? `\narch: ${saved.architecture}` : ''}`);
502
+ },
503
+ },
200
504
  ];
201
505
  export class KernelCommandRegistry {
202
506
  kernel;
@@ -220,9 +524,10 @@ export class KernelCommandRegistry {
220
524
  return this.commands.get(key) ?? this.commands.get(this.aliases.get(key) ?? '');
221
525
  }
222
526
  async execute(input) {
527
+ const federation = this.kernel.federation;
223
528
  const trimmed = (input ?? '').trim().replace(/^\//, '').trim();
224
529
  if (!trimmed)
225
- return this.resolve('help').run({ kernel: this.kernel, args: [], raw: input });
530
+ return this.resolve('help').run({ kernel: this.kernel, federation, args: [], raw: input });
226
531
  const parts = trimmed.split(/\s+/);
227
532
  const name = parts[0];
228
533
  const args = parts.slice(1);
@@ -230,7 +535,7 @@ export class KernelCommandRegistry {
230
535
  if (!cmd)
231
536
  return err(`Unknown command: /${name}. Try /help.`);
232
537
  try {
233
- return await cmd.run({ kernel: this.kernel, args, raw: input });
538
+ return await cmd.run({ kernel: this.kernel, federation, args, raw: input });
234
539
  }
235
540
  catch (e) {
236
541
  return err(`/${name} failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -8,6 +8,12 @@ import type { WorkerType } from '../kernel/types.js';
8
8
  export interface CloudProviderConfig {
9
9
  id: string;
10
10
  name: string;
11
+ /**
12
+ * Provider kind. 'local' registers the provider as a local (offline-eligible)
13
+ * endpoint — the router applies its localBonus and 'offline' mode includes it,
14
+ * and no API key is required. Defaults to 'cloud' when omitted.
15
+ */
16
+ type?: 'local' | 'cloud';
11
17
  baseURL: string;
12
18
  apiKey?: string;
13
19
  headerName?: string;
@@ -50,6 +56,11 @@ export interface InfinicodeConfig {
50
56
  policy?: string;
51
57
  workerModels?: Partial<Record<WorkerType, WorkerModelPref>>;
52
58
  cloudProviders?: CloudProviderConfig[];
59
+ /**
60
+ * Local OpenAI-compatible servers (LM Studio, vLLM, SGLang, llama.cpp).
61
+ * Registered with type 'local'; no API key required.
62
+ */
63
+ localProviders?: CloudProviderConfig[];
53
64
  federation?: FederationConfig;
54
65
  }
55
66
  export declare const DEFAULT_CONFIG: InfinicodeConfig;
@@ -7,6 +7,7 @@ export const DEFAULT_CONFIG = {
7
7
  policy: 'balanced',
8
8
  workerModels: {},
9
9
  cloudProviders: [],
10
+ localProviders: [],
10
11
  };
11
12
  export const WORKER_TYPES_ORDER = [
12
13
  'coding',
@@ -2,8 +2,10 @@ import { frame } from './protocol.js';
2
2
  /** Events that should render instantly (map animation triggers). */
3
3
  const IMMEDIATE = new Set([
4
4
  'MISSION_STARTED', 'MISSION_COMPLETED', 'MISSION_FAILED',
5
+ 'PLAN_CREATED', 'OBJECTIVE_STARTED', 'OBJECTIVE_COMPLETED',
5
6
  'TASK_STARTED', 'TASK_COMPLETED', 'TASK_FAILED',
6
- 'WORKER_SPAWNED', 'WORKER_EXITED', 'ROUTING_DECISION', 'RECOVERY', 'NOTIFICATION',
7
+ 'WORKER_SPAWNED', 'WORKER_EXITED', 'ROUTING_DECISION',
8
+ 'MODEL_CHANGED', 'PROVIDER_CHANGED', 'RECOVERY', 'NOTIFICATION',
7
9
  ]);
8
10
  /** High-frequency, low-value chatter to coalesce. */
9
11
  const COALESCE = new Set(['LOG', 'PROVIDER_HEALTH']);