@phnx-labs/agents-cli 1.22.59 → 1.22.61

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 (100) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/command-registry.d.ts +1 -0
  3. package/dist/cli/command-registry.js +2 -0
  4. package/dist/commands/browser.js +9 -4
  5. package/dist/commands/doctor.js +1 -1
  6. package/dist/commands/exec.js +35 -1
  7. package/dist/commands/harness-hooks.d.ts +55 -0
  8. package/dist/commands/harness-hooks.js +104 -0
  9. package/dist/commands/harness-wizard.d.ts +33 -14
  10. package/dist/commands/harness-wizard.js +53 -23
  11. package/dist/commands/harness.d.ts +14 -0
  12. package/dist/commands/harness.js +86 -5
  13. package/dist/commands/perf.js +10 -0
  14. package/dist/commands/reminders.d.ts +9 -0
  15. package/dist/commands/reminders.js +49 -0
  16. package/dist/commands/run-account-picker.d.ts +14 -0
  17. package/dist/commands/run-account-picker.js +13 -0
  18. package/dist/commands/sessions-picker.d.ts +13 -0
  19. package/dist/commands/sessions-picker.js +17 -8
  20. package/dist/commands/sessions.js +13 -11
  21. package/dist/commands/teams-picker.js +20 -6
  22. package/dist/commands/teams.d.ts +3 -3
  23. package/dist/commands/teams.js +86 -24
  24. package/dist/index.js +9 -0
  25. package/dist/lib/accounting/rotate.d.ts +63 -0
  26. package/dist/lib/accounting/rotate.js +240 -16
  27. package/dist/lib/accounting/usage-sync.d.ts +12 -2
  28. package/dist/lib/accounting/usage-sync.js +34 -6
  29. package/dist/lib/browser/drivers/local.d.ts +11 -0
  30. package/dist/lib/browser/drivers/local.js +26 -0
  31. package/dist/lib/browser/profiles.js +8 -6
  32. package/dist/lib/browser/service.d.ts +12 -8
  33. package/dist/lib/browser/service.js +38 -10
  34. package/dist/lib/claude-statusline.d.ts +14 -1
  35. package/dist/lib/claude-statusline.js +27 -2
  36. package/dist/lib/daemon/runner.js +17 -2
  37. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  38. package/dist/lib/devices/doctor-findings.js +22 -4
  39. package/dist/lib/doctor-diff.d.ts +21 -5
  40. package/dist/lib/doctor-diff.js +242 -76
  41. package/dist/lib/feed/events.d.ts +1 -1
  42. package/dist/lib/feed/events.js +28 -15
  43. package/dist/lib/github/gh-overload.d.ts +58 -0
  44. package/dist/lib/github/gh-overload.js +246 -0
  45. package/dist/lib/github/rest.d.ts +64 -0
  46. package/dist/lib/github/rest.js +111 -0
  47. package/dist/lib/harness-connection-test.d.ts +57 -0
  48. package/dist/lib/harness-connection-test.js +80 -0
  49. package/dist/lib/heal.js +8 -3
  50. package/dist/lib/installations/shims.d.ts +22 -0
  51. package/dist/lib/installations/shims.js +104 -0
  52. package/dist/lib/linear-project-counts.js +8 -0
  53. package/dist/lib/linear-rate-limit.d.ts +26 -0
  54. package/dist/lib/linear-rate-limit.js +163 -0
  55. package/dist/lib/mcp.d.ts +9 -0
  56. package/dist/lib/mcp.js +37 -1
  57. package/dist/lib/open-url.js +5 -3
  58. package/dist/lib/perf/db.d.ts +1 -1
  59. package/dist/lib/perf/db.js +53 -2
  60. package/dist/lib/perf/types.d.ts +14 -0
  61. package/dist/lib/permissions.d.ts +28 -0
  62. package/dist/lib/permissions.js +156 -1
  63. package/dist/lib/refresh.js +9 -1
  64. package/dist/lib/reminders.d.ts +29 -0
  65. package/dist/lib/reminders.js +88 -0
  66. package/dist/lib/resource-content-diff.d.ts +33 -0
  67. package/dist/lib/resource-content-diff.js +103 -0
  68. package/dist/lib/rules/compile.d.ts +7 -0
  69. package/dist/lib/rules/compile.js +7 -1
  70. package/dist/lib/session/active.d.ts +41 -4
  71. package/dist/lib/session/active.js +58 -7
  72. package/dist/lib/session/host-link.d.ts +22 -0
  73. package/dist/lib/session/host-link.js +40 -4
  74. package/dist/lib/session/live-metadata.js +3 -3
  75. package/dist/lib/session/trajectory.d.ts +42 -0
  76. package/dist/lib/session/trajectory.js +46 -27
  77. package/dist/lib/ssh-exec.d.ts +30 -0
  78. package/dist/lib/ssh-exec.js +37 -5
  79. package/dist/lib/startup/command-registry.js +1 -1
  80. package/dist/lib/subagents-registry.d.ts +18 -0
  81. package/dist/lib/subagents-registry.js +79 -0
  82. package/dist/lib/teams/agents.d.ts +12 -0
  83. package/dist/lib/teams/agents.js +51 -0
  84. package/dist/lib/teams/api.d.ts +8 -0
  85. package/dist/lib/teams/api.js +50 -6
  86. package/dist/lib/teams/delivery.d.ts +14 -4
  87. package/dist/lib/teams/delivery.js +15 -5
  88. package/dist/lib/traces/schema2-build.d.ts +85 -0
  89. package/dist/lib/traces/schema2-build.js +637 -0
  90. package/dist/lib/traces/schema2-danger.d.ts +36 -0
  91. package/dist/lib/traces/schema2-danger.js +185 -0
  92. package/dist/lib/traces/schema2.d.ts +149 -0
  93. package/dist/lib/traces/schema2.js +20 -0
  94. package/dist/lib/traces/sync.d.ts +93 -0
  95. package/dist/lib/traces/sync.js +75 -22
  96. package/dist/lib/traces/worker-template.js +5 -0
  97. package/dist/lib/uninstall.js +10 -1
  98. package/dist/lib/workflows.d.ts +11 -0
  99. package/dist/lib/workflows.js +67 -8
  100. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { dieFriction, relTime, truncate, isJsonMode, padRight } from '../lib/for
