@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
@@ -23,6 +23,7 @@ import * as path from 'path';
23
23
  import { fileURLToPath } from 'url';
24
24
  import { randomBytes } from 'crypto';
25
25
  import { sshExec, SSH_OPTS } from './ssh-exec.js';
26
+ import { backgroundSpawnOptions } from './platform/process.js';
26
27
  import { encodePowerShell } from './browser/drivers/ssh.js';
27
28
  import { getDevice } from './devices/registry.js';
28
29
  import { sshTargetFor } from './devices/connect.js';
@@ -56,7 +57,7 @@ export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
56
57
  const args = buildTunnelArgs(user, host, localPort, remotePort);
57
58
  const tunnel = spawn('ssh', args, {
58
59
  stdio: opts.detached ? 'ignore' : ['ignore', 'ignore', 'pipe'],
59
- detached: Boolean(opts.detached),
60
+ ...(opts.detached ? backgroundSpawnOptions() : { detached: false, windowsHide: true }),
60
61
  });
61
62
  let stderr = '';
62
63
  tunnel.stderr?.on('data', (data) => {
@@ -49,6 +49,7 @@ export declare const loadPrune: ModuleLoader;
49
49
  export declare const loadTrash: ModuleLoader;
50
50
  export declare const loadRestore: ModuleLoader;
51
51
  export declare const loadDoctor: ModuleLoader;
52
+ export declare const loadCheck: ModuleLoader;
52
53
  export declare const loadStatus: ModuleLoader;
53
54
  export declare const loadProfiles: ModuleLoader;
54
55
  export declare const loadSecrets: ModuleLoader;
@@ -57,6 +58,7 @@ export declare const loadHelper: ModuleLoader;
57
58
  export declare const loadMenubar: ModuleLoader;
58
59
  export declare const loadBeta: ModuleLoader;
59
60
  export declare const loadSync: ModuleLoader;
61
+ export declare const loadLock: ModuleLoader;
60
62
  export declare const loadRefreshRules: ModuleLoader;
61
63
  export declare const loadDrive: ModuleLoader;
62
64
  export declare const loadFactory: ModuleLoader;
@@ -66,6 +68,7 @@ export declare const loadBudget: ModuleLoader;
66
68
  export declare const loadAlias: ModuleLoader;
67
69
  export declare const loadPty: ModuleLoader;
68
70
  export declare const loadTmux: ModuleLoader;
71
+ export declare const loadWatchdog: ModuleLoader;
69
72
  export declare const loadBrowser: ModuleLoader;
70
73
  export declare const loadComputer: ModuleLoader;
71
74
  export declare const loadHosts: ModuleLoader;
@@ -79,6 +82,9 @@ export declare const loadSetup: ModuleLoader;
79
82
  export declare const loadSessions: ModuleLoader;
80
83
  export declare const loadTeams: ModuleLoader;
81
84
  export declare const loadCloud: ModuleLoader;
85
+ export declare const loadMessage: ModuleLoader;
86
+ export declare const loadServe: ModuleLoader;
87
+ export declare const loadAudit: ModuleLoader;
82
88
  /**
83
89
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
84
90
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -27,6 +27,7 @@ export const loadPrune = async () => (await import('../../commands/prune.js')).r
27
27
  export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
28
28
  export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
29
29
  export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
30
+ export const loadCheck = async () => (await import('../../commands/check.js')).registerCheckCommand;
30
31
  export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
31
32
  export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
32
33
  export const loadSecrets = async () => (await import('../../commands/secrets.js')).registerSecretsCommands;
@@ -35,6 +36,7 @@ export const loadHelper = async () => (await import('../../commands/helper.js'))
35
36
  export const loadMenubar = async () => (await import('../../commands/menubar.js')).registerMenubarCommands;
36
37
  export const loadBeta = async () => (await import('../../commands/beta.js')).registerBetaCommands;
37
38
  export const loadSync = async () => (await import('../../commands/sync.js')).registerSyncCommand;
39
+ export const loadLock = async () => (await import('../../commands/lock.js')).registerLockCommand;
38
40
  export const loadRefreshRules = async () => (await import('../../commands/refresh-rules.js')).registerRefreshRulesCommand;
39
41
  export const loadDrive = async () => (await import('../../commands/drive.js')).registerDriveCommands;
40
42
  export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
@@ -44,6 +46,7 @@ export const loadBudget = async () => (await import('../../commands/budget.js'))
44
46
  export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
45
47
  export const loadPty = async () => (await import('../../commands/pty.js')).registerPtyCommands;
46
48
  export const loadTmux = async () => (await import('../../commands/tmux.js')).registerTmuxCommands;
49
+ export const loadWatchdog = async () => (await import('../../commands/watchdog.js')).registerWatchdogCommand;
47
50
  export const loadBrowser = async () => (await import('../../commands/browser.js')).registerBrowserCommand;
48
51
  export const loadComputer = async () => (await import('../../commands/computer.js')).registerComputerCommand;
49
52
  export const loadHosts = async () => (await import('../../commands/hosts.js')).registerHostsCommand;
@@ -57,6 +60,9 @@ export const loadSetup = async () => (await import('../../commands/setup.js')).r
57
60
  export const loadSessions = async () => (await import('../../commands/sessions.js')).registerSessionsCommands;
58
61
  export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
59
62
  export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
63
+ export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
64
+ export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
65
+ export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
60
66
  /**
61
67
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
62
68
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -64,7 +70,7 @@ export const loadCloud = async () => (await import('../../commands/cloud.js')).r
64
70
  * inherit the root's custom help formatter rather than getting the per-command
65
71
  * recursive pass. Keeping that ordering preserves their `--help` output exactly.
66
72
  */
67
- export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud']);
73
+ export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
68
74
  /**
69
75
  * User-typed top-level command name -> ordered list of module loaders to run.
70
76
  *
@@ -113,6 +119,7 @@ export const COMMAND_LOADERS = {
113
119
  trash: [loadTrash],
114
120
  restore: [loadRestore],
115
121
  doctor: [loadDoctor],
122
+ check: [loadCheck],
116
123
  status: [loadStatus],
117
124
  profiles: [loadProfiles],
118
125
  secrets: [loadSecrets],
@@ -121,6 +128,7 @@ export const COMMAND_LOADERS = {
121
128
  menubar: [loadMenubar],
122
129
  beta: [loadBeta],
123
130
  sync: [loadSync],
131
+ lock: [loadLock],
124
132
  'refresh-rules': [loadRefreshRules],
125
133
  drive: [loadDrive],
126
134
  factory: [loadFactory],
@@ -130,6 +138,7 @@ export const COMMAND_LOADERS = {
130
138
  alias: [loadAlias],
131
139
  pty: [loadPty],
132
140
  tmux: [loadTmux],
141
+ watchdog: [loadWatchdog],
133
142
  browser: [loadBrowser],
134
143
  computer: [loadComputer],
135
144
  hosts: [loadHosts],
@@ -144,4 +153,7 @@ export const COMMAND_LOADERS = {
144
153
  sessions: [loadSessions],
145
154
  teams: [loadTeams],
146
155
  cloud: [loadCloud],
156
+ message: [loadMessage],
157
+ serve: [loadServe],
158
+ audit: [loadAudit],
147
159
  };
@@ -120,6 +120,8 @@ export declare function getRoutinesDir(): string;
120
120
  export declare function getProjectRoutinesDir(cwd?: string): string | null;
121
121
  /** Path to routine execution logs (~/.agents/.history/runs/). */
122
122
  export declare function getRunsDir(): string;
123
+ /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
124
+ export declare function getMailboxRootDir(): string;
123
125
  /** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
124
126
  export declare function getVersionsDir(): string;
125
127
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
@@ -226,6 +228,17 @@ export declare function getEnabledExtraRepos(): Array<{
226
228
  export declare function ensureAgentsDir(): void;
227
229
  /** Return an empty Meta object used when no agents.yaml exists yet. */
228
230
  export declare function createDefaultMeta(): Meta;
231
+ /**
232
+ * Per-device machine-local version pins — `~/.agents/devices/<machine>/agents.yaml`.
233
+ * Committed and synced, but each machine only ever writes its OWN folder, so
234
+ * pulls never conflict. `<machine>` = machineId() (Tailscale-aligned short name).
235
+ */
236
+ export declare function getDeviceMetaPath(): string;
237
+ /**
238
+ * Machine-local per-version resource tracking — `~/.agents/.history/version-resources.json`.
239
+ * Gitignored (under .history/) and regenerable; never synced.
240
+ */
241
+ export declare function getVersionResourcesPath(): string;
229
242
  /**
230
243
  * Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
231
244
  *
package/dist/lib/state.js CHANGED
@@ -28,6 +28,7 @@ import * as os from 'os';
28
28
  import * as yaml from 'yaml';
29
29
  import { ensureLockTarget, atomicWriteFileSync, withFileLock } from './fs-atomic.js';
30
30
  import { SEEDED_REGISTRIES } from './types.js';
31
+ import { machineId } from './machine-id.js';
31
32
  const HOME = process.env.HOME ?? os.homedir();
32
33
  /**
33
34
  * Compare two filesystem paths for identity, resolving symlinks and (on
@@ -87,6 +88,7 @@ const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
87
88
  const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
88
89
  const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
89
90
  const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
91
+ const MAILBOX_DIR = path.join(HISTORY_DIR, 'mailbox');
90
92
  // Cache bucket (regenerable).
91
93
  const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
92
94
  const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
@@ -309,6 +311,8 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
309
311
  }
310
312
  /** Path to routine execution logs (~/.agents/.history/runs/). */
311
313
  export function getRunsDir() { return RUNS_DIR; }
314
+ /** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
315
+ export function getMailboxRootDir() { return MAILBOX_DIR; }
312
316
  /** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
313
317
  export function getVersionsDir() { return VERSIONS_DIR; }
314
318
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
@@ -495,13 +499,37 @@ function safeMtimeMs(filePath) {
495
499
  return 0;
496
500
  }
497
501
  }
498
- /** Compute the combined cache stamp for the user + system agents.yaml files. */
502
+ /**
503
+ * Per-device machine-local version pins — `~/.agents/devices/<machine>/agents.yaml`.
504
+ * Committed and synced, but each machine only ever writes its OWN folder, so
505
+ * pulls never conflict. `<machine>` = machineId() (Tailscale-aligned short name).
506
+ */
507
+ export function getDeviceMetaPath() {
508
+ return path.join(USER_AGENTS_DIR, 'devices', machineId(), 'agents.yaml');
509
+ }
510
+ /**
511
+ * Machine-local per-version resource tracking — `~/.agents/.history/version-resources.json`.
512
+ * Gitignored (under .history/) and regenerable; never synced.
513
+ */
514
+ export function getVersionResourcesPath() {
515
+ return path.join(HISTORY_DIR, 'version-resources.json');
516
+ }
517
+ /**
518
+ * Combined cache stamp across all four Meta sources: central + system
519
+ * agents.yaml, this machine's device pins, and the version-resources tracking.
520
+ * A delimited string, NOT a numeric sum — summing down-scaled epoch-ms values
521
+ * loses precision (float64 rounds sub-unit terms away at ~1.75e12), so a change
522
+ * in any one file must contribute at full resolution.
523
+ */
499
524
  function currentMetaStamp() {
500
- return safeMtimeMs(META_FILE) + safeMtimeMs(SYSTEM_META_FILE) * 1e-3;
525
+ return safeMtimeMs(META_FILE)
526
+ + '|' + safeMtimeMs(SYSTEM_META_FILE)
527
+ + '|' + safeMtimeMs(getDeviceMetaPath())
528
+ + '|' + safeMtimeMs(getVersionResourcesPath());
501
529
  }
502
530
  /** Memoize a parsed Meta against the current file mtimes. */
503
531
  function rememberMeta(meta) {
504
- metaCache = { mtime: currentMetaStamp(), meta };
532
+ metaCache = { stamp: currentMetaStamp(), meta };
505
533
  return meta;
506
534
  }
507
535
  function withMetaLock(fn) {
@@ -526,11 +554,78 @@ function withMetaLock(fn) {
526
554
  }
527
555
  });
528
556
  }
