@phnx-labs/agents-cli 1.20.78 → 1.20.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +26 -26
  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 +66 -0
  12. package/dist/commands/doctor.js +596 -33
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  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/menubar.js +12 -0
  22. package/dist/commands/models.js +1 -1
  23. package/dist/commands/packages.js +6 -6
  24. package/dist/commands/permissions.js +1 -1
  25. package/dist/commands/profiles.js +1 -1
  26. package/dist/commands/pull.d.ts +1 -1
  27. package/dist/commands/pull.js +4 -4
  28. package/dist/commands/repo.d.ts +78 -0
  29. package/dist/commands/repo.js +187 -11
  30. package/dist/commands/routines.d.ts +8 -1
  31. package/dist/commands/routines.js +241 -77
  32. package/dist/commands/rules.js +13 -12
  33. package/dist/commands/secrets.d.ts +3 -1
  34. package/dist/commands/secrets.js +100 -32
  35. package/dist/commands/sessions-inject.d.ts +7 -4
  36. package/dist/commands/sessions-inject.js +18 -18
  37. package/dist/commands/sessions-picker.js +32 -3
  38. package/dist/commands/sessions-resume.d.ts +16 -0
  39. package/dist/commands/sessions-resume.js +14 -13
  40. package/dist/commands/sessions.d.ts +115 -0
  41. package/dist/commands/sessions.js +189 -35
  42. package/dist/commands/set.d.ts +15 -0
  43. package/dist/commands/set.js +79 -0
  44. package/dist/commands/setup-fleet.d.ts +21 -0
  45. package/dist/commands/setup-fleet.js +201 -0
  46. package/dist/commands/setup-secrets.d.ts +20 -0
  47. package/dist/commands/setup-secrets.js +223 -0
  48. package/dist/commands/setup.js +17 -3
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/sync.js +7 -2
  51. package/dist/commands/teams-picker.js +0 -1
  52. package/dist/commands/teams.d.ts +33 -1
  53. package/dist/commands/teams.js +208 -44
  54. package/dist/commands/versions.js +10 -5
  55. package/dist/commands/view.d.ts +1 -0
  56. package/dist/commands/view.js +11 -0
  57. package/dist/index.js +52 -8
  58. package/dist/lib/acp/harnesses.js +0 -7
  59. package/dist/lib/activity.d.ts +156 -0
  60. package/dist/lib/activity.js +282 -0
  61. package/dist/lib/agents.d.ts +9 -2
  62. package/dist/lib/agents.js +72 -63
  63. package/dist/lib/browser/service.js +3 -0
  64. package/dist/lib/browser/types.d.ts +7 -0
  65. package/dist/lib/capabilities.js +6 -2
  66. package/dist/lib/crabbox/lease.js +2 -2
  67. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  68. package/dist/lib/crabbox/setup-copy.js +4 -4
  69. package/dist/lib/daemon.d.ts +18 -0
  70. package/dist/lib/daemon.js +47 -6
  71. package/dist/lib/devices/fleet.d.ts +1 -1
  72. package/dist/lib/devices/fleet.js +1 -1
  73. package/dist/lib/doctor-diff.d.ts +20 -0
  74. package/dist/lib/doctor-diff.js +6 -1
  75. package/dist/lib/drift.d.ts +31 -11
  76. package/dist/lib/drift.js +58 -7
  77. package/dist/lib/events.d.ts +8 -1
  78. package/dist/lib/events.js +10 -1
  79. package/dist/lib/exec.d.ts +7 -0
  80. package/dist/lib/exec.js +69 -11
  81. package/dist/lib/feed-post.d.ts +28 -1
  82. package/dist/lib/feed-post.js +110 -2
  83. package/dist/lib/fleet/apply.js +8 -1
  84. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  85. package/dist/lib/fleet/auth-sync.js +25 -11
  86. package/dist/lib/fleet/remote-login.js +5 -0
  87. package/dist/lib/git.d.ts +14 -14
  88. package/dist/lib/git.js +41 -44
  89. package/dist/lib/hooks.d.ts +71 -0
  90. package/dist/lib/hooks.js +245 -95
  91. package/dist/lib/hosts/dispatch.d.ts +28 -1
  92. package/dist/lib/hosts/dispatch.js +33 -5
  93. package/dist/lib/hosts/option.js +2 -2
  94. package/dist/lib/hosts/passthrough.d.ts +22 -1
  95. package/dist/lib/hosts/passthrough.js +238 -13
  96. package/dist/lib/hosts/run-target.d.ts +2 -0
  97. package/dist/lib/hosts/run-target.js +1 -0
  98. package/dist/lib/hq/floor.d.ts +1 -1
  99. package/dist/lib/hq/floor.js +6 -1
  100. package/dist/lib/mcp.js +3 -71
  101. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  102. package/dist/lib/menubar/install-menubar.d.ts +24 -0
  103. package/dist/lib/menubar/install-menubar.js +52 -2
  104. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  105. package/dist/lib/menubar/notify-desktop.js +41 -5
  106. package/dist/lib/merged-resources.d.ts +11 -0
  107. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  108. package/dist/lib/permissions.d.ts +0 -32
  109. package/dist/lib/permissions.js +4 -157
  110. package/dist/lib/picker.js +1 -1
  111. package/dist/lib/platform/process.d.ts +16 -5
  112. package/dist/lib/platform/process.js +54 -0
  113. package/dist/lib/plugins.d.ts +0 -8
  114. package/dist/lib/plugins.js +4 -110
  115. package/dist/lib/project-resources.js +4 -1
  116. package/dist/lib/refresh.d.ts +5 -3
  117. package/dist/lib/refresh.js +7 -5
  118. package/dist/lib/resources/commands.js +4 -1
  119. package/dist/lib/resources/mcp.js +0 -4
  120. package/dist/lib/resources/permissions.d.ts +1 -1
  121. package/dist/lib/resources/permissions.js +1 -5
  122. package/dist/lib/resources/rules.js +4 -1
  123. package/dist/lib/resources/skills.js +4 -1
  124. package/dist/lib/resources/subagents.js +4 -0
  125. package/dist/lib/resources/types.d.ts +1 -1
  126. package/dist/lib/routines.d.ts +33 -0
  127. package/dist/lib/routines.js +122 -0
  128. package/dist/lib/run-defaults.d.ts +1 -0
  129. package/dist/lib/run-defaults.js +9 -0
  130. package/dist/lib/runner.d.ts +0 -12
  131. package/dist/lib/runner.js +49 -10
  132. package/dist/lib/scheduler.js +16 -4
  133. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  134. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  135. package/dist/lib/secrets/agent.d.ts +16 -0
  136. package/dist/lib/secrets/agent.js +105 -3
  137. package/dist/lib/secrets/audit.d.ts +46 -0
  138. package/dist/lib/secrets/audit.js +56 -0
  139. package/dist/lib/secrets/bundles.d.ts +11 -8
  140. package/dist/lib/secrets/bundles.js +31 -23
  141. package/dist/lib/secrets/index.d.ts +2 -1
  142. package/dist/lib/secrets/index.js +8 -2
  143. package/dist/lib/secrets/remote.d.ts +75 -2
  144. package/dist/lib/secrets/remote.js +139 -5
  145. package/dist/lib/secrets/sync.js +3 -3
  146. package/dist/lib/session/active.d.ts +128 -26
  147. package/dist/lib/session/active.js +192 -53
  148. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  149. package/dist/lib/session/actor-sidecar.js +101 -0
  150. package/dist/lib/session/db.d.ts +4 -1
  151. package/dist/lib/session/db.js +65 -8
  152. package/dist/lib/session/discover.d.ts +4 -0
  153. package/dist/lib/session/discover.js +13 -0
  154. package/dist/lib/session/hook-sessions.d.ts +9 -0
  155. package/dist/lib/session/hook-sessions.js +57 -8
  156. package/dist/lib/session/pid-registry.d.ts +13 -0
  157. package/dist/lib/session/render.d.ts +2 -0
  158. package/dist/lib/session/render.js +1 -1
  159. package/dist/lib/session/types.d.ts +16 -0
  160. package/dist/lib/staleness/detectors/commands.js +2 -2
  161. package/dist/lib/staleness/detectors/permissions.js +1 -45
  162. package/dist/lib/staleness/writers/commands.js +4 -5
  163. package/dist/lib/staleness/writers/hooks.js +1 -1
  164. package/dist/lib/startup/command-registry.d.ts +1 -2
  165. package/dist/lib/startup/command-registry.js +2 -4
  166. package/dist/lib/state.js +57 -1
  167. package/dist/lib/subagents-registry.js +5 -12
  168. package/dist/lib/subagents.d.ts +0 -10
  169. package/dist/lib/subagents.js +0 -12
  170. package/dist/lib/teams/agents.d.ts +0 -2
  171. package/dist/lib/teams/agents.js +10 -27
  172. package/dist/lib/teams/index.d.ts +1 -1
  173. package/dist/lib/teams/index.js +1 -1
  174. package/dist/lib/types.d.ts +17 -9
  175. package/dist/lib/usage.d.ts +28 -5
  176. package/dist/lib/usage.js +271 -8
  177. package/dist/lib/versions.js +7 -1
  178. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  179. package/dist/lib/watchdog/watchdog.js +25 -13
  180. package/package.json +1 -2
  181. package/dist/commands/check.d.ts +0 -15
  182. package/dist/commands/check.js +0 -180
  183. package/dist/commands/resources.d.ts +0 -5
