@phnx-labs/agents-cli 1.22.115 → 1.22.116

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 (88) hide show
  1. package/CHANGELOG.md +149 -14
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +11 -0
  4. package/dist/commands/exec.js +212 -141
  5. package/dist/commands/feed.js +65 -14
  6. package/dist/commands/sessions-picker.d.ts +11 -0
  7. package/dist/commands/sessions-picker.js +88 -7
  8. package/dist/commands/sessions.d.ts +21 -2
  9. package/dist/commands/sessions.js +157 -3
  10. package/dist/commands/setup-secrets.d.ts +2 -2
  11. package/dist/commands/setup-term.d.ts +24 -0
  12. package/dist/commands/setup-term.js +70 -0
  13. package/dist/commands/setup.d.ts +1 -1
  14. package/dist/commands/setup.js +12 -4
  15. package/dist/commands/ssh.js +1 -69
  16. package/dist/lib/accounting/rotate.d.ts +63 -1
  17. package/dist/lib/accounting/rotate.js +56 -0
  18. package/dist/lib/accounts/add.js +2 -2
  19. package/dist/lib/accounts/slots.js +32 -2
  20. package/dist/lib/answer-router.d.ts +11 -2
  21. package/dist/lib/answer-router.js +26 -2
  22. package/dist/lib/auth-mint.d.ts +5 -4
  23. package/dist/lib/auth-mint.js +4 -3
  24. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  25. package/dist/lib/browser/service.d.ts +10 -0
  26. package/dist/lib/browser/service.js +208 -36
  27. package/dist/lib/browser/types.d.ts +18 -0
  28. package/dist/lib/config-keys.d.ts +1 -1
  29. package/dist/lib/config-keys.js +5 -0
  30. package/dist/lib/device-config.js +61 -0
  31. package/dist/lib/devices/doctor-findings.js +2 -6
  32. package/dist/lib/feed/answer.d.ts +153 -4
  33. package/dist/lib/feed/answer.js +716 -105
  34. package/dist/lib/feed/feed.d.ts +61 -1
  35. package/dist/lib/feed/feed.js +226 -14
  36. package/dist/lib/feed/hub-server.d.ts +58 -3
  37. package/dist/lib/feed/hub-server.js +306 -54
  38. package/dist/lib/feed/pr-status.d.ts +8 -0
  39. package/dist/lib/feed/pr-status.js +9 -1
  40. package/dist/lib/feed-outcome.d.ts +1 -1
  41. package/dist/lib/feed-outcome.js +9 -2
  42. package/dist/lib/feed-policy.js +9 -3
  43. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  44. package/dist/lib/fleet/auth-sync.js +2 -89
  45. package/dist/lib/harness-auth-capabilities.js +7 -2
  46. package/dist/lib/hosts/dispatch.d.ts +20 -1
  47. package/dist/lib/hosts/dispatch.js +52 -30
  48. package/dist/lib/hosts/remote-cmd.d.ts +21 -0
  49. package/dist/lib/hosts/remote-cmd.js +26 -2
  50. package/dist/lib/mailbox.d.ts +12 -0
  51. package/dist/lib/mailbox.js +16 -2
  52. package/dist/lib/menubar/snapshot.d.ts +51 -0
  53. package/dist/lib/menubar/snapshot.js +42 -3
  54. package/dist/lib/open-url.js +2 -2
  55. package/dist/lib/projects.d.ts +23 -0
  56. package/dist/lib/projects.js +78 -0
  57. package/dist/lib/secrets-cli.d.ts +3 -3
  58. package/dist/lib/secrets-cli.js +1 -1
  59. package/dist/lib/session/active.d.ts +1 -0
  60. package/dist/lib/session/active.js +8 -0
  61. package/dist/lib/session/db.d.ts +67 -3
  62. package/dist/lib/session/db.js +381 -126
  63. package/dist/lib/session/prompt.d.ts +23 -7
  64. package/dist/lib/session/prompt.js +46 -8
  65. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  66. package/dist/lib/session/remote/remote-list.js +22 -6
  67. package/dist/lib/session/remote/watch.d.ts +12 -0
  68. package/dist/lib/session/remote/watch.js +9 -0
  69. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  70. package/dist/lib/session/remote-preview-cache.js +373 -0
  71. package/dist/lib/session/tail.d.ts +50 -0
  72. package/dist/lib/session/tail.js +219 -0
  73. package/dist/lib/setup-tool-install.js +2 -1
  74. package/dist/lib/setup-tool-status.d.ts +1 -1
  75. package/dist/lib/setup-tool-status.js +6 -1
  76. package/dist/lib/signin-badge.d.ts +19 -4
  77. package/dist/lib/signin-badge.js +29 -11
  78. package/dist/lib/term-driver.d.ts +24 -0
  79. package/dist/lib/term-driver.js +36 -0
  80. package/dist/lib/terminal/index.d.ts +1 -1
  81. package/dist/lib/terminal/index.js +1 -1
  82. package/dist/lib/terminal/inject.d.ts +38 -0
  83. package/dist/lib/terminal/inject.js +55 -9
  84. package/dist/lib/terminal/transport.d.ts +15 -5
  85. package/dist/lib/terminal/transport.js +61 -11
  86. package/package.json +1 -1
  87. package/dist/lib/fleet/remote-login.d.ts +0 -170
  88. package/dist/lib/fleet/remote-login.js +0 -568
