@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.
- package/LICENSE +75 -21
- package/README.md +19 -0
- package/dashboard/dashboard-data.mjs +103 -0
- package/dashboard/dashboard.html +539 -172
- package/dashboard/lib/broadcast-share.mjs +251 -0
- package/dashboard/lib/guards.d.mts +10 -0
- package/dashboard/lib/guards.mjs +71 -4
- package/dashboard/lib/open-browser.d.mts +31 -0
- package/dashboard/lib/open-browser.mjs +298 -0
- package/dashboard/lib/semantic-model.mjs +215 -9
- package/dashboard/open.html +213 -0
- package/dashboard/serve.mjs +6 -2
- package/dashboard/start-broadcast.mjs +75 -31
- package/dashboard/start.mjs +23 -26
- package/dist/index.js +1837 -447
- package/package.json +4 -2
|
@@ -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)
|
|
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
|
|
2157
|
-
: `${actor} \u00b7
|
|
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
|
|
2177
|
-
*
|
|
2178
|
-
*
|
|
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 "
|
|
2186
|
-
if (plan) return "
|
|
2187
|
-
return "
|
|
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),
|