3
3
  import * as fs from 'fs/promises';
4
4
  import { addHostOption } from '../lib/hosts/option.js';
5
5
  import * as path from 'path';
6
- import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
6
+ import { AgentManager, AgentStatus, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, withTeammatePrPolicy, } from '../lib/teams/agents.js';
7
7
  import { mailboxDir, enqueue } from '../lib/mailbox.js';
8
8
  import { resolveProvider } from '../lib/cloud/registry.js';
9
9
  import { emit } from '../lib/feed/events.js';
@@ -62,6 +62,7 @@ function statusColor(status) {
62
62
  case 'running': return chalk.yellow;
63
63
  case 'completed': return chalk.green;
64
64
  case 'pr_open': return chalk.magenta; // RUSH-2380: process done, PR not merged
65
+ case 'stranded': return chalk.yellow; // PHNX-2951: done but work not committed
65
66
  case 'failed': return chalk.red;
66
67
  case 'stopped': return chalk.gray;
67
68
  default: return chalk.white;
@@ -150,6 +151,7 @@ function snapshotToStatusDetail(agent) {
150
151
  after: agent.after,
151
152
  task_type: agent.task_type,
152
153
  host: agent.host,
154
+ workspace_dir: agent.workspace_dir,
153
155
  };
154
156
  }