557
+ /** Atomic write only when the on-disk content differs — avoids needless mtime
558
+ * bumps (which would thrash the meta cache) on no-op field routing. */
559
+ function writeIfChanged(filePath, content) {
560
+ let current = null;
561
+ try {
562
+ current = fs.readFileSync(filePath, 'utf-8');
563
+ }
564
+ catch { /* absent */ }
565
+ if (current === content)
566
+ return;
567
+ atomicWriteFileSync(filePath, content);
568
+ }
569
+ /**
570
+ * Partition the in-memory Meta across three files by sync-domain:
571
+ * - central `~/.agents/agents.yaml` — portable, everything else
572
+ * - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins (per-device)
573
+ * - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
574
+ * All callers funnel through writeMeta → here, so nothing else changes. Empty
575
+ * `agents:` / `versions:` are not written (no empty committed files).
576
+ */
529
577
  function writeMetaUnlocked(meta) {
530
- const content = META_HEADER + yaml.stringify(meta);
531
- atomicWriteFileSync(META_FILE, content);
578
+ const { agents, versions, ...central } = meta;
579
+ // Write the machine-local files FIRST, then strip central — so a crash mid-write
580
+ // never removes pins/versions from central before they're persisted elsewhere.
581
+ const devicePath = getDeviceMetaPath();
582
+ if (agents && Object.keys(agents).length > 0) {
583
+ fs.mkdirSync(path.dirname(devicePath), { recursive: true });
584
+ writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents }));
585
+ }
586
+ else if (fs.existsSync(devicePath)) {
587
+ // Every pin was cleared. Persist the emptied map instead of skipping the
588
+ // write — otherwise the stale device file survives and overlayMachineLocal
589
+ // re-applies the removed pin on the next read, leaving a dangling default
590
+ // (e.g. a launcher pointing at a version that was just uninstalled).
591
+ writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents: {} }));
592
+ }
593
+ if (versions && Object.keys(versions).length > 0) {
594
+ const vrPath = getVersionResourcesPath();
595
+ fs.mkdirSync(path.dirname(vrPath), { recursive: true });
596
+ writeIfChanged(vrPath, JSON.stringify(versions, null, 2) + '\n');
597
+ }
598
+ writeIfChanged(META_FILE, META_HEADER + yaml.stringify(central));
532
599
  metaCache = null;
