@phnx-labs/agents-cli 1.22.23 → 1.22.25

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 (143) hide show
  1. package/CHANGELOG.md +359 -0
  2. package/README.md +19 -8
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/attach.d.ts +2 -0
  5. package/dist/commands/attach.js +32 -7
  6. package/dist/commands/cloud.js +9 -5
  7. package/dist/commands/defaults.js +2 -0
  8. package/dist/commands/doctor.d.ts +24 -0
  9. package/dist/commands/doctor.js +105 -9
  10. package/dist/commands/exec.js +99 -44
  11. package/dist/commands/feed.d.ts +23 -0
  12. package/dist/commands/feed.js +65 -2
  13. package/dist/commands/focus.d.ts +34 -3
  14. package/dist/commands/focus.js +301 -57
  15. package/dist/commands/go.d.ts +14 -1
  16. package/dist/commands/go.js +49 -5
  17. package/dist/commands/insights.js +6 -2
  18. package/dist/commands/inspect.js +39 -5
  19. package/dist/commands/menubar.js +14 -1
  20. package/dist/commands/models.js +1 -0
  21. package/dist/commands/modes.d.ts +12 -0
  22. package/dist/commands/modes.js +147 -0
  23. package/dist/commands/routines.d.ts +3 -0
  24. package/dist/commands/routines.js +70 -47
  25. package/dist/commands/run-cloud.js +1 -1
  26. package/dist/commands/secrets-sync.js +11 -13
  27. package/dist/commands/secrets.d.ts +2 -0
  28. package/dist/commands/secrets.js +54 -17
  29. package/dist/commands/sessions-browser.d.ts +36 -1
  30. package/dist/commands/sessions-browser.js +165 -19
  31. package/dist/commands/sessions-resume.d.ts +2 -0
  32. package/dist/commands/sessions-resume.js +28 -12
  33. package/dist/commands/sessions.d.ts +23 -1
  34. package/dist/commands/sessions.js +58 -61
  35. package/dist/commands/setup-secrets.js +1 -1
  36. package/dist/commands/setup-watchdog.js +5 -10
  37. package/dist/commands/setup.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/teams.js +3 -2
  40. package/dist/commands/view.js +2 -0
  41. package/dist/commands/watchdog.d.ts +3 -4
  42. package/dist/commands/watchdog.js +26 -66
  43. package/dist/index.js +38 -2
  44. package/dist/lib/agent-modes.d.ts +49 -0
  45. package/dist/lib/agent-modes.js +70 -0
  46. package/dist/lib/agents.js +126 -21
  47. package/dist/lib/cloud/cursor.d.ts +79 -0
  48. package/dist/lib/cloud/cursor.js +228 -0
  49. package/dist/lib/cloud/registry.js +2 -0
  50. package/dist/lib/cloud/types.d.ts +7 -2
  51. package/dist/lib/cloud/types.js +14 -0
  52. package/dist/lib/crabbox/cli.d.ts +2 -2
  53. package/dist/lib/crabbox/config.d.ts +7 -8
  54. package/dist/lib/crabbox/config.js +14 -14
  55. package/dist/lib/crabbox/lease.d.ts +11 -4
  56. package/dist/lib/crabbox/lease.js +40 -8
  57. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  58. package/dist/lib/crabbox/setup-copy.js +17 -1
  59. package/dist/lib/daemon.js +27 -1
  60. package/dist/lib/device-config.js +7 -0
  61. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  62. package/dist/lib/devices/doctor-findings.js +40 -1
  63. package/dist/lib/doctor-diff.d.ts +3 -0
  64. package/dist/lib/doctor-diff.js +15 -13
  65. package/dist/lib/event-stream.d.ts +3 -1
  66. package/dist/lib/event-stream.js +14 -1
  67. package/dist/lib/events.d.ts +9 -0
  68. package/dist/lib/events.js +58 -0
  69. package/dist/lib/exec.d.ts +15 -3
  70. package/dist/lib/exec.js +112 -19
  71. package/dist/lib/feed-outcome.d.ts +3 -0
  72. package/dist/lib/feed-outcome.js +18 -10
  73. package/dist/lib/feed.d.ts +4 -0
  74. package/dist/lib/hooks/cache.js +36 -3
  75. package/dist/lib/hooks.d.ts +19 -7
  76. package/dist/lib/hooks.js +88 -38
  77. package/dist/lib/hosts/passthrough.d.ts +21 -0
  78. package/dist/lib/hosts/passthrough.js +39 -12
  79. package/dist/lib/hosts/session-index.d.ts +4 -0
  80. package/dist/lib/hosts/session-index.js +7 -0
  81. package/dist/lib/manifest.d.ts +12 -2
  82. package/dist/lib/manifest.js +60 -5
  83. package/dist/lib/mcp.js +5 -1
  84. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  85. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
  86. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  87. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  88. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  89. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  90. package/dist/lib/menubar/install-menubar.js +128 -6
  91. package/dist/lib/menubar/snapshot.d.ts +15 -0
  92. package/dist/lib/menubar/snapshot.js +40 -0
  93. package/dist/lib/plugins.js +13 -1
  94. package/dist/lib/refresh.d.ts +5 -0
  95. package/dist/lib/refresh.js +37 -33
  96. package/dist/lib/resource-inventory.d.ts +79 -0
  97. package/dist/lib/resource-inventory.js +122 -0
  98. package/dist/lib/resources/mcp.js +3 -0
  99. package/dist/lib/resources.js +8 -5
  100. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  101. package/dist/lib/routine-process-cleanup.js +73 -0
  102. package/dist/lib/run-defaults.d.ts +2 -0
  103. package/dist/lib/run-defaults.js +23 -2
  104. package/dist/lib/runner.js +27 -22
  105. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  106. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  107. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  108. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  109. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  110. package/dist/lib/secrets/bundles.js +158 -85
  111. package/dist/lib/secrets/filestore.d.ts +7 -3
  112. package/dist/lib/secrets/filestore.js +25 -8
  113. package/dist/lib/secrets/index.js +34 -8
  114. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  115. package/dist/lib/secrets/rc-hygiene.js +14 -1
  116. package/dist/lib/secrets/reaper.d.ts +10 -0
  117. package/dist/lib/secrets/reaper.js +50 -15
  118. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  119. package/dist/lib/secrets/sync-passphrase.js +78 -0
  120. package/dist/lib/session/active.d.ts +4 -0
  121. package/dist/lib/session/db.js +8 -2
  122. package/dist/lib/session/recovery.d.ts +37 -0
  123. package/dist/lib/session/recovery.js +95 -0
  124. package/dist/lib/session/remote-list.d.ts +2 -0
  125. package/dist/lib/session/remote-list.js +1 -0
  126. package/dist/lib/session/session-cache.d.ts +4 -4
  127. package/dist/lib/session/session-cache.js +4 -4
  128. package/dist/lib/shims.d.ts +2 -2
  129. package/dist/lib/shims.js +45 -6
  130. package/dist/lib/signin-badge.js +2 -0
  131. package/dist/lib/startup/command-registry.d.ts +16 -0
  132. package/dist/lib/startup/command-registry.js +44 -0
  133. package/dist/lib/sync-umbrella.d.ts +5 -0
  134. package/dist/lib/sync-umbrella.js +5 -4
  135. package/dist/lib/teams/agents.js +1 -1
  136. package/dist/lib/teams/parsers.d.ts +1 -1
  137. package/dist/lib/tmux/session.d.ts +4 -2
  138. package/dist/lib/tmux/session.js +5 -5
  139. package/dist/lib/types.d.ts +3 -1
  140. package/dist/lib/versions.js +16 -1
  141. package/dist/lib/watchdog/service.d.ts +17 -0
  142. package/dist/lib/watchdog/service.js +39 -0
  143. package/package.json +1 -1
