@cruxy/cli 0.23.0 → 0.25.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 (84) hide show
  1. package/dist/agent/loop.d.ts +21 -2
  2. package/dist/agent/loop.js +21 -5
  3. package/dist/agent/session.d.ts +13 -0
  4. package/dist/agent/session.js +6 -0
  5. package/dist/approval/index.d.ts +1 -0
  6. package/dist/approval/index.js +1 -0
  7. package/dist/approval/mutex.d.ts +45 -0
  8. package/dist/approval/mutex.js +57 -0
  9. package/dist/checkpoint/gate-hook.d.ts +28 -0
  10. package/dist/checkpoint/gate-hook.js +98 -0
  11. package/dist/checkpoint/gate.d.ts +7 -1
  12. package/dist/checkpoint/gate.js +8 -2
  13. package/dist/checkpoint/index.d.ts +1 -0
  14. package/dist/checkpoint/index.js +1 -0
  15. package/dist/checkpoint/service.d.ts +9 -0
  16. package/dist/checkpoint/service.js +20 -0
  17. package/dist/cli/commands/rollback.d.ts +4 -1
  18. package/dist/cli/commands/rollback.js +16 -9
  19. package/dist/cli/commands/run.js +62 -16
  20. package/dist/cli/onboard.js +2 -2
  21. package/dist/cli/repl.d.ts +1 -1
  22. package/dist/cli/repl.js +145 -0
  23. package/dist/cli/session-factory.d.ts +24 -10
  24. package/dist/cli/session-factory.js +179 -135
  25. package/dist/config/schema.d.ts +110 -0
  26. package/dist/config/schema.js +50 -0
  27. package/dist/errors/constructors.d.ts +41 -0
  28. package/dist/errors/constructors.js +87 -0
  29. package/dist/errors/types.d.ts +21 -0
  30. package/dist/errors/types.js +33 -0
  31. package/dist/hooks/index.d.ts +1 -0
  32. package/dist/hooks/index.js +1 -0
  33. package/dist/hooks/router.d.ts +58 -0
  34. package/dist/hooks/router.js +136 -0
  35. package/dist/hooks/runner.d.ts +12 -0
  36. package/dist/hooks/runner.js +23 -1
  37. package/dist/jobs/approval-queue.d.ts +85 -0
  38. package/dist/jobs/approval-queue.js +96 -0
  39. package/dist/jobs/dispatch-tool.d.ts +34 -0
  40. package/dist/jobs/dispatch-tool.js +96 -0
  41. package/dist/jobs/index.d.ts +6 -0
  42. package/dist/jobs/index.js +6 -0
  43. package/dist/jobs/log-buffer.d.ts +31 -0
  44. package/dist/jobs/log-buffer.js +30 -0
  45. package/dist/jobs/log-renderer.d.ts +32 -0
  46. package/dist/jobs/log-renderer.js +70 -0
  47. package/dist/jobs/manager.d.ts +139 -0
  48. package/dist/jobs/manager.js +397 -0
  49. package/dist/jobs/types.d.ts +81 -0
  50. package/dist/jobs/types.js +10 -0
  51. package/dist/mcp/index.d.ts +1 -0
  52. package/dist/mcp/index.js +1 -0
  53. package/dist/mcp/sibling-banner.d.ts +25 -0
  54. package/dist/mcp/sibling-banner.js +34 -0
  55. package/dist/memory/recall.d.ts +24 -0
  56. package/dist/memory/recall.js +54 -0
  57. package/dist/memory/remember-tool.d.ts +3 -0
  58. package/dist/memory/remember-tool.js +11 -1
  59. package/dist/sandbox/policy.js +14 -5
  60. package/dist/sandbox/service.d.ts +8 -1
  61. package/dist/sandbox/service.js +4 -1
  62. package/dist/subagent/index.d.ts +1 -0
  63. package/dist/subagent/index.js +1 -0
  64. package/dist/subagent/orchestrator.d.ts +76 -2
  65. package/dist/subagent/orchestrator.js +208 -18
  66. package/dist/subagent/registry-scope.d.ts +13 -0
  67. package/dist/subagent/registry-scope.js +28 -2
  68. package/dist/subagent/semaphore.d.ts +56 -0
  69. package/dist/subagent/semaphore.js +53 -0
  70. package/dist/subagent/spawn-tool.d.ts +57 -0
  71. package/dist/subagent/spawn-tool.js +104 -9
  72. package/dist/subagent/types.d.ts +17 -2
  73. package/dist/testing/run-tests-tool.js +1 -1
  74. package/dist/tools/file/paths.d.ts +5 -6
  75. package/dist/tools/file/paths.js +7 -8
  76. package/dist/tools/shell/exec.js +36 -4
  77. package/dist/tools/types.d.ts +16 -5
  78. package/dist/workspace/add-root.d.ts +27 -0
  79. package/dist/workspace/add-root.js +16 -0
  80. package/dist/workspace/index.d.ts +2 -1
  81. package/dist/workspace/index.js +2 -1
  82. package/dist/workspace/workspace.d.ts +9 -4
  83. package/dist/workspace/workspace.js +9 -4
  84. package/package.json +1 -1
