@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
@@ -7,6 +7,28 @@ export type MilestoneEvent = 'plan.created' | 'pr.opened' | 'pr.merged' | 'workt
7
7
  export type ActivityKind = 'file.edited';
8
8
  export type ActivityEventKind = MilestoneEvent | ActivityKind;
9
9
  export type ActivityTier = 'milestone' | 'activity';
10
+ /** Well-known attachment kinds; the field is an open string, not an enum. */
11
+ export type AttachmentKind = 'link' | 'file' | 'image' | 'audio' | 'video';
12
+ /**
13
+ * A generic artifact carried by a progress update — a rendered plan, an audio
14
+ * take, a preview URL. Deliberately domain-agnostic: `kind` is an open string,
15
+ * and `meta` is an open bag (duration, width, …) so any producer can attach
16
+ * anything without a schema change.
17
+ */
18
+ export interface Attachment {
19
+ /** Coarse kind for glyph/rendering; open string, not a closed enum. */
20
+ kind: AttachmentKind | string;
21
+ /** https:// URL, an absolute path, or a history-relative path. */
22
+ href: string;
23
+ /** Display name (defaults to basename(href) at render time). */
24
+ name?: string;
25
+ /** MIME type when known (e.g. `audio/wav`). */
26
+ mediaType?: string;
27
+ /** Size in bytes for local files, when stat succeeded. */
28
+ bytes?: number;
29
+ /** Open bag for kind-specific facts (duration, width, height, …). */
30
+ meta?: Record<string, string | number>;
31
+ }
10
32
  /** The set of milestone events, for tier classification and ordering. */
11
33
  export declare const MILESTONE_EVENTS: readonly MilestoneEvent[];
12
34
  export declare function tierForEvent(event: string): ActivityTier;
