@phnx-labs/agents-cli 1.20.34 → 1.20.36

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 (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Autonomous PR lifecycle for teams (issue #338).
3
+ *
4
+ * A team's teammates open PRs. This module watches those PRs and reacts:
5
+ * - RED CI -> spawn a fresh fix wave (a teammate --after the one that failed,
6
+ * with the CI failure logs injected) that pushes a follow-up commit.
7
+ * - NEW REVIEW COMMENT -> route it to a `bugfix` teammate (--after the source),
8
+ * with the comment body injected.
9
+ *
10
+ * The DECISION logic here is a pure function (`decidePrActions`) over a snapshot
11
+ * of check results + review comments + a set of already-handled ids. It never
12
+ * touches the network — the poll-based collector (`pollPrSnapshot`, backed by the
13
+ * `gh` CLI) and the orchestrator (`runPrWatch`) sit on top and feed it data. That
14
+ * split keeps the spawn-or-not-spawn logic fully unit-testable (see
15
+ * pr-watch.test.ts) and dedupe-correct.
16
+ *
17
+ * Follow-up (deferred, not required here): replace the poll loop with the
18
+ * event-driven webhook receiver from #331 — `pollPrSnapshot` is the seam where a
19
+ * `check_run` / `pull_request_review_comment` webhook payload would plug in,
20
+ * producing the same PrSnapshot the pure decider already consumes.
21
+ */
22
+ import { execFile } from 'child_process';
23
+ import { promisify } from 'util';
24
+ const execFileAsync = promisify(execFile);
25
+ /**
26
+ * Default cap on how many fix/bugfix waves pr-watch spawns for a single PR before
27
+ * it gives up and escalates to a human. A persistently-RED PR re-runs CI on every
28
+ * follow-up commit; without a cap the loop would spawn teammates without bound
29
+ * (issue #338). Configurable via `--max-waves`.
30
+ */
31
+ export const DEFAULT_MAX_WAVES = 3;
32
+ /**
33
+ * Check states that count as a RED CI failure worth spawning a fix wave for.
34
+ * PENDING / SUCCESS / SKIPPING / NEUTRAL are deliberately excluded — a fix wave
35
+ * only fires on a genuine, terminal failure.
36
+ */
37
+ const FAILED_STATES = new Set([
38
+ 'FAILURE',
39
+ 'ERROR',
40
+ 'CANCELLED',
41
+ 'TIMED_OUT',
42
+ 'ACTION_REQUIRED',
43
+ 'STARTUP_FAILURE',
44
+ ]);
45
+ /** True when a check is in a terminal failing state. */
46
+ export function isFailedCheck(check) {
47
+ return FAILED_STATES.has((check.state || '').trim().toUpperCase());
48
+ }
49
+ /**
50
+ * Idempotency key for a failed check on a PR — dedupe by check NAME.
51
+ *
52
+ * The name is the ONLY identity that's stable across re-runs: when a fixer pushes
53
+ * a follow-up commit, GitHub creates a fresh workflow run with a new `link` URL but
54
+ * the SAME check name. Keying off `link` (as an earlier draft did) would treat every
55
+ * re-run as a brand-new failure and spawn an unbounded chain of fixers (issue #338).
56
+ * Keying off the name means a re-run of the same check is recognised as the same
57
+ * logical failure; the per-PR wave counter then bounds how many times we retry it.
58
+ */
59
+ export function checkDedupeKey(prUrl, check) {
60
+ return `ci:${prUrl}:${check.name}`;
61
+ }
62
+ /** Idempotency key for a review comment on a PR — dedupe by comment id. */
63
+ export function commentDedupeKey(prUrl, comment) {
64
+ return `review:${prUrl}:${comment.id}`;
65
+ }
66
+ /** Idempotency key for the one-shot "needs human" escalation on a PR. */
67
+ export function needsHumanKey(prUrl) {
68
+ return `needs-human:${prUrl}`;
69
+ }
70
+ /**
71
+ * The pure heart of pr-watch: given a PR snapshot, the set of dedupe keys already
72
+ * acted on, and how many fix waves each PR has already spent, decide which
73
+ * follow-up teammates to spawn. Emits:
74
+ * - one `ci-fix` action per NEW failed check (dedup by check NAME)
75
+ * - one `review-fix` action per NEW review comment (dedup by comment id)
76
+ * - a single `needs-human` action (per PR) once the wave budget is exhausted,
77
+ * INSTEAD of spawning — so a persistently-failing PR escalates rather than
78
+ * spawning teammates without bound (issue #338).
79
+ *
80
+ * `waves` maps a PR URL to the number of fix/bugfix teammates already spawned for
81
+ * it; once that reaches `maxWaves`, no further spawns are emitted for that PR.
82
+ * Both the CI-fix and comment-routing paths draw from the SAME per-PR budget, so
83
+ * neither can loop unboundedly.
84
+ *
85
+ * No network, no side effects, no reference to state beyond the passed
86
+ * `handled` / `waves` — so the same failure never spawns twice and the caller
87
+ * stays in control of when a wave is "spent".
88
+ */
89
+ export function decidePrActions(snapshot, handled, waves = new Map(), maxWaves = DEFAULT_MAX_WAVES) {
90
+ const actions = [];
91
+ const { prUrl } = snapshot;
92
+ // Waves already spent, plus the ones we're about to emit in THIS pass — so a
93
+ // single pass with several fresh failures can't blow past the budget.
94
+ let projected = waves.get(prUrl) ?? 0;
95
+ let escalated = false;
96
+ const escalate = (subject) => {
97
+ if (escalated)
98
+ return;
99
+ const dedupeKey = needsHumanKey(prUrl);
100
+ if (handled.has(dedupeKey))
101
+ return; // already escalated this PR — stay silent
102
+ escalated = true;
103
+ actions.push({
104
+ kind: 'needs-human',
105
+ prUrl,
106
+ sourceTeammate: snapshot.sourceTeammate,
107
+ subject,
108
+ waves: projected,
109
+ dedupeKey,
110
+ });
111
+ };
112
+ for (const check of snapshot.checks) {
113
+ if (!isFailedCheck(check))
114
+ continue;
115
+ const dedupeKey = checkDedupeKey(prUrl, check);
116
+ if (handled.has(dedupeKey))
117
+ continue;
118
+ if (projected >= maxWaves) {
119
+ escalate(`CI check "${check.name}"`);
120
+ continue;
121
+ }
122
+ projected++;
123
+ actions.push({
124
+ kind: 'ci-fix',
125
+ prUrl,
126
+ sourceTeammate: snapshot.sourceTeammate,
127
+ check,
128
+ dedupeKey,
129
+ wave: projected,
130
+ });
131
+ }
132
+ for (const comment of snapshot.comments) {
133
+ const dedupeKey = commentDedupeKey(prUrl, comment);
134
+ if (handled.has(dedupeKey))
135
+ continue;
136
+ if (projected >= maxWaves) {
137
+ escalate(`review comment #${comment.id}`);
138
+ continue;
139
+ }
140
+ projected++;
141
+ actions.push({
142
+ kind: 'review-fix',
143
+ prUrl,
144
+ sourceTeammate: snapshot.sourceTeammate,
145
+ comment,
146
+ dedupeKey,
147
+ wave: projected,
148
+ });
149
+ }
150
+ return actions;
151
+ }
152
+ /**
153
+ * Build the task prompt for a CI-fix teammate. Pure so it's unit-testable: the
154
+ * failing check + the fetched logs are injected as data.
155
+ */
156
+ export function buildCiFixPrompt(action, logs) {
157
+ const logsBlock = logs.trim()
158
+ ? `\n\nCI failure logs:\n\`\`\`\n${logs.trim()}\n\`\`\``
159
+ : `\n\n(No CI logs could be fetched — inspect the run at ${action.check.link ?? action.prUrl}.)`;
160
+ return (`CI is RED on PR ${action.prUrl}. The check "${action.check.name}"` +
161
+ (action.check.workflow ? ` (workflow: ${action.check.workflow})` : '') +
162
+ ` failed with state ${action.check.state}. ` +
163
+ `Diagnose the failure from the logs below, fix it, and push a follow-up commit to the SAME PR branch ` +
164
+ `(check out the PR branch with \`gh pr checkout ${action.prUrl}\`, make the fix, commit, and push). ` +
165
+ `Do not open a new PR — push to the existing branch so this PR goes green.` +
166
+ logsBlock);
167
+ }
168
+ /**
169
+ * Build the task prompt for a review-comment bugfix teammate. Pure so it's
170
+ * unit-testable: the review comment is injected as data.
171
+ */
172
+ export function buildReviewFixPrompt(action) {
173
+ const c = action.comment;
174
+ const where = c.path ? ` on \`${c.path}\`` : '';
175
+ const who = c.user ? `@${c.user}` : 'A reviewer';
176
+ return (`${who} left a review comment${where} on PR ${action.prUrl}. ` +
177
+ `Address it and push a follow-up commit to the SAME PR branch ` +
178
+ `(check out the PR branch with \`gh pr checkout ${action.prUrl}\`, make the change, commit, and push). ` +
179
+ `Do not open a new PR.\n\n` +
180
+ `Review comment:\n${c.body}` +
181
+ (c.html_url ? `\n\n(thread: ${c.html_url})` : ''));
182
+ }
183
+ /** Parse an owner/repo/number triple out of a GitHub PR URL. Returns null when it doesn't match. */
184
+ export function parsePrUrl(prUrl) {
185
+ const m = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
186
+ if (!m)
187
+ return null;
188
+ return { owner: m[1], repo: m[2], number: Number(m[3]) };
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Impure collectors — thin `gh` CLI wrappers. Kept out of the pure decider so
192
+ // tests never hit the network. These are the seam the #331 webhook receiver
193
+ // would replace: a webhook payload can synthesize the same PrCheck /
194
+ // PrReviewComment shapes and hand them straight to decidePrActions.
195
+ // ---------------------------------------------------------------------------
196
+ /** Fetch the current CI checks for a PR via `gh pr checks`. Returns [] on any gh error. */
197
+ export async function fetchPrChecks(prUrl) {
198
+ try {
199
+ const { stdout } = await execFileAsync('gh', ['pr', 'checks', prUrl, '--json', 'name,state,link,workflow'], { maxBuffer: 8 * 1024 * 1024 });
200
+ const raw = JSON.parse(stdout);
201
+ return raw.map((r) => ({
202
+ name: String(r.name ?? ''),
203
+ state: String(r.state ?? ''),
204
+ link: r.link ? String(r.link) : undefined,
205
+ workflow: r.workflow ? String(r.workflow) : undefined,
206
+ }));
207
+ }
208
+ catch {
209
+ // `gh pr checks` exits non-zero when checks are failing OR when none are
210
+ // configured. Both surface as a throw here; treat "can't read" as "nothing
211
+ // to act on" rather than crashing the watch loop.
212
+ return [];
213
+ }
214
+ }
215
+ /** Fetch review comments for a PR via `gh api`. Returns [] on any gh error. */
216
+ export async function fetchPrReviewComments(prUrl) {
217
+ const parsed = parsePrUrl(prUrl);
218
+ if (!parsed)
219
+ return [];
220
+ try {
221
+ const { stdout } = await execFileAsync('gh', [
222
+ 'api',
223
+ `repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/comments`,
224
+ '--paginate',
225
+ ], { maxBuffer: 16 * 1024 * 1024 });
226
+ const raw = JSON.parse(stdout);
227
+ return raw.map((r) => ({
228
+ id: Number(r.id),
229
+ body: String(r.body ?? ''),
230
+ user: r.user && typeof r.user === 'object'
231
+ ? String(r.user.login ?? '')
232
+ : undefined,
233
+ path: r.path ? String(r.path) : undefined,
234
+ html_url: r.html_url ? String(r.html_url) : undefined,
235
+ }));
236
+ }
237
+ catch {
238
+ return [];
239
+ }
240
+ }
241
+ /** Fetch the failing-run logs for a check via `gh run view --log-failed`. Best-effort, truncated. */
242
+ export async function fetchCiFailureLogs(check, maxChars = 8000) {
243
+ const runId = check.link?.match(/\/runs\/(\d+)/)?.[1] ?? check.link?.match(/\/actions\/runs\/(\d+)/)?.[1];
244
+ if (!runId)
245
+ return '';
246
+ try {
247
+ const { stdout } = await execFileAsync('gh', ['run', 'view', runId, '--log-failed'], { maxBuffer: 32 * 1024 * 1024 });
248
+ if (stdout.length <= maxChars)
249
+ return stdout;
250
+ // Keep the tail — the actual error is almost always at the end of the log.
251
+ return `... (truncated ${stdout.length - maxChars} chars) ...\n` + stdout.slice(-maxChars);
252
+ }
253
+ catch {
254
+ return '';
255
+ }
256
+ }
257
+ /** Collect a live PR snapshot from `gh`. The poll-mode seam for `runPrWatch`. */
258
+ export async function pollPrSnapshot(prUrl, sourceTeammate) {
259
+ const [checks, comments] = await Promise.all([
260
+ fetchPrChecks(prUrl),
261
+ fetchPrReviewComments(prUrl),
262
+ ]);
263
+ return { prUrl, sourceTeammate, checks, comments };
264
+ }
265
+ /**
266
+ * Run the pr-watch poll loop. Each pass: retire the dedupe guards of any fixers
267
+ * that have finished (so a still-red check can spawn its next wave), resolve watch
268
+ * targets, snapshot each PR, decide actions against the running `handled` set and
269
+ * per-PR `waves` budget, then react — spawning fresh fixers, or emitting a
270
+ * one-shot `needs-human` event once a PR's wave budget is spent. Every acted-on
271
+ * dedupe key is recorded so a failure/comment never spawns twice within a wave,
272
+ * and `waves` hard-caps the total spawns per PR (issue #338).
273
+ */
274
+ export async function runPrWatch(deps, opts = {}) {
275
+ const intervalMs = opts.intervalMs ?? 15000;
276
+ const maxPolls = opts.maxPolls ?? 0;
277
+ const maxWaves = opts.maxWaves ?? DEFAULT_MAX_WAVES;
278
+ const handled = opts.handled ?? new Set();
279
+ const waves = opts.waves ?? new Map();
280
+ const pollSnapshot = deps.pollSnapshot ?? pollPrSnapshot;
281
+ const fetchLogs = deps.fetchLogs ?? fetchCiFailureLogs;
282
+ const emit = deps.onEvent ?? (() => { });
283
+ // dedupeKey -> spawned teammate label, for the settle sweep. When a fixer
284
+ // finishes, its guard clears so the next wave (if still warranted) can fire.
285
+ const inFlight = new Map();
286
+ let polls = 0;
287
+ let spawned = 0;
288
+ let neededHuman = 0;
289
+ for (;;) {
290
+ if (opts.shouldStop?.()) {
291
+ return { polls, spawned, neededHuman, handled, waves, stoppedBy: 'signal' };
292
+ }
293
+ // Retire guards for finished fixers so a check that's STILL red after a fix
294
+ // can spawn its next wave (bounded by `waves`). Skipped when no settle probe
295
+ // is wired — then a check gets one wave until pr-watch restarts.
296
+ if (deps.reactionSettled && inFlight.size > 0) {
297
+ for (const [key, label] of [...inFlight]) {
298
+ let done = false;
299
+ try {
300
+ done = await deps.reactionSettled(label);
301
+ }
302
+ catch {
303
+ done = false;
304
+ }
305
+ if (done) {
306
+ handled.delete(key);
307
+ inFlight.delete(key);
308
+ }
309
+ }
310
+ }
311
+ const targets = await deps.resolveTargets();
312
+ polls++;
313
+ emit({ type: 'poll', targets: targets.length, timestamp: new Date().toISOString() });
314
+ for (const target of targets) {
315
+ let snap;
316
+ try {
317
+ snap = await pollSnapshot(target.prUrl, target.sourceTeammate);
318
+ }
319
+ catch (err) {
320
+ emit({
321
+ type: 'error',
322
+ prUrl: target.prUrl,
323
+ message: err.message,
324
+ timestamp: new Date().toISOString(),
325
+ });
326
+ continue;
327
+ }
328
+ const actions = decidePrActions(snap, handled, waves, maxWaves);
329
+ for (const action of actions) {
330
+ // Record the key BEFORE reacting so a spawn failure can't loop-spam the
331
+ // same failure — a bad action is dropped, not retried forever.
332
+ handled.add(action.dedupeKey);
333
+ if (action.kind === 'needs-human') {
334
+ neededHuman++;
335
+ emit({
336
+ type: 'needs-human',
337
+ prUrl: action.prUrl,
338
+ subject: action.subject,
339
+ waves: action.waves,
340
+ timestamp: new Date().toISOString(),
341
+ });
342
+ continue;
343
+ }
344
+ // Spend a wave from this PR's budget for the spawn we're about to make.
345
+ waves.set(action.prUrl, (waves.get(action.prUrl) ?? 0) + 1);
346
+ const prompt = action.kind === 'ci-fix'
347
+ ? buildCiFixPrompt(action, await fetchLogs(action.check))
348
+ : buildReviewFixPrompt(action);
349
+ try {
350
+ const label = await deps.react(action, prompt);
351
+ spawned++;
352
+ if (label)
353
+ inFlight.set(action.dedupeKey, label);
354
+ emit({ type: 'spawned', action, label, timestamp: new Date().toISOString() });
355
+ }
356
+ catch (err) {
357
+ emit({
358
+ type: 'error',
359
+ prUrl: action.prUrl,
360
+ message: err.message,
361
+ timestamp: new Date().toISOString(),
362
+ });
363
+ }
364
+ }
365
+ }
366
+ if (maxPolls > 0 && polls >= maxPolls) {
367
+ return { polls, spawned, neededHuman, handled, waves, stoppedBy: 'max-polls' };
368
+ }
369
+ await new Promise((r) => setTimeout(r, intervalMs));
370
+ }
371
+ }
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import { randomBytes } from 'crypto';
14
14
  import lockfile from 'proper-lockfile';
15
15
  import { getTeamsRegistryPath } from '../state.js';
16
+ import { emit } from '../events.js';
16
17
  async function registryPath() {
17
18
  return getTeamsRegistryPath();
18
19
  }
@@ -98,39 +99,50 @@ export async function createTeam(name, options) {
98
99
  throw new Error('Cannot use both --enable-worktrees and --use-worktree. Pick one.');
99
100
  }
100
101
  const p = await registryPath();
101
- return withRegistryLock(p, async () => {
102
+ const meta = await withRegistryLock(p, async () => {
102
103
  const reg = await loadTeams();
103
104
  if (reg[name]) {
104
105
  throw new Error(`Team '${name}' already exists`);
105
106
  }
106
- const meta = {
107
+ const m = {
107
108
  created_at: new Date().toISOString(),
108
109
  ...(options?.description ? { description: options.description } : {}),
109
110
  ...(options?.enableWorktrees ? { enable_worktrees: true } : {}),
110
111
  ...(options?.useWorktree ? { use_worktree: options.useWorktree } : {}),
111
112
  };
112
- reg[name] = meta;
113
+ reg[name] = m;
113
114
  await saveTeams(reg);
114
- return meta;
115
+ return m;
115
116
  });
117
+ // Audit the lifecycle boundary, not the CLI shell — captures every creation
118
+ // path (create + ensure) with team metadata the generic command log lacks.
119
+ emit('teams.create', { module: 'teams', team: name, worktrees: Boolean(options?.enableWorktrees || options?.useWorktree) });
120
+ return meta;
116
121
  }
117
122
  /** Return existing team metadata or create a new team if it does not exist. */
118
123
  export async function ensureTeam(name) {
119
124
  const p = await registryPath();
120
- return withRegistryLock(p, async () => {
125
+ let created = false;
126
+ const meta = await withRegistryLock(p, async () => {
121
127
  const reg = await loadTeams();
122
128
  if (reg[name])
123
129
  return reg[name];
124
- const meta = { created_at: new Date().toISOString() };
125
- reg[name] = meta;
130
+ const m = { created_at: new Date().toISOString() };
131
+ reg[name] = m;
126
132
  await saveTeams(reg);
127
- return meta;
133
+ created = true;
134
+ return m;
128
135
  });
136
+ // `teams add` auto-creates the team on first teammate — audit that creation
137
+ // too, but only when it actually happened (not the get-existing path).
138
+ if (created)
139
+ emit('teams.create', { module: 'teams', team: name, worktrees: false });
140
+ return meta;
129
141
  }
130
142
  /** Remove a team from the registry. Returns false if the team did not exist. */
131
143
  export async function removeTeam(name) {
132
144
  const p = await registryPath();
133
- return withRegistryLock(p, async () => {
145
+ const existed = await withRegistryLock(p, async () => {
134
146
  const reg = await loadTeams();
135
147
  if (!reg[name])
136
148
  return false;
@@ -138,6 +150,10 @@ export async function removeTeam(name) {
138
150
  await saveTeams(reg);
139
151
  return true;
140
152
  });
153
+ // "Disband" — only fires when a real team was removed, not a no-op.
154
+ if (existed)
155
+ emit('teams.disband', { module: 'teams', team: name });
156
+ return existed;
141
157
  }
142
158
  /** Check whether a team with the given name exists in the registry. */
143
159
  export async function teamExists(name) {
@@ -18,6 +18,8 @@
18
18
  * terminal output, json-per-wave output, or a TUI.
19
19
  */
20
20
  import type { AgentManager, AgentProcess } from './agents.js';
21
+ import type { TeamBudgetWatcher } from '../budget/live-team.js';
22
+ import type { BreachInfo } from '../budget/enforce.js';
21
23
  export interface WaveSummary {
22
24
  wave: number;
23
25
  timestamp: string;
@@ -35,11 +37,22 @@ export interface SupervisorOptions {
35
37
  maxWaves?: number;
36
38
  /** Called once per wave. Return false to stop the loop gracefully. */
37
39
  onWave: (summary: WaveSummary) => void | Promise<void> | boolean | Promise<boolean>;
40
+ /**
41
+ * Optional live-spend watcher (issue #399). Polled once per wave; on first
42
+ * breach the supervisor calls `manager.stopByTask(team)` and exits with
43
+ * `stoppedBy: 'budget'`. Dormant when null — supervisor behavior is unchanged
44
+ * for teams that never opt into budget enforcement.
45
+ */
46
+ budgetWatcher?: TeamBudgetWatcher | null;
47
+ /** Called with the breach details when the budget watcher trips. */
48
+ onBudgetBreach?: (breach: BreachInfo) => void;
38
49
  }
39
50
  export interface SupervisorResult {
40
51
  waves: number;
41
- stoppedBy: 'drained' | 'max-waves' | 'signal' | 'callback';
52
+ stoppedBy: 'drained' | 'max-waves' | 'signal' | 'callback' | 'budget';
42
53
  elapsed_ms: number;
54
+ /** Set when `stoppedBy === 'budget'` — the breach that terminated the team. */
55
+ budgetBreach?: BreachInfo;
43
56
  }
44
57
  /**
45
58
  * Run the continuous DAG dispatcher until the team drains, the caller
@@ -45,6 +45,25 @@ export async function runSupervisor(mgr, opts) {
45
45
  if (keepGoing === false) {
46
46
  return { waves: wave, stoppedBy: 'callback', elapsed_ms: Date.now() - startedAt };
47
47
  }
48
+ // Live budget kill (issue #399). Poll AFTER the wave's callback so the
49
+ // most recent teammate stdout is already flushed to disk. On breach we
50
+ // stop every RUNNING teammate — the DAG effectively drains this wave.
51
+ if (opts.budgetWatcher) {
52
+ await opts.budgetWatcher.poll();
53
+ if (opts.budgetWatcher.breached()) {
54
+ const breach = opts.budgetWatcher.breach();
55
+ await mgr.stopByTask(team);
56
+ if (breach && opts.onBudgetBreach)
57
+ opts.onBudgetBreach(breach);
58
+ opts.budgetWatcher.dispose();
59
+ return {
60
+ waves: wave,
61
+ stoppedBy: 'budget',
62
+ elapsed_ms: Date.now() - startedAt,
63
+ budgetBreach: breach ?? undefined,
64
+ };
65
+ }
66
+ }
48
67
  // Re-check drain AFTER the callback. The callback may have added new
49
68
  // teammates mid-flight (that's the whole point of the dynamic DAG), so
50
69
  // trusting the pre-callback snapshot would drain prematurely. Rescan
@@ -4,6 +4,15 @@ export declare function getGitRoot(dir: string): Promise<string>;
4
4
  * Check if a worktree directory has uncommitted changes.
5
5
  */
6
6
  export declare function hasUncommittedChanges(worktreePath: string): Promise<boolean>;
7
+ /** Default cap on diff text so one giant worktree can't blow up the serve dashboard. */
8
+ export declare const DEFAULT_DIFF_MAX_BYTES = 200000;
9
+ /**
10
+ * Return the uncommitted working-tree diff for a worktree (staged + unstaged,
11
+ * relative to HEAD), capped so a huge diff can't overwhelm the read-only serve
12
+ * dashboard. Read-only: shells out to `git diff HEAD` and never mutates state.
13
+ * Returns '' when the path isn't a git worktree or has no pending changes.
14
+ */
15
+ export declare function gitDiff(worktreePath: string, maxBytes?: number): Promise<string>;
7
16
  /**
8
17
  * Create a new git worktree for a teammate.
9
18
  *
@@ -36,6 +36,38 @@ export async function hasUncommittedChanges(worktreePath) {
36
36
  return false;
37
37
  }
38
38
  }
39
+ /** Default cap on diff text so one giant worktree can't blow up the serve dashboard. */
40
+ export const DEFAULT_DIFF_MAX_BYTES = 200_000;
41
+ /**
42
+ * Return the uncommitted working-tree diff for a worktree (staged + unstaged,
43
+ * relative to HEAD), capped so a huge diff can't overwhelm the read-only serve
44
+ * dashboard. Read-only: shells out to `git diff HEAD` and never mutates state.
45
+ * Returns '' when the path isn't a git worktree or has no pending changes.
46
+ */
47
+ export async function gitDiff(worktreePath, maxBytes = DEFAULT_DIFF_MAX_BYTES) {
48
+ // Size the capture buffer off the cap (not an arbitrary 1MB floor) so a diff
49
+ // that overshoots gets TRUNCATED, never silently dropped. A diff larger than
50
+ // the buffer overflows maxBuffer, but Node still hands us the captured prefix
51
+ // on the rejection — we truncate that below rather than losing it to ''.
52
+ const maxBuffer = maxBytes * 4;
53
+ const truncate = (s) => s.slice(0, maxBytes) + `\n… [diff truncated at ${maxBytes} bytes]`;
54
+ try {
55
+ const { stdout } = await execFileAsync('git', ['diff', 'HEAD'], {
56
+ cwd: worktreePath,
57
+ maxBuffer,
58
+ });
59
+ return stdout.length > maxBytes ? truncate(stdout) : stdout;
60
+ }
61
+ catch (err) {
62
+ // ENOBUFS on an oversized diff still carries the partial capture; truncate
63
+ // it so the dashboard shows "[diff truncated]" instead of "no changes".
64
+ // A genuine non-git-worktree error has no stdout → fall through to ''.
65
+ const partial = err?.stdout;
66
+ if (typeof partial === 'string' && partial.length > 0)
67
+ return truncate(partial);
68
+ return '';
69
+ }
70
+ }
39
71
  /**
40
72
  * Create a new git worktree for a teammate.
41
73
  *
@@ -0,0 +1,6 @@
1
+ import type { TerminalBackend, SplitDirection } from '../types.js';
2
+ /** AppleScript that opens a Ghostty tab (a window if none is open). */
3
+ export declare function ghosttyTabScript(cwd: string, command: string[]): string;
4
+ /** AppleScript that splits the current Ghostty surface (a window if none is open). */
5
+ export declare function ghosttySplitScript(cwd: string, command: string[], direction: SplitDirection): string;
6
+ export declare const ghosttyBackend: TerminalBackend;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ghostty backend — drives Ghostty (>= 1.3) via AppleScript (`osascript`).
3
+ *
4
+ * Ghostty's `new surface configuration` record carries the working directory and
5
+ * command natively, so no `cd` wrapper is needed. Tab: `new tab` (or a window
6
+ * when none is open). Split: `split <surface> direction right|down`, where the
7
+ * current surface is `focused terminal of selected tab of front window`
8
+ * (verified against Ghostty 1.3.1 — a surface is a "terminal" in its AS model).
9
+ */
10
+ import * as fs from 'fs';
11
+ import { appleScriptStr } from '../quote.js';
12
+ import { execOnly, iLoginShell } from '../shell.js';
13
+ const GHOSTTY_APP = '/Applications/Ghostty.app';
14
+ function appExists(p) {
15
+ try {
16
+ return fs.existsSync(p);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /** Shared prologue: activate + a surface configuration carrying cwd + command. */
23
+ function configPrologue(cwd, command) {
24
+ const cmd = appleScriptStr(iLoginShell(execOnly(command)));
25
+ return [
26
+ 'tell application "Ghostty"',
27
+ ' activate',
28
+ ' set cfg to new surface configuration',
29
+ ` set initial working directory of cfg to ${appleScriptStr(cwd)}`,
30
+ ` set command of cfg to ${cmd}`,
31
+ ];
32
+ }
33
+ /** AppleScript that opens a Ghostty tab (a window if none is open). */
34
+ export function ghosttyTabScript(cwd, command) {
35
+ return [
36
+ ...configPrologue(cwd, command),
37
+ ' if (count of windows) is 0 then',
38
+ ' new window with configuration cfg',
39
+ ' else',
40
+ ' new tab in front window with configuration cfg',
41
+ ' end if',
42
+ 'end tell',
43
+ ].join('\n');
44
+ }
45
+ /** AppleScript that splits the current Ghostty surface (a window if none is open). */
46
+ export function ghosttySplitScript(cwd, command, direction) {
47
+ return [
48
+ ...configPrologue(cwd, command),
49
+ ' if (count of windows) is 0 then',
50
+ ' new window with configuration cfg',
51
+ ' else',
52
+ ` split (focused terminal of selected tab of front window) direction ${direction} with configuration cfg`,
53
+ ' end if',
54
+ 'end tell',
55
+ ].join('\n');
56
+ }
57
+ export const ghosttyBackend = {
58
+ id: 'ghostty',
59
+ label: 'Ghostty',
60
+ isAvailable(ctx) {
61
+ return ctx.platform === 'darwin' && appExists(GHOSTTY_APP);
62
+ },
63
+ buildTab(cwd, command) {
64
+ return { argv: ['osascript', '-e', ghosttyTabScript(cwd, command)] };
65
+ },
66
+ buildSplit(cwd, command, direction) {
67
+ return { argv: ['osascript', '-e', ghosttySplitScript(cwd, command, direction)] };
68
+ },
69
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Backend registry + current-terminal detection.
3
+ */
4
+ import type { Backend, EngineContext, TerminalBackend } from '../types.js';
5
+ import { itermBackend } from './iterm.js';
6
+ import { ghosttyBackend } from './ghostty.js';
7
+ import { tmuxBackend } from './tmux.js';
8
+ import { vscodiumAgentBackend } from './vscodium-agent.js';
9
+ /** All known interactive backends, keyed by id. */
10
+ export declare const BACKENDS: Record<Backend, TerminalBackend>;
11
+ /**
12
+ * The backend for the terminal the CLI is currently running in, or null if we
13
+ * can't open a surface into it. tmux wins (via `$TMUX`) because a tmux pane can
14
+ * live inside iTerm/Ghostty; otherwise fall back to `TERM_PROGRAM`.
15
+ */
16
+ export declare function detectCurrentBackend(ctx: EngineContext): Backend | null;
17
+ /** Backends that can actually be driven in this context. */
18
+ export declare function availableBackends(ctx: EngineContext): TerminalBackend[];
19
+ export { itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend };