@dadado/agent-kit-cli 4.8.0 → 4.8.2

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { defineCommand as defineCommand14, runMain } from "citty";
4
+ import { defineCommand as defineCommand18, runMain } from "citty";
5
5
 
6
6
  // src/commands/add.ts
7
7
  import { defineCommand } from "citty";
@@ -1093,18 +1093,34 @@ var L0_ARTIFACTS = [
1093
1093
  source: ".cursor/hooks/pre-commit/check-secrets.sh",
1094
1094
  target: ".cursor/hooks/pre-commit/check-secrets.sh"
1095
1095
  },
1096
- // Native Cursor agent hooks (session context + handoff on compact)
1096
+ // Native Cursor agent hooks (thin adapters -> agent-kit CLI)
1097
1097
  {
1098
1098
  source: ".cursor/hooks.json",
1099
1099
  target: ".cursor/hooks.json"
1100
1100
  },
1101
1101
  {
1102
- source: ".cursor/hooks/agent/session-plan-guard.py",
1103
- target: ".cursor/hooks/agent/session-plan-guard.py"
1102
+ source: ".cursor/hooks/agent/resolve-agent-kit.sh",
1103
+ target: ".cursor/hooks/agent/resolve-agent-kit.sh"
1104
1104
  },
1105
1105
  {
1106
- source: ".cursor/hooks/agent/precompact-handoff.py",
1107
- target: ".cursor/hooks/agent/precompact-handoff.py"
1106
+ source: ".cursor/hooks/agent/session-start.sh",
1107
+ target: ".cursor/hooks/agent/session-start.sh"
1108
+ },
1109
+ {
1110
+ source: ".cursor/hooks/agent/pre-compact.sh",
1111
+ target: ".cursor/hooks/agent/pre-compact.sh"
1112
+ },
1113
+ {
1114
+ source: ".cursor/hooks/agent/guard-shell.sh",
1115
+ target: ".cursor/hooks/agent/guard-shell.sh"
1116
+ },
1117
+ {
1118
+ source: ".cursor/hooks/agent/after-edit-schema.sh",
1119
+ target: ".cursor/hooks/agent/after-edit-schema.sh"
1120
+ },
1121
+ {
1122
+ source: ".cursor/hooks/agent/secrets-prompt.sh",
1123
+ target: ".cursor/hooks/agent/secrets-prompt.sh"
1108
1124
  },
1109
1125
  // Git spine docs at project root (not a nested agent-kit/ copy)
