@phnx-labs/agents-cli 1.20.77 → 1.20.82

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 (212) hide show
  1. package/CHANGELOG.md +589 -0
  2. package/README.md +27 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +74 -0
  12. package/dist/commands/doctor.js +718 -40
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +53 -8
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +195 -11
  29. package/dist/commands/routines.js +16 -4
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +108 -34
  33. package/dist/commands/sessions-browser.d.ts +45 -3
  34. package/dist/commands/sessions-browser.js +118 -12
  35. package/dist/commands/sessions-export.d.ts +3 -0
  36. package/dist/commands/sessions-export.js +13 -10
  37. package/dist/commands/sessions-inject.d.ts +7 -4
  38. package/dist/commands/sessions-inject.js +18 -18
  39. package/dist/commands/sessions-picker.d.ts +15 -1
  40. package/dist/commands/sessions-picker.js +90 -7
  41. package/dist/commands/sessions-resume.d.ts +16 -0
  42. package/dist/commands/sessions-resume.js +14 -13
  43. package/dist/commands/sessions.d.ts +126 -1
  44. package/dist/commands/sessions.js +365 -42
  45. package/dist/commands/set.d.ts +15 -0
  46. package/dist/commands/set.js +79 -0
  47. package/dist/commands/setup-fleet.d.ts +21 -0
  48. package/dist/commands/setup-fleet.js +201 -0
  49. package/dist/commands/setup-secrets.d.ts +20 -0
  50. package/dist/commands/setup-secrets.js +223 -0
  51. package/dist/commands/setup.js +17 -3
  52. package/dist/commands/skills.js +1 -1
  53. package/dist/commands/ssh.js +9 -1
  54. package/dist/commands/sync.js +7 -2
  55. package/dist/commands/teams-picker.js +0 -1
  56. package/dist/commands/teams.d.ts +33 -1
  57. package/dist/commands/teams.js +208 -44
  58. package/dist/commands/versions.js +10 -5
  59. package/dist/commands/view.d.ts +1 -0
  60. package/dist/commands/view.js +11 -0
  61. package/dist/index.js +8 -13
  62. package/dist/lib/acp/harnesses.js +0 -7
  63. package/dist/lib/activity.d.ts +156 -0
  64. package/dist/lib/activity.js +282 -0
  65. package/dist/lib/agents.d.ts +9 -23
  66. package/dist/lib/agents.js +72 -100
  67. package/dist/lib/auto-pull.d.ts +16 -8
  68. package/dist/lib/auto-pull.js +23 -28
  69. package/dist/lib/browser/service.js +3 -0
  70. package/dist/lib/browser/types.d.ts +7 -0
  71. package/dist/lib/capabilities.js +6 -2
  72. package/dist/lib/crabbox/lease.js +2 -2
  73. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  74. package/dist/lib/crabbox/setup-copy.js +4 -4
  75. package/dist/lib/daemon.d.ts +26 -40
  76. package/dist/lib/daemon.js +63 -123
  77. package/dist/lib/devices/fleet-divergence.d.ts +101 -0
  78. package/dist/lib/devices/fleet-divergence.js +188 -0
  79. package/dist/lib/devices/fleet-inventory.d.ts +19 -0
  80. package/dist/lib/devices/fleet-inventory.js +57 -0
  81. package/dist/lib/devices/fleet.d.ts +1 -1
  82. package/dist/lib/devices/fleet.js +1 -1
  83. package/dist/lib/devices/health-report.d.ts +10 -2
  84. package/dist/lib/devices/health-report.js +32 -1
  85. package/dist/lib/doctor-diff.d.ts +20 -0
  86. package/dist/lib/doctor-diff.js +6 -1
  87. package/dist/lib/drift.d.ts +31 -11
  88. package/dist/lib/drift.js +58 -7
  89. package/dist/lib/events.d.ts +8 -1
  90. package/dist/lib/events.js +10 -1
  91. package/dist/lib/exec.js +35 -8
  92. package/dist/lib/feed-post.d.ts +28 -1
  93. package/dist/lib/feed-post.js +110 -2
  94. package/dist/lib/fleet/apply.js +8 -1
  95. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  96. package/dist/lib/fleet/auth-sync.js +25 -11
  97. package/dist/lib/fleet/remote-login.js +5 -0
  98. package/dist/lib/git.d.ts +27 -14
  99. package/dist/lib/git.js +138 -43
  100. package/dist/lib/hooks.d.ts +42 -0
  101. package/dist/lib/hooks.js +137 -93
  102. package/dist/lib/hosts/dispatch.d.ts +28 -1
  103. package/dist/lib/hosts/dispatch.js +33 -5
  104. package/dist/lib/hosts/option.js +2 -2
  105. package/dist/lib/hosts/passthrough.d.ts +22 -1
  106. package/dist/lib/hosts/passthrough.js +238 -13
  107. package/dist/lib/hosts/remote-cmd.js +2 -0
  108. package/dist/lib/hosts/run-target.d.ts +2 -0
  109. package/dist/lib/hosts/run-target.js +1 -0
  110. package/dist/lib/hq/floor.d.ts +1 -1
  111. package/dist/lib/hq/floor.js +6 -1
  112. package/dist/lib/mcp.js +3 -71
  113. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
  114. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  115. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  116. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  117. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  118. package/dist/lib/menubar/notify-desktop.js +41 -5
  119. package/dist/lib/merged-resources.d.ts +11 -0
  120. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  121. package/dist/lib/permissions.d.ts +0 -32
  122. package/dist/lib/permissions.js +4 -157
  123. package/dist/lib/picker.js +1 -1
  124. package/dist/lib/platform/process.d.ts +16 -5
  125. package/dist/lib/platform/process.js +54 -0
  126. package/dist/lib/plugins.d.ts +0 -8
  127. package/dist/lib/plugins.js +4 -110
  128. package/dist/lib/project-resources.js +4 -1
  129. package/dist/lib/refresh.d.ts +5 -3
  130. package/dist/lib/refresh.js +7 -5
  131. package/dist/lib/resources/commands.js +4 -1
  132. package/dist/lib/resources/mcp.js +0 -4
  133. package/dist/lib/resources/permissions.d.ts +1 -1
  134. package/dist/lib/resources/permissions.js +1 -5
  135. package/dist/lib/resources/rules.js +4 -1
  136. package/dist/lib/resources/skills.js +4 -1
  137. package/dist/lib/resources/subagents.js +4 -0
  138. package/dist/lib/resources/types.d.ts +1 -1
  139. package/dist/lib/routines.d.ts +20 -0
  140. package/dist/lib/routines.js +6 -0
  141. package/dist/lib/run-defaults.d.ts +1 -0
  142. package/dist/lib/run-defaults.js +9 -0
  143. package/dist/lib/runner.d.ts +0 -12
  144. package/dist/lib/runner.js +71 -29
  145. package/dist/lib/sandbox.js +0 -16
  146. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  147. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  149. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  151. package/dist/lib/secrets/agent.d.ts +18 -0
  152. package/dist/lib/secrets/agent.js +131 -17
  153. package/dist/lib/secrets/audit.d.ts +46 -0
  154. package/dist/lib/secrets/audit.js +56 -0
  155. package/dist/lib/secrets/bundles.d.ts +12 -9
  156. package/dist/lib/secrets/bundles.js +47 -31
  157. package/dist/lib/secrets/index.d.ts +2 -1
  158. package/dist/lib/secrets/index.js +8 -2
  159. package/dist/lib/secrets/remote.d.ts +75 -2
  160. package/dist/lib/secrets/remote.js +139 -5
  161. package/dist/lib/secrets/scope.d.ts +26 -0
  162. package/dist/lib/secrets/scope.js +29 -0
  163. package/dist/lib/secrets/session-store.d.ts +10 -0
  164. package/dist/lib/secrets/session-store.js +64 -11
  165. package/dist/lib/secrets/sync.js +3 -3
  166. package/dist/lib/session/active.d.ts +117 -26
  167. package/dist/lib/session/active.js +199 -53
  168. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  169. package/dist/lib/session/actor-sidecar.js +101 -0
  170. package/dist/lib/session/db.d.ts +41 -1
  171. package/dist/lib/session/db.js +173 -8
  172. package/dist/lib/session/digest.js +126 -21
  173. package/dist/lib/session/discover.d.ts +19 -2
  174. package/dist/lib/session/discover.js +32 -11
  175. package/dist/lib/session/hook-sessions.d.ts +9 -0
  176. package/dist/lib/session/hook-sessions.js +57 -8
  177. package/dist/lib/session/origin-machine.d.ts +18 -0
  178. package/dist/lib/session/origin-machine.js +34 -0
  179. package/dist/lib/session/pid-registry.d.ts +13 -0
  180. package/dist/lib/session/render.d.ts +2 -0
  181. package/dist/lib/session/render.js +1 -1
  182. package/dist/lib/session/state.d.ts +1 -0
  183. package/dist/lib/session/state.js +1 -1
  184. package/dist/lib/session/sync/config.js +2 -2
  185. package/dist/lib/session/types.d.ts +16 -0
  186. package/dist/lib/smart-launch.d.ts +86 -0
  187. package/dist/lib/smart-launch.js +172 -0
  188. package/dist/lib/staleness/detectors/commands.js +2 -2
  189. package/dist/lib/staleness/detectors/permissions.js +1 -45
  190. package/dist/lib/staleness/writers/commands.js +4 -5
  191. package/dist/lib/staleness/writers/hooks.js +1 -1
  192. package/dist/lib/startup/command-registry.d.ts +1 -2
  193. package/dist/lib/startup/command-registry.js +2 -4
  194. package/dist/lib/subagents-registry.js +5 -12
  195. package/dist/lib/subagents.d.ts +0 -10
  196. package/dist/lib/subagents.js +0 -12
  197. package/dist/lib/teams/agents.d.ts +0 -2
  198. package/dist/lib/teams/agents.js +10 -27
  199. package/dist/lib/teams/index.d.ts +1 -1
  200. package/dist/lib/teams/index.js +1 -1
  201. package/dist/lib/types.d.ts +17 -9
  202. package/dist/lib/usage.d.ts +28 -5
  203. package/dist/lib/usage.js +271 -8
  204. package/dist/lib/versions.js +7 -1
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +25 -13
  207. package/package.json +1 -2
  208. package/dist/commands/check.d.ts +0 -15
  209. package/dist/commands/check.js +0 -180
  210. package/dist/commands/resources.d.ts +0 -5
  211. package/dist/lib/secrets/account-token.d.ts +0 -20
  212. package/dist/lib/secrets/account-token.js +0 -64
