@phnx-labs/agents-cli 1.22.25 → 1.22.27

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 (178) hide show
  1. package/CHANGELOG.md +427 -0
  2. package/README.md +35 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/bench.d.ts +2 -0
  7. package/dist/commands/bench.js +101 -0
  8. package/dist/commands/browser.js +35 -0
  9. package/dist/commands/doctor.js +32 -10
  10. package/dist/commands/exec.js +17 -16
  11. package/dist/commands/insights.d.ts +25 -19
  12. package/dist/commands/insights.js +146 -38
  13. package/dist/commands/output.js +100 -25
  14. package/dist/commands/reconnect.d.ts +46 -0
  15. package/dist/commands/reconnect.js +109 -0
  16. package/dist/commands/resume.d.ts +11 -0
  17. package/dist/commands/resume.js +51 -0
  18. package/dist/commands/routines.js +2 -2
  19. package/dist/commands/secrets.d.ts +2 -8
  20. package/dist/commands/secrets.js +29 -105
  21. package/dist/commands/sessions-picker.js +12 -7
  22. package/dist/commands/sessions.d.ts +47 -1
  23. package/dist/commands/sessions.js +349 -37
  24. package/dist/commands/setup-secrets.d.ts +1 -0
  25. package/dist/commands/setup-secrets.js +1 -1
  26. package/dist/commands/setup.d.ts +26 -3
  27. package/dist/commands/setup.js +105 -46
  28. package/dist/commands/ssh.js +184 -11
  29. package/dist/commands/teams.d.ts +6 -0
  30. package/dist/commands/teams.js +46 -3
  31. package/dist/commands/trends.d.ts +8 -0
  32. package/dist/commands/trends.js +10 -156
  33. package/dist/index.js +17 -21
  34. package/dist/lib/agents.d.ts +11 -0
  35. package/dist/lib/agents.js +29 -2
  36. package/dist/lib/analytics/dashboard.d.ts +10 -6
  37. package/dist/lib/analytics/dashboard.js +6 -4
  38. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  39. package/dist/lib/analytics/mix-commands.js +229 -0
  40. package/dist/lib/analytics/recipes.d.ts +19 -14
  41. package/dist/lib/analytics/recipes.js +4 -2
  42. package/dist/lib/auth-health.d.ts +47 -3
  43. package/dist/lib/auth-health.js +74 -15
  44. package/dist/lib/bench/index.d.ts +4 -0
  45. package/dist/lib/bench/index.js +4 -0
  46. package/dist/lib/bench/runner.d.ts +16 -0
  47. package/dist/lib/bench/runner.js +111 -0
  48. package/dist/lib/bench/schema.d.ts +5 -0
  49. package/dist/lib/bench/schema.js +91 -0
  50. package/dist/lib/bench/storage.d.ts +5 -0
  51. package/dist/lib/bench/storage.js +32 -0
  52. package/dist/lib/bench/types.d.ts +40 -0
  53. package/dist/lib/bench/types.js +1 -0
  54. package/dist/lib/browser/ipc.d.ts +26 -0
  55. package/dist/lib/browser/ipc.js +139 -24
  56. package/dist/lib/browser/profiles.d.ts +11 -0
  57. package/dist/lib/browser/profiles.js +1 -1
  58. package/dist/lib/browser/stream.d.ts +14 -0
  59. package/dist/lib/browser/stream.js +71 -0
  60. package/dist/lib/channels/owner-sink.d.ts +27 -0
  61. package/dist/lib/channels/owner-sink.js +93 -0
  62. package/dist/lib/claude-account-token.d.ts +2 -0
  63. package/dist/lib/claude-account-token.js +27 -1
  64. package/dist/lib/crabbox/cli.d.ts +2 -0
  65. package/dist/lib/crabbox/cli.js +2 -0
  66. package/dist/lib/crabbox/lease.js +7 -1
  67. package/dist/lib/daemon.js +20 -0
  68. package/dist/lib/devices/connect.d.ts +2 -0
  69. package/dist/lib/devices/connect.js +7 -0
  70. package/dist/lib/devices/doctor-findings.d.ts +10 -1
  71. package/dist/lib/devices/doctor-findings.js +47 -1
  72. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  73. package/dist/lib/devices/harness-inventory.js +0 -0
  74. package/dist/lib/devices/registry.d.ts +2 -0
  75. package/dist/lib/devices/ssh-config.js +3 -0
  76. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  77. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  78. package/dist/lib/exec.bench.d.ts +1 -0
  79. package/dist/lib/exec.bench.js +186 -0
  80. package/dist/lib/exec.js +18 -8
  81. package/dist/lib/fleet/apply.d.ts +59 -3
  82. package/dist/lib/fleet/apply.js +191 -12
  83. package/dist/lib/fleet/remote-login.d.ts +4 -3
  84. package/dist/lib/fleet/remote-login.js +11 -9
  85. package/dist/lib/fleet/types.d.ts +21 -2
  86. package/dist/lib/gemini-settings.d.ts +0 -1
  87. package/dist/lib/gemini-settings.js +12 -7
  88. package/dist/lib/hooks/cache.js +15 -0
  89. package/dist/lib/hooks.d.ts +8 -5
  90. package/dist/lib/hooks.js +10 -6
  91. package/dist/lib/hosts/dispatch.js +7 -3
  92. package/dist/lib/hosts/passthrough.d.ts +45 -0
  93. package/dist/lib/hosts/passthrough.js +59 -8
  94. package/dist/lib/hosts/progress.d.ts +2 -1
  95. package/dist/lib/hosts/progress.js +4 -3
  96. package/dist/lib/hosts/providers/devices.js +1 -0
  97. package/dist/lib/hosts/ready.d.ts +6 -2
  98. package/dist/lib/hosts/ready.js +33 -8
  99. package/dist/lib/hosts/reconcile.d.ts +1 -1
  100. package/dist/lib/hosts/reconcile.js +16 -7
  101. package/dist/lib/hosts/reconnect.d.ts +56 -21
  102. package/dist/lib/hosts/reconnect.js +128 -71
  103. package/dist/lib/hosts/registry.d.ts +2 -2
  104. package/dist/lib/hosts/registry.js +3 -5
  105. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  106. package/dist/lib/hosts/remote-cmd.js +29 -0
  107. package/dist/lib/hosts/tasks.d.ts +2 -0
  108. package/dist/lib/hosts/types.d.ts +1 -0
  109. package/dist/lib/hosts/types.js +3 -0
  110. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  111. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  112. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  113. package/dist/lib/menubar/install-menubar.js +20 -9
  114. package/dist/lib/models.d.ts +30 -1
  115. package/dist/lib/models.js +42 -144
  116. package/dist/lib/pricing/cost.d.ts +9 -0
  117. package/dist/lib/pricing/cost.js +24 -0
  118. package/dist/lib/pricing/index.d.ts +1 -1
  119. package/dist/lib/pricing/index.js +1 -1
  120. package/dist/lib/redact.js +8 -3
  121. package/dist/lib/remote-agents-json.d.ts +32 -0
  122. package/dist/lib/remote-agents-json.js +47 -16
  123. package/dist/lib/resource-profiles.js +1 -2
  124. package/dist/lib/routine-notify-owner.d.ts +102 -0
  125. package/dist/lib/routine-notify-owner.js +232 -0
  126. package/dist/lib/routines.d.ts +10 -0
  127. package/dist/lib/routines.js +14 -2
  128. package/dist/lib/runner.d.ts +9 -4
  129. package/dist/lib/runner.js +182 -30
  130. package/dist/lib/sandbox.d.ts +0 -2
  131. package/dist/lib/sandbox.js +2 -19
  132. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  133. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  134. package/dist/lib/secrets/agent.js +3 -2
  135. package/dist/lib/secrets/lease.d.ts +25 -0
  136. package/dist/lib/secrets/lease.js +44 -0
  137. package/dist/lib/secrets/push.d.ts +94 -0
  138. package/dist/lib/secrets/push.js +145 -0
  139. package/dist/lib/secrets/reaper.d.ts +15 -1
  140. package/dist/lib/secrets/reaper.js +30 -3
  141. package/dist/lib/self-update.d.ts +20 -5
  142. package/dist/lib/self-update.js +93 -16
  143. package/dist/lib/session/db.d.ts +38 -3
  144. package/dist/lib/session/db.js +273 -16
  145. package/dist/lib/session/discover.d.ts +22 -1
  146. package/dist/lib/session/discover.js +239 -51
  147. package/dist/lib/session/insights.d.ts +37 -0
  148. package/dist/lib/session/insights.js +219 -9
  149. package/dist/lib/session/recovery.d.ts +17 -1
  150. package/dist/lib/session/recovery.js +111 -4
  151. package/dist/lib/session/remote-list.d.ts +10 -9
  152. package/dist/lib/session/remote-list.js +25 -23
  153. package/dist/lib/session/resume-owner.d.ts +55 -0
  154. package/dist/lib/session/resume-owner.js +69 -0
  155. package/dist/lib/session/team-filter.d.ts +65 -0
  156. package/dist/lib/session/team-filter.js +98 -3
  157. package/dist/lib/session/tool-index.js +133 -22
  158. package/dist/lib/session/tool-store.d.ts +26 -2
  159. package/dist/lib/session/tool-store.js +36 -17
  160. package/dist/lib/session/types.d.ts +33 -0
  161. package/dist/lib/smart-launch.d.ts +31 -5
  162. package/dist/lib/smart-launch.js +43 -7
  163. package/dist/lib/ssh-exec.d.ts +27 -0
  164. package/dist/lib/ssh-exec.js +42 -3
  165. package/dist/lib/ssh-tunnel.d.ts +3 -2
  166. package/dist/lib/ssh-tunnel.js +25 -16
  167. package/dist/lib/startup/command-registry.d.ts +2 -0
  168. package/dist/lib/startup/command-registry.js +6 -0
  169. package/dist/lib/teams/agents.d.ts +13 -0
  170. package/dist/lib/teams/agents.js +75 -7
  171. package/dist/lib/teams/placement-probe.d.ts +21 -0
  172. package/dist/lib/teams/placement-probe.js +135 -0
  173. package/dist/lib/teams/scheduler.d.ts +74 -1
  174. package/dist/lib/teams/scheduler.js +187 -10
  175. package/dist/lib/tmux/session.d.ts +8 -0
  176. package/dist/lib/tmux/session.js +22 -0
  177. package/dist/lib/types.d.ts +2 -0
  178. package/package.json +1 -1