@@ -7,14 +7,14 @@ import { themeForColor } from "../../theme/index.js";
7
7
  import { summarizeRuns, renderSummary, } from "../../usage/index.js";
8
8
  import { CheckpointGate } from "../../checkpoint/index.js";
9
9
  import { SandboxService } from "../../sandbox/index.js";
10
- import { buildHooksService } from "../../hooks/index.js";
10
+ import { buildHooksService, buildHooksRouter } from "../../hooks/index.js";
11
11
  import { runInteractive } from "../repl.js";
12
12
  import { buildAgentSession } from "../session-factory.js";
13
13
  import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
14
14
  import { resetLspServices } from "../../lsp/index.js";
15
- import { connectMcpTools, resetMcpServices } from "../../mcp/index.js";
15
+ import { connectMcpTools, deferredSiblingServers, resetMcpServices, } from "../../mcp/index.js";
16
16
  import { defaultPromptIO } from "../../approval/index.js";
17
- import { buildWorkspace, singleRootWorkspace, } from "../../workspace/index.js";
17
+ import { buildWorkspace, sessionWorkspace, } from "../../workspace/index.js";
18
18
  /**
19
19
  * Parse one repeatable `--root` value into a {@link RootSpec} and append it. Form
20
20
  * `name=path` (explicit name) or a bare `path` (basename-named by buildWorkspace).
@@ -59,7 +59,7 @@ export function runCommand() {
59
59
  // onboarding or the session starts, never a half-built session.
60
60
  const workspace = opts.root.length
61
61
  ? await buildWorkspace(opts.root, { cwd: invocationCwd })
62
- : singleRootWorkspace(invocationCwd);
62
+ : sessionWorkspace(invocationCwd);
63
63
  const primaryRoot = workspace.primary().absPath;
64
64
  logger.info(t.muted(`model: ${config.model.provider}/${config.model.model}`));
65
65
  logger.info(t.muted(`config: ${sources.project ?? sources.global ?? "defaults"}`));
@@ -78,7 +78,7 @@ export function runCommand() {
78
78
  ? "writes fan all roots (each checkpointed)"
79
79
  : "writes scope to primary (checkpoints disabled)";
80
80
  logger.info(t.muted(`roots: ${workspace.roots().length} (${names}); primary ${workspace.primary().name} — ` +
81
- `reads fan all roots; ${writes}; hooks/MCP scope to primary this release`));
81
+ `reads fan all roots; ${writes}; hooks fan per trusted root; MCP loads from primary only this release`));
82
82
  }
83
83
  // First-run with no key (and a TTY) → guided onboarding instead of the
84
84
  // dead-end auth error. The first-win demo is offered only in the no-prompt
@@ -127,21 +127,46 @@ export function runCommand() {
127
127
  ? await SandboxService.create({
128
128
  config,
129
129
  cwd: primaryRoot,
130
+ // R5 (C.26): the command's root (primary — shell/test attribute to
131
+ // it this release) mounts read-write; every OTHER declared root
132
+ // mounts read-only. Cross-root write is only ever granted per-root
133
+ // via a named escalation, never blanket.
134
+ siblingRoots: workspace
135
+ .roots()
136
+ .filter((r) => !r.primary)
137
+ .map((r) => r.absPath),
130
138
  reporter: renderer,
131
139
  })
132
140
  : undefined;
133
141
  if (sandbox) {
134
142
  logger.info(t.muted(`sandbox: ${sandbox.runtimeName} (network ${config.sandbox.network})`));
135
143
  }
136
- // Hooks + custom slash commands (C.19). Built once per run: loads the
137
- // layered catalog and yields the lifecycle runner (threaded into the
138
- // session) + the resolved custom slash commands (given to the REPL).
139
- const hooksService = await buildHooksService({
140
- cwd: primaryRoot,
141
- config,
142
- interactive: Boolean(process.stdin.isTTY),
143
- logger,
144
- });
144
+ // Hooks + custom slash commands (C.19). Single-root uses the unchanged
145
+ // service (byte-identical: one runner, lazy trust prompt). Multi-root
146
+ // (C.26 step 5) builds ONE runner per root behind a HookRouter tool
147
+ // events fire the acting root's hooks, lifecycle events fan every trusted
148
+ // root. Untrusted roots are skipped and NAMED here (never a silent loss).
149
+ let hooksRunner;
150
+ let hookCommands;
151
+ if (workspace.isMultiRoot) {
152
+ const router = await buildHooksRouter({ workspace, config, logger });
153
+ hooksRunner = router.runner;
154
+ hookCommands = router.commands;
155
+ for (const u of router.untrustedHookRoots) {
156
+ logger.info(t.muted(`hooks: root "${u.name}" has ${u.count} untrusted hook${u.count === 1 ? "" : "s"} — ` +
157
+ `not firing (run \`cruxy hooks trust ${u.absPath}\`)`));
158
+ }
159
+ }
160
+ else {
161
+ const hooksService = await buildHooksService({
162
+ cwd: primaryRoot,
163
+ config,
164
+ interactive: Boolean(process.stdin.isTTY),
165
+ logger,
166
+ });
167
+ hooksRunner = hooksService.runner;
168
+ hookCommands = hooksService.commands;
169
+ }
145
170
  // MCP servers (C.27): connect + trust-gate BEFORE building the session so
146
171
  // the tool catalogue is complete when the model first runs. Off by
147
172
  // default (no servers connect). A non-interactive run with an untrusted
@@ -153,12 +178,29 @@ export function runCommand() {
153
178
  interactive: Boolean(process.stdin.isTTY),
154
179
  io: defaultPromptIO(shouldUseColor()),
155
180
  });
156
- const session = buildAgentSession(config, apiKey, workspace, Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksService.runner, mcp.tools);
181
+ // MCP is primary-root only this release (JC-D). Name each sibling-root
182
+ // server individually — never one generic line, never a silent drop —
183
+ // so a user who declared `github` in a sibling knows exactly why its
184
+ // tools aren't present (and that root-qualified names are a follow-up).
185
+ if (workspace.isMultiRoot) {
186
+ for (const d of deferredSiblingServers(workspace)) {
187
+ logger.info(t.muted(`mcp: server "${d.server}" declared in ${d.root} not loaded (primary-root MCP only this release)`));
188
+ }
189
+ }
190
+ const session = buildAgentSession(config, apiKey, workspace, Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksRunner, mcp.tools);
157
191
  if (interactive) {
158
192
  try {
159
- await runInteractive(session, undefined, renderer, checkpoints, hooksService.commands);
193
+ await runInteractive(session, undefined, renderer, checkpoints, hookCommands);
160
194
  }
161
195
  finally {
196
+ // Background jobs (C.28): cancel every live job on session exit —
197
+ // kill-tree'ing each job's process group (no orphan) and reporting an
198
+ // honest "N job(s) cancelled". A paused job is cancelled too, but any
199
+ // checkpoint it took survives for review/rollback.
200
+ const cancelled = (await session.jobs?.cancelAll("session exit")) ?? 0;
201
+ if (cancelled > 0) {
202
+ logger.print(t.muted(`${cancelled} background job${cancelled === 1 ? "" : "s"} cancelled on exit`));
203
+ }
162
204
  // LSP (C.12) + MCP (C.27): gracefully shut down any external server
163
205
  // processes spawned during the session (the shared process-exit
164
206
  // kill-tree is the fail-safe for a hard kill).
@@ -182,6 +224,10 @@ export function runCommand() {
182
224
  }
183
225
  finally {
184
226
  renderer.close();
227
+ // Background jobs (C.28): a one-shot run has no between-turns servicing
228
+ // loop, so cancel every live job on exit (kill-tree, no orphan) rather
229
+ // than leave one paused forever. Checkpoints taken survive for rollback.
230
+ await session.jobs?.cancelAll("session exit");
185
231
  // LSP (C.12) + MCP (C.27): gracefully shut down any external server
186
232
  // processes spawned during the run (the shared process-exit kill-tree
187
233
  // is the fail-safe for a hard kill).
@@ -1,7 +1,7 @@
1
1
  import { resolveApiKey } from "../config/index.js";
2
2
  import { createDefaultDeps, defaultOnboardingIO, isFirstRun, runOnboarding, } from "../onboarding/index.js";
3
3
  import { createRenderer } from "../render/index.js";
4
- import { singleRootWorkspace } from "../workspace/index.js";
4
+ import { sessionWorkspace } from "../workspace/index.js";
5
5
  import { buildAgentSession } from "./session-factory.js";
6
6
  /**
7
7
  * CLI-layer glue between the entry points and the onboarding module (U.6). Keeps
@@ -27,7 +27,7 @@ export async function runFirstWinTask(config, cwd, prompt) {
27
27
  const renderer = createRenderer();
28
28
  // The onboarding first-win is inherently single-root (it runs before any
29
29
  // `--root` is parsed), so it acts over a trivial workspace on its cwd.
30
- const session = buildAgentSession(config, apiKey, singleRootWorkspace(cwd), true, false, renderer);
30
+ const session = buildAgentSession(config, apiKey, sessionWorkspace(cwd), true, false, renderer);
31
31
  try {
32
32
  await session.send(prompt, renderer);
33
33
  }
@@ -7,7 +7,7 @@ import { type StreamRenderer } from "../render/index.js";
7
7
  * The REPL's slash commands — the autocomplete candidate set (U.7). Keep in
8
8
  * sync with the dispatch below and the HELP text.
9
9
  */
