ai-runtime-engine 1.2.0 → 2.7.0

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 (153) hide show
  1. package/CHANGELOG.md +602 -0
  2. package/dist/agents/admit.d.ts +69 -0
  3. package/dist/agents/admit.js +129 -0
  4. package/dist/agents/definition.d.ts +36 -0
  5. package/dist/agents/definition.js +9 -0
  6. package/dist/agents/envelope.d.ts +53 -0
  7. package/dist/agents/envelope.js +68 -0
  8. package/dist/agents/finding.d.ts +79 -0
  9. package/dist/agents/finding.js +80 -0
  10. package/dist/agents/task.d.ts +60 -0
  11. package/dist/agents/task.js +32 -0
  12. package/dist/agents/worker.d.ts +68 -0
  13. package/dist/agents/worker.js +256 -0
  14. package/dist/capabilities/capability.d.ts +117 -0
  15. package/dist/capabilities/capability.js +66 -0
  16. package/dist/capabilities/registry.d.ts +139 -0
  17. package/dist/capabilities/registry.js +413 -0
  18. package/dist/capabilities/vocabulary.d.ts +32 -0
  19. package/dist/capabilities/vocabulary.js +34 -0
  20. package/dist/cli/cli.js +56 -4
  21. package/dist/cli/commands/cleanup.js +29 -27
  22. package/dist/cli/commands/doctor.d.ts +14 -0
  23. package/dist/cli/commands/doctor.js +38 -8
  24. package/dist/cli/commands/executions.js +34 -25
  25. package/dist/cli/commands/info.d.ts +1 -0
  26. package/dist/cli/commands/info.js +11 -9
  27. package/dist/cli/commands/init.js +19 -0
  28. package/dist/cli/commands/inspect.d.ts +40 -1
  29. package/dist/cli/commands/inspect.js +157 -2
  30. package/dist/cli/commands/mcp.d.ts +45 -0
  31. package/dist/cli/commands/mcp.js +148 -0
  32. package/dist/cli/commands/route.js +21 -0
  33. package/dist/cli/commands/run.d.ts +2 -0
  34. package/dist/cli/commands/run.js +36 -4
  35. package/dist/cli/commands/skills.d.ts +2 -0
  36. package/dist/cli/commands/skills.js +29 -7
  37. package/dist/cli/interactive/ansi.d.ts +41 -0
  38. package/dist/cli/interactive/ansi.js +43 -0
  39. package/dist/cli/interactive/complete.d.ts +10 -0
  40. package/dist/cli/interactive/complete.js +19 -0
  41. package/dist/cli/interactive/repl.d.ts +3 -0
  42. package/dist/cli/interactive/repl.js +105 -16
  43. package/dist/cli/interactive/session.d.ts +12 -1
  44. package/dist/cli/interactive/session.js +83 -5
  45. package/dist/cli/render.d.ts +13 -0
  46. package/dist/cli/render.js +18 -0
  47. package/dist/cli/runtimeSession.d.ts +11 -0
  48. package/dist/cli/runtimeSession.js +17 -0
  49. package/dist/config/defaults.d.ts +3 -1
  50. package/dist/config/defaults.js +2 -0
  51. package/dist/config/schema.d.ts +1 -0
  52. package/dist/config/schema.js +2 -2
  53. package/dist/context/lossVerifier.d.ts +24 -0
  54. package/dist/context/lossVerifier.js +45 -0
  55. package/dist/context/summarize.d.ts +19 -0
  56. package/dist/context/summarize.js +53 -0
  57. package/dist/core/fallback/fallback.d.ts +8 -0
  58. package/dist/core/fallback/fallback.js +3 -1
  59. package/dist/core/router/executor.d.ts +6 -1
  60. package/dist/core/router/executor.js +9 -2
  61. package/dist/core/router/normalize.d.ts +2 -0
  62. package/dist/core/router/request.js +2 -0
  63. package/dist/core/router/router.d.ts +3 -0
  64. package/dist/core/router/router.js +7 -0
  65. package/dist/executions/execution.d.ts +13 -2
  66. package/dist/generation/generateAdapter.d.ts +14 -0
  67. package/dist/generation/generateAdapter.js +38 -0
  68. package/dist/generation/generateSkill.d.ts +26 -0
  69. package/dist/generation/generateSkill.js +51 -0
  70. package/dist/index.d.ts +44 -5
  71. package/dist/index.js +26 -2
  72. package/dist/mcp/client.d.ts +70 -0
  73. package/dist/mcp/client.js +221 -0
  74. package/dist/mcp/manager.d.ts +151 -0
  75. package/dist/mcp/manager.js +493 -0
  76. package/dist/mcp/protocol.d.ts +216 -0
  77. package/dist/mcp/protocol.js +149 -0
  78. package/dist/mcp/toolAdapter.d.ts +44 -0
  79. package/dist/mcp/toolAdapter.js +94 -0
  80. package/dist/mcp/transport.d.ts +109 -0
  81. package/dist/mcp/transport.js +383 -0
  82. package/dist/memory/embedders/hash.d.ts +12 -0
  83. package/dist/memory/embedders/hash.js +31 -0
  84. package/dist/memory/embedders/http.d.ts +25 -0
  85. package/dist/memory/embedders/http.js +48 -0
  86. package/dist/memory/memory.d.ts +19 -2
  87. package/dist/memory/memory.js +75 -11
  88. package/dist/memory/semantic.d.ts +17 -0
  89. package/dist/memory/semantic.js +29 -0
  90. package/dist/orchestration/budget.d.ts +30 -0
  91. package/dist/orchestration/budget.js +40 -0
  92. package/dist/orchestration/executor.d.ts +39 -1
  93. package/dist/orchestration/executor.js +64 -4
  94. package/dist/orchestration/orchestrator.d.ts +29 -1
  95. package/dist/orchestration/orchestrator.js +89 -8
  96. package/dist/orchestration/plan.d.ts +15 -1
  97. package/dist/orchestration/plan.js +23 -4
  98. package/dist/orchestration/planner.d.ts +19 -1
  99. package/dist/orchestration/planner.js +25 -5
  100. package/dist/plugin/ai.d.ts +4 -0
  101. package/dist/plugin/ai.js +9 -0
  102. package/dist/providers/httpClient.d.ts +25 -1
  103. package/dist/providers/httpClient.js +93 -0
  104. package/dist/providers/httpProvider.d.ts +1 -0
  105. package/dist/providers/httpProvider.js +67 -1
  106. package/dist/providers/mock/mockProvider.d.ts +3 -0
  107. package/dist/providers/mock/mockProvider.js +54 -0
  108. package/dist/providers/mock/scenarios.d.ts +7 -0
  109. package/dist/providers/provider.d.ts +6 -0
  110. package/dist/providers/wire/anthropicWire.js +34 -0
  111. package/dist/providers/wire/openaiWire.js +30 -0
  112. package/dist/providers/wire/types.d.ts +16 -0
  113. package/dist/runtime/config.js +50 -6
  114. package/dist/runtime/intent/aiClassifier.d.ts +19 -0
  115. package/dist/runtime/intent/aiClassifier.js +74 -0
  116. package/dist/runtime/models/modelProfile.d.ts +61 -0
  117. package/dist/runtime/models/modelProfile.js +139 -0
  118. package/dist/runtime/planning/deriveCapabilities.d.ts +95 -0
  119. package/dist/runtime/planning/deriveCapabilities.js +146 -0
  120. package/dist/runtime/policy.d.ts +10 -0
  121. package/dist/runtime/policy.js +9 -2
  122. package/dist/runtime/runtime.d.ts +173 -0
  123. package/dist/runtime/runtime.js +723 -50
  124. package/dist/runtime/types.d.ts +94 -2
  125. package/dist/skills/manifest.d.ts +3 -0
  126. package/dist/skills/manifest.js +24 -0
  127. package/dist/skills/registry.d.ts +16 -1
  128. package/dist/skills/registry.js +21 -1
  129. package/dist/skills/skill.d.ts +6 -1
  130. package/dist/store/area.d.ts +15 -1
  131. package/dist/store/area.js +19 -8
  132. package/dist/store/crypto.d.ts +21 -0
  133. package/dist/store/crypto.js +49 -0
  134. package/dist/store/paths.d.ts +5 -1
  135. package/dist/store/paths.js +6 -0
  136. package/dist/store/store.d.ts +15 -3
  137. package/dist/store/store.js +28 -7
  138. package/dist/telemetry/sinks/otlp.d.ts +31 -0
  139. package/dist/telemetry/sinks/otlp.js +76 -0
  140. package/dist/tools/builtins/filesystem.js +1 -0
  141. package/dist/tools/builtins/git.js +1 -0
  142. package/dist/tools/builtins/shell.js +1 -0
  143. package/dist/tools/permissions.d.ts +28 -0
  144. package/dist/tools/permissions.js +72 -0
  145. package/dist/tools/registry.d.ts +18 -2
  146. package/dist/tools/registry.js +22 -2
  147. package/dist/tools/tool.d.ts +4 -0
  148. package/dist/types.d.ts +11 -1
  149. package/dist/util/flatten.d.ts +11 -0
  150. package/dist/util/flatten.js +18 -0
  151. package/dist/util/semaphore.d.ts +19 -0
  152. package/dist/util/semaphore.js +60 -0
  153. package/package.json +24 -9