@@ -29,6 +51,13 @@ export interface ActivityEvent {
29
51
  runtime: string;
30
52
  /** Working directory at event time -- the join key to a project/git repo. */
31
53
  cwd?: string;
54
+ /**
55
+ * Project/repo name stamped by the writer (basename of cwd, worktree-aware),
56
+ * so a progress post carries its project even without a live-session join.
57
+ * Read-time enrichment ({@link enrichActivityEvents}) still fills it for
58
+ * hook-written events that lack it.
59
+ */
60
+ project?: string;
32
61
  /** Agent that produced the event (claude, codex, ...). */
33
62
  agent?: string;
34
63
  /** Tool that triggered the event (Bash, Task, ExitPlanMode, feed.post, ...). */
@@ -45,7 +74,15 @@ export interface ActivityEvent {
45
74
  terminalId?: string;
46
75
  /** `$TMUX_PANE` at launch when recorded. */
47
76
  tmuxPane?: string;
77
+ /** Generic artifacts attached to a deliberate progress post. */
78
+ attachments?: Attachment[];
48
79
  }
80
+ /**
81
+ * Coerce an unknown value into a clean {@link Attachment}[] — a fail-open reader
82
+ * over user/agent-written JSON. Drops entries without a usable `href`, clamps
83
+ * `kind` to a string, and keeps only primitive `meta` values.
84
+ */
85
+ export declare function sanitizeAttachments(value: unknown): Attachment[] | undefined;
49
86
  /**
50
87
  * Append one event to a session's activity log. Primarily the Python hook
51
88
  * writes these at runtime; this TS writer exists for tests and any in-process
@@ -111,6 +148,125 @@ export declare function styleForEvent(event: string): {
111
148
  export declare function formatActivityLine(ev: ActivityEvent, opts?: {
112
149
  showHost?: boolean;
113
150
  }): string;
151
+ /** Glyph per attachment kind, so an artifact row reads at a glance. */
152
+ export declare const ATTACHMENT_GLYPH: Record<string, string>;
153
+ export declare function attachmentGlyph(kind: string): string;
154
+ /** Display name for an attachment: its `name`, else the basename of its href. */
155
+ export declare function attachmentName(att: Attachment): string;
156
+ /**
157
+ * Short session id for a chip: strip a known prefix (`session_`, `ses_`) then
158
+ * take the first 8 chars — enough to disambiguate, matching `ag sessions`.
159
+ */
160
+ export declare function shortSessionId(sessionId: string): string;
161
+ /** Extra identity resolved at display time by joining the session index. */
162
+ export interface ProgressJoin {
163
+ ticketId?: string;
164
+ prUrl?: string;
165
+ label?: string;
166
+ }
167
+ /**
168
+ * Render a deliberate progress update (`status.posted`) as an operator-facing,
169
+ * multi-line row with full identity — not a 60-char one-liner. Shape:
170
+ *
171
+ * ```
172
+ * ▸ update · 3m ago
173
+ * grok · 0108441e · yosemite-s1 · agents
174
+ * "CHANGELOG pushed; watching CI"
175
+ * ♪ draft.wav 🖼 cover.png
176
+ * ↳ ag focus 0108441e · ag sessions 0108441e
177
+ * ```
178
+ *
179
+ * Chips (agent, short session id, host, project, optional joined ticket/label)
180
+ * are shown only when known. Non-progress milestones keep {@link formatActivityLine}.
181
+ */
182
+ export declare function formatProgressUpdate(ev: ActivityEvent, opts?: {
183
+ joined?: ProgressJoin;
184
+ }): string;
185
+ /** An activity event with the session-derived facts joined on at read time. */
186
+ export interface EnrichedActivityEvent extends ActivityEvent {
187
+ /** Resolved project/repo name (from cwd or the session join), not the raw path. */
188
+ project?: string;
189
+ /** Tracker ticket the owning session is tied to (e.g. `RUSH-1234`). */
190
+ ticket?: string;
191
+ /** Machine the event actually ran on — session `provenance.host`, else `host`. */
192
+ executionHost?: string;
193
+ }
194
+ /**
195
+ * Facts pulled off a live session to enrich its activity events. Keyed by
196
+ * `sessionId`; every field optional so callers pass whatever they resolved.
197
+ */
198
+ export interface ActivitySessionHint {
199
+ sessionId?: string | null;
200
+ /** Tracker ticket id (from `ActiveSession.ticket`). */
201
+ ticket?: string | null;
202
+ /** Machine the session executes on (`provenance.host` / `machine`). */
203
+ executionHost?: string | null;
204
+ /** Resolved project/repo slug from the session cwd. */
205
+ project?: string | null;
206
+ }
207
+ /**
208
+ * Resolve a stable project/repo name from a working directory. A worktree cwd
209
+ * (`…/<repo>/.agents/worktrees/<slug>[/sub]`) resolves to the repo dir name so
210
+ * a worktree session groups with its own repo; any other path resolves to its
211
+ * basename. Pure — no filesystem access, so it works for remote events too.
212
+ */
213
+ export declare function projectFromCwd(cwd?: string | null): string | undefined;
214
+ /**
215
+ * Join session facts (ticket / project / execution host) onto each event by
216
+ * `sessionId`. A hint wins; otherwise pre-baked enriched fields (from a remote
217
+ * peer that already enriched its own stream) are preserved, and project/host
218
+ * fall back to what the event itself carries (`cwd`, `host`). Pure.
219
+ */
220
+ export declare function enrichActivityEvents(events: EnrichedActivityEvent[], hints: ActivitySessionHint[]): EnrichedActivityEvent[];
221
+ /**
222
+ * Validate + host-tag one peer's `activity --json` payload for the fan-out
223
+ * merge. Mirrors {@link parseLine}: skip anything missing `event`/`sessionId`/
224
+ * `ts`, drop corrupt items. The event's own `host` is the execution host and is
225
+ * authoritative; only fall back to the dialed peer's `machine` when it's absent.
226
+ * Enriched fields the peer stamped (project/ticket/executionHost) ride through.
227
+ */
228
+ export declare function parseActivityPayload(stdout: string, machine: string): EnrichedActivityEvent[];
229
+ /**
230
+ * Merge local + remote event lists, keeping the first copy of a given
231
+ * host/session/ts/event and sorting newest first. Cross-machine activity dirs
232
+ * can sync in a peer's own events, so the identity key dedupes those (mirrors
233
+ * `mergeFeedBlocks`).
234
+ */
235
+ export declare function mergeActivityEvents(...groups: EnrichedActivityEvent[][]): EnrichedActivityEvent[];
236
+ export type ActivityGroupBy = 'project' | 'device' | 'agent';
237
+ /** One bucket of the grouped view. */
238
+ export interface ActivityGroup {
239
+ /** Grouping value; empty string for the "unknown" bucket. */
240
+ key: string;
241
+ /** Display label. */
242
+ label: string;
243
+ events: EnrichedActivityEvent[];
244
+ }
245
+ /** The (key, label) an event sorts under for a given grouping dimension. */
246
+ export declare function activityGroupKey(ev: EnrichedActivityEvent, by: ActivityGroupBy): {
247
+ key: string;
248
+ label: string;
249
+ };
250
+ /**
251
+ * Bucket events by project, device, or agent. Groups are ordered by event count
252
+ * (desc) then label; the "unknown" bucket always sorts last. Input order is
253
+ * preserved within a group, so newest-first survives when the input is sorted.
254
+ */
255
+ export declare function groupActivity(events: EnrichedActivityEvent[], by: ActivityGroupBy): ActivityGroup[];
256
+ /**
257
+ * Narrow events to those whose project, device/host, agent, event kind, or
258
+ * ticket contains `filter` (case-insensitive substring). An empty filter is a
259
+ * no-op. Pure.
260
+ */
261
+ export declare function filterActivityEvents(events: EnrichedActivityEvent[], filter: string): EnrichedActivityEvent[];
262
+ /** One rendered enriched line: the base activity line + `· project · ticket` tags. */
263
+ export declare function formatEnrichedActivityLine(ev: EnrichedActivityEvent, opts?: {
264
+ showHost?: boolean;
265
+ showProject?: boolean;
266
+ indent?: string;
267
+ }): string;
268
+ /** Human header for one grouped bucket: `<label> · N events · M milestones`. */
269
+ export declare function formatActivityGroupHeader(group: ActivityGroup): string;
114
270
  /**
115
271
  * The activity-log hook (Python), sibling to 10-feed-publish.py. Classifies
116
272
  * PreToolUse/PostToolUse payloads into activity events and appends one JSONL
@@ -23,6 +23,7 @@ import * as yaml from 'yaml';
23
23
  import chalk from 'chalk';
24
24
  import { relTime, truncate } from './format.js';
25
25
  import { getActivityDir, getUserAgentsDir } from './state.js';
26
+ import { normalizeHost } from './machine-id.js';
26
27
  /** The set of milestone events, for tier classification and ordering. */
27
28
  export const MILESTONE_EVENTS = [
28
29
  'plan.created',
@@ -42,6 +43,45 @@ const MILESTONE_SET = new Set(MILESTONE_EVENTS);
42
43
  export function tierForEvent(event) {
43
44
  return MILESTONE_SET.has(event) ? 'milestone' : 'activity';
44
45
  }
46
+ /**
47
+ * Coerce an unknown value into a clean {@link Attachment}[] — a fail-open reader
48
+ * over user/agent-written JSON. Drops entries without a usable `href`, clamps
49
+ * `kind` to a string, and keeps only primitive `meta` values.
50
+ */
51
+ export function sanitizeAttachments(value) {
52
+ if (!Array.isArray(value))
53
+ return undefined;
54
+ const out = [];
55
+ for (const raw of value) {
56
+ if (!raw || typeof raw !== 'object')
57
+ continue;
58
+ const a = raw;
59
+ const href = typeof a.href === 'string' ? a.href.trim() : '';
60
+ if (!href)
61
+ continue;
62
+ const att = {
63
+ kind: typeof a.kind === 'string' && a.kind.trim() ? a.kind.trim() : 'link',
64
+ href,
65
+ };
66
+ if (typeof a.name === 'string' && a.name.trim())
67
+ att.name = a.name.trim();
68
+ if (typeof a.mediaType === 'string' && a.mediaType.trim())
69
+ att.mediaType = a.mediaType.trim();
70
+ if (typeof a.bytes === 'number' && Number.isFinite(a.bytes) && a.bytes >= 0)
71
+ att.bytes = a.bytes;
72
+ if (a.meta && typeof a.meta === 'object' && !Array.isArray(a.meta)) {
73
+ const meta = {};
74
+ for (const [k, v] of Object.entries(a.meta)) {
75
+ if (typeof v === 'string' || typeof v === 'number')
76
+ meta[k] = v;
77
+ }
78
+ if (Object.keys(meta).length > 0)
79
+ att.meta = meta;
80
+ }
81
+ out.push(att);
82
+ }
83
+ return out.length > 0 ? out : undefined;
84
+ }
45
85
  function activityPath(root, sessionId) {
46
86
  const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '-');
47
87
  if (!safe)
@@ -81,6 +121,7 @@ function parseLine(line) {
81
121
  host: parsed.host ?? 'unknown',
82
122
  runtime: parsed.runtime ?? 'headless',
83
123
  cwd: parsed.cwd,
124
+ project: parsed.project,
84
125
  agent: parsed.agent,
85
126
  tool: parsed.tool,
86
127
  detail: parsed.detail,
@@ -89,6 +130,7 @@ function parseLine(line) {
89
130
  launchId: parsed.launchId,
90
131
  terminalId: parsed.terminalId,
91
132
  tmuxPane: parsed.tmuxPane,
133
+ attachments: sanitizeAttachments(parsed.attachments),
92
134
  };
93
135
  }
94
136
  catch {
@@ -217,9 +259,11 @@ export function activityEventToRecord(ev) {
217
259
  detail: ev.detail,
218
260
  url: ev.url,
219
261
  tier: ev.tier,
262
+ ...(ev.project ? { project: ev.project } : {}),
220
263
  ...(ev.launchId ? { launchId: ev.launchId } : {}),
221
264
  ...(ev.terminalId ? { terminalId: ev.terminalId } : {}),
222
265
  ...(ev.tmuxPane ? { tmuxPane: ev.tmuxPane } : {}),
266
+ ...(ev.attachments?.length ? { attachments: ev.attachments } : {}),
223
267
  };
224
268
  }
225
269
  /** Read recent activity across sessions as unified {@link EventRecord}s. */
@@ -262,6 +306,244 @@ export function formatActivityLine(ev, opts = {}) {
262
306
  return ` ${when} ${host}${label}${detail}${agent}${url}`;
263
307
  }
264
308
  // ---------------------------------------------------------------------------
309
+ // Rich progress render (RUSH-2014): `status.posted` posts are the deliberate,
310
+ // operator-facing announcements — a single truncated line under-serves them.
311
+ // `formatProgressUpdate` renders each as a multi-line row with full identity
312
+ // chips (agent · session · host · project) and any attached artifacts.
313
+ // ---------------------------------------------------------------------------
314
+ /** Glyph per attachment kind, so an artifact row reads at a glance. */
315
+ export const ATTACHMENT_GLYPH = {
316
+ image: '🖼',
317
+ audio: '♪',
318
+ video: '▶',
319
+ file: '📎',
320
+ link: '↗',
321
+ };
322
+ export function attachmentGlyph(kind) {
323
+ return ATTACHMENT_GLYPH[kind] ?? '📎';
324
+ }
325
+ /** Display name for an attachment: its `name`, else the basename of its href. */
326
+ export function attachmentName(att) {
327
+ if (att.name && att.name.trim())
328
+ return att.name.trim();
329
+ const href = att.href.replace(/[/\\]+$/, '');
330
+ const slash = Math.max(href.lastIndexOf('/'), href.lastIndexOf('\\'));
331
+ const base = slash >= 0 ? href.slice(slash + 1) : href;
332
+ return base || href;
333
+ }
334
+ /**
335
+ * Short session id for a chip: strip a known prefix (`session_`, `ses_`) then
336
+ * take the first 8 chars — enough to disambiguate, matching `ag sessions`.
337
+ */
338
+ export function shortSessionId(sessionId) {
339
+ const stripped = sessionId.replace(/^(session_|ses_)/, '');
340
+ return stripped.slice(0, 8) || sessionId.slice(0, 8);
341
+ }
342
+ /**
343
+ * Render a deliberate progress update (`status.posted`) as an operator-facing,
344
+ * multi-line row with full identity — not a 60-char one-liner. Shape:
345
+ *
346
+ * ```
347
+ * ▸ update · 3m ago
348
+ * grok · 0108441e · yosemite-s1 · agents
349
+ * "CHANGELOG pushed; watching CI"
350
+ * ♪ draft.wav 🖼 cover.png
351
+ * ↳ ag focus 0108441e · ag sessions 0108441e
352
+ * ```
353
+ *
354
+ * Chips (agent, short session id, host, project, optional joined ticket/label)
355
+ * are shown only when known. Non-progress milestones keep {@link formatActivityLine}.
356
+ */
357
+ export function formatProgressUpdate(ev, opts = {}) {
358
+ const shortId = shortSessionId(ev.sessionId);
359
+ const lines = [];
360
+ lines.push(` ${chalk.white('▸ update')} ${chalk.gray(`· ${relTime(ev.ts)}`)}`);
361
+ const chips = [
362
+ ev.agent,
363
+ shortId,
364
+ ev.host && ev.host !== 'unknown' ? ev.host : undefined,
365
+ ev.project,
366
+ opts.joined?.ticketId,
367
+ opts.joined?.label,
368
+ ].filter((c) => Boolean(c));
369
+ if (chips.length > 0)
370
+ lines.push(` ${chalk.gray(chips.join(' · '))}`);
371
+ if (ev.detail)
372
+ lines.push(` ${chalk.white(`"${ev.detail}"`)}`);
373
+ if (ev.attachments?.length) {
374
+ const parts = ev.attachments.map((a) => `${attachmentGlyph(a.kind)} ${chalk.cyan(attachmentName(a))}`);
375
+ lines.push(` ${parts.join(' ')}`);
376
+ }
377
+ if (opts.joined?.prUrl)
378
+ lines.push(` ${chalk.gray(opts.joined.prUrl)}`);
379
+ lines.push(` ${chalk.dim(`↳ ag focus ${shortId} · ag sessions ${shortId}`)}`);
380
+ return lines.join('\n');
381
+ }
382
+ /**
383
+ * Resolve a stable project/repo name from a working directory. A worktree cwd
384
+ * (`…/<repo>/.agents/worktrees/<slug>[/sub]`) resolves to the repo dir name so
385
+ * a worktree session groups with its own repo; any other path resolves to its
386
+ * basename. Pure — no filesystem access, so it works for remote events too.
387
+ */
388
+ export function projectFromCwd(cwd) {
389
+ if (!cwd)
390
+ return undefined;
391
+ const norm = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
392
+ if (!norm)
393
+ return undefined;
394
+ const wtIdx = norm.indexOf('/.agents/worktrees/');
395
+ if (wtIdx > 0) {
396
+ const repoPath = norm.slice(0, wtIdx);
397
+ const base = repoPath.slice(repoPath.lastIndexOf('/') + 1);
398
+ if (base)
399
+ return base;
400
+ }
401
+ const base = norm.slice(norm.lastIndexOf('/') + 1);
402
+ return base || undefined;
403
+ }
404
+ /**
405
+ * Join session facts (ticket / project / execution host) onto each event by
406
+ * `sessionId`. A hint wins; otherwise pre-baked enriched fields (from a remote
407
+ * peer that already enriched its own stream) are preserved, and project/host
408
+ * fall back to what the event itself carries (`cwd`, `host`). Pure.
409
+ */
410
+ export function enrichActivityEvents(events, hints) {
411
+ const bySession = new Map();
412
+ for (const h of hints)
413
+ if (h.sessionId)
414
+ bySession.set(h.sessionId, h);
415
+ return events.map((ev) => {
416
+ const hint = ev.sessionId ? bySession.get(ev.sessionId) : undefined;
417
+ const project = hint?.project ?? ev.project ?? projectFromCwd(ev.cwd);
418
+ const ticket = hint?.ticket ?? ev.ticket ?? undefined;
419
+ const executionHost = hint?.executionHost ?? ev.executionHost
420
+ ?? (ev.host && ev.host !== 'unknown' ? ev.host : undefined);
421
+ return {
422
+ ...ev,
423
+ ...(project ? { project } : {}),
424
+ ...(ticket ? { ticket } : {}),
425
+ ...(executionHost ? { executionHost } : {}),
426
+ };
427
+ });
428
+ }
429
+ /**
430
+ * Validate + host-tag one peer's `activity --json` payload for the fan-out
431
+ * merge. Mirrors {@link parseLine}: skip anything missing `event`/`sessionId`/
432
+ * `ts`, drop corrupt items. The event's own `host` is the execution host and is
433
+ * authoritative; only fall back to the dialed peer's `machine` when it's absent.
434
+ * Enriched fields the peer stamped (project/ticket/executionHost) ride through.
435
+ */
436
+ export function parseActivityPayload(stdout, machine) {
437
+ let parsed;
438
+ try {
439
+ parsed = JSON.parse(stdout);
440
+ }
441
+ catch {
442
+ return [];
443
+ }
444
+ if (!Array.isArray(parsed))
445
+ return [];
446
+ const out = [];
447
+ for (const item of parsed) {
448
+ if (!item || typeof item !== 'object' || Array.isArray(item))
449
+ continue;
450
+ const ev = item;
451
+ if (!ev.event || !ev.sessionId || !ev.ts)
452
+ continue;
453
+ const host = ev.host && ev.host !== 'unknown' ? ev.host : machine;
454
+ out.push({ ...ev, host });
455
+ }
456
+ return out;
457
+ }
458
+ /**
459
+ * Merge local + remote event lists, keeping the first copy of a given
460
+ * host/session/ts/event and sorting newest first. Cross-machine activity dirs
461
+ * can sync in a peer's own events, so the identity key dedupes those (mirrors
462
+ * `mergeFeedBlocks`).
463
+ */
464
+ export function mergeActivityEvents(...groups) {
465
+ const byKey = new Map();
466
+ for (const ev of groups.flat()) {
467
+ const key = `${normalizeHost(ev.host)}\0${ev.sessionId}\0${ev.ts}\0${ev.event}`;
468
+ if (!byKey.has(key))
469
+ byKey.set(key, ev);
470
+ }
471
+ return [...byKey.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
472
+ }
473
+ /** The (key, label) an event sorts under for a given grouping dimension. */
474
+ export function activityGroupKey(ev, by) {
475
+ if (by === 'project') {
476
+ const p = ev.project ?? projectFromCwd(ev.cwd);
477
+ return p ? { key: p, label: p } : { key: '', label: 'unknown project' };
478
+ }
479
+ if (by === 'device') {
480
+ const h = ev.executionHost ?? (ev.host && ev.host !== 'unknown' ? ev.host : undefined);
481
+ return h ? { key: h, label: h } : { key: '', label: 'unknown device' };
482
+ }
483
+ const a = ev.agent;
484
+ return a ? { key: a, label: a } : { key: '', label: 'unknown agent' };
485
+ }
486
+ /**
487
+ * Bucket events by project, device, or agent. Groups are ordered by event count
488
+ * (desc) then label; the "unknown" bucket always sorts last. Input order is
489
+ * preserved within a group, so newest-first survives when the input is sorted.
490
+ */
491
+ export function groupActivity(events, by) {
492
+ const byKey = new Map();
493
+ for (const ev of events) {
494
+ const { key, label } = activityGroupKey(ev, by);
495
+ const g = byKey.get(key);
496
+ if (g)
497
+ g.events.push(ev);
498
+ else
499
+ byKey.set(key, { key, label, events: [ev] });
500
+ }
501
+ return [...byKey.values()].sort((a, b) => {
502
+ if (!a.key && b.key)
503
+ return 1;
504
+ if (!b.key && a.key)
505
+ return -1;
506
+ if (b.events.length !== a.events.length)
507
+ return b.events.length - a.events.length;
508
+ return a.label.localeCompare(b.label);
509
+ });
510
+ }
511
+ /**
512
+ * Narrow events to those whose project, device/host, agent, event kind, or
513
+ * ticket contains `filter` (case-insensitive substring). An empty filter is a
514
+ * no-op. Pure.
515
+ */
516
+ export function filterActivityEvents(events, filter) {
517
+ const needle = filter.trim().toLowerCase();
518
+ if (!needle)
519
+ return events;
520
+ return events.filter((ev) => {
521
+ const fields = [ev.project, ev.executionHost, ev.host, ev.agent, ev.event, ev.ticket];
522
+ return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(needle));
523
+ });
524
+ }
525
+ /** One rendered enriched line: the base activity line + `· project · ticket` tags. */
526
+ export function formatEnrichedActivityLine(ev, opts = {}) {
527
+ const base = formatActivityLine(ev, { showHost: opts.showHost });
528
+ const tags = [];
529
+ if (opts.showProject && ev.project)
530
+ tags.push(ev.project);
531
+ if (ev.ticket)
532
+ tags.push(ev.ticket);
533
+ const suffix = tags.length > 0 ? chalk.gray(` · ${tags.join(' · ')}`) : '';
534
+ return `${opts.indent ?? ''}${base}${suffix}`;
535
+ }
536
+ /** Human header for one grouped bucket: `<label> · N events · M milestones`. */
537
+ export function formatActivityGroupHeader(group) {
538
+ const { milestones } = collapseActivity(group.events);
539
+ const n = group.events.length;
540
+ const m = milestones.length;
541
+ const parts = [`${n} event${n === 1 ? '' : 's'}`];
542
+ if (m > 0)
543
+ parts.push(`${m} milestone${m === 1 ? '' : 's'}`);
544
+ return `${group.label} · ${parts.join(' · ')}`;
545
+ }
546
+ // ---------------------------------------------------------------------------
265
547
  // Hook installation
266
548
  // ---------------------------------------------------------------------------
267
549
  /**
@@ -32,13 +32,17 @@ export declare function findInPath(command: string): string | null;
32
32
  * single source of truth for agent metadata consumed throughout the codebase.
33
33
  */
34
34
  export declare const AGENTS: Record<AgentId, AgentConfig>;
35
- /** All registered agent IDs derived from the AGENTS registry. */
35
+ /** All current and legacy agent IDs derived from the AGENTS registry. */
36
36
  export declare const ALL_AGENT_IDS: AgentId[];
37
+ /** Agents retained only for legacy reads, not install/import/sync targets. */
38
+ export declare const HARD_DEPRECATED_AGENT_IDS: AgentId[];
39
+ /** Agents that can receive managed installs, imports, and resource sync writes. */
40
+ export declare const MANAGED_AGENT_IDS: AgentId[];
37
41
  /**
38
42
  * A self-updating agent is a single global binary installed by an official
39
43
  * `curl … | sh` / `brew install` script that carries NO version token — the
40
44
  * installer can only ever fetch the *current* release, and the binary then keeps
41
- * itself up to date in place (droid, grok, antigravity, cursor, hermes, forge,
45
+ * itself up to date in place (droid, grok, antigravity, cursor, hermes,
42
46
  * kiro, goose). There is no semver to pin, so agents-cli must not model these as
43
47
  * having multiple installable version-homes the way it does for npm-packaged
44
48
  * agents (claude, codex, kimi, …).
@@ -55,6 +59,7 @@ export declare const ALL_AGENT_IDS: AgentId[];
55
59
  * never a scattered `agent === 'droid'`.
56
60
  */
57
61
  export declare function isSelfUpdatingAgent(agent: AgentId): boolean;
62
+ export declare function isAgentHardDeprecated(agent: AgentId): boolean;
58
63
  /** Get the chalk color function for an agent. Works for any AgentId or SessionAgentId. */
59
64
  export declare function colorAgent(agentId: string): (s: string) => string;
60
65
  /** Return the agent's display name, colored. */
@@ -383,6 +388,8 @@ export declare function isAgentName(input: string): boolean;
383
388
  * capturing stdout. Lines are plain (uncolored) text.
384
389
  */
385
390
  export declare function deprecationNotice(agent: AgentId): string[] | null;
391
+ export declare function hardDeprecationNotice(agent: AgentId): string[] | null;
392
+ export declare function hardDeprecationError(agent: AgentId): string;
386
393
  /**
387
394
  * Print a deprecation warning (yellow) if the agent's registry entry carries a
388
395
  * `deprecated` marker; no-op otherwise. Call from any user entry point that