10
- export declare const REPL_COMMANDS: readonly ["/help", "/clear", "/compact", "/reload", "/plan", "/exit", "/quit"];
10
+ export declare const REPL_COMMANDS: readonly ["/help", "/clear", "/compact", "/reload", "/plan", "/jobs", "/logs", "/cancel", "/exit", "/quit"];
11
11
  /** The stdin/stdout pair the REPL reads from and prompts on. Injectable for tests. */
12
12
  export interface ReplIO {
13
13
  input: Readable;
package/dist/cli/repl.js CHANGED
@@ -2,6 +2,7 @@ import readline from "node:readline";
2
2
  import { makeReplCompleter } from "../components/index.js";
3
3
  import { resolveSlash } from "../hooks/index.js";
4
4
  import { runGatedShell } from "../tools/shell/exec.js";
5
+ import { addRootToWorkspace } from "../workspace/index.js";
5
6
  import { themeForColor } from "../theme/index.js";
6
7
  import { formatError, fromUnknown, isVerbose, shouldUseColor, } from "../errors/index.js";
7
8
  import { createRenderer } from "../render/index.js";
@@ -19,6 +20,9 @@ export const REPL_COMMANDS = [
19
20
  "/compact",
20
21
  "/reload",
21
22
  "/plan",
23
+ "/jobs",
24
+ "/logs",
25
+ "/cancel",
22
26
  "/exit",
23
27
  "/quit",
24
28
  ];
@@ -28,6 +32,9 @@ const HELP = `Commands:
28
32
  /compact summarize older history to free up context now
29
33
  /reload re-read project instructions (CRUXY.md)
30
34
  /plan toggle plan mode (propose a plan before executing)
35
+ /jobs list background jobs and their status
36
+ /logs <id> show a background job's log
37
+ /cancel <id> cancel a background job
31
38
  /exit, /quit leave cruxy
32
39
  Ctrl+D leave cruxy`;
33
40
  const defaultIO = () => ({
@@ -113,6 +120,123 @@ function printReplError(err) {
113
120
  color: shouldUseColor(process.stdout),
114
121
  }));
115
122
  }