@@ -58,7 +58,6 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
58
58
  import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
59
59
  import { registerSnapshotCommand } from './snapshot.js';
60
60
  import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
61
- import { runFleetLogin } from '../lib/fleet/remote-login.js';
62
61
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
63
62
  import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
64
63
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
@@ -1055,46 +1054,6 @@ function renderAuthMatrix(results, opts) {
1055
1054
  }
1056
1055
  return lines;
1057
1056
  }
1058
- /** Render the final `fleet login` result summary in the house auth-matrix style. */
1059
- function renderLoginMatrix(results) {
1060
- const lines = [chalk.bold('Fleet login')];
1061
- if (results.length === 0) {
1062
- lines.push(chalk.gray(' Nothing pending — every requested account is already logged in.'));
1063
- return lines;
1064
- }
1065
- const nameW = Math.max(6, ...results.map((r) => `${r.agent}@${r.device}`.length));
1066
- for (const r of results) {
1067
- const who = `${r.agent}@${r.device}`.padEnd(nameW);
1068
- let badge;
1069
- switch (r.state) {
1070
- case 'authorized':
1071
- badge = chalk.green('authorized');
1072
- break;
1073
- case 'ready':
1074
- badge = chalk.cyan('code ready');
1075
- break;
1076
- case 'driving':
1077
- badge = chalk.yellow('driving');
1078
- break;
1079
- case 'skipped':
1080
- badge = chalk.gray('not remotable');
1081
- break;
1082
- case 'error':
1083
- badge = chalk.red('error');
1084
- break;
1085
- default:
1086
- badge = chalk.gray(r.state);
1087
- break;
1088
- }
1089
- const note = r.detail ? chalk.dim(` ${r.detail}`) : (r.reason && !r.remotable ? chalk.dim(` ${r.reason}`) : '');
1090
- lines.push(` ${who} ${badge}${note}`);
1091
- }
1092
- const auth = results.filter((r) => r.state === 'authorized').length;
1093
- const remotable = results.filter((r) => r.remotable).length;
1094
- lines.push('');
1095
- lines.push(chalk.gray(` ${auth}/${remotable} remotable logins authorized`));
1096
- return lines;
1097
- }
1098
1057
  /** Register the `agents devices` command tree (also aliased as `fleet`). */
1099
1058
  function registerDevicesCommands(program) {
1100
1059
  const devicesCmd = program
@@ -1141,7 +1100,7 @@ function registerDevicesCommands(program) {
1141
1100
  { title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
1142
1101
  { title: 'Disposable devices', names: ['lease'] },
1143
1102
  { title: 'Configure a device', names: ['config', 'describe', 'render'] },
1144
- { title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply', 'worktrees'] },
1103
+ { title: 'Fleet operations', names: ['update', 'run', 'capture', 'apply', 'worktrees'] },
1145
1104
  ]);
1146
1105
  devicesCmd
1147
1106
  .command('sync')
@@ -2008,33 +1967,6 @@ agents devices accounts --json # machine-readable, per-host account gr
2008
1967
  Collapses the installs that share one account (e.g. five claude versions on one
2009
1968
  email) into a single row. Use \`agents devices harnesses\` for the per-install view.`,
2010
1969
  });
