@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,112 @@
1
+ import { ALL_AGENT_IDS } from './agents.js';
2
+ import { getGlobalDefault, listInstalledVersions } from './versions.js';
3
+ import { loadManifest, isStale } from './staleness/index.js';
4
+ import { diffVersionResources } from './doctor-diff.js';
5
+ import { diffVersionCommands, iterCommandsCapableVersions } from './commands.js';
6
+ import { diffVersionSkills, iterSkillsCapableVersions } from './skills.js';
7
+ import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from './hooks.js';
8
+ // Lines naming exactly what's out of sync for a version, plugins prioritized:
9
+ // each divergent plugin gets its own line with specifics (stale mirror version,
10
+ // invalid manifest, or the bundled skills/commands missing from the mirror —
11
+ // the system-repo plugin content that matters most). Other kinds collapse to
12
+ // compact counts so the readout stays scannable.
13
+ export function divergenceLines(report) {
14
+ const lines = [];
15
+ for (const p of report.kinds.plugins) {
16
+ if (p.status === 'missing')
17
+ lines.push(`plugin ${p.name} — not installed`);
18
+ else if (p.status === 'diff')
19
+ lines.push(`plugin ${p.name} — ${p.detail ?? 'mirror drifted'}`);
20
+ }
21
+ for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents']) {
22
+ const rows = report.kinds[kind];
23
+ const miss = rows.filter((r) => r.status === 'missing').length;
24
+ const dif = rows.filter((r) => r.status === 'diff').length;
25
+ const bits = [];
26
+ if (miss)
27
+ bits.push(`${miss} missing`);
28
+ if (dif)
29
+ bits.push(`${dif} drifted`);
30
+ if (bits.length)
31
+ lines.push(`${kind.padEnd(11)} ${bits.join(' · ')}`);
32
+ }
33
+ return lines;
34
+ }
35
+ export function checkSyncStatus(cwd) {
36
+ const rows = [];
37
+ // Every installed version, not just the default — a stale NON-default version
38
+ // (e.g. one you launched from yesterday) is exactly the rot that silently
39
+ // serves outdated/invalid resources and that `--fix` now heals. Hiding it here
40
+ // is why that class of bug went unnoticed.
41
+ for (const agent of ALL_AGENT_IDS) {
42
+ const def = getGlobalDefault(agent);
43
+ for (const version of listInstalledVersions(agent)) {
44
+ const manifest = loadManifest(agent, version);
45
+ const status = !manifest
46
+ ? 'never-synced'
47
+ : isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
48
+ const row = { agent, version, status, isDefault: version === def };
49
+ if (status === 'stale') {
50
+ // Resolve the specifics against non-project layers (the global home is
51
+ // never reconciled against per-cwd project resources).
52
+ const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
53
+ const lines = divergenceLines(report);
54
+ if (lines.length)
55
+ row.divergence = lines;
56
+ }
57
+ rows.push(row);
58
+ }
59
+ }
60
+ return rows;
61
+ }
62
+ export function countOrphans() {
63
+ const byKey = new Map();
64
+ const ensure = (agent, version) => {
65
+ const key = `${agent}@${version}`;
66
+ let row = byKey.get(key);
67
+ if (!row) {
68
+ row = { agent, version, commands: 0, skills: 0, hooks: 0 };
69
+ byKey.set(key, row);
70
+ }
71
+ return row;
72
+ };
73
+ for (const { agent, version } of iterCommandsCapableVersions()) {
74
+ const diff = diffVersionCommands(agent, version);
75
+ if (diff.orphans.length > 0)
76
+ ensure(agent, version).commands = diff.orphans.length;
77
+ }
78
+ for (const { agent, version } of iterSkillsCapableVersions()) {
79
+ const diff = diffVersionSkills(agent, version);
80
+ if (diff.orphans.length > 0)
81
+ ensure(agent, version).skills = diff.orphans.length;
82
+ }
83
+ // Orphan hooks are scripts in the version home that no agents.yaml/hooks.yaml
84
+ // entry registers — so the registrar never wires them to an event and they
85
+ // never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
86
+ // which false-flags valid system-sourced registered hooks.)
87
+ for (const { agent, version } of iterHooksCapableVersions()) {
88
+ const dead = listUnmanagedHooksInVersionHome(agent, version);
89
+ if (dead.length > 0)
90
+ ensure(agent, version).hooks = dead.length;
91
+ }
92
+ return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
93
+ }
94
+ /**
95
+ * Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
96
+ * to a summary with a single `hasDrift` boolean. The gate `agents check` maps
97
+ * to an exit code; the readout `agents doctor` renders in full.
98
+ */
99
+ export function computeDrift(cwd) {
100
+ const syncRows = checkSyncStatus(cwd);
101
+ const orphanRows = countOrphans();
102
+ const staleCount = syncRows.filter((r) => r.status === 'stale').length;
103
+ const neverSyncedCount = syncRows.filter((r) => r.status === 'never-synced').length;
104
+ return {
105
+ syncRows,
106
+ orphanRows,
107
+ staleCount,
108
+ neverSyncedCount,
109
+ orphanVersionCount: orphanRows.length,
110
+ hasDrift: syncRows.some((r) => r.status !== 'fresh'),
111
+ };
112
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized event logging for agents-cli.
3
3
  *
4
- * Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
4
+ * Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
5
5
  * with automatic daily rotation and rich metadata for debugging/auditing.
6
6
  *
7
7
  * Features:
@@ -11,7 +11,7 @@
11
11
  * - Permissions: logs dir is 0700, files are 0600 (owner-only)
12
12
  * - Performance tracking: withTiming() wrapper for any async function
13
13
  */
14
- export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
14
+ export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
15
15
  export interface EventMeta {
16
16
  ts: string;
17
17
  tz: string;
@@ -22,12 +22,18 @@ export interface EventMeta {
22
22
  pid: number;
23
23
  ppid: number;
24
24
  event: EventType;
25
+ osUser: string;
26
+ transport: 'local' | 'ssh';
27
+ sshClientIp?: string;
25
28
  }
26
29
  export interface EventPayload {
27
30
  agent?: string;
28
31
  version?: string;
29
32
  sessionId?: string;
30
33
  cwd?: string;
34
+ /** Top-level command group, e.g. 'teams', 'secrets' — the audit filter key. */
35
+ module?: string;
36
+ /** Full command path, e.g. 'teams create', 'secrets get'. */
31
37
  command?: string;
32
38
  args?: string[];
33
39
  input?: string;
@@ -155,6 +161,7 @@ export declare function query(options: {
155
161
  eventTypes?: EventType[];
156
162
  agent?: string;
157
163
  command?: string;
164
+ module?: string;
158
165
  limit?: number;
159
166
  }): EventRecord[];
160
167
  /**
@@ -170,4 +177,4 @@ export declare function getTimingStats(label: string, options?: {
170
177
  p50Ms: number;
171
178
  p95Ms: number;
172
179
  } | null;
173
- export declare const LOGS_PATH: string;
180
+ export declare function getLogsPath(): string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralized event logging for agents-cli.
3
3
  *
4
- * Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
4
+ * Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
5
5
  * with automatic daily rotation and rich metadata for debugging/auditing.
6
6
  *
7
7
  * Features:
@@ -15,9 +15,26 @@ import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import * as os from 'os';
17
17
  import { createHash } from 'node:crypto';
18
+ import { parseSshConnection } from './session/provenance.js';
19
+ import { getLogsDir } from './state.js';
18
20
  // ─── Constants ────────────────────────────────────────────────────────────────
19
- // Logs live under the cache bucket — they're regenerable telemetry.
20
- const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
21
+ // Logs live under the cache bucket — they're regenerable telemetry. Route
22
+ // through state's canonical home anchor (HOME override → os.homedir()) rather
23
+ // than a bare os.homedir(): on Windows os.homedir() reads USERPROFILE and
24
+ // ignores a HOME override, so a test (or any caller) that redirects HOME would
25
+ // have its events silently written to the real profile instead. state.getLogsDir()
26
+ // honors the HOME override on every platform while falling back to os.homedir()
27
+ // (== USERPROFILE on Windows) in production where HOME is unset.
28
+ //
29
+ // Resolved lazily + memoized: importing this module must NOT call getLogsDir()
30
+ // at eval time. events.ts is pulled in transitively (skills/versions/exec/
31
+ // runner), and several tests mock './state.js' with partial factories that omit
32
+ // getLogsDir — an eager call would crash those on import. Deferring to first use
33
+ // keeps a bare import side-effect-free while staying a one-time resolution.
34
+ let _logsDir;
35
+ function logsDir() {
36
+ return (_logsDir ??= getLogsDir());
37
+ }
21
38
  /** Default retention period in days. */
22
39
  const DEFAULT_RETENTION_DAYS = 7;
23
40
  /** Default max length for truncated strings. */
@@ -53,16 +70,16 @@ function getLogFilePath(date = new Date()) {
53
70
  const yyyy = date.getFullYear();
54
71
  const mm = String(date.getMonth() + 1).padStart(2, '0');
55
72
  const dd = String(date.getDate()).padStart(2, '0');
56
- return path.join(LOGS_DIR, `events-${yyyy}-${mm}-${dd}.jsonl`);
73
+ return path.join(logsDir(), `events-${yyyy}-${mm}-${dd}.jsonl`);
57
74
  }
58
75
  function ensureLogsDir() {
59
- if (!fs.existsSync(LOGS_DIR)) {
60
- fs.mkdirSync(LOGS_DIR, { recursive: true, mode: DIR_MODE });
76
+ if (!fs.existsSync(logsDir())) {
77
+ fs.mkdirSync(logsDir(), { recursive: true, mode: DIR_MODE });
61
78
  }
62
79
  else {
63
80
  // Ensure permissions are correct on existing dir
64
81
  try {
65
- fs.chmodSync(LOGS_DIR, DIR_MODE);
82
+ fs.chmodSync(logsDir(), DIR_MODE);
66
83
  }
67
84
  catch {
68
85
  // May fail if not owner
@@ -130,6 +147,30 @@ function truncatePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
130
147
  }
131
148
  return result;
132
149
  }
150
+ /**
151
+ * Who is running this process and from where. Derived once per process from the
152
+ * OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
153
+ * cached — provenance can't change mid-process, so every emit() pays for it once.
154
+ */
155
+ let _origin;
156
+ function auditOrigin() {
157
+ if (_origin)
158
+ return _origin;
159
+ let osUser = 'unknown';
160
+ try {
161
+ osUser = os.userInfo().username;
162
+ }
163
+ catch {
164
+ // Container/edge cases where the uid has no passwd entry.
165
+ }
166
+ const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
167
+ _origin = {
168
+ osUser,
169
+ transport: ssh ? 'ssh' : 'local',
170
+ ...(ssh ? { sshClientIp: ssh.clientIp } : {}),
171
+ };
172
+ return _origin;
173
+ }
133
174
  // ─── Core API ─────────────────────────────────────────────────────────────────
134
175
  /**
135
176
  * Emit a structured event to the daily log file.
@@ -152,23 +193,33 @@ export function emit(event, payload = {}) {
152
193
  pid: process.pid,
153
194
  ppid: process.ppid,
154
195
  event,
196
+ ...auditOrigin(),
155
197
  ...truncatePayload(payload),
156
198
  };
157
199
  const line = JSON.stringify(record) + '\n';
158
200
  const logPath = getLogFilePath();
201
+ const isNew = !fs.existsSync(logPath);
159
202
  fs.appendFileSync(logPath, line, { mode: FILE_MODE });
160
- // Ensure file permissions (appendFileSync doesn't respect mode on existing files)
161
- try {
162
- fs.chmodSync(logPath, FILE_MODE);
163
- }
164
- catch {
165
- // May fail if not owner
203
+ // appendFileSync's mode only applies when it CREATES the file, so we chmod
204
+ // to guarantee 0600 — but only on first write to a given path this process.
205
+ // command.start/end fire on every invocation; chmod-per-append would double
206
+ // the syscalls on the hot path for no gain (perms don't drift mid-run).
207
+ if (isNew || logPath !== _chmoddedPath) {
208
+ _chmoddedPath = logPath;
209
+ try {
210
+ fs.chmodSync(logPath, FILE_MODE);
211
+ }
212
+ catch {
213
+ // May fail if not owner
214
+ }
166
215
  }
167
216
  }
168
217
  catch {
169
218
  // Silent failure - logging should never break the CLI
170
219
  }
171
220
  }
221
+ /** Last log path this process chmod'd — avoids a redundant chmod per append. */
222
+ let _chmoddedPath;
172
223
  /**
173
224
  * Convenience wrapper for timed operations.
174
225
  * Returns a function to call when the operation completes.
@@ -360,10 +411,10 @@ export function emitError(err, payload = {}) {
360
411
  */
361
412
  export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
362
413
  try {
363
- if (!fs.existsSync(LOGS_DIR))
414
+ if (!fs.existsSync(logsDir()))
364
415
  return 0;
365
416
  const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
366
- const files = fs.readdirSync(LOGS_DIR).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
417
+ const files = fs.readdirSync(logsDir()).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
367
418
  let removed = 0;
368
419
  for (const file of files) {
369
420
  const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
@@ -372,7 +423,7 @@ export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
372
423
  const [, yyyy, mm, dd] = match;
373
424
  const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
374
425
  if (fileDate.getTime() < cutoff) {
375
- fs.unlinkSync(path.join(LOGS_DIR, file));
426
+ fs.unlinkSync(path.join(logsDir(), file));
376
427
  removed++;
377
428
  }
378
429
  }
@@ -402,34 +453,58 @@ export function maybeRotate() {
402
453
  * @returns Array of event records
403
454
  */
404
455
  export function query(options) {
405
- const { startDate, endDate = new Date(), eventTypes, agent, command, limit } = options;
456
+ const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
406
457
  const results = [];
407
- if (!fs.existsSync(LOGS_DIR))
458
+ if (!fs.existsSync(logsDir()))
408
459
  return results;
409
- const files = fs.readdirSync(LOGS_DIR)
460
+ const files = fs.readdirSync(logsDir())
410
461
  .filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
411
462
  .sort()
412
463
  .reverse();
464
+ // Coarse file skip works on whole days, so floor the bounds to midnight —
465
+ // otherwise a sub-day window (`--since 2h` at 15:00 → startDate 13:00) would
466
+ // drop *today's* file, whose date stamps to 00:00. Precise filtering below is
467
+ // per-record on `ts`.
468
+ const startDay = startDate
469
+ ? new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
470
+ : undefined;
471
+ const endDay = endDate
472
+ ? new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
473
+ : undefined;
474
+ const startMs = startDate?.getTime();
475
+ const endMs = endDate?.getTime();
413
476
  for (const file of files) {
414
477
  const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
415
478
  if (!match)
416
479
  continue;
417
480
  const [, yyyy, mm, dd] = match;
418
481
  const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
419
- if (startDate && fileDate < startDate)
482
+ if (startDay && fileDate < startDay)
420
483
  continue;
421
- if (endDate && fileDate > endDate)
484
+ if (endDay && fileDate > endDay)
422
485
  continue;
423
- const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf-8');
486
+ const content = fs.readFileSync(path.join(logsDir(), file), 'utf-8');
424
487
  const lines = content.trim().split('\n').filter(Boolean);
425
488
  for (const line of lines.reverse()) {
426
489
  try {
427
490
  const record = JSON.parse(line);
491
+ // Precise per-record window — the file skip above is day-granular only.
492
+ const recMs = Date.parse(record.ts);
493
+ if (startMs !== undefined && !isNaN(recMs) && recMs < startMs)
494
+ continue;
495
+ if (endMs !== undefined && !isNaN(recMs) && recMs > endMs)
496
+ continue;
428
497
  if (eventTypes && !eventTypes.includes(record.event))
429
498
  continue;
430
499
  if (agent && record.agent !== agent)
431
500
  continue;
432
- if (command && record.command !== command)
501
+ // `--command` matches by path prefix on a word boundary, so a coarse
502
+ // "teams" catches "teams create"/"teams remove" while an exact
503
+ // "teams create" stays exact.
504
+ if (command && record.command !== command &&
505
+ !(typeof record.command === 'string' && record.command.startsWith(command + ' ')))
506
+ continue;
507
+ if (module && record.module !== module)
433
508
  continue;
434
509
  results.push(record);
435
510
  if (limit && results.length >= limit) {
@@ -469,4 +544,6 @@ export function getTimingStats(label, options = {}) {
469
544
  };
470
545
  }
471
546
  // ─── Exports ──────────────────────────────────────────────────────────────────
472
- export const LOGS_PATH = LOGS_DIR;
547
+ export function getLogsPath() {
548
+ return logsDir();
549
+ }
@@ -230,6 +230,12 @@ export interface FallbackEntry {
230
230
  agent: AgentId;
231
231
  /** Optional pinned version (e.g. '0.116.0'). When set, takes precedence over the active default. */
232
232
  version?: string;
233
+ /**
234
+ * Env vars merged over options.env for THIS attempt only. Used by profiles
235
+ * with `fallback_model` to swap the model env key (e.g. ANTHROPIC_MODEL) on
236
+ * a same-agent retry without touching auth or base URL.
237
+ */
238
+ envOverride?: Record<string, string>;
233
239
  }
234
240
  /** ExecOptions extended with a fallback chain for rate-limit cascading. */
235
241
  export interface FallbackOptions extends ExecOptions {
@@ -237,6 +243,17 @@ export interface FallbackOptions extends ExecOptions {
237
243
  fallback: FallbackEntry[];
238
244
  /** Fallback requires a prompt -- chain handoff doesn't apply to interactive sessions. */
239
245
  prompt: string;
246
+ /**
247
+ * Optional out-param the caller reads AFTER the call to learn which chain
248
+ * entry actually executed — updated to each entry as it is attempted, so on
249
+ * return it holds the agent+version whose exit code is returned. Lets the
250
+ * audit log record the fallback that really ran, not always the primary
251
+ * (issue #347).
252
+ */
253
+ dispatchSink?: {
254
+ agent?: AgentId;
255
+ version?: string;
256
+ };
240
257
  }
241
258
  /**
242
259
  * Build the prompt handed to the fallback agent when the primary was stopped
package/dist/lib/exec.js CHANGED
@@ -16,6 +16,9 @@ import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
17
17
  import { sanitizeProcessEnv } from './secrets/bundles.js';
18
18
  import { getShimsDir } from './state.js';
19
+ import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
20
+ import { mailboxDir, isValidMailboxId } from './mailbox.js';
21
+ import { composeWin32CommandLine } from './platform/index.js';
19
22
  /**
20
23
  * Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
21
24
  *
@@ -181,6 +184,14 @@ export function buildExecEnv(options) {
181
184
  : (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
182
185
  if (version) {
183
186
  result.CLAUDE_CONFIG_DIR = path.join(getVersionHomePath('claude', version), '.claude');
187
+ // A managed pin lives in a per-version dir; Claude Code's own background
188
+ // auto-updater would rewrite that pinned binary in place (and has left it
189
+ // half-swapped and broken). Disable it so a pin stays a pin. Honor an
190
+ // explicit user value — from process.env (already in result) or from
191
+ // options.env (spread over result below).
192
+ if (result.DISABLE_AUTOUPDATER === undefined) {
193
+ result.DISABLE_AUTOUPDATER = '1';
194
+ }
184
195
  }
185
196
  delete result.CODEX_HOME;
186
197
  delete result.COPILOT_HOME;
@@ -236,6 +247,14 @@ export function buildExecEnv(options) {
236
247
  delete result.COPILOT_HOME;
237
248
  delete result.KIMI_CODE_HOME;
238
249
  }
250
+ // Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
251
+ // knows which box to drain and inject mid-run. Keyed by the session id — the
252
+ // same id the writer resolves via mailboxIdForActiveSession(). A loop run
253
+ // overrides this to its run-level box via options.env (spread below), so all
254
+ // iterations share one inbox.
255
+ if (options.sessionId && isValidMailboxId(options.sessionId)) {
256
+ result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
257
+ }
239
258
  return {
240
259
  ...result,
241
260
  ...options.env,
@@ -471,11 +490,12 @@ export function buildExecCommand(options) {
471
490
  // on Linux installs where the shims dir isn't on PATH, spawning the bare
472
491
  // versioned name fails with ENOENT even though `agents view` shows the agent.
473
492
  //
474
- // On Windows the shims dir holds a `.cmd` companion next to the bash alias
475
- // (see createVersionedAlias); prefer it so spawn() can launch it (the bash
476
- // script is not directly executable by cmd.exe). When no shim exists on disk
477
- // we fall back to the bare versioned name, which spawnAgent() resolves via
478
- // PATH (+ PATHEXT/shell on Windows).
493
+ // On Windows the alias is materialized as a `.cmd` only (see
494
+ // createVersionedAlias a bash alias next to it would shadow the `.cmd` in
495
+ // cmd.exe/PowerShell name resolution); the extensionless existsSync branch
496
+ // below still matches a legacy install's bash alias. When no shim exists on
497
+ // disk we fall back to the bare versioned name, which spawnAgent() resolves
498
+ // via PATH (+ PATHEXT/shell on Windows).
479
499
  if (options.version && cmd.length > 0) {
480
500
  const versionedName = `${cmd[0]}@${options.version}`;
481
501
  const absPath = path.join(getShimsDir(), versionedName);
@@ -671,7 +691,14 @@ export function resolveShimSpawn(platform, binary, extraArgs) {
671
691
  // Use win32 path semantics regardless of the host running this (the platform
672
692
  // is the parameter, not process.platform) so `C:\...` reads as absolute.
673
693
  const useShell = !path.win32.isAbsolute(binary) || binary.endsWith('.cmd');
674
- return { command: binary, args: extraArgs, shell: useShell };
694
+ if (useShell) {
695
+ // DEP0190-safe: hand cmd.exe ONE fully-quoted command line with an EMPTY
696
+ // args array, so Node never concatenates `extraArgs` (which carry the
697
+ // user's raw prompt/flags) into the shell line unescaped — that concat is
698
+ // both the deprecation and a command-injection surface.
699
+ return { command: composeWin32CommandLine(binary, extraArgs), args: [], shell: true };
700
+ }
701
+ return { command: binary, args: extraArgs, shell: false };
675
702
  }
676
703
  return { command: binary, args: extraArgs, shell: false };
677
704
  }
@@ -704,6 +731,21 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
704
731
  const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
705
732
  return new Promise((resolve) => {
706
733
  const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
734
+ // Record the launch so `ag sessions --active` can attribute the agent
735
+ // process to its cwd (and exact session when the caller passed
736
+ // --session-id). Vital on Windows, where there is no lsof to recover a
737
+ // foreign process's cwd. On the shell path this pid is the cmd.exe
738
+ // wrapper, not the agent binary — the active scan resolves that by
739
+ // walking the candidate's ancestors (readAncestorSessionEntry).
740
+ if (child.pid) {
741
+ writePidSessionEntry({
742
+ pid: child.pid,
743
+ agent,
744
+ sessionId: extractSessionIdArg(rawArgs),
745
+ cwd,
746
+ startedAtMs: Date.now(),
747
+ });
748
+ }
707
749
  child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
708
750
  child.on('error', (err) => {
709
751
  process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
@@ -722,6 +764,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
722
764
  * unbuffered.
723
765
  */
724
766
  async function spawnAgent(options) {
767
+ // Assign a known session id up front for agents that accept one, so the
768
+ // launcher can record an EXACT pid -> session mapping (see pid-registry) —
769
+ // otherwise the headless `ag sessions --active` path can only guess
770
+ // "newest .jsonl in the cwd" and collapses co-located agents onto one row.
771
+ // Claude: `--session-id <uuid>` CREATES the session with that id (wired in
772
+ // buildExecCommand). Skip on resume — the id is the one being resumed.
773
+ if (options.agent === 'claude' && !options.resume && !options.sessionId) {
774
+ options = { ...options, sessionId: randomUUID() };
775
+ }
725
776
  const cmd = buildExecCommand(options);
726
777
  const [executable, ...args] = cmd;
727
778
  const timeoutMs = options.timeout ? parseTimeout(options.timeout) : undefined;
@@ -762,14 +813,32 @@ async function spawnAgent(options) {
762
813
  ? ['inherit', 'inherit', 'inherit']
763
814
  : ['inherit', tapStdout ? 'pipe' : 'inherit', 'pipe'];
764
815
  // On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
765
- // whether addressed by name or absolute path.
816
+ // whether addressed by name or absolute path. On that shell path, compose a
817
+ // single fully-quoted command line and pass an EMPTY args array (see
818
+ // composeWin32CommandLine) so Node never concatenates the args array — which
819
+ // carries the user's prompt — into the cmd.exe line unescaped (DEP0190 +
820
+ // command injection).
766
821
  const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
767
- const child = spawn(executable, args, {
822
+ const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
823
+ const spawnArgs = useShell ? [] : args;
824
+ const child = spawn(spawnCommand, spawnArgs, {
768
825
  cwd: options.cwd || process.cwd(),
769
826
  stdio,
770
827
  env: buildExecEnv(options),
771
828
  shell: useShell,
772
829
  });
830
+ // Record this launch so `ag sessions --active` can map the pid to its exact
831
+ // session (sessionId is set for Claude above) instead of guessing the newest
832
+ // .jsonl in the cwd — the collapse that made co-located agents indistinguishable.
833
+ // Best-effort: pruned when the pid dies; a failed write just degrades to the heuristic.
834
+ writePidSessionEntry({
835
+ pid: child.pid ?? 0,
836
+ agent: options.agent,
837
+ sessionId: options.sessionId,
838
+ cwd: options.cwd || process.cwd(),
839
+ tmuxPane: process.env.TMUX_PANE,
840
+ startedAtMs: Date.now(),
841
+ });
773
842
  // Mark startup time (time from function call to process spawn)
774
843
  timer.mark('startup');
775
844
  let budgetKilled = false;
@@ -994,9 +1063,20 @@ export async function runWithFallback(options) {
994
1063
  }
995
1064
  }
996
1065
  for (let i = 0; i < chain.length; i++) {
997
- const { agent, version } = chain[i];
1066
+ const { agent, version, envOverride } = chain[i];
1067
+ // Record the entry we're about to attempt so the caller (audit log) sees the
1068
+ // agent+version that actually ran, even after a rate-limit handoff.
1069
+ if (options.dispatchSink) {
1070
+ options.dispatchSink.agent = agent;
1071
+ options.dispatchSink.version = version;
1072
+ }
998
1073
  const pinnedSessionId = agent === 'claude' ? randomUUID() : undefined;
999
- const prompt = prevAgent
1074
+ // Same-host retry (same agent+version as previous entry — used by profile
1075
+ // `fallback_model` swaps) keeps the original prompt: the model changed,
1076
+ // not the CLI, so a `/continue` handoff prompt would be misleading.
1077
+ const prev = i > 0 ? chain[i - 1] : undefined;
1078
+ const sameHostRetry = !!prev && prev.agent === agent && prev.version === version;
1079
+ const prompt = prevAgent && !sameHostRetry
1000
1080
  ? buildFallbackPrompt(prevAgent, prevSessionId, agent, options.prompt)
1001
1081
  : options.prompt;
1002
1082
  const execOpts = {
@@ -1004,12 +1084,18 @@ export async function runWithFallback(options) {
1004
1084
  agent,
1005
1085
  version,
1006
1086
  prompt,
1087
+ env: envOverride ? { ...(options.env ?? {}), ...envOverride } : options.env,
1007
1088
  sessionId: pinnedSessionId ?? (i === 0 ? options.sessionId : undefined),
1008
1089
  };
1009
1090
  const label = version ? `${agent}@${version}` : agent;
1091
+ const modelSwapNote = sameHostRetry && envOverride
1092
+ ? ` (retry with ${Object.entries(envOverride).map(([k, v]) => `${k}=${v}`).join(', ')})`
1093
+ : '';
1010
1094
  const banner = i === 0
1011
1095
  ? `[agents] running ${label}`
1012
- : `[agents] fallback → ${label}`;
1096
+ : sameHostRetry
1097
+ ? `[agents] retry → ${label}${modelSwapNote}`
1098
+ : `[agents] fallback → ${label}`;
1013
1099
  process.stderr.write(`${banner}${pinnedSessionId ? ` (session ${pinnedSessionId.slice(0, 8)})` : ''}\n`);
1014
1100
  let result;
1015
1101
  try {
@@ -1032,7 +1118,9 @@ export async function runWithFallback(options) {
1032
1118
  }
1033
1119
  const next = chain[i + 1];
1034
1120
  const nextLabel = next.version ? `${next.agent}@${next.version}` : next.agent;
1035
- process.stderr.write(`[agents] ${label} hit rate limit. Handing off to ${nextLabel}...\n`);
1121
+ const nextSameHost = next.agent === agent && next.version === version;
1122
+ const handoffVerb = nextSameHost ? 'Retrying on same host' : 'Handing off';
1123
+ process.stderr.write(`[agents] ${label} hit rate limit. ${handoffVerb} to ${nextLabel}...\n`);
1036
1124
  prevAgent = agent;
1037
1125
  prevSessionId = pinnedSessionId;
1038
1126
  }
package/dist/lib/git.d.ts CHANGED
@@ -112,6 +112,11 @@ export declare function isSystemRepoOrigin(dir: string): Promise<boolean>;
112
112
  * Check if repo has uncommitted changes (including untracked files).
113
113
  */
114
114
  export declare function hasLocalChanges(dir: string): Promise<boolean>;
115
+ /**
116
+ * Render an absolute path in ~-relative form with forward slashes, matching the
117
+ * way the rest of the CLI prints home-anchored paths (e.g. `~/.agents/.system`).
118
+ */
119
+ export declare function displayHomePath(dir: string): string;
115
120
  /**
116
121
  * Pull changes in an existing repo.
117
122
  * Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
@@ -121,6 +126,28 @@ export declare function pullRepo(dir: string): Promise<{
121
126
  commit: string;
122
127
  error?: string;
123
128
  }>;
129
+ /**
130
+ * Rebase a repo onto its remote, optionally pushing local commits back up.
131
+ *
132
+ * The one-repo counterpart to `pullRepo` used by `agents sync <repo>`:
133
+ * 1. Refuse if the working tree is dirty (commit or discard first).
134
+ * 2. `git fetch origin` then `git pull --rebase origin <branch>` — rebase, not
135
+ * merge, so a local commit lands cleanly on top of upstream with no merge
136
+ * bubble.
137
+ * 3. When `push` is set, `git push origin <branch>` to send local commits up.
138
+ *
139
+ * The branch is read from the repo's current HEAD (falls back to `main`) rather
140
+ * than hardcoded. System repos pass `push: false` — they are pull-only mirrors
141
+ * of the npm-shipped upstream.
142
+ */
143
+ export declare function syncRepoGit(dir: string, opts: {
144
+ push: boolean;
145
+ }): Promise<{
146
+ success: boolean;
147
+ commit: string;
148
+ pushed: boolean;
149
+ error?: string;
150
+ }>;
124
151
  /**
125
152
  * Get git status for sync display.
126
153
  * Returns files categorized by their status relative to HEAD.