1110
1126
  {
@@ -1397,11 +1413,33 @@ import path10 from "path";
1397
1413
  import { defineCommand as defineCommand4 } from "citty";
1398
1414
 
1399
1415
  // src/commands/dashboard.ts
1400
- import { spawn } from "child_process";
1416
+ import { execFileSync, spawn } from "child_process";
1401
1417
  import { access as access2 } from "fs/promises";
1402
1418
  import path9 from "path";
1419
+ import { fileURLToPath } from "url";
1403
1420
  import { defineCommand as defineCommand3 } from "citty";
1404
- async function findDashboardStart(cwd) {
1421
+ function bundledDashboardCandidates(filename, moduleUrl = import.meta.url) {
1422
+ const here = path9.dirname(fileURLToPath(moduleUrl));
1423
+ return [
1424
+ // Published / built: <pkg>/dist/index.js -> <pkg>/dashboard/
1425
+ path9.join(here, "..", "dashboard", filename),
1426
+ // Dev source: packages/cli/src/commands/*.ts -> packages/cli/dashboard/ (sync copy)
1427
+ path9.join(here, "..", "..", "dashboard", filename),
1428
+ // From src/commands: packages/dashboard/ (unused layout); from dist: two levels above package
1429
+ path9.join(here, "..", "..", "..", "dashboard", filename)
1430
+ ];
1431
+ }
1432
+ async function firstExisting(candidates) {
1433
+ for (const candidate2 of candidates) {
1434
+ try {
1435
+ await access2(candidate2);
1436
+ return path9.resolve(candidate2);
1437
+ } catch {
1438
+ }
1439
+ }
1440
+ return null;
1441
+ }
1442
+ async function findDashboardStart(cwd, env = process.env, options = {}) {
1405
1443
  let dir = path9.resolve(cwd);
1406
1444
  for (; ; ) {
1407
1445
  const candidate2 = path9.join(dir, "dashboard", "start.mjs");
@@ -1411,9 +1449,40 @@ async function findDashboardStart(cwd) {
1411
1449
  } catch {
1412
1450
  }
1413
1451
  const parent = path9.dirname(dir);
1414
- if (parent === dir) return null;
1452
+ if (parent === dir) break;
1415
1453
  dir = parent;
1416
1454
  }
1455
+ for (const key of ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"]) {
1456
+ const base = env[key];
1457
+ if (!base || typeof base !== "string" || !base.trim()) continue;
1458
+ const candidate2 = path9.join(path9.resolve(base.trim()), "dashboard", "start.mjs");
1459
+ try {
1460
+ await access2(candidate2);
1461
+ return candidate2;
1462
+ } catch {
1463
+ }
1464
+ }
1465
+ const sibling = path9.join(path9.resolve(cwd), "..", "agent-kit", "dashboard", "start.mjs");
1466
+ try {
1467
+ await access2(sibling);
1468
+ return path9.resolve(sibling);
1469
+ } catch {
1470
+ }
1471
+ return firstExisting(
1472
+ bundledDashboardCandidates("start.mjs", options.moduleUrl ?? import.meta.url)
1473
+ );
1474
+ }
1475
+ function resolveDashboardSnapshotRoot(cwd) {
1476
+ const abs = path9.resolve(cwd);
1477
+ try {
1478
+ const top = execFileSync("git", ["-C", abs, "rev-parse", "--show-toplevel"], {
1479
+ encoding: "utf8",
1480
+ timeout: 5e3
1481
+ }).trim();
1482
+ if (top) return path9.resolve(top);
1483
+ } catch {
1484
+ }
1485
+ return abs;
1417
1486
  }
1418
1487
  function runStartScript(startPath, env) {
1419
1488
  return new Promise((resolve, reject) => {
@@ -1429,13 +1498,13 @@ function runStartScript(startPath, env) {
1429
1498
  var dashboardCommand = defineCommand3({
1430
1499
  meta: {
1431
1500
  name: "dashboard",
1432
- description: "Start Mission Control if needed and open http://localhost:3333 (terminal counterpart to /dashboard)."
1501
+ description: "Start Mission Control for this workspace (stable per-root port) and open the panel URL."
1433
1502
  },
1434
1503
  args: {
1435
1504
  cwd: {
1436
1505
  type: "string",
1437
1506
  default: process.cwd(),
1438
- description: "Directory to search upward for dashboard/start.mjs"
1507
+ description: "Workspace to snapshot (git root preferred); also searched upward for dashboard/start.mjs"
1439
1508
  },
1440
1509
  "no-open": {
1441
1510
  type: "boolean",
@@ -1444,15 +1513,17 @@ var dashboardCommand = defineCommand3({
1444
1513
  }
1445
1514
  },
1446
1515
  async run({ args }) {
1516
+ const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd);
1447
1517
  const startPath = await findDashboardStart(args.cwd);
1448
1518
  if (!startPath) {
1449
1519
  logger.error(
1450
- "No dashboard/start.mjs found. Mission Control ships with the agent-kit repo; run from that tree, or use npm run dashboard there."
1520
+ "No dashboard/start.mjs found. After a CLI publish that ships dashboard/ (Path C), reinstall @dadado/agent-kit-cli. Or set MISSION_CONTROL_KIT_ROOT / AGENT_KIT_HOME to an agent-kit checkout, place a sibling ../agent-kit tree, or run from that kit tree."
1451
1521
  );
1452
1522
  process.exitCode = 1;
1453
1523
  return;
1454
1524
  }
1455
1525
  const env = { ...process.env };
1526
+ env.MISSION_CONTROL_REPO_ROOT = snapshotRoot;
1456
1527
  if (args["no-open"]) env.MISSION_CONTROL_NO_OPEN = "1";
1457
1528
  const code = await runStartScript(startPath, env);
1458
1529
  if (code !== 0) process.exitCode = code;
@@ -1460,7 +1531,7 @@ var dashboardCommand = defineCommand3({
1460
1531
  });
1461
1532
 
1462
1533
  // src/commands/dashboard-broadcast.ts
1463
- async function findDashboardBroadcastStart(cwd) {
1534
+ async function findDashboardBroadcastStart(cwd, env = process.env, options = {}) {
1464
1535
  let dir = path10.resolve(cwd);
1465
1536
  for (; ; ) {
1466
1537
  const candidate2 = path10.join(dir, "dashboard", "start-broadcast.mjs");
@@ -1470,9 +1541,42 @@ async function findDashboardBroadcastStart(cwd) {
1470
1541
  } catch {
1471
1542
  }
1472
1543
  const parent = path10.dirname(dir);
1473
- if (parent === dir) return null;
1544
+ if (parent === dir) break;
1474
1545
  dir = parent;
1475
1546
  }
1547
+ for (const key of ["MISSION_CONTROL_KIT_ROOT", "AGENT_KIT_HOME"]) {
1548
+ const base = env[key];
1549
+ if (!base || typeof base !== "string" || !base.trim()) continue;
1550
+ const candidate2 = path10.join(path10.resolve(base.trim()), "dashboard", "start-broadcast.mjs");
1551
+ try {
1552
+ await access3(candidate2);
1553
+ return candidate2;
1554
+ } catch {
1555
+ }
1556
+ }
1557
+ const sibling = path10.join(
1558
+ path10.resolve(cwd),
1559
+ "..",
1560
+ "agent-kit",
1561
+ "dashboard",
1562
+ "start-broadcast.mjs"
1563
+ );
1564
+ try {
1565
+ await access3(sibling);
1566
+ return path10.resolve(sibling);
1567
+ } catch {
1568
+ }
1569
+ for (const candidate2 of bundledDashboardCandidates(
1570
+ "start-broadcast.mjs",
1571
+ options.moduleUrl ?? import.meta.url
1572
+ )) {
1573
+ try {
1574
+ await access3(candidate2);
1575
+ return path10.resolve(candidate2);
1576
+ } catch {
1577
+ }
1578
+ }
1579
+ return null;
1476
1580
  }
1477
1581
  function runStartScript2(startPath, env) {
1478
1582
  return new Promise((resolve, reject) => {
@@ -1507,7 +1611,7 @@ var dashboardBroadcastCommand = defineCommand4({
1507
1611
  if (!startPath) {
1508
1612
  const loopback = await findDashboardStart(args.cwd);
1509
1613
  logger.error(
1510
- loopback ? "No dashboard/start-broadcast.mjs found beside dashboard/start.mjs. Update the agent-kit tree, then retry." : "No dashboard/start-broadcast.mjs found. Mission Control ships with the agent-kit repo; run from that tree."
1614
+ loopback ? "No dashboard/start-broadcast.mjs found beside dashboard/start.mjs. Update the agent-kit tree or reinstall a CLI that ships dashboard/, then retry." : "No dashboard/start-broadcast.mjs found. After a CLI publish that ships dashboard/ (Path C), reinstall @dadado/agent-kit-cli. Or run from an agent-kit tree that includes dashboard/."
1511
1615
  );
1512
1616
  process.exitCode = 1;
1513
1617
  return;
@@ -1659,9 +1763,87 @@ var diffCommand = defineCommand5({
1659
1763
  });
1660
1764
 
1661
1765
  // src/commands/doctor.ts
1662
- import path21 from "path";
1766
+ import path22 from "path";
1663
1767
  import { defineCommand as defineCommand6 } from "citty";
1664
1768
 
1769
+ // src/invariants/hooks-health.ts
1770
+ import { access as access4, readFile as readFile6 } from "fs/promises";
1771
+ import path12 from "path";
1772
+ var EXPECTED_EVENTS = [
1773
+ "sessionStart",
1774
+ "preCompact",
1775
+ "beforeShellExecution",
1776
+ "afterFileEdit",
1777
+ "beforeSubmitPrompt"
1778
+ ];
1779
+ async function exists(p) {
1780
+ try {
1781
+ await access4(p);
1782
+ return true;
1783
+ } catch {
1784
+ return false;
1785
+ }
1786
+ }
1787
+ async function assessHooksHealth(rootDir) {
1788
+ const root = path12.resolve(rootDir);
1789
+ const hooksJsonPath = ".cursor/hooks.json";
1790
+ const hooksJsonAbs = path12.join(root, hooksJsonPath);
1791
+ const reasons = [];
1792
+ const wiredEvents = [];
1793
+ if (!await exists(hooksJsonAbs)) {
1794
+ return {
1795
+ status: "missing",
1796
+ reasons: ["`.cursor/hooks.json` not found"],
1797
+ hooksJsonPath,
1798
+ expectedEvents: [...EXPECTED_EVENTS],
1799
+ wiredEvents
1800
+ };
1801
+ }
1802
+ let parsed;
1803
+ try {
1804
+ parsed = JSON.parse(await readFile6(hooksJsonAbs, "utf8"));
1805
+ } catch {
1806
+ return {
1807
+ status: "degraded",
1808
+ reasons: ["`.cursor/hooks.json` is not valid JSON"],
1809
+ hooksJsonPath,
1810
+ expectedEvents: [...EXPECTED_EVENTS],
1811
+ wiredEvents
1812
+ };
1813
+ }
1814
+ const hooks = parsed.hooks ?? {};
1815
+ for (const event of EXPECTED_EVENTS) {
1816
+ const list = hooks[event];
1817
+ if (Array.isArray(list) && list.length > 0) {
1818
+ wiredEvents.push(event);
1819
+ for (const entry of list) {
1820
+ if (!entry || typeof entry !== "object") continue;
1821
+ const command = String(entry.command ?? "");
1822
+ if (command.endsWith(".py") || command.includes("python")) {
1823
+ reasons.push(`${event} still points at a Python script (${command})`);
1824
+ }
1825
+ }
1826
+ } else {
1827
+ reasons.push(`missing hook event: ${event}`);
1828
+ }
1829
+ }
1830
+ const resolveLib = path12.join(root, ".cursor", "hooks", "agent", "resolve-agent-kit.sh");
1831
+ if (!await exists(resolveLib)) {
1832
+ reasons.push("missing `.cursor/hooks/agent/resolve-agent-kit.sh` (thin adapter resolver)");
1833
+ }
1834
+ if (Array.isArray(hooks.stop) && hooks.stop.length > 0) {
1835
+ reasons.push("`stop` hook is registered (forbidden; remove it)");
1836
+ }
1837
+ const status = reasons.length === 0 && wiredEvents.length === EXPECTED_EVENTS.length ? "active" : "degraded";
1838
+ return {
1839
+ status,
1840
+ reasons,
1841
+ hooksJsonPath,
1842
+ expectedEvents: [...EXPECTED_EVENTS],
1843
+ wiredEvents
1844
+ };
1845
+ }
1846
+
1665
1847
  // src/scanner/readiness.ts
1666
1848
  import { createHash as createHash2 } from "crypto";
1667
1849
  function action(id, status, recommendation, owner) {
@@ -1909,12 +2091,12 @@ function createReadinessReport(scan, options) {
1909
2091
  }
1910
2092
 
1911
2093
  // src/scanner/safe-fixes.ts
1912
- import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
1913
- import path19 from "path";
2094
+ import { readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
2095
+ import path20 from "path";
1914
2096
 
1915
2097
  // src/scanner/detect-repository.ts
1916
- import { readFile as readFile6 } from "fs/promises";
1917
- import path12 from "path";
2098
+ import { readFile as readFile7 } from "fs/promises";
2099
+ import path13 from "path";
1918
2100
  var CONTEXT_PATHS = [
1919
2101
  ["README.md", "README"],
1920
2102
  ["README", "README"],
@@ -1933,7 +2115,7 @@ var CONTEXT_PATHS = [
1933
2115
  async function existingEvidence(rootDir, candidates) {
1934
2116
  const evidence = await Promise.all(
1935
2117
  candidates.map(
1936
- async ([relativePath, label]) => await fileExists(path12.join(rootDir, relativePath)) ? { source: "file", value: `${relativePath}:${label}` } : void 0
2118
+ async ([relativePath, label]) => await fileExists(path13.join(rootDir, relativePath)) ? { source: "file", value: `${relativePath}:${label}` } : void 0
1937
2119
  )
1938
2120
  );
1939
2121
  return evidence.flatMap((item) => item ? [item] : []);
@@ -1954,7 +2136,7 @@ async function detectContext(rootDir) {
1954
2136
  async function detectPurpose(rootDir, stack) {
1955
2137
  const entries = await listDirectory(rootDir);
1956
2138
  const lowerEntries = entries.map((entry) => entry.toLowerCase());
1957
- const packageJson = await readJson(path12.join(rootDir, "package.json"));
2139
+ const packageJson = await readJson(path13.join(rootDir, "package.json"));
1958
2140
  const categories = [];
1959
2141
  const evidence = [];
1960
2142
  const add = (category, value2) => {
@@ -1994,16 +2176,16 @@ async function detectPurpose(rootDir, stack) {
1994
2176
  }
1995
2177
  async function detectAgentKit(rootDir) {
1996
2178
  const manifestRelativePath = ".cursor/agent-kit.json";
1997
- const manifestPath = path12.join(rootDir, manifestRelativePath);
2179
+ const manifestPath = path13.join(rootDir, manifestRelativePath);
1998
2180
  const installed = await fileExists(manifestPath);
1999
2181
  const manifest = installed ? await readJson(manifestPath) : null;
2000
2182
  return {
2001
2183
  installed,
2002
2184
  manifestPath: installed ? manifestRelativePath : void 0,
2003
2185
  version: manifest?.version,
2004
- hasPlans: await fileExists(path12.join(rootDir, ".cursor/plans")),
2005
- hasHandoff: await fileExists(path12.join(rootDir, ".cursor/HANDOFF.md")),
2006
- hasMemory: await fileExists(path12.join(rootDir, ".cursor/memory"))
2186
+ hasPlans: await fileExists(path13.join(rootDir, ".cursor/plans")),
2187
+ hasHandoff: await fileExists(path13.join(rootDir, ".cursor/HANDOFF.md")),
2188
+ hasMemory: await fileExists(path13.join(rootDir, ".cursor/memory"))
2007
2189
  };
2008
2190
  }
2009
2191
  var REQUIRED_SECRET_PATTERNS = [
@@ -2017,9 +2199,9 @@ var REQUIRED_SECRET_PATTERNS = [
2017
2199
  "*service-account*.json"
2018
2200
  ];
2019
2201
  async function detectSafety(rootDir, trackedFiles) {
2020
- const gitignorePath = path12.join(rootDir, ".gitignore");
2202
+ const gitignorePath = path13.join(rootDir, ".gitignore");
2021
2203
  const hasGitignore = await fileExists(gitignorePath);
2022
- const gitignore = hasGitignore ? await readFile6(gitignorePath, "utf8") : "";
2204
+ const gitignore = hasGitignore ? await readFile7(gitignorePath, "utf8") : "";
2023
2205
  const lines = gitignore.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
2024
2206
  const ignoredSecretPatterns = REQUIRED_SECRET_PATTERNS.filter(
2025
2207
  (pattern) => lines.includes(pattern)
@@ -2028,7 +2210,7 @@ async function detectSafety(rootDir, trackedFiles) {
2028
2210
  (file) => /(^|\/)(\.env(\..+)?|.*\.(key|pem|p12|pfx)|.*credentials.*\.json)$/i.test(file)
2029
2211
  );
2030
2212
  const hookPaths = [".husky", ".git/hooks/pre-commit", "git-hooks/pre-commit"];
2031
- const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(path12.join(rootDir, item))))).some(Boolean);
2213
+ const hasHooks = (await Promise.all(hookPaths.map((item) => fileExists(path13.join(rootDir, item))))).some(Boolean);
2032
2214
  const guardCandidates = [
2033
2215
  ".husky/pre-commit",
2034
2216
  ".husky/pre-push",
@@ -2037,7 +2219,7 @@ async function detectSafety(rootDir, trackedFiles) {
2037
2219
  ];
2038
2220
  const guardContents = await Promise.all(
2039
2221
  guardCandidates.map(
2040
- async (item) => await fileExists(path12.join(rootDir, item)) ? readFile6(path12.join(rootDir, item), "utf8") : ""
2222
+ async (item) => await fileExists(path13.join(rootDir, item)) ? readFile7(path13.join(rootDir, item), "utf8") : ""
2041
2223
  )
2042
2224
  );
2043
2225
  return {
@@ -2055,11 +2237,11 @@ async function detectSafety(rootDir, trackedFiles) {
2055
2237
  }
2056
2238
 
2057
2239
  // src/scanner/scan.ts
2058
- import path18 from "path";
2240
+ import path19 from "path";
2059
2241
 
2060
2242
  // src/scanner/detect-git.ts
2061
2243
  import { execFile as execFile2 } from "child_process";
2062
- import path13 from "path";
2244
+ import path14 from "path";
2063
2245
  import { promisify as promisify2 } from "util";
2064
2246
  var exec = promisify2(execFile2);
2065
2247
  function remoteHostname(remoteUrl) {
@@ -2083,7 +2265,7 @@ function sanitizeRemoteUrl(remoteUrl) {
2083
2265
  }
2084
2266
  async function detectProvider(rootDir, remoteUrl) {
2085
2267
  const configuration = await readJson(
2086
- path13.join(rootDir, ".cursor", "agent-kit.config.json")
2268
+ path14.join(rootDir, ".cursor", "agent-kit.config.json")
2087
2269
  );
2088
2270
  const configuredProvider = configuration?.git?.provider;
2089
2271
  if (configuredProvider) {
@@ -2140,7 +2322,7 @@ async function detectProvider(rootDir, remoteUrl) {
2140
2322
  evidence: remoteEvidence
2141
2323
  };
2142
2324
  }
2143
- if (await fileExists(path13.join(rootDir, ".gitlab-ci.yml"))) {
2325
+ if (await fileExists(path14.join(rootDir, ".gitlab-ci.yml"))) {
2144
2326
  return {
2145
2327
  provider: "gitlab",
2146
2328
  providerKind: "gitlab-self-hosted",
@@ -2229,11 +2411,11 @@ async function detectGit(rootDir) {
2229
2411
  }
2230
2412
 
2231
2413
  // src/scanner/detect-ide.ts
2232
- import path14 from "path";
2414
+ import path15 from "path";
2233
2415
  async function detectIde(rootDir) {
2234
- const hasCursor = await fileExists(path14.join(rootDir, ".cursor"));
2235
- const hasVSCode = await fileExists(path14.join(rootDir, ".vscode"));
2236
- const hasWindsurf = await fileExists(path14.join(rootDir, ".windsurfrules"));
2416
+ const hasCursor = await fileExists(path15.join(rootDir, ".cursor"));
2417
+ const hasVSCode = await fileExists(path15.join(rootDir, ".vscode"));
2418
+ const hasWindsurf = await fileExists(path15.join(rootDir, ".windsurfrules"));
2237
2419
  if (hasCursor) return { ide: "cursor", plan: "cursor-pro" };
2238
2420
  if (hasVSCode) return { ide: "vscode", plan: "vscode-pro" };
2239
2421
  if (hasWindsurf) return { ide: "windsurf", plan: "windsurf" };
@@ -2241,7 +2423,7 @@ async function detectIde(rootDir) {
2241
2423
  }
2242
2424
 
2243
2425
  // src/scanner/detect-infra.ts
2244
- import path15 from "path";
2426
+ import path16 from "path";
2245
2427
 
2246
2428
  // src/types.ts
2247
2429
  var CI_PLATFORM_FILES = {
@@ -2270,12 +2452,12 @@ var PM_TOOL_LABELS = {
2270
2452
 
2271
2453
  // src/scanner/detect-infra.ts
2272
2454
  async function detectInfra(rootDir) {
2273
- const docker = await fileExists(path15.join(rootDir, "Dockerfile")) || await fileExists(path15.join(rootDir, "docker-compose.yml")) || await fileExists(path15.join(rootDir, "docker-compose.yaml"));
2274
- const kubernetes = await fileExists(path15.join(rootDir, "k8s")) || await fileExists(path15.join(rootDir, "kubernetes"));
2455
+ const docker = await fileExists(path16.join(rootDir, "Dockerfile")) || await fileExists(path16.join(rootDir, "docker-compose.yml")) || await fileExists(path16.join(rootDir, "docker-compose.yaml"));
2456
+ const kubernetes = await fileExists(path16.join(rootDir, "k8s")) || await fileExists(path16.join(rootDir, "kubernetes"));
2275
2457
  let ci = "none";
2276
2458
  const ciFiles = [];
2277
2459
  for (const [platform, filePath] of Object.entries(CI_PLATFORM_FILES)) {
2278
- if (await fileExists(path15.join(rootDir, filePath))) {
2460
+ if (await fileExists(path16.join(rootDir, filePath))) {
2279
2461
  if (ci === "none") ci = platform;
2280
2462
  ciFiles.push(filePath);
2281
2463
  }
@@ -2300,30 +2482,30 @@ async function detectInfra(rootDir) {
2300
2482
  ];
2301
2483
  const infrastructureFiles = (await Promise.all(
2302
2484
  infrastructureCandidates.map(
2303
- async (file) => await fileExists(path15.join(rootDir, file)) ? file : void 0
2485
+ async (file) => await fileExists(path16.join(rootDir, file)) ? file : void 0
2304
2486
  )
2305
2487
  )).filter((file) => file !== void 0);
2306
2488
  const deploymentFiles = (await Promise.all(
2307
2489
  deploymentCandidates.map(
2308
- async (file) => await fileExists(path15.join(rootDir, file)) ? file : void 0
2490
+ async (file) => await fileExists(path16.join(rootDir, file)) ? file : void 0
2309
2491
  )
2310
2492
  )).filter((file) => file !== void 0);
2311
2493
  return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles };
2312
2494
  }
2313
2495
 
2314
2496
  // src/scanner/detect-services.ts
2315
- import { readFile as readFile7 } from "fs/promises";
2316
- import path16 from "path";
2497
+ import { readFile as readFile8 } from "fs/promises";
2498
+ import path17 from "path";
2317
2499
  async function detectProjectManagement(rootDir) {
2318
2500
  const tools = [];
2319
2501
  const mcpConfigPaths = [
2320
- path16.join(rootDir, ".cursor", "mcp.json"),
2321
- path16.join(rootDir, "mcp.json")
2502
+ path17.join(rootDir, ".cursor", "mcp.json"),
2503
+ path17.join(rootDir, "mcp.json")
2322
2504
  ];
2323
2505
  for (const configPath of mcpConfigPaths) {
2324
2506
  if (!await fileExists(configPath)) continue;
2325
2507
  try {
2326
- const raw = await readFile7(configPath, "utf8");
2508
+ const raw = await readFile8(configPath, "utf8");
2327
2509
  const lower = raw.toLowerCase();
2328
2510
  if (lower.includes("clickup")) tools.push("clickup");
2329
2511
  if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
@@ -2334,20 +2516,20 @@ async function detectProjectManagement(rootDir) {
2334
2516
  } catch {
2335
2517
  }
2336
2518
  }
2337
- if (await fileExists(path16.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
2519
+ if (await fileExists(path17.join(rootDir, ".github", "ISSUE_TEMPLATE"))) {
2338
2520
  tools.push("github-issues");
2339
2521
  }
2340
- if (await fileExists(path16.join(rootDir, ".github", "projects"))) {
2522
+ if (await fileExists(path17.join(rootDir, ".github", "projects"))) {
2341
2523
  tools.push("github-projects");
2342
2524
  }
2343
2525
  return [...new Set(tools)];
2344
2526
  }
2345
2527
  async function detectServices(rootDir) {
2346
- const hasPrisma = await fileExists(path16.join(rootDir, "prisma/schema.prisma"));
2347
- const hasSequelize = await fileExists(path16.join(rootDir, "sequelize"));
2348
- const hasDrizzle = await fileExists(path16.join(rootDir, "drizzle.config.ts"));
2349
- const hasKnex = await fileExists(path16.join(rootDir, "knexfile.ts"));
2350
- const hasTypeorm = await fileExists(path16.join(rootDir, "ormconfig.json"));
2528
+ const hasPrisma = await fileExists(path17.join(rootDir, "prisma/schema.prisma"));
2529
+ const hasSequelize = await fileExists(path17.join(rootDir, "sequelize"));
2530
+ const hasDrizzle = await fileExists(path17.join(rootDir, "drizzle.config.ts"));
2531
+ const hasKnex = await fileExists(path17.join(rootDir, "knexfile.ts"));
2532
+ const hasTypeorm = await fileExists(path17.join(rootDir, "ormconfig.json"));
2351
2533
  const database = hasPrisma || hasSequelize || hasDrizzle || hasKnex || hasTypeorm ? "postgresql" : void 0;
2352
2534
  const orm = hasPrisma ? "prisma" : hasDrizzle ? "drizzle" : hasSequelize ? "sequelize" : hasKnex ? "knex" : hasTypeorm ? "typeorm" : void 0;
2353
2535
  const projectManagement = await detectProjectManagement(rootDir);
@@ -2359,7 +2541,7 @@ async function detectServices(rootDir) {
2359
2541
  }
2360
2542
 
2361
2543
  // src/scanner/detect-stack.ts
2362
- import path17 from "path";
2544
+ import path18 from "path";
2363
2545
  var PROJECT_MARKERS = [
2364
2546
  "package.json",
2365
2547
  "requirements.txt",
@@ -2388,7 +2570,7 @@ async function detectPackageManager(rootDir, packageJson) {
2388
2570
  };
2389
2571
  }
2390
2572
  for (const [lockfile, packageManager] of LOCKFILES) {
2391
- if (await fileExists(path17.join(rootDir, lockfile))) {
2573
+ if (await fileExists(path18.join(rootDir, lockfile))) {
2392
2574
  return {
2393
2575
  packageManager,
2394
2576
  evidence: [{ source: "file", value: lockfile }]
@@ -2405,27 +2587,27 @@ function commandsForScripts(scripts, packageManager) {
2405
2587
  return { testCommands, validationCommands };
2406
2588
  }
2407
2589
  async function detectStack(rootDir) {
2408
- const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path17.join(rootDir, item))))).some(Boolean);
2409
- const hasPackageJson = await fileExists(path17.join(rootDir, "package.json"));
2590
+ const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path18.join(rootDir, item))))).some(Boolean);
2591
+ const hasPackageJson = await fileExists(path18.join(rootDir, "package.json"));
2410
2592
  if (hasPackageJson) {
2411
- const packageJson = await readJson(path17.join(rootDir, "package.json")) ?? {};
2593
+ const packageJson = await readJson(path18.join(rootDir, "package.json")) ?? {};
2412
2594
  const scripts = packageJson.scripts ?? {};
2413
2595
  const packageManager = await detectPackageManager(rootDir, packageJson);
2414
2596
  const commands = commandsForScripts(scripts, packageManager.packageManager);
2415
- const hasNextConfig = await fileExists(path17.join(rootDir, "next.config.js")) || await fileExists(path17.join(rootDir, "next.config.mjs")) || await fileExists(path17.join(rootDir, "next.config.ts"));
2416
- const hasNestConfig = await fileExists(path17.join(rootDir, "nest-cli.json"));
2597
+ const hasNextConfig = await fileExists(path18.join(rootDir, "next.config.js")) || await fileExists(path18.join(rootDir, "next.config.mjs")) || await fileExists(path18.join(rootDir, "next.config.ts"));
2598
+ const hasNestConfig = await fileExists(path18.join(rootDir, "nest-cli.json"));
2417
2599
  return {
2418
2600
  language: "node",
2419
2601
  framework: hasNextConfig ? "nextjs" : hasNestConfig ? "nestjs" : "node",
2420
2602
  packageManager: packageManager.packageManager,
2421
2603
  packageManagerEvidence: packageManager.evidence,
2422
2604
  scripts,
2423
- workspaces: packageJson.workspaces !== void 0 || await fileExists(path17.join(rootDir, "pnpm-workspace.yaml")),
2605
+ workspaces: packageJson.workspaces !== void 0 || await fileExists(path18.join(rootDir, "pnpm-workspace.yaml")),
2424
2606
  ...commands,
2425
2607
  hasProjectFiles: hasAnyProjectMarker
2426
2608
  };
2427
2609
  }
2428
- if (await fileExists(path17.join(rootDir, "pyproject.toml"))) {
2610
+ if (await fileExists(path18.join(rootDir, "pyproject.toml"))) {
2429
2611
  return {
2430
2612
  language: "python",
2431
2613
  framework: "python",
@@ -2435,7 +2617,7 @@ async function detectStack(rootDir) {
2435
2617
  hasProjectFiles: hasAnyProjectMarker
2436
2618
  };
2437
2619
  }
2438
- if (await fileExists(path17.join(rootDir, "go.mod"))) {
2620
+ if (await fileExists(path18.join(rootDir, "go.mod"))) {
2439
2621
  return {
2440
2622
  language: "go",
2441
2623
  framework: "go",
@@ -2445,7 +2627,7 @@ async function detectStack(rootDir) {
2445
2627
  hasProjectFiles: hasAnyProjectMarker
2446
2628
  };
2447
2629
  }
2448
- if (await fileExists(path17.join(rootDir, "Cargo.toml"))) {
2630
+ if (await fileExists(path18.join(rootDir, "Cargo.toml"))) {
2449
2631
  return {
2450
2632
  language: "rust",
2451
2633
  framework: "rust",
@@ -2455,7 +2637,7 @@ async function detectStack(rootDir) {
2455
2637
  hasProjectFiles: hasAnyProjectMarker
2456
2638
  };
2457
2639
  }
2458
- if (await fileExists(path17.join(rootDir, "composer.json"))) {
2640
+ if (await fileExists(path18.join(rootDir, "composer.json"))) {
2459
2641
  return {
2460
2642
  language: "php",
2461
2643
  framework: "php",
@@ -2488,7 +2670,7 @@ function isGreenfieldByEntries(entries) {
2488
2670
  return meaningful.length === 0;
2489
2671
  }
2490
2672
  async function runScanner(rootDir) {
2491
- const normalizedRoot = path18.resolve(rootDir);
2673
+ const normalizedRoot = path19.resolve(rootDir);
2492
2674
  const entries = await listDirectory(normalizedRoot);
2493
2675
  const stack = await detectStack(normalizedRoot);
2494
2676
  const purpose = await detectPurpose(normalizedRoot, stack);
@@ -2692,21 +2874,21 @@ async function executeSafeReadinessFixes(rootDir, options) {
2692
2874
  });
2693
2875
  const changes = [];
2694
2876
  for (const relativePath of ESSENTIAL_DIRECTORIES) {
2695
- const absolutePath = path19.join(beforeScan.rootDir, relativePath);
2696
- const exists = await fileExists(absolutePath);
2697
- if (!exists && !dryRun) await ensureDir(absolutePath);
2877
+ const absolutePath = path20.join(beforeScan.rootDir, relativePath);
2878
+ const exists2 = await fileExists(absolutePath);
2879
+ if (!exists2 && !dryRun) await ensureDir(absolutePath);
2698
2880
  recordChange(
2699
2881
  changes,
2700
2882
  "ensure-agent-kit-directory",
2701
2883
  relativePath,
2702
- !exists,
2884
+ !exists2,
2703
2885
  dryRun,
2704
- relativeEvidence(relativePath, exists ? "already exists" : "missing directory")
2886
+ relativeEvidence(relativePath, exists2 ? "already exists" : "missing directory")
2705
2887
  );
2706
2888
  }
2707
2889
  const gitignoreRelativePath = ".gitignore";
2708
- const gitignorePath = path19.join(beforeScan.rootDir, gitignoreRelativePath);
2709
- const existingGitignore = await fileExists(gitignorePath) ? await readFile8(gitignorePath, "utf8") : "";
2890
+ const gitignorePath = path20.join(beforeScan.rootDir, gitignoreRelativePath);
2891
+ const existingGitignore = await fileExists(gitignorePath) ? await readFile9(gitignorePath, "utf8") : "";
2710
2892
  const mergedGitignore = mergeSecretIgnores(existingGitignore);
2711
2893
  const gitignoreChanged = mergedGitignore !== existingGitignore;
2712
2894
  if (gitignoreChanged && !dryRun) await writeFile3(gitignorePath, mergedGitignore, "utf8");
@@ -2721,7 +2903,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
2721
2903
  gitignoreChanged ? "required secret patterns are missing" : "required patterns are present"
2722
2904
  )
2723
2905
  );
2724
- const profilePath = path19.join(beforeScan.rootDir, PROFILE_RELATIVE_PATH);
2906
+ const profilePath = path20.join(beforeScan.rootDir, PROFILE_RELATIVE_PATH);
2725
2907
  const existingProfile = await readJson(profilePath) ?? {};
2726
2908
  const desiredProfile = createProfile(beforeScan, before, generatedAt);
2727
2909
  const mergedProfile = mergeMissing(existingProfile, desiredProfile);
@@ -2743,7 +2925,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
2743
2925
  generatorVersion: options.generatorVersion,
2744
2926
  generatedAt
2745
2927
  });
2746
- const contextConfigPath = path19.join(beforeScan.rootDir, CONTEXT_CONFIG_RELATIVE_PATH);
2928
+ const contextConfigPath = path20.join(beforeScan.rootDir, CONTEXT_CONFIG_RELATIVE_PATH);
2747
2929
  const existingContextConfig = await readJson(contextConfigPath) ?? {};
2748
2930
  const onboarding = reconcileOnboardingState(evidenceReport, existingContextConfig, generatedAt);
2749
2931
  const defaults = preferenceDefaults(onboarding, existingContextConfig.onboarded);
@@ -2772,24 +2954,25 @@ async function executeSafeReadinessFixes(rootDir, options) {
2772
2954
  }
2773
2955
 
2774
2956
  // src/scanner/snapshot.ts
2775
- import path20 from "path";
2957
+ import path21 from "path";
2776
2958
  var READINESS_SNAPSHOT_RELATIVE_PATH = ".cursor/context/readiness.json";
2777
2959
  async function writeReadinessSnapshot(rootDir, report) {
2778
- const snapshotPath = path20.join(rootDir, READINESS_SNAPSHOT_RELATIVE_PATH);
2960
+ const snapshotPath = path21.join(rootDir, READINESS_SNAPSHOT_RELATIVE_PATH);
2779
2961
  await writeJson(snapshotPath, report);
2780
2962
  return snapshotPath;
2781
2963
  }
2782
2964
 
2783
2965
  // src/commands/doctor.ts
2784
2966
  async function runDoctor(cwd, options = {}) {
2785
- const rootDir = path21.resolve(cwd);
2967
+ const rootDir = path22.resolve(cwd);
2968
+ const hooks = await assessHooksHealth(rootDir);
2786
2969
  if (options.fixSafe) {
2787
2970
  const execution = await executeSafeReadinessFixes(rootDir, {
2788
2971
  generatorVersion: KIT_VERSION,
2789
2972
  generatedAt: options.generatedAt
2790
2973
  });
2791
2974
  await writeReadinessSnapshot(rootDir, execution.after);
2792
- return { report: execution.after, safeChanges: execution.changes };
2975
+ return { report: execution.after, safeChanges: execution.changes, hooks };
2793
2976
  }
2794
2977
  const scan = await runScanner(rootDir);
2795
2978
  const report = createReadinessReport(scan, {
@@ -2797,7 +2980,7 @@ async function runDoctor(cwd, options = {}) {
2797
2980
  generatedAt: options.generatedAt
2798
2981
  });
2799
2982
  await writeReadinessSnapshot(rootDir, report);
2800
- return { report, safeChanges: [] };
2983
+ return { report, safeChanges: [], hooks };
2801
2984
  }
2802
2985
  function printDoctorSummary(result) {
2803
2986
  const { summary, pendingActions } = result.report;
@@ -2809,6 +2992,12 @@ function printDoctorSummary(result) {
2809
2992
  );
2810
2993
  console.log(` safe fixes applied: ${fixed}`);
2811
2994
  console.log(` pending actions: ${pendingActions.length}`);
2995
+ console.log(`hooks: ${result.hooks.status}`);
2996
+ if (result.hooks.reasons.length > 0) {
2997
+ for (const reason of result.hooks.reasons.slice(0, 5)) {
2998
+ console.log(` - ${reason}`);
2999
+ }
3000
+ }
2812
3001
  console.log(
2813
3002
  nextAction ? `Next: ${nextAction.recommendation}` : "Next: repository readiness checks are complete"
2814
3003
  );
@@ -2844,11 +3033,210 @@ var doctorCommand = defineCommand6({
2844
3033
  }
2845
3034
  });
2846
3035
 
3036
+ // src/commands/guard.ts
3037
+ import { defineCommand as defineCommand7 } from "citty";
3038
+
3039
+ // src/hooks/read-stdin-json.ts
3040
+ async function readStdinJson() {
3041
+ const chunks = [];
3042
+ for await (const chunk of process.stdin) {
3043
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
3044
+ }
3045
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
3046
+ if (!raw) return {};
3047
+ try {
3048
+ return JSON.parse(raw);
3049
+ } catch {
3050
+ return {};
3051
+ }
3052
+ }
3053
+
3054
+ // src/invariants/secrets-scan.ts
3055
+ var CITE = "agent-kit guard prompt (docs/cursor-native-audit.md)";
3056
+ var SECRET_PATTERNS2 = [
3057
+ {
3058
+ id: "json-secret-kv",
3059
+ re: /"(password|apiKey|api_key|secret|token|auth)"\s*:\s*"[^"]{12,}"/i
3060
+ },
3061
+ {
3062
+ id: "env-assignment",
3063
+ re: /\b(?:API_KEY|SECRET|PASSWORD|TOKEN|ACCESS_KEY|PRIVATE_KEY)\s*=\s*['"]?[^\s'"]{12,}/i
3064
+ },
3065
+ {
3066
+ id: "aws-access-key",
3067
+ re: /\bAKIA[0-9A-Z]{16}\b/
3068
+ },
3069
+ {
3070
+ id: "github-pat",
3071
+ re: /\bghp_[A-Za-z0-9_]{36,}\b/
3072
+ },
3073
+ {
3074
+ id: "openai-sk",
3075
+ re: /\bsk-[A-Za-z0-9]{20,}\b/
3076
+ }
3077
+ ];
3078
+ function excerptAround(text, index, len) {
3079
+ const start = Math.max(0, index - 8);
3080
+ const end = Math.min(text.length, index + len + 8);
3081
+ return text.slice(start, end).replace(/\s+/g, " ");
3082
+ }
3083
+ function scanTextForSecrets(text) {
3084
+ if (!text) return [];
3085
+ const hits = [];
3086
+ for (const { id, re } of SECRET_PATTERNS2) {
3087
+ const flags = re.flags.includes("g") ? re.flags : `${re.flags}g`;
3088
+ const global = new RegExp(re.source, flags);
3089
+ let match = global.exec(text);
3090
+ while (match) {
3091
+ hits.push({
3092
+ patternId: id,
3093
+ excerpt: excerptAround(text, match.index, match[0].length)
3094
+ });
3095
+ if (!global.global) break;
3096
+ match = global.exec(text);
3097
+ }
3098
+ }
3099
+ return hits;
3100
+ }
3101
+ function secretsAdviseMessage(hits) {
3102
+ const ids = [...new Set(hits.map((h) => h.patternId))].join(", ");
3103
+ return `Possible secret pattern(s) in prompt (${ids}). Cite: ${CITE}. Remove live credentials before submitting; use env vars or a secrets store.`;
3104
+ }
3105
+
3106
+ // src/invariants/shell-guard.ts
3107
+ var CITE2 = "agent-kit guard shell (ADR 2026-07-29_cli-invariants-thin-hook-adapters)";
3108
+ function normalizeShellCommand(command) {
3109
+ return command.replace(/\s+/g, " ").trim();
3110
+ }
3111
+ function shellInvocationHeads(command) {
3112
+ const normalized = normalizeShellCommand(command);
3113
+ if (!normalized) return [];
3114
+ return normalized.split(/(?:&&|\|\||[;|])/).map((part) => part.trim().replace(/^(?:\w+=\S+\s+)*/, "")).filter(Boolean);
3115
+ }
3116
+ function anyHeadMatches(command, re) {
3117
+ return shellInvocationHeads(command).some((head) => re.test(head));
3118
+ }
3119
+ var SHELL_DENY_RULES = [
3120
+ {
3121
+ id: "git-checkout-path",
3122
+ description: "git checkout -- <paths> discards working-tree edits",
3123
+ test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+checkout\s+--(?:\s|$)/)
3124
+ },
3125
+ {
3126
+ id: "git-restore",
3127
+ description: "git restore discards working-tree edits",
3128
+ test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+restore\b/)
3129
+ },
3130
+ {
3131
+ id: "git-reset-hard",
3132
+ description: "git reset --hard destroys uncommitted work",
3133
+ test: (cmd) => anyHeadMatches(cmd, /^(?:[\w./-]+\/)?git\s+reset\b.*--hard\b/)
3134
+ },
3135
+ {
3136
+ id: "git-clean-fd",
3137
+ description: "git clean -fd removes untracked files",
3138
+ test: (cmd) => shellInvocationHeads(cmd).some(
3139
+ (head) => /^(?:[\w./-]+\/)?git\s+clean\b/.test(head) && /(?:^|\s)-(?:[a-z]*f[a-z]*d|[a-z]*d[a-z]*f)(?:\s|$)/.test(head)
3140
+ )
3141
+ },
3142
+ {
3143
+ id: "git-push-main",
3144
+ description: "direct push to main/master/prod bypasses staging",
3145
+ test: (cmd) => shellInvocationHeads(cmd).some((head) => {
3146
+ if (!/^(?:[\w./-]+\/)?git\s+push\b/.test(head)) return false;
3147
+ return /(?:^|\s)(?:origin\/)?(?:main|master|prod)(?:\s|$|:)/.test(head) || /HEAD:(?:refs\/heads\/)?(?:main|master|prod)\b/.test(head) || /(?:^|\s)-(?:u|--set-upstream)\s+\S+\s+(?:main|master|prod)(?:\s|$)/.test(head);
3148
+ })
3149
+ }
3150
+ ];
3151
+ function evaluateShellCommand(command) {
3152
+ const normalized = normalizeShellCommand(command);
3153
+ if (!normalized) {
3154
+ return { permission: "allow" };
3155
+ }
3156
+ for (const rule of SHELL_DENY_RULES) {
3157
+ if (rule.test(normalized)) {
3158
+ const agent_message = `Denied by ${CITE2}: ${rule.description} (rule \`${rule.id}\`). Use /git-staging; never discard human hunks or push protected branches from the agent.`;
3159
+ return {
3160
+ permission: "deny",
3161
+ rule: rule.id,
3162
+ agent_message,
3163
+ user_message: agent_message
3164
+ };
3165
+ }
3166
+ }
3167
+ return { permission: "allow" };
3168
+ }
3169
+
3170
+ // src/commands/guard.ts
3171
+ var guardCommand = defineCommand7({
3172
+ meta: {
3173
+ name: "guard",
3174
+ description: "Mechanizable deny/annotate guards (shell, prompt). Hooks are thin adapters."
3175
+ },
3176
+ subCommands: {
3177
+ shell: defineCommand7({
3178
+ meta: {
3179
+ name: "shell",
3180
+ description: "Evaluate a shell command against the destructive deny-list"
3181
+ },
3182
+ args: {
3183
+ json: {
3184
+ type: "boolean",
3185
+ default: true,
3186
+ description: "Print Cursor beforeShellExecution JSON (default)"
3187
+ },
3188
+ command: {
3189
+ type: "string",
3190
+ description: "Command string (otherwise read from stdin JSON.command)"
3191
+ }
3192
+ },
3193
+ async run({ args }) {
3194
+ let command = typeof args.command === "string" ? args.command : "";
3195
+ if (!command) {
3196
+ const payload = await readStdinJson();
3197
+ command = typeof payload.command === "string" ? payload.command : "";
3198
+ }
3199
+ const result = evaluateShellCommand(command);
3200
+ console.log(JSON.stringify(result));
3201
+ }
3202
+ }),
3203
+ prompt: defineCommand7({
3204
+ meta: {
3205
+ name: "prompt",
3206
+ description: "Scan prompt text for secret patterns (advisory; fail-open at hook)"
3207
+ },
3208
+ args: {
3209
+ json: {
3210
+ type: "boolean",
3211
+ default: true
3212
+ }
3213
+ },
3214
+ async run() {
3215
+ const payload = await readStdinJson();
3216
+ const text = typeof payload.prompt === "string" && payload.prompt || typeof payload.text === "string" && payload.text || "";
3217
+ const hits = scanTextForSecrets(text);
3218
+ if (hits.length === 0) {
3219
+ console.log(JSON.stringify({ continue: true, hits: [] }));
3220
+ return;
3221
+ }
3222
+ console.log(
3223
+ JSON.stringify({
3224
+ continue: true,
3225
+ user_message: secretsAdviseMessage(hits),
3226
+ agent_message: secretsAdviseMessage(hits),
3227
+ hits
3228
+ })
3229
+ );
3230
+ }
3231
+ })
3232
+ }
3233
+ });
3234
+
2847
3235
  // src/commands/handoff.ts
2848
3236
  import { spawn as spawn3 } from "child_process";
2849
- import { readFile as readFile9, readdir as readdir2, writeFile as writeFile4 } from "fs/promises";
2850
- import path22 from "path";
2851
- import { defineCommand as defineCommand7 } from "citty";
3237
+ import { readFile as readFile10, readdir as readdir2, writeFile as writeFile4 } from "fs/promises";
3238
+ import path23 from "path";
3239
+ import { defineCommand as defineCommand8 } from "citty";
2852
3240
  function parsePlanFrontmatter(raw) {
2853
3241
  const match = raw.match(/^---\n([\s\S]*?)\n---/);
2854
3242
  if (!match?.[1]) return null;
@@ -2867,19 +3255,19 @@ async function findActivePlan(plansDir) {
2867
3255
  if (!await fileExists(plansDir)) return null;
2868
3256
  const files = (await readdir2(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
2869
3257
  for (const file of files) {
2870
- const raw = await readFile9(path22.join(plansDir, file), "utf8");
3258
+ const raw = await readFile10(path23.join(plansDir, file), "utf8");
2871
3259
  const fm = parsePlanFrontmatter(raw);
2872
3260
  if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
2873
3261
  return { file, raw };
2874
3262
  }
2875
3263
  }
2876
- return files[0] ? { file: files[0], raw: await readFile9(path22.join(plansDir, files[0]), "utf8") } : null;
3264
+ return files[0] ? { file: files[0], raw: await readFile10(path23.join(plansDir, files[0]), "utf8") } : null;
2877
3265
  }
2878
3266
  function now() {
2879
3267
  return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
2880
3268
  }
2881
3269
  async function loadProfile(rootDir) {
2882
- const configPath = path22.join(rootDir, ".cursor", "agent-kit.config.json");
3270
+ const configPath = path23.join(rootDir, ".cursor", "agent-kit.config.json");
2883
3271
  try {
2884
3272
  return await readJson(configPath);
2885
3273
  } catch {
@@ -2965,7 +3353,7 @@ function runCursorHandoff(scriptPath, cwd) {
2965
3353
  child.on("close", (code) => resolve(code ?? 1));
2966
3354
  });
2967
3355
  }
2968
- var handoffCommand = defineCommand7({
3356
+ var handoffCommand = defineCommand8({
2969
3357
  meta: {
2970
3358
  name: "handoff",
2971
3359
  description: "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists."
@@ -2979,13 +3367,13 @@ var handoffCommand = defineCommand7({
2979
3367
  },
2980
3368
  async run({ args }) {
2981
3369
  const profile = await loadProfile(args.cwd);
2982
- const plansDir = path22.join(args.cwd, ".cursor", "plans");
2983
- const handoffPath = path22.join(args.cwd, ".cursor", "HANDOFF.md");
3370
+ const plansDir = path23.join(args.cwd, ".cursor", "plans");
3371
+ const handoffPath = path23.join(args.cwd, ".cursor", "HANDOFF.md");
2984
3372
  const plan = await findActivePlan(plansDir);
2985
3373
  if (plan) {
2986
3374
  const fm = parsePlanFrontmatter(plan.raw);
2987
3375
  if (fm) {
2988
- await ensureDir(path22.join(args.cwd, ".cursor"));
3376
+ await ensureDir(path23.join(args.cwd, ".cursor"));
2989
3377
  const content = buildHandoff(plan.file, fm, profile);
2990
3378
  await writeFile4(handoffPath, content, "utf8");
2991
3379
  logger.success("HANDOFF.md updated: .cursor/HANDOFF.md");
@@ -3005,7 +3393,7 @@ var handoffCommand = defineCommand7({
3005
3393
  }
3006
3394
  logger.warn(`Plan ${plan.file} without valid frontmatter; trying legacy flow.`);
3007
3395
  }
3008
- const scriptPath = path22.join(args.cwd, "cursor-handoff");
3396
+ const scriptPath = path23.join(args.cwd, "cursor-handoff");
3009
3397
  if (!await fileExists(scriptPath)) {
3010
3398
  printV3Guidance();
3011
3399
  return;
@@ -3029,17 +3417,388 @@ var handoffCommand = defineCommand7({
3029
3417
  }
3030
3418
  });
3031
3419
 
3420
+ // src/commands/hook.ts
3421
+ import path25 from "path";
3422
+ import { defineCommand as defineCommand9 } from "citty";
3423
+
3424
+ // src/hooks/pre-compact.ts
3425
+ function buildPreCompactUserMessage(payload = {}) {
3426
+ const pct = payload.context_usage_percent;
3427
+ const trigger = payload.trigger || "auto";
3428
+ const pctTxt = pct !== void 0 && pct !== null ? `~${pct}%` : "high";
3429
+ const msg = `Context compacting (${trigger}, usage ${pctTxt}). Update \`.cursor/HANDOFF.md\` and open a new chat with \`/continue-plan\` so the next agent starts fresh.`;
3430
+ return { user_message: msg };
3431
+ }
3432
+
3433
+ // src/hooks/session-start.ts
3434
+ import { spawn as spawn4 } from "child_process";
3435
+ import { access as access5, readFile as readFile11 } from "fs/promises";
3436
+ import path24 from "path";
3437
+
3438
+ // src/invariants/handoff-schema.ts
3439
+ var MACHINE_LIST_CHECKS = [
3440
+ {
3441
+ heading: "Backlog plans",
3442
+ fieldLabels: ["Backlog plans", "Backlog"],
3443
+ code: "heading-without-field-backlog"
3444
+ },
3445
+ {
3446
+ heading: "Parked plans",
3447
+ fieldLabels: ["Parked plans"],
3448
+ code: "heading-without-field-parked"
3449
+ },
3450
+ {
3451
+ heading: "Run queue",
3452
+ fieldLabels: ["Run queue"],
3453
+ code: "heading-without-field-run-queue"
3454
+ }
3455
+ ];
3456
+ var CITE3 = "agent-kit validate handoff (see .cursor/context/templates/handoff.md)";
3457
+ function validateHandoffText(text) {
3458
+ if (!text.trim()) return [];
3459
+ const warnings = [];
3460
+ for (const check2 of MACHINE_LIST_CHECKS) {
3461
+ const headingRe = new RegExp(`^##\\s+${check2.heading}\\s*$`, "m");
3462
+ if (!headingRe.test(text)) continue;
3463
+ const hasField = check2.fieldLabels.some(
3464
+ (label) => new RegExp(`^- \\*\\*${label}:\\*\\*`, "m").test(text)
3465
+ );
3466
+ if (!hasField) {
3467
+ warnings.push({
3468
+ code: check2.code,
3469
+ message: `\`## ${check2.heading}\` found without \`- **${check2.fieldLabels[0]}:**\` (Mission Control will miss this list; rewrite as a field bullet).`,
3470
+ cite: CITE3
3471
+ });
3472
+ }
3473
+ }
3474
+ return warnings;
3475
+ }
3476
+
3477
+ // src/hooks/hard-rules.ts
3478
+ var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
3479
+
3480
+ 1. **One phase per chat.** Finish the current phase (or one to-do if the phase is huge), update \`.cursor/HANDOFF.md\`, then STOP and ask the user before starting the next phase.
3481
+ 2. **Do not burn the window.** Never run an entire multi-phase plan in one conversation unless the user explicitly ran \`/run-plan\` (or a deprecated alias \`/run-plan-loop\` / \`/run-plan-orchestrated\`).
3482
+ 3. **Context questions are not optional.** If the user asks about context / contexto / window size, run the context-guardian protocol: warn, offer handoff, do NOT dismiss with "it's fine" and keep coding.
3483
+ 4. **Read HANDOFF first** when resuming. Do not restart the plan from scratch.
3484
+ 5. **Git:** suggest \`/git-staging\` after a phase with a diff; never \`/git-prod\` without explicit confirmation.
3485
+ 6. **HITL slash commands win.** When waiting for confirmation on \`/git-staging\` or \`/git-prod\` (or similar), do not divert to continue-plan / phase-boundary chatter; stay on that routine until the user answers.
3486
+ 7. **\`/start-project\` is plan bootstrap, not execute.** Broad Intake Review first, then two gates: (A) single composite question (with active plan: backlog+write / park+write / modify / cancel; without: write / modify / cancel) \u2014 approve/write the plan file only, (B) approve the first unit. Goal text in the same message is NOT permission to edit product files. Never "create plan and start Phase 1" in one turn. If HANDOFF already has an active plan, disposition is merged into Gate A composite options; never park silently. Gates use Ask questions per \`.cursor/rules/hitl-ask-questions.mdc\`. Fallback: one numbered list per message.
3487
+ 8. **\`/continue-plan\` waits for yes.** Summarize next \`[to-do-id]\`, then stop until the user confirms before editing.
3488
+ 9. **\`/run-plan-all\` is a pure orchestrator.** After the confirm queue Ask, dispatch one Task subagent per plan (run the \`/run-plan\` tick contract inside it); never implement to-dos, run tests, or write changelogs in the orchestrator window.
3489
+ 10. **Backlog CRUD never activates.** \`/backlog-add\` enqueues (Broad Intake + write Ask + plan file + HANDOFF Backlog) without park, activate, or Gate B. \`/backlog-edit\` / \`/backlog-delete\` / \`/backlog-cancel\` require Ask confirm before mutate; delete archives from Backlog, cancel is soft in place. No Field Report cards for routine backlog CRUD.
3490
+ 11. **HANDOFF machine fields are bullet fields, not \`##\` headings.** Mission Control parses \`- **Plan:**\`, \`- **Backlog plans:**\`, \`- **Parked plans:**\`, \`- **Run queue:**\` (etc.). Canonical Plan: \`- **Plan:** \\\`name.plan.md\\\`\` or \`none\`. Nested backlog/parked rows: \`- \\\`other.plan.md\\\`\`. Never invent \`## Backlog plans\` / \`## Parked plans\` / \`## Run queue\` headings in place of those fields (Checklist / Current mission go empty or idle).`;
3491
+ var DOGFOOD_INBOX_HINT = `## Dogfood inbox
3492
+
3493
+ Unprocessed files are listed under \`dogfood/README.md\` (### Unprocessed Files). Follow the ingest ritual there (detect \u2192 analyze \u2192 memory WRITE \u2192 triage). Do not auto-start analysis unless the user asks.`;
3494
+ var UPDATE_CHECK_NUDGE = `## Agent Kit update available
3495
+
3496
+ Installed **v{installed}**; latest public **v{latest}**.
3497
+
3498
+ This is an advisory only (no files were changed). To apply, run \`/update\` and confirm via Ask questions. Bare \`agent-kit update\` is an explicit operator invoke, not a background job.`;
3499
+
3500
+ // src/hooks/session-start.ts
3501
+ var NONE_PLACEHOLDERS = /* @__PURE__ */ new Set(["none", "n/a", "empty", "nil"]);
3502
+ async function readTextLimited(filePath, limit = 60) {
3503
+ try {
3504
+ const lines = (await readFile11(filePath, "utf8")).split(/\r?\n/);
3505
+ return lines.slice(0, limit).join("\n").trim();
3506
+ } catch {
3507
+ return "";
3508
+ }
3509
+ }
3510
+ async function readFull(filePath) {
3511
+ try {
3512
+ return await readFile11(filePath, "utf8");
3513
+ } catch {
3514
+ return "";
3515
+ }
3516
+ }
3517
+ async function fileExists2(p) {
3518
+ try {
3519
+ await access5(p);
3520
+ return true;
3521
+ } catch {
3522
+ return false;
3523
+ }
3524
+ }
3525
+ function parseUnprocessedDogfoodItems(readmeText) {
3526
+ const items = [];
3527
+ let inSection = false;
3528
+ for (const line of readmeText.split(/\r?\n/)) {
3529
+ if (line.startsWith("### Unprocessed Files")) {
3530
+ inSection = true;
3531
+ continue;
3532
+ }
3533
+ if (!inSection) continue;
3534
+ if (line.startsWith("### ")) break;
3535
+ const stripped = line.trim();
3536
+ if (!stripped.startsWith("- ")) continue;
3537
+ const body = stripped.slice(2).trim();
3538
+ const normalized = body.toLowerCase().replace(/[*_]/g, "").trim();
3539
+ if (!normalized || NONE_PLACEHOLDERS.has(normalized)) continue;
3540
+ items.push(body);
3541
+ }
3542
+ return items;
3543
+ }
3544
+ async function l0Present(root) {
3545
+ const cursor = path24.join(root, ".cursor");
3546
+ return await fileExists2(path24.join(cursor, "agent-kit.json")) || await fileExists2(path24.join(cursor, "commands", "agent-kit-onboard.md")) || await fileExists2(path24.join(cursor, "commands", "start-project.md"));
3547
+ }
3548
+ function checkLabelAndRecommendation(check2) {
3549
+ const checkId = check2.id;
3550
+ if (typeof checkId !== "string" || !checkId) return null;
3551
+ const actions = check2.actions;
3552
+ if (Array.isArray(actions)) {
3553
+ for (const action2 of actions) {
3554
+ if (!action2 || typeof action2 !== "object") continue;
3555
+ const a = action2;
3556
+ if (typeof a.id === "string" && typeof a.recommendation === "string") {
3557
+ return [a.id, a.recommendation];
3558
+ }
3559
+ }
3560
+ }
3561
+ const title = check2.title;
3562
+ if (typeof title === "string" && title) return [checkId, title];
3563
+ return [checkId, "Resolve this readiness check"];
3564
+ }
3565
+ function unresolvedReadinessChecks(data) {
3566
+ const essential = [];
3567
+ const nonessential = [];
3568
+ const pillars = data.pillars;
3569
+ if (!Array.isArray(pillars)) return { essential, nonessential };
3570
+ for (const pillar2 of pillars) {
3571
+ if (!pillar2 || typeof pillar2 !== "object") continue;
3572
+ const checks = pillar2.checks;
3573
+ if (!Array.isArray(checks)) continue;
3574
+ for (const check2 of checks) {
3575
+ if (!check2 || typeof check2 !== "object") continue;
3576
+ const c = check2;
3577
+ if (c.status === "ready") continue;
3578
+ if (c.essential === true) essential.push(c);
3579
+ else nonessential.push(c);
3580
+ }
3581
+ }
3582
+ return { essential, nonessential };
3583
+ }
3584
+ async function readinessSection(root) {
3585
+ const snapshotPath = path24.join(root, ".cursor", "context", "readiness.json");
3586
+ let data;
3587
+ try {
3588
+ data = JSON.parse(await readFile11(snapshotPath, "utf8"));
3589
+ } catch {
3590
+ return null;
3591
+ }
3592
+ const { essential, nonessential } = unresolvedReadinessChecks(data);
3593
+ if (essential[0]) {
3594
+ const labeled = checkLabelAndRecommendation(essential[0]);
3595
+ if (!labeled) return null;
3596
+ const [actionId, recommendation] = labeled;
3597
+ return `## Repository readiness
3598
+
3599
+ Unresolved essential check: \`${actionId}\`. ${recommendation} Run \`/agent-kit-onboard\` before \`/start-project\`. An active plan or HANDOFF remains the current work and is not replaced.`;
3600
+ }
3601
+ if (nonessential[0]) {
3602
+ const labeled = checkLabelAndRecommendation(nonessential[0]);
3603
+ if (!labeled) return null;
3604
+ const [actionId, recommendation] = labeled;
3605
+ return `## Repository readiness
3606
+
3607
+ Optional readiness item: \`${actionId}\`. ${recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
3608
+ }
3609
+ const actions = data.pendingActions;
3610
+ if (!Array.isArray(actions) || !actions[0] || typeof actions[0] !== "object") return null;
3611
+ const first = actions[0];
3612
+ if (typeof first.id !== "string" || typeof first.recommendation !== "string") return null;
3613
+ return `## Repository readiness
3614
+
3615
+ Optional readiness item: \`${first.id}\`. ${first.recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
3616
+ }
3617
+ async function dogfoodInboxSection(root) {
3618
+ const dogfoodDir = path24.join(root, "dogfood");
3619
+ if (!await fileExists2(dogfoodDir)) return null;
3620
+ const readme = path24.join(dogfoodDir, "README.md");
3621
+ if (!await fileExists2(readme)) return null;
3622
+ try {
3623
+ const text = await readFile11(readme, "utf8");
3624
+ if (!parseUnprocessedDogfoodItems(text).length) return null;
3625
+ return DOGFOOD_INBOX_HINT;
3626
+ } catch {
3627
+ return null;
3628
+ }
3629
+ }
3630
+ async function loadUpdateCheckPrefs(root) {
3631
+ try {
3632
+ const data = JSON.parse(
3633
+ await readFile11(path24.join(root, ".cursor", "context", "config.json"), "utf8")
3634
+ );
3635
+ const uc = data.updateCheck;
3636
+ if (!uc || typeof uc !== "object" || uc.enabled !== true) {
3637
+ return null;
3638
+ }
3639
+ return uc;
3640
+ } catch {
3641
+ return null;
3642
+ }
3643
+ }
3644
+ function runUpdateCheckJson(root) {
3645
+ return new Promise((resolve) => {
3646
+ const child = spawn4(
3647
+ process.execPath,
3648
+ [
3649
+ process.argv[1] ?? "",
3650
+ "update",
3651
+ "--check",
3652
+ "--json",
3653
+ "--respect-prefs",
3654
+ "--stamp",
3655
+ "--cwd",
3656
+ root
3657
+ ],
3658
+ { stdio: ["ignore", "pipe", "ignore"], timeout: 12e3 }
3659
+ );
3660
+ let out = "";
3661
+ child.stdout?.on("data", (chunk) => {
3662
+ out += chunk.toString("utf8");
3663
+ });
3664
+ child.on("error", () => resolve(null));
3665
+ child.on("close", () => {
3666
+ try {
3667
+ const parsed = JSON.parse(out.trim());
3668
+ resolve(parsed && typeof parsed === "object" ? parsed : null);
3669
+ } catch {
3670
+ resolve(null);
3671
+ }
3672
+ });
3673
+ });
3674
+ }
3675
+ async function updateCheckSection(root) {
3676
+ if (await loadUpdateCheckPrefs(root) === null) return null;
3677
+ const result = await new Promise((resolve) => {
3678
+ const child = spawn4(
3679
+ "agent-kit",
3680
+ ["update", "--check", "--json", "--respect-prefs", "--stamp", "--cwd", root],
3681
+ { stdio: ["ignore", "pipe", "ignore"], timeout: 12e3, shell: false }
3682
+ );
3683
+ let out = "";
3684
+ child.stdout?.on("data", (chunk) => {
3685
+ out += chunk.toString("utf8");
3686
+ });
3687
+ child.on("error", () => {
3688
+ void runUpdateCheckJson(root).then(resolve);
3689
+ });
3690
+ child.on("close", (code) => {
3691
+ if (code !== 0 && !out.trim()) {
3692
+ void runUpdateCheckJson(root).then(resolve);
3693
+ return;
3694
+ }
3695
+ try {
3696
+ resolve(JSON.parse(out.trim()));
3697
+ } catch {
3698
+ void runUpdateCheckJson(root).then(resolve);
3699
+ }
3700
+ });
3701
+ });
3702
+ if (!result || result.status !== "update-available") return null;
3703
+ if (result.applyRecommended === true) return null;
3704
+ const installed = String(result.installedVersion ?? "?");
3705
+ const latest = String(result.latestVersion ?? "?");
3706
+ return UPDATE_CHECK_NUDGE.replace("{installed}", installed).replace("{latest}", latest);
3707
+ }
3708
+ async function buildSessionStartAdditionalContext(rootDir, _payload = {}) {
3709
+ const root = path24.resolve(rootDir);
3710
+ const handoffPath = path24.join(root, ".cursor", "HANDOFF.md");
3711
+ const handoffFull = await readFull(handoffPath);
3712
+ const handoff = await readTextLimited(handoffPath);
3713
+ const parts = [HARD_RULES];
3714
+ if (await l0Present(root)) {
3715
+ const readiness = await readinessSection(root);
3716
+ if (readiness) parts.push(readiness);
3717
+ }
3718
+ const dogfood = await dogfoodInboxSection(root);
3719
+ if (dogfood) parts.push(dogfood);
3720
+ const updateNudge = await updateCheckSection(root);
3721
+ if (updateNudge) parts.push(updateNudge);
3722
+ const formatWarnings = validateHandoffText(handoffFull);
3723
+ if (formatWarnings.length) {
3724
+ const bullet = formatWarnings.map((w) => `- ${w.message}`).join("\n");
3725
+ parts.push(
3726
+ `## HANDOFF format warning (Mission Control)
3727
+
3728
+ ${bullet}
3729
+
3730
+ Rewrite machine lists as \`- **Field:**\` bullets before trusting Checklist / Current mission.`
3731
+ );
3732
+ }
3733
+ if (handoff) {
3734
+ parts.push(`## Current HANDOFF.md (excerpt)
3735
+
3736
+ ${handoff}`);
3737
+ } else {
3738
+ parts.push(
3739
+ "## HANDOFF.md\n\nNo handoff file yet. If starting work, create a plan with to-dos first (`/start-project`)."
3740
+ );
3741
+ }
3742
+ return { additional_context: parts.join("\n\n") };
3743
+ }
3744
+ function resolveSessionRoot(payload, cwd = process.cwd()) {
3745
+ const roots = payload.workspace_roots;
3746
+ if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0]) {
3747
+ return roots[0];
3748
+ }
3749
+ return cwd;
3750
+ }
3751
+
3752
+ // src/commands/hook.ts
3753
+ var hookCommand = defineCommand9({
3754
+ meta: {
3755
+ name: "hook",
3756
+ description: "Cursor hook adapters (session-start, pre-compact). CLI is SoT; thin hooks shell out here."
3757
+ },
3758
+ subCommands: {
3759
+ "session-start": defineCommand9({
3760
+ meta: {
3761
+ name: "session-start",
3762
+ description: "Emit sessionStart additional_context JSON (stdin: Cursor payload)"
3763
+ },
3764
+ args: {
3765
+ cwd: {
3766
+ type: "string",
3767
+ default: process.cwd()
3768
+ }
3769
+ },
3770
+ async run({ args }) {
3771
+ const payload = await readStdinJson();
3772
+ const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
3773
+ const root = resolveSessionRoot(payload, path25.resolve(cwd));
3774
+ const out = await buildSessionStartAdditionalContext(root, payload);
3775
+ console.log(JSON.stringify(out));
3776
+ }
3777
+ }),
3778
+ "pre-compact": defineCommand9({
3779
+ meta: {
3780
+ name: "pre-compact",
3781
+ description: "Emit preCompact user_message JSON (stdin: Cursor payload)"
3782
+ },
3783
+ async run() {
3784
+ const payload = await readStdinJson();
3785
+ console.log(JSON.stringify(buildPreCompactUserMessage(payload)));
3786
+ }
3787
+ })
3788
+ }
3789
+ });
3790
+
3032
3791
  // src/commands/init.ts
3033
3792
  import { intro, outro } from "@clack/prompts";
3034
- import { defineCommand as defineCommand9 } from "citty";
3793
+ import { defineCommand as defineCommand11 } from "citty";
3035
3794
 
3036
3795
  // src/commands/install.ts
3037
- import path25 from "path";
3038
- import { defineCommand as defineCommand8 } from "citty";
3796
+ import path28 from "path";
3797
+ import { defineCommand as defineCommand10 } from "citty";
3039
3798
 
3040
3799
  // src/generator/personalization.ts
3041
- import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
3042
- import path23 from "path";
3800
+ import { readFile as readFile12, writeFile as writeFile5 } from "fs/promises";
3801
+ import path26 from "path";
3043
3802
  var PERSONALIZATION_CONTRACT_VERSION = 1;
3044
3803
  var CONTEXT_PATH = ".cursor/project-context.md";
3045
3804
  var AGENTS_PATH = "AGENTS.md";
@@ -3190,7 +3949,7 @@ function renderProjectContext(profile) {
3190
3949
  `;
3191
3950
  }
3192
3951
  async function createOwnedFile(rootDir, relativePath, content, evidence) {
3193
- const target = path23.join(rootDir, relativePath);
3952
+ const target = path26.join(rootDir, relativePath);
3194
3953
  if (await fileExists(target)) {
3195
3954
  return {
3196
3955
  kind: "file",
@@ -3200,7 +3959,7 @@ async function createOwnedFile(rootDir, relativePath, content, evidence) {
3200
3959
  evidence
3201
3960
  };
3202
3961
  }
3203
- await ensureDir(path23.dirname(target));
3962
+ await ensureDir(path26.dirname(target));
3204
3963
  await writeFile5(target, content, "utf8");
3205
3964
  return {
3206
3965
  kind: "file",
@@ -3217,7 +3976,7 @@ async function packTargets(registryRoot, packId) {
3217
3976
  async function existingTargets(projectRoot, targets) {
3218
3977
  const checks = await Promise.all(
3219
3978
  targets.map(
3220
- async (target) => await fileExists(path23.join(projectRoot, target)) ? target : null
3979
+ async (target) => await fileExists(path26.join(projectRoot, target)) ? target : null
3221
3980
  )
3222
3981
  );
3223
3982
  return checks.filter((target) => target !== null);
@@ -3239,14 +3998,14 @@ async function applyPersonalization(input) {
3239
3998
  componentResults.push({ ...item, status: "unavailable" });
3240
3999
  continue;
3241
4000
  }
3242
- const target = path23.posix.join(
4001
+ const target = path26.posix.join(
3243
4002
  ".cursor",
3244
4003
  "skills",
3245
4004
  skill.path.includes("/core/") ? "core" : "community",
3246
4005
  skill.id,
3247
4006
  "SKILL.md"
3248
4007
  );
3249
- if (await fileExists(path23.join(input.rootDir, target))) {
4008
+ if (await fileExists(path26.join(input.rootDir, target))) {
3250
4009
  componentResults.push({ ...item, status: "skipped-customized", path: target });
3251
4010
  protectedPaths.add(target);
3252
4011
  continue;
@@ -3301,7 +4060,7 @@ async function applyPersonalization(input) {
3301
4060
  items: [...fileResults, ...componentResults],
3302
4061
  protectedPaths: [...protectedPaths].sort()
3303
4062
  };
3304
- await writeJson(path23.join(input.rootDir, RESULT_PATH), result);
4063
+ await writeJson(path26.join(input.rootDir, RESULT_PATH), result);
3305
4064
  return {
3306
4065
  result,
3307
4066
  manifest: {
@@ -3319,26 +4078,26 @@ async function applyPersonalization(input) {
3319
4078
  };
3320
4079
  }
3321
4080
  async function readRepositoryProfile(rootDir) {
3322
- const target = path23.join(rootDir, ".cursor/agent-kit.config.json");
4081
+ const target = path26.join(rootDir, ".cursor/agent-kit.config.json");
3323
4082
  if (!await fileExists(target)) return null;
3324
- return JSON.parse(await readFile10(target, "utf8"));
4083
+ return JSON.parse(await readFile12(target, "utf8"));
3325
4084
  }
3326
4085
 
3327
4086
  // src/lifecycle/onboard-migration.ts
3328
4087
  import { createHash as createHash3 } from "crypto";
3329
- import { readFile as readFile11, unlink } from "fs/promises";
3330
- import path24 from "path";
4088
+ import { readFile as readFile13, unlink } from "fs/promises";
4089
+ import path27 from "path";
3331
4090
  var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
3332
4091
  var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
3333
4092
  var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
3334
4093
  "b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
3335
4094
  ]);
3336
4095
  async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
3337
- const legacyPath = path24.join(projectRoot, LEGACY_ONBOARD_PATH);
4096
+ const legacyPath = path27.join(projectRoot, LEGACY_ONBOARD_PATH);
3338
4097
  if (!await fileExists(legacyPath)) return "absent";
3339
- const namespacedPath = path24.join(projectRoot, NAMESPACED_ONBOARD_PATH);
4098
+ const namespacedPath = path27.join(projectRoot, NAMESPACED_ONBOARD_PATH);
3340
4099
  if (!await fileExists(namespacedPath)) return "preserved-customized";
3341
- const content = await readFile11(legacyPath);
4100
+ const content = await readFile13(legacyPath);
3342
4101
  const hash = createHash3("sha256").update(content).digest("hex");
3343
4102
  if (!managedHashes.has(hash)) return "preserved-customized";
3344
4103
  await unlink(legacyPath);
@@ -3407,7 +4166,7 @@ function printReadinessNarrative(result) {
3407
4166
  );
3408
4167
  }
3409
4168
  async function performInstall(options) {
3410
- const projectRoot = path25.resolve(options.cwd);
4169
+ const projectRoot = path28.resolve(options.cwd);
3411
4170
  const packs = parsePackList(options.pack);
3412
4171
  const existing = await loadAgentKitManifest(projectRoot);
3413
4172
  const registry = await resolveRegistryFromCli({
@@ -3461,7 +4220,7 @@ async function performInstall(options) {
3461
4220
  safeChanges: readinessExecution.changes
3462
4221
  };
3463
4222
  }
3464
- var installCommand = defineCommand8({
4223
+ var installCommand = defineCommand10({
3465
4224
  meta: {
3466
4225
  name: "install",
3467
4226
  description: "Bootstrap L0 (+ optional packs) from the registry and write agent-kit.json."
@@ -3483,7 +4242,7 @@ var installCommand = defineCommand8({
3483
4242
  ...REGISTRY_CLI_ARGS
3484
4243
  },
3485
4244
  async run({ args }) {
3486
- const projectRoot = path25.resolve(args.cwd);
4245
+ const projectRoot = path28.resolve(args.cwd);
3487
4246
  logger.info(`Installing into: ${projectRoot}`);
3488
4247
  const packs = parsePackList(args.pack);
3489
4248
  for (const id of packs) {
@@ -3511,7 +4270,7 @@ var installCommand = defineCommand8({
3511
4270
  async function runInitCompatibility(cwd, installer = performInstall) {
3512
4271
  return installer({ cwd });
3513
4272
  }
3514
- var initCommand = defineCommand9({
4273
+ var initCommand = defineCommand11({
3515
4274
  meta: {
3516
4275
  name: "init",
3517
4276
  description: "Guided compatibility entry point for install and repository readiness."
@@ -3535,16 +4294,186 @@ var initCommand = defineCommand9({
3535
4294
  }
3536
4295
  });
3537
4296
 
3538
- // src/commands/run-plan.ts
4297
+ // src/commands/monitors.ts
3539
4298
  import path30 from "path";
3540
- import { defineCommand as defineCommand10 } from "citty";
4299
+ import { defineCommand as defineCommand12 } from "citty";
4300
+
4301
+ // src/invariants/monitors-untriaged.ts
4302
+ import { execFile as execFile3 } from "child_process";
4303
+ import { readFile as readFile14, readdir as readdir3, stat } from "fs/promises";
4304
+ import path29 from "path";
4305
+ import { promisify as promisify3 } from "util";
4306
+ var execFileAsync2 = promisify3(execFile3);
4307
+ var TRIAGE_HEADING_RE = /^#{2,6}\s+.*\b(triage|follow-?up plan|residuals plan)\b/im;
4308
+ var CITE4 = "agent-kit monitors --untriaged (ADR 2026-07-27_plan-review-triage-untriaged-not-mtime; never newest-mtime-wins)";
4309
+ function hasOpenGaps(content) {
4310
+ if (/###\s+Still open[^\n]*\n+(?:\s*\n)*(?:None\.|none\.|\*None\*)/i.test(content)) {
4311
+ return false;
4312
+ }
4313
+ if (/###\s+Still open/i.test(content) && !/###\s+Still open[^\n]*\n+(?:\s*\n)*(?:None\.|none\.)/i.test(content)) {
4314
+ const m = content.match(/###\s+Still open[^\n]*\n([\s\S]*?)(?=\n### |\n## |$)/i);
4315
+ if (m?.[1]?.trim() && !/^(none\.?|\*none\*)$/i.test(m[1].trim())) {
4316
+ return true;
4317
+ }
4318
+ }
4319
+ if (/^#{2,6}\s+.*\bResidual items?\b/im.test(content)) return true;
4320
+ return false;
4321
+ }
4322
+ async function listMonitorFiles(memoryDir) {
4323
+ try {
4324
+ const names = await readdir3(memoryDir);
4325
+ return names.filter((n) => n.startsWith("plan-monitor-") && n.endsWith(".md")).sort();
4326
+ } catch {
4327
+ return [];
4328
+ }
4329
+ }
4330
+ async function gitFreshMonitorNames(rootDir) {
4331
+ const names = /* @__PURE__ */ new Set();
4332
+ try {
4333
+ const { stdout } = await execFileAsync2(
4334
+ "git",
4335
+ ["status", "--porcelain", "--", ".cursor/memory"],
4336
+ { cwd: rootDir, maxBuffer: 2 * 1024 * 1024 }
4337
+ );
4338
+ for (const line of stdout.split("\n")) {
4339
+ if (!line.trim()) continue;
4340
+ const file = line.slice(3).trim().replace(/^.* -> /, "");
4341
+ const base = path29.basename(file);
4342
+ if (base.startsWith("plan-monitor-") && base.endsWith(".md")) {
4343
+ names.add(base);
4344
+ }
4345
+ }
4346
+ } catch {
4347
+ }
4348
+ return names;
4349
+ }
4350
+ function extractHandoffPlanSlugs(handoff) {
4351
+ const slugs = /* @__PURE__ */ new Set();
4352
+ for (const m of handoff.matchAll(/`([a-z0-9][a-z0-9._-]*)\.plan\.md`/gi)) {
4353
+ if (m[1]) slugs.add(m[1].toLowerCase());
4354
+ }
4355
+ for (const m of handoff.matchAll(/plan-monitor-([a-z0-9][a-z0-9._-]*)\.md/gi)) {
4356
+ if (m[1]) slugs.add(m[1].toLowerCase());
4357
+ }
4358
+ return slugs;
4359
+ }
4360
+ function monitorSlugFromName(fileName) {
4361
+ return fileName.replace(/^plan-monitor-/, "").replace(/\.md$/, "").toLowerCase();
4362
+ }
4363
+ async function selectUntriagedMonitors(rootDir) {
4364
+ const root = path29.resolve(rootDir);
4365
+ const memoryDir = path29.join(root, ".cursor", "memory");
4366
+ const allNames = await listMonitorFiles(memoryDir);
4367
+ const selectionOrder = ["git-fresh", "handoff-aligned", "untriaged-scan"];
4368
+ const byName = /* @__PURE__ */ new Map();
4369
+ for (const name of allNames) {
4370
+ const abs = path29.join(memoryDir, name);
4371
+ try {
4372
+ const [content, st] = await Promise.all([readFile14(abs, "utf8"), stat(abs)]);
4373
+ byName.set(name, { content, mtimeMs: st.mtimeMs });
4374
+ } catch {
4375
+ }
4376
+ }
4377
+ const untriaged = (name) => {
4378
+ const row = byName.get(name);
4379
+ return !!row && !TRIAGE_HEADING_RE.test(row.content);
4380
+ };
4381
+ const gitFresh = await gitFreshMonitorNames(root);
4382
+ const gitFreshSet = [...gitFresh].filter(untriaged).sort();
4383
+ let handoff = "";
4384
+ try {
4385
+ handoff = await readFile14(path29.join(root, ".cursor", "HANDOFF.md"), "utf8");
4386
+ } catch {
4387
+ handoff = "";
4388
+ }
4389
+ const handoffSlugs = extractHandoffPlanSlugs(handoff);
4390
+ const handoffAligned = allNames.filter((n) => untriaged(n) && handoffSlugs.has(monitorSlugFromName(n))).sort();
4391
+ const scanAll = allNames.filter(untriaged);
4392
+ let chosen;
4393
+ if (gitFreshSet.length > 0) {
4394
+ chosen = { names: gitFreshSet, bucket: "git-fresh" };
4395
+ } else if (handoffAligned.length > 0) {
4396
+ chosen = { names: handoffAligned, bucket: "handoff-aligned" };
4397
+ } else {
4398
+ chosen = { names: scanAll, bucket: "untriaged-scan" };
4399
+ }
4400
+ const entries = [];
4401
+ for (const name of chosen.names) {
4402
+ const row = byName.get(name);
4403
+ if (!row) continue;
4404
+ entries.push({
4405
+ path: path29.join(memoryDir, name),
4406
+ relativePath: path29.relative(root, path29.join(memoryDir, name)).split(path29.sep).join("/"),
4407
+ mtimeMs: row.mtimeMs,
4408
+ hasTriageHeading: false,
4409
+ hasOpenGaps: hasOpenGaps(row.content),
4410
+ selectionBucket: chosen.bucket
4411
+ });
4412
+ }
4413
+ entries.sort((a, b) => {
4414
+ if (a.hasOpenGaps !== b.hasOpenGaps) return a.hasOpenGaps ? -1 : 1;
4415
+ return b.mtimeMs - a.mtimeMs;
4416
+ });
4417
+ return {
4418
+ selectionOrder: [...selectionOrder],
4419
+ monitors: entries,
4420
+ cite: CITE4
4421
+ };
4422
+ }
4423
+
4424
+ // src/commands/monitors.ts
4425
+ var monitorsCommand = defineCommand12({
4426
+ meta: {
4427
+ name: "monitors",
4428
+ description: "Plan-monitor selection helpers (untriaged SoT for /plan-review-triage)"
4429
+ },
4430
+ args: {
4431
+ cwd: {
4432
+ type: "string",
4433
+ default: process.cwd()
4434
+ },
4435
+ untriaged: {
4436
+ type: "boolean",
4437
+ default: false,
4438
+ description: "Select untriaged monitors (never newest-mtime-wins alone)"
4439
+ },
4440
+ json: {
4441
+ type: "boolean",
4442
+ default: false,
4443
+ description: "Machine-readable JSON"
4444
+ }
4445
+ },
4446
+ async run({ args }) {
4447
+ if (!args.untriaged) {
4448
+ console.error("Usage: agent-kit monitors --untriaged [--json] [--cwd <dir>]");
4449
+ process.exitCode = 2;
4450
+ return;
4451
+ }
4452
+ const result = await selectUntriagedMonitors(path30.resolve(args.cwd));
4453
+ if (args.json) {
4454
+ console.log(JSON.stringify(result, null, 2));
4455
+ return;
4456
+ }
4457
+ if (result.monitors.length === 0) {
4458
+ console.log("No untriaged plan-monitor files.");
4459
+ return;
4460
+ }
4461
+ for (const m of result.monitors) {
4462
+ console.log(m.relativePath);
4463
+ }
4464
+ }
4465
+ });
4466
+
4467
+ // src/commands/run-plan.ts
4468
+ import path35 from "path";
4469
+ import { defineCommand as defineCommand13 } from "citty";
3541
4470
 
3542
4471
  // src/plan-loop/backends.ts
3543
- import { execFileSync, spawn as spawn4 } from "child_process";
4472
+ import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
3544
4473
  import { createWriteStream } from "fs";
3545
4474
  async function which(bin) {
3546
4475
  try {
3547
- const out = execFileSync("which", [bin], { encoding: "utf8" }).trim();
4476
+ const out = execFileSync2("which", [bin], { encoding: "utf8" }).trim();
3548
4477
  return out || null;
3549
4478
  } catch {
3550
4479
  return null;
@@ -3553,7 +4482,7 @@ async function which(bin) {
3553
4482
  function spawnLogged(command, args, logPath) {
3554
4483
  return new Promise((resolve, reject) => {
3555
4484
  const out = createWriteStream(logPath, { flags: "w" });
3556
- const child = spawn4(command, args, {
4485
+ const child = spawn5(command, args, {
3557
4486
  stdio: ["ignore", "pipe", "pipe"]
3558
4487
  });
3559
4488
  const onData = (chunk) => {
@@ -3621,14 +4550,14 @@ function listBackendIds() {
3621
4550
  }
3622
4551
 
3623
4552
  // src/plan-loop/run-loop.ts
3624
- import { mkdir as mkdir4, readFile as readFile14, rm, unlink as unlink2 } from "fs/promises";
3625
- import path29 from "path";
4553
+ import { mkdir as mkdir4, readFile as readFile17, rm, unlink as unlink2 } from "fs/promises";
4554
+ import path34 from "path";
3626
4555
 
3627
4556
  // src/plan-loop/external-review.ts
3628
- import { spawn as spawn5 } from "child_process";
3629
- import path26 from "path";
3630
- var CANONICAL_REL = path26.join(".cursor", "scripts", "plan-external-review.sh");
3631
- var FALLBACK_REL = path26.join("scripts", "plan-external-review.sh");
4557
+ import { spawn as spawn6 } from "child_process";
4558
+ import path31 from "path";
4559
+ var CANONICAL_REL = path31.join(".cursor", "scripts", "plan-external-review.sh");
4560
+ var FALLBACK_REL = path31.join("scripts", "plan-external-review.sh");
3632
4561
  function isPlanExhaustedReason(reason) {
3633
4562
  const r = reason.trim().toLowerCase();
3634
4563
  if (!r) return false;
@@ -3646,12 +4575,12 @@ function shouldArmExternalPlanReview(input) {
3646
4575
  return false;
3647
4576
  }
3648
4577
  async function armExternalPlanReview(root, options = {}) {
3649
- const spawnFn = options.spawnFn ?? spawn5;
4578
+ const spawnFn = options.spawnFn ?? spawn6;
3650
4579
  const existsFn = options.existsFn ?? fileExists;
3651
4580
  const log = options.log ?? ((line) => console.log(line));
3652
4581
  const force = options.force === true;
3653
- const canonicalPath = path26.join(root, CANONICAL_REL);
3654
- const fallbackPath = path26.join(root, FALLBACK_REL);
4582
+ const canonicalPath = path31.join(root, CANONICAL_REL);
4583
+ const fallbackPath = path31.join(root, FALLBACK_REL);
3655
4584
  let scriptPath = null;
3656
4585
  let scriptRel = CANONICAL_REL;
3657
4586
  if (await existsFn(canonicalPath)) {
@@ -3704,7 +4633,7 @@ async function armExternalPlanReview(root, options = {}) {
3704
4633
  }
3705
4634
 
3706
4635
  // src/plan-loop/persona-banners.ts
3707
- import path27 from "path";
4636
+ import path32 from "path";
3708
4637
  import {
3709
4638
  blue,
3710
4639
  cyan as cyan2,
@@ -3740,7 +4669,7 @@ function resolveColor(name, fallback) {
3740
4669
  async function resolveCliPersonaId(root) {
3741
4670
  try {
3742
4671
  const cfg = await readJson(
3743
- path27.join(root, ".cursor", "context", "config.json")
4672
+ path32.join(root, ".cursor", "context", "config.json")
3744
4673
  );
3745
4674
  const modes = cfg?.agentPersona?.modes ?? cfg?.workspaceSkin?.modes;
3746
4675
  const id = modes?.[CLI_RUN_PLAN_MODE];
@@ -3751,7 +4680,7 @@ async function resolveCliPersonaId(root) {
3751
4680
  }
3752
4681
  async function loadPersonaPack(root, personaId) {
3753
4682
  try {
3754
- const personaPath = path27.join(root, "registry", "personas", "core", personaId, "persona.json");
4683
+ const personaPath = path32.join(root, "registry", "personas", "core", personaId, "persona.json");
3755
4684
  const pack = await readJson(personaPath);
3756
4685
  if (!pack || typeof pack.id !== "string") return null;
3757
4686
  return pack;
@@ -3800,8 +4729,8 @@ function createPersonaBannerPrinter(persona) {
3800
4729
  }
3801
4730
 
3802
4731
  // src/plan-loop/plan-state.ts
3803
- import { readFile as readFile12, readdir as readdir3 } from "fs/promises";
3804
- import path28 from "path";
4732
+ import { readFile as readFile15, readdir as readdir4 } from "fs/promises";
4733
+ import path33 from "path";
3805
4734
  function countPendingTodos(raw) {
3806
4735
  const lines = raw.split(/\r?\n/);
3807
4736
  let inFront = 0;
@@ -3828,15 +4757,15 @@ function countPendingTodos(raw) {
3828
4757
  }
3829
4758
  async function findActivePlanFile(plansDir) {
3830
4759
  if (!await fileExists(plansDir)) return null;
3831
- const files = (await readdir3(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
3832
- return files[0] ? path28.join(plansDir, files[0]) : null;
4760
+ const files = (await readdir4(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
4761
+ return files[0] ? path33.join(plansDir, files[0]) : null;
3833
4762
  }
3834
4763
  async function readPlan(planPath) {
3835
- return readFile12(planPath, "utf8");
4764
+ return readFile15(planPath, "utf8");
3836
4765
  }
3837
4766
 
3838
4767
  // src/plan-loop/sentinel.ts
3839
- import { readFile as readFile13 } from "fs/promises";
4768
+ import { readFile as readFile16 } from "fs/promises";
3840
4769
  var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
3841
4770
  function takeFromText(text) {
3842
4771
  if (!text) return null;
@@ -3883,7 +4812,7 @@ function parseSentinelFromLog(content) {
3883
4812
  }
3884
4813
  async function parseSentinelFromLogFile(logPath) {
3885
4814
  try {
3886
- const content = await readFile13(logPath, "utf8");
4815
+ const content = await readFile16(logPath, "utf8");
3887
4816
  return parseSentinelFromLog(content);
3888
4817
  } catch {
3889
4818
  return { kind: "missing" };
@@ -3906,9 +4835,9 @@ function sleep(ms) {
3906
4835
  return new Promise((r) => setTimeout(r, ms));
3907
4836
  }
3908
4837
  async function runPlanLoop(opts) {
3909
- const plansDir = path29.join(opts.root, ".cursor", "plans");
3910
- const stopFile = path29.join(opts.root, ".cursor", "loop.stop");
3911
- const logDir = path29.join(opts.root, ".cursor", "loop-logs");
4838
+ const plansDir = path34.join(opts.root, ".cursor", "plans");
4839
+ const stopFile = path34.join(opts.root, ".cursor", "loop.stop");
4840
+ const logDir = path34.join(opts.root, ".cursor", "loop-logs");
3912
4841
  const planPath = await findActivePlanFile(plansDir);
3913
4842
  if (!planPath) {
3914
4843
  logger.error("No active plan in .cursor/plans/");
@@ -3929,7 +4858,7 @@ async function runPlanLoop(opts) {
3929
4858
  try {
3930
4859
  const persona = await loadCliRunPlanPersona(opts.root);
3931
4860
  const banners = createPersonaBannerPrinter(persona);
3932
- console.log(`Active plan: ${path29.basename(planPath)}`);
4861
+ console.log(`Active plan: ${path34.basename(planPath)}`);
3933
4862
  console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
3934
4863
  console.log(`Backend: ${opts.backend.id}`);
3935
4864
  if (persona) {
@@ -3972,8 +4901,8 @@ async function runPlanLoop(opts) {
3972
4901
  planExhausted = true;
3973
4902
  break;
3974
4903
  }
3975
- const logPath = path29.join(logDir, `tick-${stamp()}.log`);
3976
- const relLog = path29.relative(opts.root, logPath);
4904
+ const logPath = path34.join(logDir, `tick-${stamp()}.log`);
4905
+ const relLog = path34.relative(opts.root, logPath);
3977
4906
  console.log("");
3978
4907
  const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
3979
4908
  if (banners) banners.tickStart(tickLine);
@@ -3992,7 +4921,7 @@ async function runPlanLoop(opts) {
3992
4921
  return 1;
3993
4922
  }
3994
4923
  try {
3995
- const logText = await readFile14(logPath, "utf8");
4924
+ const logText = await readFile17(logPath, "utf8");
3996
4925
  if (logText.includes("Too many MCP tools")) {
3997
4926
  const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
3998
4927
  if (banners) banners.stop(msg);
@@ -4049,7 +4978,7 @@ async function runPlanLoop(opts) {
4049
4978
  const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
4050
4979
  if (banners) banners.phaseComplete(finishDetail);
4051
4980
  console.log(
4052
- `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path29.relative(opts.root, logDir)}/`
4981
+ `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path34.relative(opts.root, logDir)}/`
4053
4982
  );
4054
4983
  if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
4055
4984
  await armExternalPlanReview(opts.root);
@@ -4061,7 +4990,7 @@ async function runPlanLoop(opts) {
4061
4990
  }
4062
4991
 
4063
4992
  // src/commands/run-plan.ts
4064
- var runPlanCommand = defineCommand10({
4993
+ var runPlanCommand = defineCommand13({
4065
4994
  meta: {
4066
4995
  name: "run-plan",
4067
4996
  description: "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod."
@@ -4120,7 +5049,7 @@ var runPlanCommand = defineCommand10({
4120
5049
  return;
4121
5050
  }
4122
5051
  const code = await runPlanLoop({
4123
- root: path30.resolve(args.cwd),
5052
+ root: path35.resolve(args.cwd),
4124
5053
  maxTicks,
4125
5054
  sleepSeconds,
4126
5055
  model: args.model ? String(args.model) : void 0,
@@ -4132,8 +5061,8 @@ var runPlanCommand = defineCommand10({
4132
5061
  });
4133
5062
 
4134
5063
  // src/commands/scan.ts
4135
- import { defineCommand as defineCommand11 } from "citty";
4136
- var scanCommand = defineCommand11({
5064
+ import { defineCommand as defineCommand14 } from "citty";
5065
+ var scanCommand = defineCommand14({
4137
5066
  meta: {
4138
5067
  name: "scan",
4139
5068
  description: "Scan the current repository and print detected profile."
@@ -4154,8 +5083,8 @@ var scanCommand = defineCommand11({
4154
5083
  });
4155
5084
 
4156
5085
  // src/commands/status.ts
4157
- import path31 from "path";
4158
- import { defineCommand as defineCommand12 } from "citty";
5086
+ import path36 from "path";
5087
+ import { defineCommand as defineCommand15 } from "citty";
4159
5088
  function profileStatus(profile) {
4160
5089
  if (!profile) return { origin: "none", evidence: [], profile: null };
4161
5090
  if ("detection" in profile && profile.detection && typeof profile.detection === "object") {
@@ -4168,7 +5097,7 @@ function profileStatus(profile) {
4168
5097
  }
4169
5098
  return { origin: "legacy-wizard", evidence: [], profile };
4170
5099
  }
4171
- var statusCommand = defineCommand12({
5100
+ var statusCommand = defineCommand15({
4172
5101
  meta: {
4173
5102
  name: "status",
4174
5103
  description: "Show Agent Kit distribution status (manifest + optional wizard profile)."
@@ -4185,11 +5114,11 @@ var statusCommand = defineCommand12({
4185
5114
  }
4186
5115
  },
4187
5116
  async run({ args }) {
4188
- const rootDir = path31.resolve(args.cwd);
5117
+ const rootDir = path36.resolve(args.cwd);
4189
5118
  const [manifest, rawProfile, scan] = await Promise.all([
4190
5119
  loadAgentKitManifest(rootDir),
4191
5120
  readJson(
4192
- path31.join(rootDir, ".cursor", "agent-kit.config.json")
5121
+ path36.join(rootDir, ".cursor", "agent-kit.config.json")
4193
5122
  ),
4194
5123
  runScanner(rootDir)
4195
5124
  ]);
@@ -4244,13 +5173,13 @@ var statusCommand = defineCommand12({
4244
5173
  });
4245
5174
 
4246
5175
  // src/commands/update.ts
4247
- import { defineCommand as defineCommand13 } from "citty";
5176
+ import { defineCommand as defineCommand16 } from "citty";
4248
5177
 
4249
5178
  // src/lifecycle/check-updates.ts
4250
- import { execFile as execFile3 } from "child_process";
4251
- import path32 from "path";
4252
- import { promisify as promisify3 } from "util";
4253
- var execFileAsync2 = promisify3(execFile3);
5179
+ import { execFile as execFile4 } from "child_process";
5180
+ import path37 from "path";
5181
+ import { promisify as promisify4 } from "util";
5182
+ var execFileAsync3 = promisify4(execFile4);
4254
5183
  var SEMVER_CORE = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i;
4255
5184
  var FACTORY_URL_MARKERS = ["agent-kit-dev"];
4256
5185
  var FACTORY_REFS = /* @__PURE__ */ new Set(["staging", "homologacao", "develop", "dev"]);
@@ -4309,7 +5238,7 @@ function pickLatestSemverTag(lsRemoteStdout) {
4309
5238
  }
4310
5239
  async function fetchLatestPublicVersion(registryUrl = DEFAULT_REGISTRY_URL) {
4311
5240
  assertSafeRegistrySource(registryUrl, "main");
4312
- const { stdout } = await execFileAsync2("git", ["ls-remote", "--tags", "--", registryUrl], {
5241
+ const { stdout } = await execFileAsync3("git", ["ls-remote", "--tags", "--", registryUrl], {
4313
5242
  env: gitEnv2(),
4314
5243
  timeout: 2e4
4315
5244
  });
@@ -4339,11 +5268,11 @@ function intervalElapsed(lastCheckedAt, intervalDays) {
4339
5268
  return Date.now() - last >= ms;
4340
5269
  }
4341
5270
  async function loadContextConfig(cwd) {
4342
- const configPath = path32.join(cwd, ".cursor", "context", "config.json");
5271
+ const configPath = path37.join(cwd, ".cursor", "context", "config.json");
4343
5272
  return readJson(configPath);
4344
5273
  }
4345
5274
  async function stampLastCheckedAt(cwd) {
4346
- const configPath = path32.join(cwd, ".cursor", "context", "config.json");
5275
+ const configPath = path37.join(cwd, ".cursor", "context", "config.json");
4347
5276
  const existing = await loadContextConfig(cwd) ?? {};
4348
5277
  const prev = existing.updateCheck && typeof existing.updateCheck === "object" ? { ...existing.updateCheck } : {};
4349
5278
  existing.updateCheck = {
@@ -4484,7 +5413,7 @@ async function checkForUpdates(cwd, options = {}) {
4484
5413
  }
4485
5414
 
4486
5415
  // src/commands/update.ts
4487
- var updateCommand = defineCommand13({
5416
+ var updateCommand = defineCommand16({
4488
5417
  meta: {
4489
5418
  name: "update",
4490
5419
  description: "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths. Use --check for notify-only."
@@ -4566,8 +5495,188 @@ var updateCommand = defineCommand13({
4566
5495
  }
4567
5496
  });
4568
5497
 
5498
+ // src/commands/validate.ts
5499
+ import { readFile as readFile18 } from "fs/promises";
5500
+ import path38 from "path";
5501
+ import { defineCommand as defineCommand17 } from "citty";
5502
+
5503
+ // src/invariants/plan-schema.ts
5504
+ var CITE5 = "agent-kit validate plan (.cursor/context/templates/plan.md)";
5505
+ function validatePlanFrontmatterText(text) {
5506
+ const warnings = [];
5507
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
5508
+ if (!match?.[1]) {
5509
+ warnings.push({
5510
+ code: "missing-frontmatter",
5511
+ message: "Plan file has no YAML frontmatter block.",
5512
+ cite: CITE5
5513
+ });
5514
+ return warnings;
5515
+ }
5516
+ const block = match[1];
5517
+ if (!/^todos:\s*$/m.test(block) && !/^todos:\s*\[/m.test(block)) {
5518
+ if (!/^todos:/m.test(block)) {
5519
+ warnings.push({
5520
+ code: "missing-todos",
5521
+ message: "Plan frontmatter has no `todos:` key.",
5522
+ cite: CITE5
5523
+ });
5524
+ }
5525
+ }
5526
+ if (!/^name:\s*\S+/m.test(block)) {
5527
+ warnings.push({
5528
+ code: "missing-name",
5529
+ message: "Plan frontmatter has no `name:` key.",
5530
+ cite: CITE5
5531
+ });
5532
+ }
5533
+ const hasTodoItem = /^- id:\s*\S+/m.test(block);
5534
+ if (/^todos:/m.test(block) && !hasTodoItem && !/^todos:\s*\[\s*\]/m.test(block)) {
5535
+ warnings.push({
5536
+ code: "empty-todos",
5537
+ message: "Plan frontmatter `todos:` has no `- id:` items.",
5538
+ cite: CITE5
5539
+ });
5540
+ }
5541
+ return warnings;
5542
+ }
5543
+
5544
+ // src/commands/validate.ts
5545
+ async function resolveEditedPath(cwd, explicit) {
5546
+ if (explicit) {
5547
+ const filePath2 = path38.resolve(cwd, explicit);
5548
+ try {
5549
+ return { filePath: filePath2, content: await readFile18(filePath2, "utf8") };
5550
+ } catch {
5551
+ return null;
5552
+ }
5553
+ }
5554
+ const payload = await readStdinJson();
5555
+ const rel = typeof payload.file_path === "string" && payload.file_path || typeof payload.path === "string" && payload.path || typeof payload.file === "string" && payload.file || "";
5556
+ if (!rel) return null;
5557
+ const filePath = path38.isAbsolute(rel) ? rel : path38.resolve(cwd, rel);
5558
+ try {
5559
+ return { filePath, content: await readFile18(filePath, "utf8") };
5560
+ } catch {
5561
+ return null;
5562
+ }
5563
+ }
5564
+ function isHandoffPath(filePath) {
5565
+ return filePath.replace(/\\/g, "/").endsWith(".cursor/HANDOFF.md");
5566
+ }
5567
+ function isPlanPath(filePath) {
5568
+ const norm = filePath.replace(/\\/g, "/");
5569
+ return norm.includes("/.cursor/plans/") && norm.endsWith(".plan.md");
5570
+ }
5571
+ var validateCommand = defineCommand17({
5572
+ meta: {
5573
+ name: "validate",
5574
+ description: "Advisory validators for HANDOFF / plan frontmatter (afterFileEdit adapter)"
5575
+ },
5576
+ subCommands: {
5577
+ handoff: defineCommand17({
5578
+ meta: { name: "handoff", description: "Validate HANDOFF machine fields" },
5579
+ args: {
5580
+ cwd: { type: "string", default: process.cwd() },
5581
+ file: { type: "string", description: "Path to HANDOFF.md" },
5582
+ json: { type: "boolean", default: true }
5583
+ },
5584
+ async run({ args }) {
5585
+ const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
5586
+ const fileArg = typeof args.file === "string" ? args.file : void 0;
5587
+ const filePath = fileArg ? path38.resolve(cwd, fileArg) : path38.join(path38.resolve(cwd), ".cursor", "HANDOFF.md");
5588
+ let content = "";
5589
+ try {
5590
+ content = await readFile18(filePath, "utf8");
5591
+ } catch {
5592
+ console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
5593
+ return;
5594
+ }
5595
+ const warnings = validateHandoffText(content);
5596
+ console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
5597
+ }
5598
+ }),
5599
+ plan: defineCommand17({
5600
+ meta: { name: "plan", description: "Validate plan frontmatter" },
5601
+ args: {
5602
+ cwd: { type: "string", default: process.cwd() },
5603
+ file: { type: "string", description: "Path to *.plan.md" },
5604
+ json: { type: "boolean", default: true }
5605
+ },
5606
+ async run({ args }) {
5607
+ const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
5608
+ const fileArg = typeof args.file === "string" ? args.file : void 0;
5609
+ if (!fileArg) {
5610
+ console.log(JSON.stringify({ ok: false, warnings: [{ message: "file required" }] }));
5611
+ process.exitCode = 2;
5612
+ return;
5613
+ }
5614
+ const filePath = path38.resolve(cwd, fileArg);
5615
+ let content = "";
5616
+ try {
5617
+ content = await readFile18(filePath, "utf8");
5618
+ } catch {
5619
+ console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
5620
+ return;
5621
+ }
5622
+ const warnings = validatePlanFrontmatterText(content);
5623
+ console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
5624
+ }
5625
+ }),
5626
+ "after-edit": defineCommand17({
5627
+ meta: {
5628
+ name: "after-edit",
5629
+ description: "Advisory afterFileEdit: annotate HANDOFF/plan issues (never block)"
5630
+ },
5631
+ args: {
5632
+ cwd: { type: "string", default: process.cwd() }
5633
+ },
5634
+ async run({ args }) {
5635
+ const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
5636
+ const resolved = await resolveEditedPath(path38.resolve(cwd));
5637
+ if (!resolved) {
5638
+ console.log(JSON.stringify({}));
5639
+ return;
5640
+ }
5641
+ const { filePath, content } = resolved;
5642
+ if (isHandoffPath(filePath)) {
5643
+ const warnings = validateHandoffText(content);
5644
+ if (!warnings.length) {
5645
+ console.log(JSON.stringify({}));
5646
+ return;
5647
+ }
5648
+ const msg = warnings.map((w) => w.message).join(" ");
5649
+ console.log(
5650
+ JSON.stringify({
5651
+ user_message: msg,
5652
+ agent_message: `${msg} Cite: agent-kit validate handoff.`
5653
+ })
5654
+ );
5655
+ return;
5656
+ }
5657
+ if (isPlanPath(filePath)) {
5658
+ const warnings = validatePlanFrontmatterText(content);
5659
+ if (!warnings.length) {
5660
+ console.log(JSON.stringify({}));
5661
+ return;
5662
+ }
5663
+ const msg = warnings.map((w) => w.message).join(" ");
5664
+ console.log(
5665
+ JSON.stringify({
5666
+ user_message: msg,
5667
+ agent_message: `${msg} Cite: agent-kit validate plan.`
5668
+ })
5669
+ );
5670
+ return;
5671
+ }
5672
+ console.log(JSON.stringify({}));
5673
+ }
5674
+ })
5675
+ }
5676
+ });
5677
+
4569
5678
  // src/index.ts
4570
- var main = defineCommand14({
5679
+ var main = defineCommand18({
4571
5680
  meta: {
4572
5681
  name: "agent-kit",
4573
5682
  description: "HITL framework for AI-assisted IDEs"
@@ -4585,7 +5694,11 @@ var main = defineCommand14({
4585
5694
  handoff: handoffCommand,
4586
5695
  "run-plan": runPlanCommand,
4587
5696
  dashboard: dashboardCommand,
4588
- "dashboard-broadcast": dashboardBroadcastCommand
5697
+ "dashboard-broadcast": dashboardBroadcastCommand,
5698
+ hook: hookCommand,
5699
+ guard: guardCommand,
5700
+ monitors: monitorsCommand,
5701
+ validate: validateCommand
4589
5702
  }
4590
5703
  });
4591
5704
  runMain(main);