@dadado/agent-kit-cli 5.1.0 → 5.2.1

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.
@@ -743,16 +743,19 @@ body.mc-fullscreen .top-tabs-row {
743
743
  }
744
744
 
745
745
  /* ===== Progress Bar ===== */
746
+ /* Track must stay visibly a track at 0% (contract item 5): --border-active
747
+ reads against --bg-card in both skins where --border blended in, and 6px
748
+ with rounded ends keeps mid fills from collapsing into a hairline. */
746
749
  .progress-bar {
747
- height: 4px;
748
- background: var(--border);
749
- border-radius: 2px;
750
+ height: 6px;
751
+ background: var(--border-active);
752
+ border-radius: 3px;
750
753
  overflow: hidden;
751
754
  margin-top: 8px;
752
755
  }
753
756
  .progress-fill {
754
757
  height: 100%;
755
- border-radius: 2px;
758
+ border-radius: 3px;
756
759
  transition: width 0.5s ease;
757
760
  }
758
761
  .progress-fill.green { background: var(--green); }
@@ -4267,11 +4270,11 @@ function fmtDate(iso) {
4267
4270
  return d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
4268
4271
  }
4269
4272
 
4273
+ // Presentation contract (Phase 2): progress is never an error signal. Any
4274
+ // in-flight percentage (0-99) renders the neutral accent; 100% renders green
4275
+ // (lifecycle completed with total > 0 implies 100 per the Phase 0 contract).
4270
4276
  function progressColor(pct) {
4271
- if (pct >= 100) return 'green';
4272
- if (pct >= 50) return 'blue';
4273
- if (pct >= 25) return 'yellow';
4274
- return 'red';
4277
+ return pct >= 100 ? 'green' : 'blue';
4275
4278
  }
4276
4279
 
4277
4280
  // Dot semantics: only state signals survive. completed = good, cancelled =
@@ -5614,8 +5617,8 @@ function renderConfigSection(d) {
5614
5617
  </div>
5615
5618
  <div class="config-row">
5616
5619
  <label for="config-epr-reviewerModel">Reviewer model</label>
5617
- <input type="text" id="config-epr-reviewerModel" name="eprReviewerModel" value="${escapeAttr(epr.reviewerModel || 'haiku')}" maxlength="64" data-focus-key="config-epr-reviewerModel" />
5618
- <span class="config-hint">Claude default haiku. Must differ from the implementer stamp (Auto/Auto refused).</span>
5620
+ <input type="text" id="config-epr-reviewerModel" name="eprReviewerModel" value="${escapeAttr(epr.reviewerModel || 'sonnet')}" maxlength="64" data-focus-key="config-epr-reviewerModel" />
5621
+ <span class="config-hint">Claude default sonnet (auto permission mode). Must differ from the implementer stamp (Auto/Auto refused).</span>
5619
5622
  </div>
5620
5623
  <div class="config-row">
5621
5624
  <label for="config-epr-advisorModel">Advisor model</label>
@@ -5702,7 +5705,7 @@ function collectMissionConfigPayload() {
5702
5705
  offerOnExhausted: !!document.getElementById('config-epr-offer')?.checked,
5703
5706
  autoRemediate: !!document.getElementById('config-epr-auto')?.checked,
5704
5707
  backend: document.getElementById('config-epr-backend')?.value || 'claude',
5705
- reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'haiku').trim(),
5708
+ reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'sonnet').trim(),
5706
5709
  advisorModel: (document.getElementById('config-epr-advisorModel')?.value || 'opus').trim(),
