@cruxy/cli 0.8.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 +40 -13
- package/dist/agent/loop.d.ts +28 -1
- package/dist/agent/loop.js +36 -4
- package/dist/agent/prompts.d.ts +2 -0
- package/dist/agent/prompts.js +8 -0
- package/dist/approval/classify.js +26 -0
- 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 +10 -2
- package/dist/cli/program.js +4 -0
- package/dist/cli/repl.d.ts +2 -1
- package/dist/cli/repl.js +6 -3
- package/dist/cli/session-factory.d.ts +14 -1
- package/dist/cli/session-factory.js +87 -22
- 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.js +5 -1
- package/dist/plan/submit-plan.d.ts +4 -4
- package/dist/render/diff.js +27 -0
- package/dist/render/index.d.ts +2 -1
- package/dist/render/index.js +1 -0
- package/dist/render/plain-renderer.d.ts +7 -1
- package/dist/render/plain-renderer.js +26 -0
- package/dist/render/state.d.ts +31 -0
- package/dist/render/state.js +83 -0
- package/dist/render/tty-renderer.d.ts +41 -5
- package/dist/render/tty-renderer.js +150 -23
- package/dist/render/types.d.ts +85 -1
- 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/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
|
|
@@ -335,6 +335,92 @@ export function planApprovalRequired() {
|
|
|
335
335
|
],
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
|
+
// ── checkpoint + rollback (exit 7 / 2 / 10) ───────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* Creating, reading, or restoring a working-tree checkpoint failed (C.32).
|
|
341
|
+
* Fail-loud by design: an agent run never mutates files without its undo
|
|
342
|
+
* protection unless the user explicitly disables it.
|
|
343
|
+
*/
|
|
344
|
+
export function checkpointFailed(reason, underlying) {
|
|
345
|
+
return new CruxyError({
|
|
346
|
+
code: ErrorCode.CheckpointFailed,
|
|
347
|
+
title: "the working-tree checkpoint operation failed",
|
|
348
|
+
cause: reason,
|
|
349
|
+
nextSteps: [
|
|
350
|
+
"re-run with --verbose for details",
|
|
351
|
+
"if this happened during rollback, re-run `cruxy rollback <id>` — it recomputes from disk and is safe to retry",
|
|
352
|
+
"set `checkpoint.enabled = false` in config to run without undo protection (not recommended)",
|
|
353
|
+
],
|
|
354
|
+
underlying,
|
|
355
|
+
meta: { reason },
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** The requested checkpoint id doesn't exist (or no checkpoints exist at all). */
|
|
359
|
+
export function checkpointNotFound(id) {
|
|
360
|
+
return new CruxyError({
|
|
361
|
+
code: ErrorCode.CheckpointNotFound,
|
|
362
|
+
title: id
|
|
363
|
+
? `checkpoint "${id}" not found`
|
|
364
|
+
: "no checkpoints exist in this project",
|
|
365
|
+
cause: id
|
|
366
|
+
? "no manifest with that id under .cruxy/checkpoints/"
|
|
367
|
+
: "a checkpoint is created automatically before an agent run's first file change",
|
|
368
|
+
nextSteps: [
|
|
369
|
+
"run `cruxy checkpoint list` to see the saved checkpoints",
|
|
370
|
+
"checkpoints are pruned by retention — adjust `checkpoint.retention` in config to keep more",
|
|
371
|
+
],
|
|
372
|
+
meta: { id },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Rollback needs interactive approval but cruxy is running non-interactively.
|
|
377
|
+
* Restoring is destructive and deliberate — there is no auto-rollback path, ever.
|
|
378
|
+
*/
|
|
379
|
+
export function rollbackApprovalRequired() {
|
|
380
|
+
return new CruxyError({
|
|
381
|
+
code: ErrorCode.RollbackApprovalRequired,
|
|
382
|
+
title: "rollback needs your approval, but cruxy is running non-interactively",
|
|
383
|
+
cause: "restoring a checkpoint overwrites working-tree files and can only be confirmed in an interactive terminal",
|
|
384
|
+
nextSteps: [
|
|
385
|
+
"run `cruxy rollback` in an interactive terminal to review the preview and confirm",
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// ── subagent (exit 2 / 11) ────────────────────────────────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* A subagent spawn was attempted past the configured nesting cap (C.14). The
|
|
392
|
+
* spawn tool is structurally withheld at the cap, so reaching this means the
|
|
393
|
+
* orchestrator seam was driven directly — fail loud, never spawn.
|
|
394
|
+
*/
|
|
395
|
+
export function subagentDepthExceeded(depth, maxDepth) {
|
|
396
|
+
return new CruxyError({
|
|
397
|
+
code: ErrorCode.SubagentDepthExceeded,
|
|
398
|
+
title: `subagent nesting depth ${depth + 1} exceeds the configured cap (${maxDepth})`,
|
|
399
|
+
cause: "subagents may not spawn subagents beyond subagent.maxDepth",
|
|
400
|
+
nextSteps: [
|
|
401
|
+
"raise `subagent.maxDepth` in config if deeper nesting is intended",
|
|
402
|
+
"or restructure the task so the parent dispatches the subtasks directly",
|
|
403
|
+
],
|
|
404
|
+
meta: { depth, maxDepth },
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* A subagent run failed outright (provider error, tool crash) before producing
|
|
409
|
+
* a result. Normally folded into the structured `SubagentResult` the parent
|
|
410
|
+
* reasons over; thrown only when the orchestrator itself cannot proceed.
|
|
411
|
+
*/
|
|
412
|
+
export function subagentFailed(underlying) {
|
|
413
|
+
return new CruxyError({
|
|
414
|
+
code: ErrorCode.SubagentFailed,
|
|
415
|
+
title: "the subagent run failed",
|
|
416
|
+
cause: messageOf(underlying),
|
|
417
|
+
nextSteps: [
|
|
418
|
+
"re-run with --verbose for details",
|
|
419
|
+
"retry the task, or narrow the subagent's scope",
|
|
420
|
+
],
|
|
421
|
+
underlying,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
338
424
|
// ── internal (exit 1) ─────────────────────────────────────────────────────────
|
|
339
425
|
export function internal(underlying) {
|
|
340
426
|
return new CruxyError({
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const ErrorCode: {
|
|
|
19
19
|
readonly GitProtectedBranch: "CRUXY_E_GIT_PROTECTED_BRANCH";
|
|
20
20
|
readonly PlanInvalid: "CRUXY_E_PLAN_INVALID";
|
|
21
21
|
readonly PlanRevisionLimit: "CRUXY_E_PLAN_REVISION_LIMIT";
|
|
22
|
+
readonly CheckpointNotFound: "CRUXY_E_CHECKPOINT_NOT_FOUND";
|
|
22
23
|
readonly ConfigParse: "CRUXY_E_CONFIG_PARSE";
|
|
23
24
|
readonly ConfigInvalid: "CRUXY_E_CONFIG_INVALID";
|
|
24
25
|
readonly AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY";
|
|
@@ -34,6 +35,7 @@ export declare const ErrorCode: {
|
|
|
34
35
|
readonly FileNotFound: "CRUXY_E_FILE_NOT_FOUND";
|
|
35
36
|
readonly PermissionDenied: "CRUXY_E_PERMISSION_DENIED";
|
|
36
37
|
readonly PathEscape: "CRUXY_E_PATH_ESCAPE";
|
|
38
|
+
readonly CheckpointFailed: "CRUXY_E_CHECKPOINT_FAILED";
|
|
37
39
|
readonly IndexEmbedderUnavailable: "CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE";
|
|
38
40
|
readonly IndexStoreUnavailable: "CRUXY_E_INDEX_STORE_UNAVAILABLE";
|
|
39
41
|
readonly IndexFailed: "CRUXY_E_INDEX_FAILED";
|
|
@@ -41,6 +43,11 @@ export declare const ErrorCode: {
|
|
|
41
43
|
readonly SkillNotFound: "CRUXY_E_SKILL_NOT_FOUND";
|
|
42
44
|
readonly ApprovalRequired: "CRUXY_E_APPROVAL_REQUIRED";
|
|
43
45
|
readonly PlanApprovalRequired: "CRUXY_E_PLAN_APPROVAL_REQUIRED";
|
|
46
|
+
readonly RollbackApprovalRequired: "CRUXY_E_ROLLBACK_APPROVAL_REQUIRED";
|
|
47
|
+
readonly SubagentDepthExceeded: "CRUXY_E_SUBAGENT_DEPTH_EXCEEDED";
|
|
48
|
+
/** Carried inside a SubagentResult (informational) — never fatal by itself. */
|
|
49
|
+
readonly SubagentBudget: "CRUXY_E_SUBAGENT_BUDGET";
|
|
50
|
+
readonly SubagentFailed: "CRUXY_E_SUBAGENT_FAILED";
|
|
44
51
|
};
|
|
45
52
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
46
53
|
/** The process exit code for an error code (defaults to 1 for safety). */
|
package/dist/errors/types.js
CHANGED
|
@@ -21,6 +21,7 @@ export const ErrorCode = {
|
|
|
21
21
|
GitProtectedBranch: "CRUXY_E_GIT_PROTECTED_BRANCH",
|
|
22
22
|
PlanInvalid: "CRUXY_E_PLAN_INVALID",
|
|
23
23
|
PlanRevisionLimit: "CRUXY_E_PLAN_REVISION_LIMIT",
|
|
24
|
+
CheckpointNotFound: "CRUXY_E_CHECKPOINT_NOT_FOUND",
|
|
24
25
|
// config (exit 3)
|
|
25
26
|
ConfigParse: "CRUXY_E_CONFIG_PARSE",
|
|
26
27
|
ConfigInvalid: "CRUXY_E_CONFIG_INVALID",
|
|
@@ -41,6 +42,7 @@ export const ErrorCode = {
|
|
|
41
42
|
FileNotFound: "CRUXY_E_FILE_NOT_FOUND",
|
|
42
43
|
PermissionDenied: "CRUXY_E_PERMISSION_DENIED",
|
|
43
44
|
PathEscape: "CRUXY_E_PATH_ESCAPE",
|
|
45
|
+
CheckpointFailed: "CRUXY_E_CHECKPOINT_FAILED",
|
|
44
46
|
// index (exit 8)
|
|
45
47
|
IndexEmbedderUnavailable: "CRUXY_E_INDEX_EMBEDDER_UNAVAILABLE",
|
|
46
48
|
IndexStoreUnavailable: "CRUXY_E_INDEX_STORE_UNAVAILABLE",
|
|
@@ -51,6 +53,12 @@ export const ErrorCode = {
|
|
|
51
53
|
// approval (exit 10)
|
|
52
54
|
ApprovalRequired: "CRUXY_E_APPROVAL_REQUIRED",
|
|
53
55
|
PlanApprovalRequired: "CRUXY_E_PLAN_APPROVAL_REQUIRED",
|
|
56
|
+
RollbackApprovalRequired: "CRUXY_E_ROLLBACK_APPROVAL_REQUIRED",
|
|
57
|
+
// subagent (exit 2 / 11)
|
|
58
|
+
SubagentDepthExceeded: "CRUXY_E_SUBAGENT_DEPTH_EXCEEDED",
|
|
59
|
+
/** Carried inside a SubagentResult (informational) — never fatal by itself. */
|
|
60
|
+
SubagentBudget: "CRUXY_E_SUBAGENT_BUDGET",
|
|
61
|
+
SubagentFailed: "CRUXY_E_SUBAGENT_FAILED",
|
|
54
62
|
};
|
|
55
63
|
/**
|
|
56
64
|
* Category exit codes. Distinct per category so a caller (CI, a script) can
|
|
@@ -64,6 +72,7 @@ const EXIT_CODES = {
|
|
|
64
72
|
[ErrorCode.GitProtectedBranch]: 2,
|
|
65
73
|
[ErrorCode.PlanInvalid]: 2,
|
|
66
74
|
[ErrorCode.PlanRevisionLimit]: 2,
|
|
75
|
+
[ErrorCode.CheckpointNotFound]: 2,
|
|
67
76
|
[ErrorCode.ConfigParse]: 3,
|
|
68
77
|
[ErrorCode.ConfigInvalid]: 3,
|
|
69
78
|
[ErrorCode.AuthMissingKey]: 4,
|
|
@@ -79,6 +88,7 @@ const EXIT_CODES = {
|
|
|
79
88
|
[ErrorCode.FileNotFound]: 7,
|
|
80
89
|
[ErrorCode.PermissionDenied]: 7,
|
|
81
90
|
[ErrorCode.PathEscape]: 7,
|
|
91
|
+
[ErrorCode.CheckpointFailed]: 7,
|
|
82
92
|
[ErrorCode.IndexEmbedderUnavailable]: 8,
|
|
83
93
|
[ErrorCode.IndexStoreUnavailable]: 8,
|
|
84
94
|
[ErrorCode.IndexFailed]: 8,
|
|
@@ -86,6 +96,12 @@ const EXIT_CODES = {
|
|
|
86
96
|
[ErrorCode.SkillNotFound]: 9,
|
|
87
97
|
[ErrorCode.ApprovalRequired]: 10,
|
|
88
98
|
[ErrorCode.PlanApprovalRequired]: 10,
|
|
99
|
+
[ErrorCode.RollbackApprovalRequired]: 10,
|
|
100
|
+
// Depth-exceed is a misuse of the spawn seam (usage); the other two surface
|
|
101
|
+
// inside a SubagentResult and only exit the process if thrown directly.
|
|
102
|
+
[ErrorCode.SubagentDepthExceeded]: 2,
|
|
103
|
+
[ErrorCode.SubagentBudget]: 11,
|
|
104
|
+
[ErrorCode.SubagentFailed]: 11,
|
|
89
105
|
};
|
|
90
106
|
/** The process exit code for an error code (defaults to 1 for safety). */
|
|
91
107
|
export function exitCodeFor(code) {
|
|
@@ -18,7 +18,18 @@ export interface WalkOptions {
|
|
|
18
18
|
maxFileBytes: number;
|
|
19
19
|
/** Ignore-file names to honor, relative to each directory (gitignore syntax). */
|
|
20
20
|
ignoreFileNames?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Yield binary files too (default false). The index never wants them; the
|
|
23
|
+
* checkpoint capturer (C.32) does — a snapshot must be complete to restore.
|
|
24
|
+
*/
|
|
25
|
+
includeBinary?: boolean;
|
|
21
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The C.17 secrets denylist test, shared beyond the index: the checkpoint
|
|
29
|
+
* capturer (C.32) applies the exact same rule so a working-tree snapshot can
|
|
30
|
+
* never contain a secret-bearing file either.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isSecretPath(relPath: string): boolean;
|
|
22
33
|
/**
|
|
23
34
|
* A compiled set of gitignore-style patterns, matched against paths relative to
|
|
24
35
|
* the file's own directory. Supports comments, blank lines, `!` negation,
|
package/dist/indexing/walker.js
CHANGED
|
@@ -16,7 +16,12 @@ const SECRET_PATTERNS = [
|
|
|
16
16
|
/(^|\/)\.(npmrc|netrc|pgpass)$/i, // credential dotfiles
|
|
17
17
|
/(^|\/)\.aws\/credentials$/i,
|
|
18
18
|
];
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The C.17 secrets denylist test, shared beyond the index: the checkpoint
|
|
21
|
+
* capturer (C.32) applies the exact same rule so a working-tree snapshot can
|
|
22
|
+
* never contain a secret-bearing file either.
|
|
23
|
+
*/
|
|
24
|
+
export function isSecretPath(relPath) {
|
|
20
25
|
return SECRET_PATTERNS.some((re) => re.test(relPath));
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
@@ -121,9 +126,9 @@ async function isBinaryFile(absPath) {
|
|
|
121
126
|
export async function* walkRepo(root, opts) {
|
|
122
127
|
const absRoot = path.resolve(root);
|
|
123
128
|
const ignoreFileNames = opts.ignoreFileNames ?? DEFAULT_IGNORE_FILES;
|
|
124
|
-
yield* walkDir(absRoot, absRoot, [], ignoreFileNames, opts
|
|
129
|
+
yield* walkDir(absRoot, absRoot, [], ignoreFileNames, opts);
|
|
125
130
|
}
|
|
126
|
-
async function* walkDir(dir, root, parentStack, ignoreFileNames,
|
|
131
|
+
async function* walkDir(dir, root, parentStack, ignoreFileNames, opts) {
|
|
127
132
|
const scope = await loadIgnoreScope(dir, ignoreFileNames);
|
|
128
133
|
const stack = scope ? [...parentStack, scope] : parentStack;
|
|
129
134
|
let entries;
|
|
@@ -147,7 +152,7 @@ async function* walkDir(dir, root, parentStack, ignoreFileNames, maxFileBytes) {
|
|
|
147
152
|
if (isIgnored(stack, absPath, isDir))
|
|
148
153
|
continue;
|
|
149
154
|
if (isDir) {
|
|
150
|
-
yield* walkDir(absPath, root, stack, ignoreFileNames,
|
|
155
|
+
yield* walkDir(absPath, root, stack, ignoreFileNames, opts);
|
|
151
156
|
continue;
|
|
152
157
|
}
|
|
153
158
|
let size;
|
|
@@ -157,9 +162,9 @@ async function* walkDir(dir, root, parentStack, ignoreFileNames, maxFileBytes) {
|
|
|
157
162
|
catch {
|
|
158
163
|
continue;
|
|
159
164
|
}
|
|
160
|
-
if (size > maxFileBytes)
|
|
165
|
+
if (size > opts.maxFileBytes)
|
|
161
166
|
continue;
|
|
162
|
-
if (await isBinaryFile(absPath))
|
|
167
|
+
if (!opts.includeBinary && (await isBinaryFile(absPath)))
|
|
163
168
|
continue;
|
|
164
169
|
yield { relPath, absPath, size };
|
|
165
170
|
}
|
package/dist/plan/execute.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PromptIO } from "../approval/index.js";
|
|
2
|
+
import type { StreamRenderer } from "../render/index.js";
|
|
2
3
|
import type { Plan, PlanExecutionResult, PlanStep } from "./types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Execute an approved plan step-by-step (C.31): mark each step `running`, run it,
|
|
@@ -16,5 +17,12 @@ export interface ExecuteDeps {
|
|
|
16
17
|
*/
|
|
17
18
|
runStep: (step: PlanStep) => Promise<void>;
|
|
18
19
|
io: PromptIO;
|
|
20
|
+
/**
|
|
21
|
+
* Live step progress (U.4): each step feeds `[i/n] title` to the renderer's
|
|
22
|
+
* progress register, straight from this walk of `plan.steps` — the same
|
|
23
|
+
* statuses the committed trail renders, never recomputed. Optional so the
|
|
24
|
+
* executor stays drivable without a terminal (tests, future CI mode).
|
|
25
|
+
*/
|
|
26
|
+
renderer?: StreamRenderer;
|
|
19
27
|
}
|
|
20
28
|
export declare function executePlan(plan: Plan, deps: ExecuteDeps): Promise<PlanExecutionResult>;
|
package/dist/plan/execute.js
CHANGED
|
@@ -2,30 +2,44 @@ import { CruxyError } from "../errors/index.js";
|
|
|
2
2
|
import { promptContinueAfterFailure } from "./approve.js";
|
|
3
3
|
import { renderStepStatus } from "./render.js";
|
|
4
4
|
export async function executePlan(plan, deps) {
|
|
5
|
-
const { runStep, io } = deps;
|
|
6
|
-
|
|
7
|
-
step.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
const { runStep, io, renderer } = deps;
|
|
6
|
+
try {
|
|
7
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
8
|
+
step.status = "running";
|
|
9
|
+
// Live: "[2/5] title · working…" until the step's agent turn takes over.
|
|
10
|
+
renderer?.progress({
|
|
11
|
+
step: index + 1,
|
|
12
|
+
of: plan.steps.length,
|
|
13
|
+
title: step.title,
|
|
14
|
+
});
|
|
15
|
+
renderer?.setPhase({ kind: "executing-step" });
|
|
12
16
|
io.write(renderStepStatus(step, io.color) + "\n");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
try {
|
|
18
|
+
await runStep(step);
|
|
19
|
+
step.status = "done";
|
|
20
|
+
io.write(renderStepStatus(step, io.color) + "\n");
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
step.status = "failed";
|
|
24
|
+
io.write(renderStepStatus(step, io.color) + "\n");
|
|
25
|
+
// Surface the failure via the U.5 shape when we have it.
|
|
26
|
+
const detail = err instanceof CruxyError
|
|
27
|
+
? `${err.title}${err.cause ? ` — ${err.cause}` : ""}`
|
|
28
|
+
: err.message;
|
|
29
|
+
io.write(` ${detail}\n`);
|
|
30
|
+
const cont = await promptContinueAfterFailure(io);
|
|
31
|
+
if (!cont) {
|
|
32
|
+
return { completed: false, halted: true, failedStepId: step.id };
|
|
33
|
+
}
|
|
34
|
+
// User chose to continue despite the failure; move to the next step.
|
|
25
35
|
}
|
|
26
|
-
// User chose to continue despite the failure; move to the next step.
|
|
27
36
|
}
|
|
37
|
+
const completed = plan.steps.every((s) => s.status === "done");
|
|
38
|
+
return { completed, halted: false };
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
// The plan owns the progress register; release it on every exit path so
|
|
42
|
+
// no stale "[i/n]" prefix outlives the run.
|
|
43
|
+
renderer?.progress(null);
|
|
28
44
|
}
|
|
29
|
-
const completed = plan.steps.every((s) => s.status === "done");
|
|
30
|
-
return { completed, halted: false };
|
|
31
45
|
}
|
package/dist/plan/service.js
CHANGED
|
@@ -108,7 +108,11 @@ export async function runPlanSession(args) {
|
|
|
108
108
|
renderer: args.renderer,
|
|
109
109
|
}));
|
|
110
110
|
};
|
|
111
|
-
await executePlan(plan, {
|
|
111
|
+
await executePlan(plan, {
|
|
112
|
+
runStep,
|
|
113
|
+
io: args.io,
|
|
114
|
+
renderer: args.renderer,
|
|
115
|
+
});
|
|
112
116
|
return finish();
|
|
113
117
|
}
|
|
114
118
|
feedback = decision.feedback;
|
|
@@ -7,24 +7,24 @@ declare const parameters: z.ZodObject<{
|
|
|
7
7
|
rationale: z.ZodString;
|
|
8
8
|
kind: z.ZodEnum<["read", "mutate", "destructive"]>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
-
title: string;
|
|
11
10
|
kind: "read" | "mutate" | "destructive";
|
|
11
|
+
title: string;
|
|
12
12
|
rationale: string;
|
|
13
13
|
}, {
|
|
14
|
-
title: string;
|
|
15
14
|
kind: "read" | "mutate" | "destructive";
|
|
15
|
+
title: string;
|
|
16
16
|
rationale: string;
|
|
17
17
|
}>, "many">;
|
|
18
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
19
|
steps: {
|
|
20
|
-
title: string;
|
|
21
20
|
kind: "read" | "mutate" | "destructive";
|
|
21
|
+
title: string;
|
|
22
22
|
rationale: string;
|
|
23
23
|
}[];
|
|
24
24
|
}, {
|
|
25
25
|
steps: {
|
|
26
|
-
title: string;
|
|
27
26
|
kind: "read" | "mutate" | "destructive";
|
|
27
|
+
title: string;
|
|
28
28
|
rationale: string;
|
|
29
29
|
}[];
|
|
30
30
|
}>;
|
package/dist/render/diff.js
CHANGED
|
@@ -40,6 +40,30 @@ function renderPrPreview(preview, c) {
|
|
|
40
40
|
out.push(c.dim(` ${line}`));
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Render a `rollback` restore plan (C.32). Header, warnings, and the
|
|
45
|
+
* out-of-scope note come **before** the file diffs on purpose: the global
|
|
46
|
+
* {@link PREVIEW_MAX_LINES} collapse trims from the tail, and the blast-radius
|
|
47
|
+
* warnings must never be the part that gets hidden.
|
|
48
|
+
*/
|
|
49
|
+
function renderRollbackPreview(preview, c) {
|
|
50
|
+
const out = [];
|
|
51
|
+
out.push(`${c.bold("restore checkpoint")} ${c.cyan(preview.checkpointId)} ${c.dim(`(${preview.createdAt})`)}`);
|
|
52
|
+
if (preview.runSummary)
|
|
53
|
+
out.push(c.dim(`run: ${preview.runSummary}`));
|
|
54
|
+
out.push(c.dim("working-tree files only — commits, pushes, and PRs made during the run are not undone"));
|
|
55
|
+
if (preview.externalPaths.length > 0) {
|
|
56
|
+
out.push(c.red(c.bold("changed outside this run — rollback will overwrite these too:")));
|
|
57
|
+
for (const p of preview.externalPaths)
|
|
58
|
+
out.push(c.red(`! ${p}`));
|
|
59
|
+
}
|
|
60
|
+
if (preview.attributionUnknown) {
|
|
61
|
+
out.push(c.yellow("this run executed shell commands; some changes below may not have been made by the run"));
|
|
62
|
+
}
|
|
63
|
+
out.push("");
|
|
64
|
+
out.push(...renderPatchFiles(preview.files, c));
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
43
67
|
/** Split a multi-line body into trimmed-of-trailing lines, dropping a trailing blank. */
|
|
44
68
|
function bodyLines(body) {
|
|
45
69
|
const lines = body.replace(/\s+$/, "").split("\n");
|
|
@@ -63,6 +87,9 @@ export function renderActionPreview(preview, c) {
|
|
|
63
87
|
else if (preview.type === "pr") {
|
|
64
88
|
lines = renderPrPreview(preview, c);
|
|
65
89
|
}
|
|
90
|
+
else if (preview.type === "rollback") {
|
|
91
|
+
lines = renderRollbackPreview(preview, c);
|
|
92
|
+
}
|
|
66
93
|
else {
|
|
67
94
|
const header = preview.exists
|
|
68
95
|
? c.yellow("OVERWRITE existing")
|