2011
- devicesCmd
2012
- .command('login')
2013
- .description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
2014
- .option('--agents <csv>', 'only these agents (comma-separated); default: every agent with a device-code flow')
2015
- .option('--devices <csv>', 'only these devices (comma-separated); default: every online box')
2016
- .option('--all', 'target every device-code pair regardless of cached login state (cold cache / forced re-login)')
2017
- .option('--interactive', 'guided one-box-at-a-time wizard (codes requested just-in-time)')
2018
- .option('--json', 'output the final result matrix as JSON')
2019
- .action(async (opts) => {
2020
- const csv = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
2021
- const results = await runFleetLogin({
2022
- agents: csv(opts.agents),
2023
- devices: csv(opts.devices),
2024
- all: opts.all,
2025
- interactive: opts.interactive,
2026
- json: opts.json,
2027
- });
2028
- if (opts.json) {
2029
- console.log(JSON.stringify(results, null, 2));
2030
- }
2031
- else {
2032
- for (const line of renderLoginMatrix(results))
2033
- console.log(line);
2034
- }
2035
- if (results.some((r) => r.remotable && r.state !== 'authorized'))
2036
- process.exitCode = 1;
2037
- });
2038
1970
  devicesCmd
2039
1971
  .command('show <name>')
2040
1972
  .description('Show the full profile for one device.')
@@ -4,7 +4,7 @@
4
4
  * Detects which installed versions have expired credentials and rotates
5
5
  * authentication tokens so users maintain active sessions across version switches.
6
6
  */