@@ -14,14 +14,16 @@ import { discoverSessions } from '../lib/session/discover.js';
14
14
  import { filterTeamSessions } from '../lib/session/team-filter.js';
15
15
  import { multiItemPicker, itemPicker } from '../lib/picker.js';
16
16
  import { buildPreview } from './sessions-picker.js';
17
- import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
17
+ import { formatPickerLabel, pickerColumnsFor, buildSessionRecoveryCommand, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
18
+ import { sessionMatchesQuery } from './sessions-browser.js';
18
19
  import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
19
20
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
20
21
  import { setHelpSections } from '../lib/help.js';
21
22
  import { confirm } from '@inquirer/prompts';
22
23
  import { spawn } from 'node:child_process';
23
- import { buildCanonicalResumeCommand } from '../lib/session/resume-command.js';
24
24
  import { looksLikeSessionId } from '../lib/session/discover.js';
25
+ import { machineId } from '../lib/session/sync/config.js';
26
+ import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
25
27
  /** Opening more than this many live sessions at once asks for confirmation first. */
26
28
  export const CONFIRM_THRESHOLD = 5;
27
29
  export function registerSessionsResumeCommand(sessionsCmd) {
@@ -34,7 +36,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
34
36
  .option('--teams', 'Include team-spawned sessions (hidden by default)')
35
37
  .option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
36
38
  .option('-n, --limit <n>', 'Maximum number of sessions to load into the picker', '200')
37
- .option('--host <alias>', 'Resume on a remote host over SSH (defaults to tmux there)')
39
+ .option('--host <alias>', 'Open on the session origin host over SSH; the host must match every selected session')
38
40
  .option('--iterm', 'Force the iTerm backend')
39
41
  .option('--ghostty', 'Force the Ghostty backend')
40
42
  .option('--tmux', 'Force the tmux backend')
@@ -65,8 +67,8 @@ export function registerSessionsResumeCommand(sessionsCmd) {
65
67
  - Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
66
68
  - Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
67
69
  - --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
68
- - --host <alias> resumes on a remote machine over the same SSH transport as 'sessions --host' (defaults to tmux).
69
- - Each session opens version-pinned, in its own cwd. Non-resumable agents are skipped with a note.
70
+ - --host <alias> opens the terminal surface on that host only when it is the selected sessions' origin; recovery never migrates a session to another device.
71
+ - Recovery runs on the session's origin device: exact healthy origin uses native resume; otherwise a healthy version of the same harness receives /continue <id>.
70
72
  `,
71
73
  });
72
74
  cmd.action(async (query, options) => {
@@ -110,7 +112,7 @@ async function sessionsResumeAction(query, options) {
110
112
  chosen = await multiItemPicker({
111
113
  message: 'Select sessions to resume:',
112
114
  items: sessions,
113
- filter: (q) => (q.trim() ? filterSessionsByQuery(sessions, q) : sessions),
115
+ filter: (q) => (q.trim() ? sessions.filter((s) => sessionMatchesQuery(s, q)) : sessions),
114
116
  labelFor: (s, q) => formatPickerLabel(s, q, cols),
115
117
  keyFor: (s) => s.id,
116
118
  buildPreview,
@@ -127,17 +129,25 @@ async function sessionsResumeAction(query, options) {
127
129
  }
128
130
  if (!chosen || chosen.length === 0)
129
131
  return;
130
- // 2. Split the selection into resumable surfaces and skipped agents (no silent drop).
132
+ if (options.host) {
133
+ const requestedHost = options.host;
134
+ const mismatches = chosen
135
+ .map((session) => resumeHostMismatch(session, requestedHost))
136
+ .filter((message) => message !== null);
137
+ if (mismatches.length > 0) {
138
+ for (const message of mismatches)
139
+ console.error(chalk.red(message));
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ }
144
+ // 2. Route every selection through the owning device's recovery resolver.
131
145
  const items = [];
132
146
  for (const s of chosen) {
133
- const command = buildCanonicalResumeCommand(s.id);
147
+ const command = buildSessionRecoveryCommand(s, !!options.host);
134
148
  const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
135
149
  items.push({ session: s, cwd, command });
136
150
  }
137
- if (items.length === 0) {
138
- console.log(chalk.gray('Nothing resumable in the selection.'));
139
- return;
140
- }
141
151
  // 3. Resolve the backend (and host).
142
152
  const ctx = currentContext();
143
153
  const backend = await resolveBackend(options, ctx, items.length);
@@ -221,6 +231,12 @@ async function spawnCliInPlace(args) {
221
231
  export function resolveResumePacking(options) {
222
232
  return options.splits ? 'two-per-tab' : 'tabs';
223
233
  }
234
+ export function resumeHostMismatch(session, requestedHost, self = machineId()) {
235
+ const origin = sessionOriginDevice(session, self);
236
+ return sessionRecoveryDestinationMatches(session, requestedHost, self)
237
+ ? null
238
+ : `Session ${session.shortId} originated on ${origin}; --host ${requestedHost} cannot move recovery to another device.`;
239
+ }
224
240
  /**
225
241
  * Decide which backend to launch into. Returns a concrete backend, `'inplace'`
226
242
  * (resume in the current process — no GUI/tmux available), or `'cancel'` (the
@@ -131,7 +131,7 @@ export declare function formatActiveRowDescription(s: ActiveSession): string;
131
131
  */
132
132
  export declare function indexActiveBySessionId(active: ActiveSession[]): Map<string, ActiveSession>;
133
133
  /** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
134
- type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber'>;
134
+ export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName'>;
135
135
  /**
136
136
  * Backfill display-only fields onto live rows from the indexed SessionMeta, by
137
137
  * full session id (RUSH-2205). A running process reports no agent version, and a
@@ -141,6 +141,8 @@ type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticke
141
141
  * the DB read that builds `metaById` is the caller's concern.
142
142
  */
143
143
  export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
144
+ /** Add indexed display metadata to active rows for non-renderer consumers. */
145
+ export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
144
146
  /**
145
147
  * The live decoration for a listing row: a status glyph and the latest-turn
146
148
  * preview, when the session is still running. `●` running / `◐` waiting on the
@@ -184,6 +186,16 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
184
186
  * stays excluded rather than inventing a human who can answer.
185
187
  */
186
188
  export declare function isAwaitingUser(s: ActiveSession): boolean;
189
+ /**
190
+ * Whether a row belongs in an unqualified `--active` view.
191
+ *
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
+ */
198
+ export declare function isRunningLiveSession(s: ActiveSession): boolean;
187
199
  /**
188
200
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
189
201
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
@@ -573,6 +585,13 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
573
585
  * version-pinned launcher is genuinely missing.
574
586
  */
575
587
  export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
588
+ /**
589
+ * Relaunch this CLI's one recovery path. The owning device resolves account
590
+ * health and either performs exact-home native resume or same-harness
591
+ * `/continue`; callers never guess which version home can see the transcript.
592
+ * `portable` is for an SSH/terminal-engine command that executes on a peer.
593
+ */
594
+ export declare function buildSessionRecoveryCommand(session: Pick<SessionMeta, 'id'>, portable?: boolean): string[];
576
595
  /**
577
596
  * Map a resume argv to the spawn(command, args, {shell}) triple.
578
597
  *
@@ -594,6 +613,9 @@ interface AgentFilter {
594
613
  agent?: SessionAgentId;
595
614
  version?: string;
596
615
  }
616
+ /** Resolve the harness portion of a sessions selector with the CLI's canonical
617
+ * alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
618
+ export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
597
619
  export declare function parseAgentFilter(agentName?: string): AgentFilter;
598
620
  /**
599
621
  * How a `sessions <query>` argument was resolved against the pool.
@@ -20,7 +20,7 @@ import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
20
20
  import ora from 'ora';
21
21
  import { SESSION_AGENTS } from '../lib/session/types.js';
22
22
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
23
- import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable, composeWin32CommandLine } from '../lib/platform/index.js';
23
+ import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
24
24
  import { getActiveSessions } from '../lib/session/active.js';
25
25
  import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
26
26
  import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
@@ -45,6 +45,8 @@ import { getShimsDir } from '../lib/state.js';
45
45
  import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
46
46
  import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
47
47
  import { resolveVersionAliasLoose } from '../lib/versions.js';
48
+ import { getAgentsInvocation } from '../lib/daemon.js';
49
+ import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
48
50
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
49
51
  import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
50
52
  import { setHelpSections } from '../lib/help.js';
@@ -409,6 +411,10 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
409
411
  if (!Number.isNaN(ts))
410
412
  s.startedAtMs = ts;
411
413
  }
414
+ if (!s.origin && m.origin)
415
+ s.origin = m.origin;
416
+ if (!s.routineName && m.routineName)
417
+ s.routineName = m.routineName;
412
418
  }
413
419
  }
414
420
  /**
@@ -433,6 +439,10 @@ function loadBackfillMetaFor(sessions) {
433
439
  }
434
440
  return byId;
435
441
  }
442
+ /** Add indexed display metadata to active rows for non-renderer consumers. */
443
+ export function backfillActiveRowsFromIndex(sessions) {
444
+ backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
445
+ }
436
446
  /**
437
447
  * The live decoration for a listing row: a status glyph and the latest-turn
438
448
  * preview, when the session is still running. `●` running / `◐` waiting on the
@@ -525,6 +535,20 @@ export function isAwaitingUser(s) {
525
535
  return false;
526
536
  return s.status === 'input_required' || s.activity === 'waiting_input';
527
537
  }
538
+ /**
539
+ * Whether a row belongs in an unqualified `--active` view.
540
+ *
541
+ * The live registry deliberately retains terminally-dead rows long enough for
542
+ * `--closed` / `--crashed` and recovery to find them. Presence in that registry
543
+ * therefore is not, by itself, evidence that a session is still active.
544
+ * Explicit lifecycle filters bypass this predicate and select those retained
545
+ * rows through {@link matchesLiveStatus} instead.
546
+ */
547
+ export function isRunningLiveSession(s) {
548
+ if (s.status === 'closed' || s.status === 'crashed')
549
+ return false;
550
+ return s.pidAlive !== false;
551
+ }
528
552
  /** Width of the live status column — `crashed` is the longest word it renders. */
529
553
  const LIVE_STATUS_W = 8;
530
554
  /** The colored, space-padded status cell for a listing row (empty when not live). */
@@ -1273,14 +1297,14 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
1273
1297
  // gate: exit non-zero when the union contains a session awaiting the user.
1274
1298
  const statusFiltered = opts.statuses?.length
1275
1299
  ? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
1276
- : merged;
1300
+ : merged.filter(isRunningLiveSession);
1277
1301
  const sessions = statusFiltered;
1278
1302
  // Backfill agent version + ticket/PR/label/created onto the live rows from the
1279
1303
  // historical index (RUSH-2205) — a running process reports none of these, and
1280
1304
  // an orphan row usually lacks them. Done before both the JSON and human paths
1281
1305
  // so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
1282
1306
  // transcripts sync across the fleet, so a remote row resolves from the local DB.
1283
- backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
1307
+ backfillActiveRowsFromIndex(sessions);
1284
1308
  if (asJson) {
1285
1309
  // Resolve who is watching each local tmux pane before serializing: `viewingIn`
1286
1310
  // is how a consumer distinguishes a session someone is looking at from one
@@ -3017,34 +3041,23 @@ export async function resumeSessionInPlace(session) {
3017
3041
  const cwd = session.cwd && fs.existsSync(session.cwd)
3018
3042
  ? session.cwd
3019
3043
  : process.cwd();
3020
- const resume = buildResumeCommand(session);
3021
- if (!resume) {
3022
- console.log(chalk.yellow(`Resume is not supported for ${session.agent} sessions yet. Showing summary instead.`));
3023
- await renderSession(session, 'summary', {});
3024
- return;
3025
- }
3044
+ const resume = buildSessionRecoveryCommand(session);
3026
3045
  console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
3027
- // Resolve the (possibly version-pinned) launcher up front. On Windows the
3028
- // agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
3029
- // spawnResumeCommand), a missing command exits non-zero rather than emitting
3030
- // an ENOENT `error` event — so detect a removed version here instead of
3031
- // relying on that event, keeping the fallback working on every OS.
3032
- // `resume[0]` is an absolute alias path when one exists on disk, so only a bare
3033
- // name still needs a PATH lookup. Checking existsSync first is what keeps an
3034
- // isolated install (shims deliberately off PATH) out of the fallback.
3035
- const launcherFound = path.isAbsolute(resume[0])
3036
- ? fs.existsSync(resume[0])
3037
- : !!findExecutable(resume[0]);
3038
- if (!launcherFound && session.version) {
3039
- const fallback = buildFallbackCommand(session);
3040
- if (fallback) {
3041
- console.log(chalk.gray(`Version ${session.version} is not installed. Resuming with the current version instead...`));
3042
- await spawnResumeCommand(fallback, cwd);
3043
- return;
3044
- }
3045
- }
3046
3046
  await spawnResumeCommand(resume, cwd);
3047
3047
  }
3048
+ /**
3049
+ * Relaunch this CLI's one recovery path. The owning device resolves account
3050
+ * health and either performs exact-home native resume or same-harness
3051
+ * `/continue`; callers never guess which version home can see the transcript.
3052
+ * `portable` is for an SSH/terminal-engine command that executes on a peer.
3053
+ */
3054
+ export function buildSessionRecoveryCommand(session, portable = false) {
3055
+ const args = sessionRecoveryRunArgs(session);
3056
+ if (portable)
3057
+ return ['agents', ...args];
3058
+ const invocation = getAgentsInvocation(args);
3059
+ return [invocation.command, ...invocation.args];
3060
+ }
3048
3061
  /**
3049
3062
  * Map a resume argv to the spawn(command, args, {shell}) triple.
3050
3063
  *
@@ -3112,8 +3125,6 @@ function spawnResumeCommand(cmd, cwd) {
3112
3125
  * isolated HOME where the JSONL was written — regardless of which version is
3113
3126
  * currently the default. Falls back to the bare shim when version is unknown.
3114
3127
  *
3115
- * If the versioned binary is missing (version was removed), the ENOENT
3116
- * handler in handlePickedSession retries via buildFallbackCommand.
3117
3128
  */
3118
3129
  /**
3119
3130
  * The agent's own resume invocation, given whichever launcher we resolved.
@@ -3180,19 +3191,6 @@ export function buildResumeCommand(session) {
3180
3191
  return null;
3181
3192
  }
3182
3193
  }
3183
- /**
3184
- * Fallback when the pinned version really is gone: the same resume invocation
3185
- * against the current version.
3186
- *
3187
- * This used to spawn `<cli> "/continue <id>"`, feeding a slash command into the
3188
- * TUI as a prompt. Neither CLI has `/continue` — codex documents `/resume` — so
3189
- * the agent received an unrecognised command and the session was not resumed at
3190
- * all. Reusing resumeArgv keeps the two paths from drifting apart again.
3191
- */
3192
- function buildFallbackCommand(session) {
3193
- const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
3194
- return resumeArgv(session.agent, session.id, cli);
3195
- }
3196
3194
  // ---------------------------------------------------------------------------
3197
3195
  // Cloud session source (--cloud)
3198
3196
  // ---------------------------------------------------------------------------
@@ -3263,25 +3261,24 @@ async function runCloudSessions(query, options) {
3263
3261
  // Ensure the SessionMeta points at the local cache path for renderSession.
3264
3262
  await renderSession({ ...meta, filePath: cachedPath }, mode, filterOpts, options);
3265
3263
  }
3264
+ /** Resolve the harness portion of a sessions selector with the CLI's canonical
3265
+ * alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
3266
+ export function resolveSessionAgentName(name) {
3267
+ const normalized = name.toLowerCase();
3268
+ if (SESSION_AGENTS.includes(normalized)) {
3269
+ return normalized;
3270
+ }
3271
+ const resolved = resolveAgentName(normalized);
3272
+ if (resolved && SESSION_AGENTS.includes(resolved)) {
3273
+ return resolved;
3274
+ }
3275
+ return fuzzyMatch(normalized, SESSION_AGENTS, FUZZY_PRESETS.agents);
3276
+ }
3266
3277
  export function parseAgentFilter(agentName) {
3267
3278
  if (!agentName)
3268
3279
  return {};
3269
3280
  const [name, version] = agentName.split('@', 2);
3270
- let agent = SESSION_AGENTS.includes(name)
3271
- ? name
3272
- : null;
3273
- if (!agent) {
3274
- // Aliases and single-typo corrections (cladue -> claude). SESSION_AGENTS
3275
- // includes ids (rush, hermes) that resolveAgentName doesn't know, so fall
3276
- // back to fuzzy-matching the session list directly.
3277
- const resolved = resolveAgentName(name);
3278
- if (resolved && SESSION_AGENTS.includes(resolved)) {
3279
- agent = resolved;
3280
- }
3281
- else {
3282
- agent = fuzzyMatch(name, SESSION_AGENTS, FUZZY_PRESETS.agents);
3283
- }
3284
- }
3281
+ const agent = resolveSessionAgentName(name);
3285
3282
  if (!agent) {
3286
3283
  console.error(chalk.red(`Unknown agent: ${name}. Use: ${SESSION_AGENTS.join(', ')}`));
3287
3284
  process.exit(1);
@@ -3930,7 +3927,7 @@ export function registerSessionsCommands(program) {
3930
3927
  .option('--opencode', 'Shorthand for --agent opencode')
3931
3928
  .option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
3932
3929
  .option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
3933
- .option('--teams', 'Include team-spawned sessions (hidden by default)')
3930
+ .option('--team, --teams', 'Include team-spawned sessions (hidden by default)')
3934
3931
  .option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
3935
3932
  .option('--routine', 'Show only sessions archived from routine runs')
3936
3933
  .option('-p, --project <name>', 'Filter by project name (searches across all directories)')
@@ -3997,7 +3994,7 @@ export function registerSessionsCommands(program) {
3997
3994
  agents sessions --crashed
3998
3995
 
3999
3996
  # --- Session lifecycle (one verb per intent) ---
4000
- # Jump to a live session (attach its terminal, or open a tab + resume)
3997
+ # Focus a session (attach its living terminal, or recover an ended one)
4001
3998
  agents sessions focus a1b2c3d4
4002
3999
  # Attach only — never fork a copy (old: sessions go)
4003
4000
  agents sessions focus a1b2c3d4 --attach-only
@@ -4049,7 +4046,7 @@ export function registerSessionsCommands(program) {
4049
4046
  `,
4050
4047
  notes: `
4051
4048
  Session lifecycle (pick one verb — they are not synonyms):
4052
- focus [id] jump to a live session (attach, or open tab + resume)
4049
+ focus [selector] attach a living pane, or recover on the origin device
4053
4050
  focus [id] --attach-only attach only; never fork (replaces sessions go)
4054
4051
  detach <id> interactive → headless continuation
4055
4052
  attach <id> headless → interactive in this terminal
@@ -66,7 +66,7 @@ function printBackendNotes(backend) {
66
66
  console.log(chalk.gray('backend: keychain — macOS reads may ask for Touch ID or the device password.'));
67
67
  }
68
68
  else if (backend === 'file') {
69
- console.log(chalk.gray('backend: file — set AGENTS_SECRETS_PASSPHRASE for headless encrypted-file reads.'));
69
+ console.log(chalk.gray('backend: file — encrypted at rest; headless reads need no passphrase by default, via a machine-local key at ~/.agents/.secrets-key/passphrase.'));
70
70
  }
71
71
  else {
72
72
  console.log(chalk.gray('backend: vault — synced ~/.agents/vault.age storage; unlock it with agents login.'));
@@ -3,17 +3,12 @@ import { spawnSync } from 'node:child_process';
3
3
  import { getCliLaunch } from '../lib/cli-entry.js';
4
4
  import { loadDevices } from '../lib/devices/registry.js';
5
5
  import { machineId, normalizeHost } from '../lib/machine-id.js';
6
- import { devicesWithRoutineEnabled } from '../lib/routine-activation.js';
7
- import { readJob, setJobEnabled } from '../lib/routines.js';
6
+ import { getConfigValue, setConfigValue } from '../lib/device-config.js';
8
7
  import { isPromptCancelled, requireInteractiveSelection } from './utils.js';
9
- const ROUTINE = 'watchdog';
10
8
  export async function runWatchdogSetupWizard() {
11
- if (!readJob(ROUTINE)) {
12
- throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
13
- }
14
9
  const registry = await loadDevices();
15
10
  const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
16
- const enabled = new Set(devicesWithRoutineEnabled(ROUTINE).map(normalizeHost));
11
+ const enabled = new Set(devices.filter((device) => getConfigValue('watchdog.enabled', { device }).value === true));
17
12
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
18
13
  requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --host <device>']);
19
14
  }
@@ -27,10 +22,10 @@ export async function runWatchdogSetupWizard() {
27
22
  for (const device of devices) {
28
23
  const on = selectedSet.has(device);
29
24
  if (device === normalizeHost(machineId())) {
30
- setJobEnabled(ROUTINE, on);
25
+ setConfigValue('watchdog.enabled', on);
31
26
  continue;
32
27
  }
33
- const launch = getCliLaunch(['routines', on ? 'resume' : 'pause', ROUTINE, '--host', device]);
28
+ const launch = getCliLaunch(['watchdog', on ? 'on' : 'off', '--host', device]);
34
29
  const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
35
30
  if ((result.status ?? 1) !== 0)
36
31
  throw new Error(`Could not configure watchdog on ${device}`);
@@ -49,6 +44,6 @@ export async function runWatchdogSetupWizard() {
49
44
  }
50
45
  export function registerSetupWatchdogCommand(setup) {
51
46
  setup.command('watchdog')
52
- .description('Choose the devices where the built-in watchdog routine runs.')
47
+ .description('Choose the devices where the daemon watchdog pass runs.')
53
48
  .action(runWatchdogSetupWizard);
54
49
  }
@@ -319,7 +319,7 @@ export function registerSetupCommand(program) {
319
319
  agents setup share # provision or join a Cloudflare share endpoint
320
320
  agents setup secrets # choose secrets backend/policy defaults + import
321
321
  agents setup fleet # discover Tailscale devices + configure SSH access
322
- agents setup watchdog # choose which devices run the watchdog routine
322
+ agents setup watchdog # choose which devices run the daemon watchdog pass
323
323
 
324
324
  To install CLIs from agents.yaml and sync resources into version homes:
325
325
  agents sync --local -y