@phnx-labs/agents-cli 1.20.35 → 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 (225) 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.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -2,10 +2,12 @@
2
2
  * `agents sync` — synchronize central resources into an installed agent version.
3
3
  *
4
4
  * Forms:
5
- * agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
5
+ * agents sync # umbrella: fetch config repos -> reconcile all (secrets/sessions opt-in)
6
6
  * agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
7
7
  * agents sync --cloud # umbrella: fetch all, skip reconcile
8
8
  * agents sync --local # umbrella: reconcile all, no fetch
9
+ * agents sync system # one repo: git pull --rebase (pull-only mirror)
10
+ * agents sync user # one repo: git pull --rebase + push
9
11
  * agents sync claude # one agent: uses default/sole installed version
10
12
  * agents sync claude@2.1.142 # one agent: explicit version
11
13
  * agents sync claude@latest # one agent: newest installed
@@ -29,18 +31,22 @@
29
31
  */
30
32
  import * as path from 'path';
31
33
  import chalk from 'chalk';
32
- import { agentLabel, resolveAgentName } from '../lib/agents.js';
33
- import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildRepoScopedSelection, listRepoNames, } from '../lib/versions.js';
34
+ import { agentLabel, resolveAgentName, ALL_AGENT_IDS } from '../lib/agents.js';
35
+ import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildRepoScopedSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/versions.js';
36
+ import { capableAgents } from '../lib/capabilities.js';
37
+ import { parseHookManifest, registerHooksToSettings } from '../lib/hooks.js';
34
38
  import { compileRulesForProject } from '../lib/rules/compile.js';
35
39
  import { runLaunchSync } from '../lib/project-launch.js';
36
40
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
37
41
  import { runUmbrellaSync } from '../lib/sync-umbrella.js';
38
42
  import { addHostOption } from '../lib/hosts/option.js';
43
+ import { syncRepoGit } from '../lib/git.js';
44
+ import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../lib/state.js';
39
45
  /** Register the `agents sync` command. */
40
46
  export function registerSyncCommand(program) {
41
47
  addHostOption(program.command('sync [agentSpec] [repo]'))
42
48
  .summary('Make this machine current, or sync resources into one agent')
43
- .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
49
+ .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nGive a DotAgent repo name ALONE — "agents sync system" / "agents sync user" / "agents sync <alias>" — to git-sync that one repo: refuse if the tree is dirty, else git pull --rebase against origin. The user repo and extra aliases also push local commits up; the system repo is a pull-only mirror.\n\nWith NO agent, runs the umbrella verb: fetch the config repos then reconcile them into every installed agent. Secrets and sessions are opt-in — add --secrets to pull secret bundles or --sessions to sync transcripts (sessions are also queryable live via "agents sessions --host <machine>"). Also: --cloud (fetch only), --local (reconcile only).')
44
50
  .option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
45
51
  .option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
46
52
  .option('--repo <name>', 'Scope the sync to a single DotAgent repo: system / user / project / <alias> (also accepted as a positional)')
@@ -60,6 +66,135 @@ export function registerSyncCommand(program) {
60
66
  await runSync(agentSpec, repo, opts);
61
67
  });
62
68
  }