533
600
  }
601
+ /**
602
+ * Overlay this machine's local state onto a central-portable Meta:
603
+ * - `agents:` from the device file (device wins; the union both preserves the
604
+ * one-level merge and self-heals a pre-migration central that still has pins)
605
+ * - `versions:` from the history JSON (wholesale replace; falls back to
606
+ * whatever central carried when the history file doesn't exist yet)
607
+ */
608
+ function overlayMachineLocal(meta) {
609
+ const devicePath = getDeviceMetaPath();
610
+ if (fs.existsSync(devicePath)) {
611
+ try {
612
+ const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
613
+ if (dm?.agents)
614
+ meta.agents = { ...meta.agents, ...dm.agents };
615
+ }
616
+ catch { /* ignore malformed device file */ }
617
+ }
618
+ const vrPath = getVersionResourcesPath();
619
+ if (fs.existsSync(vrPath)) {
620
+ try {
621
+ const vr = JSON.parse(fs.readFileSync(vrPath, 'utf-8'));
622
+ if (vr)
623
+ meta.versions = vr;
624
+ }
625
+ catch { /* ignore malformed history file */ }
626
+ }
627
+ return meta;
628
+ }
534
629
  function applyRegistrySeeds(meta) {
535
630
  const seeded = new Set(meta.seededPresets || []);
536
631
  let changed = false;
@@ -590,10 +685,7 @@ export function readMeta() {
590
685
  // what we last parsed. Reduces N readMeta calls per CLI invocation to ~2 stat
591
686
  // syscalls plus an in-memory object spread.
592
687
  if (metaCache) {
593
- const userMtime = safeMtimeMs(META_FILE);
594
- const systemMtime = safeMtimeMs(SYSTEM_META_FILE);
595
- const stamp = userMtime + systemMtime * 1e-3;
596
- if (stamp === metaCache.mtime) {
688
+ if (currentMetaStamp() === metaCache.stamp) {
597
689
  return metaCache.meta;
598
690
  }
599
691
  }
@@ -666,6 +758,7 @@ export function readMeta() {
666
758
  ...userMeta?.registries,
667
759
  };
668
760
  }
761
+ overlayMachineLocal(meta);
669
762
  if (applyRegistrySeeds(meta)) {
670
763
  writeMeta(meta);
671
764
  return rememberMeta(meta);
@@ -673,6 +766,7 @@ export function readMeta() {
673
766
  return rememberMeta(meta);
674
767
  }
675
768
  const meta = createDefaultMeta();
769
+ overlayMachineLocal(meta);
676
770
  if (applyRegistrySeeds(meta)) {
677
771
  writeMeta(meta);
678
772
  }
@@ -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.
@@ -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>;