@@ -4,6 +4,7 @@
4
4
  * Never prints a secret VALUE; all output is redacted. `--json` emits the structured report.
5
5
  */
6
6
  import { Runtime } from '../../runtime/runtime.js';
7
+ import type { McpServerStatus } from '../../mcp/manager.js';
7
8
  import type { IntegrityIssue } from '../../store/area.js';
8
9
  export interface DoctorReport {
9
10
  configFile?: string;
@@ -36,6 +37,19 @@ export interface DoctorReport {
36
37
  };
37
38
  skills: string[];
38
39
  tools: string[];
40
+ /**
41
+ * MCP servers (Phase 3.3). ABSENT when none are configured, so a no-MCP `doctor --json` is unchanged.
42
+ * `connected: false` always: `doctor` is read-only and never connects (it is also the REPL `/doctor`
43
+ * path, and `gatherDoctor` is side-effect-free) — so states are as last recorded. `servers` reuses
44
+ * `McpServerStatus` VERBATIM, which is name-only for credentials (`tokenEnv` + `tokenSet`, never a
45
+ * value); adding any field beyond it would risk the secret-leak scan that stringifies this report.
46
+ */
47
+ mcp?: {
48
+ connected: boolean;
49
+ servers: McpServerStatus[];
50
+ grants: Record<string, string>;
51
+ warnings: string[];
52
+ };
39
53
  permissions: {
40
54
  fsRead: boolean;
41
55
  fsWrite: boolean;
@@ -3,8 +3,9 @@
3
3
  * health, accessible models, the local store (integrity + counts), skills, tools, permissions, and git.
4
4
  * Never prints a secret VALUE; all output is redacted. `--json` emits the structured report.
5
5
  */
6
- import { Runtime } from '../../runtime/runtime.js';
6
+ import { mcpRow } from './mcp.js';
7
7
  import { loadRuntimeConfig } from '../../runtime/config.js';
8
+ import { withRuntime } from '../runtimeSession.js';
8
9
  import { print } from '../render.js';
9
10
  /** The credential env-var NAMES ai-runtime knows about (§16). Presence is reported, never the value. */
10
11
  const KNOWN_KEY_ENVS = [
@@ -45,6 +46,21 @@ export async function gatherDoctor(rt, configFile, env) {
45
46
  },
46
47
  skills: rt.skills().map((s) => `${s.id}@${s.version}`),
47
48
  tools: rt.tools().map((t) => t.id),
49
+ // No connectMcp(): doctor stays read-only. `list()` is id-sorted, so rendering is deterministic, and
50
+ // the spread makes the null-prototype grant map plain JSON.
51
+ ...(rt.hasMcpServers()
52
+ ? {
53
+ mcp: {
54
+ // Truthful rather than hardcoded: `doctor` never CONNECTS, but the REPL's `/doctor` runs
55
+ // against a session whose servers are already live, and reporting `false` there would be a
56
+ // lie. This says whether any server is connected right now, not whether doctor connected it.
57
+ connected: rt.mcp().list().some((sv) => sv.state === 'connected' || sv.state === 'degraded'),
58
+ servers: rt.mcp().list(),
59
+ grants: { ...rt.permissions().mcp.servers },
60
+ warnings: rt.mcpWarningsList(),
61
+ },
62
+ }
63
+ : {}),
48
64
  permissions: { fsRead: perms.filesystem.read, fsWrite: perms.filesystem.write, shell: perms.shell.enabled, gitCommit: perms.git.commit, gitPush: perms.git.push, network: perms.network.enabled },
49
65
  git: { repo: !!ws?.git.isRepo, ...(ws?.git.branch ? { branch: ws.git.branch } : {}) },
50
66
  };
@@ -69,6 +85,18 @@ export function renderDoctor(r) {
69
85
  lines.push(` ${r.store.conversations} conversation(s), ${r.store.executions} execution(s), ${r.store.memory} memory record(s), ${r.store.artifacts} artifact(s)`);
70
86
  lines.push(` integrity: ${r.store.integrity.length ? `${r.store.integrity.length} issue(s) — ${r.store.integrity.map((i) => `${i.file} (${i.problem})`).join(', ')}` : 'ok'}`);
71
87
  }
88
+ if (r.mcp) {
89
+ lines.push('', `MCP servers (${r.mcp.connected ? 'live in this session' : 'as last recorded'} — \`doctor\` does not connect; run \`ai-runtime mcp\` for a live check):`);
90
+ if (!r.mcp.servers.length)
91
+ lines.push(' (none)');
92
+ for (const sv of r.mcp.servers)
93
+ lines.push(mcpRow(r.mcp.grants, sv));
94
+ const ungranted = r.mcp.servers.filter((sv) => !Object.prototype.hasOwnProperty.call(r.mcp.grants, sv.id) || r.mcp.grants[sv.id] === 'off').map((sv) => sv.id);
95
+ if (ungranted.length)
96
+ lines.push(` not permitted to execute: ${ungranted.join(', ')} — grant permissions.mcp.servers.<id>: read (or full)`);
97
+ for (const w of r.mcp.warnings)
98
+ lines.push(` ! ${w}`);
99
+ }
72
100
  lines.push('', `Skills: ${r.skills.length ? r.skills.join(', ') : '(none)'}`);
73
101
  lines.push(`Tools: ${r.tools.join(', ')}`);
74
102
  lines.push(`Perms: fsRead=${r.permissions.fsRead} fsWrite=${r.permissions.fsWrite} shell=${r.permissions.shell} gitCommit=${r.permissions.gitCommit} gitPush=${r.permissions.gitPush} network=${r.permissions.network}`);
@@ -76,11 +104,13 @@ export function renderDoctor(r) {
76
104
  return lines;
77
105
  }
78
106
  export async function doctorCommand(opts) {
79
- const loaded = loadRuntimeConfig({ workspaceRoot: process.cwd(), ...(opts.config ? { explicitPath: opts.config } : {}) });
80
- const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
81
- const report = await gatherDoctor(rt, loaded.configFile, process.env);
82
- if (opts.json)
83
- return print(JSON.stringify(report, null, 2));
84
- for (const line of renderDoctor(report))
85
- print(line);
107
+ // Every Runtime-based command releases its Runtime (MCP children included) on every path.
108
+ return withRuntime(opts, async (rt) => {
109
+ const loaded = loadRuntimeConfig({ workspaceRoot: process.cwd(), ...(opts.config ? { explicitPath: opts.config } : {}) });
110
+ const report = await gatherDoctor(rt, loaded.configFile, process.env);
111
+ if (opts.json)
112
+ return print(JSON.stringify(report, null, 2));
113
+ for (const line of renderDoctor(report))
114
+ print(line);
115
+ });
86
116
  }
@@ -1,32 +1,41 @@
1
1
  /** `ai-runtime executions` (list) and `resume-execution <id>` — inspect and resume persisted work. */
2
- import { Runtime } from '../../runtime/runtime.js';
3
2
  import { print } from '../render.js';
3
+ import { withRuntime } from '../runtimeSession.js';
4
4
  export async function executionsCommand(options) {
5
- const rt = await Runtime.load({ ...(options.config ? { config: options.config } : {}) });
6
- const list = rt.executions();
7
- if (options.json)
8
- return print(JSON.stringify(list, null, 2));
9
- if (!list.length)
10
- return print('No executions.');
11
- print('Executions:');
12
- for (const e of list)
13
- print(` ${e.id} [${e.status}] ${e.mode}: ${e.goal.slice(0, 60)}`);
5
+ return withRuntime(options, async (rt) => {
6
+ const list = rt.executions();
7
+ if (options.json)
8
+ return print(JSON.stringify(list, null, 2));
9
+ if (!list.length)
10
+ return print('No executions.');
11
+ print('Executions:');
12
+ for (const e of list)
13
+ print(` ${e.id} [${e.status}] ${e.mode}: ${e.goal.slice(0, 60)}`);
14
+ });
14
15
  }
15
16
  export async function resumeExecutionCommand(id, options) {
16
- const rt = await Runtime.load({ ...(options.config ? { config: options.config } : {}) });
17
- // --approve proceeds past a waiting-for-approval gate; --deny cancels it. Neither leave the decision.
18
- const approve = options.approve ? true : options.deny ? false : undefined;
19
- const r = await rt.resumeExecution(id, {
20
- ...(options.answer ? { clarificationAnswer: options.answer } : {}),
21
- ...(approve !== undefined ? { approve } : {}),
17
+ return withRuntime(options, async (rt) => {
18
+ // A resumed plan can execute MCP tool steps, so its servers must be connected — and released on the
19
+ // way out (withRuntime), or a live stdio child keeps the CLI from ever exiting.
20
+ if (rt.hasMcpServers()) {
21
+ await rt.connectMcp();
22
+ for (const w of rt.mcpWarningsList())
23
+ print(`! ${w}`);
24
+ }
25
+ // --approve proceeds past a waiting-for-approval gate; --deny cancels it. Neither → leave the decision.
26
+ const approve = options.approve ? true : options.deny ? false : undefined;
27
+ const r = await rt.resumeExecution(id, {
28
+ ...(options.answer ? { clarificationAnswer: options.answer } : {}),
29
+ ...(approve !== undefined ? { approve } : {}),
30
+ });
31
+ if (r.response?.text)
32
+ print(r.response.text);
33
+ print(`status: ${r.status}`);
34
+ if (r.status === 'waiting_for_approval')
35
+ print('(waiting for approval — re-run with --approve to proceed or --deny to cancel)');
36
+ if (r.clarification)
37
+ print(`? ${r.clarification.question}`);
38
+ if (!r.ok)
39
+ process.exitCode = 1;
22
40
  });
23
- if (r.response?.text)
24
- print(r.response.text);
25
- print(`status: ${r.status}`);
26
- if (r.status === 'waiting_for_approval')
27
- print('(waiting for approval — re-run with --approve to proceed or --deny to cancel)');
28
- if (r.clarification)
29
- print(`? ${r.clarification.question}`);
30
- if (!r.ok)
31
- process.exitCode = 1;
32
41
  }
@@ -32,6 +32,7 @@ export interface InfoReport {
32
32
  home: string;
33
33
  project: string;
34
34
  enabled: boolean;
35
+ organization?: string;
35
36
  };
36
37
  }
37
38
  /** Gather the read-only info report from a loaded runtime. Pure aside from reading the local store. */
@@ -3,9 +3,9 @@
3
3
  * providers/models, skills, tools, memory + learning stats, and storage paths. Makes NO live calls and
4
4
  * mutates nothing. `--json` prints the structured report.
5
5
  */
6
- import { Runtime } from '../../runtime/runtime.js';
7
6
  import { resolveContextBudget } from '../../context/budget.js';
8
7
  import { summarizeWorkspace } from '../../runtime/workspace/workspace.js';
8
+ import { withRuntime } from '../runtimeSession.js';
9
9
  import { print } from '../render.js';
10
10
  /** Gather the read-only info report from a loaded runtime. Pure aside from reading the local store. */
11
11
  export async function gatherInfo(rt) {
@@ -31,7 +31,7 @@ export async function gatherInfo(rt) {
31
31
  tools: rt.tools().map((t) => t.id),
32
32
  memory: { enabled: rt.memory.enabled, facts },
33
33
  learning: { enabled: rt.learning.enabled, entries: learningSnap.length, preferredProviders: rt.learning.enabled ? rt.learning.preferredProviders() : [] },
34
- storage: { home: paths.home, project: paths.project, enabled: rt.store.enabled },
34
+ storage: { home: paths.home, project: paths.project, enabled: rt.store.enabled, ...(paths.organization ? { organization: paths.organization } : {}) },
35
35
  };
36
36
  }
37
37
  export function renderInfo(r) {
@@ -40,14 +40,16 @@ export function renderInfo(r) {
40
40
  lines.push(`workspace: ${r.workspace.name}${r.workspace.branch ? ` @ ${r.workspace.branch}` : ''}`, ` ${r.workspace.summary}`);
41
41
  else
42
42
  lines.push('workspace: (none detected)');
43
- lines.push(`mode: ${r.defaultMode}`, `context: ${r.contextBudgetTokens} token budget`, `providers: ${r.providers.total} configured (${r.providers.enabled} enabled), ${r.models} models`, `skills: ${r.skills.length ? r.skills.join(', ') : '(none — register tools to enable)'}`, `tools: ${r.tools.join(', ')}`, `memory: ${r.memory.enabled ? `${r.memory.facts} fact(s)` : 'disabled'}`, `learning: ${r.learning.enabled ? `${r.learning.entries} record(s)${r.learning.preferredProviders.length ? `, prefers ${r.learning.preferredProviders.join(', ')}` : ''}` : 'disabled'}`, `storage: ${r.storage.enabled ? r.storage.project : 'disabled (stateless)'}`);
43
+ lines.push(`mode: ${r.defaultMode}`, `context: ${r.contextBudgetTokens} token budget`, `providers: ${r.providers.total} configured (${r.providers.enabled} enabled), ${r.models} models`, `skills: ${r.skills.length ? r.skills.join(', ') : '(none — register tools to enable)'}`, `tools: ${r.tools.join(', ')}`, `memory: ${r.memory.enabled ? `${r.memory.facts} fact(s)` : 'disabled'}`, `learning: ${r.learning.enabled ? `${r.learning.entries} record(s)${r.learning.preferredProviders.length ? `, prefers ${r.learning.preferredProviders.join(', ')}` : ''}` : 'disabled'}`, `storage: ${r.storage.enabled ? r.storage.project : 'disabled (stateless)'}`, ...(r.storage.organization ? [`organization: ${r.storage.organization}`] : []));
44
44
  return lines;
45
45
  }
46
46
  export async function infoCommand(opts) {
47
- const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
48
- const report = await gatherInfo(rt);
49
- if (opts.json)
50
- return print(JSON.stringify(report, null, 2));
51
- for (const line of renderInfo(report))
52
- print(line);
47
+ // Every Runtime-based command releases its Runtime (MCP children included) on every path.
48
+ return withRuntime(opts, async (rt) => {
49
+ const report = await gatherInfo(rt);
50
+ if (opts.json)
51
+ return print(JSON.stringify(report, null, 2));
52
+ for (const line of renderInfo(report))
53
+ print(line);
54
+ });
53
55
  }
@@ -41,6 +41,24 @@ GEMINI_API_KEY=
41
41
  GROQ_API_KEY=
42
42
  ANTHROPIC_API_KEY=
43
43
  `;
44
+ const EXAMPLE_MODELS = `# models.md — optional per-mode / per-task model routing (Phase 23). Uncomment and edit.
45
+ # Each entry is a soft prefer (default), a hard pin (=provider:model), or a strategy keyword
46
+ # (best|fastest|cheapest|highest-quality|local-only|cloud-only|privacy-first|provider-specific|
47
+ # fallback-only, or 'free' → cheapest). Explicit per-run choices always win over this file.
48
+ #
49
+ # What applies where:
50
+ # - chat mode + '## By task' (router tasks via 'ai-runtime route <task>'): prefer, pin, AND strategy.
51
+ # - orchestration modes (plan/execute/orchestrate/agent/debug): soft PREFER only — a hard pin or a
52
+ # strategy is NOT applied there (the planner and skills pick their own models). Use prefer for those.
53
+ #
54
+ # ## By mode
55
+ # - chat: free # chat: cheapest/local models (strategy works here)
56
+ # - plan: anthropic:claude-sonnet-5 # orchestration: soft prefer a strong planning model
57
+ # - execute: openai:gpt-5 # orchestration: soft prefer
58
+ #
59
+ # ## By task
60
+ # - generate-playwright-tests: =anthropic:claude-opus-5 # via 'ai-runtime route generate-playwright-tests'
61
+ `;
44
62
  const GITIGNORE_ENTRIES = ['.env', '.env.*', '!.env.example', '.ai-runtime/'];
45
63
  function writeIfAbsent(file, contents) {
46
64
  const full = resolve(process.cwd(), file);
@@ -70,6 +88,7 @@ export function initCommand() {
70
88
  // and `setup` scaffold the same file. A root `ai-runtime.yaml` still works as a fallback if present.
71
89
  writeIfAbsent('.ai-runtime/config.yaml', EXAMPLE_YAML);
72
90
  writeIfAbsent('.env.example', EXAMPLE_ENV);
91
+ writeIfAbsent('models.md', EXAMPLE_MODELS);
73
92
  ensureGitignore();
74
93
  print('\nNext steps:');
75
94
  print(' 1. Copy .env.example to .env and fill in the keys you have');
@@ -2,6 +2,8 @@
2
2
  * `ai-runtime providers` / `models` / `capabilities` — read-only inspection of the configured fleet.
3
3
  * All go through the key-free facade views and redacted output.
4
4
  */
5
+ import { Runtime } from '../../runtime/runtime.js';
6
+ import type { CapabilityGapReason, GapSuggestion, MissingCapabilityReport } from '../../capabilities/capability.js';
5
7
  export declare function providersCommand(opts: {
6
8
  config?: string;
7
9
  json?: boolean;
@@ -9,8 +11,45 @@ export declare function providersCommand(opts: {
9
11
  export declare function modelsCommand(opts: {
10
12
  config?: string;
11
13
  json?: boolean;
14
+ profile?: boolean;
12
15
  }): Promise<void>;
13
- export declare function capabilitiesCommand(opts: {
16
+ /**
17
+ * Gather the ACTION-capability table (Phase 3.1). Uses a full Runtime (like `doctor`) so autoloaded
18
+ * skills appear — a listing that omitted them would misreport availability.
19
+ */
20
+ export declare function actionCapabilityRows(rt: Runtime): Array<{
21
+ id: string;
22
+ provider: string;
23
+ source: string;
24
+ effects: string;
25
+ availability: string;
26
+ evidence: string;
27
+ description: string;
28
+ }>;
29
+ export declare function gatherActionCapabilities(config?: string): Promise<Array<{
30
+ id: string;
31
+ provider: string;
32
+ source: string;
33
+ effects: string;
34
+ availability: string;
35
+ evidence: string;
36
+ description: string;
37
+ }>>;
38
+ /** Render the action-capability table as aligned text (shared by the CLI and the REPL). */
39
+ export declare function renderActionCapabilities(rows: Awaited<ReturnType<typeof gatherActionCapabilities>>): string[];
40
+ /** One-line gloss per gap reason (exhaustive over CapabilityGapReason). */
41
+ export declare const GAP_REASON_GLOSS: Record<CapabilityGapReason, string>;
42
+ /** One actionable line per GapSuggestion (exhaustive over all four arms). */
43
+ export declare function renderGapSuggestion(s: GapSuggestion): string;
44
+ /**
45
+ * Render a MissingCapabilityReport. THE one gap renderer: `run`, the REPL result tail,
46
+ * `capabilities --actions <goal>` and `/capabilities <goal>` all call this, so they cannot drift.
47
+ * Every id, hint, and policyKey passes `displaySafe` — a gap's capabilityId originates in the model's
48
+ * plan JSON, and `sanitizeSegment` restricts its charset but sets no length bound.
49
+ */
50
+ export declare function renderCapabilityGaps(report: MissingCapabilityReport): string[];
51
+ export declare function capabilitiesCommand(goal: string | undefined, opts: {
14
52
  config?: string;
15
53
  json?: boolean;
54
+ actions?: boolean;
16
55
  }): Promise<void>;
@@ -3,7 +3,18 @@
3
3
  * All go through the key-free facade views and redacted output.
4
4
  */
5
5
  import { loadAI } from '../context.js';
6
- import { print } from '../render.js';
6
+ import { withRuntime } from '../runtimeSession.js';
7
+ import { print, displaySafe } from '../render.js';
8
+ import { loadModelProfile } from '../../runtime/models/modelProfile.js';
9
+ import { candidatesFrom, deriveCapabilitiesOffline } from '../../runtime/planning/deriveCapabilities.js';
10
+ /** Render one models.md directive compactly. */
11
+ function describeDirective(d) {
12
+ if (d.kind === 'strategy')
13
+ return `strategy: ${d.strategy}`;
14
+ if (d.kind === 'pin')
15
+ return `pin: ${d.provider ?? ''}${d.model ? ':' + d.model : ''}`;
16
+ return `prefer: ${[...new Set([...d.models, ...d.providers])].join(', ')}`;
17
+ }
7
18
  export async function providersCommand(opts) {
8
19
  const { ai, hasProviders } = loadAI(opts.config);
9
20
  if (!hasProviders)
@@ -20,6 +31,30 @@ export async function providersCommand(opts) {
20
31
  }
21
32
  }
22
33
  export async function modelsCommand(opts) {
34
+ // `--profile` prints the loaded models.md routing profile (per-mode / per-task directives) instead.
35
+ if (opts.profile) {
36
+ const profile = loadModelProfile(process.cwd());
37
+ if (!profile)
38
+ return print('No models.md found (workspace root or .ai-runtime/models.md).');
39
+ if (opts.json)
40
+ return print(JSON.stringify(profile, null, 2));
41
+ print('Model routing (models.md):');
42
+ const byMode = Object.entries(profile.byMode);
43
+ const byTask = Object.entries(profile.byTask);
44
+ if (byMode.length) {
45
+ print(' By mode:');
46
+ for (const [k, d] of byMode)
47
+ print(` ${k.padEnd(12)} ${describeDirective(d)}`);
48
+ }
49
+ if (byTask.length) {
50
+ print(' By task:');
51
+ for (const [k, d] of byTask)
52
+ print(` ${k.padEnd(28)} ${describeDirective(d)}`);
53
+ }
54
+ if (!byMode.length && !byTask.length)
55
+ print(' (empty)');
56
+ return;
57
+ }
23
58
  const { ai, hasProviders } = loadAI(opts.config);
24
59
  if (!hasProviders)
25
60
  return print('No providers configured.');
@@ -36,7 +71,126 @@ export async function modelsCommand(opts) {
36
71
  }
37
72
  }
38
73
  const GROUPS = ['input', 'output', 'intelligence', 'agent'];
39
- export async function capabilitiesCommand(opts) {
74
+ /**
75
+ * Gather the ACTION-capability table (Phase 3.1). Uses a full Runtime (like `doctor`) so autoloaded
76
+ * skills appear — a listing that omitted them would misreport availability.
77
+ */
78
+ export function actionCapabilityRows(rt) {
79
+ const reg = rt.capabilities();
80
+ const rows = [];
81
+ for (const cap of reg.list()) {
82
+ for (const p of reg.providersOf(cap.id)) {
83
+ rows.push({ id: cap.id, provider: p.providerId, source: p.source, effects: p.effects.join('/'), availability: p.availability, evidence: p.verification, description: cap.description });
84
+ }
85
+ }
86
+ return rows;
87
+ }
88
+ export async function gatherActionCapabilities(config) {
89
+ return withRuntime({ ...(config ? { config } : {}) }, async (rt) => {
90
+ // This command's whole purpose is an accurate picture of what the runtime can do, so it DOES connect
91
+ // (the lazy rule is per command class): reporting a cache-seeded MCP tool's fail-closed placeholders
92
+ // as if they were the server's real annotations would be the wrong kind of honest.
93
+ if (rt.hasMcpServers())
94
+ await rt.connectMcp();
95
+ return actionCapabilityRows(rt);
96
+ });
97
+ }
98
+ /** Render the action-capability table as aligned text (shared by the CLI and the REPL). */
99
+ export function renderActionCapabilities(rows) {
100
+ if (rows.length === 0)
101
+ return ['No action capabilities registered.'];
102
+ const lines = ['Action capabilities (what this runtime can DO, and who provides it):'];
103
+ for (const r of rows)
104
+ lines.push(` ${r.id.padEnd(22)} ${r.provider.padEnd(26)} ${r.source.padEnd(8)} ${r.effects.padEnd(10)} ${r.availability.padEnd(12)} ${r.evidence}`);
105
+ return lines;
106
+ }
107
+ /** One-line gloss per gap reason (exhaustive over CapabilityGapReason). */
108
+ export const GAP_REASON_GLOSS = {
109
+ unknown: 'nothing registered provides it',
110
+ unavailable: 'its provider is not usable right now',
111
+ disabled: 'its provider is turned off',
112
+ permission: 'the runtime is not allowed to do this',
113
+ };
114
+ /** One actionable line per GapSuggestion (exhaustive over all four arms). */
115
+ export function renderGapSuggestion(s) {
116
+ switch (s.kind) {
117
+ case 'grant-permission':
118
+ return `grant ${displaySafe(s.policyKey, 60)} in .ai-runtime/config.yaml`;
119
+ case 'enable-server':
120
+ return `enable the MCP server '${displaySafe(s.serverId, 40)}' (ai-runtime mcp enable ${displaySafe(s.serverId, 40)}) and grant permissions.mcp.servers.${displaySafe(s.serverId, 40)}`;
121
+ case 'register':
122
+ return displaySafe(s.hint, 160);
123
+ case 'install-skill':
124
+ return displaySafe(s.hint, 160);
125
+ }
126
+ }
127
+ /**
128
+ * Render a MissingCapabilityReport. THE one gap renderer: `run`, the REPL result tail,
129
+ * `capabilities --actions <goal>` and `/capabilities <goal>` all call this, so they cannot drift.
130
+ * Every id, hint, and policyKey passes `displaySafe` — a gap's capabilityId originates in the model's
131
+ * plan JSON, and `sanitizeSegment` restricts its charset but sets no length bound.
132
+ */
133
+ export function renderCapabilityGaps(report) {
134
+ if (report.required.length === 0)
135
+ return [];
136
+ // "appears to need": derivation is a lexical inference over capability descriptions, and a plan step's
137
+ // provider can do more than the step will. The rows are advice, and the wording says so.
138
+ const lines = ['', 'Capabilities this goal appears to need:'];
139
+ for (const r of report.required) {
140
+ const gloss = r.status === 'available' ? displaySafe(r.providers.join(', '), 60) : GAP_REASON_GLOSS[r.status];
141
+ lines.push(` ${r.status === 'available' ? '✓' : '✗'} ${displaySafe(r.capabilityId, 30).padEnd(30)} ${r.status.padEnd(12)} ${gloss}`);
142
+ }
143
+ if (!report.gaps.length)
144
+ return lines;
145
+ const fixes = report.suggestions.map(renderGapSuggestion);
146
+ // A gap with an EMPTY suggestions array is reachable (every provider disabled, none MCP-sourced), so a
147
+ // gap is never printed without a remedy line.
148
+ for (const g of report.gaps) {
149
+ if (!g.suggestions.length)
150
+ fixes.push(`no automatic remedy for '${displaySafe(g.capabilityId, 30)}' — see \`ai-runtime capabilities --actions\``);
151
+ }
152
+ lines.push('To fix:', ...fixes.map((f) => ` • ${f}`));
153
+ return lines;
154
+ }
155
+ /**
156
+ * `capabilities --actions <goal>` — a free, OFFLINE "can you do this?" pre-flight. No model call and no
157
+ * provider needed: derivation is pure BM25 over the registry and resolution is pure, so this answers
158
+ * correctly with `providers: []` and no network.
159
+ */
160
+ async function actionsForGoal(goal, opts) {
161
+ return withRuntime({ ...(opts.config ? { config: opts.config } : {}) }, async (rt) => {
162
+ if (rt.hasMcpServers())
163
+ await rt.connectMcp();
164
+ const required = deriveCapabilitiesOffline(goal, candidatesFrom(rt.capabilities().list()));
165
+ if (!required.length) {
166
+ // `--json` must stay machine-readable on EVERY path — a consumer that asked for JSON and got prose
167
+ // has to special-case it.
168
+ if (opts.json)
169
+ return print(JSON.stringify({ goal: displaySafe(goal, 200), required: [], gaps: [], suggestions: [] }, null, 2));
170
+ return print(`No action capability matched ${JSON.stringify(displaySafe(goal, 80))} — run \`ai-runtime capabilities --actions\` to see everything this runtime can do.`);
171
+ }
172
+ const report = rt.capabilityReport(required);
173
+ if (opts.json)
174
+ return print(JSON.stringify(report, null, 2));
175
+ for (const line of renderCapabilityGaps(report))
176
+ print(line);
177
+ });
178
+ }
179
+ export async function capabilitiesCommand(goal, opts) {
180
+ // `--actions` shows ACTION capabilities (Phase 3.1); the default stays the MODEL capability matrix.
181
+ // With a goal, `--actions` answers "what does THIS need, and what is missing?" — offline (Phase 3.3).
182
+ // A goal only means anything for action capabilities (the model matrix takes no goal), so a goal
183
+ // IMPLIES --actions rather than being silently discarded.
184
+ if (goal)
185
+ return actionsForGoal(goal, opts);
186
+ if (opts.actions) {
187
+ const rows = await gatherActionCapabilities(opts.config);
188
+ if (opts.json)
189
+ return print(JSON.stringify(rows, null, 2));
190
+ for (const line of renderActionCapabilities(rows))
191
+ print(line);
192
+ return;
193
+ }
40
194
  const { ai, hasProviders } = loadAI(opts.config);
41
195
  if (!hasProviders)
42
196
  return print('No providers configured.');
@@ -57,4 +211,5 @@ export async function capabilitiesCommand(opts) {
57
211
  print(` context ${ctx.value} (${ctx.evidence})`);
58
212
  }
59
213
  }
214
+ print('\n(model capabilities — use `--actions` for action capabilities: read_file, run_tests, …)');
60
215
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `ai-runtime mcp` — inspect and manage MCP servers (Phase 3.2).
3
+ *
4
+ * mcp list every configured server with its state, tool count, and grant
5
+ * mcp <id> one server in detail, including redacted diagnostics
6
+ * mcp add <id> … add a server — persisted to the STORE, never to config.yaml
7
+ * mcp remove <id> remove a store-added server (a config-declared one is the operator's file)
8
+ * mcp enable|disable <id> flip a server without editing anything
9
+ * mcp test <id> connect, handshake, list tools, ping — and report honestly
10
+ *
11
+ * Credentials are named, never valued: `--token-env NAME` records an env-var NAME, and nothing here ever
12
+ * prints a token value. Every server-supplied string reaching this output has already passed the
13
+ * discovery-time normalization gate, so a server cannot forge a row.
14
+ */
15
+ import type { McpServerStatus } from '../../mcp/manager.js';
16
+ /**
17
+ * The per-server grant map (`permissions.mcp.servers`), passed to the renderers instead of a Runtime so
18
+ * they stay Runtime-free — `mcp`, the REPL `/mcp`, and `doctor` therefore share ONE formatter and can
19
+ * never disagree. The map is null-prototype (src/tools/permissions.ts), so `?? 'off'` is correct for
20
+ * every key, configured or not.
21
+ */
22
+ export type McpGrants = Record<string, string>;
23
+ export declare function mcpRow(grants: McpGrants, s: McpServerStatus): string;
24
+ export declare function renderMcpList(grants: McpGrants, list: McpServerStatus[]): string[];
25
+ export declare function renderMcpDetail(grants: McpGrants, s: McpServerStatus, diagnostics: string[], tools: Array<{
26
+ name: string;
27
+ description: string;
28
+ readOnly: boolean;
29
+ destructive: boolean;
30
+ }>): string[];
31
+ export interface McpCommandOptions {
32
+ config?: string;
33
+ json?: boolean;
34
+ /** add: */
35
+ command?: string;
36
+ args?: string;
37
+ url?: string;
38
+ tokenEnv?: string;
39
+ timeout?: string;
40
+ }
41
+ export declare function mcpCommand(id: string | undefined, opts: McpCommandOptions): Promise<void>;
42
+ export declare function mcpAddCommand(id: string, opts: McpCommandOptions): Promise<void>;
43
+ export declare function mcpRemoveCommand(id: string, opts: McpCommandOptions): Promise<void>;
44
+ export declare function mcpEnableCommand(id: string, enabled: boolean, opts: McpCommandOptions): Promise<void>;
45
+ export declare function mcpTestCommand(id: string, opts: McpCommandOptions): Promise<void>;