@cruxy/cli 0.24.0 → 0.26.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 (62) hide show
  1. package/dist/agent/session.d.ts +13 -0
  2. package/dist/agent/session.js +6 -0
  3. package/dist/approval/prompt.d.ts +7 -1
  4. package/dist/approval/prompt.js +52 -17
  5. package/dist/checkpoint/gate-hook.d.ts +28 -0
  6. package/dist/checkpoint/gate-hook.js +98 -0
  7. package/dist/checkpoint/gate.d.ts +7 -1
  8. package/dist/checkpoint/gate.js +8 -2
  9. package/dist/checkpoint/index.d.ts +1 -0
  10. package/dist/checkpoint/index.js +1 -0
  11. package/dist/cli/commands/rollback.d.ts +4 -1
  12. package/dist/cli/commands/rollback.js +16 -9
  13. package/dist/cli/commands/run.js +12 -0
  14. package/dist/cli/commands/skills.js +10 -2
  15. package/dist/cli/repl.d.ts +1 -1
  16. package/dist/cli/repl.js +113 -1
  17. package/dist/cli/session-factory.d.ts +4 -12
  18. package/dist/cli/session-factory.js +50 -96
  19. package/dist/components/frame.d.ts +6 -3
  20. package/dist/components/frame.js +21 -23
  21. package/dist/components/fuzzy.js +5 -1
  22. package/dist/components/select.js +4 -1
  23. package/dist/config/schema.d.ts +86 -0
  24. package/dist/config/schema.js +41 -0
  25. package/dist/errors/constructors.d.ts +18 -0
  26. package/dist/errors/constructors.js +49 -0
  27. package/dist/errors/types.d.ts +13 -0
  28. package/dist/errors/types.js +21 -0
  29. package/dist/jobs/approval-queue.d.ts +85 -0
  30. package/dist/jobs/approval-queue.js +96 -0
  31. package/dist/jobs/dispatch-tool.d.ts +34 -0
  32. package/dist/jobs/dispatch-tool.js +96 -0
  33. package/dist/jobs/index.d.ts +6 -0
  34. package/dist/jobs/index.js +6 -0
  35. package/dist/jobs/log-buffer.d.ts +31 -0
  36. package/dist/jobs/log-buffer.js +30 -0
  37. package/dist/jobs/log-renderer.d.ts +32 -0
  38. package/dist/jobs/log-renderer.js +70 -0
  39. package/dist/jobs/manager.d.ts +139 -0
  40. package/dist/jobs/manager.js +397 -0
  41. package/dist/jobs/types.d.ts +81 -0
  42. package/dist/jobs/types.js +10 -0
  43. package/dist/render/capabilities.d.ts +11 -0
  44. package/dist/render/capabilities.js +19 -3
  45. package/dist/render/diff.d.ts +1 -1
  46. package/dist/render/diff.js +23 -7
  47. package/dist/render/index.d.ts +4 -2
  48. package/dist/render/index.js +8 -2
  49. package/dist/render/layout.d.ts +59 -0
  50. package/dist/render/layout.js +158 -0
  51. package/dist/render/resize.d.ts +36 -0
  52. package/dist/render/resize.js +45 -0
  53. package/dist/render/state.d.ts +13 -0
  54. package/dist/render/state.js +38 -0
  55. package/dist/render/tty-renderer.d.ts +8 -0
  56. package/dist/render/tty-renderer.js +36 -11
  57. package/dist/render/types.d.ts +15 -1
  58. package/dist/subagent/orchestrator.d.ts +9 -0
  59. package/dist/subagent/orchestrator.js +6 -1
  60. package/dist/subagent/semaphore.d.ts +40 -11
  61. package/dist/subagent/semaphore.js +23 -26
  62. package/package.json +1 -1
@@ -39,8 +39,22 @@ export interface RenderCapabilities {
39
39
  /** Unicode glyphs are safe (U.1) — false under `TERM=dumb` / `CRUXY_ASCII`;
40
40
  * independent of `color`. Drives the theme's glyph table, not its stylers. */
41
41
  unicode: boolean;
42
- /** Terminal columns; 80 when unknown (non-TTY). */
42
+ /**
43
+ * Terminal columns (U.12) — the ONE width source every surface reads; 80 when
44
+ * unknown (non-TTY / no `columns`). Honors `COLUMNS` when set. Mutated in
45
+ * place on resize (see {@link onResize}), so a surface reading it after a
46
+ * SIGWINCH sees the new width without re-probing anything.
47
+ */
43
48
  width: number;
49
+ /**
50
+ * Subscribe to width changes (U.12): the SIGWINCH push signal for the live
51
+ * region / transient frames to reflow at the new width. The listener fires
52
+ * with the new width AFTER {@link width} has been updated; the returned
53
+ * function unsubscribes. Absent when the stream cannot resize (non-TTY / no
54
+ * `columns`) — those surfaces just read the static {@link width}. Committed
55
+ * output is never re-rendered from here; only live surfaces subscribe.
56
+ */
57
+ onResize?(listener: (width: number) => void): () => void;
44
58
  }