69
+ /**
70
+ * Resolve a DotAgent repo name to its git working directory + whether local
71
+ * commits should be pushed. `system` is a pull-only mirror of the npm-shipped
72
+ * upstream; `user` and enabled extra aliases are user-owned and push. `project`
73
+ * (and unknown names) return null — the project `.agents/` lives inside the
74
+ * user's own project repo and is not independently git-synced here.
75
+ */
76
+ function resolveRepoGitTarget(repo) {
77
+ if (repo === 'system')
78
+ return { dir: getSystemAgentsDir(), push: false };
79
+ if (repo === 'user')
80
+ return { dir: getUserAgentsDir(), push: true };
81
+ const extra = getEnabledExtraRepos().find((e) => e.alias === repo);
82
+ if (extra)
83
+ return { dir: extra.dir, push: true };
84
+ return null;
85
+ }
86
+ /**
87
+ * `agents sync <repo>` — git-sync a single DotAgent repo: refuse on a dirty
88
+ * tree, else pull --rebase against origin, pushing local commits for user-owned
89
+ * repos. Delegates the git work to `syncRepoGit`.
90
+ */
91
+ async function runRepoGitSync(repo, quiet, outLog, errLog) {
92
+ const target = resolveRepoGitTarget(repo);
93
+ if (!target) {
94
+ errLog(chalk.red(`The '${repo}' repo isn't independently git-synced.`));
95
+ errLog(chalk.gray('Syncable repos: system (pull-only), user, and enabled extra-repo aliases.'));
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ if (!quiet)
100
+ outLog(chalk.bold(`Syncing ${repo} repo…`) + chalk.gray(` (${target.dir})`));
101
+ const result = await syncRepoGit(target.dir, { push: target.push });
102
+ if (!result.success) {
103
+ errLog(chalk.red(`sync ${repo} failed: ${result.error}`));
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ if (!quiet) {
108
+ const note = result.pushed ? ' · pushed' : ' · pull-only';
109
+ outLog(chalk.green(`✓ ${repo} → ${result.commit}${note}`));
110
+ }
111
+ }
112
+ /** Human label for a repo choice in the interactive picker. */
113
+ function repoChoiceLabel(repo) {
114
+ switch (repo) {
115
+ case 'system': return 'system — shared, npm-shipped defaults';
116
+ case 'user': return 'user — your ~/.agents config';
117
+ case 'project': return "project — this repo's .agents";
118
+ default: return `${repo} — extra repo`;
119
+ }
120
+ }
121
+ /**
122
+ * Interactive bare `agents sync` (TTY, no flags): two checklists — which
123
+ * DotAgent repos to sync FROM, and which installed agents to sync INTO. Then
124
+ * freshen the selected git-syncable repos (pull-only) and reconcile the chosen
125
+ * repos' resources into each selected agent's default version, registering
126
+ * hooks so synced hook scripts actually fire.
127
+ */
128
+ async function runInteractiveReconcile(opts, outLog, errLog) {
129
+ const { checkbox } = await import('@inquirer/prompts');
130
+ const cwd = opts.cwd || process.cwd();
131
+ const installedAgents = ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0);
132
+ if (installedAgents.length === 0) {
133
+ errLog(chalk.red('No agents installed. Install one: agents add claude@latest'));
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ let repos;
138
+ let agents;
139
+ try {
140
+ repos = await checkbox({
141
+ message: 'Sync resources FROM which repos?',
142
+ choices: listRepoNames().map((r) => ({ value: r, name: repoChoiceLabel(r), checked: true })),
143
+ });
144
+ if (repos.length === 0) {
145
+ outLog(chalk.gray('No repos selected. Nothing to do.'));
146
+ return;
147
+ }
148
+ agents = await checkbox({
149
+ message: 'Sync INTO which agents?',
150
+ choices: installedAgents.map((a) => ({ value: a, name: agentLabel(a), checked: true })),
151
+ });
152
+ if (agents.length === 0) {
153
+ outLog(chalk.gray('No agents selected. Nothing to do.'));
154
+ return;
155
+ }
156
+ }
157
+ catch (e) {
158
+ if (isPromptCancelled(e)) {
159
+ outLog(chalk.gray('Cancelled. No changes made.'));
160
+ return;
161
+ }
162
+ throw e;
163
+ }
164
+ // 1. Freshen the selected git-syncable repos (pull-only; `project` has no
165
+ // independent remote). Failures are non-fatal — reconcile still runs.
166
+ for (const repo of repos) {
167
+ const target = resolveRepoGitTarget(repo);
168
+ if (!target)
169
+ continue;
170
+ const res = await syncRepoGit(target.dir, { push: false });
171
+ if (res.success)
172
+ outLog(chalk.gray(` pulled ${repo} → ${res.commit}`));
173
+ else
174
+ outLog(chalk.yellow(` ! ${repo}: ${(res.error ?? 'pull failed').split('\n')[0]}`));
175
+ }
176
+ // 2. One selection spanning the chosen repos.
177
+ const selection = mergeRepoScopedSelections(repos, cwd);
178
+ const hasResources = selection.memory === 'all' || Object.entries(selection).some(([kind, v]) => kind !== 'memory' && Array.isArray(v) && v.length > 0);
179
+ if (!hasResources) {
180
+ outLog(chalk.gray(`Nothing from ${repos.join(', ')} to sync.`));
181
+ return;
182
+ }
183
+ // 3. Reconcile into each selected agent's default (or sole) version, then
184
+ // register hooks so synced hook scripts fire.
185
+ const hookManifest = parseHookManifest();
186
+ const hookCapable = new Set(capableAgents('hooks'));
187
+ for (const agentId of agents) {
188
+ const version = resolveVersion(agentId, cwd) || listInstalledVersions(agentId).slice(-1)[0];
189
+ if (!version)
190
+ continue;
191
+ const result = syncResourcesToVersion(agentId, version, selection, { cwd });
192
+ printSyncDetail(result, agentId, version, cwd);
193
+ if (result.hooks && hookCapable.has(agentId) && Object.keys(hookManifest).length > 0) {
194
+ registerHooksToSettings(agentId, getVersionHomePath(agentId, version), hookManifest);
195
+ }
196
+ }
197
+ }
63
198
  /**
64
199
  * The umbrella verb: bare `agents sync` (no agent) makes this machine current.
65
200
  * Resolves the flags + a secrets passphrase (env-only for now; tokenized auth
@@ -67,6 +202,14 @@ export function registerSyncCommand(program) {
67
202
  * a one-line summary. Stage failures are non-fatal and surfaced as warnings.
68
203
  */
69
204
  async function runUmbrella(opts, quiet, outLog, errLog) {
205
+ // Interactive bare `agents sync` (a TTY, no --yes, no scope flag) drops into
206
+ // the two-checklist picker: which repos to sync from, which agents to sync
207
+ // into. Any explicit flag or --yes keeps the non-interactive umbrella below.
208
+ const anyExplicitFlag = !!(opts.repos || opts.secrets || opts.sessions || opts.cloud || opts.local);
209
+ if (!quiet && !opts.yes && !anyExplicitFlag && isInteractiveTerminal()) {
210
+ await runInteractiveReconcile(opts, outLog, errLog);
211
+ return;
212
+ }
70
213
  const flags = {
71
214
  repos: opts.repos,
72
215
  secrets: opts.secrets,
@@ -126,6 +269,15 @@ async function runSync(agentSpec, repoArg, opts) {
126
269
  // when an '@' was actually typed, keeping bare `claude` on the
127
270
  // default-version path.
128
271
  let selector;
272
+ // Repo-level git sync: a DotAgent repo name given ALONE (no agent, no second
273
+ // positional) means "git-sync that repo" — pull --rebase, and push for
274
+ // user-owned repos. This is distinct from the [repo] resource-scoping arg
275
+ // below, and it precedes agent-spec parsing because repo names like
276
+ // "system"/"user" would otherwise fail parseAgentSpec.
277
+ if (agentSpec && !opts.agent && !repoArg && listRepoNames().includes(agentSpec)) {
278
+ await runRepoGitSync(agentSpec, quiet, outLog, errLog);
279
+ return;
280
+ }
129
281
  if (agentSpec) {
130
282
  const parsed = parseAgentSpec(agentSpec);
131
283
  if (!parsed) {
@@ -5,6 +5,8 @@ import * as path from 'path';
5
5
  import { AgentManager, checkAllClis, checkCliSignedIn, getAgentsDir, resolveSignInAdvisory, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
6
6
  import { resolveProvider } from '../lib/cloud/registry.js';
7
7
  import { runSupervisor } from '../lib/teams/supervisor.js';
8
+ import { debug } from '../lib/teams/debug.js';
9
+ import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
8
10
  import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary, } from '../lib/teams/api.js';
9
11
  import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
10
12
  import { setHelpSections } from '../lib/help.js';
@@ -243,6 +245,105 @@ async function runOneWave(mgr, team, json) {
243
245
  }
244
246
  }
245
247
  }
248
+ /**
249
+ * Path to the pr-watch dedupe-state file for a team. Persists the set of
250
+ * already-handled check/comment dedupe keys so a restart of `teams pr-watch`
251
+ * never re-spawns a fix wave for a failure it already reacted to. Lives in the
252
+ * teammate base dir; it's a flat file so loadExistingAgents (dir-only) skips it.
253
+ */
254
+ async function prWatchStatePath(team) {
255
+ const safe = team.replace(/[^A-Za-z0-9_-]/g, '_');
256
+ return path.join(await getAgentsDir(), `pr-watch-${safe}.json`);
257
+ }
258
+ async function loadPrWatchState(team) {
259
+ try {
260
+ const raw = await fs.readFile(await prWatchStatePath(team), 'utf-8');
261
+ const parsed = JSON.parse(raw);
262
+ const handled = new Set(Array.isArray(parsed?.handled) ? parsed.handled : []);
263
+ const waves = new Map(parsed?.waves && typeof parsed.waves === 'object'
264
+ ? Object.entries(parsed.waves).map(([k, v]) => [k, Number(v) || 0])
265
+ : []);
266
+ return { handled, waves };
267
+ }
268
+ catch {
269
+ return { handled: new Set(), waves: new Map() };
270
+ }
271
+ }
272
+ async function savePrWatchState(team, state) {
273
+ try {
274
+ await fs.writeFile(await prWatchStatePath(team), JSON.stringify({ handled: [...state.handled], waves: Object.fromEntries(state.waves) }, null, 2));
275
+ }
276
+ catch (err) {
277
+ debug(`Could not persist pr-watch state for ${team}: ${err.message}`);
278
+ }
279
+ }
280
+ /**
281
+ * Resolve the PRs opened by a team's teammates, de-duplicated by PR URL. A
282
+ * teammate's PR URL comes from its own `pr_url` field or, failing that, the
283
+ * session it ran (detected from the transcript's `gh pr create`). The teammate
284
+ * name is the `--after` anchor for the fix/bugfix teammate we spawn.
285
+ */
286
+ async function resolvePrWatchTargets(mgr, team) {
287
+ const status = await handleStatus(mgr, team, 'all');
288
+ const sessions = await resolveTeammateSessions(status.agents);
289
+ const byPr = new Map();
290
+ for (const a of status.agents) {
291
+ const prUrl = a.pr_url || sessions.get(a.agent_id)?.prUrl || null;
292
+ if (!prUrl)
293
+ continue;
294
+ if (byPr.has(prUrl))
295
+ continue; // first teammate to own a PR is its source
296
+ byPr.set(prUrl, { prUrl, sourceTeammate: a.name ?? null });
297
+ }
298
+ return [...byPr.values()];
299
+ }
300
+ /**
301
+ * React to one decided pr-watch action by spawning a follow-up teammate on the
302
+ * same PR — a ci-fix teammate on RED CI, or a `bugfix` teammate on a new review
303
+ * comment — linked `--after` the teammate that opened the PR so it slots into
304
+ * the same DAG the supervisor already drains. Returns the teammate label.
305
+ */
306
+ async function reactWithTeammate(mgr, team, action, prompt) {
307
+ // Unique name per reaction. The dedupe key is stable across waves (keyed on the
308
+ // check NAME), so the wave number is what keeps successive fixers' names — and
309
+ // their worktrees — distinct within the team.
310
+ const uniq = action.dedupeKey.replace(/[^A-Za-z0-9]/g, '').slice(-10) || `${action.wave}`;
311
+ const slug = `${uniq}-w${action.wave}`;
312
+ const name = action.kind === 'ci-fix' ? `cifix-${slug}` : `bugfix-${slug}`;
313
+ const taskType = action.kind === 'review-fix' ? 'bugfix' : null;
314
+ // Link --after the source teammate when it's a real, resolvable sibling; a
315
+ // missing source means the PR couldn't be traced to a named teammate, so the
316
+ // fix runs immediately with no dependency edge.
317
+ const after = [];
318
+ if (action.sourceTeammate) {
319
+ const resolved = await mgr.resolveAgentIdInTask(team, action.sourceTeammate);
320
+ if (resolved.kind === 'ok')
321
+ after.push(action.sourceTeammate);
322
+ }
323
+ // Isolate each fixer in its own worktree so concurrent `gh pr checkout`s (across
324
+ // PRs, or across waves) never clash in a shared cwd. Requires a git repo — which
325
+ // pr-watch always is, since it operates on PRs; fall back to the cwd only when
326
+ // the checkout somehow isn't a repo.
327
+ const baseCwd = process.cwd();
328
+ let worktreeName = null;
329
+ let worktreePath = null;
330
+ let cwd = baseCwd;
331
+ if (await isGitRepo(baseCwd)) {
332
+ try {
333
+ worktreeName = `prwatch-${name}`;
334
+ worktreePath = await createWorktree(baseCwd, worktreeName);
335
+ cwd = worktreePath;
336
+ }
337
+ catch (err) {
338
+ debug(`pr-watch: could not create worktree for ${name}: ${err.message}`);
339
+ worktreeName = null;
340
+ worktreePath = null;
341
+ cwd = baseCwd;
342
+ }
343
+ }
344
+ const result = await handleSpawn(mgr, team, 'claude', prompt, cwd, 'edit', 'medium', null, cwd, null, name, after, null, null, taskType, null, null, null, null, worktreeName, worktreePath);
345
+ return result.name ?? shortId(result.agent_id);
346
+ }
246
347
  // Pick the display handle for a teammate: explicit teammate name, Claude
247
348
  // session label, then the 8-char UUID prefix.
248
349
  function handle(a) {
@@ -1193,10 +1294,22 @@ export function registerTeamsCommands(program) {
1193
1294
  const intervalMs = Math.max(1000, Number.parseInt(opts.interval, 10) * 1000 || 8000);
1194
1295
  const maxWaves = Math.max(1, Number.parseInt(opts.maxWaves, 10) || 1000);
1195
1296
  const json = isJsonMode(opts);
1297
+ // Live budget kill-switch (issue #399). Dormant when no caps set — the
1298
+ // factory dropping this in returns null and runSupervisor short-circuits.
1299
+ const { createTeamBudgetWatcher } = await import('../lib/budget/live-team.js');
1300
+ const budgetWatcher = createTeamBudgetWatcher({
1301
+ manager: mgr,
1302
+ team,
1303
+ cwd: process.cwd(),
1304
+ onBreach: (b) => {
1305
+ process.stderr.write(`[budget] cap ${b.cap} exceeded ($${b.spend.toFixed(2)} > $${b.limit.toFixed(2)}) — stopping team ${team}\n`);
1306
+ },
1307
+ });
1196
1308
  const result = await runSupervisor(mgr, {
1197
1309
  team,
1198
1310
  intervalMs,
1199
1311
  maxWaves,
1312
+ budgetWatcher,
1200
1313
  onWave: (s) => {
1201
1314
  const ts = s.timestamp.slice(11, 19);
1202
1315
  if (json) {
@@ -1222,6 +1335,110 @@ export function registerTeamsCommands(program) {
1222
1335
  else if (result.stoppedBy === 'signal') {
1223
1336
  console.error(chalk.yellow(`Stopped by signal after ${result.waves} waves.`));
1224
1337
  }
1338
+ else if (result.stoppedBy === 'budget') {
1339
+ const b = result.budgetBreach;
1340
+ console.error(chalk.red(`Budget kill-switch tripped after ${result.waves} waves` +
1341
+ (b ? ` (cap ${b.cap}: $${b.spend.toFixed(2)} > $${b.limit.toFixed(2)})` : '') +
1342
+ `.`));
1343
+ process.exitCode = 7; // Mirrors BUDGET_KILL_EXIT_CODE for CI/headless.
1344
+ }
1345
+ });
1346
+ // pr-watch — autonomous PR lifecycle (issue #338)
1347
+ addHostOption(teams.command('pr-watch [team]'))
1348
+ .description('Watch the PRs a team opened and react autonomously: RED CI -> spawn a fix teammate with the failure logs; new review comment -> route a bugfix teammate. Both slot into the team DAG (visible in `teams status`).')
1349
+ .option('--interval <seconds>', 'Seconds between polls (default 30)', '30')
1350
+ .option('--max-polls <n>', 'Stop after this many polls (default: run until Ctrl-C)', '0')
1351
+ .option('--max-waves <n>', `Fix waves per PR before escalating to a human (default ${DEFAULT_MAX_WAVES})`, String(DEFAULT_MAX_WAVES))
1352
+ .option('--once', 'Poll a single time and exit (equivalent to --max-polls 1)')
1353
+ .option('--json', 'Emit one JSON line per pr-watch event')
1354
+ .action(async (team, opts) => {
1355
+ const mgr = mkManager();
1356
+ if (!team) {
1357
+ const picked = await pickTeamOr(mgr, 'agents teams pr-watch');
1358
+ if (!picked)
1359
+ return;
1360
+ team = picked;
1361
+ }
1362
+ const resolvedTeam = team;
1363
+ const intervalMs = Math.max(1000, (Number.parseInt(opts.interval, 10) || 30) * 1000);
1364
+ const maxPolls = opts.once ? 1 : Math.max(0, Number.parseInt(opts.maxPolls, 10) || 0);
1365
+ const maxWaves = Math.max(1, Number.parseInt(opts.maxWaves, 10) || DEFAULT_MAX_WAVES);
1366
+ const json = isJsonMode(opts);
1367
+ const { handled, waves } = await loadPrWatchState(resolvedTeam);
1368
+ let stopSignal = false;
1369
+ const onSig = () => { stopSignal = true; };
1370
+ process.once('SIGINT', onSig);
1371
+ process.once('SIGTERM', onSig);
1372
+ const emit = (e) => {
1373
+ if (json) {
1374
+ console.log(JSON.stringify(e));
1375
+ return;
1376
+ }
1377
+ const ts = e.timestamp.slice(11, 19);
1378
+ if (e.type === 'poll') {
1379
+ console.log(`[${ts}] polled ${chalk.cyan(resolvedTeam)} — ${e.targets} PR(s) under watch`);
1380
+ }
1381
+ else if (e.type === 'spawned') {
1382
+ const verb = e.action.kind === 'ci-fix' ? 'CI-fix' : 'bugfix';
1383
+ const detail = e.action.kind === 'ci-fix'
1384
+ ? `check ${chalk.yellow(e.action.check.name)}`
1385
+ : `comment ${chalk.yellow('#' + e.action.comment.id)}`;
1386
+ console.log(`[${ts}] ${chalk.green('spawned')} ${verb} teammate ${chalk.cyan(e.label ?? '?')} ` +
1387
+ `(wave ${e.action.wave}/${maxWaves}) for ${detail} on ${e.action.prUrl}`);
1388
+ }
1389
+ else if (e.type === 'needs-human') {
1390
+ console.error(`[${ts}] ${chalk.red('needs human')} — ${e.prUrl} still failing after ${e.waves} wave(s) ` +
1391
+ `(${e.subject}). Not spawning again; hand it to a human.`);
1392
+ }
1393
+ else if (e.type === 'error') {
1394
+ console.error(`[${ts}] ${chalk.red('error')} on ${e.prUrl}: ${e.message}`);
1395
+ }
1396
+ };
1397
+ // A fixer's dedupe guard clears once it settles, so a still-red check can
1398
+ // spawn its next (budget-bounded) wave. Terminal = completed/failed/stopped.
1399
+ const reactionSettled = async (label) => {
1400
+ const roster = await mgr.listByTask(resolvedTeam);
1401
+ const teammate = roster.find((a) => a.name === label || shortId(a.agentId) === label);
1402
+ if (!teammate)
1403
+ return false;
1404
+ const s = String(teammate.status);
1405
+ return s === 'completed' || s === 'failed' || s === 'stopped';
1406
+ };
1407
+ try {
1408
+ const result = await runPrWatch({
1409
+ resolveTargets: async () => {
1410
+ // Drive the DAG each pass so fix/bugfix teammates staged --after a
1411
+ // now-completed source teammate actually launch — no separate
1412
+ // `teams start --watch` needed.
1413
+ await mgr.rescanFromDisk();
1414
+ await mgr.startReady(resolvedTeam);
1415
+ return resolvePrWatchTargets(mgr, resolvedTeam);
1416
+ },
1417
+ react: (action, prompt) => reactWithTeammate(mgr, resolvedTeam, action, prompt),
1418
+ reactionSettled,
1419
+ onEvent: emit,
1420
+ }, {
1421
+ intervalMs,
1422
+ maxPolls,
1423
+ maxWaves,
1424
+ handled,
1425
+ waves,
1426
+ shouldStop: () => stopSignal,
1427
+ });
1428
+ await savePrWatchState(resolvedTeam, { handled: result.handled, waves: result.waves });
1429
+ if (!json) {
1430
+ const humanNote = result.neededHuman > 0
1431
+ ? ` ${result.neededHuman} PR(s) escalated to a human.`
1432
+ : '';
1433
+ console.log(chalk.gray(`pr-watch stopped (${result.stoppedBy}) after ${result.polls} poll(s); ` +
1434
+ `spawned ${result.spawned} follow-up teammate(s).${humanNote}`));
1435
+ console.log(chalk.gray(`Check the team: agents teams status ${resolvedTeam}`));
1436
+ }
1437
+ }
1438
+ finally {
1439
+ process.off('SIGINT', onSig);
1440
+ process.off('SIGTERM', onSig);
1441
+ }
1225
1442
  });
1226
1443
  // stop
1227
1444
  addHostOption(teams.command('stop [team] [teammate]'))
@@ -155,10 +155,8 @@ async function versionPruneAction(specs, options, commandName) {
155
155
  console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${v} to trash`));
156
156
  moved.push({ agent, version: v });
157
157
  }
158
- if (globalDefault && toRemove.includes(globalDefault)) {
159
- setGlobalDefault(agent, undefined);
160
- console.log(chalk.yellow(`Default version removed. Run: agents use ${agent}@<version> to set a new default`));
161
- }
158
+ // Default reassignment/clearing is handled at the source in
159
+ // removeVersion() so it applies to every removal path uniformly.
162
160
  const remaining = listInstalledVersions(agent);
163
161
  if (remaining.length === 0) {
164
162
  removeShim(agent);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `agents watchdog` — the watchdog CONSUMER (RUSH-1415).
3
+ *
4
+ * Runs the tick loop that ties the merged pieces together: list active sessions,
5
+ * classify stalls, read the tail, decide (deterministic promise-without-toolcall
6
+ * by default), run the resolver safety gate, and inject "Continue." into the EXACT
7
+ * split — all without the Swift menu-bar. See src/lib/watchdog/runner.ts.
8
+ *
9
+ * agents watchdog one tick, dry — prints what it WOULD nudge/skip and why
10
+ * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
+ * agents watchdog --watch daemon loop (poll every --interval)
12
+ * agents watchdog --json machine-readable tick output (for the menu-bar)
13
+ * agents watchdog enable|disable flip the global auto-nudge sentinel (default OFF)
14
+ * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
+ */
16
+ import type { Command } from 'commander';
17
+ /** Register the `agents watchdog` command tree. */
18
+ export declare function registerWatchdogCommand(program: Command): void;