7
- import type { AgentId, RunStrategy } from '../types.js';
7
+ import { type AgentId, type 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';
@@ -549,6 +549,68 @@ export declare const DEFAULT_ROTATION_FAILOVER_LIMIT = 3;
549
549
  * completely unchanged.
550
550
  */
551
551
  export declare function rotationFailoverChain(rotation: RotateResult | null, pickedVersion: string, limit?: number): FallbackEntry[];
552
+ /**
553
+ * Whether a run SHAPE may take the preflight alternate-harness handoff
554
+ * ({@link preflightFallbackHandoff}) — the sibling of
555
+ * {@link shouldArmRotationFailover}, and load-bearing for the same reason.
556
+ *
557
+ * The handoff consumes the entry it uses from the `--fallback` spec, and the
558
+ * canonical `--fallback` validation runs LATER in the run command: it rejects a
559
+ * chain on an interactive, `--acp`, `--loop`, or `--resume-checkpoint` run, and
560
+ * requires a prompt. Handing off first on one of those shapes would empty the
561
+ * spec, leave that guard nothing to reject, and let the run proceed as the
562
+ * alternate harness — an interactive Codex launch from
563
+ * `agents run claude --interactive --fallback codex`, which the CLI refuses
564
+ * today. Gating the shape here keeps the original validation intact: an
565
+ * ineligible shape never switches, so the spec survives and still fails there
566
+ * with its own message.
567
+ *
568
+ * `resume` and `workflowScoped` are excluded for their own reasons: a resume is
569
+ * bound to the session's OWN harness, and a workflow's tool/MCP scoping is
570
+ * claude-only, so switching harness would silently drop the declared sandbox —
571
+ * the fail-open `runWithFallback` only warns about.
572
+ *
573
+ * Pure so the matrix is unit-testable without invoking the run command.
574
+ */
575
+ export interface PreflightHandoffContext {
576
+ hasPrompt: boolean;
577
+ interactive: boolean;
578
+ acp: boolean;
579
+ loop: boolean;
580
+ resumeCheckpoint: boolean;
581
+ resume: boolean;
582
+ workflowScoped: boolean;
583
+ }
584
+ export declare function preflightHandoffEligible(ctx: PreflightHandoffContext): boolean;
585
+ /**
586
+ * The permitted alternate harness to launch when EVERY account of the primary
587
+ * agent is already exhausted at PREFLIGHT (PHNX-3999 F19).
588
+ *
589
+ * `runWithFallback` only reaches a `--fallback` entry after the primary has
590
+ * actually RUN and hit a rate limit. When the primary's accounts are throttled
591
+ * before the run starts, version resolution reports `exhausted` and the run used
592
+ * to exit — so the alternate a user configured for exactly this case never fired.
593
+ * This resolves the first usable alternate from the same `--fallback` spec, and
594
+ * returns the remainder of the spec so the alternate becomes the primary and is
595
+ * not also listed as its own fallback.
596
+ *
597
+ * Deliberately NOT a second parser: an entry this cannot resolve exactly — a
598
+ * typo, an alias the canonical parse would fuzzy-match, the primary itself —
599
+ * makes the WHOLE spec yield `null`, so the run reaches the one canonical
600
+ * `--fallback` parse and fails (or corrects) there with the message it already
601
+ * has. `--fallback typoo,codex` must never silently launch codex while dropping
602
+ * `typoo`.
603
+ *
604
+ * Returns `null` when there is no spec, when any entry is not an exact agent id
605
+ * or names the primary, or when the primary is only sign-in-recoverable rather
606
+ * than throttled — a logged-out account is fixed by logging in, not by silently
607
+ * running a different harness.
608
+ */
609
+ export declare function preflightFallbackHandoff(spec: string | undefined, primary: AgentId, exhausted: RotateCandidate[]): {
610
+ agent: AgentId;
611
+ version?: string;
612
+ remainingSpec?: string;
613
+ } | null;
552
614
  /**
553
615
  * Whether a run is eligible to have a mid-run rate-limit failover chain armed
554
616
  * (issue #348). Failover injects synthesized `FallbackEntry`s into the same
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
+ import { isAgentId } from '../types.js';
9
10
  import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
10
11
  import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
11
12
  import { readMeta, getHelpersDir } from '../state.js';
@@ -1206,6 +1207,61 @@ export function rotationFailoverChain(rotation, pickedVersion, limit = DEFAULT_R
1206
1207
  }
1207
1208
  return chain;
1208
1209
  }
1210
+ export function preflightHandoffEligible(ctx) {
1211
+ return (ctx.hasPrompt &&
1212
+ !ctx.interactive &&
1213
+ !ctx.acp &&
1214
+ !ctx.loop &&
1215
+ !ctx.resumeCheckpoint &&
1216
+ !ctx.resume &&
1217
+ !ctx.workflowScoped);
1218
+ }
1219
+ /**
1220
+ * The permitted alternate harness to launch when EVERY account of the primary
1221
+ * agent is already exhausted at PREFLIGHT (PHNX-3999 F19).
1222
+ *
1223
+ * `runWithFallback` only reaches a `--fallback` entry after the primary has
1224
+ * actually RUN and hit a rate limit. When the primary's accounts are throttled
1225
+ * before the run starts, version resolution reports `exhausted` and the run used
1226
+ * to exit — so the alternate a user configured for exactly this case never fired.
1227
+ * This resolves the first usable alternate from the same `--fallback` spec, and
1228
+ * returns the remainder of the spec so the alternate becomes the primary and is
1229
+ * not also listed as its own fallback.
1230
+ *
1231
+ * Deliberately NOT a second parser: an entry this cannot resolve exactly — a
1232
+ * typo, an alias the canonical parse would fuzzy-match, the primary itself —
1233
+ * makes the WHOLE spec yield `null`, so the run reaches the one canonical
1234
+ * `--fallback` parse and fails (or corrects) there with the message it already
1235
+ * has. `--fallback typoo,codex` must never silently launch codex while dropping
1236
+ * `typoo`.
1237
+ *
1238
+ * Returns `null` when there is no spec, when any entry is not an exact agent id
1239
+ * or names the primary, or when the primary is only sign-in-recoverable rather
1240
+ * than throttled — a logged-out account is fixed by logging in, not by silently
1241
+ * running a different harness.
1242
+ */
1243
+ export function preflightFallbackHandoff(spec, primary, exhausted) {
1244
+ if (!spec)
1245
+ return null;
1246
+ if (exhausted.length === 0)
1247
+ return null;
1248
+ if (signInRecoverableCandidates(exhausted).length > 0)
1249
+ return null;
1250
+ const entries = spec.split(',').map((e) => e.trim()).filter(Boolean);
1251
+ if (entries.length === 0)
1252
+ return null;
1253
+ const parsed = entries.map((entry) => entry.split('@'));
1254
+ // Every entry has to be exactly resolvable, or this defers entirely.
1255
+ if (parsed.some(([name]) => !isAgentId(name) || name === primary))
1256
+ return null;
1257
+ const [name, version] = parsed[0];
1258
+ const remaining = entries.slice(1);
1259
+ return {
1260
+ agent: name,
1261
+ version: version || undefined,
1262
+ remainingSpec: remaining.length > 0 ? remaining.join(',') : undefined,
1263
+ };
1264
+ }
1209
1265
  export function shouldArmRotationFailover(ctx) {
1210
1266
  return (ctx.hasRotation &&
1211
1267
  ctx.hasVersion &&
@@ -89,11 +89,11 @@ export function workerProvisioningHint(agent) {
89
89
  parts.push(env === 'provider' ? 'a provider API key collected by add (--api-key)' : `the ${env} collected by add (--api-key)`);
90
90
  }
91
91
  else if (kind.startsWith('per-device')) {
92
- parts.push(`per-device login on each worker (--per-device / agents fleet login ${agent})`);
92
+ parts.push(`per-device login on each worker (--per-device; run \`agents run ${agent} --device <box>\` there and complete its native login)`);
93
93
  }
94
94
  }
95
95
  if (parts.length === 0)
96
- return `${agent} has no portable credential — it logs in per box (agents fleet login ${agent})`;
96
+ return `${agent} has no portable credential — it logs in per box (run \`agents run ${agent} --device <box>\` on the worker and complete its native login)`;
97
97
  return parts.join(', or ');
98
98
  }
99
99
  /**
@@ -13,7 +13,8 @@
13
13
  */
14
14
  import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
- import { agentConfigDirName } from '../agents.js';
16
+ import { AGENTS, agentConfigDirName } from '../agents.js';
17
+ import { createLink } from '../platform/links.js';
17
18
  import { harnessAuth, harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
18
19
  import { installSessionTrackerHookSync } from '../hooks/install.js';
19
20
  import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
@@ -64,7 +65,36 @@ function projectResources(harness, version, destHome, fromHome) {
64
65
  if (!writer)
65
66
  continue;
66
67
  if (kind === 'rules') {
67
- writer.write({ version, versionHome: destHome, selection: { preset: 'default' }, cwd });
68
+ // Symlink to the version home's rules instead of composing a duplicate.
69
+ // Harnesses whose shim pins a separate config dir (e.g. CLAUDE_CONFIG_DIR
70
+ // → account slot) cause the harness to load rules from both the version
71
+ // home (~/.claude/) and the slot, doubling ~5 K tokens per session.
72
+ // A symlink gives both paths the same inode so the harness can dedup.
73
+ const cap = AGENTS[harness].capabilities.rules;
74
+ if (typeof cap !== 'object')
75
+ continue;
76
+ const srcFile = path.join(fromHome, agentConfigDirName(harness), cap.file);
77
+ if (!fs.existsSync(srcFile)) {
78
+ writer.write({ version, versionHome: destHome, selection: { preset: 'default' }, cwd });
79
+ continue;
80
+ }
81
+ const destFile = path.join(destHome, agentConfigDirName(harness), cap.file);
82
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
83
+ try {
84
+ const st = fs.lstatSync(destFile);
85
+ if (st.isSymbolicLink() || st.isFile())
86
+ fs.unlinkSync(destFile);
87
+ }
88
+ catch { /* did not exist */ }
89
+ try {
90
+ createLink(srcFile, destFile);
91
+ }
92
+ catch (err) {
93
+ if (err.code === 'EEXIST') {
94
+ continue;
95
+ }
96
+ throw err;
97
+ }
68
98
  continue;
69
99
  }
70
100
  const names = getDetector(kind, harness)?.list({ version, versionHome: fromHome, cwd }) ?? [];
@@ -10,7 +10,7 @@
10
10
  * This module picks the delivery mechanism from (open feed block × session
11
11
  * liveness × runtime rail). Pure — unit-testable without a live terminal.
12
12
  */
13
- import type { OpenBlock, BlockOption } from './feed/feed.js';
13
+ import { type OpenBlock, type BlockOption } from './feed/feed.js';
14
14
  import type { ActiveSession } from './session/active.js';
15
15
  import type { InjectTarget } from './terminal/index.js';
16
16
  export type AnswerRouteKind = 'mailbox' | 'tmux' | 'iterm' | 'resume' | 'refuse';
@@ -67,7 +67,16 @@ export declare function keystrokesForAnswer(answer: string, options?: Array<Pick
67
67
  };
68
68
  /** True when the session is waiting on user input (parked on a question/plan). */
69
69
  export declare function isParkedOnInput(session: ActiveSession | null | undefined): boolean;
70
- /** True when an open feed block still needs an answer. */
70
+ /**
71
+ * True when an open feed block still needs an answer.
72
+ *
73
+ * Openness comes from {@link deriveBlockState}, not from `block.answer` being
74
+ * truthy: a PENDING claim records the answer while deliberately leaving the
75
+ * block `open` (feed.ts `recordAnswer`'s two-phase mode), so reading the field
76
+ * directly would call a claimed-but-undelivered block "closed" — and a retry of
77
+ * a stranded claim would then re-route a parked headless agent to the mailbox it
78
+ * will never drain (PHNX-3999).
79
+ */
71
80
  export declare function isOpenQuestionBlock(block: OpenBlock | null | undefined): boolean;
72
81
  /**
73
82
  * Pick the delivery mechanism for one answer.
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Parked-agent answer router (RUSH-1474).
3
+ *
4
+ * `agents message` historically only enqueued into the mailbox (delivered at the
5
+ * next PreToolUse). That steers a *running* agent between tool calls but never
6
+ * unblocks a *parked* agent:
7
+ * - interactive TUI open on AskUserQuestion (needs keystrokes, not context)
8
+ * - headless run waiting on input (no next tool call; needs resume)
9
+ *
10
+ * This module picks the delivery mechanism from (open feed block × session
11
+ * liveness × runtime rail). Pure — unit-testable without a live terminal.
12
+ */
13
+ import { deriveBlockState } from './feed/feed.js';
1
14
  import { addressabilityRecoveryHint } from './terminal/resolve.js';
2
15
  import { injectTargetFromReplyRail } from './session/inject.js';
3
16
  /** The Escape keystroke as its raw control byte — what a tmux/iterm rail reads as a real Escape. */
@@ -64,11 +77,22 @@ export function isParkedOnInput(session) {
64
77
  }
65
78
  return false;
66
79
  }
67
- /** True when an open feed block still needs an answer. */
80
+ /**
81
+ * True when an open feed block still needs an answer.
82
+ *
83
+ * Openness comes from {@link deriveBlockState}, not from `block.answer` being
84
+ * truthy: a PENDING claim records the answer while deliberately leaving the
85
+ * block `open` (feed.ts `recordAnswer`'s two-phase mode), so reading the field
86
+ * directly would call a claimed-but-undelivered block "closed" — and a retry of
87
+ * a stranded claim would then re-route a parked headless agent to the mailbox it
88
+ * will never drain (PHNX-3999).
89
+ */
68
90
  export function isOpenQuestionBlock(block) {
69
91
  if (!block)
70
92
  return false;
71
- if (block.answer || block.parkedAt || block.continuedAt || block.defaultedAt)
93
+ if (block.parkedAt || block.continuedAt || block.defaultedAt)
94
+ return false;
95
+ if (deriveBlockState(block) !== 'open')
72
96
  return false;
73
97
  return (block.questions?.length ?? 0) > 0;
74
98
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * First-class setup-token mint + seed (PHNX-2364).
3
3
  *
4
- * Closes the mint-auth manual recipe: drive `claude setup-token` through the
5
- * same injectable term driver `agents fleet login` uses, capture a well-formed
4
+ * Closes the mint-auth manual recipe: drive `claude setup-token` through an
5
+ * injectable term driver (`lib/term-driver.ts`), capture a well-formed
6
6
  * `sk-ant-oat01-…` token (the #1767 ANSI-banner guard), and seed BOTH:
7
7
  *
8
8
  * 1. a named provider account (`agents accounts add` shape, policy never)
@@ -16,7 +16,7 @@
16
16
  import type { AgentId, Meta } from './types.js';
17
17
  import { type CredentialAccount } from './account-registry.js';
18
18
  import { type StorableCredentialKind } from './secrets-policy.js';
19
- import { type DriveOptions, type TermDriver } from './fleet/remote-login.js';
19
+ import { type DriveOptions, type TermDriver } from './term-driver.js';
20
20
  /** Well-formed Claude setup-token as it appears inside a TTY blob. */
21
21
  export declare const CLAUDE_SETUP_TOKEN_CAPTURE_RE: RegExp;
22
22
  export interface MintFlow {
@@ -37,7 +37,8 @@ export interface MintFlow {
37
37
  * key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
38
38
  * from OAuth). The api-key entries describe collection only — they have no
39
39
  * mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
40
- * Native device-code login (kimi/antigravity) stays on `agents fleet login`.
40
+ * A token-less harness (kimi/antigravity) has no portable credential and logs
41
+ * in per box (run it there and complete its native login).
41
42
  * The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
42
43
  * kinds by auth-mint.test.ts — keep them in lockstep.
43
44
  */
@@ -5,7 +5,7 @@ import { bundleBackendSync, isSecretsClientError, bundleExists, bundleExistsSync
5
5
  import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
6
6
  import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
7
7
  import { shellQuote } from './ssh-exec.js';
8
- import { defaultTermDriver, } from './fleet/remote-login.js';
8
+ import { defaultTermDriver, } from './term-driver.js';
9
9
  import { showUrl } from './open-url.js';
10
10
  import { loadDevices } from './devices/registry.js';
11
11
  import { isSelfHost } from './devices/self-host.js';
@@ -21,7 +21,8 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
21
21
  * key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
22
22
  * from OAuth). The api-key entries describe collection only — they have no
23
23
  * mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
24
- * Native device-code login (kimi/antigravity) stays on `agents fleet login`.
24
+ * A token-less harness (kimi/antigravity) has no portable credential and logs
25
+ * in per box (run it there and complete its native login).
25
26
  * The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
26
27
  * kinds by auth-mint.test.ts — keep them in lockstep.
27
28
  */
@@ -63,7 +64,7 @@ export function unmintableMessage(harness) {
63
64
  return [
64
65
  `Cannot mint a setup-token for '${harness}'.`,
65
66
  `Interactive setup-token mint is implemented for: ${listMintableHarnesses().join(', ')}.`,
66
- `Device-code native login is \`agents fleet login --agent ${harness}\`.`,
67
+ `A token-less harness like '${harness}' logs in per box — run it on that device and complete its native login.`,
67
68
  `API-key / pasted-token accounts use \`agents accounts add <name> --provider <provider> --auth api-key\`.`,
68
69
  ].join(' ');
69
70
  }
@@ -7,7 +7,7 @@ interface ArcNativeTab extends ArcNativeTabRef {
7
7
  url: string;
8
8
  title: string;
9
9
  }
10
- interface ArcEnumeratedSpace {
10
+ export interface ArcEnumeratedSpace {
11
11
  windowId: string;
12
12
  spaceId: string;
13
13
  spaceTitle: string;
@@ -731,6 +731,16 @@ export declare class BrowserService {
731
731
  * async/promise evaluation fails honestly with an AppleScript error.
732
732
  */
733
733
  private evaluateArcNative;
734
+ /**
735
+ * Resolve a task's owned Arc tabs against one shared snapshot of every
736
+ * window/space/tab, rather than asking Arc per tab as `listArcTaskTabs` does.
737
+ *
738
+ * Same ownership rule as the live path — `requireArcTask`, then the recorded
739
+ * ref must be present in its original window and Space. The snapshot changes
740
+ * how often Arc is asked, never what counts as owned. Actions keep the live
741
+ * path, because a snapshot can go stale before it is acted on.
742
+ */
743
+ private listArcTaskTabsFromSnapshot;
734
744
  private listArcTaskTabs;
735
745
  /**
736
746
  * Close native Arc tabs owned by a task. Only closes tabs the task created