@dadado/agent-kit-cli 5.6.0 → 5.7.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/README.md +4 -1
- package/dashboard/README.md +17 -0
- package/dashboard/dashboard-data.mjs +215 -5
- package/dashboard/dashboard.html +285 -1
- package/dashboard/lib/live-refresh.d.mts +23 -0
- package/dashboard/lib/semantic-model.mjs +12 -5
- package/dist/index.js +693 -161
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ agent-kit dashboard
|
|
|
43
43
|
|
|
44
44
|
The panel binds to loopback by default, serves its own static files, and snapshots the current workspace. L0 install does **not** copy `dashboard/` into your app; `agent-kit dashboard` resolves the panel from the installed package.
|
|
45
45
|
|
|
46
|
+
Browser-free: `agent-kit mission-control` is a third surface (ASCII Mission, Flight Log, Checklist, Crew Monitor) that reuses the same snapshot builders without starting the HTTP server. `agent-kit mission-control --once` prints one frame (Claude Code `/agent-kit`). The web dashboard stays shipped.
|
|
47
|
+
|
|
46
48
|
Older tags before 4.8.2 do not include those assets. Prefer a current pin, or point `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` at an agent-kit checkout that contains `dashboard/`.
|
|
47
49
|
|
|
48
50
|
## Bare invoke (welcome)
|
|
@@ -73,7 +75,8 @@ On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `upda
|
|
|
73
75
|
| `agent-kit doctor` | Diagnose repository readiness (`--json` includes an `env` pillar: bin-on-PATH, npm prefix writability, Node version, shell profile) |
|
|
74
76
|
| `agent-kit setup-global` | Self-heal a root-owned npm global prefix (relocate to `~/.npm-global`, fix `PATH`, reinstall) |
|
|
75
77
|
| `agent-kit update` | Re-apply L0/packs/skills from the registry |
|
|
76
|
-
| `agent-kit dashboard` | Start Mission Control for this workspace |
|
|
78
|
+
| `agent-kit dashboard` | Start Mission Control for this workspace (browser panel) |
|
|
79
|
+
| `agent-kit mission-control` | ASCII Mission Control TUI (`--once` for one frame) |
|
|
77
80
|
| `agent-kit add <id>` | Install a skill or L1 pack |
|
|
78
81
|
| `agent-kit run-plan` | Headless continuous plan runner (never promotes to production) |
|
|
79
82
|
|
package/dashboard/README.md
CHANGED
|
@@ -23,3 +23,20 @@ before it fans out to the workspace packages).
|
|
|
23
23
|
|
|
24
24
|
`dashboard.html` is outside Biome's scope; CSS/HTML-only changes are covered by
|
|
25
25
|
`packages/cli/src/dashboard/plugin-ux-validation.test.ts` instead.
|
|
26
|
+
|
|
27
|
+
## Git tab and DevOps tab
|
|
28
|
+
|
|
29
|
+
The Git tab (`#git`) keeps the pre-rendered `git log --graph` markdown block and adds a
|
|
30
|
+
second, state-colored visual tree next to it, reusing the `.now-stepper`/`.now-step-marker`
|
|
31
|
+
timeline component built for the Current Mission panel (`renderGitVisualTree` in
|
|
32
|
+
`dashboard.html`, parsing `SNAPSHOT.git.graph`). It is single-lane by design — the markdown
|
|
33
|
+
block keeps the true branch-lane geometry; the stepper trades that for an at-a-glance
|
|
34
|
+
promotion read (HEAD, promoted to `origin/main`/`origin/staging`, or neither).
|
|
35
|
+
|
|
36
|
+
The DevOps tab (`#devops`) is scoped to CI/CD + deploy signal, separate from the
|
|
37
|
+
local-process-only Processes tab. `dashboard-data.mjs`'s `collectPipelineRuns()` shells to
|
|
38
|
+
`gh run list` (budget-guarded via `withinSnapshotBudget()`, fails soft to an honest
|
|
39
|
+
empty-state when `gh` is unavailable/unauthenticated); `collectDeploySignal()` reads `v*`
|
|
40
|
+
git tags plus the latest non-`Unreleased` `CHANGELOG.md` entry as a best-effort "what
|
|
41
|
+
shipped recently" proxy. Neither collector polls live infra/hosting — the DevOps tab never
|
|
42
|
+
implies monitoring it does not perform.
|
|
@@ -49,13 +49,33 @@ const MAX_TERMINALS = 20;
|
|
|
49
49
|
const MAX_PROCESSES = 25;
|
|
50
50
|
const MAX_GIT_GRAPH_LINES = 25;
|
|
51
51
|
const MAX_GIT_GRAPH_LINE_CHARS = 160;
|
|
52
|
+
const MAX_PIPELINE_RUNS = 5;
|
|
53
|
+
const MAX_PIPELINE_NAME_CHARS = 80;
|
|
54
|
+
// Live-measured (2026-08-24, this repo, 8 fresh `gh run list` samples via the
|
|
55
|
+
// exact trimmed command): 3476-6584ms, one earlier ad-hoc sample 7674ms.
|
|
56
|
+
// Prior review sessions (.cursor/memory/plan-monitor-mc-git-tab-visual-tree-and-devops-panel.md)
|
|
57
|
+
// spanned 2.1-9.9s. 3500ms had no headroom at all over that range; 12000ms
|
|
58
|
+
// gives real margin over the observed worst case (~2.1s over the historical
|
|
59
|
+
// 9.9s max, ~4.3s over this session's tight-sample max) while staying well
|
|
60
|
+
// under the 60s outer child-process timeout (serve.mjs DATA_SCRIPT_TIMEOUT_MS).
|
|
61
|
+
const GH_RUN_LIST_TIMEOUT_MS = 12_000;
|
|
62
|
+
const MAX_DEPLOY_TAGS = 5;
|
|
63
|
+
const MAX_DEPLOY_TAG_NAME_CHARS = 64;
|
|
64
|
+
const MAX_CHANGELOG_ITEMS = 4;
|
|
65
|
+
const MAX_CHANGELOG_ITEM_CHARS = 140;
|
|
52
66
|
|
|
53
67
|
/** Soft wall-clock budget for optional collectors (transcripts, reports, ps). */
|
|
54
68
|
const SNAPSHOT_STARTED_MS = Date.now();
|
|
69
|
+
// Default raised from 12000ms by the same delta GH_RUN_LIST_TIMEOUT_MS grew
|
|
70
|
+
// (3500ms -> 12000ms, +8500ms): collectPipelineRuns() now runs last among the
|
|
71
|
+
// budget-guarded collectors and reserves GH_RUN_LIST_TIMEOUT_MS + 400ms of
|
|
72
|
+
// budget before attempting `gh run list` (see withinSnapshotBudget call
|
|
73
|
+
// there), so the total budget must grow by the same amount the reserve did
|
|
74
|
+
// or the larger timeout can never actually be used.
|
|
55
75
|
const SNAPSHOT_BUDGET_MS = (() => {
|
|
56
76
|
const raw = process.env.AGENT_KIT_DASHBOARD_DATA_BUDGET_MS;
|
|
57
|
-
const n = raw != null && raw !== "" ? Number(raw) :
|
|
58
|
-
return Number.isFinite(n) && n > 0 ? n :
|
|
77
|
+
const n = raw != null && raw !== "" ? Number(raw) : 20_500;
|
|
78
|
+
return Number.isFinite(n) && n > 0 ? n : 20_500;
|
|
59
79
|
})();
|
|
60
80
|
function withinSnapshotBudget(reserveMs = 400) {
|
|
61
81
|
return Date.now() - SNAPSHOT_STARTED_MS + reserveMs < SNAPSHOT_BUDGET_MS;
|
|
@@ -104,7 +124,7 @@ function redactTerminalOutput(text) {
|
|
|
104
124
|
|
|
105
125
|
const SNAPSHOT = {
|
|
106
126
|
_schema: {
|
|
107
|
-
version: "1.
|
|
127
|
+
version: "1.3.0",
|
|
108
128
|
description: "Mission Control dashboard data model",
|
|
109
129
|
fields: {
|
|
110
130
|
generatedAt: "ISO-8601 timestamp of snapshot generation",
|
|
@@ -117,6 +137,8 @@ const SNAPSHOT = {
|
|
|
117
137
|
memory:
|
|
118
138
|
"Memory records: error count, decision count, recent decisions, recent parsed errors, error-o-meter stats",
|
|
119
139
|
git: "Git repository state: branch, dirty status, commit, ahead/behind, bounded files[], promotion flow vs staging/main, graph lines, staging hygiene",
|
|
140
|
+
devops:
|
|
141
|
+
"Best-effort DevOps signal: pipeline ({available, runs[], reason?} recent gh run list rows; reason is 'budget' when the shared snapshot budget ran out before gh was attempted, or 'call-failed' when gh was attempted and missing/unauthenticated/timed out/errored; reason is only present when available is false) and deploy ({tags[], changelog} v* git tags + latest CHANGELOG release entry as a 'what shipped recently' proxy, not a live infra poll)",
|
|
120
142
|
terminals:
|
|
121
143
|
"Active Cursor terminal sessions with metadata, output line count, and capped lastOutput",
|
|
122
144
|
processes:
|
|
@@ -127,7 +149,7 @@ const SNAPSHOT = {
|
|
|
127
149
|
},
|
|
128
150
|
},
|
|
129
151
|
generatedAt: new Date().toISOString(),
|
|
130
|
-
dashboardDataVersion: "1.
|
|
152
|
+
dashboardDataVersion: "1.4.0",
|
|
131
153
|
plans: [],
|
|
132
154
|
system: {
|
|
133
155
|
repoRoot: ROOT,
|
|
@@ -246,6 +268,7 @@ const kitCommandPaths = new Set();
|
|
|
246
268
|
const kitSkillDirs = new Set();
|
|
247
269
|
const kitAgentPaths = new Set();
|
|
248
270
|
const registryFile = join(ROOT, "registry", "registry.json");
|
|
271
|
+
let registryHadCommands = false;
|
|
249
272
|
if (existsSync(registryFile)) {
|
|
250
273
|
try {
|
|
251
274
|
const registry = JSON.parse(readFileSync(registryFile, "utf8"));
|
|
@@ -265,10 +288,58 @@ if (existsSync(registryFile)) {
|
|
|
265
288
|
kitAgentPaths.add(entry.path);
|
|
266
289
|
}
|
|
267
290
|
}
|
|
291
|
+
registryHadCommands = kitCommandPaths.size > 0;
|
|
268
292
|
} catch {
|
|
269
293
|
// Unreadable registry: degrade to all-editable rather than locking everything.
|
|
270
294
|
}
|
|
271
295
|
}
|
|
296
|
+
// `registry/registry.json` is a factory-only artifact index — consumer installs
|
|
297
|
+
// never ship it, so the block above always leaves kitCommandPaths empty there and
|
|
298
|
+
// every command reads kitManaged: false even though `update` owns it. Fall back to
|
|
299
|
+
// the locally-tracked kit markers: `.cursor/agent-kit.managed-hashes.json` (already
|
|
300
|
+
// keyed by the same repo-relative path as c.path / a.path) and, as a secondary
|
|
301
|
+
// source, `.cursor/agent-kit.json`'s `protected[]` list (exact paths only; glob
|
|
302
|
+
// entries like `.cursor/plans/**` don't identify individual kit-managed commands so
|
|
303
|
+
// they're skipped). Only engages when the registry is absent or yielded no commands,
|
|
304
|
+
// so factory behavior (registry present + populated) is unchanged.
|
|
305
|
+
if (!registryHadCommands) {
|
|
306
|
+
const managedHashesFile = join(ROOT, ".cursor", "agent-kit.managed-hashes.json");
|
|
307
|
+
const agentKitConfigFile = join(ROOT, ".cursor", "agent-kit.json");
|
|
308
|
+
const fallbackPaths = new Set();
|
|
309
|
+
if (existsSync(managedHashesFile)) {
|
|
310
|
+
try {
|
|
311
|
+
const managed = JSON.parse(readFileSync(managedHashesFile, "utf8"));
|
|
312
|
+
const hashes = managed?.hashes && typeof managed.hashes === "object" ? managed.hashes : {};
|
|
313
|
+
for (const path of Object.keys(hashes)) {
|
|
314
|
+
fallbackPaths.add(path);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// Unreadable managed-hashes.json: fall through to protected[] (if any).
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (existsSync(agentKitConfigFile)) {
|
|
321
|
+
try {
|
|
322
|
+
const agentKitConfig = JSON.parse(readFileSync(agentKitConfigFile, "utf8"));
|
|
323
|
+
const protectedList = Array.isArray(agentKitConfig?.protected)
|
|
324
|
+
? agentKitConfig.protected
|
|
325
|
+
: [];
|
|
326
|
+
for (const entry of protectedList) {
|
|
327
|
+
if (typeof entry === "string" && !entry.includes("*")) fallbackPaths.add(entry);
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// Unreadable agent-kit.json: managed-hashes.json (if any) still applies.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const path of fallbackPaths) {
|
|
334
|
+
if (path.startsWith(".cursor/commands/")) {
|
|
335
|
+
kitCommandPaths.add(path);
|
|
336
|
+
} else if (path.startsWith(".cursor/agents/")) {
|
|
337
|
+
kitAgentPaths.add(path);
|
|
338
|
+
} else if (path.startsWith(".cursor/skills/") && path.endsWith("/SKILL.md")) {
|
|
339
|
+
kitSkillDirs.add(path.slice(0, -"/SKILL.md".length));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
272
343
|
for (const c of SNAPSHOT.commands) {
|
|
273
344
|
c.kitManaged = kitCommandPaths.has(c.path);
|
|
274
345
|
}
|
|
@@ -402,7 +473,12 @@ if (existsSync(memoryDecisionsDir)) {
|
|
|
402
473
|
try {
|
|
403
474
|
const gitOpts = { cwd: ROOT, encoding: "utf-8", timeout: 5000 };
|
|
404
475
|
const branch = execSync("git rev-parse --abbrev-ref HEAD", gitOpts).trim();
|
|
405
|
-
|
|
476
|
+
// Only strip the trailing newline(s) here — `git status --short` uses
|
|
477
|
+
// positional XY status columns, so an unstaged-only row starts with a
|
|
478
|
+
// literal leading space (e.g. " M path"). A full-string .trim() eats that
|
|
479
|
+
// leading space on line 1 only, shifting parseGitStatusShort's staged /
|
|
480
|
+
// unstaged read for the first row.
|
|
481
|
+
const status = execSync("git status --short", gitOpts).replace(/\n+$/, "");
|
|
406
482
|
const lastCommit = execSync("git log -1 --oneline", gitOpts).trim();
|
|
407
483
|
let ahead = 0;
|
|
408
484
|
let behind = 0;
|
|
@@ -487,6 +563,128 @@ try {
|
|
|
487
563
|
SNAPSHOT._gitRecentLog = [];
|
|
488
564
|
}
|
|
489
565
|
|
|
566
|
+
// 6b. DevOps: CI/CD pipeline status (best-effort, `gh` CLI) + a "what shipped
|
|
567
|
+
// recently" deploy-activity proxy from v* git tags and the latest CHANGELOG
|
|
568
|
+
// release entry. Both fail soft to an empty/unavailable shape; this snapshot
|
|
569
|
+
// never blocks or errors on either signal, and never claims live
|
|
570
|
+
// infra/hosting monitoring it does not perform.
|
|
571
|
+
function collectPipelineRuns() {
|
|
572
|
+
// gh run list is a network call, unlike the local-only git collectors
|
|
573
|
+
// above; the budget reserve must cover its own timeout, not just a nominal
|
|
574
|
+
// buffer (see .cursor/memory/errors/2026-07-26_mission-control-dashboard-data-timeout.md
|
|
575
|
+
// for why this file's soft-budget guards exist).
|
|
576
|
+
if (!withinSnapshotBudget(GH_RUN_LIST_TIMEOUT_MS + 400)) {
|
|
577
|
+
// `gh` was never even attempted this snapshot — the shared budget ran out
|
|
578
|
+
// first. Distinguished from a failed/timed-out call below so the UI does
|
|
579
|
+
// not misattribute this (the dominant real case; see live measurements
|
|
580
|
+
// in .cursor/memory/plan-monitor-mc-git-tab-visual-tree-and-devops-panel.md
|
|
581
|
+
// and the residual note this fix landed from) to an auth/install problem.
|
|
582
|
+
return { available: false, runs: [], reason: "budget" };
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
// Trimmed to the fields renderDevopsPipelineCard() / navDevopsDot actually
|
|
586
|
+
// read (workflow, status, conclusion, branch, event, createdAt). databaseId
|
|
587
|
+
// and url are dropped: the pipeline card is display-only with no
|
|
588
|
+
// paste-destination for a run URL (ADR 2026-07-25 copy-only convention),
|
|
589
|
+
// so requesting them only adds payload cost without a consumer.
|
|
590
|
+
const out = execSync(
|
|
591
|
+
`gh run list --limit ${MAX_PIPELINE_RUNS} --json workflowName,status,conclusion,headBranch,event,createdAt`,
|
|
592
|
+
{ cwd: ROOT, encoding: "utf-8", timeout: GH_RUN_LIST_TIMEOUT_MS },
|
|
593
|
+
);
|
|
594
|
+
const rows = JSON.parse(out);
|
|
595
|
+
if (!Array.isArray(rows)) return { available: false, runs: [], reason: "call-failed" };
|
|
596
|
+
const runs = rows.slice(0, MAX_PIPELINE_RUNS).map((r) => ({
|
|
597
|
+
workflow: truncateStr(String(r.workflowName || ""), MAX_PIPELINE_NAME_CHARS),
|
|
598
|
+
status: String(r.status || ""),
|
|
599
|
+
conclusion: r.conclusion ? String(r.conclusion) : null,
|
|
600
|
+
branch: truncateStr(String(r.headBranch || ""), MAX_STRING.branch),
|
|
601
|
+
event: String(r.event || ""),
|
|
602
|
+
createdAt: r.createdAt || null,
|
|
603
|
+
}));
|
|
604
|
+
return { available: true, runs };
|
|
605
|
+
} catch {
|
|
606
|
+
// gh missing, unauthenticated, its own execSync timeout, or non-repo cwd
|
|
607
|
+
// all read the same to the caller: no pipeline signal for this snapshot.
|
|
608
|
+
// Not further split (e.g. timeout vs auth) — that would need parsing the
|
|
609
|
+
// execSync error shape, which is more taxonomy than the empty-state copy
|
|
610
|
+
// needs; "call-failed" is enough to stop the UI from guessing "auth".
|
|
611
|
+
return { available: false, runs: [], reason: "call-failed" };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Markdown decoration removed, not converted (mirrors scripts/build-landing.mjs's
|
|
616
|
+
* stripMdInline): safe against unclosed tokens after the item-length cap below. */
|
|
617
|
+
function stripMdInlineForDeploySignal(s) {
|
|
618
|
+
return s
|
|
619
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
620
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
621
|
+
.replace(/`([^`]+)`/g, "$1");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** First non-"Unreleased" `## [x.y.z] - date` entry and a capped bullet list. */
|
|
625
|
+
function parseLatestChangelogEntryForDeploySignal(changelog) {
|
|
626
|
+
const headerRe = /^## \[([^\]]+)\](?:\s*-\s*(.+))?$/gm;
|
|
627
|
+
const headers = [...changelog.matchAll(headerRe)];
|
|
628
|
+
const latest = headers.find((m) => m[1].toLowerCase() !== "unreleased");
|
|
629
|
+
if (!latest) return null;
|
|
630
|
+
const start = latest.index + latest[0].length;
|
|
631
|
+
const next = headers.find((m) => m.index > latest.index);
|
|
632
|
+
const body = changelog.slice(start, next ? next.index : undefined);
|
|
633
|
+
const items = [...body.matchAll(/^- (.+)$/gm)]
|
|
634
|
+
.map((m) => stripMdInlineForDeploySignal(m[1].trim()))
|
|
635
|
+
.filter(Boolean)
|
|
636
|
+
.slice(0, MAX_CHANGELOG_ITEMS)
|
|
637
|
+
.map((line) =>
|
|
638
|
+
line.length > MAX_CHANGELOG_ITEM_CHARS
|
|
639
|
+
? `${line.slice(0, MAX_CHANGELOG_ITEM_CHARS).trimEnd()}…`
|
|
640
|
+
: line,
|
|
641
|
+
);
|
|
642
|
+
return { version: latest[1], date: (latest[2] || "").trim() || null, items };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function collectDeploySignal() {
|
|
646
|
+
if (!withinSnapshotBudget(400)) return { tags: [], changelog: null };
|
|
647
|
+
const gitOpts = { cwd: ROOT, encoding: "utf-8", timeout: 5000 };
|
|
648
|
+
let tags = [];
|
|
649
|
+
try {
|
|
650
|
+
// v*-only: an archive/* or other non-release tag under "what shipped
|
|
651
|
+
// recently" would be exactly the misleading signal this phase avoids.
|
|
652
|
+
const out = execSync(
|
|
653
|
+
`git for-each-ref --sort=-creatordate --format='%(refname:short)|%(creatordate:short)' --count=${MAX_DEPLOY_TAGS} 'refs/tags/v*'`,
|
|
654
|
+
gitOpts,
|
|
655
|
+
).trim();
|
|
656
|
+
tags = out
|
|
657
|
+
? out
|
|
658
|
+
.split("\n")
|
|
659
|
+
.filter(Boolean)
|
|
660
|
+
.map((line) => {
|
|
661
|
+
const [name, date] = line.split("|");
|
|
662
|
+
return { name: truncateStr(name || "", MAX_DEPLOY_TAG_NAME_CHARS), date: date || null };
|
|
663
|
+
})
|
|
664
|
+
: [];
|
|
665
|
+
} catch {
|
|
666
|
+
tags = [];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let changelog = null;
|
|
670
|
+
try {
|
|
671
|
+
const changelogPath = join(ROOT, "CHANGELOG.md");
|
|
672
|
+
if (existsSync(changelogPath)) {
|
|
673
|
+
changelog = parseLatestChangelogEntryForDeploySignal(readFileSync(changelogPath, "utf-8"));
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
changelog = null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return { tags, changelog };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// SNAPSHOT.devops is assigned near the end of the snapshot build (after the
|
|
683
|
+
// transcripts/reports/ps collectors below), not here — gh run list is a
|
|
684
|
+
// network call that must not run ahead of and starve the local-only
|
|
685
|
+
// budget-guarded collectors that share SNAPSHOT_BUDGET_MS. See the call site
|
|
686
|
+
// right before the missionControl assembly for the ordering rationale.
|
|
687
|
+
|
|
490
688
|
// 7. Terminals (read from Cursor terminal files)
|
|
491
689
|
const terminalsDir = resolve(process.env.HOME || "~", ".cursor", "projects");
|
|
492
690
|
// Derive project path from ROOT rather than hardcoding a specific slug
|
|
@@ -1142,4 +1340,16 @@ function readPreviousInventory() {
|
|
|
1142
1340
|
}
|
|
1143
1341
|
SNAPSHOT._gitRecentLog = undefined;
|
|
1144
1342
|
|
|
1343
|
+
// DevOps: CI/CD pipeline status (best-effort, `gh` CLI) + a "what shipped
|
|
1344
|
+
// recently" deploy-activity proxy. Deliberately collected last, after every
|
|
1345
|
+
// other budget-guarded collector above (processes, detached-audit-sessions,
|
|
1346
|
+
// agentPrompts, externalReports, subagentRuns): collectPipelineRuns() makes
|
|
1347
|
+
// a network call (gh run list) unlike every other collector in this file, so
|
|
1348
|
+
// it must not run ahead of and eat into the shared SNAPSHOT_BUDGET_MS budget
|
|
1349
|
+
// that the local-only collectors also need.
|
|
1350
|
+
SNAPSHOT.devops = {
|
|
1351
|
+
pipeline: collectPipelineRuns(),
|
|
1352
|
+
deploy: collectDeploySignal(),
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1145
1355
|
process.stdout.write(JSON.stringify(SNAPSHOT, null, 2));
|
package/dashboard/dashboard.html
CHANGED
|
@@ -1173,6 +1173,45 @@ body.mc-fullscreen .top-tabs-row {
|
|
|
1173
1173
|
color: var(--text-secondary);
|
|
1174
1174
|
margin: 12px 0 6px;
|
|
1175
1175
|
}
|
|
1176
|
+
.git-graph-title:first-child { margin-top: 0; }
|
|
1177
|
+
|
|
1178
|
+
/* ===== DevOps ===== */
|
|
1179
|
+
/* Row visual language mirrors .git-flow-row (state dot + lane + meta) so the
|
|
1180
|
+
* new tab reads as the same product, not a second component vocabulary. */
|
|
1181
|
+
.devops-row {
|
|
1182
|
+
display: flex;
|
|
1183
|
+
align-items: center;
|
|
1184
|
+
gap: 10px;
|
|
1185
|
+
font-size: 12px;
|
|
1186
|
+
padding: 8px 10px;
|
|
1187
|
+
border: 1px solid var(--border);
|
|
1188
|
+
border-radius: 8px;
|
|
1189
|
+
background: var(--bg-card);
|
|
1190
|
+
flex-wrap: wrap;
|
|
1191
|
+
margin-bottom: 6px;
|
|
1192
|
+
}
|
|
1193
|
+
.devops-row:last-child { margin-bottom: 0; }
|
|
1194
|
+
.devops-name {
|
|
1195
|
+
font-weight: 600;
|
|
1196
|
+
color: var(--text-primary);
|
|
1197
|
+
flex-shrink: 0;
|
|
1198
|
+
}
|
|
1199
|
+
.devops-meta {
|
|
1200
|
+
color: var(--text-secondary);
|
|
1201
|
+
flex: 1;
|
|
1202
|
+
min-width: 0;
|
|
1203
|
+
}
|
|
1204
|
+
.devops-time {
|
|
1205
|
+
color: var(--text-muted);
|
|
1206
|
+
font-family: var(--mc-font-mono);
|
|
1207
|
+
font-size: 11px;
|
|
1208
|
+
flex-shrink: 0;
|
|
1209
|
+
}
|
|
1210
|
+
.devops-note {
|
|
1211
|
+
font-size: 11px;
|
|
1212
|
+
color: var(--text-muted);
|
|
1213
|
+
margin: 0 0 8px;
|
|
1214
|
+
}
|
|
1176
1215
|
.git-hygiene {
|
|
1177
1216
|
display: flex;
|
|
1178
1217
|
align-items: flex-start;
|
|
@@ -3644,6 +3683,10 @@ html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monit
|
|
|
3644
3683
|
Git
|
|
3645
3684
|
<span class="dot dot-gray" id="navGitDot" aria-hidden="true"></span>
|
|
3646
3685
|
</a>
|
|
3686
|
+
<a class="top-tab nav-more-item" role="menuitem" href="#" data-section="devops" tabindex="0" onclick="return showSection('devops')" aria-label="DevOps section">
|
|
3687
|
+
DevOps
|
|
3688
|
+
<span class="dot dot-gray" id="navDevopsDot" aria-hidden="true"></span>
|
|
3689
|
+
</a>
|
|
3647
3690
|
<a class="top-tab nav-more-item" role="menuitem" href="#" data-section="memory" tabindex="0" onclick="return showSection('memory')" aria-label="Memory section">
|
|
3648
3691
|
Memory
|
|
3649
3692
|
</a>
|
|
@@ -3819,6 +3862,7 @@ const SECTION_IDS = [
|
|
|
3819
3862
|
'commands',
|
|
3820
3863
|
'health',
|
|
3821
3864
|
'git',
|
|
3865
|
+
'devops',
|
|
3822
3866
|
'memory',
|
|
3823
3867
|
'terminals',
|
|
3824
3868
|
'processes',
|
|
@@ -5430,6 +5474,199 @@ function renderGitGraphCard(git) {
|
|
|
5430
5474
|
`;
|
|
5431
5475
|
}
|
|
5432
5476
|
|
|
5477
|
+
/** Cap for the visual tree row count (mirrors MAX_GIT_GRAPH_LINES server-side cap, further
|
|
5478
|
+
* bounded here so the stepper stays a glance, not a scroll well). */
|
|
5479
|
+
const MAX_GIT_VISUAL_TREE_ROWS = 12;
|
|
5480
|
+
|
|
5481
|
+
/**
|
|
5482
|
+
* Parse `git log --graph --oneline --decorate` lines into flattened commit
|
|
5483
|
+
* entries. Connector-only lines (`|`, `/`, `\`, whitespace between lanes)
|
|
5484
|
+
* carry no commit and are skipped — see the plan's "Phase 0 lane-flattening
|
|
5485
|
+
* interpretation" note: this trades true branch-lane geometry (still fully
|
|
5486
|
+
* present in the kept markdown block) for one row per commit.
|
|
5487
|
+
*/
|
|
5488
|
+
function parseGitGraphCommits(lines) {
|
|
5489
|
+
const commits = [];
|
|
5490
|
+
for (const line of Array.isArray(lines) ? lines : []) {
|
|
5491
|
+
const m = /\*\s*([0-9a-f]{4,40})\s+(?:\(([^)]*)\)\s*)?(.*)$/.exec(line);
|
|
5492
|
+
if (!m) continue;
|
|
5493
|
+
const decoration = (m[2] || '').trim();
|
|
5494
|
+
commits.push({
|
|
5495
|
+
hash: m[1],
|
|
5496
|
+
decoration,
|
|
5497
|
+
subject: (m[3] || '').trim(),
|
|
5498
|
+
isHead: /\bHEAD\b/.test(decoration),
|
|
5499
|
+
isPromoted: /\borigin\/(main|staging)\b/.test(decoration),
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
return commits;
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
/**
|
|
5506
|
+
* Visual companion to the markdown git-graph block: reuses the exact
|
|
5507
|
+
* .now-stepper/.now-step/.now-step-marker timeline primitive (state-colored
|
|
5508
|
+
* markers + connecting line) with one row per parsed commit. Dot/marker
|
|
5509
|
+
* semantics stay locked to the existing Now-panel vocabulary: current/blue =
|
|
5510
|
+
* HEAD, done/green = already promoted to origin/main or origin/staging,
|
|
5511
|
+
* neutral = neither. Degrades to a compact empty-state when the graph has no
|
|
5512
|
+
* parseable commit line (e.g. a shallow clone) while a branch still exists.
|
|
5513
|
+
*/
|
|
5514
|
+
function renderGitVisualTree(git) {
|
|
5515
|
+
const commits = parseGitGraphCommits(git?.graph);
|
|
5516
|
+
if (!commits.length) {
|
|
5517
|
+
if (!git?.branch) return '';
|
|
5518
|
+
return renderEmptyStateCta({
|
|
5519
|
+
headline: 'No visual tree yet',
|
|
5520
|
+
support: 'The commit graph had no parseable commit line to render.',
|
|
5521
|
+
compact: true,
|
|
5522
|
+
className: 'git-visual-tree-empty',
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
const shown = commits.slice(0, MAX_GIT_VISUAL_TREE_ROWS);
|
|
5526
|
+
const rows = shown
|
|
5527
|
+
.map((c) => {
|
|
5528
|
+
const stateClass = c.isHead ? ' now-step-current' : c.isPromoted ? ' now-step-done' : '';
|
|
5529
|
+
const marker = c.isHead ? '●' : c.isPromoted ? '✓' : '○';
|
|
5530
|
+
const label = c.decoration ? escapeHtml(c.decoration) : 'Commit';
|
|
5531
|
+
const text = `<span class="now-todo-id">${escapeHtml(c.hash)}</span>${c.subject ? ` — ${escapeHtml(c.subject)}` : ''}`;
|
|
5532
|
+
return `
|
|
5533
|
+
<li class="now-step${stateClass}">
|
|
5534
|
+
<span class="now-step-marker" aria-hidden="true">${marker}</span>
|
|
5535
|
+
<div class="now-step-body">
|
|
5536
|
+
<span class="now-step-label">${label}</span>
|
|
5537
|
+
<div class="now-step-text">${text}</div>
|
|
5538
|
+
</div>
|
|
5539
|
+
</li>
|
|
5540
|
+
`;
|
|
5541
|
+
})
|
|
5542
|
+
.join('');
|
|
5543
|
+
return `
|
|
5544
|
+
<div class="git-graph-title">Visual tree (${shown.length} of ${commits.length} commit${commits.length !== 1 ? 's' : ''})</div>
|
|
5545
|
+
<ol class="now-stepper git-visual-tree" aria-label="Visual commit tree: state-colored markers by promotion status">
|
|
5546
|
+
${rows}
|
|
5547
|
+
</ol>
|
|
5548
|
+
`;
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
/** `in_progress` / `timed_out` -> `In progress` / `Timed out`. */
|
|
5552
|
+
function humanizeRunStatus(s) {
|
|
5553
|
+
const raw = String(s || '').trim();
|
|
5554
|
+
if (!raw) return 'Unknown';
|
|
5555
|
+
return raw
|
|
5556
|
+
.split('_')
|
|
5557
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
|
|
5558
|
+
.join(' ');
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5561
|
+
/** Dot tone for one `gh run list` row: in-flight states read yellow; a
|
|
5562
|
+
* completed run reads by conclusion (green/red/yellow), never decorative. */
|
|
5563
|
+
function pipelineRunTone(run) {
|
|
5564
|
+
if (run.status !== 'completed') return 'yellow';
|
|
5565
|
+
if (run.conclusion === 'success') return 'green';
|
|
5566
|
+
if (['failure', 'cancelled', 'timed_out'].includes(run.conclusion)) return 'red';
|
|
5567
|
+
return 'yellow';
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5570
|
+
/**
|
|
5571
|
+
* CI/CD pipeline card (Phase 2): recent `gh run list` rows, honest empty-state
|
|
5572
|
+
* when the collector didn't produce runs. Display only, no CTA — a
|
|
5573
|
+
* workflow-run URL has no named paste destination in the copy-only
|
|
5574
|
+
* convention (ADR 2026-07-25), so this card ships without one rather than
|
|
5575
|
+
* inventing a destination.
|
|
5576
|
+
*
|
|
5577
|
+
* Empty-state copy is keyed off `pipeline.reason` rather than one blanket
|
|
5578
|
+
* message: live measurement (2026-08-24, mc-git-tab-devops-pipeline-timeout
|
|
5579
|
+
* residuals) found the dominant real case is the shared snapshot budget
|
|
5580
|
+
* running out before `gh` is even attempted ('budget'), not gh being
|
|
5581
|
+
* missing/unauthenticated — the old unconditional "gh is unavailable or not
|
|
5582
|
+
* authenticated" copy misattributed that case and pointed the operator at
|
|
5583
|
+
* the wrong fix.
|
|
5584
|
+
*/
|
|
5585
|
+
function renderDevopsPipelineCard(devops) {
|
|
5586
|
+
const pipeline = devops?.pipeline || null;
|
|
5587
|
+
if (!pipeline?.available) {
|
|
5588
|
+
const support = pipeline?.reason === 'budget'
|
|
5589
|
+
? 'This snapshot ran out of time before checking recent workflow runs — refresh to retry.'
|
|
5590
|
+
: 'Recent workflow runs could not be read this snapshot (gh may be missing, unauthenticated, or the call timed out).';
|
|
5591
|
+
return renderEmptyStateCta({
|
|
5592
|
+
headline: 'No pipeline signal',
|
|
5593
|
+
support,
|
|
5594
|
+
compact: true,
|
|
5595
|
+
className: 'devops-pipeline-empty',
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
5598
|
+
const runs = pipeline.runs || [];
|
|
5599
|
+
if (!runs.length) {
|
|
5600
|
+
return renderEmptyStateCta({
|
|
5601
|
+
headline: 'No recent runs',
|
|
5602
|
+
support: 'No GitHub Actions workflow runs found for this repository.',
|
|
5603
|
+
compact: true,
|
|
5604
|
+
className: 'devops-pipeline-empty',
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
const rows = runs
|
|
5608
|
+
.map((r) => {
|
|
5609
|
+
const tone = pipelineRunTone(r);
|
|
5610
|
+
const label = humanizeRunStatus(r.status === 'completed' ? r.conclusion || 'unknown' : r.status);
|
|
5611
|
+
return `
|
|
5612
|
+
<div class="devops-row">
|
|
5613
|
+
<span class="dot dot-${tone}" aria-hidden="true"></span>
|
|
5614
|
+
<span class="devops-name">${escapeHtml(r.workflow || 'Workflow')}</span>
|
|
5615
|
+
<span class="devops-meta">${escapeHtml(label)} · ${escapeHtml(r.branch || '—')} · ${escapeHtml(r.event || '—')}</span>
|
|
5616
|
+
<span class="devops-time">${escapeHtml(fmtDate(r.createdAt))}</span>
|
|
5617
|
+
</div>
|
|
5618
|
+
`;
|
|
5619
|
+
})
|
|
5620
|
+
.join('');
|
|
5621
|
+
return rows;
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5624
|
+
/**
|
|
5625
|
+
* Deploy-activity card (Phase 3, best-effort, honest): a "what shipped
|
|
5626
|
+
* recently" proxy from `v*` git tags and the latest non-Unreleased CHANGELOG
|
|
5627
|
+
* entry. Deliberately not a live infra/hosting poll — no fabricated service
|
|
5628
|
+
* tiles for sources this dashboard does not actually reach.
|
|
5629
|
+
*/
|
|
5630
|
+
function renderDevopsDeployCard(devops) {
|
|
5631
|
+
const deploy = devops?.deploy || null;
|
|
5632
|
+
const tags = deploy?.tags || [];
|
|
5633
|
+
const changelog = deploy?.changelog || null;
|
|
5634
|
+
if (!tags.length && !changelog) {
|
|
5635
|
+
return renderEmptyStateCta({
|
|
5636
|
+
headline: 'No deploy signal yet',
|
|
5637
|
+
support: 'No git release tags (v*) or a parseable CHANGELOG release entry were found. This section reflects what shipped, from tags and CHANGELOG only — it does not poll live infra or hosting.',
|
|
5638
|
+
compact: true,
|
|
5639
|
+
className: 'devops-deploy-empty',
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
const changelogRow = changelog
|
|
5643
|
+
? `
|
|
5644
|
+
<div class="devops-row">
|
|
5645
|
+
<span class="dot dot-green" aria-hidden="true"></span>
|
|
5646
|
+
<span class="devops-name">CHANGELOG ${escapeHtml(changelog.version)}</span>
|
|
5647
|
+
<span class="devops-meta">${escapeHtml((changelog.items || []).join(' · ') || 'Release notes')}</span>
|
|
5648
|
+
<span class="devops-time">${escapeHtml(changelog.date || '—')}</span>
|
|
5649
|
+
</div>
|
|
5650
|
+
`
|
|
5651
|
+
: '';
|
|
5652
|
+
const tagRows = tags
|
|
5653
|
+
.map(
|
|
5654
|
+
(t) => `
|
|
5655
|
+
<div class="devops-row">
|
|
5656
|
+
<span class="dot dot-gray" aria-hidden="true"></span>
|
|
5657
|
+
<span class="devops-name">${escapeHtml(t.name)}</span>
|
|
5658
|
+
<span class="devops-meta">git tag</span>
|
|
5659
|
+
<span class="devops-time">${escapeHtml(t.date || '—')}</span>
|
|
5660
|
+
</div>
|
|
5661
|
+
`,
|
|
5662
|
+
)
|
|
5663
|
+
.join('');
|
|
5664
|
+
return `
|
|
5665
|
+
<div class="devops-note">Best-effort "what shipped recently" from git tags + CHANGELOG — not a live infra/deploy poll.</div>
|
|
5666
|
+
${changelogRow}${tagRows}
|
|
5667
|
+
`;
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5433
5670
|
/** Staging hygiene: untracked plan-monitor WIP (add-by-name only; ADR 2026-07-29 R14). */
|
|
5434
5671
|
function renderGitHygieneHint(git) {
|
|
5435
5672
|
const wip = git?.hygiene?.monitorWip || [];
|
|
@@ -6788,7 +7025,7 @@ function nowMetaIconSvg(kind, opts) {
|
|
|
6788
7025
|
* features >= 1 unit. Detail that cannot meet the floor is drawn filled
|
|
6789
7026
|
* (fill="currentColor" stroke="none"), never as a sub-stroke stroked shape.
|
|
6790
7027
|
* Static path markup only; never concatenate untrusted text into the SVG.
|
|
6791
|
-
* @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'memory'|'terminals'|'processes'|'config'} kind
|
|
7028
|
+
* @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'processes'|'config'} kind
|
|
6792
7029
|
* @param {{ decorative?: boolean }} [opts] decorative true (default): aria-hidden next to a visible label.
|
|
6793
7030
|
* decorative false: role=img + aria-label + title for icon-only controls (e.g. more-sections).
|
|
6794
7031
|
*/
|
|
@@ -6810,6 +7047,7 @@ function spaceIconSvg(kind, opts) {
|
|
|
6810
7047
|
commands: 'Commands',
|
|
6811
7048
|
health: 'Health',
|
|
6812
7049
|
git: 'Git',
|
|
7050
|
+
devops: 'DevOps',
|
|
6813
7051
|
memory: 'Memory',
|
|
6814
7052
|
terminals: 'Terminals',
|
|
6815
7053
|
processes: 'Processes',
|
|
@@ -6877,6 +7115,12 @@ function spaceIconSvg(kind, opts) {
|
|
|
6877
7115
|
'<path d="M10.5 4.5v7a2 2 0 01-2 2h-2"/>' +
|
|
6878
7116
|
'<path d="M6.5 11.5a2 2 0 100-4 2 2 0 000 4z"/>' +
|
|
6879
7117
|
'<path d="M10.5 6.5v2"/>',
|
|
7118
|
+
// Linked nodes (devops) — two pipeline-stage circles joined by a short
|
|
7119
|
+
// link; distinct from the Git branch glyph and from the Skills gear.
|
|
7120
|
+
devops:
|
|
7121
|
+
'<circle cx="5" cy="8" r="2.25"/>' +
|
|
7122
|
+
'<circle cx="11" cy="8" r="2.25"/>' +
|
|
7123
|
+
'<path d="M7.25 8h1.5"/>',
|
|
6880
7124
|
// Chip (memory) — two internal lines 3 units apart; a third line would drop clearance below the stroke floor
|
|
6881
7125
|
memory:
|
|
6882
7126
|
'<path d="M4.5 2.5h7a1 1 0 011 1v9a1 1 0 01-1 1h-7a1 1 0 01-1-1v-9a1 1 0 011-1z"/>' +
|
|
@@ -7779,6 +8023,24 @@ function renderUnsafe() {
|
|
|
7779
8023
|
gitDot.className = 'dot dot-green';
|
|
7780
8024
|
}
|
|
7781
8025
|
|
|
8026
|
+
// DevOps nav dot: state of the most recent pipeline run only (gray = no
|
|
8027
|
+
// signal / gh unavailable, green = latest run succeeded, yellow = latest
|
|
8028
|
+
// run in progress or non-success conclusion other than a hard failure,
|
|
8029
|
+
// red = latest run failed/cancelled/timed out).
|
|
8030
|
+
const devopsDot = document.getElementById('navDevopsDot');
|
|
8031
|
+
const latestRun = (d.devops?.pipeline?.runs || [])[0] || null;
|
|
8032
|
+
if (!latestRun) {
|
|
8033
|
+
devopsDot.className = 'dot dot-gray';
|
|
8034
|
+
} else if (latestRun.status !== 'completed') {
|
|
8035
|
+
devopsDot.className = 'dot dot-yellow';
|
|
8036
|
+
} else if (latestRun.conclusion === 'success') {
|
|
8037
|
+
devopsDot.className = 'dot dot-green';
|
|
8038
|
+
} else if (['failure', 'cancelled', 'timed_out'].includes(latestRun.conclusion)) {
|
|
8039
|
+
devopsDot.className = 'dot dot-red';
|
|
8040
|
+
} else {
|
|
8041
|
+
devopsDot.className = 'dot dot-yellow';
|
|
8042
|
+
}
|
|
8043
|
+
|
|
7782
8044
|
// Terminals / processes nav items carry count badges only (no decorative dots).
|
|
7783
8045
|
|
|
7784
8046
|
// Build full HTML string once
|
|
@@ -8169,11 +8431,33 @@ function renderUnsafe() {
|
|
|
8169
8431
|
</div>
|
|
8170
8432
|
</div>
|
|
8171
8433
|
${renderGitGraphCard(d.git)}
|
|
8434
|
+
${renderGitVisualTree(d.git)}
|
|
8172
8435
|
${d.git?.dirty ? renderGitFileList(d.git) : ''}
|
|
8173
8436
|
</div>
|
|
8174
8437
|
</div>
|
|
8175
8438
|
`);
|
|
8176
8439
|
|
|
8440
|
+
// ===== DevOps =====
|
|
8441
|
+
// Distinct scope from Processes (local-process-only): CI/CD pipeline status
|
|
8442
|
+
// + a best-effort, honestly-scoped deploy-activity proxy. No fabricated
|
|
8443
|
+
// "service health" widgets for infra this dashboard does not actually poll.
|
|
8444
|
+
const devopsPipelineRuns = d.devops?.pipeline?.runs || [];
|
|
8445
|
+
const devopsRunCount = devopsPipelineRuns.length;
|
|
8446
|
+
parts.push(`
|
|
8447
|
+
<div class="content-section" id="section-devops">
|
|
8448
|
+
<div class="section-title">
|
|
8449
|
+
DevOps
|
|
8450
|
+
<span class="section-subtitle">${devopsRunCount} pipeline run${devopsRunCount !== 1 ? 's' : ''}</span>
|
|
8451
|
+
</div>
|
|
8452
|
+
<div class="card">
|
|
8453
|
+
<div class="git-graph-title">CI/CD pipeline</div>
|
|
8454
|
+
${renderDevopsPipelineCard(d.devops)}
|
|
8455
|
+
<div class="git-graph-title">Deploy activity</div>
|
|
8456
|
+
${renderDevopsDeployCard(d.devops)}
|
|
8457
|
+
</div>
|
|
8458
|
+
</div>
|
|
8459
|
+
`);
|
|
8460
|
+
|
|
8177
8461
|
// ===== Memory =====
|
|
8178
8462
|
const recentDecisions = d.memory?.recentDecisions || [];
|
|
8179
8463
|
const recentErrors = d.memory?.recentErrors || [];
|