@@ -3,6 +3,66 @@ import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types
3
3
  import { type ActiveSession } from '../lib/session/active.js';
4
4
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
5
5
  import { type PickedSession } from './sessions-picker.js';
6
+ interface SessionFilterOptions {
7
+ agent?: string;
8
+ project?: string;
9
+ all?: boolean;
10
+ teams?: boolean;
11
+ routine?: boolean;
12
+ since?: string;
13
+ until?: string;
14
+ }
15
+ interface SessionsOptions extends SessionFilterOptions {
16
+ /** Also list sessions from the user's own unmanaged ~/.<agent> installs. */
17
+ unmanaged?: boolean;
18
+ query?: string;
19
+ limit?: string;
20
+ sort?: string;
21
+ json?: boolean;
22
+ markdown?: boolean;
23
+ /** Commander populates this from `--no-redact`: true by default, false when the flag is passed. */
24
+ redact?: boolean;
25
+ include?: string;
26
+ exclude?: string;
27
+ first?: string;
28
+ last?: string;
29
+ artifacts?: boolean;
30
+ artifact?: string;
31
+ active?: boolean;
32
+ /** Emit the on-disk session-scan directories (requires --json); for watchers. */
33
+ roots?: boolean;
34
+ cloud?: boolean;
35
+ host?: string[];
36
+ /** Group the listing by directory and drop the id/version columns. */
37
+ tree?: boolean;
38
+ /** Force the plain flat table instead of the grouped default overview. */
39
+ flat?: boolean;
40
+ /** With --active: show only sessions waiting on user input; exit 1 if any. */
41
+ waiting?: boolean;
42
+ /** Enrich the listing with live glyphs/preview for running rows. Default on;
43
+ * `--no-live` sets this false. Commander's `--no-` convention. */
44
+ live?: boolean;
45
+ /** Force local-only: skip the cross-machine SSH fan-out (both the default
46
+ * listing and --active). */
47
+ local?: boolean;
48
+ /** --device <target...> — alias for --host; resolves against the device registry. */
49
+ device?: string[];
50
+ /** Per-agent shorthands: aliases for `--agent <name>` (prioritized harnesses). */
51
+ claude?: boolean;
52
+ codex?: boolean;
53
+ kimi?: boolean;
54
+ antigravity?: boolean;
55
+ grok?: boolean;
56
+ opencode?: boolean;
57
+ /** Force the printed listing even on a TTY. Commander's `--no-` convention:
58
+ * `--no-interactive` sets this false, opting out of the interactive browser. */
59
+ interactive?: boolean;
60
+ /** Print the canonical `ag sessions …` command for the given flags and exit —
61
+ * the non-interactive twin of the browser's `y` hotkey. */
62
+ printCmd?: boolean;
63
+ /** Print a compact preview of the matched session and exit (no pager). */
64
+ preview?: boolean;
65
+ }
6
66
  /**
7
67
  * Strip terminal/harness noise from a preview so the column stays a single line
8
68
  * of plain prose: OSC title escapes, CSI/SGR ANSI, and the harness wrapper tags
@@ -10,6 +70,16 @@ import { type PickedSession } from './sessions-picker.js';
10
70
  * a captured transcript tail. Collapses runs of whitespace.
11
71
  */
12
72
  export declare function cleanPreview(text: string): string;
73
+ /**
74
+ * Build the live description for an active session: checklist progress (when
75
+ * present) plus the state engine's preview (the latest turn), a user label, or
76
+ * the first-prompt topic. Used by both the flat listing's `doing` cell and as
77
+ * the snippet half of the --active row (identity is layered on in printActiveRow).
78
+ *
79
+ * Covers every ActiveSession context: terminal (interactive), headless, teams,
80
+ * cloud, and sub-agent rows that share the same ActiveSession.todos field.
81
+ */
82
+ export declare function buildSessionDescription(s: ActiveSession): string;
13
83
  /**
14
84
  * Identity + checklist + live snippet for an --active / cross-machine row.
15
85
  * Surfaces agent-adjacent identity the flat table already has (label, project)
@@ -40,12 +110,23 @@ export declare function liveGlyphAndPreview(a: ActiveSession | undefined): {
40
110
  glyph: string;
41
111
  preview: string;
42
112
  };
113
+ /**
114
+ * The one-word live status for a listing row — `working` / `waiting` / `idle`,
115
+ * the same three states the `--active` column shows, so the default list is no
116
+ * longer just a glyph. `waiting` is the actionable "needs you" case (a question /
117
+ * permission / plan-review), kept distinct from `idle` (stopped) and `working`.
118
+ * Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
119
+ * exported for the row tests.
120
+ */
121
+ export declare function liveStatusWord(a: ActiveSession | undefined): string;
43
122
  /**
44
123
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
45
124
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
46
125
  * into a scannable column instead of jamming against a truncated topic.
47
126
  */
48
127
  export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'>): string;
128
+ export declare function linkTicketCell(s: Pick<SessionMeta, 'ticketId' | 'prNumber' | 'prUrl'>, label: string): string;
129
+ export declare function linkCwdCell(s: Pick<SessionMeta, 'cwd' | '_remote'>, label: string): string;
49
130
  /**
50
131
  * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
51
132
  * joins active sessions on ticketId + project, but the raw ActiveSession nests
@@ -58,7 +139,16 @@ export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'
58
139
  export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
59
140
  ticketId: string | null;
60
141
  project: string | null;
142
+ prLink: string | null;
61
143
  }>;
144
+ /**
145
+ * Compact owner display for the `--active` owner column: the local-part of a
146
+ * resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
147
+ * when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
148
+ * or absent (a launch predating actor stamping). Honest by design — an
149
+ * unresolved local run shows no owner rather than inventing one.
150
+ */
151
+ export declare function ownerLabel(s: ActiveSession): string;
62
152
  /** Grouped + sorted view of active sessions for the --active renderer. */
