@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -17,11 +17,51 @@ export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
17
17
  && value.authRows.length > 0
18
18
  && value.authRows.every(authRow => authRow.health.checkedAt >= minimumCapturedAt);
19
19
  }
20
+ /**
21
+ * How stale a cached auth verdict may get before the periodic tick re-probes.
22
+ *
23
+ * The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
24
+ * the usage probe. Firing it every 3-minute tick on every fleet device drove one
25
+ * per-account request quota to a permanent 429, and the shared backoff then
26
+ * parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
27
+ * most every 20 minutes cuts that endpoint traffic ~5x while still catching a
28
+ * revocation within one window — and every device keeps a REAL verdict (not a
29
+ * degraded "unverified"), so `agents devices ping --strict` and the run
30
+ * auth-preflight keep working on every host, unlike a publisher/subscriber split
31
+ * that would blind every non-primary box to revocation. Fleet status still
32
+ * publishes every tick — it does not ride that endpoint.
33
+ */
34
+ export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
35
+ /**
36
+ * True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
37
+ * — i.e. reusing them would not let a verdict get staler than one probe window.
38
+ * Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
39
+ */
40
+ export function isCachedFleetAuthProbeFresh(authRows, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
41
+ return authRows.length > 0 && authRows.every((r) => now - r.health.checkedAt < maxAgeMs);
42
+ }
43
+ /**
44
+ * Whether the tick may reuse the cached auth verdict instead of re-probing the
45
+ * rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
46
+ * re-probes — the whole point of the command is a genuinely live verdict, and
47
+ * missing this `!force` is exactly how a `--strict` check silently passed a
48
+ * revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
49
+ * unit-tested so that inversion cannot regress unnoticed.
50
+ */
51
+ export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
52
+ return !force && isCachedFleetAuthProbeFresh(cached, now, maxAgeMs);
53
+ }
20
54
  /**
21
55
  * Fleet cache warm: publish THIS host's row for the caches `agents fleet
22
56
  * status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
57
+ *
58
+ * `force` is set by on-demand callers (`agents devices ping`) that must return a
59
+ * genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
60
+ * recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
61
+ * rate-limited endpoint every 3 minutes (RUSH-2998).
23
62
  */
24
- export async function refreshLocalFleetAuthState() {
63
+ export async function refreshLocalFleetAuthState(opts) {
64
+ const force = opts?.force === true;
25
65
  const { machineId } = await import('./machine-id.js');
26
66
  const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
27
67
  const { getCliVersion } = await import('./version.js');
@@ -40,11 +80,18 @@ export async function refreshLocalFleetAuthState() {
40
80
  return { row, authRows: readFleetAuthRows(self) };
41
81
  },
42
82
  // A recent daemon publication is the completed result, not a reason to probe
43
- // every provider a second time.
44
- isCompleted: (value) => isFreshFleetAuthSnapshot(value, minimumCapturedAt),
83
+ // every provider a second time. An on-demand caller (force) never accepts a
84
+ // cached snapshot it must return a genuinely live verdict.
85
+ isCompleted: (value) => !force && isFreshFleetAuthSnapshot(value, minimumCapturedAt),
45
86
  refresh: async () => {
46
- const authRows = await probeLocalFleetAuth({ cliVersion: getCliVersion() });
47
- writeFleetAuthRows(self, authRows);
87
+ // Re-probe the rate-limited /oauth/usage endpoint at most every
88
+ // AUTH_PROBE_MAX_AGE_MS; reuse the last real verdict in between (RUSH-2998).
89
+ // Fleet status publishes every tick regardless — it does not ride that endpoint.
90
+ const cached = readFleetAuthRows(self);
91
+ const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
92
+ const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion() });
93
+ if (!reuse)
94
+ writeFleetAuthRows(self, authRows);
48
95
  const row = await publishLocalFleetStatus(self);
49
96
  return { row, authRows };
50
97
  },
