@dadado/agent-kit-cli 5.3.0 → 5.5.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
@@ -63,7 +63,8 @@ On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `upda
63
63
  |---------|---------|
64
64
  | `agent-kit install` | Bootstrap L0 (+ optional packs) and write `agent-kit.json` |
65
65
  | `agent-kit status` | Show installed kit version and profile |
66
- | `agent-kit doctor` | Diagnose repository readiness |
66
+ | `agent-kit doctor` | Diagnose repository readiness (`--json` includes an `env` pillar: bin-on-PATH, npm prefix writability, Node version, shell profile) |
67
+ | `agent-kit setup-global` | Self-heal a root-owned npm global prefix (relocate to `~/.npm-global`, fix `PATH`, reinstall) |
67
68
  | `agent-kit update` | Re-apply L0/packs/skills from the registry |
68
69
  | `agent-kit dashboard` | Start Mission Control for this workspace |
69
70
  | `agent-kit add <id>` | Install a skill or L1 pack |
@@ -0,0 +1,25 @@
1
+ # Mission Control runtime
2
+
3
+ The dashboard the CLI serves. Plain ES modules, no build step: `serve.mjs` reads this directory
4
+ directly, and `packages/cli/dashboard/` is a generated copy (gitignored, produced by
5
+ `scripts/sync-cli-dashboard.mjs` at build/prepack time so the npm tarball can ship it).
6
+
7
+ | Path | Role |
8
+ | --- | --- |
9
+ | `serve.mjs` | HTTP server, auth gate, SSE |
10
+ | `start.mjs` / `start-broadcast.mjs` | loopback and LAN entry points, per-workspace port allocation |
11
+ | `dashboard-data.mjs` | snapshot builder for the panel |
12
+ | `dashboard.html`, `open.html` | panel and share shell |
13
+ | `lib/*.mjs` | guards, semantic model, live refresh, browser open, terminal snapshot |
14
+ | `lib/guards.d.mts` | hand-written types consumed by the CLI package (parity is pinned by a test) |
15
+
16
+ ## Where the tests live
17
+
18
+ Tests for these modules are in `packages/cli/src/dashboard/*.test.ts`, not next to the source. The
19
+ CLI package owns the only test runner in the workspace (vitest), and those suites import the `.mjs`
20
+ files directly (`../../../../dashboard/lib/...`) so there is one implementation under test rather
21
+ than a copy. Lint and format are covered from the repository root (`pnpm lint` checks `dashboard/**`
22
+ before it fans out to the workspace packages).
23
+
24
+ `dashboard.html` is outside Biome's scope; CSS/HTML-only changes are covered by
25
+ `packages/cli/src/dashboard/plugin-ux-validation.test.ts` instead.
@@ -111,7 +111,7 @@ const SNAPSHOT = {
111
111
  dashboardDataVersion: "Semantic version of the data model schema",
112
112
  plans: "Active plans from .cursor/plans/*.plan.md with frontmatter parsing",
113
113
  system:
114
- "System metadata: repoRoot, listen port, handoff state, allowlisted config summary, package info, version, name, contextPacks",
114
+ "System metadata: repoRoot, listen port, handoff state, allowlisted config summary, package info, version, name, contextPacks, detachedAuditSessions ({count, oldestAgeSeconds}|null; host-wide detached agent-kit-audit-* PTYs)",
115
115
  agents: "Agent definitions from .cursor/agents/*.md",
116
116
  commands: "Slash commands from .cursor/commands/*.md",
117
117
  memory:
@@ -641,6 +641,77 @@ try {
641
641
  SNAPSHOT.processes = [];
642
642
  }
643
643
 
644
+ // 13b. Detached audit-session visibility (plan phase3-visibility).
645
+ // Mirrors the sessionStart hook semantics (packages/cli/src/hooks/session-start.ts):
646
+ // whole agent-kit-audit- namespace (any token, legacy unscoped names included),
647
+ // detached sessions only; attached sessions are operator work and never counted.
648
+ // Fail-open: null when zero sessions or tmux/screen is missing/errors.
649
+ SNAPSHOT.system.detachedAuditSessions = null;
650
+ try {
651
+ if (withinSnapshotBudget(400)) {
652
+ const auditAges = [];
653
+ const nowEpoch = Math.floor(Date.now() / 1000);
654
+ try {
655
+ const tmuxOut = execSync(
656
+ "tmux list-sessions -F '#{session_name} #{session_attached} #{session_created}'",
657
+ { encoding: "utf-8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] },
658
+ );
659
+ for (const line of tmuxOut.split("\n")) {
660
+ const m = line.trim().match(/^(\S+)\s+(\d+)\s+(\d+)$/);
661
+ if (!m) continue;
662
+ if (!m[1].startsWith("agent-kit-audit-")) continue;
663
+ if (Number(m[2]) > 0) continue;
664
+ const created = Number(m[3]);
665
+ auditAges.push(nowEpoch >= created ? nowEpoch - created : -1);
666
+ }
667
+ } catch {
668
+ // tmux missing or no server: fail-open
669
+ }
670
+ try {
671
+ // `screen -ls` exits 1 while successfully listing, so soften the exit code.
672
+ const screenOut = execSync("screen -ls || true", {
673
+ encoding: "utf-8",
674
+ timeout: 2000,
675
+ stdio: ["ignore", "pipe", "ignore"],
676
+ });
677
+ // The "N Sockets in <dir>." line trails the session list; socket mtime ~ start time.
678
+ let sockdir = null;
679
+ for (const line of screenOut.split("\n")) {
680
+ const dirMatch = line.match(/^\d+\s+Sockets?\s+in\s+(.+)\.$/);
681
+ if (dirMatch) sockdir = dirMatch[1];
682
+ }
683
+ for (const line of screenOut.split("\n")) {
684
+ const m = line.match(/^\s+(\d+)\.(\S+)\s+\((.*)\)/);
685
+ if (!m) continue;
686
+ if (!m[2].startsWith("agent-kit-audit-")) continue;
687
+ // "Detached" has a single t before "ached", so it never matches [Aa]ttached.
688
+ if (/[Aa]ttached/.test(m[3])) continue;
689
+ let age = -1;
690
+ if (sockdir) {
691
+ try {
692
+ const mtimeMs = statSync(join(sockdir, `${m[1]}.${m[2]}`)).mtimeMs;
693
+ if (Date.now() >= mtimeMs) age = Math.floor((Date.now() - mtimeMs) / 1000);
694
+ } catch {
695
+ // socket not stat-able: age stays unknown, session still counted
696
+ }
697
+ }
698
+ auditAges.push(age);
699
+ }
700
+ } catch {
701
+ // screen missing: fail-open
702
+ }
703
+ if (auditAges.length > 0) {
704
+ const known = auditAges.filter((a) => a >= 0);
705
+ SNAPSHOT.system.detachedAuditSessions = {
706
+ count: auditAges.length,
707
+ oldestAgeSeconds: known.length ? Math.max(...known) : null,
708
+ };
709
+ }
710
+ }
711
+ } catch {
712
+ SNAPSHOT.system.detachedAuditSessions = null;
713
+ }
714
+
644
715
  // 10. Health checks (originally)
645
716
  const checks = [
646
717
  { id: "plans", label: "Plans directory", ok: existsSync(plansDir) && SNAPSHOT.plans.length > 0 },
@@ -4270,6 +4270,16 @@ function fmtDate(iso) {
4270
4270
  return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
4271
4271
  }
4272
4272
 
4273
+ // Humanize a duration in seconds (mirrors formatSessionAge in the sessionStart hook).
4274
+ function formatDuration(seconds) {
4275
+ const s = Number(seconds);
4276
+ if (!Number.isFinite(s) || s < 0) return 'unknown';
4277
+ if (s >= 86400) return Math.floor(s / 86400) + 'd';
4278
+ if (s >= 3600) return Math.floor(s / 3600) + 'h';
4279
+ if (s >= 60) return Math.floor(s / 60) + 'm';
4280
+ return Math.floor(s) + 's';
4281
+ }
4282
+
4273
4283
  // Presentation contract (Phase 2): progress is never an error signal. Any
4274
4284
  // in-flight percentage (0-99) renders the neutral accent; 100% renders green
4275
4285
  // (lifecycle completed with total > 0 implies 100 per the Phase 0 contract).
@@ -8289,6 +8299,14 @@ function renderUnsafe() {
8289
8299
 
8290
8300
  // ===== Processes =====
8291
8301
  const processes = d.processes || [];
8302
+ // Detached audit-session visibility (phase3): host-wide agent-kit-audit-* PTYs.
8303
+ const auditSessions = d.system?.detachedAuditSessions || null;
8304
+ const auditSessionsNote = auditSessions && auditSessions.count > 0
8305
+ ? `
8306
+ <div class="processes-note">
8307
+ <span><strong>${auditSessions.count} detached audit ${auditSessions.count === 1 ? 'session' : 'sessions'}.</strong> Host-wide <code>agent-kit-audit-*</code> plan-review ${auditSessions.count === 1 ? 'PTY is' : 'PTYs are'} still alive (${auditSessions.oldestAgeSeconds != null ? `oldest ~${formatDuration(auditSessions.oldestAgeSeconds)}` : 'oldest age unknown'}). Inspect with <code>tmux attach -t &lt;name&gt;</code> / <code>screen -r &lt;name&gt;</code>, or let the audit launcher's session GC dispose of them on the next spawn.</span>
8308
+ </div>`
8309
+ : '';
8292
8310
  parts.push(`
8293
8311
  <div class="content-section" id="section-processes">
8294
8312
  <div class="section-title">
@@ -8297,7 +8315,7 @@ function renderUnsafe() {
8297
8315
  </div>
8298
8316
  <div class="processes-note">
8299
8317
  <span><strong>Live ps snapshot.</strong> Agent chats spawned inside the IDE do not appear here; the Crew monitor on the Overview tab tracks that activity.</span>
8300
- </div>
8318
+ </div>${auditSessionsNote}
8301
8319
  ${processes.length === 0
8302
8320
  ? renderEmptyStateCta({
8303
8321
  headline: 'All quiet',
@@ -47,12 +47,12 @@ export function escapePerlDoubleQuoted(value) {
47
47
 
48
48
  /**
49
49
  * Resolve the repository root Mission Control should snapshot.
50
- * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
50
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined> | undefined} env - `undefined` falls back to `process.env`
51
51
  * @param {string} kitRoot - absolute path to the kit tree (parent of `dashboard/`)
52
52
  * @returns {string} absolute snapshot root
53
53
  */
