@dadado/agent-kit-cli 4.8.9 → 5.1.0

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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Shared Mission Control browser open helper.
3
+ *
4
+ * Preference resolution (highest wins):
5
+ * 1. env MISSION_CONTROL_PREFERRED_BROWSER
6
+ * 2. config missionControl.preferredBrowser (passed in by caller)
7
+ * 3. OS default handler (null preferred)
8
+ *
9
+ * Skips open when MISSION_CONTROL_NO_OPEN=1.
10
+ * Never opens more than one process per call (preferred may fall back once).
11
+ *
12
+ * Trust boundary: preferredBrowser is an app/binary *name*, not a path or
13
+ * shell expression. Values with path separators or shell metacharacters are
14
+ * rejected and treated as OS default.
15
+ *
16
+ * ADR: .cursor/memory/decisions/2026-08-11_mission-control-preferred-browser.md
17
+ */
18
+
19
+ import { spawn, spawnSync } from "node:child_process";
20
+ import { readFileSync } from "node:fs";
21
+ import { platform as osPlatform } from "node:os";
22
+
23
+ /** Sentinel values that mean "use OS default" (and slash-only Ask). */
24
+ export const OS_DEFAULT_TOKENS = new Set(["", "default", "os", "ask"]);
25
+
26
+ /**
27
+ * Reject path separators, absolute/relative path forms, and shell metacharacters.
28
+ * Allowed examples: "Google Chrome", "firefox", "msedge", "Brave Browser".
29
+ *
30
+ * @param {string} value
31
+ * @returns {boolean}
32
+ */
33
+ export function isSafePreferredBrowser(value) {
34
+ if (typeof value !== "string") return false;
35
+ const s = value.trim();
36
+ if (!s) return false;
37
+ if (/[/\\]/.test(s)) return false;
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional reject of C0/DEL in browser names
39
+ if (/[\0-\x1f\x7f]/.test(s)) return false;
40
+ if (/[$`;&|<>(){}[\]!*?#~"'%^=,+]/.test(s)) return false;
41
+ if (s.includes(":")) return false;
42
+ if (/^-/.test(s)) return false;
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * @param {NodeJS.ProcessEnv} [env]
48
+ * @returns {boolean}
49
+ */
50
+ export function shouldSkipOpen(env = process.env) {
51
+ return env.MISSION_CONTROL_NO_OPEN === "1";
52
+ }
53
+
54
+ /**
55
+ * @param {unknown} value
56
+ * @returns {string | null} trimmed app/binary name, or null for OS default
57
+ */
58
+ export function normalizePreferredBrowser(value) {
59
+ if (value == null) return null;
60
+ const s = String(value).trim();
61
+ if (!s || OS_DEFAULT_TOKENS.has(s.toLowerCase())) return null;
62
+ if (!isSafePreferredBrowser(s)) return null;
63
+ return s;
64
+ }
65
+
66
+ /**
67
+ * @param {{ env?: NodeJS.ProcessEnv, configValue?: unknown }} [opts]
68
+ * @returns {string | null}
69
+ */
70
+ export function resolvePreferredBrowser(opts = {}) {
71
+ const env = opts.env ?? process.env;
72
+ const fromEnv = env.MISSION_CONTROL_PREFERRED_BROWSER;
73
+ if (fromEnv != null && String(fromEnv).trim() !== "") {
74
+ return normalizePreferredBrowser(fromEnv);
75
+ }
76
+ return normalizePreferredBrowser(opts.configValue);
77
+ }
78
+
79
+ /**
80
+ * Read missionControl.preferredBrowser from a context config.json path.
81
+ * Missing/invalid file → null (OS default). Does not create the file.
82
+ *
83
+ * @param {string} configPath
84
+ * @param {{ readFileSync?: typeof readFileSync }} [fsHooks]
85
+ * @returns {unknown}
86
+ */
87
+ export function readPreferredBrowserFromConfig(configPath, fsHooks = {}) {
88
+ const read = fsHooks.readFileSync ?? readFileSync;
89
+ try {
90
+ const raw = read(configPath, "utf8");
91
+ const data = JSON.parse(raw);
92
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
93
+ const mc = data.missionControl;
94
+ if (!mc || typeof mc !== "object" || Array.isArray(mc)) return null;
95
+ return mc.preferredBrowser ?? null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Build an argv for a single open attempt (hermetic: no spawn).
103
+ *
104
+ * @param {{
105
+ * url: string,
106
+ * preferred?: string | null,
107
+ * platform?: NodeJS.Platform,
108
+ * }} opts
109
+ * @returns {{ command: string, args: string[] } | null}
110
+ */
111
+ export function buildOpenBrowserCommand(opts) {
112
+ const url = opts.url;
113
+ if (typeof url !== "string" || !url.trim()) return null;
114
+ const preferred = normalizePreferredBrowser(opts.preferred ?? null);
115
+ const os = opts.platform ?? osPlatform();
116
+
117
+ if (os === "darwin") {
118
+ if (preferred) {
119
+ return { command: "open", args: ["-a", preferred, url] };
120
+ }
121
+ return { command: "open", args: [url] };
122
+ }
123
+
124
+ if (os === "win32") {
125
+ if (preferred) {
126
+ // `start` treats the first quoted arg as window title; pass empty title.
127
+ return { command: "cmd", args: ["/c", "start", "", preferred, url] };
128
+ }
129
+ return { command: "cmd", args: ["/c", "start", "", url] };
130
+ }
131
+
132
+ // Linux / other: preferred is a binary or command name; else xdg-open.
133
+ if (preferred) {
134
+ return { command: preferred, args: [url] };
135
+ }
136
+ return { command: "xdg-open", args: [url] };
137
+ }
138
+
139
+ /**
140
+ * @param {import("node:child_process").ChildProcess | { on?: Function, unref?: Function } | null | undefined} child
141
+ */
142
+ function attachErrorSwallow(child) {
143
+ if (child && typeof child.on === "function") {
144
+ child.on("error", () => {
145
+ /* prevent unhandled 'error' (ENOENT) from killing the launcher */
146
+ });
147
+ }
148
+ if (child && typeof child.unref === "function") {
149
+ child.unref();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Open one browser for the URL. Returns whether a process was spawned.
155
+ * When a preferred open fails, falls back once to the OS default opener.
156
+ *
157
+ * @param {string} url
158
+ * @param {{
159
+ * env?: NodeJS.ProcessEnv,
160
+ * preferred?: string | null,
161
+ * configValue?: unknown,
162
+ * platform?: NodeJS.Platform,
163
+ * spawnFn?: typeof spawn,
164
+ * spawnSyncFn?: typeof spawnSync,
165
+ * }} [options]
166
+ * @returns {{ opened: boolean, reason?: string, command?: string, args?: string[] }}
167
+ */
168
+ export function openBrowser(url, options = {}) {
169
+ const env = options.env ?? process.env;
170
+ if (shouldSkipOpen(env)) {
171
+ return { opened: false, reason: "no-open" };
172
+ }
173
+
174
+ const preferred =
175
+ options.preferred !== undefined
176
+ ? normalizePreferredBrowser(options.preferred)
177
+ : resolvePreferredBrowser({ env, configValue: options.configValue });
178
+
179
+ const platform = options.platform ?? osPlatform();
180
+ const spawnFn = options.spawnFn ?? spawn;
181
+ const spawnSyncFn = options.spawnSyncFn;
182
+
183
+ /**
184
+ * @param {{ command: string, args: string[] }} built
185
+ * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }}
186
+ */
187
+ function runDetached(built) {
188
+ try {
189
+ const child = spawnFn(built.command, built.args, { detached: true, stdio: "ignore" });
190
+ attachErrorSwallow(child);
191
+ return { opened: true, command: built.command, args: built.args };
192
+ } catch {
193
+ return {
194
+ opened: false,
195
+ reason: "spawn-failed",
196
+ command: built.command,
197
+ args: built.args,
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Preferred open: detect failure before claiming success, then caller may fall back.
204
+ * Hermetic tests that only inject spawnFn use the detached path (throw = fail).
205
+ *
206
+ * @param {{ command: string, args: string[] }} built
207
+ * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }}
208
+ */
209
+ function runPreferred(built) {
210
+ if (options.spawnFn && !spawnSyncFn) {
211
+ return runDetached(built);
212
+ }
213
+
214
+ const sync = spawnSyncFn ?? spawnSync;
215
+
216
+ if (platform !== "darwin" && platform !== "win32") {
217
+ // Long-lived browser binaries: probe PATH, then detach (do not spawnSync the app).
218
+ const probe = sync("which", [built.command], { encoding: "utf8" });
219
+ if (probe.error || (typeof probe.status === "number" && probe.status !== 0)) {
220
+ return {
221
+ opened: false,
222
+ reason: "spawn-failed",
223
+ command: built.command,
224
+ args: built.args,
225
+ };
226
+ }
227
+ return runDetached(built);
228
+ }
229
+
230
+ // darwin `open` / win32 `cmd /c start` exit quickly.
231
+ try {
232
+ const result = sync(built.command, built.args, {
233
+ encoding: "utf8",
234
+ windowsHide: true,
235
+ });
236
+ if (result.error || (typeof result.status === "number" && result.status !== 0)) {
237
+ return {
238
+ opened: false,
239
+ reason: "spawn-failed",
240
+ command: built.command,
241
+ args: built.args,
242
+ };
243
+ }
244
+ return { opened: true, command: built.command, args: built.args };
245
+ } catch {
246
+ return {
247
+ opened: false,
248
+ reason: "spawn-failed",
249
+ command: built.command,
250
+ args: built.args,
251
+ };
252
+ }
253
+ }
254
+
255
+ const built = buildOpenBrowserCommand({
256
+ url,
257
+ preferred,
258
+ platform,
259
+ });
260
+ if (!built) {
261
+ return { opened: false, reason: "invalid-url" };
262
+ }
263
+
264
+ if (!preferred) {
265
+ // Same failure detection as preferred opens (probe / spawnSync) so OS-default
266
+ // missing handlers are not reported as opened:true.
267
+ return runPreferred(built);
268
+ }
269
+
270
+ const prefResult = runPreferred(built);
271
+ if (prefResult.opened) {
272
+ return prefResult;
273
+ }
274
+
275
+ const fallback = buildOpenBrowserCommand({
276
+ url,
277
+ preferred: null,
278
+ platform,
279
+ });
280
+ if (!fallback) {
281
+ return { opened: false, reason: "invalid-url" };
282
+ }
283
+ const fb = runPreferred(fallback);
284
+ if (fb.opened) {
285
+ return {
286
+ opened: true,
287
+ command: fb.command,
288
+ args: fb.args,
289
+ reason: "preferred-fallback",
290
+ };
291
+ }
292
+ return {
293
+ opened: false,
294
+ reason: "spawn-failed",
295
+ command: built.command,
296
+ args: built.args,
297
+ };
298
+ }
@@ -24,10 +24,16 @@ export const MONITOR_FEED_CAP = 20;
24
24
  /** Cap agent_step rows emitted per active plan for the denser Crew feed. */
25
25
  export const MONITOR_AGENT_STEP_EMIT_CAP = 12;
26
26
 
27
+ /** Cap subagent-run rows emitted per snapshot (fs scan bounds live in dashboard-data.mjs). */
28
+ export const MONITOR_SUBAGENT_EMIT_CAP = 8;
29
+ /** Cap plan_review pointer rows emitted per snapshot. */
30
+ export const MONITOR_PLAN_REVIEW_EMIT_CAP = 4;
31
+
27
32
  /**
28
33
  * Monitor hero curated subset over the semantic activity stream.
29
34
  * Live agent steps: run_plan / handoff / delivery plus agent_step (Task/orchestrator
30
- * to-do steps). plan_progress milestones stay on Activity / Checklist.
35
+ * to-do steps), subagent (Task worker lifecycle) and plan_review (background
36
+ * mid-batch review pointers). plan_progress milestones stay on Activity / Checklist.
31
37
  * Activity (Phase 2) is the superset; inventory kinds are excluded here.
32
38
  */
33
39
  export const MONITOR_ACTIVITY_KINDS = Object.freeze([
@@ -35,6 +41,8 @@ export const MONITOR_ACTIVITY_KINDS = Object.freeze([
35
41
  "handoff",
36
42
  "delivery",
37
43
  "agent_step",
44
+ "subagent",
45
+ "plan_review",
38
46
  ]);
39
47
 
40
48
  /**
@@ -2152,9 +2160,12 @@ export function formatDeliveryActivity(logLines, { plans = [], limit = MAX_GIT_A
2152
2160
  const kitAgent = normalizeKitAgentId(agent);
2153
2161
  const actor = briefActivityActor(kitAgent, { kind: "delivery", plan: planName });
2154
2162
  const prBit = `PR #${entry.pr}`;
2163
+ // Verb `merged` (not `shipped`, retired 2026-08-05): the row is derived from
2164
+ // a merge/squash entry, and `shipped` implied a prod promote /git-staging
2165
+ // never performed.
2155
2166
  const label = brief
2156
- ? `${actor} \u00b7 shipped \u00b7 ${brief} \u00b7 ${prBit} \u00b7 ${entry.sha}`
2157
- : `${actor} \u00b7 shipped \u00b7 ${prBit} \u00b7 ${entry.sha}`;
2167
+ ? `${actor} \u00b7 merged \u00b7 ${brief} \u00b7 ${prBit} \u00b7 ${entry.sha}`
2168
+ : `${actor} \u00b7 merged \u00b7 ${prBit} \u00b7 ${entry.sha}`;
2158
2169
  const commitType = parseDeliveryCommitType(brief, { hasPr: entry.pr != null });
2159
2170
  events.push({
2160
2171
  id: activityId("delivery", ["merge", String(entry.pr)]),
@@ -2173,18 +2184,23 @@ export function formatDeliveryActivity(logLines, { plans = [], limit = MAX_GIT_A
2173
2184
 
2174
2185
  /**
2175
2186
  * Actor segment for Monitor return-brief labels.
2176
- * Kit agent id, else Engineering Manager for delivery, else Squad when a plan
2177
- * is present (never the full plan filename), else Platform Engineer.
2178
- * Default software lexicon display masks (resolution kinds unchanged).
2187
+ * Kit agent id, else `Eng` for delivery, else `SQ` when a plan is present
2188
+ * (never the full plan filename), else `Eng`.
2189
+ *
2190
+ * Short display masks from the operator lexicon (2026-08-05): Engineering
2191
+ * Manager -> Eng, Squad -> SQ, Platform Engineer -> Eng. `Eng` is a documented
2192
+ * collision between the delivery and system fallbacks; the resolution keys
2193
+ * (`orchestrator` / `crew` / `system`) and the row's kind glyph stay distinct.
2194
+ * ADR: decisions/2026-07-27_crew-monitor-vs-plan-monitor-glossary.md.
2179
2195
  * @param {string|null|undefined} agent
2180
2196
  * @param {{ kind?: string, plan?: string|null }} [opts]
2181
2197
  */
2182
2198
  export function briefActivityActor(agent, { kind, plan } = {}) {
2183
2199
  const kit = normalizeKitAgentId(agent);
2184
2200
  if (kit) return kit;
2185
- if (kind === "delivery") return "Engineering Manager";
2186
- if (plan) return "Squad";
2187
- return "Platform Engineer";
2201
+ if (kind === "delivery") return "Eng";
2202
+ if (plan) return "SQ";
2203
+ return "Eng";
2188
2204
  }
2189
2205
 
2190
2206
  /**
@@ -2316,6 +2332,191 @@ export function formatPlanHandoffActivity({ now, handoff, plans }) {
2316
2332
  return events;
2317
2333
  }
2318
2334
 
2335
+ /**
2336
+ * Task subagent transcripts are `<uuid>.jsonl` inside a parent chat's
2337
+ * `subagents/` directory.
2338
+ */
2339
+ export const SUBAGENT_TRANSCRIPT_FILE_RE = /^([0-9a-fA-F][0-9a-fA-F-]{7,})\.jsonl$/;
2340
+
2341
+ /**
2342
+ * Worker-prompt fields the kit's own dispatch template declares (see
2343
+ * `.cursor/commands/run-plan.md`). Both forms occur in real dispatches: the
2344
+ * bare `To-do id: x` of the plain template and the `- **worker_type:** x` of a
2345
+ * bulleted orchestrator prompt, so the leading list marker and the markdown
2346
+ * emphasis on either side of the colon are optional. The captured value
2347
+ * excludes `*` and a backtick so `**explore**` and `` `explore` `` yield
2348
+ * `explore` rather than the decoration.
2349
+ */
2350
+ const SUBAGENT_TODO_ID_RE =
2351
+ /^[ \t]*(?:[-*][ \t]*)?\**To-?do id\**[ \t]*[:=][ \t]*\**[ \t]*([^\s*`]+)/im;
2352
+ const SUBAGENT_WORKER_TYPE_RE =
2353
+ /^[ \t]*(?:[-*][ \t]*)?\**(?:worker_type(?:[ \t]*\/[ \t]*subagent_type)?|subagent_type)\**[ \t]*[:=][ \t]*\**[ \t]*([^\s*`]+)/im;
2354
+
2355
+ /** Plain-text content of a transcript entry (user prompt or assistant reply). */
2356
+ function subagentEntryText(entry) {
2357
+ const content = entry?.message?.content;
2358
+ if (typeof content === "string") return content;
2359
+ if (!Array.isArray(content)) return "";
2360
+ const parts = [];
2361
+ for (const c of content) {
2362
+ if (c && c.type === "text" && typeof c.text === "string") parts.push(c.text);
2363
+ }
2364
+ return parts.join("\n");
2365
+ }
2366
+
2367
+ /**
2368
+ * Lifecycle of one Task subagent run, from the two records that carry it: the
2369
+ * dispatch prompt (first entry) and the terminal record (last entry).
2370
+ *
2371
+ * Phase contract: a transcript whose last record is not `turn_ended` is still
2372
+ * `running`; `turn_ended` with `status: "success"` is `done`; any other status
2373
+ * (including `error`) is `failed`. A transcript that is empty or entirely
2374
+ * unparsable yields `null` rather than a phantom running row.
2375
+ *
2376
+ * The fs half (directory layout, recency window, file/byte caps) lives in
2377
+ * `dashboard-data.mjs` next to the agent-prompt scan contract. Transcript paths
2378
+ * live under `$HOME`, never in the repo, so no `sourcePath` is emitted.
2379
+ *
2380
+ * @param {{ id?: string, parentId?: string|null, firstLine?: string, lastLine?: string, modifiedAt?: string|null }} input
2381
+ * @returns {{ id: string, parentId: string|null, phase: 'running'|'done'|'failed', todoId: string|null, workerType: string|null, modifiedAt: string|null }|null}
2382
+ */
2383
+ export function parseSubagentRun({
2384
+ id,
2385
+ parentId = null,
2386
+ firstLine = "",
2387
+ lastLine = "",
2388
+ modifiedAt = null,
2389
+ } = {}) {
2390
+ const runId = String(id || "").trim();
2391
+ if (!runId) return null;
2392
+
2393
+ let first = null;
2394
+ let last = null;
2395
+ try {
2396
+ first = firstLine ? JSON.parse(firstLine) : null;
2397
+ } catch {
2398
+ first = null;
2399
+ }
2400
+ try {
2401
+ last = lastLine ? JSON.parse(lastLine) : null;
2402
+ } catch {
2403
+ last = null;
2404
+ }
2405
+ if (!first && !last) return null;
2406
+
2407
+ let phase = "running";
2408
+ if (last && last.type === "turn_ended") {
2409
+ phase = last.status === "success" ? "done" : "failed";
2410
+ }
2411
+
2412
+ const promptText = first && first.role === "user" ? subagentEntryText(first) : "";
2413
+ const todoMatch = promptText ? SUBAGENT_TODO_ID_RE.exec(promptText) : null;
2414
+ const typeMatch = promptText ? SUBAGENT_WORKER_TYPE_RE.exec(promptText) : null;
2415
+ const rawTodo = todoMatch ? todoMatch[1] : null;
2416
+ const rawType = typeMatch ? typeMatch[1] : null;
2417
+ // The template writes literal placeholders when a field is unset; those are
2418
+ // not identities and must not reach a row.
2419
+ const placeholder = /^(?:<.*>|none|n\/a|-{1,2})$/i;
2420
+ return {
2421
+ id: runId,
2422
+ parentId: parentId ? String(parentId) : null,
2423
+ phase,
2424
+ todoId: rawTodo && !placeholder.test(rawTodo) ? rawTodo : null,
2425
+ workerType: rawType && !placeholder.test(rawType) ? rawType : null,
2426
+ modifiedAt: modifiedAt || null,
2427
+ };
2428
+ }
2429
+
2430
+ /**
2431
+ * Live Crew Monitor rows for Task subagent runs (start / still running /
2432
+ * complete / failed). Newest first; the caller passes an already-bounded list.
2433
+ *
2434
+ * Deliberately a distinct kind from `agent_step`: `agent_step` is derived from
2435
+ * plan to-do status, so a subagent that runs without flipping a to-do would be
2436
+ * invisible there and a to-do flipped by hand would be misattributed to a
2437
+ * worker. ADR: decisions/2026-07-27_crew-monitor-vs-plan-monitor-glossary.md.
2438
+ *
2439
+ * @param {object[]} runs - `parseSubagentRun` output
2440
+ * @param {{ limit?: number }} [opts]
2441
+ */
2442
+ export function formatSubagentActivity(runs, { limit = MONITOR_SUBAGENT_EMIT_CAP } = {}) {
2443
+ const events = [];
2444
+ for (const run of runs || []) {
2445
+ if (events.length >= limit) break;
2446
+ if (!run || !run.id) continue;
2447
+ const kitAgent = normalizeKitAgentId(run.workerType);
2448
+ // Display actor is the dispatched worker type whenever the prompt declared
2449
+ // one: a built-in type such as `explore` is a real worker identity even
2450
+ // though it is not a `.cursor/agents/` id. `agent` stays kit-id-only so
2451
+ // downstream attribution is unchanged. `Dev` is the operator-lexicon mask
2452
+ // for Developer / Full-Stack Developer, used when no type was declared.
2453
+ const actor = run.workerType ? truncateStr(String(run.workerType), 24) : "Dev";
2454
+ const shortId = String(run.id).slice(0, 8);
2455
+ const subject = run.todoId || "task";
2456
+ const visible = `${actor} · ${run.phase} · ${subject} · ${shortId}`;
2457
+ events.push({
2458
+ id: activityId("subagent", [run.id, run.phase]),
2459
+ kind: "subagent",
2460
+ at: run.modifiedAt || null,
2461
+ agent: kitAgent,
2462
+ label: truncateStr(visible, MAX_SEMANTIC_LABEL),
2463
+ labelFull: visible,
2464
+ // Transcripts live outside the repo (under $HOME); no repo path to copy.
2465
+ sourcePath: null,
2466
+ refs: { subagent: run.id, parent: run.parentId || null, phase: run.phase, todo: run.todoId },
2467
+ });
2468
+ }
2469
+ return events;
2470
+ }
2471
+
2472
+ /**
2473
+ * Crew Monitor pointer rows for background mid-batch plan reviews.
2474
+ *
2475
+ * The operator cannot otherwise see that a review ran: `plan-monitor-*.md` lands
2476
+ * silently in `.cursor/memory/` and only surfaces once Flight Log / attention
2477
+ * picks it up. These rows say a review exists and whether it is still owed
2478
+ * triage. They are pointers only — Flight Log and the attention inbox keep sole
2479
+ * ownership of triage state and actions, and a row never marks anything
2480
+ * reviewed. Boundary amend recorded in the glossary ADR (2026-08-05).
2481
+ *
2482
+ * @param {object[]} reports - `parseExternalReport` output
2483
+ * @param {object[]} plans - plan records from the snapshot
2484
+ * @param {{ limit?: number }} [opts]
2485
+ */
2486
+ export function formatPlanReviewActivity(
2487
+ reports,
2488
+ plans,
2489
+ { limit = MONITOR_PLAN_REVIEW_EMIT_CAP } = {},
2490
+ ) {
2491
+ const sorted = (reports || [])
2492
+ .filter((r) => r?.file)
2493
+ .slice()
2494
+ .sort((a, b) => String(b.modifiedAt || "").localeCompare(String(a.modifiedAt || "")));
2495
+
2496
+ const events = [];
2497
+ for (const report of sorted) {
2498
+ if (events.length >= limit) break;
2499
+ const triaged = isReportTriaged(report, plans);
2500
+ // `awaiting` reuses the existing gate verb: the review itself has landed,
2501
+ // what is outstanding is the operator's triage.
2502
+ const verb = triaged ? "done" : "awaiting";
2503
+ // `QA` is the operator-lexicon mask for QA Engineer.
2504
+ const planRef = report.reviewedPlanFile || `${report.slug}.plan.md`;
2505
+ const visible = `QA · ${verb} · review · ${planRef}`;
2506
+ events.push({
2507
+ id: activityId("plan_review", [report.file, triaged ? "triaged" : "open"]),
2508
+ kind: "plan_review",
2509
+ at: report.modifiedAt || null,
2510
+ agent: null,
2511
+ label: truncateStr(visible, MAX_SEMANTIC_LABEL),
2512
+ labelFull: visible,
2513
+ sourcePath: report.path || null,
2514
+ refs: { plan: report.reviewedPlanFile || null, report: report.file, triaged },
2515
+ });
2516
+ }
2517
+ return events;
2518
+ }
2519
+
2319
2520
  /**
2320
2521
  * Explicit run-plan loop lines in terminal output. Shared detection for the
2321
2522
  * Crew feed (formatTerminalRunEvidence) and the busy-outside-plan derivation
@@ -3562,6 +3763,7 @@ export function buildMissionControlView({
3562
3763
  deferredCheckIds = [],
3563
3764
  agentPrompts = [],
3564
3765
  externalReports = [],
3766
+ subagentRuns = [],
3565
3767
  dismissedIds = [],
3566
3768
  archivedPlanFiles = [],
3567
3769
  agents = [],
@@ -3615,7 +3817,11 @@ export function buildMissionControlView({
3615
3817
  const activity = mergeActivity([
3616
3818
  planEvents.filter((e) => e.kind === "run_plan" || e.kind === "handoff"),
3617
3819
  planEvents.filter((e) => e.kind === "agent_step"),
3820
+ // Live Task-worker lifecycle before delivery: a running subagent is the
3821
+ // freshest thing on the board and must not be starved by MAX_ACTIVITY.
3822
+ formatSubagentActivity(subagentRuns),
3618
3823
  deliveryEvents,
3824
+ formatPlanReviewActivity(externalReports, plans),
3619
3825
  planEvents.filter((e) => e.kind === "plan_progress"),
3620
3826
  formatGitActivity(gitLogLines, { excludeShas: supersededShas }),
3621
3827
  formatTerminalRunEvidence(terminals),