@dadado/agent-kit-cli 5.6.0 → 5.8.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 CHANGED
@@ -14,10 +14,10 @@ Agent Kit CLI: HITL operating-layer install and tooling for AI-assisted IDEs (ru
14
14
  From your project root (Node.js 20+):
15
15
 
16
16
  ```bash
17
- npx @dadado/agent-kit-cli install
17
+ npx @dadado/agent-kit-cli@latest install
18
18
  ```
19
19
 
20
- Unpinned `npx` resolves to the latest publish. Pin a version when you need a reproducible install:
20
+ Use `@latest` so npx does not reuse a stale cached CLI. Pin a version when you need a reproducible install:
21
21
 
22
22
  ```bash
23
23
  npx @dadado/agent-kit-cli@x.y.z install
@@ -26,7 +26,7 @@ npx @dadado/agent-kit-cli@x.y.z install
26
26
  Optional L1 packs:
27
27
 
28
28
  ```bash
29
- npx @dadado/agent-kit-cli install --pack clean-code,context-management
29
+ npx @dadado/agent-kit-cli@latest install --pack clean-code,context-management
30
30
  ```
31
31
 
32
32
  Install writes L0 kit files under `.cursor/`, plus `autogit/` and `.cursor/agent-kit.json`. It does not copy the Agent Kit monorepo into your project.
@@ -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)
@@ -60,7 +62,7 @@ NO_COLOR=1 agent-kit
60
62
  # → plain text (no ANSI); also plain when stdout is not a TTY or CI=1
61
63
  ```
62
64
 
63
- Subcommands and `agent-kit --version` are unchanged. Chat-only HITL flows (`/start-project`, `/git-staging`, `/git-prod`, `/run-plan-all`, backlog CRUD) are not CLI commands.
65
+ Subcommands and `agent-kit --version` are unchanged. `agent-kit run <slash>` starts those project slashes headless from `.cursor/commands/` (numbered-list HITL). `/git-prod` and `/kit-prod` stay operator-gated and are omitted from that catalog.
64
66
 
65
67
  On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `update`, `run-plan` ticks) show an in-process ANSI spinner plus a rotating Mission Kit tip. Set `AGENT_KIT_REDUCED_MOTION=1` for static text on a capable TTY. Runtime dependencies stay `@clack/prompts`, `citty`, and `kolorist` (no `ora` / `figlet` / `chalk` / `ink`). Window titles for `agent-kit dashboard` and `agent-kit dashboard-broadcast` use the workspace basename, not the CLI package folder.
66
68
 
@@ -73,8 +75,10 @@ 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 |
81
+ | `agent-kit run <slash>` | One headless session from an L0 slash file (numbered-list HITL; never git-prod) |
78
82
  | `agent-kit run-plan` | Headless continuous plan runner (never promotes to production) |
79
83
 
80
84
  Run `agent-kit --help` or `agent-kit <command> --help` for the full surface.
@@ -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) : 12_000;
58
- return Number.isFinite(n) && n > 0 ? n : 12_000;
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.2.0",
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.3.0",
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
- const status = execSync("git status --short", gitOpts).trim();
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));