@dadado/agent-kit-cli 4.8.4 → 4.8.9

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 ADDED
@@ -0,0 +1,59 @@
1
+ # @dadado/agent-kit-cli
2
+
3
+ Agent Kit CLI: HITL operating-layer install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, staging-to-prod, memory). It installs local workspace contracts; it is not a hosted control plane or graph workflow runtime.
4
+
5
+ ## Install
6
+
7
+ From your project root (Node.js 20+):
8
+
9
+ ```bash
10
+ npx @dadado/agent-kit-cli install
11
+ ```
12
+
13
+ Unpinned `npx` resolves to the latest publish. Pin a version when you need a reproducible install:
14
+
15
+ ```bash
16
+ npx @dadado/agent-kit-cli@x.y.z install
17
+ ```
18
+
19
+ Optional L1 packs:
20
+
21
+ ```bash
22
+ npx @dadado/agent-kit-cli install --pack clean-code,context-management
23
+ ```
24
+
25
+ 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.
26
+
27
+ After install, in Cursor run `/agent-kit-onboard`, then `/start-project` when you have a deliverable.
28
+
29
+ ## Mission Control
30
+
31
+ From package version **4.8.2** onward, this npm package includes Mission Control panel assets under `dashboard/`. In a consumer workspace after install:
32
+
33
+ ```bash
34
+ agent-kit dashboard
35
+ ```
36
+
37
+ 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.
38
+
39
+ 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/`.
40
+
41
+ ## Common commands
42
+
43
+ | Command | Purpose |
44
+ |---------|---------|
45
+ | `agent-kit install` | Bootstrap L0 (+ optional packs) and write `agent-kit.json` |
46
+ | `agent-kit status` | Show installed kit version and profile |
47
+ | `agent-kit doctor` | Diagnose repository readiness |
48
+ | `agent-kit update` | Re-apply L0/packs/skills from the registry |
49
+ | `agent-kit dashboard` | Start Mission Control for this workspace |
50
+ | `agent-kit add <id>` | Install a skill or L1 pack |
51
+ | `agent-kit run-plan` | Headless continuous plan runner (never promotes to production) |
52
+
53
+ Run `agent-kit --help` or `agent-kit <command> --help` for the full surface.
54
+
55
+ ## Docs
56
+
57
+ - Public repository and guides: https://github.com/agent-kit-startup/agent-kit
58
+ - Install contract (chat / no-CLI fallback): https://raw.githubusercontent.com/agent-kit-startup/agent-kit/main/install.md
59
+ - Getting started: https://github.com/agent-kit-startup/agent-kit/blob/main/docs/getting-started.md
@@ -24,6 +24,7 @@ import {
24
24
  buildMissionControlView,
25
25
  collectDeferredCheckIds,
26
26
  collectReadinessPendingFromReport,
27
+ describeProcess,
27
28
  detectAwaitingPrompt,
28
29
  dismissedAttentionIds,
29
30
  extractChatSnippet,
@@ -37,15 +38,26 @@ import {
37
38
  serializeFlightLogLedger,
38
39
  serializeMissionTimingLedger,
39
40
  } from "./lib/semantic-model.mjs";
41
+ import { MAX_TERMINAL_BYTES, buildTerminalSnapshotFields } from "./lib/terminal-snapshot.mjs";
40
42
 
41
43
  const KIT_ROOT = resolve(import.meta.dirname, "..");
42
44
  /** Snapshot root: consumer workspace when MISSION_CONTROL_REPO_ROOT is set, else kit tree. */
43
45
  const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
44
46
  const MAX_TERMINALS = 20;
45
47
  const MAX_PROCESSES = 25;
46
- const MAX_TERMINAL_BYTES = 64 * 1024;
47
- const MAX_LAST_OUTPUT_LINES = 15;
48
- const MAX_LAST_OUTPUT_CHARS = 1200;
48
+ const MAX_GIT_GRAPH_LINES = 25;
49
+ const MAX_GIT_GRAPH_LINE_CHARS = 160;
50
+
51
+ /** Soft wall-clock budget for optional collectors (transcripts, reports, ps). */
52
+ const SNAPSHOT_STARTED_MS = Date.now();
53
+ const SNAPSHOT_BUDGET_MS = (() => {
54
+ const raw = process.env.AGENT_KIT_DASHBOARD_DATA_BUDGET_MS;
55
+ const n = raw != null && raw !== "" ? Number(raw) : 12_000;
56
+ return Number.isFinite(n) && n > 0 ? n : 12_000;
57
+ })();
58
+ function withinSnapshotBudget(reserveMs = 400) {
59
+ return Date.now() - SNAPSHOT_STARTED_MS + reserveMs < SNAPSHOT_BUDGET_MS;
60
+ }
49
61
 
50
62
  // Agent-prompt scan bounds (fs half of the detection contract in semantic-model.mjs).
51
63
  const MAX_TRANSCRIPT_FILES = 60; // cap directory reads per snapshot
@@ -82,31 +94,6 @@ function redactTerminalOutput(text) {
82
94
  return out;
83
95
  }
84
96
 
85
- /** Last N lines of terminal body after YAML header, char-capped and redacted. */
86
- function extractLastOutput(rawContent) {
87
- const lines = rawContent.split("\n");
88
- let headerEnd = 0;
89
- let dashCount = 0;
90
- for (let i = 0; i < lines.length; i++) {
91
- if (lines[i].trim() === "---") {
92
- dashCount++;
93
- if (dashCount === 2) {
94
- headerEnd = i + 1;
95
- break;
96
- }
97
- }
98
- }
99
- if (headerEnd === 0) headerEnd = 10;
100
-
101
- const bodyLines = lines.slice(headerEnd).filter((l) => l.trim() && !l.startsWith("---"));
102
- if (bodyLines.length === 0) return null;
103
-
104
- const tail = bodyLines.slice(-MAX_LAST_OUTPUT_LINES);
105
- let text = redactTerminalOutput(tail.join("\n"));
106
- text = truncateStr(text, MAX_LAST_OUTPUT_CHARS);
107
- return text?.trim() ? text : null;
108
- }
109
-
110
97
  const SNAPSHOT = {
111
98
  _schema: {
112
99
  version: "1.2.0",
@@ -119,18 +106,20 @@ const SNAPSHOT = {
119
106
  "System metadata: repoRoot, listen port, handoff state, allowlisted config summary, package info, version, name, contextPacks",
120
107
  agents: "Agent definitions from .cursor/agents/*.md",
121
108
  commands: "Slash commands from .cursor/commands/*.md",
122
- memory: "Memory records: error count, decision count, recent decisions",
123
- git: "Git repository state: branch, dirty status, commit, ahead/behind, bounded files[]",
109
+ memory:
110
+ "Memory records: error count, decision count, recent decisions, recent parsed errors, error-o-meter stats",
111
+ git: "Git repository state: branch, dirty status, commit, ahead/behind, bounded files[], promotion flow vs staging/main, graph lines, staging hygiene",
124
112
  terminals:
125
113
  "Active Cursor terminal sessions with metadata, output line count, and capped lastOutput",
126
- processes: "Running process snapshots (node, serve.mjs, git operations)",
114
+ processes:
115
+ "Running process snapshots (node, serve.mjs, git operations) with elapsed time and a generated narration per process",
127
116
  skills: "Available skills discovered in .cursor/skills/",
128
117
  health: "Aggregated health status with per-check results",
129
118
  missionControl: "Normalized now/activity/attention/plans view model (source-backed; bounded)",
130
119
  },
131
120
  },
132
121
  generatedAt: new Date().toISOString(),
133
- dashboardDataVersion: "1.2.0",
122
+ dashboardDataVersion: "1.3.0",
134
123
  plans: [],
135
124
  system: {
136
125
  repoRoot: ROOT,
@@ -243,9 +232,123 @@ if (existsSync(commandsDir)) {
243
232
  }
244
233
  }
245
234
 
235
+ // 4b. Kit-managed marker: commands, skills, and agents listed in registry/registry.json are
236
+ // owned by kit updates (read-only from the dashboard). No registry: all project-local.
237
+ const kitCommandPaths = new Set();
238
+ const kitSkillDirs = new Set();
239
+ const kitAgentPaths = new Set();
240
+ const registryFile = join(ROOT, "registry", "registry.json");
241
+ if (existsSync(registryFile)) {
242
+ try {
243
+ const registry = JSON.parse(readFileSync(registryFile, "utf8"));
244
+ const entries = Array.isArray(registry?.artifacts) ? registry.artifacts : [];
245
+ for (const entry of entries) {
246
+ if (entry?.kind === "command" && typeof entry?.path === "string") {
247
+ kitCommandPaths.add(entry.path);
248
+ }
249
+ if (
250
+ entry?.kind === "skill" &&
251
+ typeof entry?.path === "string" &&
252
+ entry.path.startsWith("registry/skills/")
253
+ ) {
254
+ kitSkillDirs.add(entry.path.replace(/^registry\/skills\//, ".cursor/skills/"));
255
+ }
256
+ if (entry?.kind === "agent" && typeof entry?.path === "string") {
257
+ kitAgentPaths.add(entry.path);
258
+ }
259
+ }
260
+ } catch {
261
+ // Unreadable registry: degrade to all-editable rather than locking everything.
262
+ }
263
+ }
264
+ for (const c of SNAPSHOT.commands) {
265
+ c.kitManaged = kitCommandPaths.has(c.path);
266
+ }
267
+ for (const a of SNAPSHOT.agents) {
268
+ a.kitManaged = kitAgentPaths.has(a.path);
269
+ }
270
+
246
271
  // 5. Memory
247
272
  const memoryErrorsDir = join(ROOT, ".cursor", "memory", "errors");
248
273
  const memoryDecisionsDir = join(ROOT, ".cursor", "memory", "decisions");
274
+ const MAX_MEMORY_RECENT_ERRORS = 12; // cap parsed entries shipped per snapshot
275
+ const MAX_MEMORY_ERROR_BYTES = 64 * 1024; // skip oversized entries, degrade quietly
276
+
277
+ /** Parse one `.cursor/memory/errors/*.md` entry into the KPI-friendly shape. */
278
+ function parseMemoryErrorFile(dir, file) {
279
+ const id = file.replace(/\.md$/, "");
280
+ const path = `.cursor/memory/errors/${file}`;
281
+ const full = join(dir, file);
282
+ let modifiedAt = null;
283
+ try {
284
+ modifiedAt = statSync(full).mtime.toISOString();
285
+ } catch {
286
+ modifiedAt = null;
287
+ }
288
+ let raw = "";
289
+ try {
290
+ raw = readFileSync(full, "utf-8").slice(0, MAX_MEMORY_ERROR_BYTES);
291
+ } catch {
292
+ return {
293
+ id,
294
+ path,
295
+ title: id,
296
+ date: "",
297
+ error: "",
298
+ cause: "",
299
+ solution: "",
300
+ files: "",
301
+ tags: [],
302
+ modifiedAt,
303
+ };
304
+ }
305
+ const field = (...names) => {
306
+ for (const name of names) {
307
+ const m = raw.match(new RegExp(`^- \\*\\*${name}:\\*\\*\\s*(.+)$`, "im"));
308
+ if (m) return truncateStr(m[1].trim(), 600);
309
+ }
310
+ return "";
311
+ };
312
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
313
+ const tags = field("Tags")
314
+ .split(",")
315
+ .map((t) => t.trim())
316
+ .filter(Boolean);
317
+ return {
318
+ id,
319
+ path,
320
+ title: titleMatch ? truncateStr(titleMatch[1].trim(), 200) : id,
321
+ date: field("Date", "Data"),
322
+ error: field("Error", "Erro"),
323
+ cause: field("Cause", "Causa"),
324
+ solution: field("Solution", "Solução", "Solucao"),
325
+ files: field("Files", "Arquivos"),
326
+ tags,
327
+ modifiedAt,
328
+ };
329
+ }
330
+
331
+ /** Error-o-meter aggregates: counts, rates, and top tags across parsed entries. */
332
+ function computeMemoryErrorStats(entries) {
333
+ const total = entries.length;
334
+ const now = Date.now();
335
+ const DAY_MS = 24 * 60 * 60 * 1000;
336
+ const last30d = entries.filter((e) => {
337
+ const t = Date.parse(e.date || e.modifiedAt || "");
338
+ return Number.isFinite(t) && now - t <= 30 * DAY_MS;
339
+ }).length;
340
+ const weeklyRate = Math.round((last30d / 30) * 7 * 10) / 10;
341
+ const tagCounts = new Map();
342
+ for (const e of entries) {
343
+ for (const t of e.tags || []) tagCounts.set(t, (tagCounts.get(t) || 0) + 1);
344
+ }
345
+ const topTags = [...tagCounts.entries()]
346
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
347
+ .slice(0, 6)
348
+ .map(([tag, count]) => ({ tag, count }));
349
+ return { total, last30d, weeklyRate, topTags };
350
+ }
351
+
249
352
  if (existsSync(memoryErrorsDir)) {
250
353
  const errorFiles = readdirSync(memoryErrorsDir).filter((f) => f.endsWith(".md"));
251
354
  SNAPSHOT.memory.errors = errorFiles.length;
@@ -259,6 +362,11 @@ if (existsSync(memoryErrorsDir)) {
259
362
  }
260
363
  return { id, modifiedAt };
261
364
  });