123
+ /**
124
+ * `/add-root <name> <path>` (C.26 step 5) — an explicit, human-only way to
125
+ * declare another workspace root. This is a REPL command, NOT a model tool: the
126
+ * model can only reach the tool registry, and nothing named `add_root` is
127
+ * registered there, so the allowlist argument is unchanged. It validates the
128
+ * addition (TTY-only; same existence/name/overlap refusal as `--root`) and, on
129
+ * success, tells the user to relaunch with `--root` to activate it — a
130
+ * mid-session hot-swap would leave the checkpoint gate + hook router (both wired
131
+ * from the session-start workspace) half-attributed, so activation is deferred
132
+ * to a clean relaunch. A newly declared root always starts untrusted.
133
+ */
134
+ async function handleAddRoot(input, session) {
135
+ const parts = input
136
+ .slice("/add-root".length)
137
+ .trim()
138
+ .split(/\s+/)
139
+ .filter(Boolean);
140
+ if (parts.length !== 2) {
141
+ logger.print(theme.muted("usage: /add-root <name> <path>"));
142
+ return;
143
+ }
144
+ const [name, rootPath] = parts;
145
+ const ctx = session.toolContext;
146
+ const current = ctx.workspace;
147
+ try {
148
+ const next = await addRootToWorkspace(current, { name, path: rootPath }, { cwd: ctx.cwd, tty: Boolean(process.stdin.isTTY) });
149
+ const abs = next.rootByName(name).absPath;
150
+ logger.print(theme.muted(`validated root "${name}" (${abs}). relaunch with \`--root ${name}=${rootPath}\` to activate it — ` +
151
+ `it starts untrusted (its hooks and project memory stay inert until you run \`cruxy hooks/memory trust\`).`));
152
+ }
153
+ catch (err) {
154
+ printReplError(err);
155
+ }
156
+ }
157
+ /**
158
+ * Service any background-job approvals that piled up (C.28) — the auto-surface
159
+ * point. Called when the foreground is idle (between turns), so a paused job's
160
+ * gated action is decided through the SAME U.3 prompt as a foreground one, one at
161
+ * a time. No pending → a no-op (nothing printed). The job resumes on approval.
162
+ */
163
+ async function drainJobApprovals(session) {
164
+ const jobs = session.jobs;
165
+ if (!jobs || !jobs.hasPendingApprovals())
166
+ return;
167
+ logger.print(theme.muted(`\n${theme.glyph.bullet} a background job needs your approval:`));
168
+ await jobs.serviceApprovals();
169
+ }
170
+ /** Render the background-job list (`/jobs`). */
171
+ function handleJobsList(session) {
172
+ const jobs = session.jobs;
173
+ if (!jobs) {
174
+ logger.print(theme.muted("background jobs are disabled — enable with `cruxy config set jobs.enabled true`"));
175
+ return;
176
+ }
177
+ const list = jobs.list();
178
+ if (list.length === 0) {
179
+ logger.print(theme.muted("no background jobs this session"));
180
+ return;
181
+ }
182
+ for (const j of list) {
183
+ const status = j.status === "failed" ? theme.danger(j.status) : theme.accent(j.status);
184
+ const pending = j.pendingApproval
185
+ ? theme.muted(` — needs approval: ${j.pendingApproval}`)
186
+ : "";
187
+ const err = j.error ? theme.muted(` (${j.error})`) : "";
188
+ logger.print(`${theme.strong(j.id)} ${status} ${j.label}${pending}${err}`);
189
+ }
190
+ }
191
+ /** Print one job's log (`/logs <id>`). */
192
+ function handleJobLogs(input, session) {
193
+ const jobs = session.jobs;
194
+ if (!jobs) {
195
+ logger.print(theme.muted("background jobs are disabled"));
196
+ return;
197
+ }
198
+ const id = input.slice("/logs".length).trim();
199
+ if (!id) {
200
+ logger.print(theme.muted("usage: /logs <id>"));
201
+ return;
202
+ }
203
+ try {
204
+ const log = jobs.logs(id);
205
+ if (log.dropped > 0) {
206
+ logger.print(theme.muted(`… ${log.dropped} earlier line(s) rolled off`));
207
+ }
208
+ for (const line of log.lines) {
209
+ const text = line.stream === "err" ? theme.danger(line.text) : line.text;
210
+ logger.print(text);
211
+ }
212
+ logger.print(theme.muted(`(${log.status})`));
213
+ }
214
+ catch (err) {
215
+ printReplError(err);
216
+ }
217
+ }
218
+ /** Cancel a job (`/cancel <id>`). */
219
+ async function handleJobCancel(input, session) {
220
+ const jobs = session.jobs;
221
+ if (!jobs) {
222
+ logger.print(theme.muted("background jobs are disabled"));
223
+ return;
224
+ }
225
+ const id = input.slice("/cancel".length).trim();
226
+ if (!id) {
227
+ logger.print(theme.muted("usage: /cancel <id>"));
228
+ return;
229
+ }
230
+ try {
231
+ const cancelled = jobs.cancel(id);
232
+ logger.print(theme.muted(cancelled
233
+ ? `cancelling ${id} (its process tree is killed; any checkpoint survives for rollback)`
234
+ : `${id} is already finished`));
235
+ }
236
+ catch (err) {
237
+ printReplError(err);
238
+ }
239
+ }
116
240
  /**
117
241
  * Drive an interactive multi-turn session: prompt, read a line, dispatch slash
118
242
  * commands or run a turn, repeat. Assistant text and tool-call progress stream
@@ -135,6 +259,11 @@ export async function runInteractive(session, io = defaultIO(), renderer = creat
135
259
  }
136
260
  async function replLoop(session, io, renderer, checkpoints, slashCommands = []) {
137
261
  for (;;) {
262
+ // Auto-surface (C.28): before prompting, service any background-job approvals
263
+ // that piled up while the last turn ran — one at a time, through the same U.3
264
+ // prompt. Done here (foreground idle, no readline interface live) so a job's
265
+ // prompt never contends with the line reader.
266
+ await drainJobApprovals(session);
138
267
  const line = await readLine(io, PROMPT);
139
268
  // EOF / Ctrl+D.
140
269
  if (line === null) {
@@ -182,6 +311,22 @@ async function replLoop(session, io, renderer, checkpoints, slashCommands = [])
182
311
  logger.print(HELP);
183
312
  continue;
184
313
  }
314
+ if (trimmed === "/jobs") {
315
+ handleJobsList(session);
316
+ continue;
317
+ }
318
+ if (trimmed === "/logs" || trimmed.startsWith("/logs ")) {
319
+ handleJobLogs(trimmed, session);
320
+ continue;
321
+ }
322
+ if (trimmed === "/cancel" || trimmed.startsWith("/cancel ")) {
323
+ await handleJobCancel(trimmed, session);
324
+ continue;
325
+ }
326
+ if (trimmed === "/add-root" || trimmed.startsWith("/add-root ")) {
327
+ await handleAddRoot(trimmed, session);
328
+ continue;
329
+ }
185
330
  // Custom slash commands (C.19) — consulted AFTER builtins, so a custom
186
331
  // command can never shadow /help, /exit, etc. A `prompt` command expands to
187
332
  // text fed to the agent (safe); a `shell` command runs through the SAME
@@ -1,21 +1,35 @@
1
1
  import type { CruxyConfig } from "../config/index.js";
2
- import type { ApprovalDecision } from "../approval/index.js";
2
+ import { withCheckpointGate } from "../checkpoint/index.js";
3
3
  import type { CheckpointGate } from "../checkpoint/index.js";
4
+ export { withCheckpointGate };
4
5
  import type { SandboxService } from "../sandbox/index.js";
5
6
  import type { StreamRenderer } from "../render/index.js";
6
- import { type ApproveAction, type Tool } from "../tools/index.js";
7
+ import { ToolRegistry, type Tool } from "../tools/index.js";
7
8
  import { Session, type LifecycleHookRunner } from "../agent/index.js";
8
9
  import type { Workspace } from "../workspace/index.js";
9
10
  /**
10
- * Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
11
- * whole point: a tool mutates only *after* `requestApproval` resolves, so
12
- * snapshotting after an `allow` decision but before returning it means the
13
- * checkpoint always lands before the run's first mutation and a denied
14
- * action never creates one. The same seam records which paths the run touched
15
- * (file actions) or that attribution is lost (shell), for rollback's
16
- * external-change detection.
11
+ * Register every CONDITIONALLY-enabled runtime tool onto `registry`, in the fixed
12
+ * order the model sees them: `remember` (memory), the four LSP tools, the two web
13
+ * tools, the trusted MCP tools, then `spawn_subagent`. Factored out of
14
+ * {@link buildAgentSession} for ONE reason beyond tidiness: it makes the *complete
15
+ * runtime* tool set enumerable by a test (JC-B). The default registry only covers
16
+ * the 14 always-on tools; the allowlist test that pins "no tool can add a workspace
17
+ * root" is only sound if it runs against the SAME set the session ships — so the
18
+ * session factory and that test both build the surface through this one seam. A new
19
+ * conditionally-registered tool added here fails the allowlist test until it is
20
+ * consciously listed and audited.
21
+ *
22
+ * Each family is opt-in and inert when its feature flag is off, exactly as before —
23
+ * this function is a pure move of the inline registration, same guards, same order.
24
+ * `spawnTool` is passed in (already depth-bound) because it needs the orchestrator;
25
+ * it registers only when nesting is allowed (`subagent.maxDepth > 0`).
17
26
  */
18
- export declare function withCheckpointGate(requestApproval: (action: ApproveAction) => Promise<ApprovalDecision>, gate: CheckpointGate | undefined, ws: Workspace): (action: ApproveAction) => Promise<ApprovalDecision>;
27
+ export declare function registerRuntimeTools(registry: ToolRegistry, config: CruxyConfig, opts?: {
28
+ mcpTools?: Tool[];
29
+ spawnTool?: Tool;
30
+ spawnManyTool?: Tool;
31
+ jobTool?: Tool;
32
+ }): void;
19
33
  /**
20
34
  * Build a ready-to-run agent {@link Session} from a resolved key — the wiring
21
35
  * shared by `cruxy run` and the onboarding first-win task (so they can't drift).