@@ -20,7 +20,7 @@
20
20
  import * as fs from 'fs';
21
21
  import * as path from 'path';
22
22
  import chalk from 'chalk';
23
- import { discoverSessions, resolveSessionById, isCompleteSessionId } from '../lib/session/discover.js';
23
+ import { discoverSessions, resolveSessionById, looksLikeSessionId } from '../lib/session/discover.js';
24
24
  import { findSessionsById } from '../lib/session/db.js';
25
25
  import { filterSessionsByQuery, parseAgentFilter } from './sessions.js';
26
26
  import { listLocalTranscripts, SYNC_AGENTS } from '../lib/session/sync/agents.js';
@@ -222,8 +222,8 @@ function resolveAgentShorthand(g) {
222
222
  return 'antigravity';
223
223
  return undefined;
224
224
  }
225
- /** ids > query > everything-in-scope. */
226
- function selectSessions(metas, selectors) {
225
+ /** ids > query > everything-in-scope. Exported for the id-resolution test. */
226
+ export function selectSessions(metas, selectors) {
227
227
  if (selectors.length === 0)
228
228
  return metas;
229
229
  const byId = [];
@@ -231,9 +231,11 @@ function selectSessions(metas, selectors) {
231
231
  for (const sel of selectors) {
232
232
  const trimmed = sel.trim();
233
233
  // Same reason as resolveSessionQuery: the discovered pool is a minority of
234
- // the index, so a complete id absent from it may still be indexed here.
234
+ // the index, so an id-shaped selector absent from it may still be indexed
235
+ // here. Gate on looksLikeSessionId, not isCompleteSessionId, so a SHORT id
236
+ // ("d3470b57") also resolves through the index instead of falling to content.
235
237
  const hits = resolveSessionById(metas, trimmed);
236
- const resolved = hits.length > 0 || !isCompleteSessionId(trimmed) ? hits : findSessionsById(trimmed);
238
+ const resolved = hits.length > 0 || !looksLikeSessionId(trimmed) ? hits : findSessionsById(trimmed);
237
239
  if (resolved.length > 0)
238
240
  byId.push(...resolved);
239
241
  else
@@ -243,11 +245,12 @@ function selectSessions(metas, selectors) {
243
245
  const seen = new Set();
244
246
  return byId.filter(s => (seen.has(s.id) ? false : (seen.add(s.id), true)));
245
247
  }
246
- // A selector that is a COMPLETE session id and still missed cannot be widened:
247
- // the id is unique, so the text query below could only bundle sessions that
248
- // merely mention it. Bundling those would ship unrelated transcripts to
249
- // whoever receives the export, so select nothing and let the caller report it.
250
- const missingIds = unmatched.filter(isCompleteSessionId);
248
+ // A selector that is an id (complete OR a short hex id/prefix) and still missed
249
+ // cannot be widened: the id is unique, so the text query below could only bundle
250
+ // sessions that merely mention it. Bundling those would ship unrelated
251
+ // transcripts to whoever receives the export, so select nothing and let the
252
+ // caller report it — a short id like "d3470b57" must never content-search.
253
+ const missingIds = unmatched.filter(looksLikeSessionId);
251
254
  if (missingIds.length > 0) {
252
255
  process.stderr.write(chalk.red(`No session with id ${missingIds.join(', ')} on this machine.\n`));
253
256
  return [];
@@ -4,10 +4,13 @@
4
4
  * (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
5
5
  * (RUSH-1415) can shell out to nudge a stalled agent with "continue".
6
6
  *
7
- * Resolution: find the active session by id, map its `provenance.reply` rail
8
- * (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
9
- * are externally addressable; `--pane`/`--pty` target a backend directly when the
10
- * handle is already known (skipping the session lookup).
7
+ * Resolution: find the active session by id, then resolve its exact split through
8
+ * the SAME canonical resolver the watchdog uses `resolveInjectTargetForSession`
9
+ * (lib/terminal/resolve.ts), precedence tmux > iterm > vscodium > pty. Sharing one
10
+ * resolver keeps the manual unblock path and the watchdog in agreement on which
11
+ * sessions are addressable (a prior duplicate resolver read only `provenance.reply`
12
+ * and could not address a VSCodium/Cursor terminal the watchdog handled fine).
13
+ * `--pane`/`--pty` target a backend directly when the handle is already known.
11
14
  */
12
15
  import type { Command } from 'commander';
13
16
  /** Attach the `inject` subcommand to an existing `sessions` command. */
@@ -4,33 +4,32 @@
4
4
  * (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
5
5
  * (RUSH-1415) can shell out to nudge a stalled agent with "continue".
6
6
  *
7
- * Resolution: find the active session by id, map its `provenance.reply` rail
8
- * (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
9
- * are externally addressable; `--pane`/`--pty` target a backend directly when the
10
- * handle is already known (skipping the session lookup).
7
+ * Resolution: find the active session by id, then resolve its exact split through
8
+ * the SAME canonical resolver the watchdog uses `resolveInjectTargetForSession`
9
+ * (lib/terminal/resolve.ts), precedence tmux > iterm > vscodium > pty. Sharing one
10
+ * resolver keeps the manual unblock path and the watchdog in agreement on which
11
+ * sessions are addressable (a prior duplicate resolver read only `provenance.reply`
12
+ * and could not address a VSCodium/Cursor terminal the watchdog handled fine).
13
+ * `--pane`/`--pty` target a backend directly when the handle is already known.
11
14
  */
12
15
  import chalk from 'chalk';
13
16
  import { getActiveSessions } from '../lib/session/active.js';
14
- import { injectTargetFromReplyRail } from '../lib/session/inject.js';
17
+ import { resolveInjectTargetForSession } from '../lib/terminal/resolve.js';
15
18
  import { injectIntoTerminal } from '../lib/terminal/index.js';
16
19
  import { setHelpSections } from '../lib/help.js';
17
- /** Resolve a session id (short or full) to an addressable terminal target. */
20
+ /** Resolve a session id (short or full) to an addressable terminal target, via the
21
+ * same resolver the watchdog uses so both agree on what is reachable. */
18
22
  async function resolveTarget(sessionId) {
19
23
  const sessions = await getActiveSessions();
20
24
  const match = sessions.find((s) => s.sessionId === sessionId || (s.sessionId != null && s.sessionId.startsWith(sessionId)));
21
25
  if (!match)
22
26
  return { target: null, reason: `No active session matches "${sessionId}".` };
23
- const rail = match.provenance?.reply;
24
- if (!rail) {
25
- return {
26
- target: null,
27
- reason: `Session "${sessionId}" has no addressable reply rail (not running under tmux). Relaunch it under a tmux/pty rail to inject.`,
28
- };
27
+ const resolution = resolveInjectTargetForSession(match);
28
+ if (!resolution.addressable) {
29
+ // Surface the resolver's precise reason (host/rail), not a generic "not tmux".
30
+ return { target: null, reason: `Session "${sessionId}": ${resolution.reason}` };
29
31
  }
30
- const target = injectTargetFromReplyRail(rail);
31
- if (!target)
32
- return { target: null, reason: `Session "${sessionId}" reply rail is not injectable.` };
33
- return { target };
32
+ return { target: resolution.target };
34
33
  }
35
34
  async function runInject(sessionId, text, options) {
36
35
  // Direct-target shortcuts skip the session lookup — the watchdog often already
@@ -97,8 +96,9 @@ export function registerSessionsInjectCommand(sessionsCmd) {
97
96
  notes: `
98
97
  - Ink-TUI Enter semantics: by default the text and Enter are two separate
99
98
  writes, which is what Claude's Ink TUI needs. --combined fuses them.
100
- - Only sessions running under a tmux pane are addressable by id today
101
- (provenance.ts reply rails). Use --pane/--pty for direct targeting.
99
+ - A session is addressable by id when it resolves to a precise split —
100
+ tmux, iTerm, a VSCodium/Cursor/VS Code integrated terminal, or a pty
101
+ (resolveInjectTargetForSession). Use --pane/--pty for direct targeting.
102
102
  - Built on the Terminal Engine (src/lib/terminal): --host runs the tmux /
103
103
  AppleScript spec over the same SSH transport the launch engine uses.
104
104
  `,
@@ -1,4 +1,5 @@
1
- import type { SessionMeta, TodoProgress } from '../lib/session/types.js';
1
+ import type { SessionEvent, SessionMeta, TodoProgress } from '../lib/session/types.js';
2
+ import { classifyFileChanges } from '../lib/session/digest.js';
2
3
  /**
3
4
  * Compact checklist tally for list rows and previews (RUSH-2045).
4
5
  * Example: `✓6/8 · A5 wiring runner`. Empty string when there is no list.
@@ -30,5 +31,18 @@ export interface SessionPickerConfig {
30
31
  }
31
32
  /** Build a cached multi-line preview string for display in the session picker. */
32
33
  export declare function buildPreview(session: SessionMeta): string;
34
+ /** Optional dirs-touched field the parser teammate may attach; we prefer it. */
35
+ type SessionMetaWithDirs = SessionMeta & {
36
+ dirsTouched?: string[];
37
+ };
38
+ /**
39
+ * Unique directories the session touched, compact and human-readable.
40
+ * Prefer `session.dirsTouched` when the parser teammate has populated it;
41
+ * otherwise derive from file-change + tool paths already available here.
42
+ */
43
+ export declare function directoriesTouched(session: SessionMetaWithDirs, events: SessionEvent[], changes: ReturnType<typeof classifyFileChanges>): string[];
44
+ /** Relativize a file path to its parent dir, short enough for one preview line. */
45
+ export declare function relativizeDir(filePath: string, cwd?: string): string | undefined;
33
46
  /** Show an interactive session picker and return the selected session with its action (resume or view). */
34
47
  export declare function sessionPicker(config: SessionPickerConfig): Promise<PickedSession | null>;
48
+ export {};
@@ -11,9 +11,9 @@ import chalk from 'chalk';
11
11
  import { truncate, humanDuration } from '../lib/format.js';
12
12
  import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
13
13
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
14
- import { linkPath, linkUrl, relativeToCwd } from '../lib/session/render.js';
14
+ import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
15
15
  import { linearIssueUrl } from '../lib/session/linear.js';
16
- import { extractTodoProgress } from '../lib/session/state.js';
16
+ import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
17
17
  import { renderMarkdown } from '../lib/markdown.js';
18
18
  import { itemPicker } from '../lib/picker.js';
19
19
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
@@ -137,7 +137,7 @@ function displayAgent(agent) {
137
137
  }
138
138
  const DOT = chalk.gray(' · ');
139
139
  function formatHeader(session, events) {
140
- const model = extractModel(events);
140
+ const model = extractModel(events) || session.model;
141
141
  const { startedAgo, duration } = extractTiming(events);
142
142
  const totalMessages = session.messageCount ?? countMessages(events);
143
143
  const totalTokens = session.tokenCount;
@@ -147,7 +147,7 @@ function formatHeader(session, events) {
147
147
  if (session.shortId)
148
148
  line1.push(chalk.dim(session.shortId));
149
149
  if (model)
150
- line1.push(chalk.bold.white(model));
150
+ line1.push(chalk.bold.white(shortenModel(model)));
151
151
  if (session.account)
152
152
  line1.push(chalk.gray(session.account));
153
153
  // Line 2: cwd · project · branch · started X ago · lasted Y
@@ -285,6 +285,8 @@ function formatCompactPreview(events, session) {
285
285
  let planFile = '';
286
286
  /** Latest checklist from the transcript (Claude TodoWrite / Codex update_plan). */
287
287
  let latestTodos;
288
+ let subAgentCount = 0;
289
+ const toolTags = new Set();
288
290
  for (const event of events) {
289
291
  if (event.type === 'message') {
290
292
  if (event.role === 'user' && !firstUser && event.content) {
@@ -298,6 +300,11 @@ function formatCompactPreview(events, session) {
298
300
  }
299
301
  else if (event.type === 'tool_use' && !event._local) {
300
302
  const tool = event.tool || '';
303
+ const command = event.command || '';
304
+ for (const tag of classifySessionTool(tool, command))
305
+ toolTags.add(tag);
306
+ if (isSubAgentTool(tool, command))
307
+ subAgentCount++;
301
308
  const p = event.path || event.args?.file_path || event.args?.path || '';
302
309
  if (['Read', 'read_file', 'view_file', 'cat_file', 'get_file'].includes(tool) && p) {
303
310
  filesRead.add(p);
@@ -369,6 +376,13 @@ function formatCompactPreview(events, session) {
369
376
  if (activity.length) {
370
377
  lines.push(chalk.cyan('Changes: ') + activity.join(chalk.gray(' · ')));
371
378
  }
379
+ const metadata = [
380
+ ...toolTags,
381
+ subAgentCount ? `${subAgentCount} sub-agent${subAgentCount === 1 ? '' : 's'}` : '',
382
+ ].filter(Boolean);
383
+ if (metadata.length) {
384
+ lines.push(chalk.cyan('Meta: ') + chalk.gray(metadata.join(' · ')));
385
+ }
372
386
  // Tool mix (top 4) — what kind of work this was.
373
387
  const hist = toolHistogram(toolCounts, 4);
374
388
  if (hist.length) {
@@ -403,12 +417,27 @@ function formatCompactPreview(events, session) {
403
417
  }
404
418
  return lines.map(l => ' ' + l).join('\n');
405
419
  }
420
+ function classifySessionTool(tool, command) {
421
+ const toolName = tool.toLowerCase();
422
+ const commandUses = (surface) => (new RegExp(`\\b(?:agents|ag)\\b[^\\n;&|]*\\b${surface}\\b`).test(command.toLowerCase()));
423
+ const tags = [];
424
+ if (/browser|webfetch|websearch/.test(toolName) || commandUses('browser'))
425
+ tags.push('browser');
426
+ if (/computer/.test(toolName) || commandUses('computer'))
427
+ tags.push('computer');
428
+ return tags;
429
+ }
430
+ function isSubAgentTool(tool, command) {
431
+ if (/^(Agent|Task)$/i.test(tool))
432
+ return true;
433
+ return /\b(?:agents|ag)\b[^\n;&|]*\b(?:run|cloud\s+run|teams\s+(?:add|start))\b/.test(command);
434
+ }
406
435
  /**
407
436
  * Unique directories the session touched, compact and human-readable.
408
437
  * Prefer `session.dirsTouched` when the parser teammate has populated it;
409
438
  * otherwise derive from file-change + tool paths already available here.
410
439
  */
411
- function directoriesTouched(session, events, changes) {
440
+ export function directoriesTouched(session, events, changes) {
412
441
  const fromMeta = session.dirsTouched;
413
442
  if (Array.isArray(fromMeta) && fromMeta.length > 0) {
414
443
  return fromMeta
@@ -437,19 +466,73 @@ function directoriesTouched(session, events, changes) {
437
466
  .map(([d]) => d)
438
467
  .slice(0, DIRS_TOUCHED_MAX);
439
468
  }
469
+ /**
470
+ * Claude names its per-project transcript store `~/.claude/projects/<slug>`, where
471
+ * `<slug>` is the cwd with every `/` AND `.` replaced by `-` — so `/Users/me/app`
472
+ * → `-Users-me-app` and `.agents/worktrees/x` → `--agents-worktrees-x`. That slug
473
+ * leaks into transcript paths as a leading segment (`<slug>/<session-id>/…`).
474
+ *
475
+ * The encoding is LOSSY and irreversible (`-`, `/`, and `.` all collapse to `-`),
476
+ * so we never try to decode it back to a path. Instead we ENCODE the comparison
477
+ * targets (cwd, the worktree marker) into the same slug space and match there. See
478
+ * {@link relativizeDir}. `encodeClaudeSlug` mirrors Claude's own transform.
479
+ */
480
+ function encodeClaudeSlug(absPath) {
481
+ return absPath.replace(/[/.]/g, '-');
482
+ }
483
+ /** The `.agents/worktrees/<name>` marker, Claude-slug-encoded (`/.` → `--`). */
484
+ const SLUG_WORKTREE_RE = /--agents-worktrees-(.+)$/;
440
485
  /** Relativize a file path to its parent dir, short enough for one preview line. */
441
- function relativizeDir(filePath, cwd) {
486
+ export function relativizeDir(filePath, cwd) {
442
487
  const norm = filePath.replace(/\\/g, '/');
443
488
  if (!norm || norm.includes('node_modules') || norm.includes('/.git/') || norm.includes('/plans/')) {
444
489
  return undefined;
445
490
  }
446
491
  let dir = path.posix.dirname(norm);
492
+ // Claude project-slug form: a leading `-`-segment (`-home-me-…`) that carries
493
+ // the cwd, lossily encoded. Handle it in SLUG SPACE — never lossy-decode to a
494
+ // fake path. The slug is the first path segment; any real `/`-subdirs after it
495
+ // are Claude's internal storage (`<session-id>/scratchpad|tasks`), not code.
496
+ if (dir.startsWith('-')) {
497
+ const slash = dir.indexOf('/');
498
+ const slug = slash === -1 ? dir : dir.slice(0, slash);
499
+ // CWD FIRST: if the slug is (or is under) this session's own cwd, the leaked
500
+ // path is Claude's internal projects-storage scratch (`<id>/scratchpad`) —
501
+ // not a meaningful code dir — so drop it like node_modules. Precedence over
502
+ // the worktree collapse so a session editing its OWN worktree isn't relabeled.
503
+ if (cwd) {
504
+ const cwdSlug = encodeClaudeSlug(cwd.replace(/\\/g, '/').replace(/\/$/, ''));
505
+ if (slug === cwdSlug || slug.startsWith(cwdSlug + '-'))
506
+ return undefined;
507
+ }
508
+ // Only a DIFFERENT worktree than cwd reaches here: a worktree encodes its
509
+ // `/.agents/worktrees/<name>` marker as `--agents-worktrees-<name>`, so
510
+ // collapse to the worktree name to disambiguate. (The name may contain `-`;
511
+ // we can't losslessly re-split it, so show the whole encoded remainder.)
512
+ const wtSlug = slug.match(SLUG_WORKTREE_RE);
513
+ if (wtSlug)
514
+ return `⧉ ${wtSlug[1]}`;
515
+ // An unattributable slug: don't invent a `/`-joined fake path. Show only the
516
+ // trailing `-`-group as a minimal, honest token.
517
+ const segs = slug.split('-').filter(Boolean);
518
+ return segs.length ? segs[segs.length - 1] : undefined;
519
+ }
520
+ // CWD FIRST for real paths too: strip the session-cwd prefix so a session
521
+ // editing its OWN worktree renders the concise relative remainder (`src/lib`),
522
+ // not the longer `⧉ <slug>/…` collapse.
447
523
  if (cwd) {
448
524
  const base = cwd.replace(/\\/g, '/').replace(/\/$/, '');
449
525
  if (dir === base)
450
526
  return '.';
451
527
  if (dir.startsWith(base + '/'))
452
- dir = dir.slice(base.length + 1);
528
+ return dir.slice(base.length + 1);
529
+ }
530
+ // No cwd match. If the dir is in a (different) git worktree, collapse to the
531
+ // worktree NAME + in-worktree remainder to disambiguate; else home→`~` + trim.
532
+ const wt = dir.match(WORKTREE_RE);
533
+ if (wt) {
534
+ const after = dir.slice(dir.indexOf(wt[0]) + wt[0].length).replace(/^\//, '');
535
+ return after ? `⧉ ${wt[1]}/${after}` : `⧉ ${wt[1]}`;
453
536
  }
454
537
  // Collapse home prefix.
455
538
  const home = (process.env.HOME || '').replace(/\\/g, '/');
@@ -1,2 +1,18 @@
1
1
  import type { Command } from 'commander';
2
+ import { type Packing } from '../lib/terminal/index.js';
3
+ interface ResumeOptions {
4
+ agent?: string;
5
+ all?: boolean;
6
+ teams?: boolean;
7
+ since?: string;
8
+ limit?: string;
9
+ host?: string;
10
+ iterm?: boolean;
11
+ ghostty?: boolean;
12
+ tmux?: boolean;
13
+ vscodium?: boolean;
14
+ splits?: boolean;
15
+ }
2
16
  export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
17
+ export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
18
+ export {};
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * Unlike the single-select picker behind bare `agents sessions` (which resumes
6
6
  * one session in place), this opens a checkbox picker, then asks where the
7
- * chosen sessions should resume. By default they pack two-per-tab (session 1 in
8
- * a new tab, session 2 split beside it, session 3 a new tab, …) in the terminal
9
- * you're in iTerm / Ghostty / tmux, locally or on a remote host via --host.
7
+ * chosen sessions should resume. By default each session opens in its own tab in
8
+ * the terminal you're in iTerm / Ghostty / tmux, locally or on a remote host
9
+ * via --host. `--splits` opts into packing two sessions side by side per tab.
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import chalk from 'chalk';
@@ -36,25 +36,24 @@ export function registerSessionsResumeCommand(sessionsCmd) {
36
36
  .option('--ghostty', 'Force the Ghostty backend')
37
37
  .option('--tmux', 'Force the tmux backend')
38
38
  .option('--vscodium', 'Force the VSCodium agent-terminal backend (swarm-ext)')
39
- .option('--tabs', 'One tab per session (default: pack two panes per tab)');
39
+ .option('--splits', 'Pack two sessions side by side per tab (default: one tab per session)');
40
40
  setHelpSections(cmd, {
41
41
  examples: `
42
- # Pick several sessions; they pack two-per-tab in your current terminal
42
+ # Pick several sessions; each opens in its own tab
43
43
  agents sessions resume
44
44
 
45
45
  # Pre-filter the pool before selecting (space in the filter → use [query])
46
46
  agents sessions resume "auth middleware"
47
47
 
48
- # Force a backend / one tab each / a remote host
48
+ # Force a backend / side-by-side splits / a remote host
49
49
  agents sessions resume --ghostty
50
50
  agents sessions resume --vscodium
51
- agents sessions resume --tabs
51
+ agents sessions resume --splits
52
52
  agents sessions resume --host zion --tmux
53
53
  `,
54
54
  notes: `
55
55
  - space toggles a session, enter confirms; tab toggles the preview pane.
56
- - Layout: two-per-tab by default (session 1 new tab, session 2 split beside it, …). --tabs disables splitting.
57
- - VSCodium defaults to one editor tab per session (matches swarm-ext's native agent-terminal UX); the others default to two-per-tab.
56
+ - Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
58
57
  - Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
59
58
  - --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
60
59
  - --host <alias> resumes on a remote machine over the same SSH transport as 'sessions --host' (defaults to tmux).
@@ -153,10 +152,9 @@ async function sessionsResumeAction(query, options) {
153
152
  await resumeSessionInPlace(it.session);
154
153
  return;
155
154
  }
156
- // 5b. Fan out through the engine. Split-packing (two-per-tab) suits the
157
- // terminal apps; the VSCodium backend defaults to one editor tab per session,
158
- // matching swarm-ext's native agent-terminal UX. --tabs forces tabs anywhere.
159
- const packing = options.tabs || backend === 'vscodium-agent' ? 'tabs' : 'two-per-tab';
155
+ // 5b. Fan out through the engine. Full-width tabs are the default for batch
156
+ // recovery; callers can explicitly opt into pairs of side-by-side panes.
157
+ const packing = resolveResumePacking(options);
160
158
  const where = options.host ? `${backend} on ${options.host}` : backend;
161
159
  console.log(chalk.gray(`Opening ${items.length} session${items.length === 1 ? '' : 's'} in ${where} (${packing})…`));
162
160
  const results = await openSurfaces(items.map((it) => ({ cwd: it.cwd, command: it.command })), { backend, host: options.host, packing });
@@ -174,6 +172,9 @@ async function sessionsResumeAction(query, options) {
174
172
  });
175
173
  console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
176
174
  }
175
+ export function resolveResumePacking(options) {
176
+ return options.splits ? 'two-per-tab' : 'tabs';
177
+ }
177
178
  /**
178
179
  * Decide which backend to launch into. Returns a concrete backend, `'inplace'`
179
180
  * (resume in the current process — no GUI/tmux available), or `'cancel'` (the
@@ -1,6 +1,7 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
3
3
  import { type ActiveSession } from '../lib/session/active.js';
4
+ import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
4
5
  import { type PickedSession } from './sessions-picker.js';
5
6
  /**
6
7
  * Strip terminal/harness noise from a preview so the column stays a single line
@@ -39,12 +40,23 @@ export declare function liveGlyphAndPreview(a: ActiveSession | undefined): {
39
40
  glyph: string;
40
41
  preview: string;
41
42
  };
43
+ /**
44
+ * The one-word live status for a listing row — `working` / `waiting` / `idle`,
45
+ * the same three states the `--active` column shows, so the default list is no
46
+ * longer just a glyph. `waiting` is the actionable "needs you" case (a question /
47
+ * permission / plan-review), kept distinct from `idle` (stopped) and `working`.
48
+ * Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
49
+ * exported for the row tests.
50
+ */
51
+ export declare function liveStatusWord(a: ActiveSession | undefined): string;
42
52
  /**
43
53
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
44
54
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
45
55
  * into a scannable column instead of jamming against a truncated topic.
46
56
  */
47
57
  export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'>): string;
58
+ export declare function linkTicketCell(s: Pick<SessionMeta, 'ticketId' | 'prNumber' | 'prUrl'>, label: string): string;
59
+ export declare function linkCwdCell(s: Pick<SessionMeta, 'cwd' | '_remote'>, label: string): string;
48
60
  /**
49
61
  * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
50
62
  * joins active sessions on ticketId + project, but the raw ActiveSession nests
@@ -57,7 +69,16 @@ export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'
57
69
  export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
58
70
  ticketId: string | null;
59
71
  project: string | null;
72
+ prLink: string | null;
60
73
  }>;
74
+ /**
75
+ * Compact owner display for the `--active` owner column: the local-part of a
76
+ * resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
77
+ * when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
78
+ * or absent (a launch predating actor stamping). Honest by design — an
79
+ * unresolved local run shows no owner rather than inventing one.
80
+ */
81
+ export declare function ownerLabel(s: ActiveSession): string;
61
82
  /** Grouped + sorted view of active sessions for the --active renderer. */
62
83
  export interface ActiveSessionsLayout {
63
84
  workspaces: Array<{
@@ -150,6 +171,32 @@ export declare function shouldIncludeLocal(hosts: string[] | undefined, self: st
150
171
  * Exported for unit testing.
151
172
  */
152
173
  export declare function remoteHostsToDial(hosts: string[] | undefined, self: string): string[] | undefined;
174
+ /**
175
+ * The fleet-wide live-session set behind every `--active` surface. Local sessions
176
+ * come from `getActiveSessions()` and (unless `--local`) the registered online
177
+ * devices from `ag devices` are folded in over SSH. An explicit `--host`/`--device`
178
+ * list SCOPES the sweep to exactly those machines — the local machine is included
179
+ * only when it is itself named — so `--host` is a filter, not an addition.
180
+ *
181
+ * This is the single gather: the static renderer AND the interactive browser both
182
+ * call it, so the browser can never disagree with `--active --json` about which
183
+ * sessions are live (it used to call the local-only `getActiveSessions()` directly
184
+ * and silently hid every remote session).
185
+ */
186
+ export declare function gatherActiveSessions(opts?: {
187
+ local?: boolean;
188
+ hosts?: string[];
189
+ }): Promise<{
190
+ sessions: ActiveSession[];
191
+ remoteDeviceCount: number;
192
+ }>;
193
+ /** One flat table row:
194
+ * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
195
+ * `doing` is the live preview when running, else the topic. The `ticket` column
196
+ * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
197
+ * when `showTicket` — otherwise a listing with no refs would waste a column of
198
+ * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
199
+ export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns): string;
153
200
  /**
154
201
  * Group key for the overview: prefer the indexed project name; else fold the cwd
155
202
  * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
@@ -201,8 +248,19 @@ export interface PickerColumns {
201
248
  /** Total width of the machine column, sized to the widest compacted hostname
202
249
  * in the pool (capped). Falls back to PICKER_MACHINE_W when absent. */
203
250
  machineWidth?: number;
251
+ /** Render the model column only when at least one row carries a model. */
252
+ showModel?: boolean;
253
+ /** Pool-sized model column width, including one trailing separator cell. */
254
+ modelWidth?: number;
204
255
  /** Render the ticket/PR column (only when at least one row carries a ref). */
205
256
  showTicket?: boolean;
257
+ /**
258
+ * Render the host-program column — which terminal/editor the session is running
259
+ * in (`ghostty`, `codium`, `tmux→ghostty`, …). Live-only: it comes from the
260
+ * active-session scan, so it is set by the running-filtered browser and stays
261
+ * off for a plain transcript listing, where no row has a host.
262
+ */
263
+ showHost?: boolean;
206
264
  /**
207
265
  * Cells the picker prepends before each row: 2 for the single-select cursor
208
266
  * ('> '), 6 for the multi-select cursor + checkbox ('> [x] '). Reserved from
@@ -223,13 +281,30 @@ export declare function machineLabeler(machines: string[]): (m: string) => strin
223
281
  * than one box, the ticket column only when some row carries a PR/ticket ref.
224
282
  */
225
283
  export declare function pickerColumnsFor(sessions: SessionMeta[]): PickerColumns;
226
- export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag): string;
284
+ /**
285
+ * Which program a live session is running in, for the picker's host column: the
286
+ * immediate host app (`codium`, `ghostty`, `tmux`, `iterm`, …) and — when a tmux
287
+ * session is currently being watched through a different app — the app it is
288
+ * viewed in, as `tmux→ghostty`. A tmux session with no attached client stays a
289
+ * bare `tmux`, which is exactly "running detached". Empty when the session has no
290
+ * resolvable host (cloud rows, an unreadable process env).
291
+ */
292
+ export declare function liveHostLabel(a: ActiveSession | undefined): string;
293
+ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag, host?: string): string;
227
294
  /**
228
295
  * Pick a hint to show above the picker. Deterministic (keys off the pool size)
229
296
  * so it stays fixed across the picker's re-renders within a single run.
230
297
  */
231
298
  export declare function formatPickerTip(sessions: SessionMeta[]): string;
232
299
  export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string): Promise<PickedSession | null>;
300
+ /**
301
+ * Row-id prefix for a live session whose agent has not reported a session id yet
302
+ * (a booting harness, a queued teammate). The browser lists these so nothing live
303
+ * is hidden, but they address no transcript — {@link isIdlessLiveRow} is what the
304
+ * pick handler checks before trying to read or resume one.
305
+ */
306
+ export declare const LIVE_ROW_PREFIX = "live:";
307
+ export declare function isIdlessLiveRow(s: SessionMeta): boolean;
233
308
  export declare function handlePickedSession(picked: PickedSession): Promise<void>;
234
309
  /**
235
310
  * Resume a session in the current terminal — a foreground takeover of this
@@ -285,6 +360,56 @@ export interface SessionQueryResolution {
285
360
  export declare function resolveSessionQuery(pool: SessionMeta[], query: string): SessionQueryResolution;
286
361
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
287
362
  export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
363
+ /**
364
+ * Whether a missed local id lookup should widen to a cross-machine sweep.
365
+ *
366
+ * Gate (all must hold):
367
+ * - the query is id-shaped (`looksLikeSessionId`) — only an identifier resolves
368
+ * across the fleet; a search phrase never does.
369
+ * - not `--local` — the caller opted out of cross-machine lookup (deterministic
370
+ * local behavior for scripts, RUSH-2024 acceptance: "--local still restricts").
371
+ * - `AGENTS_SESSIONS_LOCAL` is unset — we are not ourselves a peer answering a
372
+ * parent's sweep, so a locate can never recurse (RUSH-2024: avoid double-fan-out).
373
+ *
374
+ * Pure + exported so the gate is unit-tested without driving discovery / SSH.
375
+ */
376
+ export declare function shouldFanOutForId(query: string, local: boolean | undefined): boolean;
377
+ /** Injectable SSH/peer boundary so the fleet-resolve logic is unit-testable
378
+ * without a live tailnet. Production wires these to the real remote-list infra. */
379
+ export interface FleetResolveDeps {
380
+ gatherRemoteList: typeof gatherRemoteList;
381
+ runOnPeer: typeof runOnPeer;
382
+ }
383
+ /** One distinct machine that reported the id, plus its winning row. */
384
+ interface FleetHit {
385
+ machine: string;
386
+ session: SessionMeta;
387
+ }
388
+ /** Group a fleet sweep's rows to the DISTINCT machines that hold the id. Each
389
+ * peer answered `sessions <id> --json --local`, which (post-fix) id-resolves and
390
+ * so returns the matching row(s); a peer with a synced MIRROR of the same id can
391
+ * emit more than one row, so we keep the first per machine. Rows the peer somehow
392
+ * returned that do NOT match the id (defensive against version skew) are dropped
393
+ * so a stray content hit can never masquerade as an exact resolution. */
394
+ export declare function fleetHitsById(rows: SessionMeta[], id: string): FleetHit[];
395
+ /**
396
+ * Locate a full session id across the online fleet and render it from the machine
397
+ * that holds it. The local disk already missed; this fans `sessions <id> --json
398
+ * --all` out to every registered online peer (or the explicit `hosts` set),
399
+ * groups the rows to distinct machines, then:
400
+ *
401
+ * - exactly one machine → delegate rendering to that peer via `runOnPeer`
402
+ * (its transcript and agent binary live there — a local `--host` hop would
403
+ * re-discover locally and dead-end), returning `'rendered'`.
404
+ * - more than one machine → print the conflict with machine labels so the user
405
+ * can disambiguate with `--device <host>`, returning `'conflict'`.
406
+ * - none → `'not-found'`, letting the caller print the local
407
+ * "no session on this machine" message.
408
+ *
409
+ * No fuzzy/content fallback: the sweep forwards a UUID, each peer id-resolves it,
410
+ * and `fleetHitsById` drops anything that isn't an exact id match.
411
+ */
412
+ export declare function resolveSessionAcrossFleet(id: string, mode: ViewMode, hosts?: string[], deps?: FleetResolveDeps): Promise<'rendered' | 'conflict' | 'not-found'>;
288
413
  /** Register the `agents sessions` command with all its options and help text. */
289
414
  export declare function registerSessionsCommands(program: Command): void;
290
415
  export {};