45
59
  /** Accumulated token usage the loop already tracks (U.4) — never fabricated. */
46
60
  export interface TokenUsage {
@@ -6,6 +6,7 @@ import type { Router } from "../routing/index.js";
6
6
  import type { ApproveAction, ToolContext, ToolRegistry } from "../tools/index.js";
7
7
  import type { SandboxService } from "../sandbox/index.js";
8
8
  import { Workspace } from "../workspace/index.js";
9
+ import { Semaphore } from "./semaphore.js";
9
10
  import type { SubagentResult, SubagentSpec } from "./types.js";
10
11
  /**
11
12
  * Everything a spawn needs from the surrounding session, injected by the
@@ -60,6 +61,14 @@ export interface SubagentOrchestratorDeps {
60
61
  * same shared checkpoint gate, those writes join the run's one set (⚖︎JC-δ).
61
62
  */
62
63
  checkpointsActive?: boolean;
64
+ /**
65
+ * The ONE shared execution semaphore (C.28). When set, parallel fan-out draws
66
+ * permits from THIS instance — the same one background jobs use — so the
67
+ * `subagent.maxConcurrency` cap bounds subagents AND jobs COMBINED, never one
68
+ * cap each. Omitted → the orchestrator constructs its own (C.33 behaviour,
69
+ * used by tests that exercise the orchestrator in isolation).
70
+ */
71
+ executionSemaphore?: Semaphore;
63
72
  }
64
73
  /**
65
74
  * Spawns subagents (C.14): the existing agent loop re-driven over isolated
@@ -32,7 +32,12 @@ export class SubagentOrchestrator {
32
32
  sem;
33
33
  constructor(deps) {
34
34
  this.deps = deps;
35
- this.sem = new Semaphore(deps.config.subagent.maxConcurrency);
35
+ // Share the session's ONE execution semaphore when injected (C.28) so
36
+ // subagents and background jobs contend for the same permits; fall back to a
37
+ // private one at the same cap when running the orchestrator in isolation.
38
+ this.sem =
39
+ deps.executionSemaphore ??
40
+ new Semaphore(deps.config.subagent.maxConcurrency);
36
41
  }
37
42
  /** Live/queued fan-out slots (inspection/tests): proves the global cap holds. */
