@cruxy/cli 0.6.0 → 0.7.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 (61) hide show
  1. package/README.md +33 -16
  2. package/dist/agent/loop.d.ts +2 -0
  3. package/dist/agent/loop.js +1 -0
  4. package/dist/agent/prompts.d.ts +2 -0
  5. package/dist/agent/prompts.js +6 -0
  6. package/dist/agent/session.d.ts +24 -0
  7. package/dist/agent/session.js +33 -7
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +40 -0
  10. package/dist/cli/commands/login.d.ts +8 -0
  11. package/dist/cli/commands/login.js +36 -0
  12. package/dist/cli/commands/run.js +31 -53
  13. package/dist/cli/onboard.d.ts +25 -0
  14. package/dist/cli/onboard.js +54 -0
  15. package/dist/cli/program.js +19 -1
  16. package/dist/cli/repl.js +9 -0
  17. package/dist/cli/session-factory.d.ts +12 -0
  18. package/dist/cli/session-factory.js +88 -0
  19. package/dist/config/credentials.d.ts +10 -0
  20. package/dist/config/credentials.js +69 -0
  21. package/dist/config/index.d.ts +1 -0
  22. package/dist/config/index.js +1 -0
  23. package/dist/config/manager.d.ts +6 -1
  24. package/dist/config/manager.js +11 -1
  25. package/dist/config/schema.d.ts +10 -0
  26. package/dist/config/schema.js +2 -0
  27. package/dist/constants.d.ts +6 -0
  28. package/dist/constants.js +6 -0
  29. package/dist/errors/constructors.d.ts +10 -0
  30. package/dist/errors/constructors.js +46 -2
  31. package/dist/errors/types.d.ts +3 -0
  32. package/dist/errors/types.js +6 -0
  33. package/dist/onboarding/detect.d.ts +26 -0
  34. package/dist/onboarding/detect.js +56 -0
  35. package/dist/onboarding/flow.d.ts +28 -0
  36. package/dist/onboarding/flow.js +100 -0
  37. package/dist/onboarding/index.d.ts +5 -0
  38. package/dist/onboarding/index.js +5 -0
  39. package/dist/onboarding/io.d.ts +8 -0
  40. package/dist/onboarding/io.js +133 -0
  41. package/dist/onboarding/steps.d.ts +17 -0
  42. package/dist/onboarding/steps.js +100 -0
  43. package/dist/onboarding/types.d.ts +81 -0
  44. package/dist/onboarding/types.js +6 -0
  45. package/dist/plan/approve.d.ts +16 -0
  46. package/dist/plan/approve.js +46 -0
  47. package/dist/plan/execute.d.ts +20 -0
  48. package/dist/plan/execute.js +31 -0
  49. package/dist/plan/index.d.ts +7 -0
  50. package/dist/plan/index.js +7 -0
  51. package/dist/plan/policy.d.ts +26 -0
  52. package/dist/plan/policy.js +45 -0
  53. package/dist/plan/render.d.ts +5 -0
  54. package/dist/plan/render.js +47 -0
  55. package/dist/plan/service.d.ts +39 -0
  56. package/dist/plan/service.js +118 -0
  57. package/dist/plan/submit-plan.d.ts +33 -0
  58. package/dist/plan/submit-plan.js +57 -0
  59. package/dist/plan/types.d.ts +60 -0
  60. package/dist/plan/types.js +6 -0
  61. package/package.json +1 -1
