@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { captureProcessStartTime } from '../platform/index.js';
1
2
  import { AgentType } from './parsers.js';
2
3
  /**
3
4
  * Compute the Lowest Common Ancestor (LCA) of multiple file paths.
@@ -26,18 +27,25 @@ export type { AgentType } from './parsers.js';
26
27
  */
27
28
  export declare function buildSentinelCommand(cmd: string[], exitCodePath: string): string;
28
29
  /**
29
- * Capture a stable identifier for a process at the moment it was started.
30
- * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
31
- * still occupies the PID we observed at spawn time. A bare kill(pid, 0)
32
- * probe cannot tell whether the OS has recycled the slot to an unrelated
33
- * process combined with detached spawns and unref(), that's exactly how
34
- * `agents teams stop` ends up SIGKILLing random process groups.
30
+ * Env for a locally-spawned teammate. Freezes ONE actor for the whole spawn
31
+ * tree: actorEnv(resolveActor()) stamps AGENTS_ACTOR* onto the child env, so the
32
+ * teammate's inner `agents run` reads it via inheritedActor and short-circuits
33
+ * computeActor instead of re-resolving every teammate under one orchestrator
34
+ * shares the orchestrator's single frozen actor (see actor.ts). Precedence:
35
+ * process env < actor < the teammate's --env overrides, so an explicit override
36
+ * still wins. Single source of truth shared by launchProcess() and its test.
37
+ */
38
+ export declare function buildTeammateSpawnEnv(envOverrides: Record<string, string> | null): NodeJS.ProcessEnv;
39
+ /**
40
+ * Re-exported from `platform/process.ts`, which owns the one implementation.
35
41
  *
36
- * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
37
- * macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
38
- * Returns null on any error so callers can skip the guard rather than crash.
42
+ * This module used to carry its own near-identical copy, and that copy had no
43
+ * Windows branch it fell through to `ps`, which does not exist there, so
44
+ * `captureProcessStartTime` always returned null and the pid-reuse guard at
45
+ * `stop()` was silently inert on Windows. That is exactly how `agents teams stop`
46
+ * ends up SIGKILLing an unrelated process group once the OS recycles a pid.
39
47
  */
40
- export declare function captureProcessStartTime(pid: number): string | null;
48
+ export { captureProcessStartTime };
41
49
  /**
42
50
  * Reasoning-intensity knob. Passed through to `agents run --effort`, which
43
51
  * translates it into per-agent reasoning flags (claude --effort, codex
@@ -130,6 +138,7 @@ export declare class AgentProcess {
130
138
  startedAt: Date;
131
139
  completedAt: Date | null;
132
140
  parentSessionId: string | null;
141
+ actor: string | null;
133
142
  cloudSessionId: string | null;
134
143
  cloudProvider: string | null;
135
144
  prUrl: string | null;
@@ -7,7 +7,7 @@
7
7
  * dependency scheduling via --after, per-teammate model/effort overrides, and
8
8
  * multiple permission modes (plan, edit, full).
9
9
  */
10
- import { spawn, execSync, execFileSync } from 'child_process';
10
+ import { spawn, execSync } from 'child_process';
11
11
  import { getAgentsInvocation } from '../daemon.js';
12
12
  import * as fs from 'fs/promises';
13
13
  import * as fsSync from 'fs';
@@ -15,7 +15,7 @@ import * as path from 'path';
15
15
  import * as os from 'os';
16
16
  import { randomUUID } from 'crypto';
17
17
  import { resolveAgentsDir } from './persistence.js';
18
- import { findExecutable } from '../platform/index.js';
18
+ import { findExecutable, captureProcessStartTime } from '../platform/index.js';
19
19
  import { normalizeEvents } from './parsers.js';
20
20
  import { debug } from './debug.js';
21
21
  import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from '../gemini-settings.js';
@@ -23,6 +23,7 @@ import { getAgentsDir as getSystemAgentsDir, getShimsDir } from '../state.js';
23
23
  import { AGENTS, getAccountInfo } from '../agents.js';
24
24
  import { resolveVersion, isVersionInstalled, verifyInstalledBinaryLaunches } from '../versions.js';
25
25
  import { sanitizeProcessEnv } from '../secrets/bundles.js';