@@ -33,16 +33,18 @@ import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/
33
33
  import { inferSessionState } from '../lib/session/state.js';
34
34
  import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
35
35
  import { findSessionsById, querySessions, getSessionById } from '../lib/session/db.js';
36
- import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
36
+ import { filterTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
37
37
  import { parseSession } from '../lib/session/parse.js';
38
38
  import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
39
39
  import { formatRelativeTime, formatCompactAge, sessionAgeParts } from '../lib/session/relative-time.js';
40
40
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
41
41
  import { linearIssueUrl } from '../lib/session/linear.js';
42
+ import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
42
43
  import { renderMarkdown } from '../lib/markdown.js';
43
44
  import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
44
45
  import { getShimsDir } from '../lib/state.js';
45
46
  import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
47
+ import { itemPicker } from '../lib/picker.js';
46
48
  import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
47
49
  import { resolveVersionAliasLoose } from '../lib/versions.js';
48
50
  import { getAgentsInvocation } from '../lib/daemon.js';
@@ -57,6 +59,7 @@ import { registerSessionsFavoriteCommand } from './sessions-favorite.js';
57
59
  import { isFavorite, listFavorites } from '../lib/session/favorites.js';
58
60
  import { registerGoCommand } from './go.js';
59
61
  import { registerFocusCommand } from './focus.js';
62
+ import { registerReconnectCommand } from './reconnect.js';
60
63
  import { registerDetachCommand } from './detach.js';
61
64
  import { registerAttachCommand } from './attach.js';
62
65
  import { registerSessionsInjectCommand } from './sessions-inject.js';
@@ -66,6 +69,7 @@ import { registerSessionsImportCommand } from './sessions-import.js';
66
69
  import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
67
70
  import { registerSessionsBackfillCommand } from './sessions-backfill.js';
68
71
  import { registerSessionsStatsCommand } from './sessions-stats.js';
72
+ import { registerSessionsInsightsCommand } from './insights.js';
69
73
  import { registerSessionsOptimizeCommand } from './sessions-optimize.js';
70
74
  import { runBrowserSessions } from '../lib/browser/sessions-list.js';
71
75
  import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
@@ -1440,6 +1444,96 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
1440
1444
  // legacy per-host stream, which prints each peer under its own banner.
1441
1445
  (options.host?.length ?? 0) <= 1);