@@ -0,0 +1,118 @@
1
+ import { planApprovalRequired, planInvalid, planRevisionLimit, } from "../errors/index.js";
2
+ import { ToolRegistry } from "../tools/index.js";
3
+ import { runAgent } from "../agent/loop.js";
4
+ import { promptPlanDecision } from "./approve.js";
5
+ import { executePlan } from "./execute.js";
6
+ import { makeSubmitPlanTool } from "./submit-plan.js";
7
+ /**
8
+ * Orchestrates a plan-mode turn (C.31): propose → approve/revise (capped) →
9
+ * execute, all on the ordinary agent loop. The propose phase runs with a
10
+ * read-only + `submit_plan` registry so the agent cannot act before approval;
11
+ * execution runs with the full registry, one step at a time, per-action U.3
12
+ * gating intact.
13
+ */
14
+ /** Default cap on plan revisions before failing loud. */
15
+ export const MAX_PLAN_REVISIONS = 3;
16
+ /** Tools available during the propose phase (read-only) — plus `submit_plan`. */
17
+ const PLAN_PHASE_TOOLS = new Set([
18
+ "list_files",
19
+ "read_file",
20
+ "glob",
21
+ "grep_files",
22
+ "git_status",
23
+ "search_codebase",
24
+ "list_skills",
25
+ "load_skill",
26
+ ]);
27
+ /** Build the read-only + `submit_plan` registry for the propose phase. */
28
+ function buildPlanPhaseRegistry(execRegistry, holder) {
29
+ const registry = new ToolRegistry();
30
+ for (const tool of execRegistry.list()) {
31
+ if (PLAN_PHASE_TOOLS.has(tool.name))
32
+ registry.register(tool);
33
+ }
34
+ registry.register(makeSubmitPlanTool(holder));
35
+ return registry;
36
+ }
37
+ export async function runPlanSession(args) {
38
+ // Fail loud before proposing — a plan is never auto-approved (U.3 discipline).
39
+ if (!args.interactive)
40
+ throw planApprovalRequired();
41
+ const maxRevisions = args.maxRevisions ?? MAX_PLAN_REVISIONS;
42
+ const usage = { input_tokens: 0, output_tokens: 0 };
43
+ let messages = args.messages;
44
+ let iterations = 0;
45
+ const accumulate = (r) => {
46
+ messages = r.messages;
47
+ usage.input_tokens += r.usage.input_tokens;
48
+ usage.output_tokens += r.usage.output_tokens;
49
+ iterations += r.iterations;
50
+ };
51
+ const finish = () => ({
52
+ messages,
53
+ iterations,
54
+ stop: "completed",
55
+ usage,
56
+ });
57
+ // ── propose / revise loop (capped) ─────────────────────────────────────────
58
+ let feedback = null;
59
+ for (let rev = 0; rev <= maxRevisions; rev++) {
60
+ if (feedback !== null) {
61
+ messages.push({
62
+ role: "user",
63
+ content: `Revise the plan based on this feedback, then call submit_plan again with the updated steps:\n\n${feedback}`,
64
+ });
65
+ }
66
+ const holder = { plan: null };
67
+ const planRegistry = buildPlanPhaseRegistry(args.execRegistry, holder);
68
+ accumulate(await runAgent({
69
+ messages,
70
+ provider: args.provider,
71
+ registry: planRegistry,
72
+ config: args.config,
73
+ ctx: args.ctx,
74
+ git: args.git,
75
+ projectInstructions: args.projectInstructions,
76
+ onText: args.onText,
77
+ planMode: true,
78
+ }));
79
+ if (!holder.plan) {
80
+ throw planInvalid("the model ended its turn without calling submit_plan");
81
+ }
82
+ const plan = holder.plan;
83
+ const decision = await promptPlanDecision(plan, args.io);
84
+ if (decision.kind === "abort") {
85
+ args.io.write("plan aborted — nothing was executed.\n");
86
+ return finish();
87
+ }
88
+ if (decision.kind !== "revise") {
89
+ if (decision.kind === "approve-grant") {
90
+ args.planPolicy.enableSafeStepGrants();
91
+ }
92
+ // Execute step-by-step, driving one agent turn per step against the full
93
+ // registry. Each step's actions still pass through the U.3 gate.
94
+ const runStep = async (step) => {
95
+ messages.push({
96
+ role: "user",
97
+ content: `The plan is approved. Do ONLY step ${step.id}: ${step.title}. ${step.rationale} ` +
98
+ "Do not start any other step. When this step is complete, stop.",
99
+ });
100
+ accumulate(await runAgent({
101
+ messages,
102
+ provider: args.provider,
103
+ registry: args.execRegistry,
104
+ config: args.config,
105
+ ctx: args.ctx,
106
+ git: args.git,
107
+ projectInstructions: args.projectInstructions,
108
+ onText: args.onText,
109
+ }));
110
+ };
111
+ await executePlan(plan, { runStep, io: args.io });
112
+ return finish();
113
+ }
114
+ feedback = decision.feedback;
115
+ }
116
+ // Rejected past the cap without converging.
117
+ throw planRevisionLimit(maxRevisions);
118
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "../tools/index.js";
3
+ import type { PlanHolder } from "./types.js";
4
+ declare const parameters: z.ZodObject<{
5
+ steps: z.ZodArray<z.ZodObject<{
6
+ title: z.ZodString;
7
+ rationale: z.ZodString;
8
+ kind: z.ZodEnum<["read", "mutate", "destructive"]>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ title: string;
11
+ kind: "read" | "mutate" | "destructive";
12
+ rationale: string;
13
+ }, {
14
+ title: string;
15
+ kind: "read" | "mutate" | "destructive";
16
+ rationale: string;
17
+ }>, "many">;
18
+ }, "strip", z.ZodTypeAny, {
19
+ steps: {
20
+ title: string;
21
+ kind: "read" | "mutate" | "destructive";
22
+ rationale: string;
23
+ }[];
24
+ }, {
25
+ steps: {
26
+ title: string;
27
+ kind: "read" | "mutate" | "destructive";
28
+ rationale: string;
29
+ }[];
30
+ }>;
31
+ /** Build a `submit_plan` tool bound to `holder`, which captures the last plan. */
32
+ export declare function makeSubmitPlanTool(holder: PlanHolder): Tool<typeof parameters>;
33
+ export {};
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The `submit_plan` tool (C.31): the agent's only way to propose a plan. It is a
4
+ * normal tool on the same loop seam as everything else — no hidden control flow.
5
+ * During the propose phase this is the *only* non-read-only tool registered, so
6
+ * the agent structurally cannot act before approval.
7
+ *
8
+ * Validation is fail-loud: a malformed or empty plan returns `{ok:false}` so the
9
+ * model sees the error and retries, exactly like any other tool.
10
+ */
11
+ const stepSchema = z.object({
12
+ title: z.string().min(1).describe("One-line imperative title for the step."),
13
+ rationale: z
14
+ .string()
15
+ .min(1)
16
+ .describe("Why this step exists / what it accomplishes."),
17
+ kind: z
18
+ .enum(["read", "mutate", "destructive"])
19
+ .describe("Risk estimate: read (no side effects), mutate (reversible file writes), " +
20
+ "destructive (shell, deletes, PR-open). Advisory — the approval gate re-checks each action."),
21
+ });
22
+ const parameters = z.object({
23
+ steps: z
24
+ .array(stepSchema)
25
+ .min(1)
26
+ .describe("Ordered steps that fully accomplish the task."),
27
+ });
28
+ /** Build a `submit_plan` tool bound to `holder`, which captures the last plan. */
29
+ export function makeSubmitPlanTool(holder) {
30
+ return {
31
+ name: "submit_plan",
32
+ description: "Propose an ordered, step-by-step plan for the user to approve before you act. " +
33
+ "Call this first (and only this) in plan mode; do not take any other action until the plan is approved.",
34
+ parameters,
35
+ async execute(input) {
36
+ // zod already guarantees ≥1 step with non-empty fields; this is the
37
+ // fail-loud belt-and-suspenders for an empty array slipping through.
38
+ if (input.steps.length === 0) {
39
+ return { ok: false, error: "a plan must have at least one step" };
40
+ }
41
+ const steps = input.steps.map((s, i) => ({
42
+ id: String(i + 1),
43
+ title: s.title.trim(),
44
+ rationale: s.rationale.trim(),
45
+ kind: s.kind,
46
+ status: "pending",
47
+ }));
48
+ const plan = { steps };
49
+ holder.plan = plan;
50
+ return {
51
+ ok: true,
52
+ output: `Plan received (${steps.length} step${steps.length === 1 ? "" : "s"}) and shown to the user for approval. ` +
53
+ "Do not take any further action now — end your turn.",
54
+ };
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Types for plan mode (C.31). A {@link Plan} is a first-class, typed artifact the
3
+ * agent produces via the `submit_plan` tool — never free text — so the user can
4
+ * review the shape of the work once, and execution can track per-step status.
5
+ */
6
+ /**
7
+ * The agent's risk *estimate* for a step, used only to render intent and shape
8
+ * the approval copy. It is NOT authoritative: the real U.3 gate re-classifies
9
+ * every concrete action at execution time (a step the agent calls "mutate" whose
10
+ * action turns out destructive still confirms).
11
+ */
12
+ export type PlanStepKind = "read" | "mutate" | "destructive";
13
+ /** Live execution status of a step. */
14
+ export type PlanStepStatus = "pending" | "running" | "done" | "failed";
15
+ export interface PlanStep {
16
+ /** Stable 1-based id assigned on submit (e.g. "1"). */
17
+ readonly id: string;
18
+ /** One-line imperative title. */
19
+ readonly title: string;
20
+ /** Why this step exists / what it accomplishes. */
21
+ readonly rationale: string;
22
+ /** The agent's risk estimate (advisory; see {@link PlanStepKind}). */
23
+ readonly kind: PlanStepKind;
24
+ /** Mutated in place as execution progresses. */
25
+ status: PlanStepStatus;
26
+ }
27
+ export interface Plan {
28
+ readonly steps: PlanStep[];
29
+ }
30
+ /**
31
+ * The user's decision at the plan-approval prompt.
32
+ * - `approve` — execute; every action still hits the U.3 gate.
33
+ * - `approve-grant` — execute AND auto-allow the safe (read/mutate, grantable)
34
+ * steps for this run; destructive/ungrantable actions still confirm.
35
+ * - `revise` — send `feedback` back to the agent for a revised plan.
36
+ * - `abort` — cancel; nothing executes (also the default-deny for EOF/Ctrl-C).
37
+ */
38
+ export type PlanDecision = {
39
+ readonly kind: "approve";
40
+ } | {
41
+ readonly kind: "approve-grant";
42
+ } | {
43
+ readonly kind: "revise";
44
+ readonly feedback: string;
45
+ } | {
46
+ readonly kind: "abort";
47
+ };
48
+ /** Outcome of executing an approved plan. */
49
+ export interface PlanExecutionResult {
50
+ /** True if every step reached `done`. */
51
+ readonly completed: boolean;
52
+ /** True if a step failed and the user chose to abort the run. */
53
+ readonly halted: boolean;
54
+ /** The id of the step that failed, when halted. */
55
+ readonly failedStepId?: string;
56
+ }
57
+ /** A mutable holder the `submit_plan` tool writes the captured plan into. */
58
+ export interface PlanHolder {
59
+ plan: Plan | null;
60
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Types for plan mode (C.31). A {@link Plan} is a first-class, typed artifact the
3
+ * agent produces via the `submit_plan` tool — never free text — so the user can
4
+ * review the shape of the work once, and execution can track per-step status.
5
+ */
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {