@phnx-labs/agents-cli 1.20.35 → 1.20.37

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 (236) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  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.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions-sync.d.ts +3 -0
  34. package/dist/commands/sessions-sync.js +44 -4
  35. package/dist/commands/sessions.d.ts +78 -1
  36. package/dist/commands/sessions.js +506 -66
  37. package/dist/commands/setup.js +4 -2
  38. package/dist/commands/sync.d.ts +3 -1
  39. package/dist/commands/sync.js +156 -4
  40. package/dist/commands/teams.js +217 -0
  41. package/dist/commands/versions.js +2 -4
  42. package/dist/commands/watchdog.d.ts +18 -0
  43. package/dist/commands/watchdog.js +238 -0
  44. package/dist/index.js +25 -2
  45. package/dist/lib/audit/log.d.ts +92 -0
  46. package/dist/lib/audit/log.js +177 -0
  47. package/dist/lib/auto-pull.js +2 -1
  48. package/dist/lib/browser/chrome.d.ts +10 -0
  49. package/dist/lib/browser/chrome.js +18 -7
  50. package/dist/lib/browser/drivers/ssh.js +2 -1
  51. package/dist/lib/browser/har.d.ts +84 -0
  52. package/dist/lib/browser/har.js +77 -0
  53. package/dist/lib/browser/ipc.js +24 -3
  54. package/dist/lib/browser/profiles.d.ts +1 -1
  55. package/dist/lib/browser/profiles.js +8 -10
  56. package/dist/lib/browser/refs.d.ts +65 -0
  57. package/dist/lib/browser/refs.js +73 -1
  58. package/dist/lib/browser/runtime-state.js +1 -0
  59. package/dist/lib/browser/service.d.ts +38 -2
  60. package/dist/lib/browser/service.js +112 -8
  61. package/dist/lib/browser/types.d.ts +14 -1
  62. package/dist/lib/budget/live-cloud.d.ts +42 -0
  63. package/dist/lib/budget/live-cloud.js +79 -0
  64. package/dist/lib/budget/live-team.d.ts +31 -0
  65. package/dist/lib/budget/live-team.js +115 -0
  66. package/dist/lib/cloud/codex.js +4 -0
  67. package/dist/lib/cloud/rush.d.ts +12 -1
  68. package/dist/lib/cloud/rush.js +13 -3
  69. package/dist/lib/cloud/types.d.ts +9 -0
  70. package/dist/lib/computer/dispatch.d.ts +8 -0
  71. package/dist/lib/computer/dispatch.js +125 -0
  72. package/dist/lib/computer/loop.d.ts +62 -0
  73. package/dist/lib/computer/loop.js +98 -0
  74. package/dist/lib/computer/model.d.ts +44 -0
  75. package/dist/lib/computer/model.js +157 -0
  76. package/dist/lib/concurrency.d.ts +19 -0
  77. package/dist/lib/concurrency.js +33 -0
  78. package/dist/lib/daemon.d.ts +57 -0
  79. package/dist/lib/daemon.js +196 -18
  80. package/dist/lib/devices/registry.d.ts +7 -0
  81. package/dist/lib/devices/registry.js +24 -0
  82. package/dist/lib/devices/resolve-target.d.ts +24 -0
  83. package/dist/lib/devices/resolve-target.js +80 -0
  84. package/dist/lib/devices/tailscale.js +1 -1
  85. package/dist/lib/drift.d.ts +52 -0
  86. package/dist/lib/drift.js +112 -0
  87. package/dist/lib/events.d.ts +1 -1
  88. package/dist/lib/events.js +31 -13
  89. package/dist/lib/exec.d.ts +17 -0
  90. package/dist/lib/exec.js +79 -13
  91. package/dist/lib/git.d.ts +27 -0
  92. package/dist/lib/git.js +56 -1
  93. package/dist/lib/hooks/cache.d.ts +6 -0
  94. package/dist/lib/hooks/cache.js +54 -12
  95. package/dist/lib/hooks.d.ts +27 -0
  96. package/dist/lib/hooks.js +127 -8
  97. package/dist/lib/hosts/dispatch.d.ts +15 -0
  98. package/dist/lib/hosts/dispatch.js +39 -6
  99. package/dist/lib/hosts/logs.js +30 -1
  100. package/dist/lib/hosts/option.js +1 -1
  101. package/dist/lib/hosts/passthrough.js +3 -1
  102. package/dist/lib/hosts/ready.d.ts +29 -6
  103. package/dist/lib/hosts/ready.js +66 -15
  104. package/dist/lib/hosts/registry.d.ts +19 -2
  105. package/dist/lib/hosts/registry.js +58 -2
  106. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  107. package/dist/lib/hosts/remote-cmd.js +70 -1
  108. package/dist/lib/hosts/remote-os.d.ts +17 -0
  109. package/dist/lib/hosts/remote-os.js +30 -0
  110. package/dist/lib/hosts/session-index.d.ts +34 -0
  111. package/dist/lib/hosts/session-index.js +56 -0
  112. package/dist/lib/hosts/tasks.d.ts +14 -0
  113. package/dist/lib/hosts/tasks.js +15 -0
  114. package/dist/lib/lock.d.ts +93 -0
  115. package/dist/lib/lock.js +207 -0
  116. package/dist/lib/loop.js +16 -1
  117. package/dist/lib/machine-id.d.ts +21 -0
  118. package/dist/lib/machine-id.js +26 -0
  119. package/dist/lib/mailbox-target.d.ts +36 -0
  120. package/dist/lib/mailbox-target.js +45 -0
  121. package/dist/lib/mailbox.d.ts +47 -0
  122. package/dist/lib/mailbox.js +194 -0
  123. package/dist/lib/mcp.d.ts +5 -0
  124. package/dist/lib/mcp.js +24 -8
  125. package/dist/lib/migrate.d.ts +19 -0
  126. package/dist/lib/migrate.js +134 -26
  127. package/dist/lib/overdue.js +3 -0
  128. package/dist/lib/picker.d.ts +2 -0
  129. package/dist/lib/picker.js +4 -1
  130. package/dist/lib/platform/exec.d.ts +46 -0
  131. package/dist/lib/platform/exec.js +74 -0
  132. package/dist/lib/platform/process.d.ts +31 -0
  133. package/dist/lib/platform/process.js +34 -1
  134. package/dist/lib/platform/winpath.js +2 -0
  135. package/dist/lib/plugins.js +16 -6
  136. package/dist/lib/profiles.d.ts +25 -0
  137. package/dist/lib/profiles.js +22 -6
  138. package/dist/lib/pty-client.js +2 -1
  139. package/dist/lib/rotate.d.ts +61 -0
  140. package/dist/lib/rotate.js +52 -0
  141. package/dist/lib/routines.d.ts +40 -2
  142. package/dist/lib/routines.js +66 -8
  143. package/dist/lib/runner.d.ts +11 -2
  144. package/dist/lib/runner.js +49 -7
  145. package/dist/lib/scheduler.js +6 -1
  146. package/dist/lib/secrets/bundles.d.ts +60 -4
  147. package/dist/lib/secrets/bundles.js +131 -12
  148. package/dist/lib/secrets/filestore.d.ts +3 -0
  149. package/dist/lib/secrets/filestore.js +42 -16
  150. package/dist/lib/secrets/index.d.ts +43 -2
  151. package/dist/lib/secrets/index.js +102 -3
  152. package/dist/lib/secrets/mcp.d.ts +93 -0
  153. package/dist/lib/secrets/mcp.js +205 -0
  154. package/dist/lib/secrets/remote.js +12 -5
  155. package/dist/lib/secrets/sync.js +83 -4
  156. package/dist/lib/secrets/windows.js +14 -3
  157. package/dist/lib/serve/data.d.ts +81 -0
  158. package/dist/lib/serve/data.js +91 -0
  159. package/dist/lib/serve/page.d.ts +7 -0
  160. package/dist/lib/serve/page.js +140 -0
  161. package/dist/lib/serve/server.d.ts +46 -0
  162. package/dist/lib/serve/server.js +115 -0
  163. package/dist/lib/session/active.d.ts +73 -0
  164. package/dist/lib/session/active.js +199 -22
  165. package/dist/lib/session/db.d.ts +2 -1
  166. package/dist/lib/session/db.js +41 -5
  167. package/dist/lib/session/discover.d.ts +39 -0
  168. package/dist/lib/session/discover.js +127 -29
  169. package/dist/lib/session/ghostty-tabs.d.ts +33 -0
  170. package/dist/lib/session/ghostty-tabs.js +126 -0
  171. package/dist/lib/session/inject.d.ts +18 -0
  172. package/dist/lib/session/inject.js +21 -0
  173. package/dist/lib/session/parse.js +23 -20
  174. package/dist/lib/session/pid-registry.d.ts +1 -0
  175. package/dist/lib/session/pid-registry.js +24 -0
  176. package/dist/lib/session/provenance.d.ts +14 -2
  177. package/dist/lib/session/provenance.js +39 -8
  178. package/dist/lib/session/relative-time.js +6 -2
  179. package/dist/lib/session/remote-active.js +19 -17
  180. package/dist/lib/session/remote-list.d.ts +51 -0
  181. package/dist/lib/session/remote-list.js +205 -0
  182. package/dist/lib/session/remote.d.ts +7 -1
  183. package/dist/lib/session/remote.js +20 -4
  184. package/dist/lib/session/sync/config.d.ts +14 -15
  185. package/dist/lib/session/sync/config.js +60 -20
  186. package/dist/lib/session/types.d.ts +23 -0
  187. package/dist/lib/shims.d.ts +36 -6
  188. package/dist/lib/shims.js +91 -29
  189. package/dist/lib/ssh-exec.js +2 -0
  190. package/dist/lib/ssh-tunnel.js +2 -1
  191. package/dist/lib/startup/command-registry.d.ts +6 -0
  192. package/dist/lib/startup/command-registry.js +13 -1
  193. package/dist/lib/state.d.ts +13 -0
  194. package/dist/lib/state.js +103 -9
  195. package/dist/lib/sync-umbrella.d.ts +14 -7
  196. package/dist/lib/sync-umbrella.js +21 -13
  197. package/dist/lib/teams/forEach.d.ts +110 -0
  198. package/dist/lib/teams/forEach.js +186 -0
  199. package/dist/lib/teams/index.d.ts +1 -0
  200. package/dist/lib/teams/index.js +1 -0
  201. package/dist/lib/teams/pr-watch.d.ts +226 -0
  202. package/dist/lib/teams/pr-watch.js +371 -0
  203. package/dist/lib/teams/supervisor.d.ts +14 -1
  204. package/dist/lib/teams/supervisor.js +19 -0
  205. package/dist/lib/teams/worktree.d.ts +9 -0
  206. package/dist/lib/teams/worktree.js +32 -0
  207. package/dist/lib/terminal/backends/index.d.ts +2 -1
  208. package/dist/lib/terminal/backends/index.js +3 -1
  209. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  210. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  211. package/dist/lib/terminal/index.d.ts +4 -1
  212. package/dist/lib/terminal/index.js +4 -1
  213. package/dist/lib/terminal/inject.d.ts +204 -0
  214. package/dist/lib/terminal/inject.js +247 -0
  215. package/dist/lib/terminal/resolve.d.ts +64 -0
  216. package/dist/lib/terminal/resolve.js +90 -0
  217. package/dist/lib/terminal/types.d.ts +1 -1
  218. package/dist/lib/tmux/session.d.ts +10 -0
  219. package/dist/lib/tmux/session.js +31 -0
  220. package/dist/lib/triggers/webhook.d.ts +85 -0
  221. package/dist/lib/triggers/webhook.js +141 -0
  222. package/dist/lib/versions.d.ts +23 -0
  223. package/dist/lib/versions.js +119 -13
  224. package/dist/lib/watchdog/index.d.ts +3 -0
  225. package/dist/lib/watchdog/index.js +5 -0
  226. package/dist/lib/watchdog/read.d.ts +35 -0
  227. package/dist/lib/watchdog/read.js +149 -0
  228. package/dist/lib/watchdog/runner.d.ts +127 -0
  229. package/dist/lib/watchdog/runner.js +322 -0
  230. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  231. package/dist/lib/watchdog/watchdog.js +166 -0
  232. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  233. package/dist/lib/watchdog/watchdogTail.js +154 -0
  234. package/dist/lib/workflows.d.ts +166 -0
  235. package/dist/lib/workflows.js +193 -0
  236. package/package.json +5 -4
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Umbrella `agents sync` orchestration — "make this machine current".
3
3
  *
4
- * Bare `agents sync` fetches remote state (config repos + secrets + sessions)
5
- * then reconciles it into every installed agent's version home. Each stage is
6
- * an existing exported library function; this module only sequences them and
7
- * decides from the flags which stages run (`planUmbrellaStages`). The
8
- * planner is pure so the flag matrix is unit-tested without any I/O.
4
+ * Bare `agents sync` fetches the config repos then reconciles them into every
5
+ * installed agent's version home. Secrets and sessions are opt-in stages
6
+ * (`--secrets` / `--sessions`) see `planUmbrellaStages` for why they're off by
7
+ * default. Each stage is an existing exported library function; this module only
8
+ * sequences them and decides — from the flags which stages run. The planner is
9
+ * pure so the flag matrix is unit-tested without any I/O.
9
10
  *
10
11
  * Stage backends:
11
12
  * repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
@@ -32,11 +33,17 @@ export interface UmbrellaPlan {
32
33
  }
33
34
  /**
34
35
  * Decide which stages run. Pure — no I/O. Semantics:
35
- * bare (no flags) fetch all three, then reconcile
36
+ * bare (no flags) fetch repos, then reconcile
36
37
  * --local reconcile only, no fetch
37
- * --cloud fetch (all, or the selected subset), skip reconcile
38
+ * --cloud fetch repos (or the selected subset), skip reconcile
38
39
  * --repos/--secrets/... fetch only the selected types, then reconcile
39
40
  * `--local` wins over everything; `--cloud` suppresses reconcile.
41
+ *
42
+ * Secrets and sessions are NOT part of the bare default — they are opt-in via
43
+ * `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
44
+ * bare `agents sync` is more blast radius than the verb should carry by
45
+ * default, and session transcripts are queryable on demand (`agents sessions
46
+ * --host <machine>`) so they don't need eager mirroring.
40
47
  */