26
+ import { resolveActor, actorEnv } from '../actor.js';
26
27
  import { recordRunName } from '../session/run-names.js';
27
28
  import { sshExec, shellQuote } from '../ssh-exec.js';
28
29
  import { resolveHost } from '../hosts/registry.js';
@@ -169,42 +170,31 @@ export function buildSentinelCommand(cmd, exitCodePath) {
169
170
  return `${cmd.map(shSingleQuote).join(' ')}; echo $? > ${shSingleQuote(exitCodePath)}`;
170
171
  }
171
172
  /**
172
- * Capture a stable identifier for a process at the moment it was started.
173
- * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
174
- * still occupies the PID we observed at spawn time. A bare kill(pid, 0)
175
- * probe cannot tell whether the OS has recycled the slot to an unrelated
176
- * process combined with detached spawns and unref(), that's exactly how
177
- * `agents teams stop` ends up SIGKILLing random process groups.
178
- *
179
- * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
180
- * macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
181
- * Returns null on any error so callers can skip the guard rather than crash.
173
+ * Env for a locally-spawned teammate. Freezes ONE actor for the whole spawn
174
+ * tree: actorEnv(resolveActor()) stamps AGENTS_ACTOR* onto the child env, so the
175
+ * teammate's inner `agents run` reads it via inheritedActor and short-circuits
176
+ * computeActor instead of re-resolving every teammate under one orchestrator
177
+ * shares the orchestrator's single frozen actor (see actor.ts). Precedence:
178
+ * process env < actor < the teammate's --env overrides, so an explicit override
179
+ * still wins. Single source of truth shared by launchProcess() and its test.
182
180
  */
183
- export function captureProcessStartTime(pid) {
184
- if (!pid || pid <= 0)
185
- return null;
186
- try {
187
- if (process.platform === 'linux') {
188
- const stat = fsSync.readFileSync(`/proc/${pid}/stat`, 'utf-8');
189
- const lastParen = stat.lastIndexOf(')');
190
- if (lastParen < 0)
191
- return null;
192
- const tail = stat.slice(lastParen + 2);
193
- const fields = tail.split(' ');
194
- // After comm we are at field 3; starttime is field 22, so index 19 here.
195
- return fields[19] || null;
196
- }
197
- const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
198
- encoding: 'utf-8',
199
- stdio: ['ignore', 'pipe', 'ignore'],
200
- });
201
- const trimmed = out.trim();
202
- return trimmed.length > 0 ? trimmed : null;
203
- }
204
- catch {
205
- return null;
206
- }
181
+ export function buildTeammateSpawnEnv(envOverrides) {
182
+ return {
183
+ ...sanitizeProcessEnv(process.env),
184
+ ...actorEnv(resolveActor()),
185
+ ...(envOverrides ?? {}),
186
+ };
207
187
  }
188
+ /**
189
+ * Re-exported from `platform/process.ts`, which owns the one implementation.
190
+ *
191
+ * This module used to carry its own near-identical copy, and that copy had no
192
+ * Windows branch — it fell through to `ps`, which does not exist there, so
193
+ * `captureProcessStartTime` always returned null and the pid-reuse guard at
194
+ * `stop()` was silently inert on Windows. That is exactly how `agents teams stop`
195
+ * ends up SIGKILLing an unrelated process group once the OS recycles a pid.
196
+ */
197
+ export { captureProcessStartTime };
208
198
  /** Agent types the team runner supports. */
