@phnx-labs/agents-cli 1.22.60 → 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 (85) hide show
  1. package/CHANGELOG.md +49 -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/reminders.d.ts +9 -0
  14. package/dist/commands/reminders.js +49 -0
  15. package/dist/commands/run-account-picker.d.ts +14 -0
  16. package/dist/commands/run-account-picker.js +13 -0
  17. package/dist/commands/teams.d.ts +1 -1
  18. package/dist/commands/teams.js +9 -3
  19. package/dist/index.js +9 -0
  20. package/dist/lib/accounting/rotate.d.ts +63 -0
  21. package/dist/lib/accounting/rotate.js +229 -13
  22. package/dist/lib/browser/drivers/local.d.ts +11 -0
  23. package/dist/lib/browser/drivers/local.js +26 -0
  24. package/dist/lib/browser/profiles.js +8 -6
  25. package/dist/lib/browser/service.d.ts +12 -8
  26. package/dist/lib/browser/service.js +38 -10
  27. package/dist/lib/claude-statusline.d.ts +14 -1
  28. package/dist/lib/claude-statusline.js +27 -2
  29. package/dist/lib/daemon/runner.js +17 -2
  30. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  31. package/dist/lib/devices/doctor-findings.js +22 -4
  32. package/dist/lib/doctor-diff.d.ts +21 -5
  33. package/dist/lib/doctor-diff.js +242 -76
  34. package/dist/lib/feed/events.d.ts +1 -1
  35. package/dist/lib/feed/events.js +25 -16
  36. package/dist/lib/github/gh-overload.d.ts +58 -0
  37. package/dist/lib/github/gh-overload.js +246 -0
  38. package/dist/lib/github/rest.d.ts +64 -0
  39. package/dist/lib/github/rest.js +111 -0
  40. package/dist/lib/harness-connection-test.d.ts +57 -0
  41. package/dist/lib/harness-connection-test.js +80 -0
  42. package/dist/lib/heal.js +8 -3
  43. package/dist/lib/installations/shims.d.ts +22 -0
  44. package/dist/lib/installations/shims.js +104 -0
  45. package/dist/lib/linear-project-counts.js +8 -0
  46. package/dist/lib/linear-rate-limit.d.ts +26 -0
  47. package/dist/lib/linear-rate-limit.js +163 -0
  48. package/dist/lib/mcp.d.ts +9 -0
  49. package/dist/lib/mcp.js +37 -1
  50. package/dist/lib/open-url.js +5 -3
  51. package/dist/lib/permissions.d.ts +28 -0
  52. package/dist/lib/permissions.js +156 -1
  53. package/dist/lib/refresh.js +9 -1
  54. package/dist/lib/reminders.d.ts +29 -0
  55. package/dist/lib/reminders.js +88 -0
  56. package/dist/lib/resource-content-diff.d.ts +33 -0
  57. package/dist/lib/resource-content-diff.js +103 -0
  58. package/dist/lib/rules/compile.d.ts +7 -0
  59. package/dist/lib/rules/compile.js +7 -1
  60. package/dist/lib/session/active.d.ts +41 -4
  61. package/dist/lib/session/active.js +58 -7
  62. package/dist/lib/session/host-link.d.ts +22 -0
  63. package/dist/lib/session/host-link.js +40 -4
  64. package/dist/lib/session/trajectory.d.ts +42 -0
  65. package/dist/lib/session/trajectory.js +46 -27
  66. package/dist/lib/ssh-exec.d.ts +30 -0
  67. package/dist/lib/ssh-exec.js +37 -5
  68. package/dist/lib/startup/command-registry.js +1 -1
  69. package/dist/lib/subagents-registry.d.ts +18 -0
  70. package/dist/lib/subagents-registry.js +79 -0
  71. package/dist/lib/teams/agents.d.ts +12 -0
  72. package/dist/lib/teams/agents.js +51 -0
  73. package/dist/lib/traces/schema2-build.d.ts +85 -0
  74. package/dist/lib/traces/schema2-build.js +637 -0
  75. package/dist/lib/traces/schema2-danger.d.ts +36 -0
  76. package/dist/lib/traces/schema2-danger.js +185 -0
  77. package/dist/lib/traces/schema2.d.ts +149 -0
  78. package/dist/lib/traces/schema2.js +20 -0
  79. package/dist/lib/traces/sync.d.ts +93 -0
  80. package/dist/lib/traces/sync.js +75 -22
  81. package/dist/lib/traces/worker-template.js +5 -0
  82. package/dist/lib/uninstall.js +10 -1
  83. package/dist/lib/workflows.d.ts +11 -0
  84. package/dist/lib/workflows.js +67 -8
  85. package/package.json +1 -1
