@phnx-labs/agents-cli 1.22.26 → 1.22.28

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 (183) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +80 -27
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/bench.d.ts +2 -0
  5. package/dist/commands/bench.js +101 -0
  6. package/dist/commands/doctor.js +24 -10
  7. package/dist/commands/exec.js +71 -26
  8. package/dist/commands/hosts.js +4 -3
  9. package/dist/commands/insights.js +39 -5
  10. package/dist/commands/output.js +100 -25
  11. package/dist/commands/perf.d.ts +10 -0
  12. package/dist/commands/perf.js +14 -6
  13. package/dist/commands/projects.js +24 -10
  14. package/dist/commands/resume.d.ts +11 -0
  15. package/dist/commands/resume.js +51 -0
  16. package/dist/commands/routines.js +31 -4
  17. package/dist/commands/run-account-picker.d.ts +37 -0
  18. package/dist/commands/run-account-picker.js +101 -17
  19. package/dist/commands/secrets.js +86 -2
  20. package/dist/commands/sessions-picker.js +12 -7
  21. package/dist/commands/sessions.d.ts +73 -7
  22. package/dist/commands/sessions.js +391 -49
  23. package/dist/commands/ssh.js +184 -11
  24. package/dist/commands/teams.js +22 -11
  25. package/dist/index.js +16 -20
  26. package/dist/lib/activity.js +45 -99
  27. package/dist/lib/auth-health.d.ts +47 -3
  28. package/dist/lib/auth-health.js +74 -15
  29. package/dist/lib/bench/index.d.ts +4 -0
  30. package/dist/lib/bench/index.js +4 -0
  31. package/dist/lib/bench/runner.d.ts +16 -0
  32. package/dist/lib/bench/runner.js +111 -0
  33. package/dist/lib/bench/schema.d.ts +5 -0
  34. package/dist/lib/bench/schema.js +91 -0
  35. package/dist/lib/bench/storage.d.ts +5 -0
  36. package/dist/lib/bench/storage.js +32 -0
  37. package/dist/lib/bench/types.d.ts +40 -0
  38. package/dist/lib/bench/types.js +1 -0
  39. package/dist/lib/claude-account-token.d.ts +2 -0
  40. package/dist/lib/claude-account-token.js +56 -3
  41. package/dist/lib/cloud/host.js +1 -0
  42. package/dist/lib/crabbox/cli.d.ts +2 -0
  43. package/dist/lib/crabbox/cli.js +2 -0
  44. package/dist/lib/crabbox/lease.js +7 -1
  45. package/dist/lib/daemon.js +20 -0
  46. package/dist/lib/devices/connect.d.ts +2 -0
  47. package/dist/lib/devices/connect.js +7 -0
  48. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  49. package/dist/lib/devices/doctor-findings.js +15 -1
  50. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  51. package/dist/lib/devices/harness-inventory.js +0 -0
  52. package/dist/lib/devices/registry.d.ts +2 -0
  53. package/dist/lib/devices/resolve-target.d.ts +1 -0
  54. package/dist/lib/devices/resolve-target.js +9 -2
  55. package/dist/lib/devices/ssh-config.js +3 -0
  56. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  57. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  58. package/dist/lib/events.d.ts +1 -1
  59. package/dist/lib/events.js +2 -2
  60. package/dist/lib/exec.js +18 -8
  61. package/dist/lib/fleet/apply.js +9 -7
  62. package/dist/lib/fleet/remote-login.d.ts +4 -3
  63. package/dist/lib/fleet/remote-login.js +11 -9
  64. package/dist/lib/gemini-settings.d.ts +0 -1
  65. package/dist/lib/gemini-settings.js +12 -7
  66. package/dist/lib/git.d.ts +9 -3
  67. package/dist/lib/git.js +39 -22
  68. package/dist/lib/hooks/profile.d.ts +12 -1
  69. package/dist/lib/hooks/profile.js +6 -1
  70. package/dist/lib/hooks.d.ts +8 -5
  71. package/dist/lib/hooks.js +14 -7
  72. package/dist/lib/hosts/dispatch.d.ts +10 -0
  73. package/dist/lib/hosts/dispatch.js +85 -25
  74. package/dist/lib/hosts/logs.js +11 -1
  75. package/dist/lib/hosts/passthrough.d.ts +27 -0
  76. package/dist/lib/hosts/passthrough.js +19 -10
  77. package/dist/lib/hosts/progress.d.ts +12 -1
  78. package/dist/lib/hosts/progress.js +41 -8
  79. package/dist/lib/hosts/providers/devices.js +1 -0
  80. package/dist/lib/hosts/ready.d.ts +45 -4
  81. package/dist/lib/hosts/ready.js +124 -20
  82. package/dist/lib/hosts/reconcile.d.ts +2 -1
  83. package/dist/lib/hosts/reconcile.js +24 -7
  84. package/dist/lib/hosts/reconnect.d.ts +45 -12
  85. package/dist/lib/hosts/reconnect.js +94 -36
  86. package/dist/lib/hosts/registry.d.ts +2 -2
  87. package/dist/lib/hosts/registry.js +4 -5
  88. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  89. package/dist/lib/hosts/remote-cmd.js +29 -0
  90. package/dist/lib/hosts/tasks.d.ts +3 -0
  91. package/dist/lib/hosts/types.d.ts +1 -0
  92. package/dist/lib/hosts/types.js +3 -0
  93. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  94. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  95. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  96. package/dist/lib/menubar/install-menubar.js +20 -9
  97. package/dist/lib/menubar/snapshot.js +15 -2
  98. package/dist/lib/models.d.ts +42 -6
  99. package/dist/lib/models.js +55 -153
  100. package/dist/lib/perf/db.js +16 -3
  101. package/dist/lib/perf/types.d.ts +12 -1
  102. package/dist/lib/pricing/cost.d.ts +9 -0
  103. package/dist/lib/pricing/cost.js +24 -0
  104. package/dist/lib/pricing/index.d.ts +1 -1
  105. package/dist/lib/pricing/index.js +1 -1
  106. package/dist/lib/project-probe.d.ts +22 -3
  107. package/dist/lib/project-probe.js +105 -17
  108. package/dist/lib/project-status.d.ts +9 -0
  109. package/dist/lib/project-status.js +15 -0
  110. package/dist/lib/redact.js +8 -3
  111. package/dist/lib/refresh.js +22 -11
  112. package/dist/lib/remote-agents-json.d.ts +32 -0
  113. package/dist/lib/remote-agents-json.js +47 -16
  114. package/dist/lib/resource-profiles.js +1 -2
  115. package/dist/lib/rotate.d.ts +22 -0
  116. package/dist/lib/rotate.js +26 -0
  117. package/dist/lib/routine-notify-owner.d.ts +102 -0
  118. package/dist/lib/routine-notify-owner.js +232 -0
  119. package/dist/lib/routines.d.ts +10 -0
  120. package/dist/lib/runner.d.ts +9 -1
  121. package/dist/lib/runner.js +182 -17
  122. package/dist/lib/sandbox.d.ts +0 -2
  123. package/dist/lib/sandbox.js +2 -19
  124. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  125. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  126. package/dist/lib/secrets/agent.d.ts +19 -3
  127. package/dist/lib/secrets/agent.js +61 -22
  128. package/dist/lib/secrets/audit.d.ts +1 -1
  129. package/dist/lib/secrets/audit.js +2 -0
  130. package/dist/lib/secrets/bundles.d.ts +3 -3
  131. package/dist/lib/secrets/bundles.js +15 -5
  132. package/dist/lib/secrets/filestore.d.ts +2 -0
  133. package/dist/lib/secrets/filestore.js +4 -0
  134. package/dist/lib/secrets/lease.d.ts +25 -0
  135. package/dist/lib/secrets/lease.js +44 -0
  136. package/dist/lib/secrets/session-store.d.ts +7 -0
  137. package/dist/lib/secrets/session-store.js +21 -0
  138. package/dist/lib/self-update.d.ts +20 -5
  139. package/dist/lib/self-update.js +93 -16
  140. package/dist/lib/session/active.d.ts +49 -0
  141. package/dist/lib/session/active.js +175 -11
  142. package/dist/lib/session/bash-command.d.ts +16 -0
  143. package/dist/lib/session/bash-command.js +65 -0
  144. package/dist/lib/session/db.d.ts +23 -2
  145. package/dist/lib/session/db.js +86 -20
  146. package/dist/lib/session/discover.d.ts +21 -1
  147. package/dist/lib/session/discover.js +124 -32
  148. package/dist/lib/session/insights.d.ts +19 -0
  149. package/dist/lib/session/insights.js +78 -10
  150. package/dist/lib/session/recovery.d.ts +17 -1
  151. package/dist/lib/session/recovery.js +111 -4
  152. package/dist/lib/session/remote-bundle.js +1 -1
  153. package/dist/lib/session/remote-list.d.ts +10 -9
  154. package/dist/lib/session/remote-list.js +25 -23
  155. package/dist/lib/session/resume-owner.d.ts +55 -0
  156. package/dist/lib/session/resume-owner.js +69 -0
  157. package/dist/lib/session/team-filter.d.ts +65 -0
  158. package/dist/lib/session/team-filter.js +98 -3
  159. package/dist/lib/session/types.d.ts +33 -0
  160. package/dist/lib/smart-launch.d.ts +31 -5
  161. package/dist/lib/smart-launch.js +43 -7
  162. package/dist/lib/ssh-exec.d.ts +27 -0
  163. package/dist/lib/ssh-exec.js +34 -1
  164. package/dist/lib/ssh-tunnel.d.ts +3 -2
  165. package/dist/lib/ssh-tunnel.js +25 -16
  166. package/dist/lib/staleness/detectors/skills.d.ts +2 -0
  167. package/dist/lib/staleness/detectors/skills.js +17 -2
  168. package/dist/lib/staleness/index.d.ts +10 -1
  169. package/dist/lib/staleness/index.js +28 -3
  170. package/dist/lib/startup/command-registry.d.ts +1 -0
  171. package/dist/lib/startup/command-registry.js +2 -0
  172. package/dist/lib/teams/agents.d.ts +1 -0
  173. package/dist/lib/teams/agents.js +38 -11
  174. package/dist/lib/teams/placement-probe.d.ts +1 -1
  175. package/dist/lib/teams/placement-probe.js +24 -24
  176. package/dist/lib/teams/remoteWorktree.d.ts +11 -7
  177. package/dist/lib/teams/remoteWorktree.js +29 -27
  178. package/dist/lib/tmux/session.d.ts +8 -0
  179. package/dist/lib/tmux/session.js +22 -0
  180. package/dist/lib/types.d.ts +2 -0
  181. package/dist/lib/versions.d.ts +1 -0
  182. package/dist/lib/versions.js +40 -22
  183. package/package.json +3 -1