41
48
  export declare function planUmbrellaStages(f: UmbrellaFlags): UmbrellaPlan;
42
49
  export interface UmbrellaResult {
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Umbrella `agents sync` orchestration — "make this machine current".
3
3
  *
4
- * Bare `agents sync` fetches remote state (config repos + secrets + sessions)
5
- * then reconciles it into every installed agent's version home. Each stage is
6
- * an existing exported library function; this module only sequences them and
7
- * decides from the flags which stages run (`planUmbrellaStages`). The
8
- * planner is pure so the flag matrix is unit-tested without any I/O.
4
+ * Bare `agents sync` fetches the config repos then reconciles them into every
5
+ * installed agent's version home. Secrets and sessions are opt-in stages
6
+ * (`--secrets` / `--sessions`) see `planUmbrellaStages` for why they're off by
7
+ * default. Each stage is an existing exported library function; this module only
8
+ * sequences them and decides — from the flags which stages run. The planner is
9
+ * pure so the flag matrix is unit-tested without any I/O.
9
10
  *
10
11
  * Stage backends:
11
12
  * repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
@@ -20,11 +21,17 @@ import { getUserAgentsDir, getEnabledExtraRepos } from './state.js';
20
21
  import { listRemoteBundles, pullBundle } from './secrets/sync.js';
21
22
  /**
22
23
  * Decide which stages run. Pure — no I/O. Semantics:
23
- * bare (no flags) fetch all three, then reconcile
24
+ * bare (no flags) fetch repos, then reconcile
24
25
  * --local reconcile only, no fetch
25
- * --cloud fetch (all, or the selected subset), skip reconcile
26
+ * --cloud fetch repos (or the selected subset), skip reconcile
26
27
  * --repos/--secrets/... fetch only the selected types, then reconcile
27
28
  * `--local` wins over everything; `--cloud` suppresses reconcile.
29
+ *
30
+ * Secrets and sessions are NOT part of the bare default — they are opt-in via
31
+ * `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
32
+ * bare `agents sync` is more blast radius than the verb should carry by
33
+ * default, and session transcripts are queryable on demand (`agents sessions
34
+ * --host <machine>`) so they don't need eager mirroring.
28
35
  */
29
36
  export function planUmbrellaStages(f) {
30
37
  if (f.local) {
@@ -39,8 +46,9 @@ export function planUmbrellaStages(f) {
39
46
  reconcile: !f.cloud,
40
47
  };
41
48
  }
42
- // No per-type selector: bare = all + reconcile; --cloud = all, no reconcile.
43
- return { fetchRepos: true, fetchSecrets: true, fetchSessions: true, reconcile: !f.cloud };
49
+ // No per-type selector: bare = repos + reconcile; --cloud = repos, no
50
+ // reconcile. Secrets/sessions stay off unless explicitly selected above.
51
+ return { fetchRepos: true, fetchSecrets: false, fetchSessions: false, reconcile: !f.cloud };
44
52
  }
45
53
  /**
46
54
  * Execute the planned stages in order: repos -> secrets -> sessions -> reconcile.
@@ -103,10 +111,10 @@ export async function runUmbrellaSync(args) {
103
111
  }
104
112
  }
105
113
  if (plan.fetchSessions) {
106
- // Gate exactly like the daemon: a missing r2.backups bundle is a clean no-op,
107
- // not an error that fails the whole sync.
108
- const { isSyncConfigured } = await import('./session/sync/config.js');
109
- if (isSyncConfigured()) {
114
+ // Gate exactly like the daemon: an off switch or a missing r2.backups bundle
115
+ // is a clean no-op, not an error that fails the whole sync.
116
+ const { isSyncConfigured, isSyncEnabled } = await import('./session/sync/config.js');
117
+ if (isSyncEnabled() && isSyncConfigured()) {
110
118
  const { syncSessions } = await import('./session/sync/sync.js');
111
119
  const r = await syncSessions();
112
120
  result.sessions = { ran: true, pushed: r.pushed, pulled: r.pulled, merged: r.merged };
@@ -0,0 +1,110 @@
1
+ import type { AgentManager, AgentProcess, EffortLevel } from './agents.js';
2
+ import { type ForEachSpec, type ForEachTeammate } from '../workflows.js';
3
+ /**
4
+ * Parse a producer's stdout into the item list `expandForEach` fans out over.
5
+ *
6
+ * Two shapes are accepted (issue #343), tried in order:
7
+ * 1. A JSON array — `["a","b","c"]` (each element coerced to a trimmed string).
8
+ * 2. Newline-delimited — one item per line.
9
+ *
10
+ * Empty lines / entries are dropped so a trailing newline or a `[]` never
11
+ * fabricates a phantom teammate. Pure and deterministic — no I/O.
12
+ */
13
+ export declare function parseProducedItems(stdout: string): string[];
14
+ export interface ProduceItemsOptions {
15
+ /** Working directory for the producer command. */
16
+ cwd?: string | null;
17
+ /** Environment for the producer command (defaults to the current process env). */
18
+ env?: NodeJS.ProcessEnv;
19
+ /** Kill the producer after this many ms (default 120_000). */
20
+ timeoutMs?: number;
21
+ /**
22
+ * Resolve an `itemsRef` (`${step}`-style reference) to a prior step's produced
23
+ * list. When a spec carries `itemsRef` and this resolver returns a list, the
24
+ * producer command is skipped entirely.
25
+ */
26
+ resolveItemsRef?: (ref: string) => string[] | undefined;
27
+ }
28
+ /**
29
+ * Resolve a `for_each` spec's item list at runtime (issue #343): either by
30
+ * running its `produce:` shell command and parsing stdout, or by resolving an
31
+ * `itemsRef` to a prior step's list. The resulting items feed `expandForEach` /
32
+ * `runForEach`.
33
+ *
34
+ * `itemsRef` wins when a resolver is supplied and returns a list; otherwise the
35
+ * `produce` command runs. A spec with neither a resolvable ref nor a produce
36
+ * command is a hard error — there is nothing to fan out over.
37
+ */
38
+ export declare function produceItems(spec: ForEachSpec, opts?: ProduceItemsOptions): Promise<string[]>;
39
+ /**
40
+ * Evaluate a verify panel's `keep_if` gate against its boolean votes (issue
41
+ * #343). A `true` vote is a skeptic confirming the finding should be kept.
42
+ *
43
+ * - `all` — every skeptic must vote keep.
44
+ * - `any` — at least one skeptic votes keep.
45
+ * - `majority` — strictly more than half vote keep (a tie does NOT pass).
46
+ *
47
+ * An empty panel returns false: with no votes there is nothing affirming the
48
+ * item, so the conservative gate drops it.
49
+ */
50
+ export declare function evaluateKeepIf(votes: boolean[], keepIf: 'majority' | 'all' | 'any'): boolean;
51
+ /** Per-item verdict after tallying its verify panel. */
52
+ export interface ForEachItemVerdict {
53
+ /** The produced item this verdict is for. */
54
+ item: string;
55
+ /** Zero-based index in the (capped) produced list. */
56
+ itemIndex: number;
57
+ /** The stage teammate that handled this item. */
58
+ stageName: string;
59
+ /** Whether the item survives its `keep_if` gate (true when it has no panel). */
60
+ kept: boolean;
61
+ /** The votes that were tallied (empty when the item has no verify panel). */
62
+ votes: boolean[];
63
+ /** The gate applied (undefined when the item has no verify panel). */
64
+ keepIf?: 'majority' | 'all' | 'any';
65
+ }
66
+ /**
67
+ * Tally the verify panels of an expanded `for_each` and gate each item (issue
68
+ * #343). Groups verify teammates by the stage teammate they depend on, reads a
69
+ * boolean vote per verify teammate via `readVote`, and applies `keep_if`.
70
+ *
71
+ * An item with no verify panel is kept unconditionally (there is no gate).
72
+ * Pure and deterministic: `readVote` is the only place runtime state enters, so
73
+ * this is unit-testable with a synthetic vote reader.
74
+ */
75
+ export declare function tallyForEach(teammates: ForEachTeammate[], readVote: (verify: ForEachTeammate) => boolean): ForEachItemVerdict[];
76
+ export interface RunForEachOptions {
77
+ /** Working directory for the spawned teammates. */
78
+ cwd?: string | null;
79
+ /**
80
+ * Name of the producer teammate the stage teammates should depend on. When
81
+ * set, each stage runs `--after` the producer so it can't start before the
82
+ * list is available.
83
+ */
84
+ producerName?: string;
85
+ /** Default effort for spawned teammates (per-item overrides live in the spec). */
86
+ effort?: EffortLevel;
87
+ /** Concurrency cap for the wave; falls back to the spec's `concurrency`. */
88
+ concurrency?: number;
89
+ }
90
+ export interface RunForEachResult {
91
+ /** The expanded descriptors (stage + verify), in spawn order. */
92
+ teammates: ForEachTeammate[];
93
+ /** The AgentProcess handles returned by `spawn`, aligned to `teammates`. */
94
+ spawned: AgentProcess[];
95
+ /** Items the producer emitted (pre-cap). */
96
+ producedCount: number;
97
+ /** Items actually fanned out (post-cap). */
98
+ usedCount: number;
99
+ /** How many items the runaway guard dropped. */
100
+ truncated: number;
101
+ }
102
+ /**
103
+ * Expand a `for_each` spec against a producer's output and stage every
104
+ * resulting teammate into the given team via the dynamic-add path.
105
+ *
106
+ * Returns the descriptors and the spawned handles so a caller can drive the
107
+ * supervisor and later gather results (e.g. to evaluate a `verify` panel's
108
+ * `keep_if` gate — that vote-counting lives downstream, not here).
109
+ */
110
+ export declare function runForEach(mgr: AgentManager, teamName: string, spec: ForEachSpec, items: string[], opts?: RunForEachOptions): Promise<RunForEachResult>;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Runtime wiring for declarative dynamic fan-out (issue #343).
3
+ *
4
+ * The declarative shape (`for_each:` in WORKFLOW.md / routine `run:` blocks) is
5
+ * parsed and expanded by `src/lib/workflows.ts` (`parseForEachBlock`,
6
+ * `expandForEach`). This module is the thin bridge that stages the expanded
7
+ * teammate descriptors into the EXISTING teams substrate — one
8
+ * `AgentManager.spawn` per descriptor, which the DAG supervisor then picks up
9
+ * mid-flight via `rescanFromDisk` / `startReady`
10
+ * (`src/lib/teams/supervisor.ts:98-103`).
11
+ *
12
+ * It deliberately adds NO new orchestration engine: `spawn` with an `--after`
13
+ * chain is the whole mechanism, so cycle detection, wave scheduling, and
14
+ * cross-vendor/rotation dispatch all carry over unchanged.
15
+ */
16
+ import { execFile } from 'node:child_process';
17
+ import { promisify } from 'node:util';
18
+ import { posixShellPath } from '../platform/exec.js';
19
+ import { expandForEach } from '../workflows.js';
20
+ const execFileAsync = promisify(execFile);
21
+ /**
22
+ * Parse a producer's stdout into the item list `expandForEach` fans out over.
23
+ *
24
+ * Two shapes are accepted (issue #343), tried in order:
25
+ * 1. A JSON array — `["a","b","c"]` (each element coerced to a trimmed string).
26
+ * 2. Newline-delimited — one item per line.
27
+ *
28
+ * Empty lines / entries are dropped so a trailing newline or a `[]` never
29
+ * fabricates a phantom teammate. Pure and deterministic — no I/O.
30
+ */
31
+ export function parseProducedItems(stdout) {
32
+ const trimmed = stdout.trim();
33
+ if (trimmed === '')
34
+ return [];
35
+ // JSON array form first — only when it actually looks like one, so a plain
36
+ // line that happens to start with '[' doesn't get swallowed by a parse error.
37
+ if (trimmed.startsWith('[')) {
38
+ try {
39
+ const parsed = JSON.parse(trimmed);
40
+ if (Array.isArray(parsed)) {
41
+ return parsed
42
+ .map((x) => (x == null ? '' : typeof x === 'string' ? x : String(x)))
43
+ .map((s) => s.trim())
44
+ .filter((s) => s.length > 0);
45
+ }
46
+ }
47
+ catch {
48
+ // Not valid JSON — fall through to newline parsing.
49
+ }
50
+ }
51
+ // Newline-delimited form.
52
+ return trimmed
53
+ .split(/\r?\n/)
54
+ .map((s) => s.trim())
55
+ .filter((s) => s.length > 0);
56
+ }
57
+ /**
58
+ * Resolve a `for_each` spec's item list at runtime (issue #343): either by
59
+ * running its `produce:` shell command and parsing stdout, or by resolving an
60
+ * `itemsRef` to a prior step's list. The resulting items feed `expandForEach` /
61
+ * `runForEach`.
62
+ *
63
+ * `itemsRef` wins when a resolver is supplied and returns a list; otherwise the
64
+ * `produce` command runs. A spec with neither a resolvable ref nor a produce
65
+ * command is a hard error — there is nothing to fan out over.
66
+ */
67
+ export async function produceItems(spec, opts = {}) {
68
+ if (spec.itemsRef && opts.resolveItemsRef) {
69
+ const resolved = opts.resolveItemsRef(spec.itemsRef);
70
+ if (resolved)
71
+ return resolved;
72
+ }
73
+ if (!spec.produce) {
74
+ if (spec.itemsRef) {
75
+ throw new Error(`for_each itemsRef '${spec.itemsRef}' could not be resolved and no produce command is set`);
76
+ }
77
+ throw new Error('for_each has neither a produce command nor a resolvable itemsRef');
78
+ }
79
+ // The producer is a shell command string (e.g. "rg -l 'x' src/"), so it runs
80
+ // under `sh -c` exactly like a teammate command — resolved portably, because
81
+ // /bin/sh does not exist on Windows (there it's Git's sh.exe from PATH).
82
+ // maxBuffer is raised well above the default 1MB so a producer emitting a
83
+ // large list isn't truncated silently mid-stream.
84
+ const { stdout } = await execFileAsync(posixShellPath(), ['-c', spec.produce], {
85
+ cwd: opts.cwd ?? undefined,
86
+ env: opts.env ?? process.env,
87
+ timeout: opts.timeoutMs ?? 120_000,
88
+ maxBuffer: 64 * 1024 * 1024,
89
+ encoding: 'utf-8',
90
+ });
91
+ return parseProducedItems(stdout);
92
+ }
93
+ /**
94
+ * Evaluate a verify panel's `keep_if` gate against its boolean votes (issue
95
+ * #343). A `true` vote is a skeptic confirming the finding should be kept.
96
+ *
97
+ * - `all` — every skeptic must vote keep.
98
+ * - `any` — at least one skeptic votes keep.
99
+ * - `majority` — strictly more than half vote keep (a tie does NOT pass).
100
+ *
101
+ * An empty panel returns false: with no votes there is nothing affirming the
102
+ * item, so the conservative gate drops it.
103
+ */
104
+ export function evaluateKeepIf(votes, keepIf) {
105
+ const total = votes.length;
106
+ if (total === 0)
107
+ return false;
108
+ const yes = votes.reduce((n, v) => (v ? n + 1 : n), 0);
109
+ switch (keepIf) {
110
+ case 'all':
111
+ return yes === total;
112
+ case 'any':
113
+ return yes >= 1;
114
+ case 'majority':
115
+ return yes * 2 > total;
116
+ }
117
+ }
118
+ /**
119
+ * Tally the verify panels of an expanded `for_each` and gate each item (issue
120
+ * #343). Groups verify teammates by the stage teammate they depend on, reads a
121
+ * boolean vote per verify teammate via `readVote`, and applies `keep_if`.
122
+ *
123
+ * An item with no verify panel is kept unconditionally (there is no gate).
124
+ * Pure and deterministic: `readVote` is the only place runtime state enters, so
125
+ * this is unit-testable with a synthetic vote reader.
126
+ */
127
+ export function tallyForEach(teammates, readVote) {
128
+ const stages = teammates.filter((t) => t.role === 'stage');
129
+ const verifiersByStage = new Map();
130
+ for (const t of teammates) {
131
+ if (t.role !== 'verify')
132
+ continue;
133
+ const stageName = t.after[0];
134
+ if (!stageName)
135
+ continue;
136
+ const list = verifiersByStage.get(stageName) ?? [];
137
+ list.push(t);
138
+ verifiersByStage.set(stageName, list);
139
+ }
140
+ return stages.map((stage) => {
141
+ const panel = verifiersByStage.get(stage.name) ?? [];
142
+ if (panel.length === 0) {
143
+ return {
144
+ item: stage.item,
145
+ itemIndex: stage.itemIndex,
146
+ stageName: stage.name,
147
+ kept: true,
148
+ votes: [],
149
+ };
150
+ }
151
+ const keepIf = panel[0].keep_if ?? 'majority';
152
+ const votes = panel.map((v) => readVote(v));
153
+ return {
154
+ item: stage.item,
155
+ itemIndex: stage.itemIndex,
156
+ stageName: stage.name,
157
+ kept: evaluateKeepIf(votes, keepIf),
158
+ votes,
159
+ keepIf,
160
+ };
161
+ });
162
+ }
163
+ /**
164
+ * Expand a `for_each` spec against a producer's output and stage every
165
+ * resulting teammate into the given team via the dynamic-add path.
166
+ *
167
+ * Returns the descriptors and the spawned handles so a caller can drive the
168
+ * supervisor and later gather results (e.g. to evaluate a `verify` panel's
169
+ * `keep_if` gate — that vote-counting lives downstream, not here).
170
+ */
171
+ export async function runForEach(mgr, teamName, spec, items, opts = {}) {
172
+ const { teammates, producedCount, usedCount, truncated } = expandForEach(spec, items, {
173
+ producerName: opts.producerName,
174
+ });
175
+ const effort = opts.effort ?? 'medium';
176
+ const spawned = [];
177
+ for (const t of teammates) {
178
+ const agent = await mgr.spawn(teamName, t.agentType, t.prompt, opts.cwd ?? null, null, // mode: inherit team default
179
+ effort, null, // parentSessionId
180
+ null, // workspaceDir
181
+ null, // version
182
+ t.name, t.after);
183
+ spawned.push(agent);
184
+ }
185
+ return { teammates, spawned, producedCount, usedCount, truncated };
186
+ }
@@ -14,4 +14,5 @@ export { type EffortLevel } from './agents.js';
14
14
  export { collapseEvents, getToolBreakdown, groupAndFlattenEvents, summarizeEvents, getDelta, filterEventsByPriority, getLastTool, getToolUses, getLastMessages, getQuickStatus, getStatusSummary, AgentSummary, PRIORITY, type QuickStatus, } from './summarizer.js';
15
15
  export { extractFileOpsFromBash } from './file_ops.js';
16
16
  export { debug } from './debug.js';
17
+ export { runForEach, type RunForEachOptions, type RunForEachResult, } from './forEach.js';
17
18
  export { createWorktree, removeWorktree, isGitRepo, getGitRoot, hasUncommittedChanges, getWorktreePath, getWorktreeBranch, } from './worktree.js';
@@ -12,4 +12,5 @@ export { resolveAgentsDir, resolveBaseDir, } from './persistence.js';
12
12
  export { collapseEvents, getToolBreakdown, groupAndFlattenEvents, summarizeEvents, getDelta, filterEventsByPriority, getLastTool, getToolUses, getLastMessages, getQuickStatus, getStatusSummary, AgentSummary, PRIORITY, } from './summarizer.js';
13
13
  export { extractFileOpsFromBash } from './file_ops.js';
14
14
  export { debug } from './debug.js';
15
+ export { runForEach, } from './forEach.js';
15
16
  export { createWorktree, removeWorktree, isGitRepo, getGitRoot, hasUncommittedChanges, getWorktreePath, getWorktreeBranch, } from './worktree.js';
@@ -0,0 +1,226 @@
1
+ /**
2
+ * A CI check result. Shaped after `gh pr checks <pr> --json name,state,link,workflow`,
3
+ * where `state` is one of SUCCESS | FAILURE | PENDING | ERROR | CANCELLED | SKIPPING | ...
4
+ *
5
+ * Note on identity: `gh pr checks` exposes NO stable check-run id — `link` is the
6
+ * per-run URL, which changes every time a workflow re-runs. So dedupe keys off the
7
+ * check NAME (stable across re-runs), not the link (see `checkDedupeKey`). `link`
8
+ * is retained only for the human-facing prompt and for scraping run logs.
9
+ */
10
+ export interface PrCheck {
11
+ name: string;
12
+ state: string;
13
+ link?: string;
14
+ workflow?: string;
15
+ }
16
+ /** A PR review comment. Shaped after GitHub's `repos/{owner}/{repo}/pulls/{n}/comments`. */
17
+ export interface PrReviewComment {
18
+ id: number;
19
+ body: string;
20
+ user?: string;
21
+ path?: string;
22
+ html_url?: string;
23
+ }
24
+ /** One PR under watch, with its current CI + comment snapshot. */
25
+ export interface PrSnapshot {
26
+ /** The PR URL (https://github.com/{owner}/{repo}/pull/{n}). */
27
+ prUrl: string;
28
+ /**
29
+ * Name of the teammate whose work opened this PR — the `--after` anchor the
30
+ * spawned fix/bugfix teammate links to. Null when the PR can't be traced to a
31
+ * named teammate (the reaction still fires, just without a dependency edge).
32
+ */
33
+ sourceTeammate: string | null;
34
+ checks: PrCheck[];
35
+ comments: PrReviewComment[];
36
+ }
37
+ /**
38
+ * Default cap on how many fix/bugfix waves pr-watch spawns for a single PR before
39
+ * it gives up and escalates to a human. A persistently-RED PR re-runs CI on every
40
+ * follow-up commit; without a cap the loop would spawn teammates without bound
41
+ * (issue #338). Configurable via `--max-waves`.
42
+ */
43
+ export declare const DEFAULT_MAX_WAVES = 3;
44
+ /** A decision to spawn a follow-up teammate. Pure output of `decidePrActions`. */
45
+ export type PrWatchAction = {
46
+ kind: 'ci-fix';
47
+ prUrl: string;
48
+ sourceTeammate: string | null;
49
+ check: PrCheck;
50
+ /** Idempotency key — record it in `handled` once acted on so it never re-fires. */
51
+ dedupeKey: string;
52
+ /** 1-based wave number for this PR (used to name the spawned teammate uniquely). */
53
+ wave: number;
54
+ } | {
55
+ kind: 'review-fix';
56
+ prUrl: string;
57
+ sourceTeammate: string | null;
58
+ comment: PrReviewComment;
59
+ dedupeKey: string;
60
+ wave: number;
61
+ } | {
62
+ /**
63
+ * The per-PR wave budget is exhausted — stop spawning, escalate to a human.
64
+ * Emitted once per PR (deduped via `needsHumanKey`) instead of an N+1th fix.
65
+ */
66
+ kind: 'needs-human';
67
+ prUrl: string;
68
+ sourceTeammate: string | null;
69
+ /** What triggered the escalation (e.g. `CI check "test"`). */
70
+ subject: string;
71
+ /** Waves already spent on this PR when the cap was hit. */
72
+ waves: number;
73
+ dedupeKey: string;
74
+ };
75
+ /** The spawnable subset of actions — everything except the `needs-human` escalation. */
76
+ export type PrWatchSpawnAction = Extract<PrWatchAction, {
77
+ kind: 'ci-fix' | 'review-fix';
78
+ }>;
79
+ /** True when a check is in a terminal failing state. */
80
+ export declare function isFailedCheck(check: PrCheck): boolean;
81
+ /**
82
+ * Idempotency key for a failed check on a PR — dedupe by check NAME.
83
+ *
84
+ * The name is the ONLY identity that's stable across re-runs: when a fixer pushes
85
+ * a follow-up commit, GitHub creates a fresh workflow run with a new `link` URL but
86
+ * the SAME check name. Keying off `link` (as an earlier draft did) would treat every
87
+ * re-run as a brand-new failure and spawn an unbounded chain of fixers (issue #338).
88
+ * Keying off the name means a re-run of the same check is recognised as the same
89
+ * logical failure; the per-PR wave counter then bounds how many times we retry it.
90
+ */
91
+ export declare function checkDedupeKey(prUrl: string, check: PrCheck): string;
92
+ /** Idempotency key for a review comment on a PR — dedupe by comment id. */
93
+ export declare function commentDedupeKey(prUrl: string, comment: PrReviewComment): string;
94
+ /** Idempotency key for the one-shot "needs human" escalation on a PR. */
95
+ export declare function needsHumanKey(prUrl: string): string;
96
+ /**
97
+ * The pure heart of pr-watch: given a PR snapshot, the set of dedupe keys already
98
+ * acted on, and how many fix waves each PR has already spent, decide which
99
+ * follow-up teammates to spawn. Emits:
100
+ * - one `ci-fix` action per NEW failed check (dedup by check NAME)
101
+ * - one `review-fix` action per NEW review comment (dedup by comment id)
102
+ * - a single `needs-human` action (per PR) once the wave budget is exhausted,
103
+ * INSTEAD of spawning — so a persistently-failing PR escalates rather than
104
+ * spawning teammates without bound (issue #338).
105
+ *
106
+ * `waves` maps a PR URL to the number of fix/bugfix teammates already spawned for
107
+ * it; once that reaches `maxWaves`, no further spawns are emitted for that PR.
108
+ * Both the CI-fix and comment-routing paths draw from the SAME per-PR budget, so
109
+ * neither can loop unboundedly.
110
+ *
111
+ * No network, no side effects, no reference to state beyond the passed
112
+ * `handled` / `waves` — so the same failure never spawns twice and the caller
113
+ * stays in control of when a wave is "spent".
114
+ */
115
+ export declare function decidePrActions(snapshot: PrSnapshot, handled: ReadonlySet<string>, waves?: ReadonlyMap<string, number>, maxWaves?: number): PrWatchAction[];
116
+ /**
117
+ * Build the task prompt for a CI-fix teammate. Pure so it's unit-testable: the
118
+ * failing check + the fetched logs are injected as data.
119
+ */
120
+ export declare function buildCiFixPrompt(action: Extract<PrWatchAction, {
121
+ kind: 'ci-fix';
122
+ }>, logs: string): string;
123
+ /**
124
+ * Build the task prompt for a review-comment bugfix teammate. Pure so it's
125
+ * unit-testable: the review comment is injected as data.
126
+ */
127
+ export declare function buildReviewFixPrompt(action: Extract<PrWatchAction, {
128
+ kind: 'review-fix';
129
+ }>): string;
130
+ /** Parse an owner/repo/number triple out of a GitHub PR URL. Returns null when it doesn't match. */
131
+ export declare function parsePrUrl(prUrl: string): {
132
+ owner: string;
133
+ repo: string;
134
+ number: number;
135
+ } | null;
136
+ /** Fetch the current CI checks for a PR via `gh pr checks`. Returns [] on any gh error. */
137
+ export declare function fetchPrChecks(prUrl: string): Promise<PrCheck[]>;
138
+ /** Fetch review comments for a PR via `gh api`. Returns [] on any gh error. */
139
+ export declare function fetchPrReviewComments(prUrl: string): Promise<PrReviewComment[]>;
140
+ /** Fetch the failing-run logs for a check via `gh run view --log-failed`. Best-effort, truncated. */
141
+ export declare function fetchCiFailureLogs(check: PrCheck, maxChars?: number): Promise<string>;
142
+ /** Collect a live PR snapshot from `gh`. The poll-mode seam for `runPrWatch`. */
143
+ export declare function pollPrSnapshot(prUrl: string, sourceTeammate: string | null): Promise<PrSnapshot>;
144
+ /** A PR to watch, paired with the teammate that opened it. */
145
+ export interface WatchTarget {
146
+ prUrl: string;
147
+ sourceTeammate: string | null;
148
+ }
149
+ export interface PrWatchDeps {
150
+ /** Discover which PRs to watch this pass (teammates may open PRs mid-run). */
151
+ resolveTargets: () => Promise<WatchTarget[]>;
152
+ /** Snapshot one PR's CI + review comments. Defaults to `pollPrSnapshot`. */
153
+ pollSnapshot?: (prUrl: string, sourceTeammate: string | null) => Promise<PrSnapshot>;
154
+ /** Fetch CI failure logs for a ci-fix action. Defaults to `fetchCiFailureLogs`. */
155
+ fetchLogs?: (check: PrCheck) => Promise<string>;
156
+ /**
157
+ * React to one decided spawn action: spawn the fix/bugfix teammate. The prompt
158
+ * is pre-built with logs/comment injected. Return the spawned teammate label for
159
+ * logging, or null if the spawn was skipped. `needs-human` actions never reach
160
+ * here — they surface as a `needs-human` event instead.
161
+ */
162
+ react: (action: PrWatchSpawnAction, prompt: string) => Promise<string | null>;
163
+ /**
164
+ * True when a previously-spawned reaction has finished. Once its fixer settles,
165
+ * the dedupe guard for that check clears so a check that is STILL red can spawn
166
+ * its next (budget-bounded) wave; without this, a check would only ever get one
167
+ * fix attempt per pr-watch run. Optional — when omitted, a check gets a single
168
+ * wave until pr-watch restarts.
169
+ */
170
+ reactionSettled?: (label: string) => Promise<boolean>;
171
+ /** Progress sink — one call per notable event (poll, spawn, escalate, error). */
172
+ onEvent?: (event: PrWatchEvent) => void;
173
+ }
174
+ export type PrWatchEvent = {
175
+ type: 'poll';
176
+ targets: number;
177
+ timestamp: string;
178
+ } | {
179
+ type: 'spawned';
180
+ action: PrWatchSpawnAction;
181
+ label: string | null;
182
+ timestamp: string;
183
+ } | {
184
+ type: 'needs-human';
185
+ prUrl: string;
186
+ subject: string;
187
+ waves: number;
188
+ timestamp: string;
189
+ } | {
190
+ type: 'error';
191
+ prUrl: string;
192
+ message: string;
193
+ timestamp: string;
194
+ };
195
+ export interface PrWatchOptions {
196
+ intervalMs?: number;
197
+ /** Stop after this many polls (0 / undefined = run until signalled). */
198
+ maxPolls?: number;
199
+ /** Seed of already-handled dedupe keys (e.g. restored from disk). */
200
+ handled?: Set<string>;
201
+ /** Per-PR fix-wave counts, carried across restarts so the budget survives a restart. */
202
+ waves?: Map<string, number>;
203
+ /** Cap on fix waves per PR before escalating to a human. Defaults to DEFAULT_MAX_WAVES. */
204
+ maxWaves?: number;
205
+ /** Abort signal — resolves the loop at the next interval boundary. */
206
+ shouldStop?: () => boolean;
207
+ }
208
+ export interface PrWatchResult {
209
+ polls: number;
210
+ spawned: number;
211
+ /** How many PRs escalated to a human (wave budget exhausted). */
212
+ neededHuman: number;
213
+ handled: Set<string>;
214
+ waves: Map<string, number>;
215
+ stoppedBy: 'max-polls' | 'signal';
216
+ }
217
+ /**
218
+ * Run the pr-watch poll loop. Each pass: retire the dedupe guards of any fixers
219
+ * that have finished (so a still-red check can spawn its next wave), resolve watch
220
+ * targets, snapshot each PR, decide actions against the running `handled` set and
221
+ * per-PR `waves` budget, then react — spawning fresh fixers, or emitting a
222
+ * one-shot `needs-human` event once a PR's wave budget is spent. Every acted-on
223
+ * dedupe key is recorded so a failure/comment never spawns twice within a wave,
224
+ * and `waves` hard-caps the total spawns per PR (issue #338).
225
+ */
226
+ export declare function runPrWatch(deps: PrWatchDeps, opts?: PrWatchOptions): Promise<PrWatchResult>;