54
- export function resolveSnapshotRepoRoot(env = process.env, kitRoot) {
55
- const raw = env?.[REPO_ROOT_ENV];
54
+ export function resolveSnapshotRepoRoot(env, kitRoot) {
55
+ const raw = (env ?? process.env)?.[REPO_ROOT_ENV];
56
56
  if (typeof raw === "string" && raw.trim()) {
57
57
  return resolve(raw.trim());
58
58
  }
@@ -400,7 +400,7 @@ export function tokensMatch(a, b) {
400
400
  /**
401
401
  * Resolve bind + token gate for Mission Control serve.
402
402
  * Non-loopback bind requires a valid MISSION_CONTROL_TOKEN (no warn-only 0.0.0.0).
403
- * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
403
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined> | undefined} env - `undefined` falls back to `process.env`
404
404
  * @returns
405
405
  * | { ok: true, host: string, tokenRequired: boolean, token: string | null, broadcast: boolean }
406
406
  * | { ok: false, error: string }
@@ -11,8 +11,7 @@
11
11
  * ## Follow-up plan (also "Followup plan")
12
12
  * ## Residuals plan
13
13
  */
14
- export const TRIAGE_HEADING_RE =
15
- /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
14
+ export const TRIAGE_HEADING_RE = /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
16
15
 
17
16
  /** True when markdown carries a durable triage heading. */
18
17
  export function hasTriageHeading(text) {