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
@@ -0,0 +1,139 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ /**
3
+ * infinicode — mesh command palette
4
+ *
5
+ * Adds the OpenKernel mesh/kernel slash-commands natively to the TUI's `/`
6
+ * command palette. Each command is dispatched to a LOCAL mesh node over HTTP
7
+ * (POST /fed/command → kernel.command()), so the same command surface exposed by
8
+ * `infinicode serve`'s console and the CLI is available right inside the TUI.
9
+ *
10
+ * The local node is started (or reused) by `infinicode run`, which injects:
11
+ * INFINICODE_MESH_URL e.g. http://127.0.0.1:47913
12
+ * INFINICODE_MESH_TOKEN optional bearer token
13
+ *
14
+ * Argless commands (/nodes, /running, /activity, /whoami, /mesh-help) run
15
+ * immediately and show output in an alert. Arg-taking commands (/node, /build,
16
+ * /follow, /dispatch, /goal, /mission, /role) open a prompt first.
17
+ */
18
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
19
+ import { createBindingLookup } from "@opentui/keymap/extras"
20
+
21
+ type CommandResult = { ok?: boolean; text?: string; data?: unknown }
22
+
23
+ const meshUrl = () => (process.env.INFINICODE_MESH_URL ?? "http://127.0.0.1:47913").replace(/\/+$/, "")
24
+ const meshToken = () => process.env.INFINICODE_MESH_TOKEN
25
+
26
+ /** POST a slash-command to the local mesh node and return its CommandResult. */
27
+ async function send(input: string): Promise<CommandResult> {
28
+ const token = meshToken()
29
+ try {
30
+ const res = await fetch(`${meshUrl()}/fed/command`, {
31
+ method: "POST",
32
+ headers: {
33
+ "content-type": "application/json",
34
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
35
+ },
36
+ body: JSON.stringify({ input }),
37
+ signal: AbortSignal.timeout(30_000),
38
+ })
39
+ if (!res.ok) return { ok: false, text: `mesh error ${res.status} — is a node running? (infinicode serve)` }
40
+ return (await res.json()) as CommandResult
41
+ } catch (e) {
42
+ const msg = e instanceof Error ? e.message : String(e)
43
+ return { ok: false, text: `no local mesh node reachable at ${meshUrl()} (${msg})` }
44
+ }
45
+ }
46
+
47
+ /** Render a CommandResult in a scrollable alert dialog. */
48
+ function show(api: TuiPluginApi, title: string, res: CommandResult): void {
49
+ const DialogAlert = api.ui.DialogAlert
50
+ const message = (res.text && res.text.trim()) || (res.ok ? "(no output)" : "command failed")
51
+ api.ui.dialog.setSize("large")
52
+ api.ui.dialog.replace(() => (
53
+ <DialogAlert title={title} message={message} onConfirm={() => api.ui.dialog.clear()} />
54
+ ))
55
+ }
56
+
57
+ /** Run a fixed slash-command now and show the result. */
58
+ function runNow(api: TuiPluginApi, title: string, input: string): void {
59
+ api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
60
+ void send(input).then((res) => show(api, title, res))
61
+ }
62
+
63
+ /** Open a prompt, then dispatch `<slash> <entered args>`. */
64
+ function runWithArgs(
65
+ api: TuiPluginApi,
66
+ title: string,
67
+ slash: string,
68
+ placeholder: string,
69
+ ): void {
70
+ const DialogPrompt = api.ui.DialogPrompt
71
+ api.ui.dialog.setSize("medium")
72
+ api.ui.dialog.replace(() => (
73
+ <DialogPrompt
74
+ title={title}
75
+ value=""
76
+ placeholder={placeholder}
77
+ onConfirm={(args: string) => {
78
+ api.ui.dialog.clear()
79
+ const input = args && args.trim() ? `${slash} ${args.trim()}` : slash
80
+ api.ui.toast({ variant: "info", title: "mesh", message: `${input} …`, duration: 1200 })
81
+ void send(input).then((res) => show(api, title, res))
82
+ }}
83
+ onCancel={() => api.ui.dialog.clear()}
84
+ />
85
+ ))
86
+ }
87
+
88
+ interface Spec {
89
+ name: string
90
+ slashName: string
91
+ title: string
92
+ /** if set, prompt for args first with this placeholder; else run immediately */
93
+ arg?: string
94
+ /** the underlying kernel slash-command (defaults to `/${slashName}`) */
95
+ cmd?: string
96
+ }
97
+
98
+ const SPECS: Spec[] = [
99
+ { name: "mesh_nodes", slashName: "nodes", title: "Mesh nodes" },
100
+ { name: "mesh_running", slashName: "running", title: "Running tasks" },
101
+ { name: "mesh_activity", slashName: "activity", title: "Mesh activity" },
102
+ { name: "mesh_whoami", slashName: "whoami", title: "This node" },
103
+ { name: "mesh_help", slashName: "mesh-help", title: "Mesh commands", cmd: "/help" },
104
+ { name: "mesh_node", slashName: "node", title: "Node control", arg: "<node> <message>", cmd: "/node" },
105
+ { name: "mesh_build", slashName: "build", title: "Phased build", arg: "<node> <goal>", cmd: "/build" },
106
+ { name: "mesh_dispatch", slashName: "dispatch", title: "Dispatch task", arg: "<node> <task>", cmd: "/dispatch" },
107
+ { name: "mesh_follow", slashName: "follow", title: "Follow run", arg: "<run-id>", cmd: "/follow" },
108
+ { name: "mesh_goal", slashName: "goal", title: "New goal", arg: "<goal description>", cmd: "/goal" },
109
+ { name: "mesh_mission", slashName: "mission", title: "Mission", arg: "<mission description>", cmd: "/mission" },
110
+ { name: "mesh_role", slashName: "role", title: "Set role", arg: "<role>", cmd: "/role" },
111
+ ]
112
+
113
+ const tui: TuiPlugin = async (api, options) => {
114
+ if (options?.enabled === false) return
115
+ const keys = createBindingLookup({})
116
+
117
+ api.keymap.registerLayer({
118
+ commands: SPECS.map((s) => ({
119
+ name: s.name,
120
+ title: s.title,
121
+ category: "Mesh",
122
+ namespace: "palette" as const,
123
+ slashName: s.slashName,
124
+ run() {
125
+ const cmd = s.cmd ?? `/${s.slashName}`
126
+ if (s.arg) runWithArgs(api, s.title, cmd, s.arg)
127
+ else runNow(api, s.title, cmd)
128
+ },
129
+ })),
130
+ bindings: keys.gather("mesh.global", SPECS.map((s) => s.name)),
131
+ })
132
+ }
133
+
134
+ const plugin: TuiPluginModule & { id: string } = {
135
+ id: "infinicode-mesh-commands",
136
+ tui,
137
+ }
138
+
139
+ export default plugin
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.4';
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'),
@@ -16,14 +16,14 @@
16
16
  */
