@cruxy/cli 0.7.0 → 0.9.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/README.md +46 -13
- package/dist/agent/loop.d.ts +35 -6
- package/dist/agent/loop.js +84 -10
- package/dist/agent/prompts.d.ts +2 -0
- package/dist/agent/prompts.js +8 -0
- package/dist/agent/session.d.ts +6 -4
- package/dist/agent/session.js +6 -5
- package/dist/approval/classify.js +26 -0
- package/dist/approval/prompt.d.ts +9 -0
- package/dist/approval/prompt.js +2 -77
- package/dist/checkpoint/capture.d.ts +17 -0
- package/dist/checkpoint/capture.js +73 -0
- package/dist/checkpoint/git-store.d.ts +61 -0
- package/dist/checkpoint/git-store.js +171 -0
- package/dist/checkpoint/index.d.ts +6 -0
- package/dist/checkpoint/index.js +6 -0
- package/dist/checkpoint/restore.d.ts +23 -0
- package/dist/checkpoint/restore.js +195 -0
- package/dist/checkpoint/service.d.ts +80 -0
- package/dist/checkpoint/service.js +276 -0
- package/dist/checkpoint/shadow-store.d.ts +23 -0
- package/dist/checkpoint/shadow-store.js +93 -0
- package/dist/checkpoint/types.d.ts +117 -0
- package/dist/checkpoint/types.js +18 -0
- package/dist/cli/commands/checkpoint.d.ts +7 -0
- package/dist/cli/commands/checkpoint.js +31 -0
- package/dist/cli/commands/rollback.d.ts +10 -0
- package/dist/cli/commands/rollback.js +51 -0
- package/dist/cli/commands/run.js +24 -10
- package/dist/cli/onboard.js +9 -4
- package/dist/cli/program.js +4 -0
- package/dist/cli/repl.d.ts +10 -4
- package/dist/cli/repl.js +26 -12
- package/dist/cli/session-factory.d.ts +15 -1
- package/dist/cli/session-factory.js +104 -18
- package/dist/config/schema.d.ts +133 -0
- package/dist/config/schema.js +40 -0
- package/dist/errors/constructors.d.ts +25 -0
- package/dist/errors/constructors.js +86 -0
- package/dist/errors/types.d.ts +7 -0
- package/dist/errors/types.js +16 -0
- package/dist/indexing/walker.d.ts +11 -0
- package/dist/indexing/walker.js +11 -6
- package/dist/plan/execute.d.ts +8 -0
- package/dist/plan/execute.js +36 -22
- package/dist/plan/service.d.ts +2 -1
- package/dist/plan/service.js +7 -3
- package/dist/plan/submit-plan.d.ts +4 -4
- package/dist/render/capabilities.d.ts +12 -0
- package/dist/render/capabilities.js +27 -0
- package/dist/render/diff.d.ts +19 -0
- package/dist/render/diff.js +107 -0
- package/dist/render/highlight.d.ts +47 -0
- package/dist/render/highlight.js +265 -0
- package/dist/render/index.d.ts +15 -0
- package/dist/render/index.js +21 -0
- package/dist/render/plain-renderer.d.ts +38 -0
- package/dist/render/plain-renderer.js +87 -0
- package/dist/render/state.d.ts +31 -0
- package/dist/render/state.js +83 -0
- package/dist/render/tty-renderer.d.ts +83 -0
- package/dist/render/tty-renderer.js +276 -0
- package/dist/render/types.d.ts +160 -0
- package/dist/render/types.js +1 -0
- package/dist/subagent/budget.d.ts +34 -0
- package/dist/subagent/budget.js +57 -0
- package/dist/subagent/index.d.ts +5 -0
- package/dist/subagent/index.js +5 -0
- package/dist/subagent/orchestrator.d.ts +67 -0
- package/dist/subagent/orchestrator.js +241 -0
- package/dist/subagent/registry-scope.d.ts +28 -0
- package/dist/subagent/registry-scope.js +63 -0
- package/dist/subagent/spawn-tool.d.ts +29 -0
- package/dist/subagent/spawn-tool.js +94 -0
- package/dist/subagent/types.d.ts +55 -0
- package/dist/subagent/types.js +1 -0
- package/dist/tools/types.d.ts +20 -2
- package/package.json +1 -1
package/dist/cli/commands/run.js
CHANGED
|
@@ -3,8 +3,9 @@ import pc from "picocolors";
|
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
4
|
import { loadConfig, resolveApiKey } from "../../config/index.js";
|
|
5
5
|
import { authMissingKey, usageError } from "../../errors/index.js";
|
|
6
|
+
import { createRenderer } from "../../render/index.js";
|
|
7
|
+
import { CheckpointService } from "../../checkpoint/index.js";
|
|
6
8
|
import { runInteractive } from "../repl.js";
|
|
7
|
-
import { createStreamPrinter } from "../stream-print.js";
|
|
8
9
|
import { buildAgentSession } from "../session-factory.js";
|
|
9
10
|
import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
|
|
10
11
|
export function runCommand() {
|
|
@@ -50,22 +51,35 @@ export function runCommand() {
|
|
|
50
51
|
}
|
|
51
52
|
// Plan mode is opt-in: --plan flag overrides the config default.
|
|
52
53
|
const planMode = opts.plan ?? config.agent.planMode;
|
|
53
|
-
|
|
54
|
+
// One renderer for the whole run (U.2): the streaming path and the
|
|
55
|
+
// approval prompt's status-suspend hook must share the same live region.
|
|
56
|
+
const renderer = createRenderer();
|
|
57
|
+
// Checkpoint-before-first-mutation (C.32): the service is latched per run
|
|
58
|
+
// and fires from the approval seam inside the session, so one instance
|
|
59
|
+
// covers the one-shot path, every REPL turn, and plan-mode execution.
|
|
60
|
+
const checkpoints = config.checkpoint.enabled
|
|
61
|
+
? new CheckpointService({ root: process.cwd(), config })
|
|
62
|
+
: undefined;
|
|
63
|
+
const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode, renderer, checkpoints);
|
|
54
64
|
if (interactive) {
|
|
55
|
-
await runInteractive(session);
|
|
65
|
+
await runInteractive(session, undefined, renderer, checkpoints);
|
|
56
66
|
return;
|
|
57
67
|
}
|
|
68
|
+
checkpoints?.beginRun(prompt);
|
|
58
69
|
// One-shot: a single turn, then exit. Preserves scripting/pipe use.
|
|
59
|
-
// Assistant text streams to stdout delta by delta (same as the REPL)
|
|
60
|
-
//
|
|
61
|
-
// loop terminates the line.
|
|
70
|
+
// Assistant text streams to stdout delta by delta (same as the REPL);
|
|
71
|
+
// piped output degrades to the plain renderer (no ANSI, chrome on stderr).
|
|
62
72
|
logger.print(`${pc.cyan("cruxy")} ${pc.dim("›")} ${prompt}\n`);
|
|
63
73
|
// Provider/network/auth failures propagate to the top-level boundary,
|
|
64
74
|
// which classifies them (e.g. CRUXY_E_GATEWAY_UNREACHABLE) and exits with
|
|
65
75
|
// the matching code — a one-shot run must fail non-zero on error.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
try {
|
|
77
|
+
const result = await session.send(prompt, renderer);
|
|
78
|
+
logger.debug(`agent finished: ${result.stop} after ${result.iterations} turn(s); ` +
|
|
79
|
+
`tokens in/out ${result.usage.input_tokens}/${result.usage.output_tokens}`);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
renderer.close();
|
|
83
|
+
}
|
|
70
84
|
});
|
|
71
85
|
}
|
package/dist/cli/onboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveApiKey } from "../config/index.js";
|
|
2
2
|
import { createDefaultDeps, defaultOnboardingIO, isFirstRun, runOnboarding, } from "../onboarding/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createRenderer } from "../render/index.js";
|
|
4
4
|
import { buildAgentSession } from "./session-factory.js";
|
|
5
5
|
/**
|
|
6
6
|
* CLI-layer glue between the entry points and the onboarding module (U.6). Keeps
|
|
@@ -23,9 +23,14 @@ export async function runFirstWinTask(config, cwd, prompt) {
|
|
|
23
23
|
const apiKey = resolveApiKey(config.model.provider);
|
|
24
24
|
if (!apiKey)
|
|
25
25
|
return; // defensive — the key was just persisted
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
26
|
+
const renderer = createRenderer();
|
|
27
|
+
const session = buildAgentSession(config, apiKey, cwd, true, false, renderer);
|
|
28
|
+
try {
|
|
29
|
+
await session.send(prompt, renderer);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
renderer.close();
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* Run the guided first-run flow **iff** this is a first run; otherwise return
|
package/dist/cli/program.js
CHANGED
|
@@ -10,6 +10,8 @@ import { skillsCommand } from "./commands/skills.js";
|
|
|
10
10
|
import { prCommand } from "./commands/pr.js";
|
|
11
11
|
import { loginCommand } from "./commands/login.js";
|
|
12
12
|
import { initCommand } from "./commands/init.js";
|
|
13
|
+
import { checkpointCommand } from "./commands/checkpoint.js";
|
|
14
|
+
import { rollbackCommand } from "./commands/rollback.js";
|
|
13
15
|
import { loadConfig } from "../config/index.js";
|
|
14
16
|
import { maybeRunOnboarding } from "./onboard.js";
|
|
15
17
|
export function buildProgram() {
|
|
@@ -36,6 +38,8 @@ export function buildProgram() {
|
|
|
36
38
|
program.addCommand(prCommand());
|
|
37
39
|
program.addCommand(loginCommand());
|
|
38
40
|
program.addCommand(initCommand());
|
|
41
|
+
program.addCommand(checkpointCommand());
|
|
42
|
+
program.addCommand(rollbackCommand());
|
|
39
43
|
// Default action: bare `cruxy` -> entrypoint. An unrecognized first operand
|
|
40
44
|
// means an unknown command (Commander runs the default action with it as an
|
|
41
45
|
// operand rather than erroring), so reject it as a usage error.
|
package/dist/cli/repl.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
import type { Session } from "../agent/index.js";
|
|
3
|
+
import type { CheckpointService } from "../checkpoint/index.js";
|
|
4
|
+
import { type StreamRenderer } from "../render/index.js";
|
|
3
5
|
/** The stdin/stdout pair the REPL reads from and prompts on. Injectable for tests. */
|
|
4
6
|
export interface ReplIO {
|
|
5
7
|
input: Readable;
|
|
@@ -7,9 +9,13 @@ export interface ReplIO {
|
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Drive an interactive multi-turn session: prompt, read a line, dispatch slash
|
|
10
|
-
* commands or run a turn, repeat. Assistant text
|
|
11
|
-
* `
|
|
12
|
+
* commands or run a turn, repeat. Assistant text and tool-call progress stream
|
|
13
|
+
* through the `renderer` from within `session.send`; this loop only owns input
|
|
14
|
+
* and control.
|
|
12
15
|
*
|
|
13
|
-
* `io` defaults to real stdin/stdout; tests inject a scripted stream pair.
|
|
16
|
+
* `io` defaults to real stdin/stdout; tests inject a scripted stream pair. The
|
|
17
|
+
* renderer defaults to whatever `io.output` supports (a TTY gets the managed
|
|
18
|
+
* live region, anything else the plain append-only renderer); `cruxy run`
|
|
19
|
+
* passes its own so the approval prompt's status-suspend hook shares it.
|
|
14
20
|
*/
|
|
15
|
-
export declare function runInteractive(session: Session, io?: ReplIO): Promise<void>;
|
|
21
|
+
export declare function runInteractive(session: Session, io?: ReplIO, renderer?: StreamRenderer, checkpoints?: CheckpointService): Promise<void>;
|
package/dist/cli/repl.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { formatError, fromUnknown, isVerbose, shouldUseColor, } from "../errors/index.js";
|
|
4
|
+
import { createRenderer } from "../render/index.js";
|
|
4
5
|
import { logger } from "../utils/logger.js";
|
|
5
|
-
import { createStreamPrinter } from "./stream-print.js";
|
|
6
6
|
const PROMPT = `${pc.cyan("cruxy")} ${pc.dim("›")} `;
|
|
7
7
|
const HELP = `Commands:
|
|
8
8
|
/help show this help
|
|
@@ -65,13 +65,25 @@ function printReplError(err) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Drive an interactive multi-turn session: prompt, read a line, dispatch slash
|
|
68
|
-
* commands or run a turn, repeat. Assistant text
|
|
69
|
-
* `
|
|
68
|
+
* commands or run a turn, repeat. Assistant text and tool-call progress stream
|
|
69
|
+
* through the `renderer` from within `session.send`; this loop only owns input
|
|
70
|
+
* and control.
|
|
70
71
|
*
|
|
71
|
-
* `io` defaults to real stdin/stdout; tests inject a scripted stream pair.
|
|
72
|
+
* `io` defaults to real stdin/stdout; tests inject a scripted stream pair. The
|
|
73
|
+
* renderer defaults to whatever `io.output` supports (a TTY gets the managed
|
|
74
|
+
* live region, anything else the plain append-only renderer); `cruxy run`
|
|
75
|
+
* passes its own so the approval prompt's status-suspend hook shares it.
|
|
72
76
|
*/
|
|
73
|
-
export async function runInteractive(session, io = defaultIO()) {
|
|
77
|
+
export async function runInteractive(session, io = defaultIO(), renderer = createRenderer(io.output, process.stderr), checkpoints) {
|
|
74
78
|
logger.print(pc.dim("interactive session — /help for commands, /exit or Ctrl+D to quit"));
|
|
79
|
+
try {
|
|
80
|
+
await replLoop(session, io, renderer, checkpoints);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
renderer.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function replLoop(session, io, renderer, checkpoints) {
|
|
75
87
|
for (;;) {
|
|
76
88
|
const line = await readLine(io, PROMPT);
|
|
77
89
|
// EOF / Ctrl+D.
|
|
@@ -120,14 +132,16 @@ export async function runInteractive(session, io = defaultIO()) {
|
|
|
120
132
|
logger.print(HELP);
|
|
121
133
|
continue;
|
|
122
134
|
}
|
|
123
|
-
// A real turn. Assistant text streams
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
135
|
+
// A real turn. Assistant text streams through the renderer delta by delta
|
|
136
|
+
// (leading blank lines trimmed, code fences highlighted); the agent loop
|
|
137
|
+
// closes each segment with one newline, so the next prompt lands on its own
|
|
138
|
+
// line. Errors (provider/API failures) log and return to the prompt rather
|
|
139
|
+
// than killing the REPL.
|
|
128
140
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
// Each REPL turn is its own undo unit (C.32): a fresh checkpoint latch,
|
|
142
|
+
// so `cruxy rollback` reverts exactly one turn's mutations.
|
|
143
|
+
checkpoints?.beginRun(trimmed);
|
|
144
|
+
await session.send(line, renderer);
|
|
131
145
|
}
|
|
132
146
|
catch (err) {
|
|
133
147
|
logger.error(err.message);
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { CruxyConfig } from "../config/index.js";
|
|
2
|
+
import type { ApprovalDecision } from "../approval/index.js";
|
|
3
|
+
import type { CheckpointService } from "../checkpoint/index.js";
|
|
4
|
+
import type { StreamRenderer } from "../render/index.js";
|
|
5
|
+
import { type ApproveAction } from "../tools/index.js";
|
|
2
6
|
import { Session } from "../agent/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
|
|
9
|
+
* whole point: a tool mutates only *after* `requestApproval` resolves, so
|
|
10
|
+
* snapshotting after an `allow` decision but before returning it means the
|
|
11
|
+
* checkpoint always lands before the run's first mutation — and a denied
|
|
12
|
+
* action never creates one. The same seam records which paths the run touched
|
|
13
|
+
* (file actions) or that attribution is lost (shell), for rollback's
|
|
14
|
+
* external-change detection.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withCheckpointGate(requestApproval: (action: ApproveAction) => Promise<ApprovalDecision>, checkpoints: CheckpointService | undefined, cwd: string): (action: ApproveAction) => Promise<ApprovalDecision>;
|
|
3
17
|
/**
|
|
4
18
|
* Build a ready-to-run agent {@link Session} from a resolved key — the wiring
|
|
5
19
|
* shared by `cruxy run` and the onboarding first-win task (so they can't drift).
|
|
@@ -9,4 +23,4 @@ import { Session } from "../agent/index.js";
|
|
|
9
23
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
10
24
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
11
25
|
*/
|
|
12
|
-
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean): Session;
|
|
26
|
+
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService): Session;
|
|
@@ -2,11 +2,78 @@ import { createProvider } from "@cruxy/sdk";
|
|
|
2
2
|
import { loadProjectInstructions } from "../config/index.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { getGitInfo } from "../utils/git.js";
|
|
5
|
-
import { ApprovalService, InteractivePolicy, SessionAllowlist, defaultPromptIO, } from "../approval/index.js";
|
|
5
|
+
import { ApprovalService, InteractivePolicy, SessionAllowlist, classify, defaultPromptIO, } from "../approval/index.js";
|
|
6
6
|
import { shouldUseColor } from "../errors/index.js";
|
|
7
7
|
import { buildDefaultRegistry } from "../tools/index.js";
|
|
8
8
|
import { Session } from "../agent/index.js";
|
|
9
9
|
import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
|
|
10
|
+
import { SubagentOrchestrator, makeSpawnSubagentTool, } from "../subagent/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a PromptIO so the live region yields before any prompt text lands
|
|
13
|
+
* (U.2/U.4): the prompt writes to stderr while the status line owns the last
|
|
14
|
+
* stdout row of the same terminal. Entering the `awaiting-approval` phase
|
|
15
|
+
* hides the live line (the prompt IS the visible state) while keeping the
|
|
16
|
+
* step-progress register intact, so the line comes back with full context on
|
|
17
|
+
* the next transition after the user decides.
|
|
18
|
+
*/
|
|
19
|
+
function suspendStatusOnPrompt(io, renderer) {
|
|
20
|
+
if (!renderer)
|
|
21
|
+
return io;
|
|
22
|
+
return {
|
|
23
|
+
...io,
|
|
24
|
+
write: (text) => {
|
|
25
|
+
renderer.setPhase({ kind: "awaiting-approval" });
|
|
26
|
+
io.write(text);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Restore the live line once an approval request fully settles (U.4). The
|
|
32
|
+
* settle point must be the service call, not the prompt's key read: the
|
|
33
|
+
* prompt writes a trailing newline AFTER the read, which re-enters
|
|
34
|
+
* `awaiting-approval` — resolving here is the first moment no more prompt
|
|
35
|
+
* bytes can follow. Fires on every decision (prompted or not); the renderer
|
|
36
|
+
* treats it as a no-op unless a prompt actually displaced the line.
|
|
37
|
+
*/
|
|
38
|
+
function resumeLineAfterApproval(requestApproval, renderer) {
|
|
39
|
+
if (!renderer)
|
|
40
|
+
return requestApproval;
|
|
41
|
+
return async (action) => {
|
|
42
|
+
try {
|
|
43
|
+
return await requestApproval(action);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
renderer.promptResolved();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
|
|
52
|
+
* whole point: a tool mutates only *after* `requestApproval` resolves, so
|
|
53
|
+
* snapshotting after an `allow` decision but before returning it means the
|
|
54
|
+
* checkpoint always lands before the run's first mutation — and a denied
|
|
55
|
+
* action never creates one. The same seam records which paths the run touched
|
|
56
|
+
* (file actions) or that attribution is lost (shell), for rollback's
|
|
57
|
+
* external-change detection.
|
|
58
|
+
*/
|
|
59
|
+
export function withCheckpointGate(requestApproval, checkpoints, cwd) {
|
|
60
|
+
if (!checkpoints)
|
|
61
|
+
return requestApproval;
|
|
62
|
+
return async (action) => {
|
|
63
|
+
const decision = await requestApproval(action);
|
|
64
|
+
if (!decision.allow)
|
|
65
|
+
return decision;
|
|
66
|
+
const request = classify(action, cwd);
|
|
67
|
+
if (request.tier === "read")
|
|
68
|
+
return decision;
|
|
69
|
+
await checkpoints.ensureCheckpoint();
|
|
70
|
+
if (action.kind === "shell")
|
|
71
|
+
await checkpoints.recordShellMutation();
|
|
72
|
+
else
|
|
73
|
+
await checkpoints.recordTouched([...request.targets]);
|
|
74
|
+
return decision;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
10
77
|
/**
|
|
11
78
|
* Build a ready-to-run agent {@link Session} from a resolved key — the wiring
|
|
12
79
|
* shared by `cruxy run` and the onboarding first-win task (so they can't drift).
|
|
@@ -16,7 +83,7 @@ import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
|
|
|
16
83
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
17
84
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
18
85
|
*/
|
|
19
|
-
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false) {
|
|
86
|
+
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints) {
|
|
20
87
|
const provider = createProvider({
|
|
21
88
|
provider: config.model.provider,
|
|
22
89
|
apiKey,
|
|
@@ -28,10 +95,35 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
28
95
|
const execRegistry = buildDefaultRegistry();
|
|
29
96
|
const git = getGitInfo(cwd);
|
|
30
97
|
const projectInstructions = loadProjectInstructions(cwd);
|
|
98
|
+
// One io shared by every prompt in the session (plan approval, the U.3 gate,
|
|
99
|
+
// and any gate inside a subagent), so they all coordinate with the same live
|
|
100
|
+
// region. The full wrapper stack around an ApprovalService is factored here
|
|
101
|
+
// because subagents must get the *identical* stack over a FRESH service: same
|
|
102
|
+
// prompt + same checkpoint hook, but a new (empty) session allowlist — a
|
|
103
|
+
// grant in the parent never silently widens a child's authority.
|
|
104
|
+
const io = suspendStatusOnPrompt(defaultPromptIO(shouldUseColor()), renderer);
|
|
105
|
+
const gate = (approval) => withCheckpointGate(resumeLineAfterApproval((action) => approval.requestApproval(action), renderer), checkpoints, cwd);
|
|
106
|
+
// Subagent orchestration (C.14): spawn_subagent goes on the main registry
|
|
107
|
+
// only when depth allows (maxDepth 0 disables the feature structurally).
|
|
108
|
+
// Registered before the plan wiring so plan-mode execution steps can
|
|
109
|
+
// dispatch subagents too; the propose phase filters it out (read-only).
|
|
110
|
+
const orchestrator = new SubagentOrchestrator({
|
|
111
|
+
provider,
|
|
112
|
+
config,
|
|
113
|
+
parentRegistry: execRegistry,
|
|
114
|
+
cwd,
|
|
115
|
+
logger,
|
|
116
|
+
git,
|
|
117
|
+
projectInstructions,
|
|
118
|
+
renderer,
|
|
119
|
+
makeChildApproval: () => gate(new ApprovalService({ cwd, interactive: ttyInteractive, io })),
|
|
120
|
+
});
|
|
121
|
+
if (config.subagent.maxDepth > 0) {
|
|
122
|
+
execRegistry.register(makeSpawnSubagentTool(orchestrator, 0));
|
|
123
|
+
}
|
|
31
124
|
if (planMode) {
|
|
32
|
-
// One
|
|
125
|
+
// One allowlist shared by the plan-approval prompt and the per-action
|
|
33
126
|
// gate, so a grant recorded during execution is honored by U.3's own check.
|
|
34
|
-
const io = defaultPromptIO(shouldUseColor());
|
|
35
127
|
const allowlist = new SessionAllowlist();
|
|
36
128
|
const planPolicy = new PlanExecutionPolicy(allowlist, new InteractivePolicy(allowlist, io));
|
|
37
129
|
const approval = new ApprovalService({
|
|
@@ -40,13 +132,8 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
40
132
|
policy: planPolicy,
|
|
41
133
|
io,
|
|
42
134
|
});
|
|
43
|
-
const ctx = {
|
|
44
|
-
|
|
45
|
-
config,
|
|
46
|
-
logger,
|
|
47
|
-
requestApproval: (action) => approval.requestApproval(action),
|
|
48
|
-
};
|
|
49
|
-
const planRunner = ({ messages, projectInstructions, onText, }) => runPlanSession({
|
|
135
|
+
const ctx = { cwd, config, logger, requestApproval: gate(approval) };
|
|
136
|
+
const planRunner = ({ messages, projectInstructions, renderer: turnRenderer, }) => runPlanSession({
|
|
50
137
|
provider,
|
|
51
138
|
config,
|
|
52
139
|
ctx,
|
|
@@ -57,7 +144,7 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
57
144
|
messages,
|
|
58
145
|
git,
|
|
59
146
|
projectInstructions,
|
|
60
|
-
|
|
147
|
+
renderer: turnRenderer,
|
|
61
148
|
});
|
|
62
149
|
return new Session({
|
|
63
150
|
provider,
|
|
@@ -70,13 +157,12 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
70
157
|
planRunner,
|
|
71
158
|
});
|
|
72
159
|
}
|
|
73
|
-
const approval = new ApprovalService({
|
|
74
|
-
const ctx = {
|
|
160
|
+
const approval = new ApprovalService({
|
|
75
161
|
cwd,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
162
|
+
interactive: ttyInteractive,
|
|
163
|
+
io,
|
|
164
|
+
});
|
|
165
|
+
const ctx = { cwd, config, logger, requestApproval: gate(approval) };
|
|
80
166
|
return new Session({
|
|
81
167
|
provider,
|
|
82
168
|
registry: execRegistry,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -203,6 +203,65 @@ export declare const IndexConfigSchema: z.ZodObject<{
|
|
|
203
203
|
overlapLines?: number | undefined;
|
|
204
204
|
} | undefined;
|
|
205
205
|
}>;
|
|
206
|
+
/**
|
|
207
|
+
* Working-tree checkpoints (C.32): a snapshot taken before an agent run's first
|
|
208
|
+
* file mutation, so `cruxy rollback` can undo the whole run atomically.
|
|
209
|
+
*/
|
|
210
|
+
export declare const CheckpointConfigSchema: z.ZodObject<{
|
|
211
|
+
/** Auto-checkpoint before a run's first mutation (and enable `cruxy rollback`). */
|
|
212
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
213
|
+
/** How many checkpoints to keep; older ones are pruned oldest-first. */
|
|
214
|
+
retention: z.ZodDefault<z.ZodNumber>;
|
|
215
|
+
}, "strict", z.ZodTypeAny, {
|
|
216
|
+
enabled: boolean;
|
|
217
|
+
retention: number;
|
|
218
|
+
}, {
|
|
219
|
+
enabled?: boolean | undefined;
|
|
220
|
+
retention?: number | undefined;
|
|
221
|
+
}>;
|
|
222
|
+
/**
|
|
223
|
+
* Subagent orchestration (C.14): scoped child agents the main agent can spawn
|
|
224
|
+
* for bounded subtasks. Every cap here is a hard bound — a subagent can narrow
|
|
225
|
+
* its budget at spawn time but never exceed these ceilings.
|
|
226
|
+
*/
|
|
227
|
+
export declare const SubagentConfigSchema: z.ZodObject<{
|
|
228
|
+
/**
|
|
229
|
+
* Maximum subagent nesting depth. The main agent is depth 0; the default of
|
|
230
|
+
* 1 lets it spawn subagents that cannot themselves spawn (no fork bombs).
|
|
231
|
+
*/
|
|
232
|
+
maxDepth: z.ZodDefault<z.ZodNumber>;
|
|
233
|
+
/** Per-subagent budget ceilings; spawn-time overrides are clamped to these. */
|
|
234
|
+
defaultBudget: z.ZodDefault<z.ZodObject<{
|
|
235
|
+
/** Hard cap on the subagent's model turns. */
|
|
236
|
+
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
237
|
+
/** Hard cap on the subagent's combined input+output tokens. */
|
|
238
|
+
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
239
|
+
/** Optional wall-clock cap; unset means no time limit. */
|
|
240
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
241
|
+
}, "strict", z.ZodTypeAny, {
|
|
242
|
+
maxTokens: number;
|
|
243
|
+
maxIterations: number;
|
|
244
|
+
timeoutMs?: number | undefined;
|
|
245
|
+
}, {
|
|
246
|
+
maxTokens?: number | undefined;
|
|
247
|
+
maxIterations?: number | undefined;
|
|
248
|
+
timeoutMs?: number | undefined;
|
|
249
|
+
}>>;
|
|
250
|
+
}, "strict", z.ZodTypeAny, {
|
|
251
|
+
maxDepth: number;
|
|
252
|
+
defaultBudget: {
|
|
253
|
+
maxTokens: number;
|
|
254
|
+
maxIterations: number;
|
|
255
|
+
timeoutMs?: number | undefined;
|
|
256
|
+
};
|
|
257
|
+
}, {
|
|
258
|
+
maxDepth?: number | undefined;
|
|
259
|
+
defaultBudget?: {
|
|
260
|
+
maxTokens?: number | undefined;
|
|
261
|
+
maxIterations?: number | undefined;
|
|
262
|
+
timeoutMs?: number | undefined;
|
|
263
|
+
} | undefined;
|
|
264
|
+
}>;
|
|
206
265
|
/** MCP server entry — stdio or URL transport (wired up in a later phase). */
|
|
207
266
|
export declare const McpServerSchema: z.ZodObject<{
|
|
208
267
|
command: z.ZodOptional<z.ZodString>;
|
|
@@ -405,6 +464,56 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
405
464
|
overlapLines?: number | undefined;
|
|
406
465
|
} | undefined;
|
|
407
466
|
}>>;
|
|
467
|
+
checkpoint: z.ZodDefault<z.ZodObject<{
|
|
468
|
+
/** Auto-checkpoint before a run's first mutation (and enable `cruxy rollback`). */
|
|
469
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
470
|
+
/** How many checkpoints to keep; older ones are pruned oldest-first. */
|
|
471
|
+
retention: z.ZodDefault<z.ZodNumber>;
|
|
472
|
+
}, "strict", z.ZodTypeAny, {
|
|
473
|
+
enabled: boolean;
|
|
474
|
+
retention: number;
|
|
475
|
+
}, {
|
|
476
|
+
enabled?: boolean | undefined;
|
|
477
|
+
retention?: number | undefined;
|
|
478
|
+
}>>;
|
|
479
|
+
subagent: z.ZodDefault<z.ZodObject<{
|
|
480
|
+
/**
|
|
481
|
+
* Maximum subagent nesting depth. The main agent is depth 0; the default of
|
|
482
|
+
* 1 lets it spawn subagents that cannot themselves spawn (no fork bombs).
|
|
483
|
+
*/
|
|
484
|
+
maxDepth: z.ZodDefault<z.ZodNumber>;
|
|
485
|
+
/** Per-subagent budget ceilings; spawn-time overrides are clamped to these. */
|
|
486
|
+
defaultBudget: z.ZodDefault<z.ZodObject<{
|
|
487
|
+
/** Hard cap on the subagent's model turns. */
|
|
488
|
+
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
489
|
+
/** Hard cap on the subagent's combined input+output tokens. */
|
|
490
|
+
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
491
|
+
/** Optional wall-clock cap; unset means no time limit. */
|
|
492
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
493
|
+
}, "strict", z.ZodTypeAny, {
|
|
494
|
+
maxTokens: number;
|
|
495
|
+
maxIterations: number;
|
|
496
|
+
timeoutMs?: number | undefined;
|
|
497
|
+
}, {
|
|
498
|
+
maxTokens?: number | undefined;
|
|
499
|
+
maxIterations?: number | undefined;
|
|
500
|
+
timeoutMs?: number | undefined;
|
|
501
|
+
}>>;
|
|
502
|
+
}, "strict", z.ZodTypeAny, {
|
|
503
|
+
maxDepth: number;
|
|
504
|
+
defaultBudget: {
|
|
505
|
+
maxTokens: number;
|
|
506
|
+
maxIterations: number;
|
|
507
|
+
timeoutMs?: number | undefined;
|
|
508
|
+
};
|
|
509
|
+
}, {
|
|
510
|
+
maxDepth?: number | undefined;
|
|
511
|
+
defaultBudget?: {
|
|
512
|
+
maxTokens?: number | undefined;
|
|
513
|
+
maxIterations?: number | undefined;
|
|
514
|
+
timeoutMs?: number | undefined;
|
|
515
|
+
} | undefined;
|
|
516
|
+
}>>;
|
|
408
517
|
mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
409
518
|
command: z.ZodOptional<z.ZodString>;
|
|
410
519
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -471,6 +580,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
471
580
|
overlapLines: number;
|
|
472
581
|
};
|
|
473
582
|
};
|
|
583
|
+
checkpoint: {
|
|
584
|
+
enabled: boolean;
|
|
585
|
+
retention: number;
|
|
586
|
+
};
|
|
587
|
+
subagent: {
|
|
588
|
+
maxDepth: number;
|
|
589
|
+
defaultBudget: {
|
|
590
|
+
maxTokens: number;
|
|
591
|
+
maxIterations: number;
|
|
592
|
+
timeoutMs?: number | undefined;
|
|
593
|
+
};
|
|
594
|
+
};
|
|
474
595
|
mcpServers: Record<string, {
|
|
475
596
|
command?: string | undefined;
|
|
476
597
|
args?: string[] | undefined;
|
|
@@ -529,6 +650,18 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
529
650
|
overlapLines?: number | undefined;
|
|
530
651
|
} | undefined;
|
|
531
652
|
} | undefined;
|
|
653
|
+
checkpoint?: {
|
|
654
|
+
enabled?: boolean | undefined;
|
|
655
|
+
retention?: number | undefined;
|
|
656
|
+
} | undefined;
|
|
657
|
+
subagent?: {
|
|
658
|
+
maxDepth?: number | undefined;
|
|
659
|
+
defaultBudget?: {
|
|
660
|
+
maxTokens?: number | undefined;
|
|
661
|
+
maxIterations?: number | undefined;
|
|
662
|
+
timeoutMs?: number | undefined;
|
|
663
|
+
} | undefined;
|
|
664
|
+
} | undefined;
|
|
532
665
|
mcpServers?: Record<string, {
|
|
533
666
|
command?: string | undefined;
|
|
534
667
|
args?: string[] | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -137,6 +137,44 @@ export const IndexConfigSchema = z
|
|
|
137
137
|
.default({}),
|
|
138
138
|
})
|
|
139
139
|
.strict();
|
|
140
|
+
/**
|
|
141
|
+
* Working-tree checkpoints (C.32): a snapshot taken before an agent run's first
|
|
142
|
+
* file mutation, so `cruxy rollback` can undo the whole run atomically.
|
|
143
|
+
*/
|
|
144
|
+
export const CheckpointConfigSchema = z
|
|
145
|
+
.object({
|
|
146
|
+
/** Auto-checkpoint before a run's first mutation (and enable `cruxy rollback`). */
|
|
147
|
+
enabled: z.boolean().default(true),
|
|
148
|
+
/** How many checkpoints to keep; older ones are pruned oldest-first. */
|
|
149
|
+
retention: z.number().int().positive().default(10),
|
|
150
|
+
})
|
|
151
|
+
.strict();
|
|
152
|
+
/**
|
|
153
|
+
* Subagent orchestration (C.14): scoped child agents the main agent can spawn
|
|
154
|
+
* for bounded subtasks. Every cap here is a hard bound — a subagent can narrow
|
|
155
|
+
* its budget at spawn time but never exceed these ceilings.
|
|
156
|
+
*/
|
|
157
|
+
export const SubagentConfigSchema = z
|
|
158
|
+
.object({
|
|
159
|
+
/**
|
|
160
|
+
* Maximum subagent nesting depth. The main agent is depth 0; the default of
|
|
161
|
+
* 1 lets it spawn subagents that cannot themselves spawn (no fork bombs).
|
|
162
|
+
*/
|
|
163
|
+
maxDepth: z.number().int().nonnegative().default(1),
|
|
164
|
+
/** Per-subagent budget ceilings; spawn-time overrides are clamped to these. */
|
|
165
|
+
defaultBudget: z
|
|
166
|
+
.object({
|
|
167
|
+
/** Hard cap on the subagent's model turns. */
|
|
168
|
+
maxIterations: z.number().int().positive().default(10),
|
|
169
|
+
/** Hard cap on the subagent's combined input+output tokens. */
|
|
170
|
+
maxTokens: z.number().int().positive().default(32000),
|
|
171
|
+
/** Optional wall-clock cap; unset means no time limit. */
|
|
172
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
173
|
+
})
|
|
174
|
+
.strict()
|
|
175
|
+
.default({}),
|
|
176
|
+
})
|
|
177
|
+
.strict();
|
|
140
178
|
/** MCP server entry — stdio or URL transport (wired up in a later phase). */
|
|
141
179
|
export const McpServerSchema = z
|
|
142
180
|
.object({
|
|
@@ -156,6 +194,8 @@ export const CruxyConfigSchema = z
|
|
|
156
194
|
context: ContextConfigSchema.default({}),
|
|
157
195
|
approval: ApprovalConfigSchema.default({}),
|
|
158
196
|
index: IndexConfigSchema.default({}),
|
|
197
|
+
checkpoint: CheckpointConfigSchema.default({}),
|
|
198
|
+
subagent: SubagentConfigSchema.default({}),
|
|
159
199
|
mcpServers: z.record(z.string(), McpServerSchema).default({}),
|
|
160
200
|
logLevel: z.enum(LOG_LEVELS).default("info"),
|
|
161
201
|
})
|
|
@@ -53,6 +53,31 @@ export declare function planRevisionLimit(limit: number): CruxyError;
|
|
|
53
53
|
* tell it apart from a per-action approval requirement.
|
|
54
54
|
*/
|
|
55
55
|
export declare function planApprovalRequired(): CruxyError;
|
|
56
|
+
/**
|
|
57
|
+
* Creating, reading, or restoring a working-tree checkpoint failed (C.32).
|
|
58
|
+
* Fail-loud by design: an agent run never mutates files without its undo
|
|
59
|
+
* protection unless the user explicitly disables it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function checkpointFailed(reason: string, underlying?: unknown): CruxyError;
|
|
62
|
+
/** The requested checkpoint id doesn't exist (or no checkpoints exist at all). */
|
|
63
|
+
export declare function checkpointNotFound(id?: string): CruxyError;
|
|
64
|
+
/**
|
|
65
|
+
* Rollback needs interactive approval but cruxy is running non-interactively.
|
|
66
|
+
* Restoring is destructive and deliberate — there is no auto-rollback path, ever.
|
|
67
|
+
*/
|
|
68
|
+
export declare function rollbackApprovalRequired(): CruxyError;
|
|
69
|
+
/**
|
|
70
|
+
* A subagent spawn was attempted past the configured nesting cap (C.14). The
|
|
71
|
+
* spawn tool is structurally withheld at the cap, so reaching this means the
|
|
72
|
+
* orchestrator seam was driven directly — fail loud, never spawn.
|
|
73
|
+
*/
|
|
74
|
+
export declare function subagentDepthExceeded(depth: number, maxDepth: number): CruxyError;
|
|
75
|
+
/**
|
|
76
|
+
* A subagent run failed outright (provider error, tool crash) before producing
|
|
77
|
+
* a result. Normally folded into the structured `SubagentResult` the parent
|
|
78
|
+
* reasons over; thrown only when the orchestrator itself cannot proceed.
|
|
79
|
+
*/
|
|
80
|
+
export declare function subagentFailed(underlying?: unknown): CruxyError;
|
|
56
81
|
export declare function internal(underlying?: unknown): CruxyError;
|
|
57
82
|
/**
|
|
58
83
|
* Map a known provider/transport error (from `@cruxy/sdk`) to a typed
|