@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -118,6 +118,57 @@ export function usageUnreachableError(agent, cause) {
118
118
  ? `${agent} usage read failed: ${detail}`
119
119
  : `${agent} usage read failed.`;
120
120
  }
121
+ /**
122
+ * Marker for a log-based (`network: false`) provider — Codex, Grok — that has
123
+ * simply never recorded a rate-limit event on this machine yet: no session
124
+ * log exists, or no session in it carries usage data. Distinct on purpose from
125
+ * `usageUnreachableError`: that one means the local log COULDN'T be read (a
126
+ * real failure worth surfacing distinctly); this one means there is nothing to
127
+ * read because the account has not run here, which is expected for a fresh
128
+ * install and should render as a benign state, not an error (RUSH-3040).
129
+ */
130
+ export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
131
+ export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
132
+ /**
133
+ * Shared error-classification + 429 backoff for a networked usage fetch whose
134
+ * only signal is an HTTP status (or none at all, on a network failure) —
135
+ * Antigravity's :retrieveUserQuota and Muse's Meta Model API probe are both
136
+ * this shape. They were added to `USAGE_SOURCES` after the four original
137
+ * `usageXError` constructors and did not get their own scheme (usage.ts's
138
+ * error handling was written for "four networked providers"; RUSH-3040).
139
+ * Route every no-snapshot outcome for either through this one function so a
140
+ * future entry cannot be added second-class again — it owns noting the 429
141
+ * backoff, so callers must NOT also call {@link noteUsageRateLimited} for the
142
+ * same response.
143
+ */
144
+ export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHeader, usageScope) {
145
+ if (status === 429) {
146
+ noteUsageRateLimited(agentId, retryAfterHeader ?? null, { account: usageScope });
147
+ return usageRejectedError(agent, 429);
148
+ }
149
+ if (status !== null)
150
+ return usageRejectedError(agent, status);
151
+ return usageUnreachableError(agent);
152
+ }
153
+ /** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
154
+ export function classifyUsageErrorKind(error) {
155
+ if (!error)
156
+ return null;
157
+ if (isUsageHeadlessScopeError(error))
158
+ return 'headless-scope';
159
+ if (error.startsWith('No readable '))
160
+ return 'no-credential';
161
+ if (error.includes('credential expired'))
162
+ return 'expired-credential';
163
+ if (error.includes('rate-limited this machine') || error.includes('is rate-limiting the usage endpoint')) {
164
+ return 'rate-limited';
165
+ }
166
+ if (error.includes('rejected the usage read'))
167
+ return 'rejected';
168
+ if (error.includes('usage read failed'))
169
+ return 'unreachable';
170
+ return 'rejected';
171
+ }
121
172
  /**
122
173
  * True when a Claude OAuth access token is within the refresh leeway of expiry
123
174
  * (or already expired) — i.e. it "would need a refresh" before the next use.
@@ -165,6 +216,14 @@ const COMPACT_BAR_LEN = 5;
165
216
  const USAGE_BAR_LEN = 10;
166
217
  const FULL = '\u2588';
167
218
  const EMPTY = '\u2591';
219
+ /** Construct the benign no-local-log result without overloading `error`. */
220
+ export function usageNoRecentUsageInfo() {
221
+ return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
222
+ }
223
+ /** Read a benign state for the human renderer; symbols are omitted by JSON serialization. */
224
+ export function getUsageBenignState(info) {
225
+ return info[USAGE_BENIGN_STATE] ?? null;
226
+ }
168
227
  /** The single registry of agent usage sources and their transport. */
169
228
  const USAGE_SOURCES = {
170
229
  claude: { fetch: getClaudeUsageInfo, network: true },
@@ -333,6 +392,9 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
333
392
  home: input.home,
334
393
  cliVersion: input.cliVersion,
335
394
  organizationId: input.info.organizationId,
395
+ // Scope this fetch's 429 backoff to the account being fetched, so one
396
+ // throttled account cannot park the whole provider (RUSH-3036).
397
+ usageScope: usageKey,
336
398
  fileOnly,
337
399
  });
338
400
  if (usage.snapshot) {
@@ -391,6 +453,26 @@ export function pickCompactUsageWindows(windows, maxWindows) {
391
453
  }
392
454
  return chosen;
393
455
  }
