@dadado/agent-kit-cli 5.1.0 → 5.2.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.
@@ -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.
@@ -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,9 @@ 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"
198
200
  ]);
199
201
 
200
202
  // src/lifecycle/paths.ts
@@ -588,18 +590,36 @@ async function readLockOwner(lockDir) {
588
590
  }
589
591
  async function writeLockOwner(lockDir, owner) {
590
592
  const finalPath = lockOwnerPath(lockDir);
591
- const tmpPath = path5.join(lockDir, `owner.${owner.uuid}.tmp`);
593
+ const tmpPath = path5.join(lockDir, `owner.${owner.uuid}.${randomUUID()}.tmp`);
592
594
  await writeFile3(tmpPath, JSON.stringify(owner), "utf8");
593
595
  await rename(tmpPath, finalPath);
594
596
  }
595
597
  async function refreshLockOwner(lockDir, uuid) {
596
598
  const owner = await readLockOwner(lockDir);
597
- if (!owner || owner.uuid !== uuid) return;
598
- await writeLockOwner(lockDir, { ...owner, updatedAt: Date.now() });
599
+ if (owner && owner.uuid !== uuid) return;
600
+ await writeLockOwner(lockDir, { pid: process.pid, uuid, updatedAt: Date.now() });
599
601
  }