@@ -1,6 +1,8 @@
1
1
  import { select } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
2
3
  import { agentLabel } from '../lib/agents.js';
3
- import { collectRunCandidates, readinessFromCandidate, } from '../lib/rotate.js';
4
+ import { loginHint } from '../lib/signin-badge.js';
5
+ import { collectRunCandidates, readinessFromCandidate, isSignInRecoverable, } from '../lib/rotate.js';
4
6
  import { compareVersions, getGlobalDefault } from '../lib/versions.js';
5
7
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
6
8
  const CANCEL_SELECTION = '__agents_cancel_account_selection__';
@@ -30,14 +32,21 @@ export function formatAccountLimits(candidate) {
30
32
  })
31
33
  .join(' · ');
32
34
  }
33
- function disabledReason(candidate) {
34
- const readiness = readinessFromCandidate(candidate);
35
+ /**
36
+ * Why a row cannot be picked. Only a THROTTLE disables a row: the account is
37
+ * signed in and out of capacity, so launching it just hammers an exhausted
38
+ * account (RUSH-2132) and nothing the user does at this prompt helps.
39
+ *
40
+ * An AUTH exclusion (`signed_out` / `revoked`) is deliberately NOT disabled —
41
+ * the harness's own TUI is the login surface, so picking that row and launching
42
+ * is the only way to sign in through agents-cli. Disabling it left a fully
43
+ * logged-out harness with no reachable account at all (RUSH-2334).
44
+ */
45
+ function disabledReason(candidate, readiness) {
35
46
  if (readiness.ready)
36
47
  return undefined;
37
- if (readiness.reason === 'signed_out')
38
- return 'logged out';
39
- if (readiness.reason === 'revoked')
40
- return 'needs re-login';
48
+ if (isSignInRecoverable(readiness))
49
+ return undefined;
41
50
  if (readiness.reason === 'out_of_credits')
42
51
  return 'out of credits';
43
52
  const windows = candidate.usageSnapshot?.windows ?? [];
@@ -53,24 +62,44 @@ function disabledReason(candidate) {
53
62
  /** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
54
63
  export function buildRunAccountChoices(candidates, globalDefault) {
55
64
  const rows = candidates.map((candidate) => {
56
- const disabled = disabledReason(candidate);
65
+ const readiness = readinessFromCandidate(candidate);
66
+ const disabled = disabledReason(candidate, readiness);
67
+ const signInRequired = isSignInRecoverable(readiness);
57
68
  const version = candidate.version === globalDefault
58
69
  ? `${candidate.version} (default)`
59
70
  : candidate.version;
71
+ // `revoked` is signed-in-but-rejected, so it reads as a re-login rather
72
+ // than "logged out"; every throttle is still a signed-in account.
73
+ const authReason = readiness.ready ? null : readiness.reason;
74
+ const status = authReason === 'revoked'
75
+ ? 'needs re-login'
76
+ : authReason === 'signed_out'
77
+ ? 'logged out'
78
+ : 'logged in';
60
79
  return {
61
80
  candidate,
62
81
  account: candidate.accountLabel || 'account unavailable',
63
82
  version,
64
- status: candidate.signedIn ? 'logged in' : 'logged out',
83
+ status,
65
84
  plan: candidate.usageSnapshot?.plan ?? candidate.plan ?? 'plan unavailable',
66
- limits: formatAccountLimits(candidate),
85
+ // An auth-blocked row shows what picking it DOES; its quota is moot until
86
+ // there is a credential to spend it with.
87
+ limits: authReason === 'revoked'
88
+ ? 'launch to re-authenticate'
89
+ : authReason === 'signed_out'
90
+ ? 'launch to sign in'
91
+ : formatAccountLimits(candidate),
67
92
  disabled,
68
- ready: disabled === undefined,
93
+ ready: readiness.ready,
94
+ signInRequired,
69
95
  };
70
96
  });
97
+ // Ready accounts first, then the ones a login would unlock (actionable), then
98
+ // the throttled rows the user can do nothing about at this prompt.
99
+ const rank = (row) => row.ready ? 0 : row.signInRequired ? 1 : 2;
71
100
  rows.sort((a, b) => {
72
- if (a.ready !== b.ready)
73
- return a.ready ? -1 : 1;
101
+ if (rank(a) !== rank(b))
102
+ return rank(a) - rank(b);
74
103
  const aDefault = a.candidate.version === globalDefault;
75
104
  const bDefault = b.candidate.version === globalDefault;
76
105
  if (aDefault !== bDefault)
@@ -92,8 +121,56 @@ export function buildRunAccountChoices(candidates, globalDefault) {
92
121
  value: row.candidate.version,
93
122
  disabled: row.disabled,
94
123
  ready: row.ready,
124
+ signInRequired: row.signInRequired,
95
125
  }));
96
126
  }
127
+ /**
128
+ * Whether a zero-healthy run may recover by launching for a login, or must keep
129
+ * failing loud. Three inputs, all of which have to hold:
130
+ *
131
+ * - `recoverable` — at least one excluded account is only auth-blocked. An
132
+ * all-throttled set is never launched (RUSH-2132): only a window reset clears it.
133
+ * - `tty` — a login needs a human present; off a TTY nobody can complete one.
134
+ * - `json` — `--json` marks a MACHINE consumer, which must never be handed a
135
+ * picker or dropped into a login TUI. This mirrors the canonical
136
+ * `Surface.interactive = tty && !json` in `commands/utils.ts`; a `--json` caller
137
+ * gets the parseable fail-loud error instead.
138
+ */
139
+ export function signInLaunchDecision(input) {
140
+ const humanPresent = input.tty && !input.json;
141
+ return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
142
+ }
143
+ /**
144
+ * Choose which installed version to launch so the user can authenticate, when a
145
+ * strategy found zero healthy accounts but at least one is merely signed out
146
+ * (RUSH-2334). Returns the version to launch, or null if the user cancelled.
147
+ *
148
+ * A single candidate does NOT prompt — a one-item picker is pure noise, and the
149
+ * only thing to decide has one answer. Several candidates fall through to the
150
+ * normal account picker, which shows every account with its state so the choice
151
+ * is informed (throttled rows stay disabled there).
152
+ *
153
+ * Callers MUST have already confirmed an interactive terminal: off a TTY there
154
+ * is nobody to complete the login, and the run should fail loud instead.
155
+ */
156
+ export async function pickSignInLaunchVersion(agent, recoverable, quiet = false) {
157
+ if (recoverable.length === 0)
158
+ return null;
159
+ if (recoverable.length > 1) {
160
+ const selected = await pickRunAccountCandidate(agent);
161
+ return selected?.version ?? null;
162
+ }
163
+ const [only] = recoverable;
164
+ if (!quiet) {
165
+ const readiness = readinessFromCandidate(only);
166
+ const why = !readiness.ready && readiness.reason === 'revoked'
167
+ ? 'has no valid credential (the server rejected its token)'
168
+ : 'has no signed-in account';
169
+ process.stderr.write(chalk.yellow(`${agentLabel(agent)} ${why} — launching ${agent}@${only.version} so you can sign in.\n`));
170
+ process.stderr.write(chalk.gray(`Sign in with: ${loginHint(agent)}\n`));
171
+ }
172
+ return only.version;
173
+ }
97
174
  /** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
98
175
  export async function pickRunAccountCandidate(agent) {
99
176
  if (!isInteractiveTerminal()) {
@@ -107,9 +184,14 @@ export async function pickRunAccountCandidate(agent) {
107
184
  throw new Error(`No installed ${agentLabel(agent)} versions are available. Run: agents add ${agent}@latest`);
108
185
  }
109
186
  const choices = buildRunAccountChoices(candidates, getGlobalDefault(agent));
110
- const hasReadyAccount = choices.some((choice) => choice.ready);
111
- const promptChoices = choices.map(({ ready: _ready, ...choice }) => choice);
112
- if (!hasReadyAccount) {
187
+ // "Selectable" is broader than "ready": an auth-blocked row is pickable so the
188
+ // launch can carry you into the harness's login (RUSH-2334). Only offer the
189
+ // bail-out row when literally nothing can be chosen — i.e. every account is
190
+ // throttled, which no amount of signing in fixes.
191
+ const hasSelectableAccount = choices.some((choice) => !choice.disabled);
192
+ const needsSignIn = choices.some((choice) => choice.signInRequired);
193
+ const promptChoices = choices.map(({ ready: _ready, signInRequired: _signInRequired, ...choice }) => choice);
194
+ if (!hasSelectableAccount) {
113
195
  promptChoices.push({
114
196
  name: 'No usable accounts — cancel',
115
197
  value: CANCEL_SELECTION,
@@ -117,7 +199,9 @@ export async function pickRunAccountCandidate(agent) {
117
199
  }
118
200
  try {
119
201
  const version = await select({
120
- message: `Select a ${agentLabel(agent)} account for this run:`,
202
+ message: needsSignIn
203
+ ? `Select a ${agentLabel(agent)} account for this run (pick a logged-out one to sign in):`
204
+ : `Select a ${agentLabel(agent)} account for this run:`,
121
205
  choices: promptChoices,
122
206
  loop: false,
123
207
  });
@@ -24,8 +24,9 @@ import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore
24
24
  import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
25
25
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
26
26
  import { GLOBAL_HARNESS } from '../lib/secrets/scope.js';
27
- import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
28
- import { saveSession, deleteBundleSessions, deleteAllSessions } from '../lib/secrets/session-store.js';
27
+ import { createSecretLease, selectLeasedEnv } from '../lib/secrets/lease.js';
28
+ import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentSocketExists, agentStatus, agentRevoke, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
29
+ import { activeLeaseSessions, saveSession, deleteBundleSessions, deleteAllSessions, deleteLeaseSession } from '../lib/secrets/session-store.js';
29
30
  import { getCliVersionFresh } from '../lib/version.js';
30
31
  import { readMeta } from '../lib/state.js';
31
32
  import { parseDuration } from '../lib/hooks/cache.js';
@@ -2426,6 +2427,89 @@ Examples:
2426
2427
  console.log(password);
2427
2428
  }
2428
2429
  });
2430
+ cmd
2431
+ .command('lease <bundle>')
2432
+ .description('Hold only an explicit subset of a bundle until an independent expiry.')
2433
+ .requiredOption('--keys <keys>', 'Comma-separated key subset')
2434
+ .requiredOption('--for <duration>', 'Lease duration, for example 30m or 8h')
2435
+ .option('--agent <agent>', 'Narrow the lease to one harness; default is global')
2436
+ .option('--durable', 'Keep the lease across sleep as well as broker restart')
2437
+ .action(async (name, opts) => {
2438
+ if (process.platform !== 'darwin') {
2439
+ throw new Error('Scoped lease brokering is not available on this platform yet.');
2440
+ }
2441
+ const seconds = parseDuration(opts.for);
2442
+ if (seconds === null)
2443
+ throw new Error(`Invalid lease duration '${opts.for}'.`);
2444
+ const ttlMs = seconds * 1000;
2445
+ const harness = opts.agent || GLOBAL_HARNESS;
2446
+ const { bundle, env } = readAndResolveBundleEnv(name, {
2447
+ noAgent: true,
2448
+ interactiveUnlock: true,
2449
+ caller: 'lease secrets',
2450
+ agent: harness,
2451
+ keyMode: 'storage',
2452
+ });
2453
+ const lease = createSecretLease({
2454
+ bundle: name,
2455
+ keys: opts.keys.split(','),
2456
+ availableKeys: Object.keys(env),
2457
+ ttlMs,
2458
+ harness,
2459
+ sleepPersist: opts.durable,
2460
+ });
2461
+ const leasedEnv = selectLeasedEnv(lease, env);
2462
+ if (!(await ensureAgentRunning()) || !(await agentLoad(name, bundle, leasedEnv, ttlMs, harness, lease))) {
2463
+ throw new Error('Could not load the scoped lease into the secrets broker.');
2464
+ }
2465
+ ensureDaemonStarted();
2466
+ saveSession(name, {
2467
+ bundle,
2468
+ env: leasedEnv,
2469
+ expiresAt: lease.expiresAt,
2470
+ sleepPersist: lease.sleepPersist,
2471
+ harness,
2472
+ lease,
2473
+ });
2474
+ emitSecretAudit({
2475
+ event: 'secrets.unlocked', bundle: name, operation: 'lease', source: lease.sleepPersist ? 'broker+durable' : 'broker',
2476
+ status: 'success', keys: lease.keys, keyCount: lease.keys.length, agent: harness, ttlMs: lease.expiresAt - lease.createdAt,
2477
+ });
2478
+ console.log(`${chalk.green('leased')} ${chalk.cyan(lease.id)} ${chalk.gray(`(${name}: ${lease.keys.join(', ')}, ${humanRemaining(lease.expiresAt)})`)}`);
2479
+ });
2480
+ cmd
2481
+ .command('leases')
2482
+ .description('List active scoped secret leases.')
2483
+ .action(async () => {
2484
+ const brokerLeases = (await agentStatus()).filter((entry) => entry.leaseId);
2485
+ const byId = new Map(brokerLeases.map((entry) => [entry.leaseId, entry]));
2486
+ for (const { name, lease } of activeLeaseSessions()) {
2487
+ if (!byId.has(lease.id))
2488
+ byId.set(lease.id, { name, expiresAt: lease.expiresAt, keyCount: lease.keys.length, harness: lease.harness, leaseId: lease.id, keys: lease.keys });
2489
+ }
2490
+ const leases = [...byId.values()];
2491
+ if (leases.length === 0) {
2492
+ console.log(chalk.gray('No active secret leases.'));
2493
+ return;
2494
+ }
2495
+ console.log(`${'ID'.padEnd(24)} ${'BUNDLE'.padEnd(24)} ${'KEYS'.padEnd(24)} EXPIRES IN`);
2496
+ for (const lease of leases) {
2497
+ console.log(`${lease.leaseId.padEnd(24)} ${lease.name.padEnd(24)} ${(lease.keys ?? []).join(',').padEnd(24)} ${humanRemaining(lease.expiresAt)}`);
2498
+ }
2499
+ });
2500
+ cmd
2501
+ .command('revoke <lease-id>')
2502
+ .description('Revoke one scoped secret lease immediately.')
2503
+ .action(async (leaseId) => {
2504
+ if (agentSocketExists() && !(await agentPing()).reachable) {
2505
+ throw new Error('Cannot revoke while the secrets broker is using an incompatible protocol. Restart the broker and retry.');
2506
+ }
2507
+ const persisted = deleteLeaseSession(leaseId);
2508
+ const wiped = await agentRevoke(leaseId);
2509
+ if (wiped + persisted === 0)
2510
+ throw new Error(`Secret lease '${leaseId}' is not active.`);
2511
+ console.log(chalk.green(`Revoked secret lease ${leaseId}.`));
2512
+ });
2429
2513
  cmd
2430
2514
  .command('unlock [names...]')
2431
2515
  .description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
@@ -19,12 +19,17 @@ import { renderMarkdown } from '../lib/markdown.js';
19
19
  import { itemPicker } from '../lib/picker.js';
20
20
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
21
21
  import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills } from '../lib/session/highlights.js';
22
- /** A session whose transcript lives on another machine (folded in over the live
23
- * cross-machine fan-out): its `filePath` is on that peer's disk, so the preview
24
- * can't parse it locally — it shows metadata + a "resume there" note instead.
25
- * Keys off `_remote`, not the machine tag, so locally-readable synced mirrors
26
- * still parse their file normally. */
27
- function remoteMachineOf(session) {
22
+ /** A session whose transcript FILE is on another machine (folded in over the
23
+ * live cross-machine fan-out): its `filePath` is on that peer's disk, so the
24
+ * preview can't parse it locally — it shows metadata + a "resume there" note
25
+ * instead. Keys off `_remote`, not the machine tag, so locally-readable synced
26
+ * mirrors still parse their file normally.
27
+ *
28
+ * This is the READ rule and only the read rule. Whether a session may be
29
+ * RESUMED here is a different question with a different answer — a mirror is
30
+ * readable but not resumable — and `sessionOwnerDevice`
31
+ * (lib/session/resume-owner.ts) is the single place that answers it (RUSH-2022). */
32
+ function transcriptOnPeerOf(session) {
28
33
  return session._remote ? session.machine : undefined;
29
34
  }
30
35
  /**
@@ -98,7 +103,7 @@ function sanitizeMeta(s) {
98
103
  const previewCache = new Map();
99
104
  /** Build a cached multi-line preview string for display in the session picker. */
100
105
  export function buildPreview(session) {
101
- const remote = remoteMachineOf(session);
106
+ const remote = transcriptOnPeerOf(session);
102
107
  const cacheKey = remote ? `${remote}:${session.id}` : session.id;
103
108
  const cached = previewCache.get(cacheKey);
104
109
  if (cached)
@@ -11,7 +11,7 @@ interface SessionFilterOptions {
11
11
  all?: boolean;
12
12
  teams?: boolean;
13
13
  inTeam?: string;
14
- routine?: boolean;
14
+ routine?: boolean | string;
15
15
  since?: string;
16
16
  until?: string;
17
17
  }
@@ -187,13 +187,33 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
187
187
  */
188
188
  export declare function isAwaitingUser(s: ActiveSession): boolean;
189
189
  /**
190
- * Whether a row belongs in an unqualified `--active` view.
190
+ * Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
191
+ * ONE canonical selector every bare-active surface shares: the CLI's grouped
192
+ * table and `--json` (`renderActiveSessions`), the interactive browser's
193
+ * `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
194
+ * attach gate (`isAttachableLiveSession`), and the menubar snapshot
195
+ * ({@link computeMenubarSnapshot}).
191
196
  *
192
- * The live registry deliberately retains terminally-dead rows long enough for
193
- * `--closed` / `--crashed` and recovery to find them. Presence in that registry
194
- * therefore is not, by itself, evidence that a session is still active.
195
- * Explicit lifecycle filters bypass this predicate and select those retained
196
- * rows through {@link matchesLiveStatus} instead.
197
+ * The live registry deliberately retains terminally-dead AND not-yet-started
198
+ * rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
199
+ * find them. Presence in that registry therefore is not, by itself, evidence
200
+ * that a session is CURRENTLY active. Explicit lifecycle filters bypass this
201
+ * predicate and select those retained rows through {@link matchesLiveStatus}
202
+ * instead — this function only decides the unqualified default.
203
+ *
204
+ * - `queued` / `closed` / `crashed` are never active here: queued hasn't
205
+ * started yet, closed/crashed are unconditionally dead. All three stay
206
+ * reachable through their explicit filter.
207
+ * - A cloud row carries no local pid at all — it is active on the
208
+ * provider's own word (`cloudProvider` + `cloudTaskId`), never a
209
+ * fabricated pid.
210
+ * - Every other row is a real OS process (terminal/tmux/headless/team). It
211
+ * is active only when the row names the machine it runs on, carries a
212
+ * positive pid, AND has POSITIVELY verified that pid is alive
213
+ * (`pidAlive === true` — not merely "not known dead"). A live orphaned
214
+ * row, or a live-but-stuck abandoned row, still qualifies this way since
215
+ * both carry a genuinely alive pid; unknown liveness (an older peer's
216
+ * row, or a pid that could never be resolved) does not.
197
217
  */
198
218
  export declare function isRunningLiveSession(s: ActiveSession): boolean;
199
219
  /**
@@ -389,6 +409,22 @@ export declare function isBareBrowserListing(options: SessionsOptions, query: st
389
409
  * `runSessionBrowser` picker cannot represent.
390
410
  */
391
411
  export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
412
+ /** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
413
+ export declare function resolveRoutineName(query: string, names: readonly string[]): string | null;
414
+ export interface RoutineRunGroup {
415
+ runId: string;
416
+ timestamp: string;
417
+ sessions: SessionMeta[];
418
+ }
419
+ export interface RoutineChoice {
420
+ name: string;
421
+ lastRunAt: string;
422
+ runCount: number;
423
+ latestRunSessionCount: number;
424
+ }
425
+ export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
426
+ export declare function buildRoutineChoices(sessions: SessionMeta[]): RoutineChoice[];
427
+ export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
392
428
  /** Resolve a session by id/query globally and print its compact preview (no pager).
393
429
  * Backs `--preview` — the fast path for the "peek before resume" hot loop. */
394
430
  export declare function renderSessionPreview(query: string, scope: {
@@ -478,6 +514,10 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
478
514
  groups: OverviewGroup[];
479
515
  projectCount: number;
480
516
  };
517
+ export declare function printRoutineRunOverview(sessions: SessionMeta[], liveIndex: Map<string, ActiveSession> | undefined, opts?: {
518
+ hiddenCount?: number;
519
+ hiddenUnmanaged?: number;
520
+ }): void;
481
521
  /**
482
522
  * Render a resolved session to stdout — the non-follow view behind
483
523
  * `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
@@ -577,6 +617,16 @@ export declare function pickSessionInteractive(sessions: SessionMeta[], message?
577
617
  export declare const LIVE_ROW_PREFIX = "live:";
578
618
  export declare function isIdlessLiveRow(s: SessionMeta): boolean;
579
619
  export declare function handlePickedSession(picked: PickedSession): Promise<void>;
620
+ /**
621
+ * Resume `session` on the machine that owns it, when that is not this one.
622
+ * Returns true when it handled the resume (hopped, or reported an unreachable
623
+ * owner), false when the session is local and the caller should take over here.
624
+ *
625
+ * The one hop for a foreground, one-session-at-a-time resume — shared by the
626
+ * `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
627
+ * would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
628
+ */
629
+ export declare function resumeOnOwnerIfRemote(session: SessionMeta): Promise<boolean>;
580
630
  /**
581
631
  * Resume a session in the current terminal — a foreground takeover of this
582
632
  * process. Used by the single-select picker and by `sessions resume` when the
@@ -647,6 +697,22 @@ export declare function resolveSessionQuery(pool: SessionMeta[], query: string,
647
697
  export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
648
698
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
649
699
  export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
700
+ /**
701
+ * Narrow a session list by --project and --agent before search resolution.
702
+ * Without this, a query like "scoped search" could match sessions in BOTH
703
+ * the project you specified AND elsewhere, producing an ambiguity error
704
+ * even though the user already pointed at the correct scope.
705
+ */
706
+ export declare function applyScopeFilters(sessions: SessionMeta[], scope: {
707
+ agent?: string;
708
+ project?: string;
709
+ routine?: boolean | string;
710
+ }): SessionMeta[];
711
+ export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): {
712
+ agent?: string;
713
+ project?: string;
714
+ routine?: boolean | string;
715
+ };
650
716
  /**
651
717
  * Whether a missed local id lookup should widen to a cross-machine sweep.
652
718
  *