365
+ const parsedErrors = errorFiles
366
+ .map((f) => parseMemoryErrorFile(memoryErrorsDir, f))
367
+ .sort((a, b) => String(b.date || b.id).localeCompare(String(a.date || a.id)));
368
+ SNAPSHOT.memory.recentErrors = parsedErrors.slice(0, MAX_MEMORY_RECENT_ERRORS);
369
+ SNAPSHOT.memory.errorStats = computeMemoryErrorStats(parsedErrors);
262
370
  }
263
371
  if (existsSync(memoryDecisionsDir)) {
264
372
  const files = readdirSync(memoryDecisionsDir).filter((f) => f.endsWith(".md"));
@@ -314,6 +422,44 @@ try {
314
422
  recentLog = [];
315
423
  }
316
424
 
425
+ // Promotion flow state: ahead/behind of HEAD vs BOTH origin/staging and
426
+ // origin/main, plus staging vs main (pending promotion count).
427
+ const countDivergence = (range) => {
428
+ try {
429
+ const out = execSync(`git rev-list --left-right --count ${range}`, gitOpts).trim();
430
+ const [left, right] = out.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
431
+ return { ahead: right, behind: left };
432
+ } catch {
433
+ return null;
434
+ }
435
+ };
436
+ const flow = {
437
+ vsStaging: countDivergence("origin/staging...HEAD"),
438
+ vsMain: countDivergence("origin/main...HEAD"),
439
+ stagingVsMain: countDivergence("origin/main...origin/staging"),
440
+ };
441
+
442
+ // Readable graph (branch lanes + merges) as pre-rendered text lines.
443
+ let graphLines = [];
444
+ try {
445
+ graphLines = execSync(
446
+ `git log --graph --oneline --decorate --date-order --all -n ${MAX_GIT_GRAPH_LINES}`,
447
+ gitOpts,
448
+ )
449
+ .trimEnd()
450
+ .split("\n")
451
+ .filter(Boolean)
452
+ .map((line) => truncateStr(line, MAX_GIT_GRAPH_LINE_CHARS));
453
+ } catch {
454
+ graphLines = [];
455
+ }
456
+
457
+ // Staging hygiene: untracked plan-monitor WIP (add-by-name only, never a
458
+ // broad git add of .cursor/memory/; ADR 2026-07-29 staging hygiene R14/R15).
459
+ const monitorWip = parsed.files
460
+ .filter((f) => f.untracked && /^\.cursor\/memory\/plan-monitor-.+\.md$/.test(f.path))
461
+ .map((f) => f.path);
462
+
317
463
  SNAPSHOT.git = {
318
464
  branch: truncateStr(branch, MAX_STRING.branch),
319
465
  dirty: parsed.total > 0,
@@ -323,6 +469,9 @@ try {
323
469
  lastCommit: truncateStr(lastCommit, MAX_STRING.lastCommit),
324
470
  ahead,
325
471
  behind,
472
+ flow,
473
+ graph: graphLines,
474
+ hygiene: { monitorWip },
326
475
  };
327
476
  SNAPSHOT._gitRecentLog = recentLog;
328
477
  } catch {
@@ -344,26 +493,24 @@ if (existsSync(terminalProjectPath)) {
344
493
  for (const file of files) {
345
494
  const full = join(terminalProjectPath, file);
346
495
  const raw = readFileSync(full, "utf-8");
347
- // Cap huge terminal dumps: only header meta + a line count estimate is needed
348
- const content = raw.length > MAX_TERMINAL_BYTES ? raw.slice(0, MAX_TERMINAL_BYTES) : raw;
349
- const lines = content.split("\n");
350
- const meta = {};
351
- for (const line of lines.slice(0, 15)) {
352
- if (line.startsWith("pid:")) meta.pid = line.slice(4).trim();
353
- if (line.startsWith("cwd:")) meta.cwd = line.slice(4).trim();
354
- if (line.startsWith("command:")) meta.lastCommand = line.slice(8).trim();
355
- if (line.startsWith("last_command:")) meta.lastCommand = line.slice(13).trim();
356
- if (line.startsWith("last_exit_code:")) meta.lastExitCode = line.slice(15).trim();
357
- }
358
- const outputLines = lines.slice(10).filter((l) => {
359
- return l.trim() && !l.startsWith("---");
360
- }).length;
361
- const lastOutput = extractLastOutput(content);
496
+ // Meta from file head; body/output from tail-cap so over-cap terminals keep pid/cwd/exit
497
+ // (plain tail-slice previously dropped the header and blanked exit-code dots).
498
+ const { meta, outputLines, lastOutput } = buildTerminalSnapshotFields(raw, {
499
+ maxBytes: MAX_TERMINAL_BYTES,
500
+ redact: redactTerminalOutput,
501
+ truncate: truncateStr,
502
+ });
362
503
  const entry = {
363
504
  id: file,
364
505
  ...redactTerminalMeta(meta),
365
506
  outputLines,
366
507
  };
508
+ // File mtime feeds the busy-outside-plan freshness window (semantic model).
509
+ try {
510
+ entry.updatedAt = statSync(full).mtime.toISOString();
511
+ } catch {
512
+ // Missing mtime only disables the busy freshness signal for this terminal.
513
+ }
367
514
  if (lastOutput) entry.lastOutput = lastOutput;
368
515
  SNAPSHOT.terminals.push(entry);
369
516
  }
@@ -431,6 +578,7 @@ if (existsSync(skillsDir)) {
431
578
  title: titleMatch ? titleMatch[1].trim() : relativeDir.split("/").pop(),
432
579
  description: descMatch ? descMatch[1].trim().slice(0, 150) : "",
433
580
  file: fullPath.replace(`${ROOT}/`, ""),
581
+ kitManaged: kitSkillDirs.has(`.cursor/skills/${relativeDir}`),
434
582
  });
435
583
  }
436
584
  }