38
43
  get concurrency() {
@@ -1,27 +1,56 @@
1
1
  /**
2
- * A counting semaphore (C.33): bounds how many subagent runs execute at once.
2
+ * A counting semaphore (C.33): bounds how many agent runs execute at once.
3
3
  * FIFO — waiters are served in arrival order, so a fan-out's results stay
4
4
  * dispatch-order-fair — and the permit is handed directly from a releaser to
5
5
  * the next waiter, so the live count never transiently exceeds the cap.
6
6
  *
7
- * Used as the ONE shared bound on parallel fan-out. Parallel dispatch happens at
8
- * depth 0 only (the `spawn_subagents` tool is never granted to a child), and a
9
- * permit is held for a child's whole lifetime including any *sequential*
10
- * nested spawn beneath it, which is deliberately un-permitted. Because no permit
11
- * holder ever blocks trying to acquire a second permit, the semaphore cannot be
12
- * part of a wait cycle: it is deadlock-free by construction (see the C.33 design
13
- * doc's deadlock argument).
7
+ * Used as the ONE shared bound on all concurrent execution: parallel subagent
8
+ * fan-out (C.33) AND background jobs (C.28) draw permits from the SAME instance,
9
+ * so `subagent.maxConcurrency` caps their COMBINED live countnever one cap
10
+ * per subsystem. Parallel dispatch happens at depth 0 only (the `spawn_subagents`
11
+ * tool is never granted to a child), and a permit is held for a child's whole
12
+ * lifetime including any *sequential* nested spawn beneath it, which is
13
+ * deliberately un-permitted. Because no permit holder ever blocks trying to
14
+ * acquire a second permit, the semaphore cannot be part of a wait cycle: it is
15
+ * deadlock-free by construction (see the C.33 design doc's deadlock argument).
16
+ *
17
+ * C.28 adds a PRIORITY lane to {@link acquire}. A background job that pauses to
18
+ * await a human approval RELEASES its permit (JC-D: a job blocked on a human is
19
+ * not using compute, and holding the slot would deadlock the cap if N jobs all
20
+ * paused). When the human approves, the job re-acquires with `priority: true` so
21
+ * it jumps ahead of newly-dispatched work — a resumed job never starves behind a
22
+ * fresh fan-out that filled the queue while it waited. Normal (non-priority)
23
+ * waiters keep strict FIFO among themselves, so C.33's dispatch-order fairness is
24
+ * unchanged for everything that does not pause.
14
25
  */
26
+ export interface AcquireOptions {
27
+ /**
28
+ * Serve this waiter ahead of all non-priority waiters (still FIFO among
29
+ * priority waiters). Used by a resumed background job re-acquiring its slot
30
+ * after an approval, so it is not starved by work dispatched while it paused.
31
+ */
32
+ priority?: boolean;
33
+ }
15
34
  export declare class Semaphore {
16
35
  private permits;
17
- private readonly queue;
36
+ /** Waiters that take the next permit ahead of {@link normal} (FIFO within). */
37
+ private readonly priority;
38
+ /** Ordinary waiters, served strictly FIFO after any priority waiters. */
39
+ private readonly normal;
18
40
  constructor(permits: number);
19
41
  /** Run `fn` while holding one permit; the permit is released even if it throws. */
20
42
  run<T>(fn: () => Promise<T>): Promise<T>;
43
+ /**
44
+ * Take one permit, waiting if none is free. Resolves immediately when a permit
45
+ * is available; otherwise enqueues onto the priority or normal lane and resolves
46
+ * when {@link release} hands it a permit. Callers that acquire directly (a
47
+ * paused/resumed job) MUST call {@link release} exactly once per acquire.
48
+ */
49
+ acquire(opts?: AcquireOptions): Promise<void>;
50
+ /** Return one permit — handed straight to the next waiter (priority first). */
51
+ release(): void;
21
52
  /** Permits currently available (inspection/tests). */
22
53
  get available(): number;
23
54
  /** Callers currently blocked waiting for a permit (inspection/tests). */
24
55
  get waiting(): number;
25
- private acquire;
26
- private release;
27
56
  }
@@ -1,20 +1,9 @@
1
- /**
2
- * A counting semaphore (C.33): bounds how many subagent runs execute at once.
3
- * FIFO — waiters are served in arrival order, so a fan-out's results stay
4
- * dispatch-order-fair — and the permit is handed directly from a releaser to
5
- * the next waiter, so the live count never transiently exceeds the cap.
6
- *
7
- * Used as the ONE shared bound on parallel fan-out. Parallel dispatch happens at
8
- * depth 0 only (the `spawn_subagents` tool is never granted to a child), and a
9
- * permit is held for a child's whole lifetime — including any *sequential*
10
- * nested spawn beneath it, which is deliberately un-permitted. Because no permit
11
- * holder ever blocks trying to acquire a second permit, the semaphore cannot be
12
- * part of a wait cycle: it is deadlock-free by construction (see the C.33 design
13
- * doc's deadlock argument).
14
- */
15
1
  export class Semaphore {
16
2
  permits;
17
- queue = [];
3
+ /** Waiters that take the next permit ahead of {@link normal} (FIFO within). */
4
+ priority = [];
5
+ /** Ordinary waiters, served strictly FIFO after any priority waiters. */
6
+ normal = [];
18
7
  constructor(permits) {
19
8
  // A non-positive cap would wedge every run; clamp to at least 1.
20
9
  this.permits = Math.max(1, Math.floor(permits));
@@ -29,23 +18,23 @@ export class Semaphore {
29
18
  this.release();
30
19
  }
31
20
  }
32
- /** Permits currently available (inspection/tests). */
33
- get available() {
34
- return this.permits;
35
- }
36
- /** Callers currently blocked waiting for a permit (inspection/tests). */
37
- get waiting() {
38
- return this.queue.length;
39
- }
40
- acquire() {
21
+ /**
22
+ * Take one permit, waiting if none is free. Resolves immediately when a permit
23
+ * is available; otherwise enqueues onto the priority or normal lane and resolves
24
+ * when {@link release} hands it a permit. Callers that acquire directly (a
25
+ * paused/resumed job) MUST call {@link release} exactly once per acquire.
26
+ */
27
+ acquire(opts = {}) {
41
28
  if (this.permits > 0) {
42
29
  this.permits--;
43
30
  return Promise.resolve();
44
31
  }
45
- return new Promise((resolve) => this.queue.push(resolve));
32
+ const lane = opts.priority ? this.priority : this.normal;
33
+ return new Promise((resolve) => lane.push(resolve));
46
34
  }
35
+ /** Return one permit — handed straight to the next waiter (priority first). */
47
36
  release() {
48
- const next = this.queue.shift();
37
+ const next = this.priority.shift() ?? this.normal.shift();
49
38
  // Hand the permit straight to the next waiter (never bump the count above
50
39
  // the cap); only when nobody waits does the count grow back.
51
40
  if (next)
@@ -53,4 +42,12 @@ export class Semaphore {
53
42
  else
54
43
  this.permits++;
55
44
  }
45
+ /** Permits currently available (inspection/tests). */
46
+ get available() {
47
+ return this.permits;
48
+ }
49
+ /** Callers currently blocked waiting for a permit (inspection/tests). */
50
+ get waiting() {
51
+ return this.priority.length + this.normal.length;
52
+ }
56
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {