@phnx-labs/agents-cli 1.22.24 → 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 (85) hide show
  1. package/CHANGELOG.md +167 -1
  2. package/README.md +14 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +9 -5
  5. package/dist/commands/doctor.d.ts +24 -0
  6. package/dist/commands/doctor.js +100 -9
  7. package/dist/commands/exec.js +19 -16
  8. package/dist/commands/feed.d.ts +5 -0
  9. package/dist/commands/feed.js +21 -1
  10. package/dist/commands/focus.js +2 -2
  11. package/dist/commands/menubar.js +8 -0
  12. package/dist/commands/routines.d.ts +3 -0
  13. package/dist/commands/routines.js +70 -47
  14. package/dist/commands/run-cloud.js +1 -1
  15. package/dist/commands/sessions-browser.d.ts +1 -1
  16. package/dist/commands/sessions-browser.js +27 -7
  17. package/dist/commands/sessions-resume.js +3 -2
  18. package/dist/commands/sessions.d.ts +13 -1
  19. package/dist/commands/sessions.js +24 -2
  20. package/dist/commands/setup-watchdog.js +5 -10
  21. package/dist/commands/setup.js +1 -1
  22. package/dist/commands/teams.js +3 -2
  23. package/dist/commands/watchdog.d.ts +3 -4
  24. package/dist/commands/watchdog.js +26 -66
  25. package/dist/index.js +36 -1
  26. package/dist/lib/agents.js +126 -21
  27. package/dist/lib/cloud/cursor.d.ts +79 -0
  28. package/dist/lib/cloud/cursor.js +228 -0
  29. package/dist/lib/cloud/registry.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +7 -2
  31. package/dist/lib/cloud/types.js +14 -0
  32. package/dist/lib/crabbox/cli.d.ts +2 -2
  33. package/dist/lib/crabbox/config.d.ts +7 -8
  34. package/dist/lib/crabbox/config.js +14 -14
  35. package/dist/lib/crabbox/lease.d.ts +11 -4
  36. package/dist/lib/crabbox/lease.js +40 -8
  37. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  38. package/dist/lib/crabbox/setup-copy.js +17 -1
  39. package/dist/lib/daemon.js +27 -1
  40. package/dist/lib/device-config.js +7 -0
  41. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  42. package/dist/lib/devices/doctor-findings.js +40 -1
  43. package/dist/lib/events.d.ts +9 -0
  44. package/dist/lib/events.js +58 -0
  45. package/dist/lib/exec.d.ts +3 -3
  46. package/dist/lib/exec.js +24 -10
  47. package/dist/lib/feed-outcome.d.ts +3 -0
  48. package/dist/lib/feed-outcome.js +18 -10
  49. package/dist/lib/feed.d.ts +4 -0
  50. package/dist/lib/hosts/passthrough.d.ts +21 -0
  51. package/dist/lib/hosts/passthrough.js +39 -12
  52. package/dist/lib/mcp.js +5 -1
  53. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  54. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  55. package/dist/lib/menubar/snapshot.d.ts +15 -0
  56. package/dist/lib/menubar/snapshot.js +40 -0
  57. package/dist/lib/plugins.js +13 -1
  58. package/dist/lib/resources/mcp.js +3 -0
  59. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  60. package/dist/lib/routine-process-cleanup.js +73 -0
  61. package/dist/lib/runner.js +5 -5
  62. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  63. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  64. package/dist/lib/secrets/bundles.js +1 -20
  65. package/dist/lib/secrets/filestore.d.ts +2 -0
  66. package/dist/lib/secrets/filestore.js +13 -0
  67. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  68. package/dist/lib/secrets/rc-hygiene.js +14 -1
  69. package/dist/lib/session/active.d.ts +4 -0
  70. package/dist/lib/session/db.js +8 -2
  71. package/dist/lib/session/remote-list.d.ts +2 -0
  72. package/dist/lib/session/remote-list.js +1 -0
  73. package/dist/lib/session/session-cache.d.ts +4 -4
  74. package/dist/lib/session/session-cache.js +4 -4
  75. package/dist/lib/shims.js +21 -1
  76. package/dist/lib/signin-badge.js +2 -0
  77. package/dist/lib/startup/command-registry.d.ts +15 -0
  78. package/dist/lib/startup/command-registry.js +42 -0
  79. package/dist/lib/teams/agents.js +1 -1
  80. package/dist/lib/teams/parsers.d.ts +1 -1
  81. package/dist/lib/types.d.ts +1 -1
  82. package/dist/lib/versions.js +16 -1
  83. package/dist/lib/watchdog/service.d.ts +17 -0
  84. package/dist/lib/watchdog/service.js +39 -0
  85. package/package.json +1 -1
