@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -6,11 +6,14 @@
6
6
  * response) and delegates to the generic `itemPicker` for the interactive UI.
7
7
  */
8
8
  import fs from 'node:fs';
9
+ import path from 'node:path';
9
10
  import chalk from 'chalk';
10
11
  import { truncate, humanDuration } from '../lib/format.js';
11
12
  import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
12
13
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
13
- import { linkPath, relativeToCwd } from '../lib/session/render.js';
14
+ import { linkPath, linkUrl, relativeToCwd } from '../lib/session/render.js';
15
+ import { linearIssueUrl } from '../lib/session/linear.js';
16
+ import { extractTodoProgress } from '../lib/session/state.js';
14
17
  import { renderMarkdown } from '../lib/markdown.js';
15
18
  import { itemPicker } from '../lib/picker.js';
16
19
  import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
@@ -22,6 +25,32 @@ import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } fr
22
25
  function remoteMachineOf(session) {
23
26
  return session._remote ? session.machine : undefined;
24
27
  }
28
+ /**
29
+ * Compact checklist tally for list rows and previews (RUSH-2045).
30
+ * Example: `✓6/8 · A5 wiring runner`. Empty string when there is no list.
31
+ * Consumes `SessionMeta.todos` / `ActiveSession.todos` as populated by the
32
+ * state engine — does not re-parse transcripts.
33
+ */
34
+ export function formatTodoCompact(todos) {
35
+ if (!todos || !Number.isFinite(todos.total) || todos.total < 1)
36
+ return '';
37
+ const done = Number.isFinite(todos.done) ? Math.max(0, todos.done) : 0;
38
+ const tally = `✓${done}/${todos.total}`;
39
+ const step = todos.activeForm?.replace(/\s+/g, ' ').trim();
40
+ return step ? `${tally} · ${step}` : tally;
41
+ }
42
+ /**
43
+ * Best-effort GitHub repo URL from a checkout path shaped like
44
+ * `…/github.com/<owner>/<repo>/…`. Used to make the project name clickable
45
+ * when no Linear project URL is available.
46
+ */
47
+ export function githubRepoUrlFromCwd(cwd) {
48
+ if (!cwd)
49
+ return undefined;
50
+ const norm = cwd.replace(/\\/g, '/');
51
+ const m = norm.match(/\/github\.com\/([^/]+\/[^/]+)/);
52
+ return m ? `https://github.com/${m[1]}` : undefined;
53
+ }
25
54
  /**
26
55
  * SessionMeta originates in discover.ts (gitBranch, cwd, label, etc. read from
27
56
  * untrusted session files). parseSession sanitizes event payloads at its
@@ -30,6 +59,17 @@ function remoteMachineOf(session) {
30
59
  */