5707
5710
  waitSliceSeconds: (() => {
5708
5711
  const raw = document.getElementById('config-epr-waitSlice')?.value;
@@ -6334,10 +6337,18 @@ function mergePlansForUi(d) {
6334
6337
  const inProgress = fromItems
6335
6338
  ? items.filter((t) => t.status === 'in_progress').length
6336
6339
  : (raw.todos?.inProgress ?? 0);
6340
+ // Cancelled counts toward the fill numerator (terminal work, mirrors
6341
+ // TERMINAL_TODO_STATUSES / todoStats in dashboard/lib/semantic-model.mjs)
6342
+ // so the bar reaches 100% whenever the lifecycle pill says COMPLETED.
6343
+ const cancelled = fromItems
6344
+ ? items.filter((t) => t.status === 'cancelled').length
6345
+ : (raw.todos?.cancelled ?? enriched.progress?.cancelled ?? 0);
6337
6346
  const nextActionTodo = planNextActionTodo(items);
6338
6347
  let lifecycle = enriched.lifecycle;
6339
6348
  if (!lifecycle) {
6340
- if (total > 0 && completed >= total && (raw.todos?.inProgress || 0) === 0) {
6349
+ // Mirrors classifyPlan: terminal (completed + cancelled) exhausting the
6350
+ // list means todoStats.open === 0 → completed.
6351
+ if (total > 0 && completed + cancelled >= total && inProgress === 0) {
6341
6352
  lifecycle = 'completed';
6342
6353
  } else {
6343
6354
  lifecycle = 'incomplete';
@@ -6351,12 +6362,13 @@ function mergePlansForUi(d) {
6351
6362
  modifiedAt: raw.modifiedAt || enriched.modifiedAt || null,
6352
6363
  progressPct:
6353
6364
  total > 0
6354
- ? Math.round((completed / total) * 100)
6365
+ ? Math.round(((completed + cancelled) / total) * 100)
6355
6366
  : typeof raw.progress === 'number'
6356
6367
  ? raw.progress
6357
6368
  : 0,
6358
- progressLabel: `${completed} of ${total} complete`,
6369
+ progressLabel: `${completed} of ${total} complete` + (cancelled > 0 ? ` · ${cancelled} cancelled` : ''),
6359
6370
  progressCompleted: completed,
6371
+ progressCancelled: cancelled,
6360
6372
  progressTotal: total,
6361
6373
  progressInProgress: inProgress,
6362
6374
  nextActionTodo,
@@ -1,5 +1,84 @@
1
1
  /** Ambient types for dashboard/lib/guards.mjs (consumed by CLI TypeScript). */
2
2
 
3
+ type ProcessEnvLike = NodeJS.ProcessEnv | Record<string, string | undefined>;
4
+ type HeaderMap = Record<string, string | string[] | undefined>;
5
+ type GuardRequest = { headers?: HeaderMap };
6
+ type PortOpts = { base?: number; range?: number };
7
+ type GitStatusFile = {
8
+ path: string;
9
+ status: string;
10
+ staged: boolean;
11
+ unstaged: boolean;
12
+ untracked: boolean;
13
+ oldPath?: string;
14
+ renamed?: boolean;
15
+ };
16
+
17
+ export const DEFAULT_HOST: "127.0.0.1";
18
+ export const BROADCAST_TOKEN_ENV: "MISSION_CONTROL_TOKEN";
19
+ export const REPO_ROOT_ENV: "MISSION_CONTROL_REPO_ROOT";
20
+ export const KIT_ROOT_ENV_KEYS: readonly ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"];
21
+ export const BROADCAST_TOKEN_MIN_LEN: 16;
22
+ export const BROADCAST_TOKEN_COOKIE: "mc_token";
23
+ export const DEFAULT_PORT_BASE: 3333;
24
+ export const DEFAULT_PORT_RANGE: 256;
25
+ export const MAX_STRING: {
26
+ branch: number;
27
+ lastCommit: number;
28
+ terminalCwd: number;
29
+ terminalCommand: number;
30
+ processCommand: number;
31
+ };
32
+ export const MAX_GIT_FILES: 50;
33
+ export const MAX_GIT_PATH: 240;
34
+ export const CONTEXT_CONFIG_REL: ".cursor/context/config.json";
35
+ export const CONFIG_PERSONA_IDS: readonly ["autopilot", "night-shift", "ghost-runner"];
36
+ export const CONFIG_PERSONA_MODES: readonly ["continue-plan", "run-plan", "cli-run-plan"];
37
+ export const CONFIG_REVIEW_BACKENDS: readonly ["auto", "claude", "cursor"];
38
+ export const CONFIG_REVIEW_MODES: readonly ["paste", "autonomous"];
39
+ export const CONFIG_REVIEW_PREFLIGHT: readonly ["off", "warn", "block"];
40
+
41
+ export function escapePerlDoubleQuoted(value: string): string;
42
+ export function resolveSnapshotRepoRoot(env: ProcessEnvLike | undefined, kitRoot: string): string;
43
+ export function normalizeRepoRootKey(repoRoot: string): string;
44
+ export function hashRepoRoot(repoRoot: string): number;
45
+ export function repoRootLogId(repoRoot: string): string;
46
+ export function preferredPortForRepoRoot(repoRoot: string, opts?: PortOpts): number;
47
+ export function portCandidatesForRepoRoot(repoRoot: string, opts?: PortOpts): number[];
48
+ export function sameRepoRoot(a: string | null | undefined, b: string | null | undefined): boolean;
49
+ export function resolveMissionControlPort(args: {
50
+ repoRoot: string;
51
+ envPort?: string | number | null;
52
+ probe: (port: number) => { listening: boolean; repoRoot: string | null };
53
+ opts?: PortOpts;
54
+ }): { port: number; reuse: boolean; explicit: boolean };
55
+ export function isSafeRepoRelativePath(relPath: unknown): boolean;
56
+ export function resolveBindHost(envHost?: string | null): string;
57
+ export function isLoopbackBindHost(host: string | undefined | null): boolean;
58
+ export function normalizeAuthToken(raw: unknown): string;
59
+ export function isValidBroadcastToken(token: unknown): boolean;
60
+ export function generateBroadcastToken(): string;
61
+ export function tokensMatch(a: unknown, b: unknown): boolean;
62
+ export function resolveBroadcastAuth(
63
+ env?: ProcessEnvLike,
64
+ ):
65
+ | { ok: true; host: string; tokenRequired: boolean; token: string | null; broadcast: boolean }
66
+ | { ok: false; error: string };
67
+ export function extractRequestToken(req: GuardRequest, url: URL): string;
68
+ export function authorizeMissionControlRequest(
69
+ req: GuardRequest,
70
+ url: URL,
71
+ opts: { tokenRequired: boolean; expectedToken: string | null },
72
+ ): { ok: true; viaQuery: boolean } | { ok: false; status: number; error: string };
73
+ export function broadcastAuthCookieHeader(token: string): string;
74
+ export function listLanIPv4Addresses(): string[];
75
+ export function truncateStr(value: unknown, maxLen: number): unknown;
76
+ export function parseGitStatusShort(output: unknown): {
77
+ files: GitStatusFile[];
78
+ total: number;
79
+ truncated: boolean;
80
+ };
81
+ export function isLoopbackAddress(addr: string | undefined | null): boolean;
3
82
  export function resolveContextConfigPath(
4
83
  repoRoot: string,
5
84
  fsHooks?: {
@@ -8,3 +87,27 @@ export function resolveContextConfigPath(
8
87
  mkdirSync?: (path: string, opts?: { recursive?: boolean }) => void;
9
88
  },
10
89
  ): { ok: true; path: string } | { ok: false; error: string };
90
+ export function validateConfigWriteBody(
91
+ body: unknown,
92
+ ): { ok: true; patch: Record<string, unknown> } | { ok: false; error: string };
93
+ export function mergeConfigAllowlist(
94
+ existing: Record<string, unknown> | object,
95
+ patch: Record<string, unknown> | object,
96
+ ): Record<string, unknown>;
97
+ export function allowlistConfig(raw: unknown): Record<string, unknown>;
98
+ export function isAllowedOrigin(origin: unknown, port: unknown): boolean;
99
+ export function applyCorsHeaders(
100
+ req: GuardRequest,
101
+ res: { setHeader: (name: string, value: string) => unknown },
102
+ port: unknown,
103
+ ): boolean;
104
+ export function isUnderDashboard(resolvedPath: string, dashboardReal: string): boolean;
105
+ export function resolveDashboardStatic(
106
+ pathname: string,
107
+ hooks: {
108
+ dashboardDir: string;
109
+ dashboardReal: string;
110
+ existsSync: (path: string) => boolean;
111
+ realpathSync: (path: string) => string;
112
+ },
113
+ ): string | null;
@@ -201,16 +201,11 @@ export function openBrowser(url, options = {}) {
201
201
 
202
202
  /**
203
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
204
  *
206
205
  * @param {{ command: string, args: string[] }} built
207
206
  * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }}
208
207
  */
209
208
  function runPreferred(built) {
210
- if (options.spawnFn && !spawnSyncFn) {
211
- return runDetached(built);
212
- }
213
-
214
209
  const sync = spawnSyncFn ?? spawnSync;
215
210
 
216
211
  if (platform !== "darwin" && platform !== "win32") {
@@ -3064,10 +3064,16 @@ export function enrichPlans(plans, handoff) {
3064
3064
  path: plan.path,
3065
3065
  overview: truncateStr(plan.overview || "", MAX_SEMANTIC_LABEL),
3066
3066
  modifiedAt: plan.modifiedAt || null,
3067
+ // Counter SoT for plan progress (todoStats / TERMINAL_TODO_STATUSES).
3068
+ // `terminal` (completed + cancelled) is the fill numerator so the bar can
3069
+ // reach 100% exactly when classifyPlan says completed (open === 0).
3070
+ // Label format is mirrored by mergePlansForUi in dashboard/dashboard.html.
3067
3071
  progress: {
3068
3072
  completed: stats.completed,
3073
+ cancelled: stats.cancelled,
3074
+ terminal: stats.completed + stats.cancelled,
3069
3075
  total: stats.total,
3070
- label: `${stats.completed} of ${stats.total}`,
3076
+ label: `${stats.completed} of ${stats.total} complete${stats.cancelled > 0 ? ` · ${stats.cancelled} cancelled` : ""}`,
3071
3077
  },
3072
3078
  lifecycle: classifyPlan(plan, handoff),
3073
3079
  // Preserved when lifecycle is completed so UI/sort can still know provenance.
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
2
+ <!-- Marketplace 1:1 logotype. Plate fill is Mission Control bg-primary #0b0e14. Artwork is the Cursor-skin stroke helmet from logo-cursor.svg, centered with ~10% padding. Chrome marks stay in logo.svg and logo-cursor.svg. -->
3
+ <rect width="512" height="512" rx="96" fill="#0b0e14"/>
4
+ <g transform="translate(51.839 51.2) scale(18.244989)">
5
+ <path d="M9.61,22.1c-4.45-1.22-7.48-4.71-8.17-9.63v-2.47c0-5.39,4.37-9.75,9.75-9.75h0c5.39,0,9.75,4.37,9.75,9.75,0,.62.04,1.85,0,2.47-.34,4.86-3.54,8.41-8.2,9.63-.49.13-2.63.13-3.13,0Z" stroke="#e4e4e4" stroke-width=".5" stroke-miterlimit="10" fill="none"/>
6
+ <path d="M18.42,9.54c-.38,3.74-1.81,8.07-7.22,8.07s-6.66-4.49-7.22-8.07v-.19c0-3.47,3.23-6.28,7.22-6.28s7.22,2.81,7.22,6.28v.19Z" stroke="#e4e4e4" stroke-width=".5" stroke-miterlimit="10" fill="none"/>
7
+ <path d="M13.59,6.08c1.18.76,1.99,1.85,2.2,3.16.07.43.07.86,0,1.28s-.19.84-.37,1.25" stroke="#e4e4e4" stroke-width=".5" stroke-linecap="round" fill="none"/>
8
+ <g>
9
+ <line x1=".25" y1="7.9" x2=".25" y2="13.68" stroke="#e4e4e4" stroke-width=".5" stroke-linecap="round"/>
10
+ <line x1="22.13" y1="7.9" x2="22.13" y2="13.68" stroke="#e4e4e4" stroke-width=".5" stroke-linecap="round"/>
11
+ </g>
12
+ </g>
13
+ </svg>
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { execFileSync, execSync, spawn } from "node:child_process";
11
- import { existsSync, openSync } from "node:fs";
11
+ import { existsSync, openSync, realpathSync } from "node:fs";
12
12
  import { basename, dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import {
@@ -241,7 +241,7 @@ async function main() {
241
241
  return;
242
242
  }
243
243
  let configValue = null;
244
- const cfg = resolveContextConfigPath(ROOT, { existsSync });
244
+ const cfg = resolveContextConfigPath(ROOT, { existsSync, realpathSync });
245
245
  if (cfg.ok) {
246
246
  configValue = readPreferredBrowserFromConfig(cfg.path);
247
247
  }
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { execFileSync, execSync, spawn } from "node:child_process";
20
- import { existsSync, openSync } from "node:fs";
20
+ import { existsSync, openSync, realpathSync } from "node:fs";
21
21
  import { basename, dirname, join, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import {
@@ -257,7 +257,7 @@ async function main() {
257
257
  return;
258
258
  }
259
259
  let configValue = null;
260
- const cfg = resolveContextConfigPath(ROOT, { existsSync });
260
+ const cfg = resolveContextConfigPath(ROOT, { existsSync, realpathSync });
261
261
  if (cfg.ok) {
262
262
  configValue = readPreferredBrowserFromConfig(cfg.path);
263
263
  }
package/dist/index.js CHANGED
@@ -194,7 +194,11 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
194
194
  "f981764422d468567b5aff31148dc659aeee22cb13dc0ecd873d737deaf07372",
195
195
  "fa306a0cdb0f40c817e32164cd03b946564a02b4b7f7c3f4f0b9513096584d28",
196
196
  "fa5cf460eb314437081f7cea30dc8041c0bd6fc3f560a74bc2d7be1bf07384b0",
197
- "fc39ec6d8a22498697f968ffc0fe5f717bed97afd68f922c76a80ed1f10d6579"
197
+ "fc39ec6d8a22498697f968ffc0fe5f717bed97afd68f922c76a80ed1f10d6579",
198
+ "a8756742197c3a2e1a6d64f4bde2a88db48abb66a0f449d625cd2e1c7b8d0cb4",
199
+ "6a5f8795a4a26b419f167e249576b798781c95308dc1ea50958351de713231d4",
200
+ "4009c5e2faf9775d5708cdff0b5f9da80fd3ce390cc42aed0bb94272f6b05b1b",
201
+ "a8e070fae187908ef7b2cf2a41605e3079329403b1802b5fc67e83294c96a070"
198
202
  ]);
199
203
 
200
204
  // src/lifecycle/paths.ts
@@ -588,18 +592,36 @@ async function readLockOwner(lockDir) {
588
592
  }
589
593
  async function writeLockOwner(lockDir, owner) {
590
594
  const finalPath = lockOwnerPath(lockDir);
591
- const tmpPath = path5.join(lockDir, `owner.${owner.uuid}.tmp`);
595
+ const tmpPath = path5.join(lockDir, `owner.${owner.uuid}.${randomUUID()}.tmp`);
592
596
  await writeFile3(tmpPath, JSON.stringify(owner), "utf8");
593
597
  await rename(tmpPath, finalPath);
594
598
  }
595
599
  async function refreshLockOwner(lockDir, uuid) {
596
600
  const owner = await readLockOwner(lockDir);
597
- if (!owner || owner.uuid !== uuid) return;
598
- await writeLockOwner(lockDir, { ...owner, updatedAt: Date.now() });
601
+ if (owner && owner.uuid !== uuid) return;
602
+ await writeLockOwner(lockDir, { pid: process.pid, uuid, updatedAt: Date.now() });
599
603
  }
600
604
  async function releaseCacheLock(lockDir, uuid) {
601
- const owner = await readLockOwner(lockDir);
602
- if (!owner || owner.uuid !== uuid) {
605
+ const claimPath = path5.join(lockDir, `releasing.${uuid}`);
606
+ try {
607
+ await rename(lockOwnerPath(lockDir), claimPath);
608
+ } catch {
609
+ return;
610
+ }
611
+ let claimed = null;
612
+ try {
613
+ const raw = await readFile4(claimPath, "utf8");
614
+ const parsed = JSON.parse(raw);
615
+ if (typeof parsed.pid === "number" && typeof parsed.uuid === "string") {
616
+ claimed = parsed;
617
+ }
618
+ } catch {
619
+ }
620
+ if (!claimed || claimed.uuid !== uuid) {
621
+ try {
622
+ await rename(claimPath, lockOwnerPath(lockDir));
623
+ } catch {
624
+ }
603
625
  return;
604
626
  }
605
627
  try {
@@ -638,12 +660,18 @@ async function acquireCacheLock(cacheDir) {
638
660
  const uuid = randomUUID();
639
661
  const owner = { pid: process.pid, uuid, updatedAt: Date.now() };
640
662
  await mkdir4(path5.dirname(lockDir), { recursive: true });
663
+ let lastErrorCode;
641
664
  while (Date.now() < deadline) {
642
665
  try {
643
666
  await mkdir4(lockDir, { recursive: false });
644
667
  await writeLockOwner(lockDir, owner);
668
+ let refreshing = false;
645
669
  const refreshInterval = setInterval(() => {
670
+ if (refreshing) return;
671
+ refreshing = true;
646
672
  refreshLockOwner(lockDir, uuid).catch(() => {
673
+ }).finally(() => {
674
+ refreshing = false;
647
675
  });
648
676
  }, LOCK_REFRESH_MS);
649
677
  return async () => {
@@ -652,6 +680,7 @@ async function acquireCacheLock(cacheDir) {
652
680
  };
653
681
  } catch (err) {
654
682
  const code = err.code;
683
+ lastErrorCode = code;
655
684
  if (code === "EEXIST") {
656
685
  if (await tryReclaimStaleLock(lockDir)) {
657
686
  continue;
@@ -660,12 +689,16 @@ async function acquireCacheLock(cacheDir) {
660
689
  continue;
661
690
  }
662
691
  if (code === "ENOENT") {
692
+ await mkdir4(path5.dirname(lockDir), { recursive: true });
693
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS + Math.random() * 100));
663
694
  continue;
664
695
  }
665
696
  throw err;
666
697
  }
667
698
  }
668
- throw new Error(`Timed out waiting for cache lock on ${cacheDir}. Another install may be stuck.`);
699
+ throw new Error(
700
+ lastErrorCode === "ENOENT" ? `Timed out acquiring cache lock on ${cacheDir}: the lock parent directory kept vanishing (concurrent cache clear?).` : `Timed out waiting for cache lock on ${cacheDir}. Another install may be stuck.`
701
+ );
669
702
  }
670
703
  var DEFAULT_REGISTRY_URL = "https://github.com/agent-kit-startup/agent-kit";
671
704
  var DEFAULT_REGISTRY_REF = "main";
@@ -1851,6 +1884,7 @@ async function resolveInventoryRoot(cwd) {
1851
1884
  return dir;
1852
1885
  } catch {
1853
1886
  }
1887
+ if (await fileExists(path10.join(dir, ".git"))) break;
1854
1888
  const parent = path10.dirname(dir);
1855
1889
  if (parent === dir) break;
1856
1890
  dir = parent;
@@ -2027,12 +2061,16 @@ function baseResult(partial) {
2027
2061
  };
2028
2062
  }
2029
2063
  async function checkCursorUpdateAwareness(cwd, options = {}) {
2030
- const prefs = readCursorUpdateCheckPrefs(await loadContextConfig(cwd));
2064
+ const inventoryRoot = await resolveInventoryRoot(cwd);
2065
+ const prefs = readCursorUpdateCheckPrefs(
2066
+ inventoryRoot ? await loadContextConfig(inventoryRoot) : null
2067
+ );
2031
2068
  const changelogUrl = options.changelogUrl ?? prefs.changelogUrl;
2032
2069
  if (options.respectPrefs) {
2033
2070
  if (!prefs.enabled) {
2034
2071
  return baseResult({
2035
2072
  status: "skipped-disabled",
2073
+ inventoryRoot,
2036
2074
  inventoryPath: INVENTORY_REL,
2037
2075
  featuresPath: FEATURES_REL,
2038
2076
  changelogUrl,
@@ -2047,6 +2085,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2047
2085
  if (!intervalElapsed(prefs.lastCheckedAt, prefs.intervalDays)) {
2048
2086
  return baseResult({
2049
2087
  status: "skipped-interval",
2088
+ inventoryRoot,
2050
2089
  inventoryPath: INVENTORY_REL,
2051
2090
  featuresPath: FEATURES_REL,
2052
2091
  changelogUrl,
@@ -2059,10 +2098,10 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2059
2098
  });
2060
2099
  }
2061
2100
  }
2062
- const inventoryRoot = await resolveInventoryRoot(cwd);
2063
2101
  if (!inventoryRoot) {
2064
2102
  return baseResult({
2065
2103
  status: "error",
2104
+ inventoryRoot: null,
2066
2105
  inventoryPath: INVENTORY_REL,
2067
2106
  featuresPath: FEATURES_REL,
2068
2107
  changelogUrl,
@@ -2082,6 +2121,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2082
2121
  } catch {
2083
2122
  return baseResult({
2084
2123
  status: "error",
2124
+ inventoryRoot,
2085
2125
  inventoryPath: INVENTORY_REL,
2086
2126
  featuresPath: FEATURES_REL,
2087
2127
  changelogUrl,
@@ -2166,6 +2206,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2166
2206
  const msg = err instanceof Error ? err.message : String(err);
2167
2207
  return baseResult({
2168
2208
  status: "error",
2209
+ inventoryRoot,
2169
2210
  inventoryPath: INVENTORY_REL,
2170
2211
  featuresPath: FEATURES_REL,
2171
2212
  changelogUrl,
@@ -2178,8 +2219,8 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2178
2219
  });
2179
2220
  }
2180
2221
  }
2181
- if (options.stamp) {
2182
- await stampCursorUpdateCheck(cwd, {
2222
+ if (options.stamp && inventoryRoot) {
2223
+ await stampCursorUpdateCheck(inventoryRoot, {
2183
2224
  lastSeenCursorVersion: latestCursorVersion ?? prefs.lastSeenCursorVersion
2184
2225
  });
2185
2226
  }
@@ -2187,6 +2228,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2187
2228
  const message = status === "current" ? "No advisory Cursor-update gaps vs inventory (check-only)." : `Found ${gaps.length} advisory gap(s). ${CONVEYOR_HINT}`;
2188
2229
  return baseResult({
2189
2230
  status,
2231
+ inventoryRoot,
2190
2232
  inventoryPath: INVENTORY_REL,
2191
2233
  featuresPath: FEATURES_REL,
2192
2234
  changelogUrl: options.offline ? null : changelogUrl,
@@ -5008,11 +5050,19 @@ async function fileExists2(p) {
5008
5050
  return false;
5009
5051
  }
5010
5052
  }
5053
+ function isMarkdownTableSeparator(line) {
5054
+ const stripped = line.trim();
5055
+ if (!stripped.startsWith("|")) return false;
5056
+ const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
5057
+ return parts.length > 0 && parts.every((cell) => /^:?-+:?$/.test(cell));
5058
+ }
5011
5059
  function parseUnprocessedDogfoodItems(readmeText) {
5012
5060
  const items = [];
5013
5061
  let inSection = false;
5014
5062
  let sectionLevel = 0;
5015
- for (const line of readmeText.split(/\r?\n/)) {
5063
+ const lines = readmeText.split(/\r?\n/);
5064
+ for (let i = 0; i < lines.length; i++) {
5065
+ const line = lines[i] ?? "";
5016
5066
  const unprocessedMatch = /^(#{2,3})\s+Unprocessed Files\b/.exec(line);
5017
5067
  if (unprocessedMatch) {
5018
5068
  const hashes = unprocessedMatch[1];
@@ -5024,11 +5074,14 @@ function parseUnprocessedDogfoodItems(readmeText) {
5024
5074
  if (!inSection) continue;
5025
5075
  const headingMatch = /^(#{1,6})\s+/.exec(line);
5026
5076
  if (headingMatch) {
5027
- if (/\bProcessed Files\b/.test(line)) break;
5077
+ if (/^#{1,6}\s+Processed(?:\s+Files)?\b/.test(line)) break;
5028
5078
  const hashes = headingMatch[1];
5029
5079
  if (hashes && hashes.length <= sectionLevel) break;
5030
5080
  continue;
5031
5081
  }
5082
+ if (line.trim().startsWith("|") && isMarkdownTableSeparator(lines[i + 1] ?? "")) {
5083
+ continue;
5084
+ }
5032
5085
  const body = extractUnprocessedDogfoodLine(line);
5033
5086
  if (!body) continue;
5034
5087
  items.push(body);
@@ -5046,13 +5099,10 @@ function extractUnprocessedDogfoodLine(line) {
5046
5099
  if (numbered?.[2]) {
5047
5100
  raw = numbered[2].trim();
5048
5101
  } else if (stripped.startsWith("|")) {
5102
+ if (isMarkdownTableSeparator(stripped)) return null;
5049
5103
  const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
5050
5104
  if (parts.length === 0) return null;
5051
- if (parts.every((cell) => /^:?-+:?$/.test(cell))) return null;
5052
- const first = parts[0] ?? "";
5053
- const headerish = first.toLowerCase().replace(/[*_`]/g, "").trim();
5054
- if (/^(note|file|entrada|title|name|item|path)$/.test(headerish)) return null;
5055
- raw = first.trim();
5105
+ raw = (parts[0] ?? "").trim();
5056
5106
  }
5057
5107
  }
5058
5108
  if (!raw) return null;
@@ -5818,7 +5868,40 @@ function buildPersonalizationPlan(profile, report, registry) {
5818
5868
  (item) => componentAvailable(registry, item) ? item : { ...item, status: "unavailable" }
5819
5869
  ).sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
5820
5870
  }
5821
- function renderProjectContext(profile) {
5871
+ var INSTALLED_SKILL_STATUSES = /* @__PURE__ */ new Set(["applied", "skipped-customized"]);
5872
+ function installedSkillItems(items, installedIds = []) {
5873
+ const rows = [];
5874
+ const seen = /* @__PURE__ */ new Set();
5875
+ for (const item of items) {
5876
+ if (item.kind !== "skill" || !INSTALLED_SKILL_STATUSES.has(item.status)) continue;
5877
+ if (seen.has(item.id)) continue;
5878
+ seen.add(item.id);
5879
+ rows.push(item);
5880
+ }
5881
+ for (const id of installedIds) {
5882
+ if (seen.has(id)) continue;
5883
+ seen.add(id);
5884
+ rows.push({
5885
+ kind: "skill",
5886
+ id,
5887
+ status: "applied",
5888
+ evidence: [{ source: "configuration", value: `.cursor/agent-kit.json skills[]:${id}` }]
5889
+ });
5890
+ }
5891
+ return rows.sort((left, right) => left.id.localeCompare(right.id));
5892
+ }
5893
+ function relevantSkillTableRows(skills) {
5894
+ if (skills.length === 0) {
5895
+ return ["| (none yet) | No installed or project-owned skills detected | \u2014 |"];
5896
+ }
5897
+ return skills.map((skill) => {
5898
+ const label = skill.path ?? skill.id;
5899
+ const role = skill.status === "skipped-customized" ? "Already present (customized)" : "Installed by personalization";
5900
+ const evidence = skill.path ?? skill.evidence[0]?.value ?? "personalization";
5901
+ return `| ${label} | ${role} | ${evidence} |`;
5902
+ });
5903
+ }
5904
+ function renderProjectContext(profile, skillItems = []) {
5822
5905
  const sections = ["# Project Context", "", "Verified repository facts:"];
5823
5906
  const purpose = purposeEvidence(profile);
5824
5907
  if (purpose.length > 0 && profile.purpose.value !== "unknown") {
@@ -5845,7 +5928,7 @@ function renderProjectContext(profile) {
5845
5928
  "",
5846
5929
  "| Skill / path | Role | Evidence |",
5847
5930
  "|--------------|------|----------|",
5848
- "| (none yet) | Add rows when `/agent-kit-onboard` scaffolds domain skills or personalization installs packs | \u2014 |"
5931
+ ...relevantSkillTableRows(installedSkillItems(skillItems))
5849
5932
  );
5850
5933
  if (profile.context.sources.length > 0) {
5851
5934
  sections.push("", "## Sources", ...profile.context.sources.map((item) => `- ${item.value}`));
@@ -5946,7 +6029,7 @@ async function applyPersonalization(input) {
5946
6029
  createOwnedFile(
5947
6030
  input.rootDir,
5948
6031
  CONTEXT_PATH,
5949
- renderProjectContext(input.profile),
6032
+ renderProjectContext(input.profile, installedSkillItems(componentResults, skills)),
5950
6033
  profileEvidence
5951
6034
  ),
5952
6035
  createOwnedFile(
@@ -6269,7 +6352,8 @@ var installCommand = defineCommand11({
6269
6352
  console.error(`
6270
6353
  ${hint.recovery}
6271
6354
  `);
6272
- process.exit(1);
6355
+ process.exitCode = 1;
6356
+ return;
6273
6357
  }
6274
6358
  }
6275
6359
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dadado/agent-kit-cli",
3
- "version": "5.1.0",
3
+ "version": "5.2.1",
4
4
  "description": "Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context).",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "type": "module",
@@ -32,6 +32,8 @@
32
32
  "dev": "tsx src/index.ts",
33
33
  "start": "tsx src/index.ts",
34
34
  "lint": "biome check src",
35
+ "overlay:hashes": "tsx src/lifecycle/refresh-known-hashes.ts",
36
+ "overlay:hashes:check": "tsx src/lifecycle/refresh-known-hashes.ts --check",
35
37
  "test": "vitest run",
36
38
  "typecheck": "tsc --noEmit"
37
39
  }