@cruxy/cli 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/agent/loop.d.ts +21 -2
  2. package/dist/agent/loop.js +21 -5
  3. package/dist/agent/session.d.ts +13 -0
  4. package/dist/agent/session.js +6 -0
  5. package/dist/approval/index.d.ts +1 -0
  6. package/dist/approval/index.js +1 -0
  7. package/dist/approval/mutex.d.ts +45 -0
  8. package/dist/approval/mutex.js +57 -0
  9. package/dist/checkpoint/gate-hook.d.ts +28 -0
  10. package/dist/checkpoint/gate-hook.js +98 -0
  11. package/dist/checkpoint/gate.d.ts +7 -1
  12. package/dist/checkpoint/gate.js +8 -2
  13. package/dist/checkpoint/index.d.ts +1 -0
  14. package/dist/checkpoint/index.js +1 -0
  15. package/dist/checkpoint/service.d.ts +9 -0
  16. package/dist/checkpoint/service.js +20 -0
  17. package/dist/cli/commands/rollback.d.ts +4 -1
  18. package/dist/cli/commands/rollback.js +16 -9
  19. package/dist/cli/commands/run.js +62 -16
  20. package/dist/cli/onboard.js +2 -2
  21. package/dist/cli/repl.d.ts +1 -1
  22. package/dist/cli/repl.js +145 -0
  23. package/dist/cli/session-factory.d.ts +24 -10
  24. package/dist/cli/session-factory.js +179 -135
  25. package/dist/config/schema.d.ts +110 -0
  26. package/dist/config/schema.js +50 -0
  27. package/dist/errors/constructors.d.ts +41 -0
  28. package/dist/errors/constructors.js +87 -0
  29. package/dist/errors/types.d.ts +21 -0
  30. package/dist/errors/types.js +33 -0
  31. package/dist/hooks/index.d.ts +1 -0
  32. package/dist/hooks/index.js +1 -0
  33. package/dist/hooks/router.d.ts +58 -0
  34. package/dist/hooks/router.js +136 -0
  35. package/dist/hooks/runner.d.ts +12 -0
  36. package/dist/hooks/runner.js +23 -1
  37. package/dist/jobs/approval-queue.d.ts +85 -0
  38. package/dist/jobs/approval-queue.js +96 -0
  39. package/dist/jobs/dispatch-tool.d.ts +34 -0
  40. package/dist/jobs/dispatch-tool.js +96 -0
  41. package/dist/jobs/index.d.ts +6 -0
  42. package/dist/jobs/index.js +6 -0
  43. package/dist/jobs/log-buffer.d.ts +31 -0
  44. package/dist/jobs/log-buffer.js +30 -0
  45. package/dist/jobs/log-renderer.d.ts +32 -0
  46. package/dist/jobs/log-renderer.js +70 -0
  47. package/dist/jobs/manager.d.ts +139 -0
  48. package/dist/jobs/manager.js +397 -0
  49. package/dist/jobs/types.d.ts +81 -0
  50. package/dist/jobs/types.js +10 -0
  51. package/dist/mcp/index.d.ts +1 -0
  52. package/dist/mcp/index.js +1 -0
  53. package/dist/mcp/sibling-banner.d.ts +25 -0
  54. package/dist/mcp/sibling-banner.js +34 -0
  55. package/dist/memory/recall.d.ts +24 -0
  56. package/dist/memory/recall.js +54 -0
  57. package/dist/memory/remember-tool.d.ts +3 -0
  58. package/dist/memory/remember-tool.js +11 -1
  59. package/dist/sandbox/policy.js +14 -5
  60. package/dist/sandbox/service.d.ts +8 -1
  61. package/dist/sandbox/service.js +4 -1
  62. package/dist/subagent/index.d.ts +1 -0
  63. package/dist/subagent/index.js +1 -0
  64. package/dist/subagent/orchestrator.d.ts +76 -2
  65. package/dist/subagent/orchestrator.js +208 -18
  66. package/dist/subagent/registry-scope.d.ts +13 -0
  67. package/dist/subagent/registry-scope.js +28 -2
  68. package/dist/subagent/semaphore.d.ts +56 -0
  69. package/dist/subagent/semaphore.js +53 -0
  70. package/dist/subagent/spawn-tool.d.ts +57 -0
  71. package/dist/subagent/spawn-tool.js +104 -9
  72. package/dist/subagent/types.d.ts +17 -2
  73. package/dist/testing/run-tests-tool.js +1 -1
  74. package/dist/tools/file/paths.d.ts +5 -6
  75. package/dist/tools/file/paths.js +7 -8
  76. package/dist/tools/shell/exec.js +36 -4
  77. package/dist/tools/types.d.ts +16 -5
  78. package/dist/workspace/add-root.d.ts +27 -0
  79. package/dist/workspace/add-root.js +16 -0
  80. package/dist/workspace/index.d.ts +2 -1
  81. package/dist/workspace/index.js +2 -1
  82. package/dist/workspace/workspace.d.ts +9 -4
  83. package/dist/workspace/workspace.js +9 -4
  84. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ export class ApprovalQueue {
2
+ queue = [];
3
+ seq = 0;
4
+ /**
5
+ * A job submits a gated action and awaits the decision. Resolves when the
6
+ * foreground services it ({@link serviceAll}) or a caller {@link withdraw}s it;
7
+ * rejects if the finalize throws. The action does NOT execute here — the caller
8
+ * only proceeds after this resolves `{allow:true}`.
9
+ */
10
+ submit(sub) {
11
+ return new Promise((resolve, reject) => {
12
+ this.queue.push({
13
+ id: `pa-${++this.seq}`,
14
+ jobId: sub.jobId,
15
+ summary: sub.summary,
16
+ tier: sub.tier,
17
+ finalize: sub.finalize,
18
+ resolve,
19
+ reject,
20
+ settled: false,
21
+ });
22
+ });
23
+ }
24
+ /** Whether any request is waiting to be serviced. */
25
+ hasPending() {
26
+ return this.queue.length > 0;
27
+ }
28
+ /** Count of requests waiting. */
29
+ get size() {
30
+ return this.queue.length;
31
+ }
32
+ /** Read-only snapshot of every pending request, FIFO order. */
33
+ pending() {
34
+ return this.queue.map((e) => ({
35
+ id: e.id,
36
+ jobId: e.jobId,
37
+ summary: e.summary,
38
+ tier: e.tier,
39
+ }));
40
+ }
41
+ /** The pending request for one job (a job has at most one at a time), if any. */
42
+ pendingFor(jobId) {
43
+ return this.pending().find((p) => p.jobId === jobId);
44
+ }
45
+ /**
46
+ * Foreground drain: service every currently-pending request, FIFO, ONE AT A
47
+ * TIME — each `finalize` fully settles (including the human's keypress) before
48
+ * the next begins, so two background prompts never overlap and the shared mutex
49
+ * inside `finalize` also serializes them against any foreground action. Returns
50
+ * the number serviced. Requests that arrive AFTER draining starts wait for the
51
+ * next drain (bounded work per idle window). A finalize throw rejects that one
52
+ * producer and drains on — one job's checkpoint failure never wedges the queue.
53
+ */
54
+ async serviceAll() {
55
+ let serviced = 0;
56
+ for (;;) {
57
+ const entry = this.queue.shift();
58
+ if (!entry)
59
+ break;
60
+ if (entry.settled)
61
+ continue; // withdrawn between snapshot and here
62
+ try {
63
+ const decision = await entry.finalize();
64
+ this.settle(entry, () => entry.resolve(decision));
65
+ }
66
+ catch (err) {
67
+ this.settle(entry, () => entry.reject(err));
68
+ }
69
+ serviced++;
70
+ }
71
+ return serviced;
72
+ }
73
+ /**
74
+ * Withdraw a job's pending request WITHOUT prompting — used when the job is
75
+ * cancelled (or the session exits) while it is paused: settle the producer with
76
+ * `decision` (a deny) so its `submit` promise resolves and the job can finish
77
+ * tearing down instead of blocking forever. Returns true if one was withdrawn.
78
+ */
79
+ withdraw(jobId, decision) {
80
+ const entry = this.queue.find((e) => e.jobId === jobId && !e.settled);
81
+ if (!entry)
82
+ return false;
83
+ const i = this.queue.indexOf(entry);
84
+ if (i !== -1)
85
+ this.queue.splice(i, 1);
86
+ this.settle(entry, () => entry.resolve(decision));
87
+ return true;
88
+ }
89
+ /** Settle an entry exactly once (double-settle is a defensive no-op). */
90
+ settle(entry, run) {
91
+ if (entry.settled)
92
+ return;
93
+ entry.settled = true;
94
+ run();
95
+ }
96
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "../tools/index.js";
3
+ import type { JobManager } from "./manager.js";
4
+ /**
5
+ * The `run_in_background` tool (C.28): the agent-facing seam for dispatching a
6
+ * background job. Unlike `spawn_subagent`, it does NOT block — it returns a job
7
+ * id immediately and the job runs CONCURRENTLY with the rest of the turn and the
8
+ * session. The agent uses it for long-running, self-contained work it does not
9
+ * want to wait on inline; the human watches it with `/jobs` and `/logs`, and any
10
+ * side effect the job takes still requires a foreground approval.
11
+ */
12
+ export declare const RUN_IN_BACKGROUND_TOOL_NAME = "run_in_background";
13
+ declare const parameters: z.ZodObject<{
14
+ task: z.ZodString;
15
+ tools: z.ZodOptional<z.ZodArray<z.ZodString, "atleastone">>;
16
+ root: z.ZodOptional<z.ZodString>;
17
+ maxIterations: z.ZodOptional<z.ZodNumber>;
18
+ maxTokens: z.ZodOptional<z.ZodNumber>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ task: string;
21
+ root?: string | undefined;
22
+ maxTokens?: number | undefined;
23
+ maxIterations?: number | undefined;
24
+ tools?: [string, ...string[]] | undefined;
25
+ }, {
26
+ task: string;
27
+ root?: string | undefined;
28
+ maxTokens?: number | undefined;
29
+ maxIterations?: number | undefined;
30
+ tools?: [string, ...string[]] | undefined;
31
+ }>;
32
+ /** Build the `run_in_background` tool bound to the session's job manager. */
33
+ export declare function makeRunInBackgroundTool(manager: JobManager): Tool<typeof parameters>;
34
+ export {};
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { CruxyError } from "../errors/index.js";
3
+ /**
4
+ * The `run_in_background` tool (C.28): the agent-facing seam for dispatching a
5
+ * background job. Unlike `spawn_subagent`, it does NOT block — it returns a job
6
+ * id immediately and the job runs CONCURRENTLY with the rest of the turn and the
7
+ * session. The agent uses it for long-running, self-contained work it does not
8
+ * want to wait on inline; the human watches it with `/jobs` and `/logs`, and any
9
+ * side effect the job takes still requires a foreground approval.
10
+ */
11
+ export const RUN_IN_BACKGROUND_TOOL_NAME = "run_in_background";
12
+ const parameters = z.object({
13
+ task: z
14
+ .string()
15
+ .min(1)
16
+ .describe("The complete, self-contained task for the background job. It starts with " +
17
+ "NO context beyond this text — include every path, constraint, and expected " +
18
+ "output. The job runs concurrently; you get a job id back immediately."),
19
+ tools: z
20
+ .array(z.string().min(1))
21
+ .nonempty()
22
+ .optional()
23
+ .describe("Tool names to grant the job — a subset of your own. Omit for the default " +
24
+ "read-only set. Grant write/shell tools only when the job needs them; every " +
25
+ "side effect still pauses for a foreground human approval."),
26
+ root: z
27
+ .string()
28
+ .min(1)
29
+ .optional()
30
+ .describe("Workspace root name (from your Environment) to confine the job's writes to. " +
31
+ "Omit for a read-only job or a single-root workspace."),
32
+ maxIterations: z
33
+ .number()
34
+ .int()
35
+ .positive()
36
+ .optional()
37
+ .describe("Cap on the job's model turns (clamped to the configured ceiling)."),
38
+ maxTokens: z
39
+ .number()
40
+ .int()
41
+ .positive()
42
+ .optional()
43
+ .describe("Cap on the job's total tokens (clamped to the configured ceiling)."),
44
+ });
45
+ /** Build the `run_in_background` tool bound to the session's job manager. */
46
+ export function makeRunInBackgroundTool(manager) {
47
+ return {
48
+ name: RUN_IN_BACKGROUND_TOOL_NAME,
49
+ description: "Dispatch a self-contained task to run as a BACKGROUND JOB, concurrently with " +
50
+ "the rest of this session. Returns a job id immediately (non-blocking) — the " +
51
+ "job runs on its own while you continue. Use it for long or independent work " +
52
+ "you don't need to wait on inline (e.g. \"run the full test suite and fix any " +
53
+ "failures\"). The job's output goes to a log (the user reads it with /logs); any " +
54
+ "file/shell action it takes pauses for the user's approval. For a subtask whose " +
55
+ "RESULT you need before continuing, use spawn_subagent (which blocks) instead.",
56
+ parameters,
57
+ async execute(input) {
58
+ const spec = {
59
+ task: input.task,
60
+ ...(input.tools ? { tools: input.tools } : {}),
61
+ ...(input.root !== undefined ? { root: input.root } : {}),
62
+ budget: {
63
+ ...(input.maxIterations !== undefined
64
+ ? { maxIterations: input.maxIterations }
65
+ : {}),
66
+ ...(input.maxTokens !== undefined
67
+ ? { maxTokens: input.maxTokens }
68
+ : {}),
69
+ },
70
+ };
71
+ let view;
72
+ try {
73
+ view = manager.dispatch(spec);
74
+ }
75
+ catch (err) {
76
+ // Disabled / job-limit / unknown-root are the model's to correct — feed
77
+ // the coded, actionable message back as a tool error, never a stack.
78
+ if (CruxyError.is(err)) {
79
+ return {
80
+ ok: false,
81
+ error: `${err.code}: ${err.title}${err.cause ? ` — ${err.cause}` : ""}`,
82
+ };
83
+ }
84
+ throw err;
85
+ }
86
+ return {
87
+ ok: true,
88
+ output: JSON.stringify({
89
+ jobId: view.id,
90
+ status: view.status,
91
+ note: "dispatched — runs in the background; check with /jobs and /logs",
92
+ }),
93
+ };
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./log-buffer.js";
3
+ export * from "./log-renderer.js";
4
+ export * from "./approval-queue.js";
5
+ export * from "./manager.js";
6
+ export * from "./dispatch-tool.js";
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./log-buffer.js";
3
+ export * from "./log-renderer.js";
4
+ export * from "./approval-queue.js";
5
+ export * from "./manager.js";
6
+ export * from "./dispatch-tool.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A bounded ring buffer of a background job's log lines (C.28). A job runs
3
+ * non-interactively, so its progress isn't on screen — `cruxy logs <id>` reads
4
+ * it back here. Bounded so a chatty job cannot grow memory without limit: once
5
+ * `capacity` lines are held, the oldest roll off. When anything is dropped the
6
+ * buffer records how many, so a reader is never misled into thinking a truncated
7
+ * tail is the whole story (`cruxy logs` prints an honest "… N earlier lines").
8
+ */
9
+ export interface LogLine {
10
+ /** Milliseconds since the job started (monotonic, injected — never wall clock
11
+ * inside the buffer, so it stays deterministic and testable). */
12
+ atMs: number;
13
+ /** `out` for normal progress, `err` for diagnostics/failures. */
14
+ stream: "out" | "err";
15
+ /** The line text (no trailing newline). */
16
+ text: string;
17
+ }
18
+ export declare class LogBuffer {
19
+ private readonly capacity;
20
+ private readonly lines;
21
+ private droppedCount;
22
+ constructor(capacity: number);
23
+ /** Append a line, rolling the oldest off (and counting it) when at capacity. */
24
+ append(line: LogLine): void;
25
+ /** The retained lines, oldest-first (a copy — callers never mutate the buffer). */
26
+ snapshot(): LogLine[];
27
+ /** How many lines rolled off the front (0 when nothing was dropped). */
28
+ get dropped(): number;
29
+ /** Lines currently retained. */
30
+ get size(): number;
31
+ }
@@ -0,0 +1,30 @@
1
+ export class LogBuffer {
2
+ capacity;
3
+ lines = [];
4
+ droppedCount = 0;
5
+ constructor(capacity) {
6
+ // A non-positive capacity would drop everything; clamp to at least 1 so a
7
+ // misconfigured buffer still shows the most recent line.
8
+ this.capacity = Math.max(1, Math.floor(capacity));
9
+ }
10
+ /** Append a line, rolling the oldest off (and counting it) when at capacity. */
11
+ append(line) {
12
+ this.lines.push(line);
13
+ while (this.lines.length > this.capacity) {
14
+ this.lines.shift();
15
+ this.droppedCount++;
16
+ }
17
+ }
18
+ /** The retained lines, oldest-first (a copy — callers never mutate the buffer). */
19
+ snapshot() {
20
+ return [...this.lines];
21
+ }
22
+ /** How many lines rolled off the front (0 when nothing was dropped). */
23
+ get dropped() {
24
+ return this.droppedCount;
25
+ }
26
+ /** Lines currently retained. */
27
+ get size() {
28
+ return this.lines.length;
29
+ }
30
+ }
@@ -0,0 +1,32 @@
1
+ import { type Theme } from "../theme/index.js";
2
+ import type { RenderCapabilities, StreamRenderer, ToolLifecycleEvent } from "../render/index.js";
3
+ /**
4
+ * A {@link StreamRenderer} for a background job (C.28) that captures activity into
5
+ * the job's log buffer and writes NOTHING to any terminal — a job runs
6
+ * non-interactively, off screen, and its foreground session owns the terminal.
7
+ * `cruxy logs <id>` (a REPL `/logs`) reads back what was captured here.
8
+ *
9
+ * Assistant text is accumulated and flushed a line at a time on `endSegment`;
10
+ * committed chrome notes and tool-call completions are captured verbatim. The
11
+ * live-region methods (status, phase, progress, prompt-resolved) are dropped —
12
+ * transient decor has no meaning in an append-only log.
13
+ */
14
+ export declare class JobLogRenderer implements StreamRenderer {
15
+ private readonly sink;
16
+ readonly caps: RenderCapabilities;
17
+ readonly theme: Theme;
18
+ private pending;
19
+ constructor(sink: (stream: "out" | "err", text: string) => void);
20
+ beginTurn(): void;
21
+ write(delta: string): void;
22
+ endSegment(): void;
23
+ note(text: string): void;
24
+ toolLifecycle(event: ToolLifecycleEvent): void;
25
+ endTurn(): void;
26
+ preview(): void;
27
+ status(): void;
28
+ setPhase(): void;
29
+ progress(): void;
30
+ promptResolved(): void;
31
+ close(): void;
32
+ }
@@ -0,0 +1,70 @@
1
+ import { themeForColor } from "../theme/index.js";
2
+ /** Non-terminal capabilities: a background job renders to NOTHING on screen. */
3
+ const OFFSCREEN_CAPS = {
4
+ tty: false,
5
+ color: false,
6
+ cursor: false,
7
+ spinner: false,
8
+ reducedMotion: true,
9
+ screenReader: false,
10
+ unicode: true,
11
+ width: 80,
12
+ };
13
+ /**
14
+ * A {@link StreamRenderer} for a background job (C.28) that captures activity into
15
+ * the job's log buffer and writes NOTHING to any terminal — a job runs
16
+ * non-interactively, off screen, and its foreground session owns the terminal.
17
+ * `cruxy logs <id>` (a REPL `/logs`) reads back what was captured here.
18
+ *
19
+ * Assistant text is accumulated and flushed a line at a time on `endSegment`;
20
+ * committed chrome notes and tool-call completions are captured verbatim. The
21
+ * live-region methods (status, phase, progress, prompt-resolved) are dropped —
22
+ * transient decor has no meaning in an append-only log.
23
+ */
24
+ export class JobLogRenderer {
25
+ sink;
26
+ caps = OFFSCREEN_CAPS;
27
+ theme = themeForColor(false);
28
+ pending = "";
29
+ constructor(sink) {
30
+ this.sink = sink;
31
+ }
32
+ beginTurn() {
33
+ this.pending = "";
34
+ }
35
+ write(delta) {
36
+ this.pending += delta;
37
+ // Flush any complete lines immediately; keep the trailing partial line.
38
+ const nl = this.pending.lastIndexOf("\n");
39
+ if (nl === -1)
40
+ return;
41
+ const complete = this.pending.slice(0, nl);
42
+ this.pending = this.pending.slice(nl + 1);
43
+ for (const line of complete.split("\n"))
44
+ this.sink("out", line);
45
+ }
46
+ endSegment() {
47
+ if (this.pending.trim())
48
+ this.sink("out", this.pending.trimEnd());
49
+ this.pending = "";
50
+ }
51
+ note(text) {
52
+ this.sink("out", text);
53
+ }
54
+ toolLifecycle(event) {
55
+ if (event.event !== "end")
56
+ return; // only the committed outcome is log-worthy
57
+ const mark = event.ok ? "ok" : "fail";
58
+ this.sink(event.ok ? "out" : "err", `[${mark}] ${event.label}`);
59
+ }
60
+ endTurn() {
61
+ this.endSegment();
62
+ }
63
+ // Transient decor / previews have no place in an append-only job log.
64
+ preview() { }
65
+ status() { }
66
+ setPhase() { }
67
+ progress() { }
68
+ promptResolved() { }
69
+ close() { }
70
+ }
@@ -0,0 +1,139 @@
1
+ import type { Provider } from "@cruxy/sdk";
2
+ import { runAgent } from "../agent/loop.js";
3
+ import { type ApprovalMutex, type PromptIO } from "../approval/index.js";
4
+ import type { CruxyConfig } from "../config/index.js";
5
+ import type { Router } from "../routing/index.js";
6
+ import type { SandboxService } from "../sandbox/index.js";
7
+ import { Semaphore } from "../subagent/index.js";
8
+ import type { ToolRegistry } from "../tools/index.js";
9
+ import type { logger as Logger } from "../utils/logger.js";
10
+ import { Workspace } from "../workspace/index.js";
11
+ import { ApprovalQueue } from "./approval-queue.js";
12
+ import type { JobLog, JobSpec, JobView } from "./types.js";
13
+ /**
14
+ * Everything the manager needs from the surrounding session, injected by the
15
+ * session factory. The SHARED members are the crux of C.28's "one system, not
16
+ * two": `semaphore`, `approvalQueue`, and `approvalMutex` are the SAME instances
17
+ * the foreground loop and subagents use, so a job contends for the one execution
18
+ * cap and serializes its prompts/checkpoints on the one mutex — never a parallel
19
+ * universe with its own limits.
20
+ */
21
+ export interface JobManagerDeps {
22
+ config: CruxyConfig;
23
+ provider: Provider;
24
+ router?: Router;
25
+ /** The parent's registry — the ceiling every job's tool scope derives from. */
26
+ parentRegistry: ToolRegistry;
27
+ cwd: string;
28
+ workspace: Workspace;
29
+ logger: typeof Logger;
30
+ git?: {
31
+ branch: string;
32
+ dirty: boolean;
33
+ } | null;
34
+ projectInstructions?: string | null;
35
+ sandbox?: SandboxService;
36
+ /** The ONE execution semaphore, shared with subagents (JC-D, the shared cap). */
37
+ semaphore: Semaphore;
38
+ /** The ONE foreground pending-approval queue jobs produce onto. */
39
+ approvalQueue: ApprovalQueue;
40
+ /** The ONE approval mutex — job prompts/checkpoints serialize on it. */
41
+ approvalMutex: ApprovalMutex;
42
+ /**
43
+ * Whether an interactive foreground exists to service the queue. FALSE (a
44
+ * one-shot / piped run) means a job's gated action fails loud with
45
+ * `CRUXY_E_APPROVAL_REQUIRED` — it never auto-approves and never hangs on a
46
+ * human who will never come.
47
+ */
48
+ foregroundInteractive: boolean;
49
+ /** The shared prompt I/O used when the foreground services a job's request. */
50
+ promptIO: PromptIO;
51
+ /** Test seam: the loop driver. Defaults to the real {@link runAgent}. */
52
+ runAgentFn?: typeof runAgent;
53
+ /** Test seam: deterministic job ids. Defaults to `job-<seq>-<rand>`. */
54
+ idFactory?: () => string;
55
+ /** Test seam: log-line clock (ms). Defaults to `Date.now`. */
56
+ now?: () => number;
57
+ }
58
+ /**
59
+ * Session-scoped background jobs (C.28). The main agent dispatches a job with
60
+ * `run_in_background`; it runs the SAME agent loop as a subagent but CONCURRENTLY
61
+ * with the foreground session, off screen (its output goes to a log buffer). A
62
+ * gated action pauses the job, releases its execution slot, and enqueues an
63
+ * approval request the foreground human services between turns; on approval the
64
+ * job re-acquires a slot (with priority) and runs on — its pre- AND post-pause
65
+ * mutations coalescing under ONE checkpoint. Cancel and session exit kill-tree a
66
+ * job's process tree (via its cancellation signal), leaving no orphan.
67
+ *
68
+ * The manager is session-scoped and dies with the session (NOT a daemon): nothing
69
+ * here persists a live registry across processes. What survives on disk is a
70
+ * job's checkpoint set, so `cruxy rollback <id>` still works afterwards.
71
+ */
72
+ export declare class JobManager {
73
+ private readonly deps;
74
+ private readonly jobs;
75
+ /** In-flight executions, awaited on {@link cancelAll} so teardown completes. */
76
+ private readonly running;
77
+ private readonly idFactory;
78
+ private readonly now;
79
+ private readonly runAgentFn;
80
+ private seq;
81
+ constructor(deps: JobManagerDeps);
82
+ /**
83
+ * Dispatch a background job (the `run_in_background` seam). Fails loud when the
84
+ * feature is disabled, when the live-job ceiling is reached, or when a named
85
+ * root is unknown — all BEFORE the job is registered, so a refused dispatch
86
+ * leaves no ghost. Returns a view immediately; the job runs asynchronously.
87
+ */
88
+ dispatch(spec: JobSpec): JobView;
89
+ /** Live view of every job this session, dispatch order. */
90
+ list(): JobView[];
91
+ /** One job's view, or undefined if the id is unknown. */
92
+ get(id: string): JobView | undefined;
93
+ /** One job's full log (for `/logs <id>`). Fails loud on an unknown id. */
94
+ logs(id: string): JobLog;
95
+ /**
96
+ * Cancel one job: abort its run (kill-tree'ing any in-flight process via the
97
+ * signal reaching `ctx.signal`) and, if it is paused, withdraw its queued
98
+ * approval so it stops waiting. A checkpoint it already took SURVIVES for
99
+ * review/rollback. Fails loud on an unknown id; a no-op on an already-terminal
100
+ * job (returns false).
101
+ */
102
+ cancel(id: string): boolean;
103
+ /**
104
+ * Cancel EVERY live job (session exit / teardown) and AWAIT their teardown, so
105
+ * nothing is left running or orphaned. Returns how many were cancelled — the
106
+ * caller prints the honest "N job(s) cancelled". Idempotent.
107
+ */
108
+ cancelAll(reason: string): Promise<number>;
109
+ /** Jobs that are not yet terminal (queued + running + paused). */
110
+ liveCount(): number;
111
+ /** Whether any job is paused awaiting a foreground approval (drive the drain). */
112
+ hasPendingApprovals(): boolean;
113
+ /** Service every pending job approval on the foreground (called between turns). */
114
+ serviceApprovals(): Promise<number>;
115
+ /** Abort a job's run and unblock it if paused (shared by cancel/cancelAll). */
116
+ private abort;
117
+ /** Drive one job to completion, mapping every outcome onto its status. */
118
+ private execute;
119
+ /** Build the {@link runAgent} args for a job: scoped registry, budget, ctx. */
120
+ private runArgs;
121
+ /**
122
+ * The job's approval gate — the producer side of the pending-approval queue.
123
+ * A read-tier action passes freely. A mutate/destructive action with NO
124
+ * interactive foreground fails loud (never auto-approve, never hang). Otherwise
125
+ * the job PAUSES: it releases its execution slot (JC-D), enqueues the request
126
+ * for the foreground human, and blocks. On resolution it re-acquires a slot
127
+ * with PRIORITY (so it is not starved by work dispatched while it waited) and
128
+ * returns the decision. The real prompt + this job's checkpoint snapshot happen
129
+ * inside `finalize` on the foreground, serialized on the shared mutex.
130
+ */
131
+ private makeJobApproval;
132
+ /** Resolve a job's scope from an optional root name (fail-loud on unknown). */
133
+ private jobScope;
134
+ private statusFor;
135
+ private errorFor;
136
+ private requireJob;
137
+ private log;
138
+ private view;
139
+ }