@@ -16,6 +16,7 @@ import { notifyUrgentBlock } from '../lib/notify.js';
16
16
  import { gcMailbox } from '../lib/mailbox-gc.js';
17
17
  import { isValidMailboxId } from '../lib/mailbox.js';
18
18
  import { getActiveSessions } from '../lib/session/active.js';
19
+ import { backfillActiveRowsFromMeta } from './sessions.js';
19
20
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
20
21
  import { GLYPH, masthead } from '../lib/comms-render.js';
21
22
  import { discoverSessions } from '../lib/session/discover.js';
@@ -154,7 +155,7 @@ export function prepareLocalFeedBlocks(localBlocks, opts) {
154
155
  }
155
156
  function renderBlock(b, localHost, indent = '') {
156
157
  const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
157
- const runtime = chalk.gray(b.runtime);
158
+ const runtime = chalk.gray(formatFeedRuntime(b));
158
159
  const age = chalk.gray(relTime(b.ts));
159
160
  const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
160
161
  const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
@@ -218,6 +219,21 @@ function renderBlock(b, localHost, indent = '') {
218
219
  }
219
220
  console.log();
220
221
  }
222
+ /** Specific host app + routine provenance for one feed row. */
223
+ export function formatFeedRuntime(block) {
224
+ const host = {
225
+ ghostty: 'Ghostty',
226
+ iterm: 'iTerm',
227
+ terminal: 'terminal',
228
+ kitty: 'Kitty',
229
+ codium: 'VSCodium',
230
+ code: 'VS Code',
231
+ cursor: 'Cursor',
232
+ }[block.runtime.toLowerCase()] ?? block.runtime;
233
+ if (block.origin !== 'routine')
234
+ return host;
235
+ return `${host} · routine${block.routineName ? `:${block.routineName}` : ''}`;
236
+ }
221
237
  /** Human summary line for one outcome rollup. */
222
238
  export function formatOutcomeHeader(group) {
223
239
  const { agents, open, answered, parked } = group.counts;
@@ -251,6 +267,9 @@ export function sessionHintsFromActive(sessions) {
251
267
  prUrl: s.pr?.url,
252
268
  worktreeSlug: s.worktree?.slug,
253
269
  project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
270
+ host: s.host,
271
+ origin: s.origin,
272
+ routineName: s.routineName,
254
273
  }));
255
274
  }
256
275
  /**
@@ -489,6 +508,7 @@ export function registerFeedCommand(program) {
489
508
  console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
490
509
  }
491
510
  sessionMetas = loaded.metas;
511
+ backfillActiveRowsFromMeta(sessions, new Map(sessionMetas.map((meta) => [meta.id, meta])));
492
512
  }
493
513
  const localSignals = buildSessionSignals(sessions, sessionMetas);
494
514
  if (opts.pause || opts.kill) {
@@ -16,7 +16,7 @@ import fs from 'node:fs';
16
16
  import chalk from 'chalk';
17
17
  import { confirm } from '@inquirer/prompts';
18
18
  import { gatherLiveTargets, pickLiveTarget, pickLiveTargets, jumpTo, probeAttachRail, refuseFallback } from './go.js';
19
- import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
19
+ import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, isRunningLiveSession, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
20
20
  import { resolveBackend, CONFIRM_THRESHOLD } from './sessions-resume.js';
21
21
  import { runOnPeer } from '../lib/session/remote-list.js';
22
22
  import { discoverSessions } from '../lib/session/discover.js';
@@ -276,7 +276,7 @@ export async function focusAction(id, opts) {
276
276
  }
277
277
  /** A retained pane is not attachable merely because tmux can still display it. */
278
278
  export function isAttachableLiveSession(session) {
279
- return session.pidAlive !== false && session.status !== 'closed' && session.status !== 'crashed';
279
+ return isRunningLiveSession(session);
280
280
  }
281
281
  const FOCUS_AGENT_SHORTHANDS = ['claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode'];
282
282
  function focusOptionAgent(opts) {
@@ -137,6 +137,14 @@ export function registerMenubarCommands(program) {
137
137
  again, run \`agents menubar disable\`.
138
138
  `,
139
139
  });
140
+ menubar
141
+ .command('snapshot', { hidden: true })
142
+ .description('Emit the consolidated AGI Menu polling snapshot.')
143
+ .option('--json', 'Emit machine-readable JSON')
144
+ .action(async () => {
145
+ const { computeMenubarSnapshot } = await import('../lib/menubar/snapshot.js');
146
+ process.stdout.write(`${JSON.stringify(await computeMenubarSnapshot())}\n`);
147
+ });
140
148
  menubar
141
149
  .command('enable')
142
150
  .description('Install and start AGI Menu (launches at login)')
@@ -46,5 +46,8 @@ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: De
46
46
  */
47
47
  export declare function groupRoutineJobsByProject(jobs: JobConfig[], knownProjectNames: Set<string>): RoutineListGroup[];
48
48
  export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
49
+ /** Build the exact structured routine rows shared by `routines list --json`
50
+ * and the one-process AGI Menu snapshot. */
51
+ export declare function buildRoutineListJson(): Record<string, unknown>[];
49
52
  /** Register the `agents routines` command tree. */
50
53
  export declare function registerRoutinesCommands(program: Command): void;