@@ -443,36 +591,43 @@ if (existsSync(skillsDir)) {
443
591
 
444
592
  // 13. Process scanning (capped list: the UI only needs a sample of relevant procs)
445
593
  try {
446
- const psOutput = execSync("ps -axo pid=,pcpu=,pmem=,command=", {
447
- encoding: "utf-8",
448
- timeout: 3000,
449
- }).trim();
450
- if (psOutput) {
451
- const interesting = [];
452
- for (const line of psOutput.split("\n")) {
453
- const trimmed = line.trim();
454
- if (!trimmed) continue;
455
- if (!/node|git|serve\.mjs|dashboard/i.test(trimmed)) continue;
456
- if (/grep|dashboard-data/.test(trimmed)) continue;
457
- const parts = trimmed.split(/\s+/);
458
- const pid = parts[0];
459
- const cpu = parts[1];
460
- const mem = parts[2];
461
- const cmd = parts.slice(3).join(" ") || "unknown";
462
- let label = "other";
463
- if (cmd.includes("serve.mjs") || cmd.includes("node dashboard")) label = "dashboard-server";
464
- else if (/\bgit\b/.test(cmd)) label = "git";
465
- else if (cmd.includes("node")) label = "node";
466
- interesting.push({
467
- pid,
468
- cpu,
469
- mem,
470
- command: truncateStr(cmd, MAX_STRING.processCommand),
471
- label,
472
- });
473
- if (interesting.length >= MAX_PROCESSES) break;
594
+ if (!withinSnapshotBudget(500)) {
595
+ SNAPSHOT.processes = [];
596
+ } else {
597
+ const psOutput = execSync("ps -axo pid=,pcpu=,pmem=,etime=,command=", {
598
+ encoding: "utf-8",
599
+ timeout: 3000,
600
+ }).trim();
601
+ if (psOutput) {
602
+ const interesting = [];
603
+ for (const line of psOutput.split("\n")) {
604
+ const trimmed = line.trim();
605
+ if (!trimmed) continue;
606
+ if (!/node|git|serve\.mjs|dashboard/i.test(trimmed)) continue;
607
+ if (/grep|dashboard-data/.test(trimmed)) continue;
608
+ const parts = trimmed.split(/\s+/);
609
+ const pid = parts[0];
610
+ const cpu = parts[1];
611
+ const mem = parts[2];
612
+ const etime = parts[3];
613
+ const cmd = parts.slice(4).join(" ") || "unknown";
614
+ let label = "other";
615
+ if (cmd.includes("serve.mjs") || cmd.includes("node dashboard")) label = "dashboard-server";
616
+ else if (/\bgit\b/.test(cmd)) label = "git";
617
+ else if (cmd.includes("node")) label = "node";
618
+ interesting.push({
619
+ pid,
620
+ cpu,
621
+ mem,
622
+ etime,
623
+ command: truncateStr(cmd, MAX_STRING.processCommand),
624
+ label,
625
+ description: describeProcess({ label, command: cmd, cpu, etime }),
626
+ });
627
+ if (interesting.length >= MAX_PROCESSES) break;
628
+ }
629
+ SNAPSHOT.processes = interesting;
474
630
  }
475
- SNAPSHOT.processes = interesting;
476
631
  }
477
632
  } catch {
478
633
  SNAPSHOT.processes = [];
@@ -483,7 +638,8 @@ const checks = [
483
638
  { id: "plans", label: "Plans directory", ok: existsSync(plansDir) && SNAPSHOT.plans.length > 0 },
484
639
  // Present + parseable HANDOFF is healthy even when Plan is none/null (idle).
485
640
  { id: "handoff", label: "HANDOFF.md", ok: !!SNAPSHOT.system.handoff },
486
- { id: "agents", label: "Agents", ok: SNAPSHOT.agents.length > 0 },
641
+ // L0-optional: empty .cursor/agents/ is healthy (packs/skills may add agents later).
642
+ { id: "agents", label: "Agents", ok: true },
487
643
  { id: "commands", label: "Commands", ok: SNAPSHOT.commands.length > 0 },
488
644
  {
489
645
  id: "memory",
@@ -518,6 +674,7 @@ SNAPSHOT.health.status = checks.every((c) => c.ok)
518
674
  * never an error state.
519
675
  */
520
676
  function collectAgentPrompts() {
677
+ if (!withinSnapshotBudget(800)) return [];
521
678
  const projectsDir = resolve(process.env.HOME || "~", ".cursor", "projects");
522
679
  const slug = ROOT.replace(/\//g, "-").replace(/^-/, "");
523
680
  const transcriptsDir = join(projectsDir, slug, "agent-transcripts");
@@ -592,6 +749,7 @@ function collectAgentPrompts() {
592
749
  * directory yields an empty list, never an error state.
593
750
  */
594
751
  function collectExternalReports() {
752
+ if (!withinSnapshotBudget(600)) return [];
595
753
  const memoryDir = join(ROOT, ".cursor", "memory");
596
754
  if (!existsSync(memoryDir)) return [];
597
755