209
199
  const TEAM_AGENT_TYPES = ['codex', 'cursor', 'gemini', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
210
200
  // Suffix appended to all prompts to ensure agents provide a summary
@@ -479,6 +469,12 @@ export class AgentProcess {
479
469
  startedAt = new Date();
480
470
  completedAt = null;
481
471
  parentSessionId = null;
472
+ // Frozen actor (resolveActor().id) this teammate runs under. Stamped onto the
473
+ // local spawn env via actorEnv (buildTeammateSpawnEnv) so the teammate's inner
474
+ // `agents run` inherits one actor for the whole tree instead of re-resolving,
475
+ // and persisted so the record shows who ran it. Set from the resolved actor at
476
+ // construction; loadFromDisk restores the persisted value.
477
+ actor = null;
482
478
  cloudSessionId = null;
483
479
  cloudProvider = null;
484
480
  prUrl = null;
@@ -564,6 +560,7 @@ export class AgentProcess {
564
560
  this.completedAt = completedAt;
565
561
  this.baseDir = baseDir;
566
562
  this.parentSessionId = parentSessionId;
563
+ this.actor = resolveActor().id;
567
564
  this.cloudSessionId = cloudSessionId;
568
565
  this.cloudProvider = cloudProvider;
569
566
  this.prUrl = prUrl;
@@ -628,6 +625,7 @@ export class AgentProcess {
628
625
  duration: this.duration(),
629
626
  mode: this.mode,
630
627
  parent_session_id: this.parentSessionId,
628
+ actor: this.actor,
631
629
  workspace_dir: this.workspaceDir,
632
630
  cloud_session_id: this.cloudSessionId,
633
631
  cloud_provider: this.cloudProvider,
@@ -884,6 +882,7 @@ export class AgentProcess {
884
882
  started_at: this.startedAt.toISOString(),
885
883
  completed_at: this.completedAt?.toISOString() || null,
886
884
  parent_session_id: this.parentSessionId,
885
+ actor: this.actor,
887
886
  cloud_session_id: this.cloudSessionId,
888
887
  cloud_provider: this.cloudProvider,
889
888
  pr_url: this.prUrl,
@@ -948,6 +947,10 @@ export class AgentProcess {
948
947
  ? meta.task_type
949
948
  : null, meta.cloud_repo || null, meta.cloud_branch || null, meta.worktree_name || null, meta.worktree_path || null, meta.profile_name || null);
950
949
  agent.startTime = typeof meta.start_time === 'string' ? meta.start_time : null;
950
+ // The persisted actor is the truth for a reload; the constructor set it to
951
+ // THIS process's resolved actor, which is wrong for a teammate someone else
952
+ // ran. Legacy teammates predating the field carry no actor -> null.
953
+ agent.actor = meta.actor ?? null;
951
954
  // Distributed-team fields: set post-construction (like startTime) so the
952
955
  // constructor signature stays fixed. Null on every pre-existing teammate.
953
956
  agent.hostName = meta.host_name || null;
@@ -1556,9 +1559,7 @@ export class AgentManager {
1556
1559
  stdio: ['ignore', stdoutFd, stdoutFd],
1557
1560
  cwd: agent.cwd || undefined,
1558
1561
  detached: true,
1559
- env: agent.envOverrides
1560
- ? { ...sanitizeProcessEnv(process.env), ...agent.envOverrides }
1561
- : sanitizeProcessEnv(process.env),
1562
+ env: buildTeammateSpawnEnv(agent.envOverrides),
1562
1563
  });
1563
1564
  await new Promise((resolve, reject) => {
1564
1565
  childProcess.once('spawn', resolve);
@@ -689,6 +689,26 @@ export interface BrandConfig {
689
689
  /** False to keep the config but stop minting/using the brand. */
690
690
  enabled: boolean;
691
691
  }
692
+ /**
693
+ * An actor -- a responsible entity behind a run (a human today, a top-level
694
+ * agent later). Keyed in the `actors:` map by a short slug. Every field is
695
+ * optional enrichment over what `tailscale whois` already resolves: pin a
696
+ * preferred git email, add a github handle, or override the display name.
697
+ * `login` is the tailnet login-name to match against (defaults to the map key).
698
+ * See lib/actor.ts.
699
+ */
700
+ export interface ActorConfig {
701
+ /** 'human' (default) or 'agent'. Only humans get personal git credit. */
702
+ kind?: 'human' | 'agent';
703
+ /** Display + git author name. Overrides the tailnet DisplayName. */
704
+ name?: string;
705
+ /** Git author email. Overrides the tailnet login email. */
706
+ email?: string;
707
+ /** GitHub handle, for PR attribution. */
708
+ github?: string;
709
+ /** Tailnet login-name this entry matches. Defaults to the map key. */
710
+ login?: string;
711
+ }
692
712
  /** Top-level structure of ~/.agents/.system/agents.yaml -- the CLI's persistent state. */
693
713
  export interface Meta {
694
714
  agents?: Partial<Record<AgentId, string>>;
@@ -765,8 +785,23 @@ export interface Meta {
765
785
  */
766
786
  brands?: Record<string, BrandConfig>;
767
787
  /**
768
- * Keys like `skill.hermes` registries seeded from SEEDED_REGISTRIES exactly
769
- * once. Tracked so a user `registry remove` won't silently re-seed.
788
+ * Actors keyed by slug -- who is behind a run. Enriches or overrides the
789
+ * identity `tailscale whois` resolves (git email, github handle, display
790
+ * name, human vs agent). See lib/actor.ts.
791
+ */
792
+ actors?: Record<string, ActorConfig>;
793
+ /**
794
+ * Removal tombstones for SEEDED_REGISTRIES presets, keyed like `skill.hermes`.
795
+ *
796
+ * Seeded presets are resolved in memory by `getRegistries` (see
797
+ * `offeredSeeds`) rather than written into agents.yaml — persisting them from
798
+ * the read path dirtied this git-tracked file and deadlocked
799
+ * `agents repo pull` (RUSH-1925). A key listed here means the user ran
800
+ * `registry remove` on that preset, so it is no longer offered.
801
+ *
802
+ * Entries written by the pre-RUSH-1925 seeding also appear here; those files
803
+ * carry the registry in their own `registries:` block too, which takes
804
+ * precedence, so the preset stays exactly as configured.
770
805
  */
771
806
  seededPresets?: string[];
772
807
  /**
@@ -831,6 +866,16 @@ export interface Meta {
831
866
  };
832
867
  transports?: Record<string, string>;
833
868
  };
869
+ /**
870
+ * Routines daemon settings. `projects` is the allowlist of project roots
871
+ * whose `.agents/routines/*.yml` may be synced into `~/.agents/routines/`
872
+ * and fired by the daemon — never auto-discovered from a cloned public
873
+ * repo without an explicit opt-in (`agents routines enable-project`).
874
+ * Paths are stored absolute (or home-relative `~/…`).
875
+ */
876
+ routines?: {
877
+ projects?: string[];
878
+ };
834
879
  }
835
880
  /** Persisted agent-host entry in agents.yaml (overlay or inline). */
836
881
  export interface HostEntry {
@@ -151,6 +151,22 @@ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot |
151
151
  export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
152
152
  /** Format a multi-line usage section for detailed agent views. */
153
153
  export declare function formatUsageSection(usage: UsageInfo): string[];
154
+ /**
155
+ * The access token to use for a READ-ONLY Claude usage fetch, or null when the
156
+ * stored token is within the refresh leeway.
157
+ *
158
+ * Returns null instead of refreshing on purpose. Claude's refresh token is
159
+ * single-use and rotates server-side on every refresh; with one account signed
160
+ * into several machines, refreshing here would stampede that one token and
161
+ * silently invalidate every other holder — the RUSH-1822 failure, except in the
162
+ * usage path (fired in the background by the SWR cache and by `agents run`'s
163
+ * default "balanced" rotation on every unpinned run) rather than the health
164
+ * probe. So a usage read must never rotate: a near-expiry token yields "no usage
165
+ * right now" instead of a fleet-wide logout. Mirrors {@link probeClaudeStatus};
166
+ * the single legitimate refresh belongs to the actual claude run, never a read.
167
+ * Pure — unit-tested.
168
+ */
169
+ export declare function claudeUsageAccessTokenNoRefresh(oauth: Pick<ClaudeOauthCredentials, 'accessToken' | 'expiresAt'>): string | null;
154
170
  /** Raw quota bucket from the Kimi /usages response (numbers arrive as strings). */
155
171
  interface KimiUsageQuota {
156
172
  limit?: string | number | null;
@@ -242,8 +258,24 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
242
258
  * Without step 2 the live usage fetch got no token on Linux, so `agents view`
243
259
  * (run remotely over SSH by `--host`) rendered no usage bars even though the
244
260
  * account + plan — read from the plaintext `.claude.json` — showed fine.
261
+ *
262
+ * `opts.accessTokenCache` opts INTO the no-ACL access-token cache (the Touch ID
263
+ * storm fix, see the cache helpers above). It is OFF by default so every caller
264
+ * keeps the full ACL-read credential — the cached copy deliberately omits the
265
+ * refresh token, so callers that refresh (`isClaudeAuthValid` ->
266
+ * `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
267
+ * for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
268
+ * access-token-only consumers (the usage fetch and the auth-health probe) opt in.
269
+ */
270
+ export declare function loadClaudeOauth(home?: string, opts?: {
271
+ accessTokenCache?: boolean;
272
+ }): Promise<ClaudeOauthCredentials | null>;
273
+ /**
274
+ * Save Claude OAuth credentials to the system keychain/keyring.
275
+ * Reads the existing payload, merges the new OAuth fields, and writes back.
276
+ * Exported for regression tests; not part of the public command surface.
245
277
  */
246
- export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
278
+ export declare function saveClaudeOauth(home: string | undefined, credentials: ClaudeOauthCredentials): Promise<boolean>;
247
279
  /**
248
280
  * Derive the Keychain service name for a Claude home directory.
249
281
  * Managed (non-default) homes get a hash suffix for isolation.
package/dist/lib/usage.js CHANGED
@@ -17,7 +17,7 @@ import { promisify } from 'util';
17
17
  import chalk from 'chalk';
18
18
  import { decodeJwtPayload, decryptDroidAuthPayload } from './agents.js';
19
19
  import { walkForFiles } from './fs-walk.js';
20
- import { getKeychainToken, setKeychainToken, deleteKeychainToken, } from './secrets/index.js';
20
+ import { getKeychainToken, setKeychainToken, deleteKeychainToken, isKeychainBackendOverridden, } from './secrets/index.js';
21
21
  import { getCacheDir } from './state.js';
22
22
  const execFileAsync = promisify(execFile);
23
23
  const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
@@ -375,10 +375,33 @@ async function getCodexUsageInfo(options) {
375
375
  return { snapshot: null, error: null };
376
376
  }
377
377
  }
378
+ /**
379
+ * The access token to use for a READ-ONLY Claude usage fetch, or null when the
380
+ * stored token is within the refresh leeway.
381
+ *
382
+ * Returns null instead of refreshing on purpose. Claude's refresh token is
383
+ * single-use and rotates server-side on every refresh; with one account signed
384
+ * into several machines, refreshing here would stampede that one token and
385
+ * silently invalidate every other holder — the RUSH-1822 failure, except in the
386
+ * usage path (fired in the background by the SWR cache and by `agents run`'s
387
+ * default "balanced" rotation on every unpinned run) rather than the health
388
+ * probe. So a usage read must never rotate: a near-expiry token yields "no usage
389
+ * right now" instead of a fleet-wide logout. Mirrors {@link probeClaudeStatus};
390
+ * the single legitimate refresh belongs to the actual claude run, never a read.
391
+ * Pure — unit-tested.
392
+ */
393
+ export function claudeUsageAccessTokenNoRefresh(oauth) {
394
+ if (claudeAccessTokenNeedsRefresh(oauth.expiresAt ?? null))
395
+ return null;
396
+ const token = oauth.accessToken?.trim();
397
+ return token ? token : null;
398
+ }
378
399
  /** Fetch Claude usage via the Anthropic OAuth usage API. */
379
400
  async function getClaudeUsageInfo(options) {
380
401
  try {
381
- const oauth = await loadClaudeOauth(options?.home);
402
+ // Opt into the no-ACL access-token cache: this is the every-60s watchdog hot
403
+ // path and usage needs only the access token, so it kills the Touch ID storm.
404
+ const oauth = await loadClaudeOauth(options?.home, { accessTokenCache: true });
382
405
  if (!oauth?.accessToken) {
383
406
  return { snapshot: null, error: null };
384
407
  }
@@ -387,7 +410,8 @@ async function getClaudeUsageInfo(options) {
387
410
  if (!isClaudeUsageOrgMatch(requestedOrgId, liveOrgId)) {
388
411
  return { snapshot: null, error: null };
389
412
  }
390
- const accessToken = await getClaudeAccessToken(oauth, options?.home);
413
+ // Read-only: never refresh a single-use token just to read usage (RUSH-1822).
414
+ const accessToken = claudeUsageAccessTokenNoRefresh(oauth);
391
415
  if (!accessToken) {
392
416
  return { snapshot: null, error: null };
393
417
  }
@@ -627,7 +651,10 @@ async function getDroidUsageInfo(options) {
627
651
  }
628
652
  /** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
629
653
  export async function probeClaudeStatus(home, cliVersion) {
630
- const oauth = await loadClaudeOauth(home);
654
+ // Opt into the no-ACL access-token cache: the daemon warms this probe every ~3
655
+ // min per account and it never refreshes (access token only), so caching is safe
656
+ // and stops it from adding to the Touch ID storm.
657
+ const oauth = await loadClaudeOauth(home, { accessTokenCache: true });
631
658
  const accessToken = oauth?.accessToken?.trim();
632
659
  if (!accessToken)
633
660
  return { status: null, token: 'missing' };
@@ -862,6 +889,103 @@ function parseClaudeOauthPayload(raw) {
862
889
  return null;
863
890
  }
864
891
  }
892
+ // ── No-ACL cache for Claude's OAuth token (kills the macOS Touch ID storm) ──
893
+ //
894
+ // The source item `Claude Code-credentials-<hash>` is ACL-bound to Claude Code's
895
+ // own process, so every read agents-cli makes (via `/usr/bin/security`) pops
896
+ // Touch ID. The Factory watchdog polls `agents view --json` every 60s per agent,
897
+ // and each poll that crosses the 2-minute usage cache fires a background refresh
898
+ // -> loadClaudeOauth -> keychain read -> a biometric prompt. With many agents and
899
+ // accounts that is a prompt every couple of minutes, per account.
900
+ //
901
+ // Fix: after one real (prompting) read, cache the ACCESS token in a device-local
902
+ // NO-ACL keychain item — the same `set-no-acl` mechanism secrets/session-store.ts
903
+ // uses for unlocked bundles, whose reads never prompt — and serve every later read
904
+ // from it until the token's own `expiresAt`. The ACL read then happens at most once
905
+ // per token lifetime (~a few times a day), shared across every agent process.
906
+ //
907
+ // Security posture: only the short-lived access token is cached, never the refresh
908
+ // token, and the entry dies at the token's expiry (capped below). This mirrors the
909
+ // no-ACL posture session-store.ts already accepts for held bundles.
910
+ const CLAUDE_OAUTH_CACHE_PREFIX = 'agents-cli.claude-oauth-cache.';
911
+ /** Hard cap so a token with a distant or absent `expiresAt` can't pin a stale entry. */
912
+ const CLAUDE_OAUTH_CACHE_MAX_TTL_MS = 8 * 60 * 60 * 1000;
913
+ /** The no-ACL cache item name for a Claude keychain service (hashed to stay tidy). */
914
+ function claudeOauthCacheItem(service) {
915
+ const hash = createHash('sha256').update(service).digest('hex').slice(0, 16);
916
+ return `${CLAUDE_OAUTH_CACHE_PREFIX}${hash}`;
917
+ }
918
+ /** Whether the no-ACL cache is meaningful: macOS (where the source read prompts) or
919
+ * whenever a test keychain backend is installed, so the path is exercisable on CI.
920
+ * Off macOS in production the source read is prompt-free, so there is nothing to cache. */
921
+ function claudeOauthCacheActive() {
922
+ return process.platform === 'darwin' || isKeychainBackendOverridden();
923
+ }
924
+ /** Read the cached access token (prompt-free). Null on miss, or when the cache entry
925
+ * or the token itself has expired — in which case the stale entry is dropped. */
926
+ function readCachedClaudeOauth(service) {
927
+ try {
928
+ const entry = JSON.parse(getKeychainToken(claudeOauthCacheItem(service)));
929
+ if (!entry || typeof entry.accessToken !== 'string' || !entry.accessToken)
930
+ return null;
931
+ const now = Date.now();
932
+ const tokenExpired = typeof entry.expiresAt === 'number' && entry.expiresAt > 0 && now >= entry.expiresAt;
933
+ if (now >= entry.cacheExpiresAt || tokenExpired) {
934
+ try {
935
+ deleteKeychainToken(claudeOauthCacheItem(service));
936
+ }
937
+ catch {
938
+ /* best-effort eviction */
939
+ }
940
+ return null;
941
+ }
942
+ return {
943
+ accessToken: entry.accessToken,
944
+ refreshToken: null,
945
+ expiresAt: entry.expiresAt ?? null,
946
+ scopes: entry.scopes ?? null,
947
+ subscriptionType: entry.subscriptionType ?? null,
948
+ rateLimitTier: entry.rateLimitTier ?? null,
949
+ organizationUuid: entry.organizationUuid ?? null,
950
+ };
951
+ }
952
+ catch {
953
+ return null;
954
+ }
955
+ }
956
+ /** Populate the no-ACL cache after a real (prompting) source read. Best-effort — the
957
+ * cache is an optimization, so any failure just means the next read prompts again. */
958
+ function writeCachedClaudeOauth(service, creds) {
959
+ if (!creds.accessToken)
960
+ return;
961
+ try {
962
+ const now = Date.now();
963
+ const cap = now + CLAUDE_OAUTH_CACHE_MAX_TTL_MS;
964
+ const tokenExp = typeof creds.expiresAt === 'number' && creds.expiresAt > now ? creds.expiresAt : cap;
965
+ const entry = {
966
+ accessToken: creds.accessToken,
967
+ expiresAt: creds.expiresAt ?? null,
968
+ scopes: creds.scopes ?? null,
969
+ subscriptionType: creds.subscriptionType ?? null,
970
+ rateLimitTier: creds.rateLimitTier ?? null,
971
+ organizationUuid: creds.organizationUuid ?? null,
972
+ cacheExpiresAt: Math.min(tokenExp, cap),
973
+ };
974
+ setKeychainToken(claudeOauthCacheItem(service), JSON.stringify(entry), { noAcl: true });
975
+ }
976
+ catch {
977
+ /* best-effort — cache is an optimization */
978
+ }
979
+ }
980
+ /** Evict the no-ACL cache so a source rotation or sign-out is reflected immediately. */
981
+ function deleteCachedClaudeOauth(service) {
982
+ try {
983
+ deleteKeychainToken(claudeOauthCacheItem(service));
984
+ }
985
+ catch {
986
+ /* best-effort — cache is an optimization */
987
+ }
988
+ }
865
989
  /**
866
990
  * Load a version home's Claude OAuth credential from the two stores Claude Code
867
991
  * uses, tried in order:
@@ -878,19 +1002,48 @@ function parseClaudeOauthPayload(raw) {
878
1002
  * Without step 2 the live usage fetch got no token on Linux, so `agents view`
879
1003
  * (run remotely over SSH by `--host`) rendered no usage bars even though the
880
1004
  * account + plan — read from the plaintext `.claude.json` — showed fine.
1005
+ *
1006
+ * `opts.accessTokenCache` opts INTO the no-ACL access-token cache (the Touch ID
1007
+ * storm fix, see the cache helpers above). It is OFF by default so every caller
1008
+ * keeps the full ACL-read credential — the cached copy deliberately omits the
1009
+ * refresh token, so callers that refresh (`isClaudeAuthValid` ->
1010
+ * `getClaudeAccessToken`) or export the full blob (`readClaudeCredentialsBlob`
1011
+ * for Rush Cloud dispatch) must NOT pass it. Only the read-only, high-frequency
1012
+ * access-token-only consumers (the usage fetch and the auth-health probe) opt in.
881
1013
  */
882
- export async function loadClaudeOauth(home) {
1014
+ export async function loadClaudeOauth(home, opts) {
883
1015
  // The OS keychain/keyring step is macOS/Linux-only. Windows skips straight
884
1016
  // to the .credentials.json fallback — the Claude CLI has no keychain
885
- // integration there and stores its OAuth token in that file too.
886
- if (process.platform === 'darwin' || process.platform === 'linux') {
1017
+ // integration there and stores its OAuth token in that file too. An injected
1018
+ // test backend is the exception: it makes the keychain path exercisable
1019
+ // anywhere, so the platform check must yield to it exactly as the inner
1020
+ // claudeOauthCacheActive() gate does — otherwise this whole block is dead on
1021
+ // a Windows runner and the tests below it read an empty home.
1022
+ if (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden()) {
1023
+ const service = getClaudeKeychainService(home);
1024
+ // Serve the no-ACL cache first (opt-in, macOS/test only) so the ACL-gated read
1025
+ // below — the one that pops Touch ID — happens at most once per token lifetime
1026
+ // instead of on every usage refresh. Off unless the caller opts in, because the
1027
+ // cached copy drops the refresh token (see the doc comment above).
1028
+ const cacheActive = opts?.accessTokenCache === true && claudeOauthCacheActive();
1029
+ if (cacheActive) {
1030
+ const cached = readCachedClaudeOauth(service);
1031
+ if (cached)
1032
+ return cached;
1033
+ }
887
1034
  try {
888
- const fromKeychain = parseClaudeOauthPayload(getKeychainToken(getClaudeKeychainService(home)));
889
- if (fromKeychain)
1035
+ const fromKeychain = parseClaudeOauthPayload(getKeychainToken(service));
1036
+ if (fromKeychain) {
1037
+ if (cacheActive)
1038
+ writeCachedClaudeOauth(service, fromKeychain);
890
1039
  return fromKeychain;
1040
+ }
891
1041
  }
892
1042
  catch {
893
1043
  // No keychain item, or no reachable keyring (headless Linux) — fall through.
1044
+ // Evict any stale no-ACL cache so a sign-out/deletion isn't masked.
1045
+ if (cacheActive)
1046
+ deleteCachedClaudeOauth(service);
894
1047
  }
895
1048
  }
896
1049
  const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
@@ -907,10 +1060,15 @@ export async function loadClaudeOauth(home) {
907
1060
  /**
908
1061
  * Save Claude OAuth credentials to the system keychain/keyring.
909
1062
  * Reads the existing payload, merges the new OAuth fields, and writes back.
1063
+ * Exported for regression tests; not part of the public command surface.
910
1064
  */
911
- async function saveClaudeOauth(home, credentials) {
912
- // Windows not yet supported
913
- if (process.platform !== 'darwin' && process.platform !== 'linux') {
1065
+ export async function saveClaudeOauth(home, credentials) {
1066
+ // Windows not yet supported. An injected test backend is the exception, for
1067
+ // the same reason as the loadClaudeOauth guard above: it makes the keychain
1068
+ // path exercisable anywhere, and without it this returns before the rotated
1069
+ // credential is written OR the no-ACL cache is evicted — so a cached reader
1070
+ // keeps serving the pre-rotation access token.
1071
+ if (process.platform !== 'darwin' && process.platform !== 'linux' && !isKeychainBackendOverridden()) {
914
1072
  return false;
915
1073
  }
916
1074
  try {
@@ -944,6 +1102,8 @@ async function saveClaudeOauth(home, credentials) {
944
1102
  // Entry might not exist, ignore
945
1103
  }
946
1104
  setKeychainToken(service, payloadJson);
1105
+ // A new credential rotation means any cached access token is stale.
1106
+ deleteCachedClaudeOauth(service);
947
1107
  return true;
948
1108
  }
949
1109
  catch {
@@ -444,8 +444,15 @@ export declare function verifyInstalledBinaryLaunches(agent: AgentId, version: s
444
444
  * the exact failure this repairs. Agents with a global/native binary (grok,
445
445
  * droid) have no such tarball and are returned unchanged.
446
446
  */
447
- export declare function ensureAgentRunnable(agent: AgentId, version: string, log?: (message: string) => void): Promise<string | null>;
448
- export declare function healBrokenDefaultLaunches(log?: (m: string) => void): Promise<string[]>;
447
+ export declare function ensureAgentRunnable(agent: AgentId, version: string, log?: (message: string) => void, opts?: {
448
+ allowDefaultSwitch?: boolean;
449
+ }): Promise<string | null>;
450
+ export declare function healBrokenDefaultLaunches(log?: (m: string) => void, opts?: {
451
+ allowDefaultSwitch?: boolean;
452
+ }): Promise<{
453
+ repaired: string[];
454
+ unhealed: string[];
455
+ }>;
449
456
  /** Outcome of syncing resources to a version home, keyed by resource type. */
450
457
  export interface SyncResult {
451
458
  commands: boolean;