155
157
  function fullName(type, version) {
@@ -371,7 +373,10 @@ export function wireCloudDispatcher(mgr) {
371
373
  }
372
374
  export function cloudDispatchOptions(agent) {
373
375
  return {
374
- prompt: agent.prompt,
376
+ // PHNX-3236: a cloud teammate runs in the provider sandbox and never inherits
377
+ // the local merge-guard.sh hook, so the prompt policy is its ONLY self-merge
378
+ // boundary — apply the same helper buildRunArgv uses for local/remote.
379
+ prompt: withTeammatePrPolicy(agent.prompt, agent.mode),
375
380
  agent: agent.agentType,
376
381
  repo: agent.cloudRepo ?? undefined,
377
382
  branch: agent.cloudBranch ?? undefined,
@@ -784,6 +789,9 @@ function printAgentDetail(a, session) {
784
789
  }
785
790
  if (a.has_errors)
786
791
  console.log(` ${chalk.red('! reported an error')}`);
792
+ if (delivery === 'stranded' && a.workspace_dir) {
793
+ console.log(` ${chalk.yellow('! stranded')} uncommitted work at ${a.workspace_dir}`);
794
+ }
787
795
  if (a.pr_url)
788
796
  console.log(` ${chalk.gray('PR ')}${a.pr_url}`);
789
797
  }
@@ -864,17 +872,34 @@ function printAgentSummary(s) {
864
872
  console.log(` ${chalk.gray('>')} ${truncate(firstLine, 96)}`);
865
873
  }
866
874
  }
875
+ if (delivery === 'stranded' && s.workspace_dir) {
876
+ console.log(` ${chalk.yellow('! stranded')} uncommitted work at ${s.workspace_dir}`);
877
+ }
867
878
  if (s.pr_url)
868
879
  console.log(` ${chalk.gray('PR ')} ${chalk.cyan(s.pr_url)}`);
869
880
  }
881
+ function formatTeamStatusSummary(summary) {
882
+ const done = Math.max(0, summary.completed - summary.stranded);
883
+ const parts = [];
884
+ if (summary.pending > 0)
885
+ parts.push(`${summary.pending} pending`);
886
+ if (summary.running > 0 || parts.length === 0)
887
+ parts.push(`${summary.running} working`);
888
+ if (done > 0 || summary.stranded === 0)
889
+ parts.push(`${done} done`);
890
+ if (summary.stranded > 0)
891
+ parts.push(`${summary.stranded} stranded`);
892
+ if (summary.failed > 0 || parts.length === 0)
893
+ parts.push(`${summary.failed} failed`);
894
+ if (summary.stopped > 0 || parts.length === 0)
895
+ parts.push(`${summary.stopped} stopped`);
896
+ return `(${parts.join(', ')})`;
897
+ }
870
898
  // Render a team's status in the same format the `status` subcommand uses, so
871
899
  // the interactive picker's Enter action drops the user into a familiar view.