63
153
  export interface ActiveSessionsLayout {
64
154
  workspaces: Array<{
@@ -170,6 +260,27 @@ export declare function gatherActiveSessions(opts?: {
170
260
  sessions: ActiveSession[];
171
261
  remoteDeviceCount: number;
172
262
  }>;
263
+ /**
264
+ * A bare interactive fleet listing — no query, no render/filter flag — that the
265
+ * `runSessionBrowser` picker can represent. The single predicate shared by the
266
+ * bare-browser branch and the `--host` early-return guard so they can't drift:
267
+ * when this holds, an explicit `--host`/`--device` scope is folded into the
268
+ * browser (preview-rich, selectable) instead of the legacy per-host raw stream.
269
+ */
270
+ export declare function isBareBrowserListing(options: SessionsOptions, query: string | undefined): boolean;
271
+ /**
272
+ * Pure flag-gate half of {@link isBareBrowserListing} (TTY-independent, so it is
273
+ * unit-testable): true when no query, render, or filter flag is present that the
274
+ * `runSessionBrowser` picker cannot represent.
275
+ */
276
+ export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
277
+ /** One flat table row:
278
+ * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
279
+ * `doing` is the live preview when running, else the topic. The `ticket` column
280
+ * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
281
+ * when `showTicket` — otherwise a listing with no refs would waste a column of
282
+ * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
283
+ export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns): string;
173
284
  /**
174
285
  * Group key for the overview: prefer the indexed project name; else fold the cwd
175
286
  * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
@@ -221,6 +332,10 @@ export interface PickerColumns {
221
332
  /** Total width of the machine column, sized to the widest compacted hostname
222
333
  * in the pool (capped). Falls back to PICKER_MACHINE_W when absent. */
223
334
  machineWidth?: number;
335
+ /** Render the model column only when at least one row carries a model. */
336
+ showModel?: boolean;
337
+ /** Pool-sized model column width, including one trailing separator cell. */
338
+ modelWidth?: number;
224
339
  /** Render the ticket/PR column (only when at least one row carries a ref). */
225
340
  showTicket?: boolean;
226
341
  /**
@@ -32,7 +32,7 @@ import { filterTeamSessions } from '../lib/session/team-filter.js';
32
32
  import { parseSession } from '../lib/session/parse.js';
33
33
  import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
34
34
  import { formatRelativeTime } from '../lib/session/relative-time.js';
35
- import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkUrl } from '../lib/session/render.js';
35
+ import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
36
36
  import { linearIssueUrl } from '../lib/session/linear.js';
37
37
  import { renderMarkdown } from '../lib/markdown.js';
38
38
  import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
@@ -182,6 +182,11 @@ function statusColor(status) {
182
182
  case 'idle': return chalk.gray;
183
183
  case 'queued': return chalk.blue;
184
184
  case 'input_required': return chalk.yellow;
185
+ // Dead process: dimmed so it recedes from the live rows without being mistaken
186
+ // for the gray `idle` (which reads as "done, waiting for you" — a live state).
187
+ case 'closed': return chalk.dim;
188
+ // Days-stale / dangling: red so a session nobody is driving stands out.
189
+ case 'abandoned': return chalk.red;
185
190
  // Alive but un-introspectable (a harness whose transcript we can't parse).
186
191
  // Magenta so it never reads as the gray "idle" it used to be faked as.
187
192
  case 'unknown': return chalk.magenta;
@@ -235,7 +240,7 @@ export function cleanPreview(text) {
235
240
  * Covers every ActiveSession context: terminal (interactive), headless, teams,
236
241
  * cloud, and sub-agent rows that share the same ActiveSession.todos field.
237
242
  */
238
- function buildSessionDescription(s) {
243
+ export function buildSessionDescription(s) {
239
244
  const todo = formatTodoCompact(s.todos);
240
245
  if (s.context === 'cloud') {
241
246
  const base = s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`;
@@ -243,6 +248,11 @@ function buildSessionDescription(s) {
243
248
  }
244
249
  if (s.context === 'teams') {
245
250
  const parts = [s.teamName];
251
+ // Lineage: which orchestrator spun up this team. Prefer the resolved label,
252
+ // else the short session id, so "by whom" is answerable at a glance.
253
+ const orch = s.orchestratorLabel || (s.orchestratorSessionId ? s.orchestratorSessionId.slice(0, 8) : '');
254
+ if (orch)
255
+ parts.push(`by ${orch}`);
246
256
  if (todo)
247
257
  parts.push(todo);
248
258
  if (s.preview)
@@ -303,6 +313,10 @@ export function formatActiveRowDescription(s) {
303
313
  }
304
314
  /** Short human word for a session's activity (falls back to the coarse status). */
305
315
  function activityLabel(s) {
316
+ // Lifecycle status wins over any residual parsed activity — a dead/dangling
317
+ // session must read `closed`/`abandoned`, not the `idle` its stale tail infers.
318
+ if (s.status === 'closed' || s.status === 'abandoned')
319
+ return s.status;
306
320
  if (s.activity === 'waiting_input')
307
321
  return 'waiting';
308
322
  if (s.activity === 'working')
@@ -335,14 +349,54 @@ export function indexActiveBySessionId(active) {
335
349
  export function liveGlyphAndPreview(a) {
336
350
  if (!a)
337
351
  return { glyph: '', preview: '' };
352
+ // `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
353
+ // so an opaque harness is never mistaken for a finished one. `⊘` = abandoned /
354
+ // dangling; `×` = closed (dead pid) — both distinct from the `○` idle a live,
355
+ // stopped session shows, so a gone session never masquerades as a resting one.
356
+ if (a.status === 'abandoned')
357
+ return { glyph: statusColor(a.status)('⊘'), preview: buildSessionDescription(a) };
358
+ if (a.status === 'closed')
359
+ return { glyph: statusColor(a.status)('×'), preview: buildSessionDescription(a) };
338
360
  const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
339
361
  const running = a.status === 'running' || a.activity === 'working';
340
- // `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
341
- // so an opaque harness is never mistaken for a finished one.
342
362
  const unknown = a.status === 'unknown';
343
- const shape = waiting ? '◐' : running ? '●' : unknown ? '◌' : '○';
363
+ const shape = waiting ? '◐'
364
+ : running ? '●'
365
+ : unknown ? '◌'
366
+ : '○';
344
367
  return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
345
368
  }
369
+ /**
370
+ * The one-word live status for a listing row — `working` / `waiting` / `idle`,
371
+ * the same three states the `--active` column shows, so the default list is no
372
+ * longer just a glyph. `waiting` is the actionable "needs you" case (a question /
373
+ * permission / plan-review), kept distinct from `idle` (stopped) and `working`.
374
+ * Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
375
+ * exported for the row tests.
376
+ */
377
+ export function liveStatusWord(a) {
378
+ if (!a)
379
+ return '';
380
+ // Lifecycle status is definitive — surface it ahead of any parsed activity.
381
+ if (a.status === 'closed' || a.status === 'abandoned')
382
+ return a.status;
383
+ if (a.status === 'input_required' || a.activity === 'waiting_input')
384
+ return 'waiting';
385
+ if (a.status === 'running' || a.activity === 'working')
386
+ return 'working';
387
+ if (a.status === 'idle' || a.activity === 'idle')
388
+ return 'idle';
389
+ if (a.status === 'queued')
390
+ return 'queued';
391
+ return '';
392
+ }
393
+ /** The colored, space-padded status cell for a listing row (empty when not live). */
394
+ function liveStatusCell(live) {
395
+ const word = liveStatusWord(live);
396
+ if (!word || !live)
397
+ return { cell: '', width: 0 };
398
+ return { cell: statusColor(live.status)(padToWidth(word, 8)), width: 8 };
399
+ }
346
400
  /**
347
401
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
348
402
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
@@ -351,6 +405,21 @@ export function liveGlyphAndPreview(a) {
351
405
  export function ticketLabel(s) {
352
406
  return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
353
407
  }
408
+ function ticketUrl(s) {
409
+ if (s.ticketId)
410
+ return linearIssueUrl(s.ticketId);
411
+ return s.prNumber ? s.prUrl : undefined;
412
+ }
413
+ export function linkTicketCell(s, label) {
414
+ const url = ticketUrl(s);
415
+ return url && label.trim() !== '-' ? linkUrl(url, label) : label;
416
+ }
417
+ export function linkCwdCell(s, label) {
418
+ return s.cwd && !s._remote ? linkPath(s.cwd, label) : label;
419
+ }
420
+ function modelLabel(model) {
421
+ return model ? shortenModel(model) : '-';
422
+ }
354
423
  /**
355
424
  * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
356
425
  * joins active sessions on ticketId + project, but the raw ActiveSession nests
@@ -365,6 +434,7 @@ export function serializeActiveSessionsForJson(sessions) {
365
434
  ...s,
366
435
  ticketId: s.ticket?.id ?? null,
367
436
  project: s.cwd ? path.basename(s.cwd) : null,
437
+ prLink: s.pr?.url ?? null,
368
438
  }));
369
439
  }
370
440
  /**
@@ -435,11 +505,12 @@ function locatorBadge(s) {
435
505
  * terminal width so the row never wraps.
436
506
  */
437
507
  function printActiveRow(s, indent) {
438
- // shortId (8-char) · agent · host · status · badges · identity+todos+snippet
508
+ // shortId (8-char) · agent · host · status · owner · badges · identity+todos+snippet
439
509
  const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', 9));
440
510
  const kindCol = colorAgent(s.kind)(padToWidth(truncateToWidth(s.kind, 8), 9));
441
511
  const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
442
512
  const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
513
+ const ownerCol = chalk.cyan(padToWidth(truncateToWidth(ownerLabel(s), 8), 9));
443
514
  const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
444
515
  const badges = (fork ? fork : '') + [signalBadges(s), locatorBadge(s)].filter(Boolean).join(' ');
445
516
  // Identity (label/project/ticket clickable) + checklist + live snippet.
@@ -447,10 +518,24 @@ function printActiveRow(s, indent) {
447
518
  // via the SSH fan-out already populated (including todos when the peer has them).
448
519
  const desc = formatActiveRowDescription(s) || '-';
449
520
  // Fill the remaining width with the preview so nothing wraps under tmux/SSH.
450
- const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
521
+ const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
451
522
  const room = Math.max(12, terminalWidth() - fixed - 1);
452
523
  const descCol = chalk.white(truncateToWidth(desc, room));
453
- console.log(indent + idCol + kindCol + hostCol + statusCol + (badges ? badges + ' ' : '') + descCol);
524
+ console.log(indent + idCol + kindCol + hostCol + statusCol + ownerCol + (badges ? badges + ' ' : '') + descCol);
525
+ }
526
+ /**
527
+ * Compact owner display for the `--active` owner column: the local-part of a
528
+ * resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
529
+ * when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
530
+ * or absent (a launch predating actor stamping). Honest by design — an
531
+ * unresolved local run shows no owner rather than inventing one.
532
+ */
533
+ export function ownerLabel(s) {
534
+ const owner = s.owner;
535
+ if (!owner || owner.startsWith('UNRESOLVED@'))
536
+ return '-';
537
+ const at = owner.indexOf('@');
538
+ return at > 0 ? owner.slice(0, at) : owner;
454
539
  }
455
540
  /**
456
541
  * Short label for an IDE window. The slice key in live-terminals.json is
@@ -666,8 +751,8 @@ async function runRemoteSessionsJson(hosts) {
666
751
  process.stdout.write(serializeSessionsJson(sessions));
667
752
  }
668
753
  /**
669
- * `running N · idle N · waiting N · queued N · unknown N` for a bucket of
670
- * sessions (zero buckets omitted). Same bucketing as the grand-total summary so
754
+ * `running N · idle N · waiting N · queued N · closed N · abandoned N · unknown N`
755
+ * for a bucket of sessions (zero buckets omitted). Same bucketing as the summary so
671
756
  * per-group counts reconcile with the `(total)` beside the header — the `unknown`
672
757
  * bucket is what keeps an alive-but-opaque row from silently vanishing from the
673
758
  * tally. Empty when nothing.
@@ -677,6 +762,8 @@ function groupTally(sessions) {
677
762
  const idle = sessions.filter(s => s.status === 'idle').length;
678
763
  const waiting = sessions.filter(s => s.status === 'input_required').length;
679
764
  const queued = sessions.filter(s => s.status === 'queued').length;
765
+ const closed = sessions.filter(s => s.status === 'closed').length;
766
+ const abandoned = sessions.filter(s => s.status === 'abandoned').length;
680
767
  const unknown = sessions.filter(s => s.status === 'unknown').length;
681
768
  const parts = [];
682
769
  if (running)
@@ -687,6 +774,10 @@ function groupTally(sessions) {
687
774
  parts.push(`${waiting} waiting`);
688
775
  if (queued)
689
776
  parts.push(`${queued} queued`);
777
+ if (closed)
778
+ parts.push(`${closed} closed`);
779
+ if (abandoned)
780
+ parts.push(`${abandoned} abandoned`);
690
781
  if (unknown)
691
782
  parts.push(`${unknown} unknown`);
692
783
  return parts.join(' · ');
@@ -911,6 +1002,33 @@ function printCrossMachineTip() {
911
1002
  function useInteractiveBrowser(options) {
912
1003
  return options.interactive !== false && !options.json && isInteractiveTerminal();
913
1004
  }
1005
+ /**
1006
+ * A bare interactive fleet listing — no query, no render/filter flag — that the
1007
+ * `runSessionBrowser` picker can represent. The single predicate shared by the
1008
+ * bare-browser branch and the `--host` early-return guard so they can't drift:
1009
+ * when this holds, an explicit `--host`/`--device` scope is folded into the
1010
+ * browser (preview-rich, selectable) instead of the legacy per-host raw stream.
1011
+ */
1012
+ export function isBareBrowserListing(options, query) {
1013
+ return useInteractiveBrowser(options) && hasNoBrowserDisqualifyingFlags(options, query);
1014
+ }
1015
+ /**
1016
+ * Pure flag-gate half of {@link isBareBrowserListing} (TTY-independent, so it is
1017
+ * unit-testable): true when no query, render, or filter flag is present that the
1018
+ * `runSessionBrowser` picker cannot represent.
1019
+ */
1020
+ export function hasNoBrowserDisqualifyingFlags(options, query) {
1021
+ return (!query &&
1022
+ !options.routine &&
1023
+ !options.flat &&
1024
+ !options.tree &&
1025
+ !options.markdown &&
1026
+ !options.until &&
1027
+ !options.project &&
1028
+ !options.sort &&
1029
+ !options.artifacts &&
1030
+ options.artifact === undefined);
1031
+ }
914
1032
  /** The canonical `ag sessions …` command for a set of flags — the twin of the
915
1033
  * browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
916
1034
  function canonicalSessionsCommand(query, options) {
@@ -958,6 +1076,21 @@ async function renderSessionPreview(query, scope) {
958
1076
  console.log(chalk.gray(`No session matches "${query}".`));
959
1077
  return;
960
1078
  }
1079
+ // Lead with the live status when the session is still running, so the preview
1080
+ // says working / waiting / idle up front — not just the historical transcript.
1081
+ let live;
1082
+ try {
1083
+ live = indexActiveBySessionId(await getActiveSessions()).get(session.id);
1084
+ }
1085
+ catch { /* plain preview on any probe failure */ }
1086
+ if (live) {
1087
+ const { glyph } = liveGlyphAndPreview(live);
1088
+ const word = liveStatusWord(live) || live.status;
1089
+ const needsYou = live.status === 'input_required' || live.activity === 'waiting_input';
1090
+ const reason = live.awaitingReason ? ` (${live.awaitingReason.replace('_', ' ')})` : '';
1091
+ const suffix = needsYou ? chalk.yellow(` ← needs you${reason}`) : '';
1092
+ console.log(`${glyph} ${statusColor(live.status)(word)}${suffix}`);
1093
+ }
961
1094
  console.log(buildPreview(session));
962
1095
  }
963
1096
  /** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
@@ -998,14 +1131,21 @@ async function sessionsAction(query, options) {
998
1131
  await runRemoteSessionsJson(options.host);
999
1132
  return;
1000
1133
  }
1001
- try {
1002
- runRemoteSessions(options.host);
1003
- }
1004
- catch (err) {
1005
- console.error(chalk.red(err.message));
1006
- process.exit(1);
1134
+ // A bare interactive `--host`/`--device <box>` listing falls through to the
1135
+ // fleet browser below, which folds the named host(s) into the same merged,
1136
+ // preview-rich, selectable view as the local listing (via gatherRemoteList).
1137
+ // A query, a render/filter flag, or a non-interactive caller keeps the legacy
1138
+ // per-host raw stream under a `── host ──` banner.
1139
+ if (!isBareBrowserListing(options, query)) {
1140
+ try {
1141
+ runRemoteSessions(options.host);
1142
+ }
1143
+ catch (err) {
1144
+ console.error(chalk.red(err.message));
1145
+ process.exit(1);
1146
+ }
1147
+ return;
1007
1148
  }
1008
- return;
1009
1149
  }
1010
1150
  // --preview <id/query>: resolve one session and print its compact preview, then
1011
1151
  // exit — checked before --active so `--active --preview <id>` peeks the id
@@ -1059,17 +1199,7 @@ async function sessionsAction(query, options) {
1059
1199
  // filter the browser can't represent), or --no-interactive keep the existing
1060
1200
  // printed/render paths (agents and scripts unaffected). An explicit --since seeds
1061
1201
  // the browser's window so the flag is honored, not swallowed.
1062
- if (useInteractiveBrowser(options) &&
1063
- !query &&
1064
- !options.routine &&
1065
- !options.flat &&
1066
- !options.tree &&
1067
- !options.markdown &&
1068
- !options.until &&
1069
- !options.project &&
1070
- !options.sort &&
1071
- !options.artifacts &&
1072
- options.artifact === undefined) {
1202
+ if (isBareBrowserListing(options, query)) {
1073
1203
  const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
1074
1204
  await runSessionBrowser(bareBrowserSeed({
1075
1205
  teams: options.teams,
@@ -1322,12 +1452,12 @@ function metaSignals(s) {
1322
1452
  };
1323
1453
  }
1324
1454
  /** One flat table row:
1325
- * shortId · agent · version · project · [glyph] label·doing · [ticket] · [wt] · time
1455
+ * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
1326
1456
  * `doing` is the live preview when running, else the topic. The `ticket` column
1327
1457
  * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
1328
1458
  * when `showTicket` — otherwise a listing with no refs would waste a column of
1329
1459
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
1330
- function flatSessionRow(session, live, showTicket = false, cols = {}) {
1460
+ export function flatSessionRow(session, live, showTicket = false, cols = {}) {
1331
1461
  const agentColor = colorAgent(session.agent);
1332
1462
  const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
1333
1463
  const project = session.project || '-';
@@ -1350,19 +1480,31 @@ function flatSessionRow(session, live, showTicket = false, cols = {}) {
1350
1480
  : '';
1351
1481
  const TICKET_W = 10;
1352
1482
  const ticketCell = showTicket
1353
- ? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
1483
+ ? chalk.blue(linkTicketCell(session, padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1)))
1354
1484
  : '';
1485
+ // Live status word (working / waiting / idle) next to the glyph — the default
1486
+ // list is no longer a bare glyph. Empty (zero width) for resting rows.
1487
+ const { cell: statusCell, width: statusW } = liveStatusCell(live);
1355
1488
  const glyphW = glyph ? 2 : 0;
1356
1489
  const machineW = cols.showMachine ? machineColW : 0;
1357
1490
  const ticketW = showTicket ? TICKET_W + 1 : 0;
1358
1491
  const wtW = wt ? stringWidth(wt) + 1 : 0;
1359
- const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - machineW - ticketW - wtW - stringWidth(when) - 1);
1492
+ const width = terminalWidth();
1493
+ const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
1494
+ const fixedW = (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + stringWidth(when) + 1;
1495
+ const modelSlack = width - fixedW - 16;
1496
+ const modelW = requestedModelW <= modelSlack
1497
+ ? requestedModelW
1498
+ : modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
1499
+ const topicW = Math.max(16, width - fixedW - modelW);
1360
1500
  return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
1361
1501
  agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
1362
1502
  chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
1503
+ (modelW ? chalk.yellow(padToWidth(truncateToWidth(modelLabel(session.model), modelW - 1), modelW)) : '') +
1363
1504
  machineCell +
1364
- chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
1505
+ chalk.cyan(linkCwdCell(session, padToWidth(truncateToWidth(project, 14), 16))) +
1365
1506
  (glyph ? glyph + ' ' : '') +
1507
+ statusCell +
1366
1508
  renderTopicCell(label, doing, '', topicW, topicW) +
1367
1509
  ticketCell +
1368
1510
  (wt ? wt + ' ' : '') +
@@ -1383,13 +1525,15 @@ function treeSessionRow(session, live) {
1383
1525
  const badges = signalBadges(metaSignals(session));
1384
1526
  const badgeW = badges ? stringWidth(badges) + 1 : 0;
1385
1527
  const head = label ? `${label} · ${topic}` : topic;
1528
+ const { cell: statusCell, width: statusW } = liveStatusCell(live);
1386
1529
  const glyphW = glyph ? 2 : 0;
1387
- const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - badgeW - stringWidth(when) - 1);
1530
+ const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - stringWidth(when) - 1);
1388
1531
  return (' ' +
1389
1532
  chalk.dim(padToWidth(session.shortId, 9)) +
1390
1533
  agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
1391
1534
  (badges ? badges + ' ' : '') +
1392
1535
  (glyph ? glyph + ' ' : '') +
1536
+ statusCell +
1393
1537
  padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
1394
1538
  ' ' + chalk.gray(when));
1395
1539
  }
@@ -1506,7 +1650,9 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
1506
1650
  console.log();
1507
1651
  first = false;
1508
1652
  const group = byDir.get(key);
1509
- console.log(`${chalk.cyan.bold(shortCwd(key))} ${chalk.gray(`(${group.length})`)}`);
1653
+ const cwd = group.find((s) => s.cwd && !s._remote)?.cwd;
1654
+ const header = cwd ? linkPath(cwd, shortCwd(key)) : shortCwd(key);
1655
+ console.log(`${chalk.cyan.bold(header)} ${chalk.gray(`(${group.length})`)}`);
1510
1656
  for (const s of group)
1511
1657
  console.log(treeSessionRow(s, liveIndex?.get(s.id)));
1512
1658
  }
@@ -1702,12 +1848,18 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
1702
1848
  const PICKER_MACHINE_W = 11;
1703
1849
  const PICKER_MACHINE_MIN = 8;
1704
1850
  const PICKER_MACHINE_MAX = 18;
1851
+ const PICKER_MODEL_MIN = 6;
1852
+ const PICKER_MODEL_MAX = 13;
1705
1853
  /** Column width that shows every compacted hostname in `machines` whole (one
1706
1854
  * trailing space for separation), bounded by MIN/MAX. */
1707
1855
  function machineColumnWidth(machines, label) {
1708
1856
  const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
1709
1857
  return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
1710
1858
  }
1859
+ function modelColumnWidth(sessions) {
1860
+ const widest = sessions.reduce((width, session) => (Math.max(width, session.model ? stringWidth(modelLabel(session.model)) : 0)), 0);
1861
+ return Math.min(PICKER_MODEL_MAX, Math.max(PICKER_MODEL_MIN, widest + 1));
1862
+ }
1711
1863
  /**
1712
1864
  * Compact display form for machine ids: strip the longest shared dash-delimited
1713
1865
  * prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
@@ -1743,6 +1895,8 @@ export function pickerColumnsFor(sessions) {
1743
1895
  showMachine: distinct.length > 1,
1744
1896
  machineLabel,
1745
1897
  machineWidth: machineColumnWidth(distinct, machineLabel),
1898
+ showModel: sessions.some((s) => !!s.model),
1899
+ modelWidth: modelColumnWidth(sessions),
1746
1900
  showTicket: sessions.some((s) => ticketLabel(s) !== ''),
1747
1901
  };
1748
1902
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `agents set` — ergonomic top-level setter for per-agent/version run defaults.
3
+ *
4
+ * `agents set claude@2.1.220 --model opus-5` pins the default model (and/or
5
+ * mode) that `agents run` uses for that agent version. It reads and writes the
6
+ * same store as `agents defaults run set` (agents.yaml -> run.defaults), so the
7
+ * two stay consistent — `set` is just the short front door.
8
+ *
9
+ * agents set # list every configured default
10
+ * agents set claude@2.1.220 # show the default for one version
11
+ * agents set claude@2.1.220 --model opus-5
12
+ * agents set 'claude:*' --mode auto --model opus
13
+ */
14
+ import type { Command } from 'commander';
15
+ export declare function registerSetCommand(program: Command): void;