456
+ /** Human label for a classified usage error, for the no-bars branch of {@link formatUsageSummary}. */
457
+ function formatUsageErrorKindLabel(kind, detail) {
458
+ switch (kind) {
459
+ case 'no-credential':
460
+ return 'sign in / provision token';
461
+ case 'expired-credential':
462
+ return 're-auth for usage';
463
+ case 'rate-limited': {
464
+ const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
465
+ return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
466
+ }
467
+ case 'rejected':
468
+ case 'unreachable':
469
+ case 'headless-scope':
470
+ case null:
471
+ case undefined:
472
+ default:
473
+ return 'usage unavailable';
474
+ }
475
+ }
394
476
  /** Format a one-line usage summary with compact bars for inline display. */
395
477
  export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
396
478
  const parts = [];
@@ -445,11 +527,16 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
445
527
  // mistaken for a missing setup-token or seeding failure (RUSH-2392).
446
528
  parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
447
529
  }
530
+ else if (opts?.benignState === 'no-recent-usage') {
531
+ parts.push(chalk.dim(USAGE_NO_RECENT_USAGE_MARKER));
532
+ }
448
533
  else if (opts?.unavailable) {
449
534
  // Signed-in account we could NOT fetch usage for (no live token in a reachable
450
535
  // home / org mismatch / fetch error). Say so explicitly instead of drawing a
451
- // blank gauge that reads like "0% used".
452
- parts.push(chalk.dim('usage unavailable'));
536
+ // blank gauge that reads like "0% used" — and name the SPECIFIC cause when
537
+ // the caller passed one, rather than the generic bucket that used to cover
538
+ // ~6 different failures (RUSH-3040).
539
+ parts.push(chalk.dim(formatUsageErrorKindLabel(opts.errorKind, opts.errorDetail)));
453
540
  }
454
541
  return parts.join(' ');
455
542
  }
@@ -598,11 +685,17 @@ async function getCodexUsageInfo(options) {
598
685
  return { snapshot: null, error: null };
599
686
  }
600
687
  const files = collectCodexSessionFiles(options?.home, sinceMs);
688
+ const now = new Date();
601
689
  for (const filePath of files) {
602
690
  const match = await readLatestCodexRateLimits(filePath);
603
691
  if (!match)
604
692
  continue;
605
- const windows = normalizeCodexWindows(match.rateLimits);
693
+ // Same freshness filter Grok already applies (RUSH-3040): a window whose
694
+ // reset time or windowMinutes-derived expiry has passed is a STALE read,
695
+ // not a current one — rendering it as-is is how a codex bar kept showing
696
+ // "100% used" past its own reset. Try the next-older session file rather
697
+ // than surfacing a stale bar.
698
+ const windows = normalizeCodexWindows(match.rateLimits).filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
606
699
  if (windows.length === 0)
607
700
  continue;
608
701
  return {
@@ -615,10 +708,14 @@ async function getCodexUsageInfo(options) {
615
708
  error: null,
616
709
  };
617
710
  }
618
- return { snapshot: null, error: null };
711
+ // No session ever recorded a rate-limit event on this machine (or none of
712
+ // the ones found were still fresh) — a benign "nothing to show yet", not a
713
+ // failure (RUSH-3040). Distinct from the outer catch below, which is a
714
+ // genuine read/parse failure.
715
+ return usageNoRecentUsageInfo();
619
716
  }
620
- catch {
621
- return { snapshot: null, error: null };
717
+ catch (err) {
718
+ return { snapshot: null, error: usageUnreachableError('Codex', err) };
622
719
  }
623
720
  }