31
60
  function sanitizeMeta(s) {
32
61
  const clean = (v) => (v == null ? v : sanitizeForTerminal(v));
62
+ const todos = s.todos
63
+ ? {
64
+ ...s.todos,
65
+ activeForm: clean(s.todos.activeForm),
66
+ items: s.todos.items.map((it) => ({
67
+ ...it,
68
+ content: sanitizeForTerminal(it.content),
69
+ activeForm: clean(it.activeForm),
70
+ })),
71
+ }
72
+ : s.todos;
33
73
  return {
34
74
  ...s,
35
75
  id: sanitizeForTerminal(s.id),
@@ -42,6 +82,9 @@ function sanitizeMeta(s) {
42
82
  account: clean(s.account),
43
83
  topic: clean(s.topic),
44
84
  label: clean(s.label),
85
+ ticketId: clean(s.ticketId),
86
+ prUrl: clean(s.prUrl),
87
+ todos,
45
88
  };
46
89
  }
47
90
  const previewCache = new Map();
@@ -59,7 +102,8 @@ export function buildPreview(session) {
59
102
  if (remote) {
60
103
  const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
61
104
  + chalk.gray(` — enter to resume there, or space then enter to read it over SSH`);
62
- const output = [formatHeader(safe, []), '', note].join('\n');
105
+ const metaBody = formatMetaOnlyBody(safe);
106
+ const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
63
107
  previewCache.set(cacheKey, output);
64
108
  return output;
65
109
  }
@@ -67,7 +111,8 @@ export function buildPreview(session) {
67
111
  // entry (e.g. `sessions go`). Show the header + a clean note, not a parse error.
68
112
  if (!session.filePath || !fs.existsSync(session.filePath)) {
69
113
  const note = ' ' + chalk.gray('Live session — full transcript not indexed here.');
70
- const output = [formatHeader(safe, []), '', note].filter(Boolean).join('\n');
114
+ const metaBody = formatMetaOnlyBody(safe);
115
+ const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
71
116
  previewCache.set(cacheKey, output);
72
117
  return output;
73
118
  }
@@ -96,19 +141,27 @@ function formatHeader(session, events) {
96
141
  const { startedAgo, duration } = extractTiming(events);
97
142
  const totalMessages = session.messageCount ?? countMessages(events);
98
143
  const totalTokens = session.tokenCount;
99
- // Line 1: Agent v version · model · account
144
+ // Line 1: Agent v version · shortId · model · account
100
145
  const line1 = [];
101
146
  line1.push(chalk.gray(`${displayAgent(session.agent)}${session.version ? ` v${session.version}` : ''}`));
147
+ if (session.shortId)
148
+ line1.push(chalk.dim(session.shortId));
102
149
  if (model)
103
150
  line1.push(chalk.bold.white(model));
104
151
  if (session.account)
105
152
  line1.push(chalk.gray(session.account));
106
- // Line 2: cwd · branch · started X ago · lasted Y
153
+ // Line 2: cwd · project · branch · started X ago · lasted Y
154
+ // Project is clickable: Linear issue URL is for tickets (line 4); for the
155
+ // project name prefer a GitHub repo URL derived from the checkout path.
107
156
  const line2 = [];
108
157
  if (session.cwd) {
109
158
  const label = relativeToCwd(session.cwd);
110
159
  line2.push(chalk.bold.white(linkPath(session.cwd, label)));
111
160
  }
161
+ if (session.project) {
162
+ const repoUrl = githubRepoUrlFromCwd(session.cwd);
163
+ line2.push(chalk.cyan(repoUrl ? linkUrl(repoUrl, session.project) : session.project));
164
+ }
112
165
  if (session.gitBranch)
113
166
  line2.push(chalk.cyan(session.gitBranch));
114
167
  if (startedAgo)
@@ -125,13 +178,46 @@ function formatHeader(session, events) {
125
178
  }
126
179
  if (session.label)
127
180
  line3.push(chalk.white(session.label));
128
- line3.push(chalk.gray(linkPath(session.filePath, session.id)));
181
+ if (session.filePath)
182
+ line3.push(chalk.gray(linkPath(session.filePath, session.id)));
183
+ else
184
+ line3.push(chalk.gray(session.id));
185
+ // Line 4: ticket + PR — clickable when a URL is resolvable (OSC 8 hyperlink),
186
+ // plain text otherwise. Only rendered when the session carries either.
187
+ const line4 = [];
188
+ if (session.ticketId) {
189
+ const url = linearIssueUrl(session.ticketId);
190
+ line4.push(chalk.blue(url ? linkUrl(url, session.ticketId) : session.ticketId));
191
+ }
192
+ if (session.prUrl) {
193
+ const label = session.prNumber ? `PR#${session.prNumber}` : 'PR';
194
+ line4.push(chalk.blue(linkUrl(session.prUrl, label)));
195
+ }
129
196
  return [
130
197
  line1.join(DOT),
131
198
  line2.join(DOT),
132
199
  line3.join(DOT),
200
+ ...(line4.length ? [line4.join(DOT)] : []),
133
201
  ].join('\n');
134
202
  }
203
+ /**
204
+ * Body lines available from SessionMeta alone (no transcript parse) — used for
205
+ * remote / unindexed sessions so checklist progress still surfaces when the
206
+ * parser teammate (or a prior scan) has populated `session.todos`.
207
+ */
208
+ function formatMetaOnlyBody(session) {
209
+ const lines = [];
210
+ if (session.topic) {
211
+ lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(session.topic.trim(), (process.stdout.columns || 80) - 12)));
212
+ }
213
+ const compact = formatTodoCompact(session.todos);
214
+ if (compact) {
215
+ lines.push(chalk.cyan('Todos: ') + chalk.white(compact));
216
+ }
217
+ if (lines.length === 0)
218
+ return '';
219
+ return lines.map(l => ' ' + l).join('\n');
220
+ }
135
221
  function extractModel(events) {
136
222
  for (let i = events.length - 1; i >= 0; i--) {
137
223
  const m = events[i].model;
@@ -189,6 +275,7 @@ function stripTags(text) {
189
275
  const LAST_RESPONSE_MAX_LINES = 15;
190
276
  const LAST_RESPONSE_MAX_LINES_WITH_TODOS = 8;
191
277
  const TODOS_MAX_ITEMS = 5;
278
+ const DIRS_TOUCHED_MAX = 5;
192
279
  function formatCompactPreview(events, session) {
193
280
  let firstUser = '';
194
281
  let lastAssistant = '';
@@ -196,7 +283,8 @@ function formatCompactPreview(events, session) {
196
283
  const toolCounts = {};
197
284
  let toolCalls = 0;
198
285
  let planFile = '';
199
- let latestTodos = null;
286
+ /** Latest checklist from the transcript (Claude TodoWrite / Codex update_plan). */
287
+ let latestTodos;
200
288
  for (const event of events) {
201
289
  if (event.type === 'message') {
202
290
  if (event.role === 'user' && !firstUser && event.content) {
@@ -217,25 +305,53 @@ function formatCompactPreview(events, session) {
217
305
  if (!planFile && p && /\/plans\/[^/]+\.md$/.test(p)) {
218
306
  planFile = p;
219
307
  }
220
- if (tool === 'TodoWrite' && Array.isArray(event.args?.todos)) {
221
- latestTodos = event.args.todos;
308
+ // Claude TodoWrite (`todos`) and Codex update_plan (`plan`) — same source as
309
+ // extractTodoProgress in the state engine. Prefer the most recent write.
310
+ if (tool === 'TodoWrite' || tool === 'update_plan') {
311
+ const progress = extractTodoProgress(event.args);
312
+ if (progress)
313
+ latestTodos = progress;
222
314
  }
223
315
  if (tool)
224
316
  toolCounts[tool] = (toolCounts[tool] ?? 0) + 1;
225
317
  toolCalls++;
226
318
  }
227
319
  }
320
+ // Prefer the transcript-derived list when we just re-parsed the file (freshest
321
+ // checklist write); fall back to SessionMeta.todos for rows where the scan/
322
+ // fan-out attached progress but the event stream has no TodoWrite yet.
323
+ const todos = latestTodos ?? session.todos;
228
324
  // Digest signals folded into the preview: change lifecycle, tool mix, tests.
229
325
  const changes = classifyFileChanges(events);
230
326
  const chg = changeCounts(changes);
231
327
  const lines = [];
232
328
  const termWidth = process.stdout.columns || 80;
329
+ // Originating user prompt (first non-system user turn).
233
330
  if (firstUser) {
234
331
  const first = extractSessionTopic(firstUser) || cleanSessionPrompt(firstUser).split('\n').find(l => l.trim()) || '';
235
332
  if (first) {
236
333
  lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(first.trim(), termWidth - 12)));
237
334
  }
238
335
  }
336
+ else if (session.topic) {
337
+ lines.push(chalk.cyan('Prompt: ') + chalk.white(truncate(session.topic.trim(), termWidth - 12)));
338
+ }
339
+ // Compact checklist: ✓done/total · current step (RUSH-2045).
340
+ const compact = formatTodoCompact(todos);
341
+ if (compact) {
342
+ lines.push(chalk.cyan('Todos: ') + chalk.white(compact));
343
+ }
344
+ const todosRendered = todos?.items?.length ? renderTodos(todos.items, termWidth) : [];
345
+ if (todosRendered.length > 0) {
346
+ for (const l of todosRendered)
347
+ lines.push(' ' + l);
348
+ }
349
+ // Recent activity = directories touched (not raw tool calls). Prefer a
350
+ // parser-supplied dirsTouched when present; else derive from event paths.
351
+ const dirs = directoriesTouched(session, events, changes);
352
+ if (dirs.length) {
353
+ lines.push(chalk.cyan('Dirs: ') + chalk.white(dirs.join(chalk.gray(' · '))));
354
+ }
239
355
  const activity = [];
240
356
  const changed = chg.created + chg.modified + chg.deleted;
241
357
  if (changed) {
@@ -272,14 +388,8 @@ function formatCompactPreview(events, session) {
272
388
  const basename = planFile.split('/').pop() || planFile;
273
389
  lines.push(chalk.cyan('Plan: ') + chalk.white(linkPath(planFile, basename)));
274
390
  }
275
- const todosRendered = latestTodos ? renderTodos(latestTodos, termWidth) : [];
276
- if (todosRendered.length > 0) {
277
- lines.push(chalk.cyan('Todos:'));
278
- for (const l of todosRendered)
279
- lines.push(' ' + l);
280
- }
281
391
  if (lastAssistant) {
282
- const maxLines = todosRendered.length > 0 ? LAST_RESPONSE_MAX_LINES_WITH_TODOS : LAST_RESPONSE_MAX_LINES;
392
+ const maxLines = todosRendered.length > 0 || compact ? LAST_RESPONSE_MAX_LINES_WITH_TODOS : LAST_RESPONSE_MAX_LINES;
283
393
  const rendered = renderLastResponse(lastAssistant, maxLines);
284
394
  if (rendered.length > 0) {
285
395
  lines.push('');
@@ -293,6 +403,64 @@ function formatCompactPreview(events, session) {
293
403
  }
294
404
  return lines.map(l => ' ' + l).join('\n');
295
405
  }
406
+ /**
407
+ * Unique directories the session touched, compact and human-readable.
408
+ * Prefer `session.dirsTouched` when the parser teammate has populated it;
409
+ * otherwise derive from file-change + tool paths already available here.
410
+ */
411
+ function directoriesTouched(session, events, changes) {
412
+ const fromMeta = session.dirsTouched;
413
+ if (Array.isArray(fromMeta) && fromMeta.length > 0) {
414
+ return fromMeta
415
+ .map((d) => sanitizeForTerminal(String(d).trim()))
416
+ .filter(Boolean)
417
+ .slice(0, DIRS_TOUCHED_MAX);
418
+ }
419
+ const counts = new Map();
420
+ const bump = (raw) => {
421
+ const dir = relativizeDir(raw, session.cwd);
422
+ if (!dir)
423
+ return;
424
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
425
+ };
426
+ for (const ch of changes)
427
+ bump(ch.path);
428
+ for (const event of events) {
429
+ if (event.type !== 'tool_use' || event._local)
430
+ continue;
431
+ const p = event.path || event.args?.file_path || event.args?.path || '';
432
+ if (p)
433
+ bump(p);
434
+ }
435
+ return [...counts.entries()]
436
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
437
+ .map(([d]) => d)
438
+ .slice(0, DIRS_TOUCHED_MAX);
439
+ }
440
+ /** Relativize a file path to its parent dir, short enough for one preview line. */
441
+ function relativizeDir(filePath, cwd) {
442
+ const norm = filePath.replace(/\\/g, '/');
443
+ if (!norm || norm.includes('node_modules') || norm.includes('/.git/') || norm.includes('/plans/')) {
444
+ return undefined;
445
+ }
446
+ let dir = path.posix.dirname(norm);
447
+ if (cwd) {
448
+ const base = cwd.replace(/\\/g, '/').replace(/\/$/, '');
449
+ if (dir === base)
450
+ return '.';
451
+ if (dir.startsWith(base + '/'))
452
+ dir = dir.slice(base.length + 1);
453
+ }
454
+ // Collapse home prefix.
455
+ const home = (process.env.HOME || '').replace(/\\/g, '/');
456
+ if (home && dir.startsWith(home + '/'))
457
+ dir = '~' + dir.slice(home.length);
458
+ // Drop ultra-deep absolute noise; keep last 3 segments.
459
+ const parts = dir.split('/').filter(Boolean);
460
+ if (parts.length > 3 && dir.startsWith('/'))
461
+ dir = parts.slice(-3).join('/');
462
+ return dir || undefined;
463
+ }
296
464
  function renderLastResponse(content, maxLines = LAST_RESPONSE_MAX_LINES) {
297
465
  const cleaned = stripTags(content).trim();
298
466
  if (!cleaned)
@@ -9,6 +9,18 @@ import { type PickedSession } from './sessions-picker.js';
9
9
  * a captured transcript tail. Collapses runs of whitespace.
10
10
  */
11
11
  export declare function cleanPreview(text: string): string;
12
+ /**
13
+ * Identity + checklist + live snippet for an --active / cross-machine row.
14
+ * Surfaces agent-adjacent identity the flat table already has (label, project)
15
+ * with a clickable project when a GitHub URL is resolvable, then the checklist
16
+ * tally and the latest-turn snippet.
17
+ *
18
+ * Free-text fields are cleaned individually; OSC 8 hyperlinks are applied
19
+ * *after* cleaning so `cleanPreview` does not strip the clickable targets
20
+ * (RUSH-2045 review). Ticket is clickable via {@link signalBadges}, not here,
21
+ * so the id is not printed twice.
22
+ */
23
+ export declare function formatActiveRowDescription(s: ActiveSession): string;
12
24
  /**
13
25
  * Index live sessions by their full session UUID so a historical `SessionMeta`
14
26
  * row (`meta.id`) can be matched to the session that is still running now.
@@ -33,6 +45,19 @@ export declare function liveGlyphAndPreview(a: ActiveSession | undefined): {
33
45
  * into a scannable column instead of jamming against a truncated topic.
34
46
  */
35
47
  export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'>): string;
48
+ /**
49
+ * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
50
+ * joins active sessions on ticketId + project, but the raw ActiveSession nests
51
+ * the ticket (`ticket.id`) and carries no `project` at all — so a naive join
52
+ * silently drops every row. Emit both as flat, always-present top-level keys
53
+ * (null when unknown) alongside the raw fields, so every active row is joinable.
54
+ * `project` uses the same derivation SessionMeta does — basename(cwd) (see
55
+ * discover.ts) — so the active view and the history view join identically.
56
+ */
57
+ export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
58
+ ticketId: string | null;
59
+ project: string | null;
60
+ }>;
36
61
  /** Grouped + sorted view of active sessions for the --active renderer. */
37
62
  export interface ActiveSessionsLayout {
38
63
  workspaces: Array<{
@@ -162,6 +187,12 @@ export declare function renderSessionLog(session: SessionMeta, mode?: ViewMode):
162
187
  */
163
188
  export declare function renderSessionLogJson(session: SessionMeta): Promise<void>;
164
189
  /** Column-visibility flags for the picker row, computed once over the whole pool. */
190
+ /** The SSH-launch origin for a picker row, resolved from the live session's
191
+ * provenance (transport 'ssh'). `device` is set when the client IP matched a
192
+ * registered device; absent for an unresolved IP (renders a bare `ssh`). */
193
+ export interface SshOriginTag {
194
+ device?: string;
195
+ }
165
196
  export interface PickerColumns {
166
197
  /** Render the machine column (only when the pool spans more than one machine). */
167
198
  showMachine?: boolean;
@@ -192,7 +223,7 @@ export declare function machineLabeler(machines: string[]): (m: string) => strin
192
223
  * than one box, the ticket column only when some row carries a PR/ticket ref.
193
224
  */
194
225
  export declare function pickerColumnsFor(sessions: SessionMeta[]): PickerColumns;
195
- export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns): string;
226
+ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?: PickerColumns, ssh?: SshOriginTag): string;
196
227
  /**
197
228
  * Pick a hint to show above the picker. Deterministic (keys off the pool size)
198
229
  * so it stays fixed across the picker's re-renders within a single run.
@@ -204,8 +235,8 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
204
235
  * Resume a session in the current terminal — a foreground takeover of this
205
236
  * process. Used by the single-select picker and by `sessions resume` when the
206
237
  * chosen destination is "in place" (unknown emulator / off-macOS, single pick).
207
- * Falls back to the current version via `/continue` when the version-pinned
208
- * binary is missing (ENOENT).
238
+ * Falls back to the same resume invocation against the current version when the
239
+ * version-pinned launcher is genuinely missing.
209
240
  */
210
241
  export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
211
242
  /**
@@ -224,23 +255,34 @@ export declare function resumeSpawnInvocation(cmd: string[], platform?: NodeJS.P
224
255
  args: string[];
225
256
  shell: boolean;
226
257
  };
227
- /**
228
- * Build the shell command that resumes a picked session.
229
- *
230
- * When the session's originating version is known, uses the version-pinned
231
- * binary (e.g. `claude@2.1.138`) so the resume always runs in the same
232
- * isolated HOME where the JSONL was written — regardless of which version is
233
- * currently the default. Falls back to the bare shim when version is unknown.
234
- *
235
- * If the versioned binary is missing (version was removed), the ENOENT
236
- * handler in handlePickedSession retries via buildFallbackCommand.
237
- */
238
258
  export declare function buildResumeCommand(session: SessionMeta): string[] | null;
239
259
  interface AgentFilter {
240
260
  agent?: SessionAgentId;
241
261
  version?: string;
242
262
  }
243
263
  export declare function parseAgentFilter(agentName?: string): AgentFilter;
264
+ /**
265
+ * How a `sessions <query>` argument was resolved against the pool.
266
+ *
267
+ * `byId` records that the rows came from an id lookup, so only then does an
268
+ * ambiguous result mean "your id prefix is too short". `completeId` records that
269
+ * the query was a whole session id: it is unique by construction, so a miss is
270
+ * final and must NOT widen into a text/content search.
271
+ */
272
+ export interface SessionQueryResolution {
273
+ matches: SessionMeta[];
274
+ byId: boolean;
275
+ completeId: boolean;
276
+ }
277
+ /**
278
+ * The single entry point for turning a `sessions <query>` argument into rows.
279
+ *
280
+ * An id-shaped query — a complete id OR a hex short-id/prefix (looksLikeSessionId)
281
+ * — resolves by id alone, through the index, and never falls back to content
282
+ * search (a bare id must not surface every transcript that merely mentions it).
283
+ * A genuine search phrase keeps the ranked metadata+content search.
284
+ */
285
+ export declare function resolveSessionQuery(pool: SessionMeta[], query: string): SessionQueryResolution;
244
286
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
245
287
  export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
246
288
  /** Register the `agents sessions` command with all its options and help text. */