1442
1446
  }
1447
+ /** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
1448
+ export function resolveRoutineName(query, names) {
1449
+ const normalized = query.trim().toLowerCase();
1450
+ if (!normalized)
1451
+ return null;
1452
+ const exact = names.find((name) => name.toLowerCase() === normalized);
1453
+ if (exact)
1454
+ return exact;
1455
+ const containing = names.filter((name) => name.toLowerCase().includes(normalized));
1456
+ if (containing.length === 1)
1457
+ return containing[0];
1458
+ return fuzzyMatch(query, names, FUZZY_PRESETS.dynamic);
1459
+ }
1460
+ export function buildRoutineRunGroups(sessions) {
1461
+ const byRun = new Map();
1462
+ for (const session of sessions) {
1463
+ const runId = session.routineRunId ?? session.timestamp;
1464
+ (byRun.get(runId) ?? byRun.set(runId, []).get(runId)).push(session);
1465
+ }
1466
+ return [...byRun.entries()]
1467
+ .map(([runId, rows]) => ({
1468
+ runId,
1469
+ timestamp: rows.reduce((latest, row) => (row.lastActivity ?? row.timestamp) > latest ? (row.lastActivity ?? row.timestamp) : latest, rows[0].lastActivity ?? rows[0].timestamp),
1470
+ sessions: rows.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)),
1471
+ }))
1472
+ .sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
1473
+ }
1474
+ export function buildRoutineChoices(sessions) {
1475
+ const byName = new Map();
1476
+ for (const session of sessions) {
1477
+ if (!session.routineName)
1478
+ continue;
1479
+ (byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
1480
+ }
1481
+ return [...byName.entries()]
1482
+ .map(([name, rows]) => {
1483
+ const runs = buildRoutineRunGroups(rows);
1484
+ return {
1485
+ name,
1486
+ lastRunAt: runs[0].timestamp,
1487
+ runCount: runs.length,
1488
+ latestRunSessionCount: runs[0].sessions.length,
1489
+ };
1490
+ })
1491
+ .sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
1492
+ }
1493
+ async function selectRoutineName(sessions) {
1494
+ const choices = buildRoutineChoices(sessions);
1495
+ const picked = await itemPicker({
1496
+ message: 'Select a routine:',
1497
+ items: choices,
1498
+ filter: (query) => {
1499
+ const normalized = query.trim().toLowerCase();
1500
+ if (!normalized)
1501
+ return choices;
1502
+ return choices.filter((choice) => choice.name.toLowerCase().includes(normalized));
1503
+ },
1504
+ labelFor: (choice) => {
1505
+ const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
1506
+ const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
1507
+ return `${choice.name} ${chalk.gray(`${runs} · ${sessions} · ${formatRelativeTime(choice.lastRunAt)}`)}`;
1508
+ },
1509
+ shortIdFor: (choice) => choice.name,
1510
+ emptyMessage: 'No routines match.',
1511
+ enterHint: 'show sessions',
1512
+ });
1513
+ return picked?.item.name ?? null;
1514
+ }
1515
+ export async function filterSessionsByRoutine(sessions, routine, interactive) {
1516
+ const routineNames = [...new Set(sessions.map((session) => session.routineName).filter((name) => !!name))].sort((a, b) => a.localeCompare(b));
1517
+ let selectedRoutine = null;
1518
+ if (typeof routine === 'string') {
1519
+ selectedRoutine = resolveRoutineName(routine, routineNames);
1520
+ if (!selectedRoutine) {
1521
+ console.error(chalk.red(`No routine matches "${routine}".`));
1522
+ if (routineNames.length > 0)
1523
+ console.error(chalk.gray(`Available routines: ${routineNames.join(', ')}`));
1524
+ process.exitCode = 1;
1525
+ return null;
1526
+ }
1527
+ }
1528
+ else if (interactive) {
1529
+ selectedRoutine = await selectRoutineName(sessions);
1530
+ if (!selectedRoutine)
1531
+ return null;
1532
+ }
1533
+ return selectedRoutine
1534
+ ? sessions.filter((session) => session.routineName === selectedRoutine)
1535
+ : sessions;
1536
+ }
1443
1537
  /** The canonical `ag sessions …` command for a set of flags — the twin of the
1444
1538
  * browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
1445
1539
  function canonicalSessionsCommand(query, options) {
@@ -1466,8 +1560,11 @@ function canonicalSessionsCommand(query, options) {
1466
1560
  a.push('--teams');
1467
1561
  if (options.inTeam)
1468
1562
  a.push('--in-team', options.inTeam);
1469
- if (options.routine)
1563
+ if (options.routine) {
1470
1564
  a.push('--routine');
1565
+ if (typeof options.routine === 'string')
1566
+ a.push(options.routine);
1567
+ }
1471
1568
  if (options.agent)
1472
1569
  a.push('-a', options.agent);
1473
1570
  for (const h of options.host ?? [])
@@ -1908,7 +2005,13 @@ limitSource) {
1908
2005
  // filter the browser can't represent), or --no-interactive keep the existing
1909
2006
  // printed/render paths (agents and scripts unaffected). An explicit --since seeds
1910
2007
  // the browser's window so the flag is honored, not swallowed.
1911
- if (isBareBrowserListing(options, query)) {
2008
+ //
2009
+ // `--teams` diverts to the printed team-grouped report (printTeamsView, below):
2010
+ // grouping teammates under their team is a shape the flat fuzzy picker cannot
2011
+ // represent, so like --tree it is a printed listing, not an interactive pick.
2012
+ // --teams --flat/--tree keep their inline table rendering; a search query keeps
2013
+ // the picker so `<term> --teams` still searches.
2014
+ if (isBareBrowserListing(options, query) && !options.teams) {
1912
2015
  const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
1913
2016
  await runSessionBrowser(bareBrowserSeed({
1914
2017
  teams: options.teams,
@@ -1955,13 +2058,13 @@ limitSource) {
1955
2058
  const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
1956
2059
  // Artifact-list or artifact-read paths: widen scope and resolve session globally.
1957
2060
  if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
1958
- await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, { agent: options.agent, project: options.project });
2061
+ await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, artifactLookupScope(options.agent, options.project, options.routine));
1959
2062
  return;
1960
2063
  }
1961
2064
  // When the user explicitly asks to render (via mode flag), resolve the
1962
2065
  // query globally so sessions outside the default cwd/30d window are found.
1963
2066
  if (wantsRender && searchQuery) {
1964
- await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
2067
+ await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
1965
2068
  return;
1966
2069
  }
1967
2070
  // Interactive picker loads a deep pool but shows only recent sessions
@@ -2144,6 +2247,14 @@ limitSource) {
2144
2247
  const hiddenCount = options.teams || options.inTeam
2145
2248
  ? 0
2146
2249
  : countSessionsInScope({ ...scope, onlyTeamOrigin: true });
2250
+ // A typed routine scope must apply before smart ID routing. Otherwise an ID
2251
+ // from another routine resolves and renders before the routine filter below.
2252
+ if (typeof options.routine === 'string') {
2253
+ const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
2254
+ if (!filteredByRoutine)
2255
+ return;
2256
+ sessions = filteredByRoutine;
2257
+ }
2147
2258
  // Smart ID routing: a bare query that resolves to one session renders
2148
2259
  // directly. If nothing matches in the scoped window and the query looks
2149
2260
  // like a session ID, widen to global scope (incl. Claude /resume history).
@@ -2161,11 +2272,17 @@ limitSource) {
2161
2272
  return;
2162
2273
  }
2163
2274
  if (idMatches.length === 0 && looksLikeSessionId(searchQuery)) {
2164
- await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
2275
+ await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
2165
2276
  return;
2166
2277
  }
2167
2278
  }
2168
2279
  if (options.json) {
2280
+ if (options.routine === true) {
2281
+ const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
2282
+ if (!filteredByRoutine)
2283
+ return;
2284
+ sessions = filteredByRoutine;
2285
+ }
2169
2286
  // An id-shaped query resolves by id ONLY — the same rule the render path
2170
2287
  // uses (resolveSessionQuery). Without this a `--json <uuid>` (as issued by
2171
2288
  // the fleet locate sweep, which runs `sessions <uuid> --json --local` on
@@ -2208,6 +2325,12 @@ limitSource) {
2208
2325
  fanSpinner?.stop();
2209
2326
  }
2210
2327
  }
2328
+ if (options.routine) {
2329
+ const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, isInteractive);
2330
+ if (!filteredByRoutine)
2331
+ return;
2332
+ sessions = filteredByRoutine;
2333
+ }
2211
2334
  if (sessions.length === 0) {
2212
2335
  if (pathFilter) {
2213
2336
  console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
@@ -2223,11 +2346,24 @@ limitSource) {
2223
2346
  }
2224
2347
  return;
2225
2348
  }
2349
+ // `--teams` prints the team-grouped report: teammates under their team (with
2350
+ // spawner + spawn time), bare SDK spawns in their own bucket. --flat/--tree
2351
+ // keep their inline table rendering; a search query keeps the picker. Placed
2352
+ // before the project overview and picker so it wins the bare `--teams` case.
2353
+ if (options.teams && !options.flat && !options.tree && !searchQuery && !pathFilter) {
2354
+ const liveIndex = await maybeLiveIndex(options);
2355
+ printTeamsView(sessions, liveIndex, hiddenUnmanaged);
2356
+ return;
2357
+ }
2226
2358
  // The grouped project overview is the bare interactive default: a scannable
2227
2359
  // dashboard of the whole fleet grouped by project, newest-active first.
2228
2360
  // Interact/resume via `agents sessions <project>` or `agents sessions resume`.
2229
2361
  if (wantsOverview) {
2230
2362
  const liveIndex = await maybeLiveIndex(options);
2363
+ if (options.routine) {
2364
+ printRoutineRunOverview(sessions, liveIndex, { hiddenCount, hiddenUnmanaged });
2365
+ return;
2366
+ }
2231
2367
  // Per-project row cap is fixed (--limit carries a default of 50 and drives
2232
2368
  // the fetch pool, not the display); `--all` expands every group instead.
2233
2369
  printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
@@ -2551,6 +2687,25 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
2551
2687
  if (opts.hiddenUnmanaged)
2552
2688
  console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
2553
2689
  }
2690
+ export function printRoutineRunOverview(sessions, liveIndex, opts = {}) {
2691
+ const groups = buildRoutineRunGroups(sessions);
2692
+ console.log(chalk.gray(`${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${groups.length} routine run${groups.length === 1 ? '' : 's'} · newest run first\n`));
2693
+ let first = true;
2694
+ for (const group of groups) {
2695
+ if (!first)
2696
+ console.log();
2697
+ first = false;
2698
+ console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
2699
+ `${chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`)}`);
2700
+ for (const session of group.sessions)
2701
+ console.log(treeSessionRow(session, liveIndex?.get(session.id)));
2702
+ }
2703
+ console.log(chalk.gray('\nGrouped by routine run id and timestamp.'));
2704
+ if (opts.hiddenCount)
2705
+ console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
2706
+ if (opts.hiddenUnmanaged)
2707
+ console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
2708
+ }
2554
2709
  function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
2555
2710
  if (tree) {
2556
2711
  // Group by directory; drop the id/version columns from view. The short id
@@ -2597,6 +2752,118 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
2597
2752
  console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
2598
2753
  }
2599
2754
  }
2755
+ /** Width of the mode cell (plan/edit/auto/skip) in a team-member row. */
2756
+ const TEAM_MODE_W = 5;
2757
+ /** Longest teammate handle folded into a team-member row before it truncates. */
2758
+ const TEAM_HANDLE_W = 16;
2759
+ /**
2760
+ * One row under a team group: `shortId · agent · mode · handle · doing · time`.
2761
+ * Unlike {@link flatSessionRow}/{@link treeSessionRow} it drops the
2762
+ * `[team/handle]` topic prefix — the group header already names the team — and
2763
+ * promotes the teammate's mode and handle to their own cells, the richer
2764
+ * identity the `--teams` view exists to show (RUSH-1997).
2765
+ */
2766
+ function teamMemberRow(session, live) {
2767
+ const agentColor = colorAgent(session.agent);
2768
+ const origin = session.teamOrigin;
2769
+ const age = sessionAgeParts(session.timestamp, session.lastActivity);
2770
+ const handle = safeTeamText(origin?.handle) ?? session.shortId;
2771
+ const mode = safeTeamText(origin?.mode) ?? '';
2772
+ const { glyph, preview } = liveGlyphAndPreview(live);
2773
+ // Match flatSessionRow: a live preview already folds ActiveSession.todos; a
2774
+ // resting row surfaces SessionMeta.todos when the scan attached it.
2775
+ const restingTodo = !live ? formatTodoCompact(session.todos) : '';
2776
+ const doing = [restingTodo, preview || session.topic || ''].filter(Boolean).join(' · ');
2777
+ const shownHandle = truncateToWidth(handle, TEAM_HANDLE_W);
2778
+ const head = doing ? `${shownHandle} · ${doing}` : shownHandle;
2779
+ const badges = signalBadges(metaSignals(session));
2780
+ const badgeW = badges ? stringWidth(badges) + 1 : 0;
2781
+ const { cell: statusCell, width: statusW } = liveStatusCell(live);
2782
+ const glyphW = glyph ? 2 : 0;
2783
+ const baseTopicW = terminalWidth() - (2 + 9 + 8 + (TEAM_MODE_W + 1)) - glyphW - statusW - badgeW - stringWidth(age.last) - 1;
2784
+ const when = timeCell(age, baseTopicW);
2785
+ const topicW = Math.max(12, baseTopicW - when.extraW);
2786
+ return (' ' +
2787
+ chalk.dim(padToWidth(session.shortId, 9)) +
2788
+ agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
2789
+ chalk.yellow(padToWidth(truncateToWidth(mode || '-', TEAM_MODE_W), TEAM_MODE_W + 1)) +
2790
+ (badges ? badges + ' ' : '') +
2791
+ (glyph ? glyph + ' ' : '') +
2792
+ statusCell +
2793
+ padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
2794
+ ' ' +
2795
+ when.text);
2796
+ }
2797
+ /** The header line for one team group (or the residual no-team bucket). */
2798
+ function teamGroupHeader(g, glyph, labelById) {
2799
+ const count = chalk.gray(`(${g.sessions.length})`);
2800
+ if (g.kind === 'noTeam') {
2801
+ // These carry the isTeamOrigin flag but no teammate meta.json — a Task
2802
+ // sub-agent's transcript is never indexed, so in practice this bucket is
2803
+ // headless `agents run` spawns or teammates whose meta aged out. Named
2804
+ // honestly rather than claimed as real teammates.
2805
+ return (chalk.magenta('▸') + ' ' + chalk.magenta.bold(NO_TEAM_GROUP_KEY) + ' ' + count +
2806
+ ' ' + chalk.gray('team-flagged spawns with no team record (`agents run`, or aged-out teammates)'));
2807
+ }
2808
+ const bits = [chalk.cyan('▸') + ' ' + chalk.cyan.bold(g.key), count];
2809
+ if (glyph)
2810
+ bits.push(glyph);
2811
+ if (g.spawnerSessionId) {
2812
+ const who = labelById.get(g.spawnerSessionId) ?? g.spawnerSessionId.slice(0, 8);
2813
+ bits.push(chalk.gray(`by ${who}`));
2814
+ }
2815
+ bits.push(chalk.gray(`spawned ${formatRelativeTime(g.firstSpawnTs)}`));
2816
+ return bits.join(' ');
2817
+ }
2818
+ /**
2819
+ * The `agents sessions --teams` view: team-origin sessions grouped by team, the
2820
+ * richer display RUSH-1997 asks for. Each named team names its spawner (the
2821
+ * orchestrator session that created it) and spawn time, with its teammates'
2822
+ * mode + handle under it; team-flagged spawns that carry no teammate record fall
2823
+ * into a trailing no-team bucket, so a real teammate and a bare spawn are never
2824
+ * conflated. A printed report like `--tree`, not an interactive pick.
2825
+ */
2826
+ function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
2827
+ const groups = groupSessionsByTeam(pool);
2828
+ if (groups.length === 0) {
2829
+ console.log(chalk.gray('No team sessions found.'));
2830
+ console.log(chalk.gray('Team sessions are spawned by `agents teams` — see `agents teams status`.'));
2831
+ return;
2832
+ }
2833
+ // Resolve a spawner session id to its human label from the pool. The
2834
+ // orchestrator may or may not be in view; fall back to the short id, the same
2835
+ // degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
2836
+ const labelById = new Map();
2837
+ for (const s of pool) {
2838
+ const label = s.label || s.topic;
2839
+ if (label)
2840
+ labelById.set(s.id, cleanPreview(label));
2841
+ }
2842
+ const total = groups.reduce((n, g) => n + g.sessions.length, 0);
2843
+ const teamCount = groups.filter((g) => g.kind === 'team').length;
2844
+ const noTeam = groups.find((g) => g.kind === 'noTeam');
2845
+ const parts = [
2846
+ `${total} team session${total === 1 ? '' : 's'}`,
2847
+ `${teamCount} team${teamCount === 1 ? '' : 's'}`,
2848
+ ];
2849
+ if (noTeam)
2850
+ parts.push(`${noTeam.sessions.length} without a team record`);
2851
+ console.log(chalk.gray(parts.join(' · ') + '\n'));
2852
+ let first = true;
2853
+ for (const g of groups) {
2854
+ if (!first)
2855
+ console.log();
2856
+ first = false;
2857
+ const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.sessions[0].id));
2858
+ console.log(teamGroupHeader(g, glyph, labelById));
2859
+ for (const s of g.sessions)
2860
+ console.log(teamMemberRow(s, liveIndex?.get(s.id)));
2861
+ }
2862
+ console.log();
2863
+ console.log(chalk.gray('newest-active team first · resume any row with `agents sessions resume <id>`'));
2864
+ if (hiddenUnmanaged > 0)
2865
+ console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
2866
+ }
2600
2867
  function buildFilterOptions(options) {
2601
2868
  const opts = {};
2602
2869
  if (options.include)
@@ -2971,15 +3238,6 @@ export async function pickSessionInteractive(sessions, message = 'Search session
2971
3238
  throw err;
2972
3239
  }
2973
3240
  }