624
721
  /**
@@ -671,7 +768,7 @@ async function getClaudeUsageInfo(options) {
671
768
  }
672
769
  // Honour a live Retry-After rather than re-arming the penalty (see
673
770
  // usage-backoff.ts). No request at all while the window is open.
674
- const throttledUntil = usageRateLimitedUntil('claude');
771
+ const throttledUntil = usageRateLimitedUntil('claude', Date.now(), options?.usageScope);
675
772
  if (throttledUntil) {
676
773
  return { snapshot: null, error: usageThrottledError('Claude', throttledUntil) };
677
774
  }
@@ -687,7 +784,7 @@ async function getClaudeUsageInfo(options) {
687
784
  });
688
785
  if (!response.ok) {
689
786
  if (response.status === 429) {
690
- noteUsageRateLimited('claude', response.headers.get('retry-after'));
787
+ noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: options?.usageScope });
691
788
  }
692
789
  // Setup-token is user:inference only; usage needs user:profile → 403
693
790
  // with a scope-requirement body. Distinct from a real rejection so the
@@ -780,7 +877,7 @@ async function getKimiUsageInfo(options) {
780
877
  }
781
878
  // Honour a live Retry-After rather than re-arming the penalty (see
782
879
  // usage-backoff.ts). No request at all while the window is open.
783
- const throttledUntil = usageRateLimitedUntil('kimi');
880
+ const throttledUntil = usageRateLimitedUntil('kimi', Date.now(), options?.usageScope);
784
881
  if (throttledUntil) {
785
882
  return { snapshot: null, error: usageThrottledError('Kimi', throttledUntil) };
786
883
  }
@@ -796,7 +893,7 @@ async function getKimiUsageInfo(options) {
796
893
  // way there are no bars to draw, and the status is what tells them apart.
797
894
  if (!response.ok) {
798
895
  if (response.status === 429) {
799
- noteUsageRateLimited('kimi', response.headers.get('retry-after'));
896
+ noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: options?.usageScope });
800
897
  }
801
898
  return { snapshot: null, error: usageRejectedError('Kimi', response.status) };
802
899
  }
@@ -914,7 +1011,7 @@ async function getDroidUsageInfo(options) {
914
1011
  }
915
1012
  // Honour a live Retry-After rather than re-arming the penalty (see
916
1013
  // usage-backoff.ts). No request at all while the window is open.
917
- const throttledUntil = usageRateLimitedUntil('droid');
1014
+ const throttledUntil = usageRateLimitedUntil('droid', Date.now(), options?.usageScope);
918
1015
  if (throttledUntil) {
919
1016
  return { snapshot: null, error: usageThrottledError('Droid', throttledUntil) };
920
1017
  }
@@ -929,7 +1026,7 @@ async function getDroidUsageInfo(options) {
929
1026
  // 401 => revoked/expired token. No bars to draw, and the status says why.
930
1027
  if (!response.ok) {
931
1028
  if (response.status === 429) {
932
- noteUsageRateLimited('droid', response.headers.get('retry-after'));
1029
+ noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: options?.usageScope });
933
1030
  }
934
1031
  return { snapshot: null, error: usageRejectedError('Droid', response.status) };
935
1032
  }
@@ -956,7 +1053,7 @@ async function getDroidUsageInfo(options) {
956
1053
  }
957
1054
  }
958
1055
  /** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
959
- export async function probeClaudeStatus(home, cliVersion) {
1056
+ export async function probeClaudeStatus(home, cliVersion, usageScope) {
960
1057
  // accessTokenCache: the daemon warms this probe every ~3 min per account, so it
961
1058
  // reads ONLY the file-based setup-token and never the interactive login —
962
1059
  // transmitting that ACL-bound token to the usage API from a background loop is
@@ -981,7 +1078,7 @@ export async function probeClaudeStatus(home, cliVersion) {
981
1078
  // window is open, report the throttle from the recorded state instead of
982
1079
  // firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
983
1080
  // probe is what created the loop it now respects.
984
- if (usageRateLimitedUntil('claude'))
1081
+ if (usageRateLimitedUntil('claude', Date.now(), usageScope))
985
1082
  return { status: 429, token: 'present' };
986
1083
  try {
987
1084
  const response = await fetch(CLAUDE_USAGE_URL, {
@@ -995,7 +1092,7 @@ export async function probeClaudeStatus(home, cliVersion) {
995
1092
  signal: AbortSignal.timeout(8000),
996
1093
  });
997
1094
  if (response.status === 429) {
998
- noteUsageRateLimited('claude', response.headers.get('retry-after'));
1095
+ noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: usageScope });
999
1096
  }
1000
1097
  // Setup-token is user:inference only; usage needs user:profile → 403.
1001
1098
  // That is NOT a revocation — the account still runs (RUSH-2392).
@@ -1023,7 +1120,7 @@ export async function probeClaudeStatus(home, cliVersion) {
1023
1120
  }
1024
1121
  }
1025
1122
  /** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
1026
- export async function probeKimiStatus(home) {
1123
+ export async function probeKimiStatus(home, usageScope) {
1027
1124
  const credPath = resolveKimiCredentialPath(home);
1028
1125
  if (!credPath)
1029
1126
  return { status: null, token: 'missing' };
@@ -1047,7 +1144,7 @@ export async function probeKimiStatus(home) {
1047
1144
  // is what created the loop it now respects. It sits AFTER the local
1048
1145
  // missing/expired checks — as in probeClaudeStatus and probeDroidStatus — so
1049
1146
  // a genuinely broken credential is never misreported as merely throttled.
1050
- if (usageRateLimitedUntil('kimi'))
1147
+ if (usageRateLimitedUntil('kimi', Date.now(), usageScope))
1051
1148
  return { status: 429, token: 'present' };
1052
1149
  try {
1053
1150
  const response = await fetch(KIMI_USAGES_URL, {
@@ -1056,7 +1153,7 @@ export async function probeKimiStatus(home) {
1056
1153
  signal: AbortSignal.timeout(8000),
1057
1154
  });
1058
1155
  if (response.status === 429) {
1059
- noteUsageRateLimited('kimi', response.headers.get('retry-after'));
1156
+ noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: usageScope });
1060
1157
  }
1061
1158
  return { status: response.status, token: 'present' };
1062
1159
  }
@@ -1065,7 +1162,7 @@ export async function probeKimiStatus(home) {
1065
1162
  }
1066
1163
  }
1067
1164
  /** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
1068
- export async function probeDroidStatus(home) {
1165
+ export async function probeDroidStatus(home, usageScope) {
1069
1166
  const cred = decryptDroidAuthPayload(home || os.homedir());
1070
1167
  const accessToken = cred?.access_token;
1071
1168
  if (typeof accessToken !== 'string' || !accessToken)
@@ -1077,7 +1174,7 @@ export async function probeDroidStatus(home) {
1077
1174
  // window is open, report the throttle from the recorded state instead of
1078
1175
  // firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
1079
1176
  // probe is what created the loop it now respects.
1080
- if (usageRateLimitedUntil('droid'))
1177
+ if (usageRateLimitedUntil('droid', Date.now(), usageScope))
1081
1178
  return { status: 429, token: 'present' };
1082
1179
  try {
1083
1180
  const response = await fetch(DROID_USAGE_URL, {
@@ -1086,7 +1183,7 @@ export async function probeDroidStatus(home) {
1086
1183
  signal: AbortSignal.timeout(8000),
1087
1184
  });
1088
1185
  if (response.status === 429) {
1089
- noteUsageRateLimited('droid', response.headers.get('retry-after'));
1186
+ noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: usageScope });
1090
1187
  }
1091
1188
  return { status: response.status, token: 'present' };
1092
1189
  }
@@ -1561,6 +1658,10 @@ function serializeClaudeUsageSnapshot(snapshot) {
1561
1658
  * candidate (RUSH-2858). Dropping mirrors the Grok collector, and an all-expired
1562
1659
  * snapshot deserializes to null so `readClaudeUsageCache` deletes the entry and
1563
1660
  * callers surface "usage unavailable" plus the recorded throttle reason.
1661
+ *
1662
+ * A row that carries a plan survives even with no fresh windows: the plan is a
1663
+ * truthful reading in its own right, and losing it is what made the cached view
1664
+ * contradict the refreshed one for meterless harnesses. See the guard below.
1564
1665
  */