@@ -17,7 +17,9 @@ import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists,
17
17
  import { listPresets, getPreset } from '../lib/profiles-presets.js';
18
18
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
19
19
  import { findAccount } from '../lib/account-registry.js';
20
- import { runWizardSteps, createSteps, editSteps, defaultWizardIO, } from './harness-wizard.js';
20
+ import { runWizardSteps, createSteps, editSteps, defaultWizardIO, runConnectionTest, } from './harness-wizard.js';
21
+ import { harnessHooks } from './harness-hooks.js';
22
+ import { CONNECTION_TEST_PROMPT } from '../lib/harness-connection-test.js';
21
23
  /** Short capability summary for a native harness — its supported run modes. */
22
24
  function nativeModes(id) {
23
25
  const modes = AGENTS[id]?.capabilities?.modes ?? [];
@@ -196,6 +198,73 @@ export function addNeedsWizard(name, opts) {
196
198
  return true;
197
199
  return !getPreset(name);
198
200
  }
201
+ /**
202
+ * Resolve the tri-state connection-test gate (RUSH-2221), pure so the branching
203
+ * is unit-tested with no prompt or spawn. `--test` forces it on, `--no-test`
204
+ * forces it off, and with neither flag a TTY is asked (default yes) while a
205
+ * non-interactive caller (`--key-stdin`, piped, CI) skips it — so scripting stays
206
+ * non-interactive unless it opts in with `--test`.
207
+ */
208
+ export function connectionTestGate(testFlag, interactive) {
209
+ if (testFlag === true)
210
+ return 'on';
211
+ if (testFlag === false)
212
+ return 'off';
213
+ return interactive ? 'ask' : 'off';
214
+ }
215
+ /**
216
+ * Pre-save connection test (RUSH-2221). The harness is already on disk (the test
217
+ * drives the real `agents run <name>` path, so it must be), so this runs a
218
+ * classified smoke test and — on a TTY, when it fails — offers to keep it, edit
219
+ * it, or delete-and-cancel. A test is never blocking on its own: a save is only
220
+ * discarded when the user explicitly chooses to, so a `--test` failure in a
221
+ * non-interactive shell warns and keeps rather than exiting non-zero.
222
+ */
223
+ async function preSaveConnectionTest(name, testFlag) {
224
+ const interactive = isInteractiveTerminal();
225
+ const gate = connectionTestGate(testFlag, interactive);
226
+ let shouldTest;
227
+ if (gate === 'on')
228
+ shouldTest = true;
229
+ else if (gate === 'off')
230
+ shouldTest = false;
231
+ else {
232
+ const { confirm } = await import('@inquirer/prompts');
233
+ shouldTest = await confirm({ message: `Test the connection for '${name}' now?`, default: true });
234
+ }
235
+ if (!shouldTest)
236
+ return;
237
+ console.log(chalk.gray(`Testing '${name}' — sending "${CONNECTION_TEST_PROMPT}" through agents run…`));
238
+ const result = await runConnectionTest({ mode: 'create', name }, harnessHooks());
239
+ if (!result)
240
+ return;
241
+ if (result.ok) {
242
+ console.log(chalk.green(`✓ ${result.message}`));
243
+ return;
244
+ }
245
+ console.log(chalk.yellow(`✗ ${result.message}`));
246
+ if (!interactive) {
247
+ console.log(chalk.gray(`Kept anyway. Fix and retest with: agents harness edit ${name}`));
248
+ return;
249
+ }
250
+ const { select } = await import('@inquirer/prompts');
251
+ const action = await select({
252
+ message: 'The connection test failed. What would you like to do?',
253
+ choices: [
254
+ { name: 'Keep it (the endpoint may just be down right now)', value: 'keep' },
255
+ { name: 'Edit it now', value: 'edit' },
256
+ { name: 'Delete it and cancel', value: 'delete' },
257
+ ],
258
+ });
259
+ if (action === 'edit') {
260
+ await runEditWizard(name, {});
261
+ return;
262
+ }
263
+ if (action === 'delete') {
264
+ deleteProfile(name);
265
+ throw new Error(`Harness '${name}' deleted after a failed connection test.`);
266
+ }
267
+ }
199
268
  /**
200
269
  * Shared build+persist flow for a fork — used by `agents harness fork`'s
201
270
  * flag-driven path AND by the wizard (both for `fork` and, when it falls back
@@ -223,6 +292,7 @@ async function runForkFlow(source, name, opts) {
223
292
  writeProfile(forked);
224
293
  console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
225
294
  console.log(chalk.gray(`Try: agents run ${name} "hello"`));
295
+ await preSaveConnectionTest(name, opts.test);
226
296
  }
227
297
  /**
228
298
  * Interactive `agents harness add`/`fork` wizard — runs when required info is
@@ -234,7 +304,7 @@ async function runForkFlow(source, name, opts) {
234
304
  */
235
305
  async function runCreateWizard() {
236
306
  const io = await defaultWizardIO();
237
- const draft = await runWizardSteps(createSteps(), { mode: 'create' }, io);
307
+ const draft = await runWizardSteps(createSteps(), { mode: 'create' }, io, harnessHooks());
238
308
  return {
239
309
  source: draft.source,
240
310
  name: draft.name,
@@ -292,7 +362,7 @@ async function runEditWizard(name, cliOpts) {
292
362
  }
293
363
  const original = readProfile(name);
294
364
  const io = await defaultWizardIO();
295
- const draft = await runWizardSteps(editSteps(original), { mode: 'edit', original, host: original.host.agent, name }, io);
365
+ const draft = await runWizardSteps(editSteps(original), { mode: 'edit', original, host: original.host.agent, name }, io, harnessHooks());
296
366
  const opts = { ...draftToEditOptions(draft, original), keyStdin: cliOpts.keyStdin };
297
367
  if (!hasEditFlags(opts)) {
298
368
  console.log(chalk.gray(`No changes made to '${name}'.`));
@@ -308,6 +378,9 @@ async function runEditWizard(name, cliOpts) {
308
378
  writeProfile(edited);
309
379
  console.log(chalk.green(`Harness '${name}' updated.`));
310
380
  console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
381
+ // An edit that touched host/model/endpoint/auth is exactly what can break the
382
+ // harness, so re-test after saving (RUSH-2221). cliOpts carries the --test flag.
383
+ await preSaveConnectionTest(name, cliOpts.test);
311
384
  }
312
385
  export function registerHarnessCommands(program) {
313
386
  const cmd = program
@@ -362,6 +435,8 @@ Examples:
362
435
  .option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
363
436
  .option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
364
437
  .option('--force', 'Overwrite an existing harness with the same name')
438
+ .option('--test', 'Run a connection test before saving (default: ask on a terminal)')
439
+ .option('--no-test', 'Skip the pre-save connection test')
365
440
  .action(async (name, opts) => {
366
441
  try {
367
442
  if (opts.authProvider || opts.fromSecrets)
@@ -371,10 +446,11 @@ Examples:
371
446
  throw new Error("'agents harness add' needs --preset or --host + --model (or a name and an interactive terminal for the wizard).");
372
447
  }
373
448
  const wiz = await runCreateWizard();
374
- await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
449
+ await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin, test: opts.test });
375
450
  return;
376
451
  }
377
452
  await addProfile(name, opts, 'Harness');
453
+ await preSaveConnectionTest(name, opts.test);
378
454
  }
379
455
  catch (err) {
380
456
  console.error(chalk.red(err.message));
@@ -393,6 +469,8 @@ Examples:
393
469
  .option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
394
470
  .option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
395
471
  .option('--force', 'Overwrite an existing harness with the same name')
472
+ .option('--test', 'Run a connection test before saving (default: ask on a terminal)')
473
+ .option('--no-test', 'Skip the pre-save connection test')
396
474
  .addHelpText('after', `
397
475
  Examples:
398
476
  # Fork OpenCode into a harness pinned to a DeepSeek model on OpenRouter
@@ -417,7 +495,7 @@ Examples:
417
495
  throw new Error("'agents harness fork' needs <source> and <name> (or an interactive terminal for the wizard).");
418
496
  }
419
497
  const wiz = await runCreateWizard();
420
- await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin });
498
+ await runForkFlow(wiz.source, wiz.name, { ...wiz.opts, force: opts.force, keyStdin: opts.keyStdin, test: opts.test });
421
499
  return;
422
500
  }
423
501
  await runForkFlow(source, name, opts);
@@ -439,6 +517,8 @@ Examples:
439
517
  .option('--fallback-model <id>', 'Secondary model retried on the same host on a rate limit (pass an empty string to clear it)')
440
518
  .option('--from-secrets <bundle>[:<key>]', 'Removed: import the value with agents accounts add, then use --account')
441
519
  .option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
520
+ .option('--test', 'Run a connection test after saving (default: ask on a terminal)')
521
+ .option('--no-test', 'Skip the connection test')
442
522
  .addHelpText('after', `
443
523
  Examples:
444
524
  # Swap the pinned model
@@ -479,6 +559,7 @@ Examples:
479
559
  writeProfile(edited);
480
560
  console.log(chalk.green(`Harness '${name}' updated.`));
481
561
  console.log(chalk.gray(`Model: ${profileModelLabel(edited)}`));
562
+ await preSaveConnectionTest(name, opts.test);
482
563
  }
483
564
  catch (err) {
484
565
  console.error(chalk.red(err.message));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `agents reminders` — list your personal operating reminders.
3
+ *
4
+ * The reminders live in `~/.agents/reminders/reminders.yaml` and are surfaced
5
+ * succinctly in the Claude statusline — one per session, chosen from the session
6
+ * id. This command shows the full set (and the file to edit).
7
+ */
8
+ import type { Command } from 'commander';
9
+ export declare function registerRemindersCommand(program: Command): void;
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { loadReminders, remindersFilePath } from '../lib/reminders.js';
3
+ import { setHelpSections } from '../lib/help.js';
4
+ const EXAMPLES = [
5
+ 'agents reminders # list your operating reminders',
6
+ 'agents reminders --json # machine-readable output',
7
+ ].join('\n');
8
+ const NOTES = [
9
+ "Reminders live in ~/.agents/reminders/reminders.yaml and sync across the fleet via 'agents repo push'.",
10
+ 'One shows succinctly in the Claude statusline per session, chosen deterministically from the session id,',
11
+ 'so concurrent agents each show a different one and it stays stable within a session.',
12
+ ].join('\n');
13
+ export function registerRemindersCommand(program) {
14
+ const cmd = program
15
+ .command('reminders')
16
+ .description('Personal operating reminders shown in the Claude statusline')
17
+ .option('--json', 'output as JSON')
18
+ .action((opts) => {
19
+ const file = remindersFilePath();
20
+ let reminders;
21
+ try {
22
+ reminders = loadReminders();
23
+ }
24
+ catch (err) {
25
+ console.error(chalk.red(`Could not read reminders: ${err instanceof Error ? err.message : String(err)}`));
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ if (opts.json) {
30
+ process.stdout.write(`${JSON.stringify({ file, reminders }, null, 2)}\n`);
31
+ return;
32
+ }
33
+ if (reminders.length === 0) {
34
+ console.log(`No reminders yet. Add them to ${chalk.cyan(file)}`);
35
+ console.log(chalk.dim('Each shows succinctly in the Claude statusline — one per session.'));
36
+ return;
37
+ }
38
+ const count = `${reminders.length} reminder${reminders.length === 1 ? '' : 's'}`;
39
+ console.log(chalk.dim(`${count} · ${file}`));
40
+ console.log(chalk.dim('One shows in the Claude statusline per session, chosen from the session id.\n'));
41
+ for (const reminder of reminders) {
42
+ console.log(` ${chalk.cyan('◆')} ${chalk.bold(reminder.short)}`);
43
+ if (reminder.full && reminder.full !== reminder.short) {
44
+ console.log(` ${chalk.dim(reminder.full)}`);
45
+ }
46
+ }
47
+ });
48
+ setHelpSections(cmd, { examples: EXAMPLES, notes: NOTES });
49
+ }
@@ -52,6 +52,20 @@ export declare function signInLaunchDecision(input: {
52
52
  tty: boolean;
53
53
  json: boolean;
54
54
  }): 'launch' | 'fail-loud';
55
+ /**
56
+ * How a `balanced`/`available` run reacts when every account's usage is stale and
57
+ * none is verified (PHNX-2526). A human present at a real terminal gets the
58
+ * account `picker` — they can choose knowing the numbers are stale — while every
59
+ * unattended shape (`--headless`, `--json`, or no TTY) `fail-loud`s with
60
+ * NO_VERIFIED_USAGE rather than silently guess on a stale snapshot. `headless`
61
+ * joins the gate because a routine/machine dispatch can carry a TTY yet have no
62
+ * human to answer a picker; the split mirrors `signInLaunchDecision`.
63
+ */
64
+ export declare function noVerifiedUsageDecision(input: {
65
+ tty: boolean;
66
+ json: boolean;
67
+ headless: boolean;
68
+ }): 'picker' | 'fail-loud';
55
69
  /**
56
70
  * Choose which installed version to launch so the user can authenticate, when a
57
71
  * strategy found zero healthy accounts but at least one is merely signed out
@@ -221,6 +221,19 @@ export function signInLaunchDecision(input) {
221
221
  const humanPresent = input.tty && !input.json;
222
222
  return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
223
223
  }
224
+ /**
225
+ * How a `balanced`/`available` run reacts when every account's usage is stale and
226
+ * none is verified (PHNX-2526). A human present at a real terminal gets the
227
+ * account `picker` — they can choose knowing the numbers are stale — while every
228
+ * unattended shape (`--headless`, `--json`, or no TTY) `fail-loud`s with
229
+ * NO_VERIFIED_USAGE rather than silently guess on a stale snapshot. `headless`
230
+ * joins the gate because a routine/machine dispatch can carry a TTY yet have no
231
+ * human to answer a picker; the split mirrors `signInLaunchDecision`.
232
+ */
233
+ export function noVerifiedUsageDecision(input) {
234
+ const humanPresent = input.tty && !input.json && !input.headless;
235
+ return humanPresent ? 'picker' : 'fail-loud';
236
+ }
224
237
  /**
225
238
  * Choose which installed version to launch so the user can authenticate, when a
226
239
  * strategy found zero healthy accounts but at least one is merely signed out
@@ -108,7 +108,7 @@ export declare function staleRepoError(params: {
108
108
  * the teammate itself so we don't need the original --cloud CLI args.
109
109
  */
110
110
  export declare function wireCloudDispatcher(mgr: AgentManager): void;
111
- export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt' | 'agentType' | 'cloudRepo' | 'cloudBranch' | 'model'>): DispatchOptions;
111
+ export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt' | 'agentType' | 'cloudRepo' | 'cloudBranch' | 'model' | 'mode'>): DispatchOptions;
112
112
  /**
113
113
  * Single-wave start used by `teams start` without --watch. startReady()
114
114
  * terminalizes a placement/spawn/cloud/dependency failure to FAILED with
@@ -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';
@@ -373,7 +373,10 @@ export function wireCloudDispatcher(mgr) {
373
373
  }
374
374
  export function cloudDispatchOptions(agent) {
375
375
  return {
376
- 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),
377
380
  agent: agent.agentType,
378
381
  repo: agent.cloudRepo ?? undefined,
379
382
  branch: agent.cloudBranch ?? undefined,
@@ -1933,7 +1936,10 @@ export function registerTeamsCommands(program) {
1933
1936
  mgr.setCloudDispatcher(async (a) => {
1934
1937
  const prov = resolveProvider(providerId);
1935
1938
  const dispatchOpts = {
1936
- 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),
1937
1943
  agent: a.agentType,
1938
1944
  repo: opts.repo,
1939
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