@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.
Files changed (78) hide show
  1. package/README.md +46 -13
  2. package/dist/agent/loop.d.ts +35 -6
  3. package/dist/agent/loop.js +84 -10
  4. package/dist/agent/prompts.d.ts +2 -0
  5. package/dist/agent/prompts.js +8 -0
  6. package/dist/agent/session.d.ts +6 -4
  7. package/dist/agent/session.js +6 -5
  8. package/dist/approval/classify.js +26 -0
  9. package/dist/approval/prompt.d.ts +9 -0
  10. package/dist/approval/prompt.js +2 -77
  11. package/dist/checkpoint/capture.d.ts +17 -0
  12. package/dist/checkpoint/capture.js +73 -0
  13. package/dist/checkpoint/git-store.d.ts +61 -0
  14. package/dist/checkpoint/git-store.js +171 -0
  15. package/dist/checkpoint/index.d.ts +6 -0
  16. package/dist/checkpoint/index.js +6 -0
  17. package/dist/checkpoint/restore.d.ts +23 -0
  18. package/dist/checkpoint/restore.js +195 -0
  19. package/dist/checkpoint/service.d.ts +80 -0
  20. package/dist/checkpoint/service.js +276 -0
  21. package/dist/checkpoint/shadow-store.d.ts +23 -0
  22. package/dist/checkpoint/shadow-store.js +93 -0
  23. package/dist/checkpoint/types.d.ts +117 -0
  24. package/dist/checkpoint/types.js +18 -0
  25. package/dist/cli/commands/checkpoint.d.ts +7 -0
  26. package/dist/cli/commands/checkpoint.js +31 -0
  27. package/dist/cli/commands/rollback.d.ts +10 -0
  28. package/dist/cli/commands/rollback.js +51 -0
  29. package/dist/cli/commands/run.js +24 -10
  30. package/dist/cli/onboard.js +9 -4
  31. package/dist/cli/program.js +4 -0
  32. package/dist/cli/repl.d.ts +10 -4
  33. package/dist/cli/repl.js +26 -12
  34. package/dist/cli/session-factory.d.ts +15 -1
  35. package/dist/cli/session-factory.js +104 -18
  36. package/dist/config/schema.d.ts +133 -0
  37. package/dist/config/schema.js +40 -0
  38. package/dist/errors/constructors.d.ts +25 -0
  39. package/dist/errors/constructors.js +86 -0
  40. package/dist/errors/types.d.ts +7 -0
  41. package/dist/errors/types.js +16 -0
  42. package/dist/indexing/walker.d.ts +11 -0
  43. package/dist/indexing/walker.js +11 -6
  44. package/dist/plan/execute.d.ts +8 -0
  45. package/dist/plan/execute.js +36 -22
  46. package/dist/plan/service.d.ts +2 -1
  47. package/dist/plan/service.js +7 -3
  48. package/dist/plan/submit-plan.d.ts +4 -4
  49. package/dist/render/capabilities.d.ts +12 -0
  50. package/dist/render/capabilities.js +27 -0
  51. package/dist/render/diff.d.ts +19 -0
  52. package/dist/render/diff.js +107 -0
  53. package/dist/render/highlight.d.ts +47 -0
  54. package/dist/render/highlight.js +265 -0
  55. package/dist/render/index.d.ts +15 -0
  56. package/dist/render/index.js +21 -0
  57. package/dist/render/plain-renderer.d.ts +38 -0
  58. package/dist/render/plain-renderer.js +87 -0
  59. package/dist/render/state.d.ts +31 -0
  60. package/dist/render/state.js +83 -0
  61. package/dist/render/tty-renderer.d.ts +83 -0
  62. package/dist/render/tty-renderer.js +276 -0
  63. package/dist/render/types.d.ts +160 -0
  64. package/dist/render/types.js +1 -0
  65. package/dist/subagent/budget.d.ts +34 -0
  66. package/dist/subagent/budget.js +57 -0
  67. package/dist/subagent/index.d.ts +5 -0
  68. package/dist/subagent/index.js +5 -0
  69. package/dist/subagent/orchestrator.d.ts +67 -0
  70. package/dist/subagent/orchestrator.js +241 -0
  71. package/dist/subagent/registry-scope.d.ts +28 -0
  72. package/dist/subagent/registry-scope.js +63 -0
  73. package/dist/subagent/spawn-tool.d.ts +29 -0
  74. package/dist/subagent/spawn-tool.js +94 -0
  75. package/dist/subagent/types.d.ts +55 -0
  76. package/dist/subagent/types.js +1 -0
  77. package/dist/tools/types.d.ts +20 -2
  78. package/package.json +1 -1
@@ -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
- const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode);
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
- // through a printer that trims the model's leading blank lines; the agent
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
- const print = createStreamPrinter((text) => process.stdout.write(text));
67
- const result = await session.send(prompt, print);
68
- logger.debug(`agent finished: ${result.stop} after ${result.iterations} turn(s); ` +
69
- `tokens in/out ${result.usage.input_tokens}/${result.usage.output_tokens}`);
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
  }
@@ -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 { createStreamPrinter } from "./stream-print.js";
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 session = buildAgentSession(config, apiKey, cwd, true);
27
- const print = createStreamPrinter((text) => process.stdout.write(text));
28
- await session.send(prompt, print);
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
@@ -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.
@@ -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 streams to stdout from within
11
- * `session.send` (via the logger); this loop only owns input and control.
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 streams to stdout from within
69
- * `session.send` (via the logger); this loop only owns input and control.
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 to the output delta by delta through a
124
- // single printer (which trims the model's leading blank lines); the agent
125
- // loop closes the segment with one newline, so the next prompt lands on its
126
- // own line. Errors (provider/API failures) log and return to the prompt
127
- // rather than killing the REPL.
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
- const print = createStreamPrinter((text) => io.output.write(text));
130
- await session.send(line, print);
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 io + allowlist shared by the plan-approval prompt and the per-action
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
- cwd,
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
- onText,
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({ cwd, interactive: ttyInteractive });
74
- const ctx = {
160
+ const approval = new ApprovalService({
75
161
  cwd,
76
- config,
77
- logger,
78
- requestApproval: (action) => approval.requestApproval(action),
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,
@@ -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;
@@ -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