1565
1666
  function deserializeClaudeUsageSnapshot(snapshot, now) {
1566
1667
  const capturedAt = parseDateValue(snapshot.capturedAt);
@@ -1575,7 +1676,17 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
1575
1676
  }))
1576
1677
  .filter((window) => isCachedUsageWindowFresh(window, capturedAt, now));
1577
1678
  const unavailable = deserializeUnavailable(snapshot.unavailable, now);
1578
- if (windows.length === 0 && !unavailable) {
1679
+ // A windowless row is not automatically worthless. Grok's collector reports
1680
+ // the subscription tier and no meters at all, so treating "no fresh windows"
1681
+ // as "nothing cached" deleted the only truthful thing we knew about the
1682
+ // account: `--refresh` wrote {plan: 'SuperGrok Heavy', windows: []}, the very
1683
+ // next plain `agents view` deserialized it to null, `readClaudeUsageCache`
1684
+ // pruned the row, and the row rendered "usage unavailable" one read after a
1685
+ // successful refresh. Keep a plan-bearing row — it renders as the plan alone,
1686
+ // and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
1687
+ // it can never read as a 0% bar or an "available" badge (the RUSH-2858
1688
+ // property that made expired windows drop in the first place).
1689
+ if (windows.length === 0 && !unavailable && !snapshot.plan) {
1579
1690
  return null;
1580
1691
  }
1581
1692
  return {
@@ -1955,11 +2066,12 @@ async function getGrokUsageInfo(options) {
1955
2066
  try {
1956
2067
  const base = options?.home || os.homedir();
1957
2068
  const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
2069
+ // No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
1958
2070
  if (!fs.existsSync(logPath))
1959
- return { snapshot: null, error: null };
2071
+ return usageNoRecentUsageInfo();
1960
2072
  const match = await readLatestGrokBilling(logPath);
1961
2073
  if (!match)
1962
- return { snapshot: null, error: null };
2074
+ return usageNoRecentUsageInfo();
1963
2075
  // Grok has no live usage API (`network: false`) — bars are last-seen from
1964
2076
  // this machine's unified.jsonl only. Drop windows whose billing period has
1965
2077
  // already ended so a stale 100% does not paint "rate-limited" after reset,
@@ -1968,6 +2080,10 @@ async function getGrokUsageInfo(options) {
1968
2080
  // (see readLatestGrokBilling).
1969
2081
  const now = new Date();
1970
2082
  const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
2083
+ // NOTE: an empty `windows` here (all windows expired) still returns a real
2084
+ // snapshot, not the no-recent-usage marker — the plan/tier is still valid
2085
+ // and callers (e.g. `formatUsageSummary`) render "no bars" correctly from
2086
+ // an empty array. The marker above is only for "nothing was ever recorded".
1971
2087
  return {
1972
2088
  snapshot: {
1973
2089
  source: 'last_seen',
@@ -1979,8 +2095,8 @@ async function getGrokUsageInfo(options) {
1979
2095
  error: null,
1980
2096
  };
1981
2097
  }
1982
- catch {
1983
- return { snapshot: null, error: null };
2098
+ catch (err) {
2099
+ return { snapshot: null, error: usageUnreachableError('Grok', err) };
1984
2100
  }
1985
2101
  }
1986
2102
  /**
@@ -1994,16 +2110,40 @@ async function getGrokUsageInfo(options) {
1994
2110
  async function getMuseUsageInfo(options) {
1995
2111
  try {
1996
2112
  const base = options?.home || os.homedir();
1997
- const live = await probeMuseRateLimits(base);
1998
- if (live)
1999
- return { snapshot: live, error: null };
2113
+ // Honour a live Retry-After rather than re-arming the penalty (see
2114
+ // usage-backoff.ts). The local log fallback still works while the live
2115
+ // probe is throttled only report the throttle when there is truly
2116
+ // nothing else to show.
2117
+ const throttledUntil = usageRateLimitedUntil('muse', Date.now(), options?.usageScope);
2118
+ if (throttledUntil) {
2119
+ const local = await readMuseLocalSessionUsage(base);
2120
+ if (local)
2121
+ return { snapshot: local, error: null };
2122
+ return { snapshot: null, error: usageThrottledError('Muse', throttledUntil) };
2123
+ }
2124
+ const probe = await probeMuseRateLimits(base);
2125
+ if (probe.snapshot)
2126
+ return { snapshot: probe.snapshot, error: null };
2000
2127
  const local = await readMuseLocalSessionUsage(base);
2001
- if (!local)
2002
- return { snapshot: null, error: null };
2003
- return { snapshot: local, error: null };
2128
+ if (local)
2129
+ return { snapshot: local, error: null };
2130
+ // No live snapshot and no local log every source came up empty. Only
2131
+ // now does the probe's own outcome become the reported error, mirroring
2132
+ // Cursor's "surface the last resort's failure" pattern above.
2133
+ if (!probe.hasKey)
2134
+ return { snapshot: null, error: usageNoCredentialError('Muse') };
2135
+ if (probe.noHeaders)
2136
+ return usageNoRecentUsageInfo();
2137
+ return {
2138
+ snapshot: null,
2139
+ error: classifyUsageFetchFailure('Muse', 'muse', probe.status, probe.retryAfter, options?.usageScope),
2140
+ };
2004
2141
  }
2005
- catch {
2006
- return { snapshot: null, error: null };
2142
+ catch (err) {
2143
+ // A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
2144
+ // read like any other — staying silent here would hand the caller a stale
2145
+ // snapshot to render as confirmed (RUSH-3040).
2146
+ return { snapshot: null, error: usageUnreachableError('Muse', err) };
2007
2147
  }
2008
2148
  }
2009
2149
  /** Resolve a Muse API key from env or auth.json without logging the value. */
@@ -2037,25 +2177,28 @@ function resolveMuseApiKey(base) {
2037
2177
  }
2038
2178
  /**
2039
2179
  * Probe Meta Model API for rate-limit headers. Uses GET /v1/models (no token
2040
- * spend). Returns null when unauthenticated or the headers are absent.
2180
+ * spend). The 429 backoff is noted by the CALLER via
2181
+ * {@link classifyUsageFetchFailure}, not here, so a throttled read is recorded
2182
+ * exactly once regardless of which branch of `getMuseUsageInfo` observes it.
2041
2183
  */
2042
2184
  async function probeMuseRateLimits(base) {
2043
- if (usageRateLimitedUntil('muse'))
2044
- return null;
2045
2185
  const key = resolveMuseApiKey(base);
2046
2186
  if (!key)
2047
- return null;
2187
+ return { snapshot: null, hasKey: false, status: null, retryAfter: null, noHeaders: false };
2048
2188
  try {
2049
2189
  const response = await fetch('https://api.meta.ai/v1/models', {
2050
2190
  headers: { Authorization: `Bearer ${key}` },
2051
2191
  signal: AbortSignal.timeout(8_000),
2052
2192
  });
2053
- if (response.status === 429) {
2054
- noteUsageRateLimited('muse', response.headers.get('retry-after'));
2055
- return null;
2193
+ if (!response.ok) {
2194
+ return {
2195
+ snapshot: null,
2196
+ hasKey: true,
2197
+ status: response.status,
2198
+ retryAfter: response.headers.get('retry-after'),
2199
+ noHeaders: false,
2200
+ };
2056
2201
  }
2057
- if (!response.ok)
2058
- return null;
2059
2202
  const limitTokens = headerNumber(response.headers, 'x-ratelimit-limit-tokens');
2060
2203
  const remainingTokens = headerNumber(response.headers, 'x-ratelimit-remaining-tokens');
2061
2204
  const limitRequests = headerNumber(response.headers, 'x-ratelimit-limit-requests');
@@ -2083,18 +2226,25 @@ async function probeMuseRateLimits(base) {
2083
2226
  windowMinutes: 1,
2084
2227
  });
2085
2228
  }
2086
- if (windows.length === 0)
2087
- return null;
2229
+ if (windows.length === 0) {
2230
+ return { snapshot: null, hasKey: true, status: response.status, retryAfter: null, noHeaders: true };
2231
+ }
2088
2232
  return {
2089
- source: 'live',
2090
- sourceLabel: 'Meta Model API rate limits',
2091
- capturedAt: new Date(),
2092
- windows,
2093
- plan: 'Meta Model API',
2233
+ snapshot: {
2234
+ source: 'live',
2235
+ sourceLabel: 'Meta Model API rate limits',
2236
+ capturedAt: new Date(),
2237
+ windows,
2238
+ plan: 'Meta Model API',
2239
+ },
2240
+ hasKey: true,
2241
+ status: response.status,
2242
+ retryAfter: null,
2243
+ noHeaders: false,
2094
2244
  };
2095
2245
  }
2096
2246
  catch {
2097
- return null;
2247
+ return { snapshot: null, hasKey: true, status: null, retryAfter: null, noHeaders: false };
2098
2248
  }
2099
2249
  }
2100
2250
  function headerNumber(headers, name) {
@@ -2660,10 +2810,14 @@ async function refreshAntigravityAccessToken(refreshToken) {
2660
2810
  }
2661
2811
  /**
2662
2812
  * POST :retrieveUserQuota against the Code Assist endpoints in order, returning
2663
- * the first successful bucket list. null when every endpoint rejects (expired
2664
- * token, no quota API for the account) or the network fails.
2813
+ * the first successful bucket list. `buckets: null` when every endpoint rejects
2814
+ * (expired token, no quota API for the account) or the network fails — `status`
2815
+ * carries the LAST rejection's HTTP status (or null when every attempt threw)
2816
+ * so the caller can classify the failure instead of it reading as silence.
2665
2817
  */
2666
2818
  async function fetchAntigravityQuota(accessToken) {
2819
+ let lastStatus = null;
2820
+ let lastRetryAfter = null;
2667
2821
  for (const url of ANTIGRAVITY_QUOTA_URLS) {
2668
2822
  try {
2669
2823
  const response = await fetch(url, {
@@ -2676,16 +2830,19 @@ async function fetchAntigravityQuota(accessToken) {
2676
2830
  body: '{}',
2677
2831
  signal: AbortSignal.timeout(5000),
2678
2832
  });
2679
- if (!response.ok)
2833
+ if (!response.ok) {
2834
+ lastStatus = response.status;
2835
+ lastRetryAfter = response.headers.get('retry-after');
2680
2836
  continue;
2837
+ }
2681
2838
  const data = (await response.json());
2682
- return Array.isArray(data?.buckets) ? data.buckets : [];
2839
+ return { buckets: Array.isArray(data?.buckets) ? data.buckets : [], status: response.status, retryAfter: null };
2683
2840
  }
2684
2841
  catch {
2685
2842
  continue;
2686
2843
  }
2687
2844
  }
2688
- return null;
2845
+ return { buckets: null, status: lastStatus, retryAfter: lastRetryAfter };
2689
2846
  }
2690
2847
  /** Compact model tag for the inline bar — 'gemini-2.5-flash-lite' => '2.5FL'. */
2691
2848
  export function antigravityModelShortLabel(modelId) {
@@ -2747,16 +2904,28 @@ async function getAntigravityUsageInfo(options) {
2747
2904
  try {
2748
2905
  const token = await loadAntigravityOauth(options?.home);
2749
2906
  if (!token)
2750
- return { snapshot: null, error: null };
2907
+ return { snapshot: null, error: usageNoCredentialError('Antigravity') };
2751
2908
  let accessToken = normalizeString(token.access_token);
2752
2909
  if ((!accessToken || antigravityTokenNeedsRefresh(token.expiry)) && token.refresh_token) {
2753
2910
  accessToken = await refreshAntigravityAccessToken(token.refresh_token);
2754
2911
  }
2755
2912
  if (!accessToken)
2756
- return { snapshot: null, error: null };
2757
- const buckets = await fetchAntigravityQuota(accessToken);
2758
- if (!buckets)
2759
- return { snapshot: null, error: null };
2913
+ return { snapshot: null, error: usageExpiredCredentialError('Antigravity') };
2914
+ // Honour a live Retry-After rather than re-arming the penalty (see
2915
+ // usage-backoff.ts). No request at all while the window is open. Antigravity
2916
+ // previously had NO rate-limit backoff at all (RUSH-3040) — every refresh
2917
+ // re-hit a throttled endpoint.
2918
+ const throttledUntil = usageRateLimitedUntil('antigravity', Date.now(), options?.usageScope);
2919
+ if (throttledUntil) {
2920
+ return { snapshot: null, error: usageThrottledError('Antigravity', throttledUntil) };
2921
+ }
2922
+ const { buckets, status, retryAfter } = await fetchAntigravityQuota(accessToken);
2923
+ if (!buckets) {
2924
+ return {
2925
+ snapshot: null,
2926
+ error: classifyUsageFetchFailure('Antigravity', 'antigravity', status, retryAfter, options?.usageScope),
2927
+ };
2928
+ }
2760
2929
  const windows = normalizeAntigravityWindows(buckets);
2761
2930
  if (windows.length === 0)
2762
2931
  return { snapshot: null, error: null };
@@ -2770,7 +2939,10 @@ async function getAntigravityUsageInfo(options) {
2770
2939
  error: null,
2771
2940
  };
2772
2941
  }
2773
- catch {
2774
- return { snapshot: null, error: null };
2942
+ catch (err) {
2943
+ // A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
2944
+ // read like any other — staying silent here would hand the caller a stale
2945
+ // snapshot to render as confirmed (RUSH-3040).
2946
+ return { snapshot: null, error: usageUnreachableError('Antigravity', err) };
2775
2947
  }
2776
2948
  }
@@ -14,9 +14,28 @@ import { resolveRunDefaults } from './run-defaults.js';
14
14
  export const MODE_DESCRIPTIONS = {
15
15
  plan: 'read-only investigation; no writes, no shell side-effects',
16
16
  edit: 'may edit files; prompts for shell / risky operations',
17
- auto: 'smart classifier auto-approves safe ops, prompts for risky',
17
+ auto: 'more autonomy than edit; the exact policy is per-harness (see notes)',
18
18
  skip: 'bypass every permission prompt (dangerously-skip-permissions)',
19
19
  };
20
+ /**
21
+ * What `auto` actually does, per harness. Two genuinely different mechanisms
22
+ * wear the same mode name, and conflating them is a safety error in both
23
+ * directions: telling a claude operator that auto never prompts invites an
24
+ * unattended run that stalls on a risky-operation gate, and telling a codex
25
+ * operator that auto prompts for risky work asserts a gate codex does not have.
26
+ * Absent entry = no extra note; the row's own text suffices.
27
+ */
28
+ const AUTO_SEMANTICS = {
29
+ // Classifier-style: still escalates. --permission-mode auto / --autopilot.
30
+ claude: 'a smart classifier auto-approves safe operations and still prompts for risky ones.',
31
+ copilot: 'a smart classifier auto-approves safe operations and still prompts for risky ones.',
32
+ // Approvals off, sandbox kept: never escalates. A denied command just fails.
33
+ codex: 'approval_policy=never over the same sandbox as edit — it never prompts, and a sandbox-denied command fails instead of raising an approval request.',
34
+ muse: 'runs --disable-approval, which turns approvals off while keeping the sandbox — like codex, it never prompts and a denied command fails.',
35
+ // Native autonomy dial at its top setting.
36
+ droid: 'runs --auto high, droid\'s full-autonomy setting.',
37
+ kimi: "runs kimi's native --auto.",
38
+ };
20
39
  /**
21
40
  * Build the permission-mode catalog for one harness.
22
41
  * `version` only affects the configured run.defaults lookup (modes themselves
@@ -42,6 +61,15 @@ export function getAgentModesCatalog(agent, version, cwd = process.cwd()) {
42
61
  if (unsupported.includes('auto')) {
43
62
  notes.push(`--mode auto degrades to edit on ${agent} (no native auto classifier).`);
44
63
  }
64
+ // MODE_DESCRIPTIONS is one flat Record<Mode, string> rendered for every agent,
65
+ // so it cannot name any single harness's mechanism without lying about the
66
+ // others: claude/copilot auto STILL PROMPTS for risky operations, while codex
67
+ // auto never prompts at all. The row states only what is true everywhere; the
68
+ // mechanism rides here, per harness.
69
+ const autoSemantics = AUTO_SEMANTICS[agent];
70
+ if (autoSemantics && supported.includes('auto')) {
71
+ notes.push(`${agent} --mode auto: ${autoSemantics}`);
72
+ }
45
73
  if (unsupported.includes('plan')) {
46
74
  notes.push(`--mode plan degrades to ${defaultMode} on ${agent} (no native read-only mode).`);
47
75
  }
@@ -552,9 +552,9 @@ export declare function resolveAgentName(input: string): AgentId | null;
552
552
  /** Check whether the input string matches any known agent name or alias. */
553
553
  export declare function isAgentName(input: string): boolean;
554
554
  /**
555
- * Split a CLI-provided `<agent>[@<version>]` spec into its bare agent id and
556
- * optional exact version token, the same way `agents run` does
557
- * (commands/exec.ts parses its positional with a plain `split('@')`) — NOT the
555
+ * Split a CLI-provided `<agent>[@<version>][#<label>]` spec into its bare agent
556
+ * id, optional exact version token, and optional account label, the same way
557
+ * `agents run` does — NOT the
558
558
  * agent-spec qualifier engine (`@latest`/`@all`), which enumerates installed
559
559
  * versions for read/diagnostic commands, not a single launch target. Returns an
560
560
  * error message rather than throwing so callers decide exit-vs-continue.
@@ -562,6 +562,7 @@ export declare function isAgentName(input: string): boolean;
562
562
  export declare function parseAgentVersionSpec(raw: string): {
563
563
  agent: AgentId;
564
564
  version?: string;
565
+ label?: string;
565
566
  } | {
566
567
  error: string;
567
568
  };