@@ -243,8 +243,7 @@ export const CONFIG_KEYS = [
243
243
  type: 'string',
244
244
  description: "What this device is for, fleet-wide: 'worker' (a box agents run on) or 'personal' (a machine you sit at — never " +
245
245
  'picked automatically). Marking ANY device worker turns automatic placement into an allowlist: `--device auto` then ' +
246
- 'picks only from the marked workers. (A paired iPhone/iPad cockpit is marked control by `agents devices pair-ios` ' +
247
- 'and is excluded from placement by that role, not this key.)',
246
+ 'picks only from the marked workers.',
248
247
  validate: (v) => DEVICE_ROLES.includes(v)
249
248
  ? null
250
249
  : `role must be one of ${DEVICE_ROLES.join(' | ')}.`,
@@ -19,9 +19,9 @@ export declare function loadDeviceDiscoveryPolicies(): Map<string, DeviceDiscove
19
19
  * Only devices with an EXPLICIT entry in the synced policy are touched. A
20
20
  * device absent from the map is left alone, whether or not the map itself is
21
21
  * defined — the map is not treated as authoritative-by-omission, because not
22
- * every local registration path writes a policy entry (pair-ios control
23
- * devices, daemon/umbrella auto-refresh, and any device registered before this
24
- * feature shipped). Wiping those on the next `agents repo pull user` would be
22
+ * every local registration path writes a policy entry (daemon/umbrella
23
+ * auto-refresh, and any device registered before this feature shipped).
24
+ * Wiping those on the next `agents repo pull user` would be
25
25
  * silent, fleet-wide data loss for a decision (e.g. "ignore this one iPad")
26
26
  * that never meant to touch them.
27
27
  */
@@ -48,9 +48,9 @@ export function loadDeviceDiscoveryPolicies() {
48
48
  * Only devices with an EXPLICIT entry in the synced policy are touched. A
49
49
  * device absent from the map is left alone, whether or not the map itself is
50
50
  * defined — the map is not treated as authoritative-by-omission, because not
51
- * every local registration path writes a policy entry (pair-ios control
52
- * devices, daemon/umbrella auto-refresh, and any device registered before this
53
- * feature shipped). Wiping those on the next `agents repo pull user` would be
51
+ * every local registration path writes a policy entry (daemon/umbrella
52
+ * auto-refresh, and any device registered before this feature shipped).
53
+ * Wiping those on the next `agents repo pull user` would be
54
54
  * silent, fleet-wide data loss for a decision (e.g. "ignore this one iPad")
55
55
  * that never meant to touch them.
56
56
  */
@@ -42,7 +42,7 @@ import { AGENTS, ALL_AGENT_IDS, supportsAccountInspection } from '../agents.js';
42
42
  import { blocksLocalScripts } from '../platform/winpath.js';
43
43
  import { loginHint } from '../signin-badge.js';
44
44
  import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
45
- import { padToWidth, stringWidth } from '../session/width.js';
45
+ import { padToWidth, stringWidth } from '../text/width.js';
46
46
  import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
47
47
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
48
48
  /** Agents with NO per-version credential isolation: their login is shared across
@@ -7,9 +7,9 @@
7
7
  * throws (misconfigured auth, etc.) become `failed` rows — they never abort
8
8
  * the remaining devices.
9
9
  */
10
- import { type DeviceProfile, type DeviceRegistry } from './registry.js';
10
+ import type { DeviceProfile, DeviceRegistry } from './registry.js';
11
11
  import type { DeviceStats } from './health.js';
12
- export type FleetSkipReason = 'offline' | 'no-address' | 'control';
12
+ export type FleetSkipReason = 'offline' | 'no-address';
13
13
  /** npm dist-tags / semver pins only — rejects shell metacharacters. */
14
14
  export declare const FLEET_VERSION_RE: RegExp;
15
15
  export interface FleetTarget {
@@ -39,7 +39,6 @@ export interface FanOutDeviceResult<T> {
39
39
  /**
40
40
  * Classify each registered device for a fleet operation.
41
41
  *
42
- * - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
43
42
  * - Tailscale-offline → skip `offline`
44
43
  * - No address → skip `no-address`
45
44
  * - Everything else is a target (including this machine, reached over ssh when
@@ -48,10 +47,7 @@ export interface FanOutDeviceResult<T> {
48
47
  export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
49
48
  /**
50
49
  * Remote fan-out targets for the fleet health/drift gates (`fleet status`,
51
- * `doctor --check --devices`): every planned device except this machine and control-only
52
- * cockpits. A control device never runs agents (mirrors doctor's fan-out, which
53
- * drops it via `isControlDevice`), so counting it as unreachable/drift would make
54
- * the CI gate fail on every run for a fleet that merely has a cockpit registered.
50
+ * `doctor --check --devices`): every planned device except this machine.
55
51
  * Offline / no-address devices are kept — those are genuine faults a gate should
56
52
  * surface — so their `skip` reason still flows through as an `unreachable` row.
57
53
  */
@@ -72,7 +68,7 @@ export declare function remoteFleetTargets(planned: FleetTarget[], self: string)
72
68
  * minutes is the only false-negative window — it renders `unreachable` until the
73
69
  * next stats warm, which beats letting it hang the status glance for 45s.
74
70
  *
75
- * An existing skip (control/offline/no-address from {@link planFleetTargets})
71
+ * An existing skip (offline/no-address from {@link planFleetTargets})
76
72
  * always wins — those are classified before any probe.
77
73
  */
78
74
  export declare function fleetHealthSkip(currentSkip: FleetSkipReason | string | undefined, stats: DeviceStats | undefined): FleetSkipReason | string | undefined;
@@ -8,7 +8,6 @@
8
8
  * the remaining devices.
9
9
  */
10
10
  import { spawnSync } from 'child_process';
11
- import { isControlDevice } from './registry.js';
12
11
  import { isSelfHost } from './self-host.js';
13
12
  import { buildSshInvocation, sshTargetFor, writeAskpassShim } from './connect.js';
14
13
  /** npm dist-tags / semver pins only — rejects shell metacharacters. */
@@ -16,7 +15,6 @@ export const FLEET_VERSION_RE = /^[A-Za-z0-9._-]+$/;
16
15
  /**
17
16
  * Classify each registered device for a fleet operation.
18
17
  *
19
- * - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
20
18
  * - Tailscale-offline → skip `offline`
21
19
  * - No address → skip `no-address`
22
20
  * - Everything else is a target (including this machine, reached over ssh when
@@ -26,11 +24,6 @@ export function planFleetTargets(reg) {
26
24
  const names = Object.keys(reg).sort();
27
25
  return names.map((name) => {
28
26
  const device = reg[name];
29
- // A control device drives the fleet but never runs agents — never a target
30
- // for update/run/stats, whatever its platform reads as.
31
- if (isControlDevice(device)) {
32
- return { device, skip: 'control' };
33
- }
34
27
  if (device.tailscale && !device.tailscale.online) {
35
28
  return { device, skip: 'offline' };
36
29
  }
@@ -45,10 +38,7 @@ export function planFleetTargets(reg) {
45
38
  }
46
39
  /**
47
40
  * Remote fan-out targets for the fleet health/drift gates (`fleet status`,
48
- * `doctor --check --devices`): every planned device except this machine and control-only
49
- * cockpits. A control device never runs agents (mirrors doctor's fan-out, which
50
- * drops it via `isControlDevice`), so counting it as unreachable/drift would make
51
- * the CI gate fail on every run for a fleet that merely has a cockpit registered.
41
+ * `doctor --check --devices`): every planned device except this machine.
52
42
  * Offline / no-address devices are kept — those are genuine faults a gate should
53
43
  * surface — so their `skip` reason still flows through as an `unreachable` row.
54
44
  */
@@ -57,7 +47,7 @@ export function remoteFleetTargets(planned, self) {
57
47
  // device referenced by its dnsName slipped past the bare name check and got a
58
48
  // remote version+doctor dial back to THIS box, which orphaned on timeout and
59
49
  // piled up (RUSH-2114). `isSelfHost` matches every alias the box answers to.
60
- return planned.filter((t) => t.device.name !== self && !isSelfHost(t.device.name) && t.skip !== 'control');
50
+ return planned.filter((t) => t.device.name !== self && !isSelfHost(t.device.name));
61
51
  }
62
52
  /**
63
53
  * Decide whether a fleet-health target should skip the expensive version+doctor
@@ -75,7 +65,7 @@ export function remoteFleetTargets(planned, self) {
75
65
  * minutes is the only false-negative window — it renders `unreachable` until the
76
66
  * next stats warm, which beats letting it hang the status glance for 45s.
77
67
  *
78
- * An existing skip (control/offline/no-address from {@link planFleetTargets})
68
+ * An existing skip (offline/no-address from {@link planFleetTargets})
79
69
  * always wins — those are classified before any probe.
80
70
  */
81
71
  export function fleetHealthSkip(currentSkip, stats) {
@@ -92,8 +82,6 @@ export function skipLabel(reason) {
92
82
  return 'offline';
93
83
  case 'no-address':
94
84
  return 'no address';
95
- case 'control':
96
- return 'control device';
97
85
  }
98
86
  }
99
87
  /**
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../session/width.js';
2
+ import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../text/width.js';
3
3
  import { fmtBytes, headroom } from './health.js';
4
4
  import { formatCheckedAge } from '../auth-health.js';
5
5
  import { compareFleetInventories } from './fleet-divergence.js';
@@ -314,7 +314,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
314
314
  const seen = row.lastSeen ? ` · last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}` : '';
315
315
  items.push({ glyph: 'offline', subject: row.name, detail: `offline${seen}`, fix: 'check the box' });
316
316
  }
317
- // 2) Boxes that need `agents apply` — merge config drift and a stark CLI gap
317
+ // 2) Boxes that need `agents fleet apply` — merge config drift and a stark CLI gap
318
318
  // into ONE item per box (both are fixed by the same command, so don't
319
319
  // double-list). An offline box's config/CLI is unknowable, so skip it.
320
320
  for (const row of report.devices) {
@@ -327,7 +327,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
327
327
  if (stark)
328
328
  reasons.push(`only ${stark.installed} of ${stark.total} agent CLIs installed`);
329
329
  if (reasons.length > 0) {
330
- items.push({ glyph: 'warn', subject: row.name, detail: reasons.join(' · '), fix: `agents apply ${row.name}` });
330
+ items.push({ glyph: 'warn', subject: row.name, detail: reasons.join(' · '), fix: `agents fleet apply --device ${row.name}` });
331
331
  }
332
332
  }
333
333
  // 3) Version skew across the fleet — one line.
@@ -354,7 +354,7 @@ export function buildFleetAttentionItems(report, now = Date.now()) {
354
354
  glyph: 'warn',
355
355
  subject: w.devices[0] ?? 'fleet',
356
356
  detail: w.message.replace(`${w.devices[0]} `, ''),
357
- fix: `agents apply ${w.devices[0] ?? ''}`.trim(),
357
+ fix: `agents fleet apply --device ${w.devices[0] ?? ''}`.trim(),
358
358
  });
359
359
  }
360
360
  return items;
@@ -19,12 +19,6 @@
19
19
  *
20
20
  * `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
21
21
  * machine the user sits at is marked precisely so agents stay off it.
22
- *
23
- * Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
24
- * by `agents devices pair-ios`) are excluded by the CALLER that reads the
25
- * registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
26
- * role is machine-local by nature and already has a home; duplicating it in the
27
- * shared config would be a second store for one concept.
28
22
  */
29
23
  import { type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
30
24
  export interface AutoPoolOptions {
@@ -19,12 +19,6 @@
19
19
  *
20
20
  * `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
21
21
  * machine the user sits at is marked precisely so agents stay off it.
22
- *
23
- * Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
24
- * by `agents devices pair-ios`) are excluded by the CALLER that reads the
25
- * registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
26
- * role is machine-local by nature and already has a home; duplicating it in the
27
- * shared config would be a second store for one concept.
28
22
  */
29
23
  import { autoPoolMode, listConfiguredDeviceRoles } from '../device-config.js';
30
24
  import { normalizeHost } from '../machine-id.js';
@@ -1,13 +1,5 @@
1
1
  /** Operating-system family of a device, used to pick the remote shell. */
2
2
  export type DevicePlatform = 'windows' | 'linux' | 'macos' | 'unknown';
3
- /**
4
- * What a device is *for*. A `worker` (the default) can run agents — it's dialed
5
- * over SSH for sessions and eligible for run/team placement. A `control` device
6
- * is a cockpit that drives the fleet but never executes agents itself (an
7
- * iPhone/iPad running the companion app): it appears in the fleet but is never
8
- * dialed for SSH nor scheduled work. Absent `role` means `worker`.
9
- */
10
- export type DeviceRole = 'worker' | 'control';
11
3
  /** Remote shell dialect derived from the platform. */
12
4
  export type DeviceShell = 'powershell' | 'posix';
13
5
  /** How `agents ssh` authenticates to a device. Both are first-class, fully
@@ -73,15 +65,9 @@ export interface DeviceProfile {
73
65
  * cached {@link DeviceTailscale.online} snapshot when rendering online/offline,
74
66
  * because the live probe reflects whether the box answered right now. */
75
67
  reachability?: DeviceReachability;
76
- /** What the device is for. Absent means `worker` (see {@link DeviceRole}). */
77
- role?: DeviceRole;
78
68
  createdAt: string;
79
69
  updatedAt: string;
80
70
  }
81
- /** A device's effective role, defaulting to `worker` when unset. */
82
- export declare function deviceRole(d: DeviceProfile): DeviceRole;
83
- /** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
84
- export declare function isControlDevice(d: DeviceProfile): boolean;
85
71
  /**
86
72
  * Whether a fan-out should dial this device, honouring the preference stated on
87
73
  * {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
@@ -127,7 +113,6 @@ export interface DeviceInput {
127
113
  auth?: DeviceAuth;
128
114
  tailscale?: DeviceTailscale;
129
115
  reachability?: DeviceReachability;
130
- role?: DeviceRole;
131
116
  }
132
117
  /**
133
118
  * Create the device if absent, otherwise merge the supplied fields into the
@@ -19,14 +19,6 @@ import * as path from 'path';
19
19
  import lockfile from 'proper-lockfile';
20
20
  import { getDevicesRegistryPath, getDevicesIgnoredPath } from '../state.js';
21
21
  import { atomicWriteJsonSync } from '../fs-atomic.js';
22
- /** A device's effective role, defaulting to `worker` when unset. */
23
- export function deviceRole(d) {
24
- return d.role ?? 'worker';
25
- }
26
- /** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
27
- export function isControlDevice(d) {
28
- return deviceRole(d) === 'control';
29
- }
30
22
  /**
31
23
  * Whether a fan-out should dial this device, honouring the preference stated on
32
24
  * {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
@@ -198,7 +190,6 @@ export async function upsertDevice(name, input) {
198
190
  auth: input.auth ?? prev?.auth ?? { method: 'key' },
199
191
  tailscale: input.tailscale ?? prev?.tailscale,
200
192
  reachability: input.reachability ?? prev?.reachability,
201
- role: input.role ?? prev?.role,
202
193
  createdAt: prev?.createdAt ?? now,
203
194
  updatedAt: now,
204
195
  };
@@ -1,5 +1,15 @@
1
1
  import { probeFleetStats, probeLocalStats, type DeviceStats } from './health.js';
2
2
  import type { DeviceProfile } from './registry.js';
3
+ /**
4
+ * Freshness bound for a cached row. Matches the agent-count mirror's window
5
+ * (`AGENT_STATUS_STALE_MS` in `commands/ssh.ts`) so both columns of
6
+ * `devices list` / `fleet status` share one staleness model. A row older than
7
+ * this is never served as current — it is re-probed live (and the probe result
8
+ * rewrites the cache, unreachable boxes included).
9
+ */
10
+ export declare const STATS_STALE_MS: number;
11
+ /** True when a cached row is still within {@link STATS_STALE_MS}. */
12
+ export declare function isFreshDeviceStats(stats: DeviceStats, now?: number): boolean;
3
13
  /** Read the whole cache (best-effort; a missing/corrupt file yields an empty map). */
4
14
  export declare function readStatsCache(): Record<string, DeviceStats>;
5
15
  /**
@@ -27,6 +37,8 @@ export interface LoadFleetStatsOptions {
27
37
  probeLocal?: typeof probeLocalStats;
28
38
  readCache?: typeof readStatsCache;
29
39
  writeCache?: typeof writeStatsCache;
40
+ /** Clock override for tests (drives the {@link STATS_STALE_MS} bound). */
41
+ now?: number;
30
42
  }
31
43
  /**
32
44
  * Load fleet stats cache-first. See the module doc for the default vs
@@ -15,16 +15,32 @@
15
15
  * - **`--refresh` / `--live`:** skip the cache and live-probe every device,
16
16
  * rewriting the cache.
17
17
  *
18
- * The daemon warms this cache (~every 3 min, see `lib/daemon/daemon.ts
19
- * runFleetCacheWarm`), so in steady state a default read has zero
20
- * remote ssh round-trips and returns immediately with data that is at most a
21
- * few minutes old surfaced to the user as an "as of …" freshness note.
18
+ * Every cached row is bounded by {@link STATS_STALE_MS}: a row older than the
19
+ * window is treated exactly like a missing one re-probed live this call and
20
+ * rewritten so the default read is itself the cache's writer and a value can
21
+ * never fossilize at the last manual `--refresh`. (The daemon used to warm this
22
+ * cache every ~3 min; RUSH-2061 removed that N² fleet probe, which left the
23
+ * unbounded read serving multi-day-old rows as current fleet state — the
24
+ * scheduler, `--device auto`, and the session-start banner all read these
25
+ * numbers. See #2666.)
22
26
  */
23
27
  import * as fs from 'fs';
24
28
  import * as path from 'path';
25
29
  import { getCacheDir } from '../state.js';
26
30
  import { probeFleetStats, probeLocalStats } from './health.js';
27
31
  const CACHE_FILE = '.fleet-stats.json';
32
+ /**
33
+ * Freshness bound for a cached row. Matches the agent-count mirror's window
34
+ * (`AGENT_STATUS_STALE_MS` in `commands/ssh.ts`) so both columns of
35
+ * `devices list` / `fleet status` share one staleness model. A row older than
36
+ * this is never served as current — it is re-probed live (and the probe result
37
+ * rewrites the cache, unreachable boxes included).
38
+ */
39
+ export const STATS_STALE_MS = 3 * 60_000;
40
+ /** True when a cached row is still within {@link STATS_STALE_MS}. */
41
+ export function isFreshDeviceStats(stats, now = Date.now()) {
42
+ return now - stats.fetchedAt <= STATS_STALE_MS;
43
+ }
28
44
  function cacheFilePath() {
29
45
  return path.join(getCacheDir(), CACHE_FILE);
30
46
  }
@@ -71,6 +87,7 @@ export async function loadFleetStats(devices, opts = {}) {
71
87
  const readCache = opts.readCache ?? readStatsCache;
72
88
  const writeCache = opts.writeCache ?? writeStatsCache;
73
89
  const self = opts.selfName;
90
+ const now = opts.now ?? Date.now();
74
91
  const cache = opts.forceRefresh ? {} : readCache();
75
92
  const stats = new Map();
76
93
  const toProbe = [];
@@ -82,11 +99,13 @@ export async function loadFleetStats(devices, opts = {}) {
82
99
  continue;
83
100
  }
84
101
  const cached = cache[d.name];
85
- if (cached) {
102
+ if (cached && isFreshDeviceStats(cached, now)) {
86
103
  stats.set(d.name, cached);
87
104
  servedFromCache = true;
88
105
  }
89
106
  else {
107
+ // Missing OR beyond the staleness bound: a stale number rendered as
108
+ // current is worse than the probe's cost, so re-probe live (#2666).
90
109
  toProbe.push(d);
91
110
  }
92
111
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Interactive drift-sync flow — the single "we detected drift, want to fix it?"
3
- * action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
3
+ * action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
4
4
  * SYNC" row.
5
5
  *
6
6
  * It composes existing pieces, re-implementing nothing:
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Interactive drift-sync flow — the single "we detected drift, want to fix it?"
3
- * action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
3
+ * action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
4
4
  * SYNC" row.
5
5
  *
6
6
  * It composes existing pieces, re-implementing nothing:
@@ -115,7 +115,7 @@ export async function promptDriftSync(opts = {}) {
115
115
  if (opts.yes || !isInteractiveTerminal()) {
116
116
  if (!opts.yes) {
117
117
  // Non-TTY without --yes: report, don't act, don't throw.
118
- console.log(chalk.gray('\nRun `agents status --yes` to sync, or `agents status` in a terminal to choose.'));
118
+ console.log(chalk.gray('\nRun `agents sync status --yes` to sync, or `agents sync status` in a terminal to choose.'));
119
119
  return base;
120
120
  }
121
121
  const systemPulled = await pullSystem(status);
@@ -62,7 +62,7 @@ export interface DriftSummary {
62
62
  * True when the install is out of sync: any installed version is stale,
63
63
  * never-synced, carries unwired hooks, or has a broken generated hook runtime,
64
64
  * OR a source layer is behind origin.
65
- * `agents doctor` surfaces it as "run `agents status`"; `agents doctor --check`
65
+ * `agents doctor` surfaces it as "run `agents sync status`"; `agents doctor --check`
66
66
  * maps it to a non-zero exit. Orphans are a `prune` concern, not sync drift, so they do
67
67
  * NOT set this flag (mirrors the sync-status engine: an orphan alone never
68
68
  * flags needsSync).
package/dist/lib/exec.js CHANGED
@@ -37,6 +37,8 @@ import { applyAddDirs } from './add-dir.js';
37
37
  import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
38
38
  import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js';
39
39
  import { resolveConfigVersion } from './harness/exec-config-version.js';
40
+ import { getAccountInfo } from './agents.js';
41
+ import { getUsageLookupKey, noteClaudeSessionLimit, parseClaudeSessionLimitReset } from './accounting/usage.js';
40
42
  /**
41
43
  * Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
42
44
  *
@@ -2083,12 +2085,20 @@ export async function runWithFallback(options) {
2083
2085
  }
2084
2086
  throw err;
2085
2087
  }
2086
- if (result.exitCode === 0)
2088
+ const output = `${result.stderr}\n${result.stdout}`;
2089
+ const sessionLimitReset = agent === 'claude' ? parseClaudeSessionLimitReset(output) : null;
2090
+ if (sessionLimitReset && version) {
2091
+ const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
2092
+ const usageKey = getUsageLookupKey(account);
2093
+ if (usageKey)
2094
+ noteClaudeSessionLimit(usageKey, sessionLimitReset);
2095
+ }
2096
+ if (result.exitCode === 0 && !sessionLimitReset)
2087
2097
  return 0;
2088
2098
  const isLast = i === chain.length - 1;
2089
2099
  if (isLast)
2090
- return result.exitCode;
2091
- if (!detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
2100
+ return result.exitCode || 1;
2101
+ if (!sessionLimitReset && !detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
2092
2102
  return result.exitCode;
2093
2103
  }
2094
2104
  const next = chain[i + 1];
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { FleetManifest, FleetDefaults } from './types.js';
17
17
  export interface CaptureInputs {
18
- /** Registered, non-control device names to record (the roster — names only). */
18
+ /** Registered device names to record (the roster — names only). */
19
19
  devices: string[];
20
20
  /** Optional per-device agent specs (e.g. from `--from-pins`), keyed by name. */
21
21
  agentsByDevice?: Record<string, string[]>;
@@ -124,7 +124,7 @@ export function readFleetFile(filePath) {
124
124
  }
125
125
  const fleet = doc.fleet;
126
126
  if (fleet === undefined) {
127
- throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents apply --help\`).`);
127
+ throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents fleet apply --help\`).`);
128
128
  }
129
129
  return parseFleetManifest(fleet);
130
130
  }
@@ -63,7 +63,7 @@ export interface SelectLoginOptions {
63
63
  *
64
64
  * A pair is pending when its cached verdict {@link isPendingVerdict} (or
65
65
  * `includeLoggedIn` forces it). Agents with no defined login flow are dropped
66
- * silently (nothing to log into). Control devices are excluded by the caller.
66
+ * silently (nothing to log into).
67
67
  */
68
68
  export declare function selectLoginTargets(devices: DeviceProfile[], agents: (AgentId | string)[], cache: Record<string, AuthHealth>, opts?: SelectLoginOptions): PendingLogin[];
69
69
  /**
@@ -146,7 +146,7 @@ export interface DetectOptions {
146
146
  includeLoggedIn?: boolean;
147
147
  }
148
148
  /**
149
- * Resolve the online (non-control) fleet devices, then compute pending logins
149
+ * Resolve the online fleet devices, then compute pending logins
150
150
  * from the shared auth-health cache (populated by `agents fleet ping`). Thin I/O
151
151
  * over the pure {@link selectLoginTargets}. Devices/agents can be narrowed by the
152
152
  * caller's flags; the default agent set is every agent with a defined flow.
@@ -27,7 +27,6 @@
27
27
  * human works through the earlier ones.
28
28
  */
29
29
  import * as http from 'http';
30
- import { isControlDevice } from '../devices/registry.js';
31
30
  import { loadDevices } from '../devices/registry.js';
32
31
  import { deviceIdentityArgs, fleetDialTarget } from '../devices/connect.js';
33
32
  import { planFleetTargets } from '../devices/fleet.js';
@@ -117,7 +116,7 @@ function worstCachedVerdict(cache, host, agent) {
117
116
  *
118
117
  * A pair is pending when its cached verdict {@link isPendingVerdict} (or
119
118
  * `includeLoggedIn` forces it). Agents with no defined login flow are dropped
120
- * silently (nothing to log into). Control devices are excluded by the caller.
119
+ * silently (nothing to log into).
121
120
  */
122
121
  export function selectLoginTargets(devices, agents, cache, opts = {}) {
123
122
  const out = [];
@@ -408,7 +407,7 @@ export async function driveRemoteLogin(target, flow, driver, opts = {}, extraSsh
408
407
  }
409
408
  }
410
409
  /**
411
- * Resolve the online (non-control) fleet devices, then compute pending logins
410
+ * Resolve the online fleet devices, then compute pending logins
412
411
  * from the shared auth-health cache (populated by `agents fleet ping`). Thin I/O
413
412
  * over the pure {@link selectLoginTargets}. Devices/agents can be narrowed by the
414
413
  * caller's flags; the default agent set is every agent with a defined flow.
@@ -417,7 +416,7 @@ export async function detectPending(opts = {}) {
417
416
  const reg = await loadDevices();
418
417
  const self = machineId();
419
418
  const online = planFleetTargets(reg)
420
- .filter((t) => !t.skip && t.device.name !== self && !isControlDevice(t.device))
419
+ .filter((t) => !t.skip && t.device.name !== self)
421
420
  .map((t) => t.device);
422
421
  const deviceFilter = opts.devices && opts.devices.length > 0 ? new Set(opts.devices) : null;
423
422
  const devices = deviceFilter ? online.filter((d) => deviceFilter.has(d.name)) : online;
@@ -27,6 +27,15 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
27
27
  * keep the original red-stderr behavior.
28
28
  */
29
29
  export declare function die(msg: string, code?: number, opts?: DieOptions): never;
30
+ /**
31
+ * Await a command action and turn a thrown Error into a clean `die(message)`
32
+ * instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
33
+ * rethrows plain Errors as engineering bugs, so a command whose helpers throw
34
+ * user-actionable errors (auth, org) wraps its `.action(...)` call site with
35
+ * this. The helpers keep throwing so tests can assert on them. Pass `json`
36
+ * so a `--json` caller still gets the structured `{"error"}` payload.
37
+ */
38
+ export declare function runOrDie(fn: () => void | Promise<void>, opts?: DieOptions): Promise<void>;
30
39
  /**
31
40
  * `die()` with a structured friction event attached. Use this at CLI error
32
41
  * chokepoints so the nightly routine can classify and rank recurring failures
@@ -44,6 +44,22 @@ export function die(msg, code = 1, opts = {}) {
44
44
  console.error(text);
45
45
  process.exit(code);
46
46
  }
47
+ /**
48
+ * Await a command action and turn a thrown Error into a clean `die(message)`
49
+ * instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
50
+ * rethrows plain Errors as engineering bugs, so a command whose helpers throw
51
+ * user-actionable errors (auth, org) wraps its `.action(...)` call site with
52
+ * this. The helpers keep throwing so tests can assert on them. Pass `json`
53
+ * so a `--json` caller still gets the structured `{"error"}` payload.
54
+ */
55
+ export async function runOrDie(fn, opts = {}) {
56
+ try {
57
+ await fn();
58
+ }
59
+ catch (err) {
60
+ die(err instanceof Error ? err.message : String(err), 1, opts);
61
+ }
62
+ }
47
63
  /**
48
64
  * `die()` with a structured friction event attached. Use this at CLI error
49
65
  * chokepoints so the nightly routine can classify and rank recurring failures