2974
- /**
2975
- * The machine a picked session lives on when its transcript is on that peer's
2976
- * disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
2977
- * NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
2978
- * is a local mirror path, so it must be read/resumed locally like any other.
2979
- */
2980
- function remoteMachineOf(session) {
2981
- return session._remote ? session.machine : undefined;
2982
- }
2983
3241
  /** True when the peer wasn't a dialable device; prints one clear line so a
2984
3242
  * remote pick never dead-ends silently. */
2985
3243
  function warnNoPeerTarget(machine, session) {
@@ -3005,31 +3263,50 @@ export async function handlePickedSession(picked) {
3005
3263
  console.log(chalk.gray(`Watch for it with: agents sessions --active${picked.session.machine ? ` --host ${picked.session.machine}` : ''}`));
3006
3264
  return;
3007
3265
  }
3008
- // A session on another machine is read/resumed ON that machine over SSH — its
3009
- // transcript and agent binary live there. Both actions execute on the peer
3010
- // (not a local `--host` hop, which would discover locally and dead-end for a
3011
- // session that exists only on the peer).
3012
- const remote = remoteMachineOf(picked.session);
3013
- if (remote) {
3014
- if (picked.action === 'view') {
3015
- const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], remote);
3016
- if (rc === 'no-target')
3017
- warnNoPeerTarget(remote, picked.session);
3018
- }
3019
- else {
3020
- console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
3021
- const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
3266
+ // Reading and resuming are on DIFFERENT machines' terms, and conflating them
3267
+ // is what RUSH-2022 was. Reading follows the FILE: a synced mirror sits on
3268
+ // this disk, so only a live fan-out row (`_remote`) has to be read on the
3269
+ // peer. Resuming follows the HARNESS STATE, which is on the owning machine
3270
+ // whatever the transcript's location — a mirror included.
3271
+ if (picked.action === 'view') {
3272
+ const readFrom = picked.session._remote ? picked.session.machine : undefined;
3273
+ if (readFrom) {
3274
+ const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], readFrom);
3022
3275
  if (rc === 'no-target')
3023
- warnNoPeerTarget(remote, picked.session);
3276
+ warnNoPeerTarget(readFrom, picked.session);
3277
+ return;
3024
3278
  }