872
900
  async function printTeamStatus(team, result) {
873
901
  const { summary, agents } = result;
874
- console.log(chalk.bold(`Team ${chalk.cyan(team)} `) +
875
- chalk.gray(summary.pending > 0
876
- ? `(${summary.pending} pending, ${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`
877
- : `(${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`));
902
+ console.log(chalk.bold(`Team ${chalk.cyan(team)} `) + chalk.gray(formatTeamStatusSummary(summary)));
878
903
  if (agents.length === 0) {
879
904
  console.log(chalk.gray(' (no teammates yet — add one with `agents teams add`)'));
880
905
  }
@@ -900,10 +925,7 @@ async function printTeamStatus(team, result) {
900
925
  // verbose/legacy layout.
901
926
  function printTeamSummary(team, result) {
902
927
  const { summary, agents } = result;
903
- console.log(chalk.bold(`Team ${chalk.cyan(team)} `) +
904
- chalk.gray(summary.pending > 0
905
- ? `(${summary.pending} pending, ${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`
906
- : `(${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`));
928
+ console.log(chalk.bold(`Team ${chalk.cyan(team)} `) + chalk.gray(formatTeamStatusSummary(summary)));
907
929
  if (agents.length === 0) {
908
930
  console.log(chalk.gray(' (no teammates yet — add one with `agents teams add`)'));
909
931
  }
@@ -925,12 +947,13 @@ function printTeamSummary(team, result) {
925
947
  console.log(chalk.gray('Raw log: agents teams logs --team ' + team + ' --teammate <name>'));
926
948
  }
927
949
  // Classify a team into a single bucket for --status filtering.
928
- // - empty: no teammates (created but nobody added yet)
929
- // - waiting: only staged teammates — call `teams start` to kick them off
930
- // - working: at least one teammate still running
931
- // - failed: at least one teammate failed or was stopped (any failure wins —
932
- // even if others finished, you want to know about the failure)
933
- // - done: everyone finished successfully, no failures
950
+ // - empty: no teammates (created but nobody added yet)
951
+ // - waiting: only staged teammates — call `teams start` to kick them off
952
+ // - working: at least one teammate still running
953
+ // - failed: at least one teammate failed or was stopped (any failure wins —
954
+ // even if others finished, you want to know about the failure)
955
+ // - stranded: everyone finished, but at least one has uncommitted work and no PR
956
+ // - done: everyone finished successfully, no failures, no stranded work
934
957
  function classifyTeamStatus(t) {
935
958
  if (t.agent_count === 0)
936
959
  return 'empty';
@@ -943,6 +966,10 @@ function classifyTeamStatus(t) {
943
966
  const accounted = t.running + t.completed + t.failed + t.stopped;
944
967
  if (accounted < t.agent_count)
945
968
  return 'waiting';
969
+ // Completed-without-delivery is not "done": the work is still in the worktree
970
+ // and will be lost on cleanup (PHNX-2951).
971
+ if ((t.stranded ?? 0) > 0)
972
+ return 'stranded';
946
973
  return 'done';
947
974
  }
948
975
  // Merge persistent team registry with tasks derived from live agents so empty
@@ -959,6 +986,7 @@ function mergeTeams(registry, tasks) {
959
986
  pending: 0,
960
987
  running: 0,
961
988
  completed: 0,
989
+ stranded: 0,
962
990
  failed: 0,
963
991
  stopped: 0,
964
992
  workspace_dir: null,
@@ -969,7 +997,7 @@ function mergeTeams(registry, tasks) {
969
997
  }
970
998
  return Array.from(byName.values()).sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime());
971
999
  }
972
- function buildTasksFromSnapshots(agents) {
1000
+ async function buildTasksFromSnapshots(agents) {
973
1001
  const byTeam = new Map();
974
1002
  for (const agent of agents) {
975
1003
  const teamAgents = byTeam.get(agent.task_name) || [];
@@ -981,6 +1009,7 @@ function buildTasksFromSnapshots(agents) {
981
1009
  let pending = 0;
982
1010
  let running = 0;
983
1011
  let completed = 0;
1012
+ let stranded = 0;
984
1013
  let failed = 0;
985
1014
  let stopped = 0;
986
1015
  let earliestStart = null;
@@ -998,6 +1027,16 @@ function buildTasksFromSnapshots(agents) {
998
1027
  failed++;
999
1028
  else if (status === AgentStatus.STOPPED)
1000
1029
  stopped++;
1030
+ // Stranded = completed with no PR and a dirty local worktree (PHNX-2951).
1031
+ if (status === AgentStatus.COMPLETED &&
1032
+ !agent.pr_url?.trim() &&
1033
+ !agent.host &&
1034
+ agent.workspace_dir) {
1035
+ const dirty = await hasUncommittedChanges(agent.workspace_dir);
1036
+ if (dirty) {
1037
+ stranded++;
1038
+ }
1039
+ }
1001
1040
  const startedAt = parseTimestamp(agent.started_at);
1002
1041
  if (startedAt && (!earliestStart || startedAt < earliestStart)) {
1003
1042
  earliestStart = startedAt;
@@ -1017,6 +1056,7 @@ function buildTasksFromSnapshots(agents) {
1017
1056
  pending,
1018
1057
  running,
1019
1058
  completed,
1059
+ stranded,
1020
1060
  failed,
1021
1061
  stopped,
1022
1062
  workspace_dir: workspaceDir,
@@ -1026,7 +1066,7 @@ function buildTasksFromSnapshots(agents) {
1026
1066
  }
1027
1067
  return tasks.sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime());
1028
1068
  }
1029
- export function buildTeamRowsFromSnapshots(registry, agents,
1069
+ export async function buildTeamRowsFromSnapshots(registry, agents,
1030
1070
  /** team name -> the session that spawned it (see teamSpawners). */
1031
1071
  spawners) {
1032
1072
  const byTeam = new Map();
@@ -1035,7 +1075,26 @@ spawners) {
1035
1075
  details.push(snapshotToStatusDetail(agent));
1036
1076
  byTeam.set(agent.task_name, details);
1037
1077
  }
1038
- const teams = mergeTeams(registry, buildTasksFromSnapshots(agents));
1078
+ const teams = mergeTeams(registry, await buildTasksFromSnapshots(agents));
1079
+ // Recompute delivery for cached snapshots so `teams list` reflects stranded
1080
+ // work discovered by probing the real worktree (PHNX-2951).
1081
+ for (const details of byTeam.values()) {
1082
+ for (const a of details) {
1083
+ const snapshot = agents.find((s) => s.agent_id === a.agent_id);
1084
+ if (snapshot &&
1085
+ normalizeTeamListStatus(a.status) === AgentStatus.COMPLETED &&
1086
+ !snapshot.pr_url?.trim() &&
1087
+ !snapshot.host &&
1088
+ snapshot.workspace_dir) {
1089
+ const dirty = await hasUncommittedChanges(snapshot.workspace_dir);
1090
+ a.delivery = resolveTeammateDelivery({
1091
+ status: a.status,
1092
+ prUrl: snapshot.pr_url,
1093
+ hasUncommittedChanges: dirty,
1094
+ });
1095
+ }
1096
+ }
1097
+ }
1039
1098
  return {
1040
1099
  teams,
1041
1100
  rows: teams.map((team) => ({
@@ -1107,7 +1166,7 @@ async function loadTeamRows(_mgr) {
1107
1166
  catch {
1108
1167
  // The index is an enrichment here — a missing/locked DB just drops the column.
1109
1168
  }
1110
- return buildTeamRowsFromSnapshots(registry, agents, spawners);
1169
+ return await buildTeamRowsFromSnapshots(registry, agents, spawners);
1111
1170
  }
1112
1171
  // Picker fallback for `teams logs` when the teammate ref is omitted. Shows a
1113
1172
  // flat list of every teammate with their team context; Enter picks one.
@@ -1271,7 +1330,7 @@ export function registerTeamsCommands(program) {
1271
1330
  .alias('ls')
1272
1331
  .description('List your teams, most recent activity first')
1273
1332
  .option('-a, --agent <agent>', 'Filter: only teams with this agent (e.g. claude or claude@2.1.112)')
1274
- .option('--status <status>', 'Filter: only teams with this status (working, done, failed, or empty)')
1333
+ .option('--status <status>', 'Filter: only teams with this status (working, done, stranded, failed, or empty)')
1275
1334
  .option('--since <time>', 'Filter: teams active after this time (e.g. "2h", "7d", or ISO date)')
1276
1335
  .option('--until <time>', 'Filter: teams active before this time (e.g. "30d", or ISO date)')
1277
1336
  .option('-n, --limit <n>', 'Show at most this many teams (default: 20)', '20')
@@ -1298,7 +1357,7 @@ export function registerTeamsCommands(program) {
1298
1357
  catch {
1299
1358
  // The session index is an enrichment here; a missing one drops the column.
1300
1359
  }
1301
- let rows = buildTeamRowsFromSnapshots(registry, everyAgent, spawners).rows;
1360
+ let rows = (await buildTeamRowsFromSnapshots(registry, everyAgent, spawners)).rows;
1302
1361
  // --- query: substring match on team name ---
1303
1362
  if (query) {
1304
1363
  const q = query.toLowerCase();
@@ -1318,7 +1377,7 @@ export function registerTeamsCommands(program) {
1318
1377
  // --- --status: classify each team, filter ---
1319
1378
  if (opts.status) {
1320
1379
  const want = opts.status.toLowerCase();
1321
- const validStatuses = ['working', 'done', 'failed', 'empty'];
1380
+ const validStatuses = ['working', 'done', 'stranded', 'failed', 'empty'];
1322
1381
  if (!validStatuses.includes(want)) {
1323
1382
  dieFriction('teams', 'invalid-status-filter', `Invalid --status '${opts.status}'. Use one of: ${validStatuses.join(', ')}`);
1324
1383
  }
@@ -1877,7 +1936,10 @@ export function registerTeamsCommands(program) {
1877
1936
  mgr.setCloudDispatcher(async (a) => {
1878
1937
  const prov = resolveProvider(providerId);
1879
1938
  const dispatchOpts = {
1880
- prompt: a.prompt,
1939
+ // PHNX-3236: same self-merge boundary as the local/remote path — a
1940
+ // cloud teammate has no inherited merge-guard.sh, so the prompt is
1941
+ // its only layer.
1942
+ prompt: withTeammatePrPolicy(a.prompt, a.mode),
1881
1943
  agent: a.agentType,
1882
1944
  repo: opts.repo,
1883
1945
  branch: opts.branch,
package/dist/index.js CHANGED
@@ -72,6 +72,15 @@ if (process.argv[2] === '__shim') {
72
72
  const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
73
73
  process.exit(code);
74
74
  }
75
+ // gh overload delegate: the `gh` PATH shim routes `gh pr checks` here as
76
+ // `agents __gh --real-gh <path> -- pr checks …`, so the rate-limit-prone read
77
+ // runs over REST instead of GraphQL (PHNX-3501). Above bootstrap for the same
78
+ // reason as __shim: no update check, no command-tree load — this is on the hot
79
+ // path of an agent's CI watch, and the gh argv must pass through untouched.
80
+ if (process.argv[2] === '__gh') {
81
+ const { runGhOverload } = await import('./lib/github/gh-overload.js');
82
+ process.exit(await runGhOverload(process.argv.slice(3)));
83
+ }
75
84
  if (process.argv[2] === '__claude-statusline') {
76
85
  const { runClaudeStatusLine } = await import('./lib/claude-statusline.js');
77
86
  process.exit(await runClaudeStatusLine());
@@ -8,6 +8,7 @@ import type { AgentId, RunStrategy } from '../types.js';
8
8
  import type { FallbackEntry } from '../exec.js';
9
9
  import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
10
10
  import { type AccountInfo, type CredentialPresence } from '../agents.js';
11
+ import { type EventPayload } from '../feed/events.js';
11
12
  import { type UsageSnapshot } from './usage.js';
12
13
  import { type AuthVerdict } from '../auth-health.js';
13
14
  export interface RotateCandidate {
@@ -75,6 +76,21 @@ export interface RotateResult {
75
76
  * was silently reported as verified.)
76
77
  */
77
78
  usageUnverified?: boolean;
79
+ /**
80
+ * True when NO candidate carries a fresh usage snapshot AND at least one
81
+ * carries a STALE-but-present one — the "entirely stale usage" case
82
+ * (PHNX-2526). The INITIAL route MUST NOT be decided on a stale number that
83
+ * looks plausible but is wrong (the yosemite-s1 incident: 26h–2.7d-old
84
+ * snapshots read 48% while the account was at its weekly cap). `picked` is
85
+ * still populated (a stale candidate) so `healthy` stays intact for BOUNDED
86
+ * post-rejection failover, but a caller doing the initial selection MUST NOT
87
+ * launch it — it diverts to the account picker (interactive) or fails loud
88
+ * with NO_VERIFIED_USAGE (unattended). Distinct from a BLIND pool with no
89
+ * snapshot at all (a worker box whose usage endpoint 403s, RUSH-2392): that
90
+ * carries no misleading number, so it still draws a pick and this stays
91
+ * false.
92
+ */
93
+ noVerifiedUsage?: boolean;
78
94
  }
79
95
  export declare const RUN_STRATEGIES: RunStrategy[];
80
96
  /**
@@ -139,6 +155,20 @@ export declare const USAGE_DECISION_MAX_AGE_MS: number;
139
155
  * narrowing rule below exists to prevent.
140
156
  */
141
157
  export declare function isUsageVerified(candidate: RotateCandidate, nowMs?: number): boolean;
158
+ /**
159
+ * Whether this candidate carries a STALE-but-present usage number: a snapshot
160
+ * with windows whose capture time is older than {@link USAGE_DECISION_MAX_AGE_MS}.
161
+ *
162
+ * This is the misleading case the initial route must refuse — the number reads
163
+ * "48% used" with the same confidence whether captured a minute or three days
164
+ * ago, and a box whose refresh is failing stays wrong indefinitely. It is
165
+ * deliberately NARROWER than "not verified": a BLIND candidate with no snapshot
166
+ * (or a plan-only meterless one with no windows) carries no number to be misled
167
+ * by — a worker box whose usage endpoint 403s (RUSH-2392), or a meterless Grok
168
+ * login — so it is not "stale", and an entirely-blind pool still draws a pick
169
+ * (PHNX-3392) rather than fail loud with NO_VERIFIED_USAGE.
170
+ */
171
+ export declare function hasStaleUsage(candidate: RotateCandidate, nowMs?: number): boolean;
142
172
  /**
143
173
  * Whether a specific account can serve a run right now, and — when it can't —
144
174
  * why. `signed_out` covers a missing usable credential; `revoked` is a token the
@@ -291,6 +321,16 @@ export declare function earliestResetAcross(candidates: RotateCandidate[], nowMs
291
321
  * and `resets <time>` (parsed for the rotate cooldown). Do not deviate.
292
322
  */
293
323
  export declare function formatNoHealthyAccountError(agent: AgentId, strategy: RunStrategy, excluded: RotateCandidate[], nowMs?: number): string;
324
+ /**
325
+ * The all-stale-usage error (PHNX-2526) an UNATTENDED `balanced`/`available`
326
+ * run fails loud with when no account's usage is fresh enough to route on. EXACT
327
+ * contract — it MUST contain the literal `NO_VERIFIED_USAGE` so a machine caller
328
+ * (and the Factory watchdog) can tail-detect it distinctly from the
329
+ * `no healthy` throttle error, which is a different condition (throttled vs
330
+ * merely stale). Names each candidate with how stale its snapshot is, so the
331
+ * operator can see the failing-refresh box rather than guess.
332
+ */
333
+ export declare function formatNoVerifiedUsageError(agent: AgentId, strategy: RunStrategy, candidates: RotateCandidate[], nowMs?: number): string;
294
334
  /**
295
335
  * The zero-healthy-harness error for `agents run auto` — names each harness's
296
336
  * exclusion reason plus the earliest reset across all snapshots.
@@ -336,6 +376,16 @@ export declare function resolveAccountVersion(agent: AgentId, account: string):
336
376
  export declare function selectBalancedVersion(agent: AgentId): Promise<RotateResult | null>;
337
377
  /** Select the configured version if available, otherwise another available version. */
338
378
  export declare function selectAvailableVersion(agent: AgentId, preferredVersion?: string | null): Promise<RotateResult | null>;
379
+ /**
380
+ * Build the enriched `rotation.resolved`/`rotation.unresolved` event payload:
381
+ * the full candidate pool as the router saw it, the pick and WHY, and a
382
+ * freshness tally. Replaces the old `{ version, healthy: <n>, excluded: <n> }`
383
+ * shape, which recorded only counts and a device-local version and so could not
384
+ * tell a blind-pool draw from a skewed-verified pick from a refused-stale route
385
+ * — the exact ambiguity that keeps a bad pick undebuggable from the log. All
386
+ * candidates share ONE `nowMs` so their `tier`/`ageMs` are mutually consistent.
387
+ */
388
+ export declare function buildRotationDecisionEvent(rotation: RotateResult, agent: AgentId, strategy: RunStrategy): EventPayload;
339
389
  /**
340
390
  * Resolve the version `agents run` should use when the caller did not pin
341
391
  * one with `@version`. The caller supplies the effective strategy.
@@ -362,6 +412,19 @@ export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy,
362
412
  * path — there is no account to be "unhealthy").
363
413
  */
364
414
  exhausted?: RotateCandidate[];
415
+ /**
416
+ * Set (with `version: null`) for a `balanced`/`available` route when EVERY
417
+ * eligible account's usage is stale and none is verified (PHNX-2526). The
418
+ * initial selection MUST NOT auto-launch on a stale number: an interactive
419
+ * caller diverts to the account picker, an unattended one fails loud with
420
+ * NO_VERIFIED_USAGE (`formatNoVerifiedUsageError`). `rotation` is still
421
+ * returned — its `healthy` set (the stale candidates) is preserved ONLY for
422
+ * bounded post-rejection failover, never the initial pick. Undefined when a
423
+ * verified account exists, when the pool is entirely blind (no snapshots —
424
+ * the worker-box case still draws a pick), for `pinned`, and for the
425
+ * zero-healthy `exhausted` case.
426
+ */
427
+ noVerifiedUsage?: boolean;
365
428
  }>;
366
429
  /**
367
430
  * Cap on the number of healthy accounts a single run will re-dispatch through