600
602
  async function releaseCacheLock(lockDir, uuid) {
601
- const owner = await readLockOwner(lockDir);
602
- if (!owner || owner.uuid !== uuid) {
603
+ const claimPath = path5.join(lockDir, `releasing.${uuid}`);
604
+ try {
605
+ await rename(lockOwnerPath(lockDir), claimPath);
606
+ } catch {
607
+ return;
608
+ }
609
+ let claimed = null;
610
+ try {
611
+ const raw = await readFile4(claimPath, "utf8");
612
+ const parsed = JSON.parse(raw);
613
+ if (typeof parsed.pid === "number" && typeof parsed.uuid === "string") {
614
+ claimed = parsed;
615
+ }
616
+ } catch {
617
+ }
618
+ if (!claimed || claimed.uuid !== uuid) {
619
+ try {
620
+ await rename(claimPath, lockOwnerPath(lockDir));
621
+ } catch {
622
+ }
603
623
  return;
604
624
  }
605
625
  try {
@@ -638,12 +658,18 @@ async function acquireCacheLock(cacheDir) {
638
658
  const uuid = randomUUID();
639
659
  const owner = { pid: process.pid, uuid, updatedAt: Date.now() };
640
660
  await mkdir4(path5.dirname(lockDir), { recursive: true });
661
+ let lastErrorCode;
641
662
  while (Date.now() < deadline) {
642
663
  try {
643
664
  await mkdir4(lockDir, { recursive: false });
644
665
  await writeLockOwner(lockDir, owner);
666
+ let refreshing = false;
645
667
  const refreshInterval = setInterval(() => {
668
+ if (refreshing) return;
669
+ refreshing = true;
646
670
  refreshLockOwner(lockDir, uuid).catch(() => {
671
+ }).finally(() => {
672
+ refreshing = false;
647
673
  });
648
674
  }, LOCK_REFRESH_MS);
649
675
  return async () => {
@@ -652,6 +678,7 @@ async function acquireCacheLock(cacheDir) {
652
678
  };
653
679
  } catch (err) {
654
680
  const code = err.code;
681
+ lastErrorCode = code;
655
682
  if (code === "EEXIST") {
656
683
  if (await tryReclaimStaleLock(lockDir)) {
657
684
  continue;
@@ -660,12 +687,16 @@ async function acquireCacheLock(cacheDir) {
660
687
  continue;
661
688
  }
662
689
  if (code === "ENOENT") {
690
+ await mkdir4(path5.dirname(lockDir), { recursive: true });
691
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS + Math.random() * 100));
663
692
  continue;
664
693
  }
665
694
  throw err;
666
695
  }
667
696
  }
668
- throw new Error(`Timed out waiting for cache lock on ${cacheDir}. Another install may be stuck.`);
697
+ throw new Error(
698
+ 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.`
699
+ );
669
700
  }
670
701
  var DEFAULT_REGISTRY_URL = "https://github.com/agent-kit-startup/agent-kit";
671
702
  var DEFAULT_REGISTRY_REF = "main";
@@ -1851,6 +1882,7 @@ async function resolveInventoryRoot(cwd) {
1851
1882
  return dir;
1852
1883
  } catch {
1853
1884
  }
1885
+ if (await fileExists(path10.join(dir, ".git"))) break;
1854
1886
  const parent = path10.dirname(dir);
1855
1887
  if (parent === dir) break;
1856
1888
  dir = parent;
@@ -2027,12 +2059,16 @@ function baseResult(partial) {
2027
2059
  };
2028
2060
  }
2029
2061
  async function checkCursorUpdateAwareness(cwd, options = {}) {
2030
- const prefs = readCursorUpdateCheckPrefs(await loadContextConfig(cwd));
2062
+ const inventoryRoot = await resolveInventoryRoot(cwd);
2063
+ const prefs = readCursorUpdateCheckPrefs(
2064
+ inventoryRoot ? await loadContextConfig(inventoryRoot) : null
2065
+ );
2031
2066
  const changelogUrl = options.changelogUrl ?? prefs.changelogUrl;
2032
2067
  if (options.respectPrefs) {
2033
2068
  if (!prefs.enabled) {
2034
2069
  return baseResult({
2035
2070
  status: "skipped-disabled",
2071
+ inventoryRoot,
2036
2072
  inventoryPath: INVENTORY_REL,
2037
2073
  featuresPath: FEATURES_REL,
2038
2074
  changelogUrl,
@@ -2047,6 +2083,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2047
2083
  if (!intervalElapsed(prefs.lastCheckedAt, prefs.intervalDays)) {
2048
2084
  return baseResult({
2049
2085
  status: "skipped-interval",
2086
+ inventoryRoot,
2050
2087
  inventoryPath: INVENTORY_REL,
2051
2088
  featuresPath: FEATURES_REL,
2052
2089
  changelogUrl,
@@ -2059,10 +2096,10 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2059
2096
  });
2060
2097
  }
2061
2098
  }
2062
- const inventoryRoot = await resolveInventoryRoot(cwd);
2063
2099
  if (!inventoryRoot) {
2064
2100
  return baseResult({
2065
2101
  status: "error",
2102
+ inventoryRoot: null,
2066
2103
  inventoryPath: INVENTORY_REL,
2067
2104
  featuresPath: FEATURES_REL,
2068
2105
  changelogUrl,
@@ -2082,6 +2119,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2082
2119
  } catch {
2083
2120
  return baseResult({
2084
2121
  status: "error",
2122
+ inventoryRoot,
2085
2123
  inventoryPath: INVENTORY_REL,
2086
2124
  featuresPath: FEATURES_REL,
2087
2125
  changelogUrl,
@@ -2166,6 +2204,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2166
2204
  const msg = err instanceof Error ? err.message : String(err);
2167
2205
  return baseResult({
2168
2206
  status: "error",
2207
+ inventoryRoot,
2169
2208
  inventoryPath: INVENTORY_REL,
2170
2209
  featuresPath: FEATURES_REL,
2171
2210
  changelogUrl,
@@ -2178,8 +2217,8 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2178
2217
  });
2179
2218
  }
2180
2219
  }
2181
- if (options.stamp) {
2182
- await stampCursorUpdateCheck(cwd, {
2220
+ if (options.stamp && inventoryRoot) {
2221
+ await stampCursorUpdateCheck(inventoryRoot, {
2183
2222
  lastSeenCursorVersion: latestCursorVersion ?? prefs.lastSeenCursorVersion
2184
2223
  });
2185
2224
  }
@@ -2187,6 +2226,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2187
2226
  const message = status === "current" ? "No advisory Cursor-update gaps vs inventory (check-only)." : `Found ${gaps.length} advisory gap(s). ${CONVEYOR_HINT}`;
2188
2227
  return baseResult({
2189
2228
  status,
2229
+ inventoryRoot,
2190
2230
  inventoryPath: INVENTORY_REL,
2191
2231
  featuresPath: FEATURES_REL,
2192
2232
  changelogUrl: options.offline ? null : changelogUrl,
@@ -5008,11 +5048,19 @@ async function fileExists2(p) {
5008
5048
  return false;
5009
5049
  }
5010
5050
  }
5051
+ function isMarkdownTableSeparator(line) {
5052
+ const stripped = line.trim();
5053
+ if (!stripped.startsWith("|")) return false;
5054
+ const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
5055
+ return parts.length > 0 && parts.every((cell) => /^:?-+:?$/.test(cell));
5056
+ }
5011
5057
  function parseUnprocessedDogfoodItems(readmeText) {
5012
5058
  const items = [];
5013
5059
  let inSection = false;
5014
5060
  let sectionLevel = 0;
5015
- for (const line of readmeText.split(/\r?\n/)) {
5061
+ const lines = readmeText.split(/\r?\n/);
5062
+ for (let i = 0; i < lines.length; i++) {
5063
+ const line = lines[i] ?? "";
5016
5064
  const unprocessedMatch = /^(#{2,3})\s+Unprocessed Files\b/.exec(line);
5017
5065
  if (unprocessedMatch) {
5018
5066
  const hashes = unprocessedMatch[1];
@@ -5024,11 +5072,14 @@ function parseUnprocessedDogfoodItems(readmeText) {
5024
5072
  if (!inSection) continue;
5025
5073
  const headingMatch = /^(#{1,6})\s+/.exec(line);
5026
5074
  if (headingMatch) {
5027
- if (/\bProcessed Files\b/.test(line)) break;
5075
+ if (/^#{1,6}\s+Processed(?:\s+Files)?\b/.test(line)) break;
5028
5076
  const hashes = headingMatch[1];
5029
5077
  if (hashes && hashes.length <= sectionLevel) break;
5030
5078
  continue;
5031
5079
  }
5080
+ if (line.trim().startsWith("|") && isMarkdownTableSeparator(lines[i + 1] ?? "")) {
5081
+ continue;
5082
+ }
5032
5083
  const body = extractUnprocessedDogfoodLine(line);
5033
5084
  if (!body) continue;
5034
5085
  items.push(body);
@@ -5046,13 +5097,10 @@ function extractUnprocessedDogfoodLine(line) {
5046
5097
  if (numbered?.[2]) {
5047
5098
  raw = numbered[2].trim();
5048
5099
  } else if (stripped.startsWith("|")) {
5100
+ if (isMarkdownTableSeparator(stripped)) return null;
5049
5101
  const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
5050
5102
  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();
5103
+ raw = (parts[0] ?? "").trim();
5056
5104
  }
5057
5105
  }
5058
5106
  if (!raw) return null;
@@ -5818,7 +5866,40 @@ function buildPersonalizationPlan(profile, report, registry) {
5818
5866
  (item) => componentAvailable(registry, item) ? item : { ...item, status: "unavailable" }
5819
5867
  ).sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`));
5820
5868
  }
5821
- function renderProjectContext(profile) {
5869
+ var INSTALLED_SKILL_STATUSES = /* @__PURE__ */ new Set(["applied", "skipped-customized"]);
5870
+ function installedSkillItems(items, installedIds = []) {
5871
+ const rows = [];
5872
+ const seen = /* @__PURE__ */ new Set();
5873
+ for (const item of items) {
5874
+ if (item.kind !== "skill" || !INSTALLED_SKILL_STATUSES.has(item.status)) continue;
5875
+ if (seen.has(item.id)) continue;
5876
+ seen.add(item.id);
5877
+ rows.push(item);
5878
+ }
5879
+ for (const id of installedIds) {
5880
+ if (seen.has(id)) continue;
5881
+ seen.add(id);
5882
+ rows.push({
5883
+ kind: "skill",
5884
+ id,
5885
+ status: "applied",
5886
+ evidence: [{ source: "configuration", value: `.cursor/agent-kit.json skills[]:${id}` }]
5887
+ });
5888
+ }
5889
+ return rows.sort((left, right) => left.id.localeCompare(right.id));
5890
+ }
5891
+ function relevantSkillTableRows(skills) {
5892
+ if (skills.length === 0) {
5893
+ return ["| (none yet) | No installed or project-owned skills detected | \u2014 |"];
5894
+ }
5895
+ return skills.map((skill) => {
5896
+ const label = skill.path ?? skill.id;
5897
+ const role = skill.status === "skipped-customized" ? "Already present (customized)" : "Installed by personalization";
5898
+ const evidence = skill.path ?? skill.evidence[0]?.value ?? "personalization";
5899
+ return `| ${label} | ${role} | ${evidence} |`;
5900
+ });
5901
+ }
5902
+ function renderProjectContext(profile, skillItems = []) {
5822
5903
  const sections = ["# Project Context", "", "Verified repository facts:"];
5823
5904
  const purpose = purposeEvidence(profile);
5824
5905
  if (purpose.length > 0 && profile.purpose.value !== "unknown") {
@@ -5845,7 +5926,7 @@ function renderProjectContext(profile) {
5845
5926
  "",
5846
5927
  "| Skill / path | Role | Evidence |",
5847
5928
  "|--------------|------|----------|",
5848
- "| (none yet) | Add rows when `/agent-kit-onboard` scaffolds domain skills or personalization installs packs | \u2014 |"
5929
+ ...relevantSkillTableRows(installedSkillItems(skillItems))
5849
5930
  );
5850
5931
  if (profile.context.sources.length > 0) {
5851
5932
  sections.push("", "## Sources", ...profile.context.sources.map((item) => `- ${item.value}`));
@@ -5946,7 +6027,7 @@ async function applyPersonalization(input) {
5946
6027
  createOwnedFile(
5947
6028
  input.rootDir,
5948
6029
  CONTEXT_PATH,
5949
- renderProjectContext(input.profile),
6030
+ renderProjectContext(input.profile, installedSkillItems(componentResults, skills)),
5950
6031
  profileEvidence
5951
6032
  ),
5952
6033
  createOwnedFile(
@@ -6269,7 +6350,8 @@ var installCommand = defineCommand11({
6269
6350
  console.error(`
6270
6351
  ${hint.recovery}
6271
6352
  `);
6272
- process.exit(1);
6353
+ process.exitCode = 1;
6354
+ return;
6273
6355
  }
6274
6356
  }
6275
6357
  });
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.0",
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
  }