@aragon-agent/cli 0.6.2 → 0.6.4

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 (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Is this command one that does not exit on its own?
3
+ * (background-service-supervision §3.5.)
4
+ *
5
+ * PURE. No process, no filesystem, no clock — so the whole table below is
6
+ * enumerable by a test and readable by a reviewer in one screen.
7
+ *
8
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
9
+ *
10
+ * A CONSERVATIVE ALLOWLIST OVER THE LEADING TOKEN AND SCRIPT NAME, NEVER A VERB
11
+ * HEURISTIC (R-1). The cost of a false positive is real: a build that is
12
+ * backgrounded returns before it finished, and the model reads a half-empty log
13
+ * as success. So `npm run build` and `npm test` must not match, and a chained
14
+ * command must not match at all — a chain's TAIL is what the model actually
15
+ * wants the result of.
16
+ */
17
+ /**
18
+ * The classifier's table, exported so `classify.test.ts` can enumerate it and a
19
+ * reviewer can read it whole.
20
+ *
21
+ * Each pattern is anchored at the start of the (trimmed, env-prefix-stripped)
22
+ * command. `\b`-style boundaries are spelled explicitly because a bare `vite`
23
+ * must not match `vitest`.
24
+ */
25
+ export declare const LONG_RUNNING_PATTERNS: readonly RegExp[];
26
+ /**
27
+ * Does the command contain a TOP-LEVEL `&&`, `||`, `;` or `|`?
28
+ *
29
+ * "Top level" means outside quotes: `echo "a && b"` is one command. Anything
30
+ * quoted is skipped, which is enough for the judgement being made here — this
31
+ * decides whether to REFUSE to background, so an over-eager reading is the safe
32
+ * direction.
33
+ */
34
+ export declare function hasTopLevelChain(command: string): boolean;
35
+ /**
36
+ * Would this command sit there rather than exiting?
37
+ *
38
+ * FALSE FOR ANYTHING CHAINED, unconditionally and before the table is consulted.
39
+ */
40
+ export declare function looksLongRunning(command: string): boolean;
41
+ /**
42
+ * The port the command asks for, when it names one.
43
+ *
44
+ * Used ONLY to enable the probe detector, so a miss is free (the URL detector
45
+ * still runs) and a wrong hit is bounded by the pre-flight probe: a port that
46
+ * already answers before we spawn disables the detector entirely rather than
47
+ * declaring every launch instantly ready.
48
+ *
49
+ * `--port N`, `--port=N`, `-p N`, `PORT=N`, and a `:N` suffix on a host in the
50
+ * command text (`php -S 127.0.0.1:8080`).
51
+ */
52
+ export declare function extractPortHint(command: string): number | undefined;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Is this command one that does not exit on its own?
3
+ * (background-service-supervision §3.5.)
4
+ *
5
+ * PURE. No process, no filesystem, no clock — so the whole table below is
6
+ * enumerable by a test and readable by a reviewer in one screen.
7
+ *
8
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
9
+ *
10
+ * A CONSERVATIVE ALLOWLIST OVER THE LEADING TOKEN AND SCRIPT NAME, NEVER A VERB
11
+ * HEURISTIC (R-1). The cost of a false positive is real: a build that is
12
+ * backgrounded returns before it finished, and the model reads a half-empty log
13
+ * as success. So `npm run build` and `npm test` must not match, and a chained
14
+ * command must not match at all — a chain's TAIL is what the model actually
15
+ * wants the result of.
16
+ */
17
+ /**
18
+ * The classifier's table, exported so `classify.test.ts` can enumerate it and a
19
+ * reviewer can read it whole.
20
+ *
21
+ * Each pattern is anchored at the start of the (trimmed, env-prefix-stripped)
22
+ * command. `\b`-style boundaries are spelled explicitly because a bare `vite`
23
+ * must not match `vitest`.
24
+ */
25
+ export const LONG_RUNNING_PATTERNS = Object.freeze([
26
+ // Package-manager scripts whose NAME says "does not exit".
27
+ /^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|watch|preview)(?:\s|$)/i,
28
+ // Node/JS dev servers and bundlers in watch mode.
29
+ /^next\s+(?:dev|start)(?:\s|$)/i,
30
+ /^vite(?:\s|$)/i,
31
+ /^nuxt\s+dev(?:\s|$)/i,
32
+ /^astro\s+dev(?:\s|$)/i,
33
+ /^remix\s+dev(?:\s|$)/i,
34
+ /^ng\s+serve(?:\s|$)/i,
35
+ /^webpack\s+serve(?:\s|$)/i,
36
+ /^nodemon(?:\s|$)/i,
37
+ /^tsc\s+(?:--watch|-w)(?:\s|$)/i,
38
+ /^(?:serve|http-server)(?:\s|$)/i,
39
+ // Python.
40
+ /^uvicorn(?:\s|$)/i,
41
+ /^gunicorn(?:\s|$)/i,
42
+ /^hypercorn(?:\s|$)/i,
43
+ /^flask\s+run(?:\s|$)/i,
44
+ /^python[0-9.]*\s+(?:\S*[\\/])?manage\.py\s+runserver(?:\s|$)/i,
45
+ /^python[0-9.]*\s+-m\s+http\.server(?:\s|$)/i,
46
+ // Ruby / PHP / Rust.
47
+ /^rails\s+(?:server|s)(?:\s|$)/i,
48
+ /^php\s+-S(?:\s|$)/i,
49
+ /^cargo\s+watch(?:\s|$)/i,
50
+ // Containers. `up` WITHOUT `-d` only; the `-d` form is checked below.
51
+ /^docker(?:\s+compose|-compose)\s+up(?:\s|$)/i,
52
+ // Plain watchers.
53
+ /^tail\s+-f(?:\s|$)/i,
54
+ /^watch\s+/i,
55
+ ]);
56
+ /** `docker compose up -d` returns on its own, so it is a foreground command. */
57
+ const DOCKER_DETACHED = /(?:^|\s)(?:-d|--detach)(?:\s|$)/i;
58
+ /**
59
+ * A leading `KEY=value ` run of assignments, which POSIX shells accept and which
60
+ * would otherwise hide the real leading token (`PORT=5173 vite`).
61
+ */
62
+ const ENV_PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/;
63
+ /** Strip a leading env-assignment run, so the table can anchor on the command. */
64
+ function stripEnvPrefix(command) {
65
+ return command.replace(ENV_PREFIX, '');
66
+ }
67
+ /**
68
+ * Does the command contain a TOP-LEVEL `&&`, `||`, `;` or `|`?
69
+ *
70
+ * "Top level" means outside quotes: `echo "a && b"` is one command. Anything
71
+ * quoted is skipped, which is enough for the judgement being made here — this
72
+ * decides whether to REFUSE to background, so an over-eager reading is the safe
73
+ * direction.
74
+ */
75
+ export function hasTopLevelChain(command) {
76
+ let quote = null;
77
+ for (let i = 0; i < command.length; i += 1) {
78
+ const ch = command[i];
79
+ if (quote) {
80
+ if (ch === quote)
81
+ quote = null;
82
+ continue;
83
+ }
84
+ if (ch === '"' || ch === "'") {
85
+ quote = ch;
86
+ continue;
87
+ }
88
+ if (ch === ';')
89
+ return true;
90
+ // A lone `|` is a pipe and `||` is an or; both disqualify. So does `&`, in
91
+ // BOTH of its readings: `&&` is a chain, and a trailing single `&` is the
92
+ // manual-detach the prompt block bans - whatever it backgrounds, we cannot
93
+ // supervise. One branch each, because the two `&` cases have one answer.
94
+ if (ch === '|' || ch === '&')
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ /**
100
+ * Would this command sit there rather than exiting?
101
+ *
102
+ * FALSE FOR ANYTHING CHAINED, unconditionally and before the table is consulted.
103
+ */
104
+ export function looksLongRunning(command) {
105
+ const trimmed = command.trim();
106
+ if (trimmed.length === 0)
107
+ return false;
108
+ if (hasTopLevelChain(trimmed))
109
+ return false;
110
+ const bare = stripEnvPrefix(trimmed);
111
+ if (/^docker(?:\s+compose|-compose)\s+up(?:\s|$)/i.test(bare) && DOCKER_DETACHED.test(bare)) {
112
+ return false;
113
+ }
114
+ return LONG_RUNNING_PATTERNS.some((re) => re.test(bare));
115
+ }
116
+ /**
117
+ * The port the command asks for, when it names one.
118
+ *
119
+ * Used ONLY to enable the probe detector, so a miss is free (the URL detector
120
+ * still runs) and a wrong hit is bounded by the pre-flight probe: a port that
121
+ * already answers before we spawn disables the detector entirely rather than
122
+ * declaring every launch instantly ready.
123
+ *
124
+ * `--port N`, `--port=N`, `-p N`, `PORT=N`, and a `:N` suffix on a host in the
125
+ * command text (`php -S 127.0.0.1:8080`).
126
+ */
127
+ export function extractPortHint(command) {
128
+ const candidates = [
129
+ /(?:^|\s)--port[=\s]+(\d{1,5})(?:\s|$)/i,
130
+ /(?:^|\s)-p[=\s]+(\d{1,5})(?:\s|$)/,
131
+ /(?:^|\s)PORT=(\d{1,5})(?:\s|$)/,
132
+ /(?:^|\s)\S*?:(\d{2,5})(?:\s|$)/,
133
+ ];
134
+ for (const re of candidates) {
135
+ const m = re.exec(command);
136
+ if (!m || !m[1])
137
+ continue;
138
+ const port = Number.parseInt(m[1], 10);
139
+ if (Number.isFinite(port) && port > 0 && port <= 65535)
140
+ return port;
141
+ }
142
+ return undefined;
143
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Killing a process TREE, on both platform families
3
+ * (background-service-supervision §3.5 / P1-1 / P1-2).
4
+ *
5
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ONE OWNER FOR BOTH KINDS OF CHILD. This was `bash-tool.ts`'s private helper;
8
+ * hoisting it is what lets a foreground `bash` child and a supervised service be
9
+ * reaped by the same code, which is the only way "no service outlives the CLI"
10
+ * can be true on every exit path.
11
+ *
12
+ * THE POSIX BRANCH IS A PROCESS-GROUP KILL, NOT `child.kill()` (P1-1). With
13
+ * `shell: true` the direct child is the shell wrapper, and `npm run dev` re-execs
14
+ * into node children that survive a signal aimed at the shell alone — which is
15
+ * why every `npm run dev` grandchild used to survive today's `killTree` on
16
+ * POSIX. `process.kill(-pid, sig)` reaches the whole group, and a group only
17
+ * EXISTS because `supervisor.ts` spawns with `detached: true` there. On Windows
18
+ * the flag is irrelevant to reaping: `taskkill /t` walks the parent-child table.
19
+ *
20
+ * THE SYNC VARIANT EXISTS BECAUSE `spawn` IS ASYNCHRONOUS (P1-2). A process that
21
+ * is already exiting will not live long enough for an async `taskkill`, and
22
+ * `process.on('exit')` permits only synchronous work — so the reaper an exit hook
23
+ * calls must be `spawnSync` end to end.
24
+ */
25
+ /** Is the pid still there? `signal 0` tests for existence without delivering. */
26
+ export declare function isAlive(pid: number | undefined): boolean;
27
+ /**
28
+ * Kill the tree rooted at `pid`, asynchronously. Best-effort; never throws.
29
+ *
30
+ * `signal` is honoured on POSIX (so a graceful `SIGTERM` -> `SIGKILL` ladder is
31
+ * possible); Windows `taskkill /f` has no gentle mode, so a `SIGTERM` there is a
32
+ * plain `taskkill` without `/f` and the escalation adds it.
33
+ */
34
+ export declare function killTree(pid: number | undefined, signal?: NodeJS.Signals): void;
35
+ /**
36
+ * Kill the tree rooted at `pid`, SYNCHRONOUSLY. Never throws, never awaits.
37
+ *
38
+ * The only kill primitive a signal handler, `process.on('exit')` or
39
+ * `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
40
+ * that can block or throw is worse than one that misses a child.
41
+ */
42
+ export declare function killTreeSync(pid: number | undefined): void;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Killing a process TREE, on both platform families
3
+ * (background-service-supervision §3.5 / P1-1 / P1-2).
4
+ *
5
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ONE OWNER FOR BOTH KINDS OF CHILD. This was `bash-tool.ts`'s private helper;
8
+ * hoisting it is what lets a foreground `bash` child and a supervised service be
9
+ * reaped by the same code, which is the only way "no service outlives the CLI"
10
+ * can be true on every exit path.
11
+ *
12
+ * THE POSIX BRANCH IS A PROCESS-GROUP KILL, NOT `child.kill()` (P1-1). With
13
+ * `shell: true` the direct child is the shell wrapper, and `npm run dev` re-execs
14
+ * into node children that survive a signal aimed at the shell alone — which is
15
+ * why every `npm run dev` grandchild used to survive today's `killTree` on
16
+ * POSIX. `process.kill(-pid, sig)` reaches the whole group, and a group only
17
+ * EXISTS because `supervisor.ts` spawns with `detached: true` there. On Windows
18
+ * the flag is irrelevant to reaping: `taskkill /t` walks the parent-child table.
19
+ *
20
+ * THE SYNC VARIANT EXISTS BECAUSE `spawn` IS ASYNCHRONOUS (P1-2). A process that
21
+ * is already exiting will not live long enough for an async `taskkill`, and
22
+ * `process.on('exit')` permits only synchronous work — so the reaper an exit hook
23
+ * calls must be `spawnSync` end to end.
24
+ */
25
+ import { spawn, spawnSync } from 'node:child_process';
26
+ import process from 'node:process';
27
+ /** Is the pid still there? `signal 0` tests for existence without delivering. */
28
+ export function isAlive(pid) {
29
+ if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
30
+ return false;
31
+ try {
32
+ process.kill(pid, 0);
33
+ return true;
34
+ }
35
+ catch (err) {
36
+ // EPERM means it exists and belongs to somebody else, which for our purposes
37
+ // is still "alive" - reporting it dead would make the honest
38
+ // "may have left a detached child" warning silently unreachable.
39
+ return err?.code === 'EPERM';
40
+ }
41
+ }
42
+ /**
43
+ * Kill the tree rooted at `pid`, asynchronously. Best-effort; never throws.
44
+ *
45
+ * `signal` is honoured on POSIX (so a graceful `SIGTERM` -> `SIGKILL` ladder is
46
+ * possible); Windows `taskkill /f` has no gentle mode, so a `SIGTERM` there is a
47
+ * plain `taskkill` without `/f` and the escalation adds it.
48
+ */
49
+ export function killTree(pid, signal = 'SIGKILL') {
50
+ if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
51
+ return;
52
+ if (process.platform === 'win32') {
53
+ const args = ['/pid', String(pid), '/t'];
54
+ if (signal === 'SIGKILL')
55
+ args.push('/f');
56
+ try {
57
+ const child = spawn('taskkill', args, { windowsHide: true, stdio: 'ignore' });
58
+ // A reaper must never be the reason the CLI does not exit (I-3), and it
59
+ // must never crash it either: `taskkill` missing from PATH emits 'error'.
60
+ child.unref();
61
+ child.on('error', () => { });
62
+ }
63
+ catch {
64
+ /* ignore */
65
+ }
66
+ return;
67
+ }
68
+ // THE GROUP FIRST, THE PID SECOND. The group is what reaches the grandchild;
69
+ // the single-pid fallback covers a child that was NOT spawned detached (a
70
+ // foreground `bash` child), where `-pid` is not a group at all.
71
+ try {
72
+ process.kill(-pid, signal);
73
+ return;
74
+ }
75
+ catch {
76
+ /* fall through to the single-process attempt */
77
+ }
78
+ try {
79
+ process.kill(pid, signal);
80
+ }
81
+ catch {
82
+ /* already gone */
83
+ }
84
+ }
85
+ /**
86
+ * Kill the tree rooted at `pid`, SYNCHRONOUSLY. Never throws, never awaits.
87
+ *
88
+ * The only kill primitive a signal handler, `process.on('exit')` or
89
+ * `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
90
+ * that can block or throw is worse than one that misses a child.
91
+ */
92
+ export function killTreeSync(pid) {
93
+ if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
94
+ return;
95
+ if (process.platform === 'win32') {
96
+ try {
97
+ spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], {
98
+ windowsHide: true,
99
+ stdio: 'ignore',
100
+ timeout: 2000,
101
+ });
102
+ }
103
+ catch {
104
+ /* ignore */
105
+ }
106
+ return;
107
+ }
108
+ try {
109
+ process.kill(-pid, 'SIGKILL');
110
+ }
111
+ catch {
112
+ try {
113
+ process.kill(pid, 'SIGKILL');
114
+ }
115
+ catch {
116
+ /* already gone */
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * PROC_LIMITS — the single authority on every structural bound the process
3
+ * supervisor enforces (background-service-supervision §6).
4
+ *
5
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`), whose hardcoded directory regex this feature
7
+ * extends. A NEW TREE IS INVISIBLE TO THAT SCAN UNTIL IT IS NAMED THERE, and a
8
+ * guard rail that silently stops guarding is worse than no guard rail — this
9
+ * package has now paid for that edit eleven times.
10
+ *
11
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
12
+ * `TODO_LIMITS` and `TEAM_LIMITS` each record at length:
13
+ *
14
+ * - Everything below is STRUCTURAL: how many rows a tail can physically carry,
15
+ * how many children one session may hold, how long a kill may take before it
16
+ * stops being a kill. A user has no business tuning them, so they are
17
+ * constants.
18
+ * - The four `bash.*` config keys (`config/schema.ts`) are POLICY: register the
19
+ * tools at all, let the classifier fire, how long a launch may block the
20
+ * turn, how long to wait for readiness. They are clamped, persisted and
21
+ * user-facing.
22
+ */
23
+ export declare const PROC_LIMITS: {
24
+ /**
25
+ * Rows the per-service ring retains.
26
+ *
27
+ * DELIBERATELY A DIFFERENT NUMBER FROM `cardTailRows` BELOW, and from
28
+ * `LIVE_TAIL_ROWS` (8) in `tools/tool-output-store.ts`. They bound unrelated
29
+ * things — what `bash_output` can page back through, versus what one card
30
+ * draws — and a shared literal is how a later edit collapses two bounds into
31
+ * one.
32
+ */
33
+ readonly serviceTailRows: 200;
34
+ /** Rows a snapshot (and therefore a card) carries. */
35
+ readonly cardTailRows: 8;
36
+ /**
37
+ * Service RECORDS one session may hold.
38
+ *
39
+ * Eviction only ever removes a record in a TERMINAL state
40
+ * (`exited | failed | stopped`). When all 16 are live `start()` refuses and
41
+ * names the oldest ids rather than evicting: evicting a live record would
42
+ * orphan a running server that nothing can any longer name or stop.
43
+ */
44
+ readonly maxServices: 16;
45
+ /**
46
+ * How long `bash` waits after `'exit'` for the stdio streams to close before
47
+ * settling anyway.
48
+ *
49
+ * This number IS defence 1. `'close'` fires only once every stream is closed,
50
+ * and a detached grandchild that inherited the pipe keeps it open after the
51
+ * shell is long gone — so a tool that settles on `'close'` alone has a path
52
+ * that never settles at all.
53
+ */
54
+ readonly drainMs: 250;
55
+ /**
56
+ * How long an aborted/timed-out `bash` waits after the kill before settling
57
+ * WHETHER OR NOT the child ever reported exit.
58
+ *
59
+ * A pending promise after an abort is the bug; a possibly-premature result is
60
+ * not.
61
+ */
62
+ readonly killGraceMs: 2000;
63
+ /** Readiness port probe interval. */
64
+ readonly probeIntervalMs: 500;
65
+ /** Socket timeout for one readiness probe. */
66
+ readonly probeSocketTimeoutMs: 1000;
67
+ /**
68
+ * Output-event coalescing window inside the supervisor.
69
+ *
70
+ * At the SOURCE rather than in the view, because the same event is also
71
+ * consumed by `/bg` and by `bash_output`. A webpack build emitting thousands
72
+ * of lines a second must not become thousands of React renders.
73
+ */
74
+ readonly outputCoalesceMs: 100;
75
+ /** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
76
+ readonly stopGraceMs: 3000;
77
+ /** Below this many terminal columns the status chip degrades to `[N]`. */
78
+ readonly statusCompactCols: 92;
79
+ };
80
+ /**
81
+ * Bumped whenever the wording of the `<background_services>` block changes, so a
82
+ * behaviour report can be tied to a block revision with one grep.
83
+ */
84
+ export declare const BACKGROUND_SERVICES_BLOCK_VERSION = "v1";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * PROC_LIMITS — the single authority on every structural bound the process
3
+ * supervisor enforces (background-service-supervision §6).
4
+ *
5
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`), whose hardcoded directory regex this feature
7
+ * extends. A NEW TREE IS INVISIBLE TO THAT SCAN UNTIL IT IS NAMED THERE, and a
8
+ * guard rail that silently stops guarding is worse than no guard rail — this
9
+ * package has now paid for that edit eleven times.
10
+ *
11
+ * TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE — the distinction
12
+ * `TODO_LIMITS` and `TEAM_LIMITS` each record at length:
13
+ *
14
+ * - Everything below is STRUCTURAL: how many rows a tail can physically carry,
15
+ * how many children one session may hold, how long a kill may take before it
16
+ * stops being a kill. A user has no business tuning them, so they are
17
+ * constants.
18
+ * - The four `bash.*` config keys (`config/schema.ts`) are POLICY: register the
19
+ * tools at all, let the classifier fire, how long a launch may block the
20
+ * turn, how long to wait for readiness. They are clamped, persisted and
21
+ * user-facing.
22
+ */
23
+ export const PROC_LIMITS = {
24
+ /**
25
+ * Rows the per-service ring retains.
26
+ *
27
+ * DELIBERATELY A DIFFERENT NUMBER FROM `cardTailRows` BELOW, and from
28
+ * `LIVE_TAIL_ROWS` (8) in `tools/tool-output-store.ts`. They bound unrelated
29
+ * things — what `bash_output` can page back through, versus what one card
30
+ * draws — and a shared literal is how a later edit collapses two bounds into
31
+ * one.
32
+ */
33
+ serviceTailRows: 200,
34
+ /** Rows a snapshot (and therefore a card) carries. */
35
+ cardTailRows: 8,
36
+ /**
37
+ * Service RECORDS one session may hold.
38
+ *
39
+ * Eviction only ever removes a record in a TERMINAL state
40
+ * (`exited | failed | stopped`). When all 16 are live `start()` refuses and
41
+ * names the oldest ids rather than evicting: evicting a live record would
42
+ * orphan a running server that nothing can any longer name or stop.
43
+ */
44
+ maxServices: 16,
45
+ /**
46
+ * How long `bash` waits after `'exit'` for the stdio streams to close before
47
+ * settling anyway.
48
+ *
49
+ * This number IS defence 1. `'close'` fires only once every stream is closed,
50
+ * and a detached grandchild that inherited the pipe keeps it open after the
51
+ * shell is long gone — so a tool that settles on `'close'` alone has a path
52
+ * that never settles at all.
53
+ */
54
+ drainMs: 250,
55
+ /**
56
+ * How long an aborted/timed-out `bash` waits after the kill before settling
57
+ * WHETHER OR NOT the child ever reported exit.
58
+ *
59
+ * A pending promise after an abort is the bug; a possibly-premature result is
60
+ * not.
61
+ */
62
+ killGraceMs: 2000,
63
+ /** Readiness port probe interval. */
64
+ probeIntervalMs: 500,
65
+ /** Socket timeout for one readiness probe. */
66
+ probeSocketTimeoutMs: 1000,
67
+ /**
68
+ * Output-event coalescing window inside the supervisor.
69
+ *
70
+ * At the SOURCE rather than in the view, because the same event is also
71
+ * consumed by `/bg` and by `bash_output`. A webpack build emitting thousands
72
+ * of lines a second must not become thousands of React renders.
73
+ */
74
+ outputCoalesceMs: 100,
75
+ /** `SIGTERM` -> `SIGKILL` escalation window for a graceful stop. */
76
+ stopGraceMs: 3000,
77
+ /** Below this many terminal columns the status chip degrades to `[N]`. */
78
+ statusCompactCols: 92,
79
+ };
80
+ /**
81
+ * Bumped whenever the wording of the `<background_services>` block changes, so a
82
+ * behaviour report can be tied to a block revision with one grep.
83
+ */
84
+ export const BACKGROUND_SERVICES_BLOCK_VERSION = 'v1';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The bounded, sanitised output tail one supervised service keeps
3
+ * (background-service-supervision §4).
4
+ *
5
+ * ASCII ONLY: `src/proc/**` is inside the glyph scanner's scope.
6
+ *
7
+ * DELIBERATELY NOT `tools/tool-output-store.ts` (P2-4), which is the closest
8
+ * existing thing and would be the reflex merge. Three differences, each
9
+ * load-bearing:
10
+ *
11
+ * - that store keeps 8 rows, this keeps 200 — one is what a card DRAWS, the
12
+ * other is what `bash_output` can page back through;
13
+ * - that store is cleared at `tool_execution_end`, and a service outlives by
14
+ * minutes or hours the tool call that started it;
15
+ * - that store has no cursor, and paging is the whole point of `since`.
16
+ *
17
+ * It reuses `sanitizeChunk` for the same reason that store does — the contract
18
+ * is "terminal-safe rows", and no consumer can forget — and it follows the same
19
+ * "two bounds are two different literals" rule.
20
+ */
21
+ export interface LogRingPage {
22
+ rows: readonly string[];
23
+ /** Pass as the next `since`. */
24
+ cursor: number;
25
+ /** Rows between `since` and the first row returned were evicted. */
26
+ truncated: boolean;
27
+ }
28
+ /**
29
+ * A per-service ring.
30
+ *
31
+ * `rowsSeen` is the MONOTONIC count of completed rows ever appended, which is
32
+ * both the paging cursor and the transcript's revision term. It is deliberately
33
+ * not `rows.length`: the ring evicts while appending, so the length can be
34
+ * identical across two different tails.
35
+ */
36
+ export declare class LogRing {
37
+ private readonly capacity;
38
+ private readonly rows;
39
+ private carry;
40
+ private seen;
41
+ private evicted;
42
+ constructor(capacity?: number);
43
+ /**
44
+ * Append a raw chunk; returns the rows it completed.
45
+ *
46
+ * The IN-PROGRESS line is NOT a row and is not counted: a command whose output
47
+ * is one continuously-rewritten line (every progress bar there is) would
48
+ * otherwise burn the whole ring on one logical line. It is rendered through
49
+ * `toDisplayRow` by `tail()` instead, so the display still shows it.
50
+ */
51
+ append(chunk: string): readonly string[];
52
+ /** The last `n` display rows, in-progress line included when non-empty. */
53
+ tail(n: number): readonly string[];
54
+ /** Every completed row plus the in-progress line. */
55
+ all(): readonly string[];
56
+ /** Completed rows ever appended — the paging cursor and the revision term. */
57
+ get rowsSeen(): number;
58
+ /** The ring has dropped at least one row. */
59
+ get truncated(): boolean;
60
+ /**
61
+ * Rows after cursor `since`, or the whole tail when `since` is omitted.
62
+ *
63
+ * `truncated` reports that the cursor FELL OFF the ring — the caller asked for
64
+ * row 3 and the oldest row still held is 57 — which is a real loss the model
65
+ * is entitled to be told about rather than have silently papered over.
66
+ */
67
+ page(since?: number): LogRingPage;
68
+ }