@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.
- package/dist/agent/loop.d.ts +21 -2
- package/dist/agent/loop.js +21 -5
- package/dist/agent/session.d.ts +13 -0
- package/dist/agent/session.js +6 -0
- package/dist/approval/index.d.ts +1 -0
- package/dist/approval/index.js +1 -0
- package/dist/approval/mutex.d.ts +45 -0
- package/dist/approval/mutex.js +57 -0
- package/dist/checkpoint/gate-hook.d.ts +28 -0
- package/dist/checkpoint/gate-hook.js +98 -0
- package/dist/checkpoint/gate.d.ts +7 -1
- package/dist/checkpoint/gate.js +8 -2
- package/dist/checkpoint/index.d.ts +1 -0
- package/dist/checkpoint/index.js +1 -0
- package/dist/checkpoint/service.d.ts +9 -0
- package/dist/checkpoint/service.js +20 -0
- package/dist/cli/commands/rollback.d.ts +4 -1
- package/dist/cli/commands/rollback.js +16 -9
- package/dist/cli/commands/run.js +62 -16
- package/dist/cli/onboard.js +2 -2
- package/dist/cli/repl.d.ts +1 -1
- package/dist/cli/repl.js +145 -0
- package/dist/cli/session-factory.d.ts +24 -10
- package/dist/cli/session-factory.js +179 -135
- package/dist/config/schema.d.ts +110 -0
- package/dist/config/schema.js +50 -0
- package/dist/errors/constructors.d.ts +41 -0
- package/dist/errors/constructors.js +87 -0
- package/dist/errors/types.d.ts +21 -0
- package/dist/errors/types.js +33 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/router.d.ts +58 -0
- package/dist/hooks/router.js +136 -0
- package/dist/hooks/runner.d.ts +12 -0
- package/dist/hooks/runner.js +23 -1
- package/dist/jobs/approval-queue.d.ts +85 -0
- package/dist/jobs/approval-queue.js +96 -0
- package/dist/jobs/dispatch-tool.d.ts +34 -0
- package/dist/jobs/dispatch-tool.js +96 -0
- package/dist/jobs/index.d.ts +6 -0
- package/dist/jobs/index.js +6 -0
- package/dist/jobs/log-buffer.d.ts +31 -0
- package/dist/jobs/log-buffer.js +30 -0
- package/dist/jobs/log-renderer.d.ts +32 -0
- package/dist/jobs/log-renderer.js +70 -0
- package/dist/jobs/manager.d.ts +139 -0
- package/dist/jobs/manager.js +397 -0
- package/dist/jobs/types.d.ts +81 -0
- package/dist/jobs/types.js +10 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/sibling-banner.d.ts +25 -0
- package/dist/mcp/sibling-banner.js +34 -0
- package/dist/memory/recall.d.ts +24 -0
- package/dist/memory/recall.js +54 -0
- package/dist/memory/remember-tool.d.ts +3 -0
- package/dist/memory/remember-tool.js +11 -1
- package/dist/sandbox/policy.js +14 -5
- package/dist/sandbox/service.d.ts +8 -1
- package/dist/sandbox/service.js +4 -1
- package/dist/subagent/index.d.ts +1 -0
- package/dist/subagent/index.js +1 -0
- package/dist/subagent/orchestrator.d.ts +76 -2
- package/dist/subagent/orchestrator.js +208 -18
- package/dist/subagent/registry-scope.d.ts +13 -0
- package/dist/subagent/registry-scope.js +28 -2
- package/dist/subagent/semaphore.d.ts +56 -0
- package/dist/subagent/semaphore.js +53 -0
- package/dist/subagent/spawn-tool.d.ts +57 -0
- package/dist/subagent/spawn-tool.js +104 -9
- package/dist/subagent/types.d.ts +17 -2
- package/dist/testing/run-tests-tool.js +1 -1
- package/dist/tools/file/paths.d.ts +5 -6
- package/dist/tools/file/paths.js +7 -8
- package/dist/tools/shell/exec.js +36 -4
- package/dist/tools/types.d.ts +16 -5
- package/dist/workspace/add-root.d.ts +27 -0
- package/dist/workspace/add-root.js +16 -0
- package/dist/workspace/index.d.ts +2 -1
- package/dist/workspace/index.js +2 -1
- package/dist/workspace/workspace.d.ts +9 -4
- package/dist/workspace/workspace.js +9 -4
- package/package.json +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -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,
|
|
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
|
-
:
|
|
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
|
|
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).
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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,
|
|
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).
|
package/dist/cli/onboard.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
30
|
+
const session = buildAgentSession(config, apiKey, sessionWorkspace(cwd), true, false, renderer);
|
|
31
31
|
try {
|
|
32
32
|
await session.send(prompt, renderer);
|
|
33
33
|
}
|
package/dist/cli/repl.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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).
|