@@ -405,6 +405,72 @@ function runMetaJson(run) {
405
405
  export function buildRunsJson(runs) {
406
406
  return runs.map(runMetaJson);
407
407
  }
408
+ /** Build the exact structured routine rows shared by `routines list --json`
409
+ * and the one-process AGI Menu snapshot. */
410
+ export function buildRoutineListJson() {
411
+ try {
412
+ monitorRunningJobs();
413
+ }
414
+ catch { /* best-effort orphan reap */ }
415
+ const jobs = listAllJobs(process.cwd());
416
+ if (jobs.length === 0)
417
+ return [];
418
+ const scheduler = new JobScheduler(async () => { });
419
+ scheduler.loadAll();
420
+ try {
421
+ const overdueSet = new Set();
422
+ try {
423
+ for (const job of detectOverdueJobs())
424
+ overdueSet.add(job.name);
425
+ }
426
+ catch {
427
+ // Best-effort indicator; never block the list on detection errors.
428
+ }
429
+ const now = new Date();
430
+ const knownProjectNames = new Set(listProjectDefs().map((project) => project.name));
431
+ return jobs.map((job) => {
432
+ const latestRun = localLatestRun(job);
433
+ const enabledDevices = devicesWithRoutineEnabled(job.name);
434
+ return {
435
+ name: job.name,
436
+ agent: job.agent ?? null,
437
+ workflow: job.workflow ?? null,
438
+ command: job.command ?? null,
439
+ repo: job.repo ?? null,
440
+ schedule: job.schedule ?? null,
441
+ scheduleHuman: fireConditionLabel(job),
442
+ trigger: job.trigger ?? null,
443
+ timezone: job.timezone ?? null,
444
+ devices: enabledDevices,
445
+ enabledDevices,
446
+ host: job.host ?? null,
447
+ hostStrategy: resolveHostStrategy(job),
448
+ source: job.source ?? null,
449
+ sourceRepo: job.source?.repo ?? job.repo ?? null,
450
+ sourceBranch: job.source?.branch ?? null,
451
+ runOnce: Boolean(job.runOnce),
452
+ catchup: job.catchup !== false,
453
+ oneShot: isOneShotRoutine(job),
454
+ expired: isPastOneShotRoutine(job, now),
455
+ runsHere: jobRunsOnThisDevice(job),
456
+ enabled: job.enabled,
457
+ overdue: overdueSet.has(job.name),
458
+ nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
459
+ nextRunHuman: nextRunLabel(job, scheduler, now),
460
+ lastStatus: latestRun?.status ?? null,
461
+ exitCode: latestRun?.exitCode ?? null,
462
+ failureReason: latestRun?.errorMessage ?? null,
463
+ lastRunStartedAt: latestRun?.startedAt ?? null,
464
+ lastRunCompletedAt: latestRun?.completedAt ?? null,
465
+ projects: job.projects ?? [],
466
+ projectGroup: computeProjectGroup(job.projects, knownProjectNames),
467
+ };
468
+ });
469
+ }
470
+ finally {
471
+ scheduler.stopAll();
472
+ }
473
+ }
408
474
  /** Detect Ctrl+C or premature stream close during an interactive prompt. */
409
475
  function isPromptCancelled(err) {
410
476
  return (err instanceof Error &&
@@ -554,6 +620,10 @@ export function registerRoutinesCommands(program) {
554
620
  console.error(chalk.red(`Unsupported --group-by '${options.groupBy}'. Use: project (default) or device`));
555
621
  process.exit(1);
556
622
  }
623
+ if (options.json) {
624
+ process.stdout.write(JSON.stringify(buildRoutineListJson()) + '\n');
625
+ return;
626
+ }
557
627
  try {
558
628
  monitorRunningJobs();
559
629
  }
@@ -579,53 +649,6 @@ export function registerRoutinesCommands(program) {
579
649
  catch {
580
650
  // Best-effort indicator; never block the list on detection errors.
581
651
  }
582
- // Machine-readable path: same data the table renders, but structured.
583
- // The menu bar helper relies on this so it never reimplements cron math.
584
- if (options.json) {
585
- const nowJson = new Date();
586
- const knownProjectNames = new Set(listProjectDefs().map((p) => p.name));
587
- const payload = jobs.map((job) => {
588
- const latestRun = localLatestRun(job);
589
- const enabledDevices = devicesWithRoutineEnabled(job.name);
590
- return {
591
- name: job.name,
592
- agent: job.agent ?? null,
593
- workflow: job.workflow ?? null,
594
- command: job.command ?? null,
595
- repo: job.repo ?? null,
596
- schedule: job.schedule ?? null,
597
- scheduleHuman: fireConditionLabel(job),
598
- trigger: job.trigger ?? null,
599
- timezone: job.timezone ?? null,
600
- devices: enabledDevices,
601
- enabledDevices,
602
- host: job.host ?? null,
603
- hostStrategy: resolveHostStrategy(job),
604
- source: job.source ?? null,
605
- sourceRepo: job.source?.repo ?? job.repo ?? null,
606
- sourceBranch: job.source?.branch ?? null,
607
- runOnce: Boolean(job.runOnce),
608
- catchup: job.catchup !== false,
609
- oneShot: isOneShotRoutine(job),
610
- expired: isPastOneShotRoutine(job, nowJson),
611
- runsHere: jobRunsOnThisDevice(job),
612
- enabled: job.enabled,
613
- overdue: overdueSet.has(job.name),
614
- nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
615
- nextRunHuman: nextRunLabel(job, scheduler, nowJson),
616
- lastStatus: latestRun?.status ?? null,
617
- exitCode: latestRun?.exitCode ?? null,
618
- failureReason: latestRun?.errorMessage ?? null,
619
- lastRunStartedAt: latestRun?.startedAt ?? null,
620
- lastRunCompletedAt: latestRun?.completedAt ?? null,
621
- projects: job.projects ?? [],
622
- projectGroup: computeProjectGroup(job.projects, knownProjectNames),
623
- };
624
- });
625
- scheduler.stopAll();
626
- process.stdout.write(JSON.stringify(payload) + '\n');
627
- return;
628
- }
629
652
  console.log(chalk.bold('Scheduled Jobs\n'));
630
653
  // OSC 8 hyperlink helper — renders as a clickable link in supporting terminals.
631
654
  // Guarded on process.stdout.isTTY so that piped/redirected output never
@@ -91,7 +91,7 @@ export function resolveRunCloudProvider(agentId, explicitProvider) {
91
91
  return resolveProvider(explicitProvider);
92
92
  if (!nativeProviderForAgent(agentId)) {
93
93
  throw new RunCloudError(`${agentId} has no native cloud. Cloud-capable agents: ${cloudCapableAgentIds().join(', ')}. ` +
94
- `Override with --provider <id> (rush | codex | factory | antigravity | host).`);
94
+ `Override with --provider <id> (rush | codex | cursor | factory | antigravity | host).`);
95
95
  }
96
96
  return resolveProvider(undefined, agentId);
97
97
  }
@@ -147,7 +147,7 @@ export declare function liveSessionToMeta(a: ActiveSession, self: string): Sessi
147
147
  * `--active --json` listed it. Live sessions the pool lacks are appended as their
148
148
  * own rows, then the whole set is grouped local-machine-first.
149
149
  */
150
- export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string, ActiveSession>, self: string): SessionMeta[];
150
+ export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string, ActiveSession>, self: string, includeUnindexed?: boolean): SessionMeta[];
151
151
  /**
152
152
  * Whether to render the host-program column. It belongs to the running view
153
153
  * only, so this gates on the FILTER — not on the live index being populated.
@@ -22,7 +22,7 @@ import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
22
22
  import { listFavorites, toggleFavorite } from '../lib/session/favorites.js';
23
23
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
24
24
  import { buildPreview } from './sessions-picker.js';
25
- import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
25
+ import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, isRunningLiveSession, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
26
26
  /**
27
27
  * Complete a seed into the filter the picker actually runs on.
28
28
  *
@@ -73,6 +73,8 @@ function poolCacheKey(f) {
73
73
  }
74
74
  /** Pool size when a team filter is active; one team's rows can sit anywhere. */
75
75
  const WHOLE_TEAM_POOL_LIMIT = 5000;
76
+ /** A cold peer may need to resolve an installed-version alias and scan its index. */
77
+ const BROWSER_PEER_TIMEOUT_MS = 30_000;
76
78
  /** Ordered window cycle for the `W` key. `undefined` = all time. */
77
79
  const WINDOW_CYCLE = [undefined, '1d', '7d', '30d'];
78
80
  /** Return the next value in `[undefined, ...options]`, wrapping. */
@@ -309,7 +311,9 @@ async function fetchRawPool(f, self, local, hosts, fixedFilters = false) {
309
311
  // --in-team itself, which a peer on an older build would reject as an
310
312
  // unknown option and fail the whole fan-out.
311
313
  const forwarded = remotePoolArgs(f, fixedFilters);
312
- const remoteResult = await gatherRemoteList(forwarded, remoteHosts);
314
+ const remoteResult = await gatherRemoteList(forwarded, remoteHosts, {
315
+ timeoutMs: BROWSER_PEER_TIMEOUT_MS,
316
+ });
313
317
  unreachable = remoteResult.unreachable;
314
318
  if (remoteResult.sessions.length > 0)
315
319
  rows = mergeLocalFirst([...rows, ...remoteResult.sessions], self);
@@ -423,7 +427,12 @@ export function liveSessionToMeta(a, self) {
423
427
  * `--active --json` listed it. Live sessions the pool lacks are appended as their
424
428
  * own rows, then the whole set is grouped local-machine-first.
425
429
  */
426
- export function mergeLiveIntoPool(rows, live, self) {
430
+ export function mergeLiveIntoPool(rows, live, self, includeUnindexed = true) {
431
+ // A latest/oldest selector is resolved by the peer's transcript query. A live
432
+ // row absent from that result has no proof it belongs to the resolved version,
433
+ // so callers using an alias fail closed instead of widening the selector.
434
+ if (!includeUnindexed)
435
+ return rows;
427
436
  const known = new Set(rows.map((r) => r.id));
428
437
  const extra = [];
429
438
  for (const [key, a] of live) {
@@ -483,8 +492,15 @@ export function applyFilters(rows, live, f, self, favorites) {
483
492
  const cwd = process.cwd();
484
493
  out = out.filter((r) => !!r.cwd && (r.cwd === cwd || r.cwd.startsWith(cwd + '/')));
485
494
  }
486
- if (f.running)
487
- out = out.filter((r) => live.has(r.id));
495
+ // The live registry retains dead rows so explicit --closed/--crashed filters
496
+ // can recover them. Bare --active means currently active; a lifecycle filter
497
+ // intentionally replaces that default predicate with its exact status union.
498
+ if (f.running && f.statuses.length === 0) {
499
+ out = out.filter((r) => {
500
+ const active = live.get(r.id);
501
+ return !!active && isRunningLiveSession(active);
502
+ });
503
+ }
488
504
  if (f.statuses.length > 0) {
489
505
  out = out.filter((r) => {
490
506
  const active = live.get(r.id);
@@ -507,8 +523,9 @@ export async function collectSessionCandidates(initial, opts = {}) {
507
523
  const { sessions } = await gatherActiveSessions({ local: opts.local ?? false, hosts });
508
524
  liveById = indexLiveRows(sessions, self);
509
525
  }
526
+ const includeUnindexedLive = !filter.agent?.match(/@(latest|oldest)$/);
510
527
  const rows = filter.running || opts.includeLive
511
- ? mergeLiveIntoPool(pool.rows, liveById, self)
528
+ ? mergeLiveIntoPool(pool.rows, liveById, self, includeUnindexedLive)
512
529
  : pool.rows;
513
530
  const sessions = applyFilters(rows, liveById, filter, self, listFavorites());
514
531
  return { sessions, liveById, self, unreachable: pool.unreachable };
@@ -618,7 +635,10 @@ export async function runSessionBrowser(initial = {}, opts = {}) {
618
635
  liveCache = live;
619
636
  // Live sessions the transcript pool lacks become rows of their own, so the
620
637
  // running view lists every active session, not just the ones already indexed.
621
- const rows = f.running && live ? mergeLiveIntoPool(pool.rows, live, self) : pool.rows;
638
+ const includeUnindexedLive = !f.agent?.match(/@(latest|oldest)$/);
639
+ const rows = f.running && live
640
+ ? mergeLiveIntoPool(pool.rows, live, self, includeUnindexedLive)
641
+ : pool.rows;
622
642
  agentsInPool = distinct(rows.map((r) => r.agent));
623
643
  devicesInPool = distinct(rows.map((r) => r.machine ?? self));
624
644
  // Both ends of the lineage seed the cycle: teams a row spawned, and teams a
@@ -14,7 +14,8 @@ 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, buildSessionRecoveryCommand, 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';
@@ -111,7 +112,7 @@ async function sessionsResumeAction(query, options) {
111
112
  chosen = await multiItemPicker({
112
113
  message: 'Select sessions to resume:',
113
114
  items: sessions,
114
- filter: (q) => (q.trim() ? filterSessionsByQuery(sessions, q) : sessions),
115
+ filter: (q) => (q.trim() ? sessions.filter((s) => sessionMatchesQuery(s, q)) : sessions),
115
116
  labelFor: (s, q) => formatPickerLabel(s, q, cols),
116
117
  keyFor: (s) => s.id,
117
118
  buildPreview,
@@ -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
@@ -411,6 +411,10 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
411
411
  if (!Number.isNaN(ts))
412
412
  s.startedAtMs = ts;
413
413
  }
414
+ if (!s.origin && m.origin)
415
+ s.origin = m.origin;
416
+ if (!s.routineName && m.routineName)
417
+ s.routineName = m.routineName;
414
418
  }
415
419
  }
416
420
  /**
@@ -435,6 +439,10 @@ function loadBackfillMetaFor(sessions) {
435
439
  }
436
440
  return byId;
437
441
  }
442
+ /** Add indexed display metadata to active rows for non-renderer consumers. */
443
+ export function backfillActiveRowsFromIndex(sessions) {
444
+ backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
445
+ }
438
446
  /**
439
447
  * The live decoration for a listing row: a status glyph and the latest-turn
440
448
  * preview, when the session is still running. `●` running / `◐` waiting on the
@@ -527,6 +535,20 @@ export function isAwaitingUser(s) {
527
535
  return false;
528
536
  return s.status === 'input_required' || s.activity === 'waiting_input';
529
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
+ }
530
552
  /** Width of the live status column — `crashed` is the longest word it renders. */
531
553
  const LIVE_STATUS_W = 8;
532
554
  /** The colored, space-padded status cell for a listing row (empty when not live). */
@@ -1275,14 +1297,14 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
1275
1297
  // gate: exit non-zero when the union contains a session awaiting the user.
1276
1298
  const statusFiltered = opts.statuses?.length
1277
1299
  ? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
1278
- : merged;
1300
+ : merged.filter(isRunningLiveSession);
1279
1301
  const sessions = statusFiltered;
1280
1302
  // Backfill agent version + ticket/PR/label/created onto the live rows from the
1281
1303
  // historical index (RUSH-2205) — a running process reports none of these, and
1282
1304
  // an orphan row usually lacks them. Done before both the JSON and human paths
1283
1305
  // so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
1284
1306
  // transcripts sync across the fleet, so a remote row resolves from the local DB.
1285
- backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
1307
+ backfillActiveRowsFromIndex(sessions);
1286
1308
  if (asJson) {
1287
1309
  // Resolve who is watching each local tmux pane before serializing: `viewingIn`
1288
1310
  // is how a consumer distinguishes a session someone is looking at from one
@@ -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
@@ -47,8 +47,9 @@ const AGENT_NAMES = {
47
47
  antigravity: 'Antigravity',
48
48
  kimi: 'Kimi',
49
49
  droid: 'Droid',
50
+ warp: 'Warp',
50
51
  };
51
- const VALID_AGENTS = ['claude', 'codex', 'cursor', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
52
+ const VALID_AGENTS = ['claude', 'codex', 'cursor', 'opencode', 'grok', 'antigravity', 'kimi', 'droid', 'warp'];
52
53
  // 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
53
54
  const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
54
55
  const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
@@ -1279,7 +1280,7 @@ export function registerTeamsCommands(program) {
1279
1280
  .alias('a')
1280
1281
  .description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
1281
1282
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
1282
- .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/droid/opencode; kimi/grok/cursor/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
1283
+ .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
1283
1284
  .option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
1284
1285
  .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
1285
1286
  .option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
@@ -10,12 +10,11 @@
10
10
  * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
11
  * agents watchdog --watch manual poll loop (dry unless --nudge)
12
12
  * agents watchdog --json machine-readable tick output (for the menu-bar)
13
- * agents watchdog on|off turn the always-on watchdog routine on/off
13
+ * agents watchdog on|off turn the device-local daemon pass on/off
14
14
  * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
15
  *
16
- * The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
17
- * hand-rolled loop. Its immutable definition comes from the system DotAgents
18
- * repo; on/off only changes this device's routine membership.
16
+ * The agents daemon is the sole automatic watchdog scheduler. The menu bar reads
17
+ * persisted state; it never runs a tick.
19
18
  */
20
19
  import type { Command } from 'commander';
21
20
  /** Register the `agents watchdog` command tree. */