3025
- return;
3026
- }
3027
- if (picked.action === 'view') {
3028
3279
  await renderSession(picked.session, 'summary', {});
3029
3280
  return;
3030
3281
  }
3282
+ if (await resumeOnOwnerIfRemote(picked.session))
3283
+ return;
3031
3284
  await resumeSessionInPlace(picked.session);
3032
3285
  }
3286
+ /**
3287
+ * Resume `session` on the machine that owns it, when that is not this one.
3288
+ * Returns true when it handled the resume (hopped, or reported an unreachable
3289
+ * owner), false when the session is local and the caller should take over here.
3290
+ *
3291
+ * The one hop for a foreground, one-session-at-a-time resume — shared by the
3292
+ * `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
3293
+ * would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
3294
+ */
3295
+ export async function resumeOnOwnerIfRemote(session) {
3296
+ const owner = sessionOwnerDevice(session);
3297
+ if (!owner)
3298
+ return false;
3299
+ console.log(chalk.gray(`Resuming ${session.shortId} on ${owner} over SSH...`));
3300
+ // `agents resume <id>` is the strict single-session path — one pick, one
3301
+ // resume. (`sessions resume <shortId>` would re-open a picker over there.)
3302
+ const rc = await runOnPeer(['resume', session.id], owner, {
3303
+ tty: true,
3304
+ env: { [RESUME_PINNED_ENV]: '1' },
3305
+ });
3306
+ if (rc === 'no-target')
3307
+ warnNoPeerTarget(owner, session);
3308
+ return true;
3309
+ }
3033
3310
  /**
3034
3311
  * Resume a session in the current terminal — a foreground takeover of this
3035
3312
  * process. Used by the single-select picker and by `sessions resume` when the
@@ -3038,6 +3315,20 @@ export async function handlePickedSession(picked) {
3038
3315
  * version-pinned launcher is genuinely missing.
3039
3316
  */