17
17
  import chalk from 'chalk';
18
18
  import ora from 'ora';
19
- import { existsSync } from 'node:fs';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
- import { fileURLToPath } from 'node:url';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
  import { createRequire } from 'node:module';
23
23
  import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from '../ascii-video-animation.js';
24
24
  import { getProviderPreset } from '../kernel/free-providers.js';
25
25
  import { buildKernelFromConfig } from '../kernel/setup.js';
26
- import { SilentLogger } from '../kernel/index.js';
26
+ import { SilentLogger, Federation, loadOrCreateIdentity } from '../kernel/index.js';
27
27
  import { openAICompatBaseURL } from '../kernel/provider-url.js';
28
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
29
  const PROMPT_ANIMATION_MAX_FRAMES = Math.min(ASCII_VIDEO_FRAMES.length, ASCII_VIDEO_FPS * 3);
@@ -309,6 +309,81 @@ function ensureModelInjected(providers, providerId, modelId) {
309
309
  if (!p.models[modelId])
310
310
  p.models[modelId] = { name: modelId };
311
311
  }
312
+ function runPkgVersion() {
313
+ try {
314
+ return JSON.parse(readFileSync(join(resolve(__dirname, '..', '..'), 'package.json'), 'utf8')).version ?? '0.0.0';
315
+ }
316
+ catch {
317
+ return '0.0.0';
318
+ }
319
+ }
320
+ /** Is a mesh node already listening at this URL? (e.g. a separate `infinicode serve`). */
321
+ async function meshAlive(url, token) {
322
+ try {
323
+ const res = await fetch(`${url}/fed/manifest`, {
324
+ headers: token ? { authorization: `Bearer ${token}` } : undefined,
325
+ signal: AbortSignal.timeout(1500),
326
+ });
327
+ return res.ok;
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
333
+ /**
334
+ * Make the TUI a mesh participant so the in-TUI slash commands (/nodes, /build,
335
+ * /activity …) have a local node to talk to. If a node is already running on the
336
+ * configured port (a separate `infinicode serve`), point the TUI at THAT one;
337
+ * otherwise start an in-process node for the session. Best-effort: on any
338
+ * failure the TUI still launches, just without the live mesh palette.
339
+ */
340
+ async function startLocalMeshNode(config) {
341
+ if (process.env.INFINICODE_NO_MESH)
342
+ return null;
343
+ const fed = config.get('federation') ?? {};
344
+ const port = fed.port ?? 47913;
345
+ const token = fed.token;
346
+ const url = `http://127.0.0.1:${port}`;
347
+ try {
348
+ // Reuse an already-running node (don't double-bind the port).
349
+ if (await meshAlive(url, token))
350
+ return { url, token, stop: async () => { } };
351
+ const kernel = buildKernelFromConfig(config, { logger: new SilentLogger() });
352
+ const role = (fed.role ?? 'peer');
353
+ const identity = loadOrCreateIdentity({ role, displayName: fed.displayName });
354
+ const version = runPkgVersion();
355
+ const describe = () => {
356
+ const defs = kernel.workerRuntime.listRegisteredDefinitions();
357
+ return {
358
+ version,
359
+ capabilities: [...new Set(defs.flatMap(d => d.capabilities))],
360
+ workers: defs.map(d => d.type),
361
+ commands: kernel.commands.list().map(c => c.name),
362
+ providers: kernel.providerManager.listInfos().map(p => p.id),
363
+ };
364
+ };
365
+ const federation = new Federation({
366
+ kernel, identity, describe, logger: new SilentLogger(),
367
+ options: {
368
+ version, port, host: fed.host, token, seeds: fed.seeds,
369
+ autoPullConfig: role === 'satellite',
370
+ },
371
+ });
372
+ kernel.attachFederation(federation);
373
+ await federation.start();
374
+ if (fed.lan)
375
+ federation.startLanDiscovery(undefined, fed.lanPort);
376
+ for (const s of fed.seeds ?? [])
377
+ await federation.connect(s).catch(() => undefined);
378
+ return {
379
+ url, token,
380
+ stop: async () => { await federation.stop(); kernel.destroy(); },
381
+ };
382
+ }
383
+ catch {
384
+ return null;
385
+ }
386
+ }
312
387
  export async function runAgent(_masterUrl, _model, _useTui, config) {
313
388
  const masterUrl = await resolveConfiguredMasterUrl(config);
314
389
  const masterReachable = await isOllamaReachable(masterUrl);
@@ -316,6 +391,15 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
316
391
  const cloudProviders = config.get('cloudProviders') ?? [];
317
392
  const enabledCloud = cloudProviders.filter(p => p.enabled);
318
393
  const policyName = config.get('policy') ?? 'balanced';
394
+ // Make this TUI session a mesh participant so the in-TUI slash commands
395
+ // (/nodes, /build, /activity, …) have a local node to talk to. Reuses a
396
+ // running `infinicode serve` if one is up, else starts an in-process node.
397
+ const mesh = await startLocalMeshNode(config);
398
+ if (mesh) {
399
+ process.env.INFINICODE_MESH_URL = mesh.url;
400
+ if (mesh.token)
401
+ process.env.INFINICODE_MESH_TOKEN = mesh.token;
402
+ }
319
403
  // One kernel pass: load every provider's full model catalog (in parallel) and
320
404
  // route the default model under the policy.
321
405
  const routeSpinner = ora('loading model catalog & routing...').start();
@@ -340,12 +424,23 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
340
424
  const policy = config.get('policy') ?? 'local-first';
341
425
  const pinnedCount = Object.keys(config.get('workerModels') ?? {}).length;
342
426
  const routingInfo = `AUTO ${policy} ${providerCount}P ${pinnedCount}pins`;
427
+ // Runtime TUI plugin that adds the mesh slash-commands (/nodes, /build, …)
428
+ // to the `/` palette. Shipped in the package at .opencode/plugins; loaded via
429
+ // a file:// URL so it resolves regardless of the TUI's cwd. Only injected when
430
+ // a local mesh node is available (else the commands would have nothing to hit).
431
+ const pluginPaths = [];
432
+ if (mesh) {
433
+ const pluginFile = join(resolve(__dirname, '..', '..'), '.opencode', 'plugins', 'mesh-commands.tsx');
434
+ if (existsSync(pluginFile))
435
+ pluginPaths.push(pathToFileURL(pluginFile).href);
436
+ }
343
437
  process.env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
344
438
  $schema: 'https://opencode.ai/config.json',
345
439
  provider: providers,
346
440
  model: modelRef,
347
441
  small_model: modelRef,
348
442
  enabled_providers: Object.keys(providers),
443
+ ...(pluginPaths.length > 0 ? { plugin: pluginPaths } : {}),
349
444
  agent: {
350
445
  build: {
351
446
  model: modelRef,
@@ -392,10 +487,12 @@ export async function runAgent(_masterUrl, _model, _useTui, config) {
392
487
  });
393
488
  }
394
489
  catch (e) {
490
+ await mesh?.stop().catch(() => undefined);
395
491
  const err = e;
396
492
  if (err.exitCode) {
397
493
  process.exit(err.exitCode);
398
494
  }
399
495
  throw e;
400
496
  }
497
+ await mesh?.stop().catch(() => undefined);
401
498
  }
@@ -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
  }