3040
3317
  export async function resumeSessionInPlace(session) {
3318
+ // This function is the LOCAL takeover, and every caller is responsible for
3319
+ // routing a peer-owned session before it gets here (the picker above,
3320
+ // `agents resume`, `sessions attach`). Reaching it with one anyway means a
3321
+ // caller skipped that step, so refuse rather than start the harness against
3322
+ // state this box does not have — the `fs.existsSync(session.cwd)` fallback
3323
+ // just below is exactly how that used to pass unnoticed, quietly swapping in
3324
+ // `process.cwd()` when the recorded directory was a peer's path (RUSH-2022).
3325
+ const owner = sessionOwnerDevice(session);
3326
+ if (owner) {
3327
+ console.error(chalk.red(`Session ${session.shortId} belongs to ${owner} — it cannot resume on this machine.`));
3328
+ console.error(chalk.gray(` Resume it there: agents resume ${session.id}`));
3329
+ process.exitCode = 1;
3330
+ return;
3331
+ }
3041
3332
  const cwd = session.cwd && fs.existsSync(session.cwd)
3042
3333
  ? session.cwd
3043
3334
  : process.cwd();
@@ -3457,7 +3748,7 @@ function scoreSessionQuery(session, terms) {
3457
3748
  * the project you specified AND elsewhere, producing an ambiguity error
3458
3749
  * even though the user already pointed at the correct scope.
3459
3750
  */
3460
- function applyScopeFilters(sessions, scope) {
3751
+ export function applyScopeFilters(sessions, scope) {
3461
3752
  let filtered = sessions;
3462
3753
  if (scope.project) {
3463
3754
  const projectQuery = scope.project.toLowerCase();
@@ -3480,8 +3771,21 @@ function applyScopeFilters(sessions, scope) {
3480
3771
  return true;
3481
3772
  });
3482
3773
  }
3774
+ if (scope.routine) {
3775
+ filtered = filtered.filter((session) => session.origin === 'routine');
3776
+ if (typeof scope.routine === 'string') {
3777
+ const names = [...new Set(filtered.map((session) => session.routineName).filter((name) => !!name))];
3778
+ const selected = resolveRoutineName(scope.routine, names);
3779
+ filtered = selected
3780
+ ? filtered.filter((session) => session.routineName === selected)
3781
+ : [];
3782
+ }
3783
+ }
3483
3784
  return filtered;
3484
3785
  }
3786
+ export function artifactLookupScope(agent, project, routine) {
3787
+ return { agent, project, routine };
3788
+ }
3485
3789
  async function renderArtifactsGlobal(query, listAll, name, scope) {
3486
3790
  const spinner = ora().start();
3487
3791
  const tracker = createScanProgressTracker(FIND_VERBS, 'session', spinner);
@@ -3927,9 +4231,9 @@ export function registerSessionsCommands(program) {
3927
4231
  .option('--opencode', 'Shorthand for --agent opencode')
3928
4232
  .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.')
3929
4233
  .option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
3930
- .option('--team, --teams', 'Include team-spawned sessions (hidden by default)')
4234
+ .option('--team, --teams', 'Show team-spawned sessions (hidden by default), grouped by team — each team names its spawner and spawn time, teammates show their mode + handle, and team-flagged spawns with no teammate record sink into a (no team) bucket. --flat/--tree keep the plain inline table')
3931
4235
  .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.")
3932
- .option('--routine', 'Show only sessions archived from routine runs')
4236
+ .option('--routines, --routine [name]', 'Show routine-run sessions; omit the name on a TTY to pick one interactively (fuzzy name matching)')
3933
4237
  .option('-p, --project <name>', 'Filter by project name (searches across all directories)')
3934
4238
  .option('--skill <name>', 'Only sessions that invoked this skill (matches a bare name or a namespaced plugin skill\'s short name, e.g. --skill design finds rush:design)')
3935
4239
  .option('--plugin <name>', 'Only sessions that used a skill/command owned by this plugin')
@@ -4012,12 +4316,18 @@ export function registerSessionsCommands(program) {
4012
4316
  # Search across every directory, not just this project
4013
4317
  agents sessions "topic" --all
4014
4318
 
4319
+ # Team-spawned sessions, grouped by team (spawner + spawn time per team,
4320
+ # teammate mode/handle per row; team-flagged spawns with no team record
4321
+ # in a trailing (no team) bucket)
4322
+ agents sessions --teams
4323
+
4015
4324
  # Who spawned which team: an orchestrator row carries team:<name>, and a
4016
4325
  # teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
4017
4326
  agents sessions --in-team redesign --teams
4018
4327
 
4019
4328
  # Show routine-run sessions and open one by routine run id
4020
4329
  agents sessions --routine --all
4330
+ agents sessions --routine nightly-review --all
4021
4331
  agents sessions 2026-07-21T10-30-00-000Z
4022
4332
 
4023
4333
  # Export for analysis
@@ -4064,7 +4374,7 @@ export function registerSessionsCommands(program) {
4064
4374
  - --first and --last are mutually exclusive.
4065
4375
  - A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
4066
4376
  - --cloud sources from Rush Cloud captured runs instead of local disk.
4067
- - --routine shows only transcripts archived from routine run directories; routine rows also resolve by run id.
4377
+ - --routine [name] shows transcripts archived from routine runs. On a TTY, omit the name to pick a routine; a name accepts exact, substring, or unambiguous typo matches. --routines is an alias. Routine rows also resolve by run id.
4068
4378
  - Without --teams, team-spawned sessions are hidden by default.
4069
4379
  `,
4070
4380
  });
@@ -4082,6 +4392,7 @@ export function registerSessionsCommands(program) {
4082
4392
  registerSessionsFavoriteCommand(sessionsCmd);
4083
4393
  registerGoCommand(sessionsCmd);
4084
4394
  registerFocusCommand(sessionsCmd);
4395
+ registerReconnectCommand(sessionsCmd);
4085
4396
  registerDetachCommand(sessionsCmd);
4086
4397
  registerAttachCommand(sessionsCmd);
4087
4398
  registerSessionsInjectCommand(sessionsCmd);
@@ -4092,6 +4403,7 @@ export function registerSessionsCommands(program) {
4092
4403
  registerSessionsMigrationsCommand(sessionsCmd);
4093
4404
  registerSessionsBackfillCommand(sessionsCmd);
4094
4405
  registerSessionsStatsCommand(sessionsCmd);
4406
+ registerSessionsInsightsCommand(sessionsCmd);
4095
4407
  registerSessionsOptimizeCommand(sessionsCmd);
4096
4408
  // Observe-umbrella alias (Phase 3): roster → sessions --active.
4097
4409
  registerSessionsObserveAliases(program);
@@ -15,6 +15,7 @@ interface SetupSecretsOptions {
15
15
  iUnderstand?: boolean;
16
16
  force?: boolean;
17
17
  }
18
+ export declare function setupSecretsPrefsPath(): string;
18
19
  /** The wizard's `--policy` shares the canonical parser with `agents secrets
19
20
  * policy`, so it accepts `hold` (and the `daily`/`session` aliases) rather than
20
21
  * carrying its own copy that knows only the retired vocabulary. `parsePolicyOpt`
@@ -15,7 +15,7 @@ import { parsePolicyOpt } from './secrets.js';
15
15
  function defaultBackendForPlatform(platform = process.platform) {
16
16
  return platform === 'darwin' ? 'keychain' : 'file';
17
17
  }
18
- function setupSecretsPrefsPath() {
18
+ export function setupSecretsPrefsPath() {
19
19
  return path.join(getHistoryDir(), 'setup', 'secrets.json');
20
20
  }
21
21
  function parseBackend(raw) {
@@ -5,12 +5,14 @@
5
5
  * ~/.agents/.system/ and installs agent CLIs with resource syncing.
6
6
  */
7
7
  import type { Command } from 'commander';
8
- /** First-run setup. Clones ~/.agents/.system/ from the system repo if needed. */
9
- export declare function runSetup(program: Command, options?: {
8
+ interface RunSetupOptions {
10
9
  force?: boolean;
11
10
  suppressFooter?: boolean;
12
11
  systemRepo?: boolean;
13
- }): Promise<void>;
12
+ runHub?: () => Promise<void>;
13
+ }
14
+ /** First-run setup. Clones ~/.agents/.system/ from the system repo if needed. */
15
+ export declare function runSetup(program: Command, options?: RunSetupOptions): Promise<void>;
14
16
  /**
15
17
  * Ensure the system repo exists before running a command that needs it.
16
18
  * If ~/.agents/.system/ is not a git repo AND we're in an interactive TTY,
@@ -18,5 +20,26 @@ export declare function runSetup(program: Command, options?: {
18
20
  * error and exit.
19
21
  */
20
22
  export declare function ensureInitialized(program: Command): Promise<void>;
23
+ /**
24
+ * Interactive "what else do you want to set up?" menu shown after the bare
25
+ * `agents setup` finishes on a TTY. Each pick runs that capability's guided
26
+ * wizard. Never throws — a cancel or an optional wizard's error just skips the
27
+ * rest and lets core setup complete.
28
+ */
29
+ export type SetupPhase = 'browser' | 'computer' | 'share' | 'secrets' | 'fleet' | 'watchdog' | 'preferences';
30
+ export type SetupStatusState = 'ready' | 'missing' | 'n/a';
31
+ export interface SetupStatusRow {
32
+ phase: 'core' | SetupPhase;
33
+ state: SetupStatusState;
34
+ detail: string;
35
+ }
36
+ export declare function getSetupStatus(): Promise<SetupStatusRow[]>;
37
+ export declare function renderSetupStatus(rows: SetupStatusRow[]): void;
38
+ export declare function runSetupHub(deps?: {
39
+ interactive?: boolean;
40
+ selectPhase?: (rows: SetupStatusRow[]) => Promise<SetupPhase | 'exit'>;
41
+ runPhase?: (phase: SetupPhase) => Promise<void>;
42
+ }): Promise<void>;
21
43
  /** Register the `agents setup` command and its capability subcommands. */
22
44
  export declare function registerSetupCommand(program: Command): void;
45
+ export {};