@dadado/agent-kit-cli 5.8.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +3199 -423
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -273,7 +273,74 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
273
273
  "3a88df22375f90a3a6743f50d9a01129e0373f4ca8dff03fb744c1de3c149409",
274
274
  "481a9994ccecaa611173f9ef91a1dbe7458d89758c6a6b5e7c3bbbbf460888ce",
275
275
  "691d91fd1e9032b6f9315f29348d4473201917da257258afd74eb954298d8d71",
276
- "ab0808b7dbc333e523b87b126bb655353cde2574db30d849cc9c9037843bef8c"
276
+ "ab0808b7dbc333e523b87b126bb655353cde2574db30d849cc9c9037843bef8c",
277
+ "85514e26af694bbe38e7ddf625710a32719ddf6afc173843c9cdfbffdad8e466",
278
+ "35b6c0cde7538fb1837d54ba841668e82ac7b9e3ef274adcea3d7f34ba4bfb64",
279
+ "4230e45f2ccd89cf8e25c56d21dfbfbe134613c686ea361243f94c49cf6802af",
280
+ "b4fbb2c15a1330ba36351a90490696fd558a347d25ac14af768017d6155335c0",
281
+ "c37ee51662b6eca65224d737911a4bb209a3742694b37a4d00ed29abc4e0b253",
282
+ "f46a0e36c1ee887ad7e62fa00a8facefe90605827de7bf2e6f90655d6660ed0e",
283
+ "40edd2d21be802af95501cf3492665ee48423439cfc64c14f17ed1e7eb8bdfdf",
284
+ "08c15346f42360bd7a4b4e803ca88e5f1727d9db106e7dc4d96523ee7aba129a",
285
+ "0cc36b633ca1a18fa472ed8040a5b927b4c918318dbd17f6163b706cd7a8acbe",
286
+ "0fdc92828174abba815db3799a274aca70cccacca573d949e412d4622928ec50",
287
+ "33fc7d6bc49cea32bd737e46aaf63ad2bedbc72a52615fc2b881e6ddbb34587a",
288
+ "45c475337798bdbf5bd3a42c1f085278d1563a9e76c1295961b1ce66a89b4820",
289
+ "484df32ebf132fb121ab1e31b0e64345058c9c82b2a39498cc06d1794c270f23",
290
+ "60aa86ca3f2873908e57c2830675eec393c114cd711b3b162d02be355792b697",
291
+ "85c307d01e36dc62d4a341c1932b0a7fae3663f28ec0e32ad5cda30f35ffca86",
292
+ "9133d73c070243947b3bf9ffec78d21d66a5cb7488c11a4bf731b3c9c3529b3c",
293
+ "c082e903d9569b297c93a41ea561718a5258dfefd6a0f0457402f565caa1bbd0",
294
+ "c10bcbc952e58e8f89818c5119cde7b54bab5b74c5b194b5d26b2b125ec1036a",
295
+ "ed68d0fe3dca46c159402a1ae62148d2482352718be792ff65fd38efae706bef",
296
+ "8356eda7f3597c3d3726b32f87f6f4da87999be93760e738748138acc2a3d73e",
297
+ "90a8e3a67e41c33b93da824290148e4af84d3d49848727fa88b34d8df9bad29a",
298
+ "ec51cebd225fc861072b62e4ee0292fd4ba01ba0817770e7a379ad41d7bd850a",
299
+ "3edb378d29c89081c9ad6439b077aa928a21aed864aed5ef02e819b75ac33d7a",
300
+ "634f1d9ac6f632074054c5b029567303d783f1fc01e62be588668d20044fadcc",
301
+ "bc5c76a5b63fad600c94786300284987f54d237e3f7d38bb8b520906a9685ba2",
302
+ "efcbbf40f1e616e2e7ae513765463806b192bd5e6b8acd3be832bf590c14a542",
303
+ "2ef2688b31b86a4bbf358f3662495a65314b2edf2b7b2215d71e5be9cff90d22",
304
+ "a9cad5b77ae12cfa907a4e460d39be75c8f864a27e7f85394bef67120a6fba45",
305
+ "c35c76052f76cb45a970fcb825d5ddc49f7e371441f0953735ffb9d072da2680",
306
+ "611279d799db865e5329ed8723d73ebd2d839c273b52aef12803b4b11a75a75a",
307
+ "28840c700bdf8968b6f02c6bbb9cf965643f30f0911b2622a9c01dfae233d8fb",
308
+ "947f39141a3f830cd29145a74058d48c2feb8fbcd958ea7f90522e46fb0c25c5",
309
+ "c8535a73997daf1800394626d0e4845c923544040bae85a724ca427f56ba9812",
310
+ "9486e4ae754782114b1ec7d22baf661a8f33413d01fcf3a0759f97f1eedd84c1",
311
+ "8453f7d9d818b101d0d7b0cf5d0985e57174285f4cb7899517d5af52bc6490d5",
312
+ "9a877468512ffa14e77940757b3532c20f6fda7878419c9986f6d9e03ca31081",
313
+ "db310ca8f9841b5937f475ab042d768f84bc00a29282fbcce159fc9bf7646921",
314
+ "052217cbd533ef7214bb55f9e7a7d8cb979f0a4b32e42f8b98b57ebd86935d25",
315
+ "0aab08e34dd433fd38fa3f4d6729d1a7a2f626387fdb2b9bc8a48bd0f84be2f3",
316
+ "4bdbc2837b195c529d83302bd3c830d7d669879757ac3f92ec1d1ac703b21522",
317
+ "744c0d44e26b8a8fb7272b07eff884da583a8b73a50f7a1e4a9be55ecb40b39e",
318
+ "7b791a2ec4ff31bb6f19d60bc19152fb1ee5139b56254b9a3ba783a7f1bf9016",
319
+ "7e5a4a5f88ee48f17fe40e66772da4816480b1fba5fff5a58026d05753614393",
320
+ "c0b51e50d6117ce597c096386a1928f803e3abf1a65f6948ff7b00583651db5e",
321
+ "d1c8ea35ae9bdb5dad47c3ab80d73d5a327a0379216bead38eda18c0f2836fe9",
322
+ "e299dd585ed6c37292758d2821663aa46a80d6c7964f67b3d1d3279cf746e764",
323
+ "51a5608322e14694fafad15cbb710663800dc315d4deb12516c1591ac94e3ed7",
324
+ "78d553f98cc172dd6f0f3b2e01056813a32a894f6bdca64e665265df84a8eb0b",
325
+ "90c466fbe5bf7e6f3936b8a46407fbd5d0d542582c39b697ecbb5ada1be482ce",
326
+ "d2f539aa6340b1e41f6f47d179a2c17666415b82e7bf02be9416061855b5534c",
327
+ "05034e22b01b830b1fe12df4834e6c57eb0b152539e469d8d4e393b2b61dd76d",
328
+ "3268c3549d9634aefe29e399961a3be0ebb6e205d454609411ea877da4552d51",
329
+ "5c583058ae2d35e9a650a7a0d2ee304be8e71e361b51ea6903b2cd6ac14fd6a1",
330
+ "7c3cb1f7f449b6a459532d1dc315d574b7148f0d373f3e3271a52ad0151e0eb4",
331
+ "1163b51a64118363c90db5f0b8f7280fcea335631768f83e64fdf1948b41df89",
332
+ "a1590ecae718e7c43f356ddfc76c983f499725970a1953bf6d4d2d4ebf8fa1dc",
333
+ "2b69c26c0dacd5596906178138f85b2b14076552e1995817ea64bf99f48e0ed2",
334
+ "6e3e62f4d47d66cc55f659cf37b7c1ff4f9b43581b9470e740d60768217c1a23",
335
+ "60a3ef8eddf0f262fce40a32b715ee4c2e68d52e18f74d790e68a0f54c8657f1",
336
+ "dfd40e3c2c62a2810030e21a78f71f2646eab19b3658e326ecb5cd8def258aad",
337
+ "ee5572cfba7b96285c433ea2f6e469a205c783fba216af6cf4aafefdbf33ff89",
338
+ "85cd5f00c51efcc263b45fe91d1e10278a866d9266392e0ed480cd8ef5da6cc1",
339
+ "8ea5a65412bf4b95fdbb57fcce7c1a046848f161cf8235a3d9b7e752068775a8",
340
+ "974b143673aa6bd202205264d82f30898777e5694d4269449f564fd536f402d7",
341
+ "2317949f51d8a9d31b700c3af60b6fd3bccff1a8520af7707d7909f42bc5cff2",
342
+ "18426bc3bc8fdf0ab7577d92e31310faf3b83001f8e6e6fe5ef1a1043230e178",
343
+ "24cc918e7ffa54df7f6007989fe62b83af1035c9bf6071cac8c4f848e28b04ca"
277
344
  ]);
278
345
 
279
346
  // src/lifecycle/paths.ts
@@ -942,6 +1009,9 @@ function loadPackageVersion() {
942
1009
  }
943
1010
  var KIT_VERSION = loadPackageVersion();
944
1011
  var KIT_PACKAGE_SPEC = "@dadado/agent-kit-cli";
1012
+ function pinnedCliSpec(version = KIT_VERSION) {
1013
+ return `${KIT_PACKAGE_SPEC}@${version}`;
1014
+ }
945
1015
 
946
1016
  // src/manifest/index.ts
947
1017
  import path6 from "path";
@@ -1517,6 +1587,54 @@ var L0_ARTIFACTS = [
1517
1587
  source: ".cursor/rules/hitl-ask-questions.mdc",
1518
1588
  target: ".cursor/rules/hitl-ask-questions.mdc"
1519
1589
  },
1590
+ {
1591
+ source: ".cursor/skills/core/hitl-gates/SKILL.md",
1592
+ target: ".cursor/skills/core/hitl-gates/SKILL.md"
1593
+ },
1594
+ {
1595
+ source: ".cursor/skills/core/hitl-gates/procedures.md",
1596
+ target: ".cursor/skills/core/hitl-gates/procedures.md"
1597
+ },
1598
+ {
1599
+ source: ".cursor/skills/core/hitl-gates/run-plan-tick-contract.md",
1600
+ target: ".cursor/skills/core/hitl-gates/run-plan-tick-contract.md"
1601
+ },
1602
+ {
1603
+ source: ".cursor/skills/core/hitl-gates/start-project-intake.md",
1604
+ target: ".cursor/skills/core/hitl-gates/start-project-intake.md"
1605
+ },
1606
+ {
1607
+ source: ".cursor/skills/core/hitl-gates/run-plan-all-queue.md",
1608
+ target: ".cursor/skills/core/hitl-gates/run-plan-all-queue.md"
1609
+ },
1610
+ {
1611
+ source: ".cursor/skills/core/qa/SKILL.md",
1612
+ target: ".cursor/skills/core/qa/SKILL.md"
1613
+ },
1614
+ {
1615
+ source: ".cursor/skills/core/dashboard-broadcast/SKILL.md",
1616
+ target: ".cursor/skills/core/dashboard-broadcast/SKILL.md"
1617
+ },
1618
+ {
1619
+ source: ".cursor/skills/core/plan-review-triage/procedure.md",
1620
+ target: ".cursor/skills/core/plan-review-triage/procedure.md"
1621
+ },
1622
+ {
1623
+ source: ".cursor/skills/core/plan-external-review/procedure.md",
1624
+ target: ".cursor/skills/core/plan-external-review/procedure.md"
1625
+ },
1626
+ {
1627
+ source: ".cursor/skills/core/agent-kit-onboard/procedure.md",
1628
+ target: ".cursor/skills/core/agent-kit-onboard/procedure.md"
1629
+ },
1630
+ {
1631
+ source: ".cursor/skills/core/field-report-resolve/procedure.md",
1632
+ target: ".cursor/skills/core/field-report-resolve/procedure.md"
1633
+ },
1634
+ {
1635
+ source: ".cursor/skills/core/backlog-add/procedure.md",
1636
+ target: ".cursor/skills/core/backlog-add/procedure.md"
1637
+ },
1520
1638
  {
1521
1639
  source: "registry/rules/git-secrets-safety.mdc",
1522
1640
  target: ".cursor/rules/git-secrets-safety.mdc"
@@ -1586,11 +1704,8 @@ var L0_ARTIFACTS = [
1586
1704
  target: ".cursor/commands/git-staging.md"
1587
1705
  },
1588
1706
  { source: ".cursor/commands/git-prod.md", target: ".cursor/commands/git-prod.md" },
1589
- {
1590
- source: ".cursor/commands/kit-staging.md",
1591
- target: ".cursor/commands/kit-staging.md"
1592
- },
1593
- { source: ".cursor/commands/kit-prod.md", target: ".cursor/commands/kit-prod.md" },
1707
+ // Factory landing wraps: keep `.cursor/commands/kit-staging.md` and
1708
+ // `kit-prod.md` on disk. They are not consumer L0 (surface diet).
1594
1709
  {
1595
1710
  source: ".cursor/commands/plan-external-review.md",
1596
1711
  target: ".cursor/commands/plan-external-review.md"
@@ -1614,6 +1729,10 @@ var L0_ARTIFACTS = [
1614
1729
  source: ".cursor/commands/cursor-update-awareness.md",
1615
1730
  target: ".cursor/commands/cursor-update-awareness.md"
1616
1731
  },
1732
+ {
1733
+ source: ".cursor/commands/qa.md",
1734
+ target: ".cursor/commands/qa.md"
1735
+ },
1617
1736
  {
1618
1737
  source: ".cursor/commands/update.md",
1619
1738
  target: ".cursor/commands/update.md"
@@ -2164,6 +2283,94 @@ function parseFeatureMapNames(markdown) {
2164
2283
  }
2165
2284
  return names;
2166
2285
  }
2286
+ var HEADING_NOISE = /* @__PURE__ */ new Set([
2287
+ "changelog",
2288
+ "improvements",
2289
+ "bug fixes",
2290
+ "get started",
2291
+ "settings",
2292
+ "powered by",
2293
+ "powered by cloud agents",
2294
+ "shared context",
2295
+ "desktop improvements",
2296
+ "desktop bug fixes",
2297
+ "agents window improvements",
2298
+ "agents window bug fixes",
2299
+ "what's new in cursor",
2300
+ "latest updates",
2301
+ "release notes"
2302
+ ]);
2303
+ var MAX_FEATURE_KEYWORD_GAPS = 12;
2304
+ function visibleTextFromHtml(html) {
2305
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<svg[\s\S]*?<\/svg>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&#?\w+;/g, " ").replace(/\s+/g, " ").trim();
2306
+ }
2307
+ function normalizeFeatureKeyword(raw) {
2308
+ let value = raw.replace(/^#+\s*/, "").replace(/\s+/g, " ").trim();
2309
+ value = value.replace(/[.,:;]+$/g, "");
2310
+ value = value.replace(/\s+(improvements|bug fixes)$/i, "").trim();
2311
+ if (!value) return null;
2312
+ if (value.startsWith("/")) {
2313
+ const cmd = value.match(/^\/[a-z][a-z0-9-]{1,32}$/i);
2314
+ return cmd?.[0] ? cmd[0].toLowerCase() : null;
2315
+ }
2316
+ const lower = value.toLowerCase();
2317
+ if (HEADING_NOISE.has(lower)) return null;
2318
+ if (/^\d+(\.\d+){1,2}$/.test(value)) return null;
2319
+ if (value.length < 4 || value.length > 80) return null;
2320
+ return value;
2321
+ }
2322
+ function slugifyFeatureKeyword(name) {
2323
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
2324
+ }
2325
+ function extractChangelogFeatureKeywords(changelogText) {
2326
+ const text = String(changelogText ?? "");
2327
+ const buckets = {
2328
+ slash: [],
2329
+ h1: [],
2330
+ h2: [],
2331
+ h3: []
2332
+ };
2333
+ const seen = /* @__PURE__ */ new Set();
2334
+ const push = (raw, bucket) => {
2335
+ const keyword = normalizeFeatureKeyword(raw);
2336
+ if (!keyword) return;
2337
+ const key = keyword.toLowerCase();
2338
+ if (seen.has(key)) return;
2339
+ seen.add(key);
2340
+ buckets[bucket].push(keyword);
2341
+ };
2342
+ for (const match of text.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/gi)) {
2343
+ push(visibleTextFromHtml(match[1] ?? ""), "h1");
2344
+ }
2345
+ for (const match of text.matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>/gi)) {
2346
+ push(visibleTextFromHtml(match[1] ?? ""), "h2");
2347
+ }
2348
+ for (const match of text.matchAll(/<h3[^>]*>([\s\S]*?)<\/h3>/gi)) {
2349
+ push(visibleTextFromHtml(match[1] ?? ""), "h3");
2350
+ }
2351
+ for (const match of text.matchAll(/^#\s+(.+)$/gm)) {
2352
+ push(match[1] ?? "", "h1");
2353
+ }
2354
+ for (const match of text.matchAll(/^##\s+(.+)$/gm)) {
2355
+ push(match[1] ?? "", "h2");
2356
+ }
2357
+ for (const match of text.matchAll(/^###\s+(.+)$/gm)) {
2358
+ push(match[1] ?? "", "h3");
2359
+ }
2360
+ for (const match of text.matchAll(/(^|[\s>`(])(\/[a-z][a-z0-9-]{1,32})(?![/\w-])/gi)) {
2361
+ push(match[2] ?? "", "slash");
2362
+ }
2363
+ return [...buckets.slash, ...buckets.h1, ...buckets.h2, ...buckets.h3];
2364
+ }
2365
+ function findMissingInventoryKeywords(keywords, haystack) {
2366
+ const lower = String(haystack ?? "").toLowerCase();
2367
+ return keywords.filter((keyword) => {
2368
+ const needle = keyword.toLowerCase();
2369
+ if (lower.includes(needle)) return false;
2370
+ if (needle.startsWith("/") && lower.includes(needle.slice(1))) return false;
2371
+ return true;
2372
+ });
2373
+ }
2167
2374
  async function defaultFetchText(url) {
2168
2375
  if (!url.startsWith("https://")) {
2169
2376
  throw new Error(`Refusing non-HTTPS changelog URL: ${url}`);
@@ -2262,6 +2469,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2262
2469
  const inventoryRefreshed = parseInventoryRefreshed(inventoryMd);
2263
2470
  const openActionIds = parseOpenActionIds(inventoryMd);
2264
2471
  const gaps = [];
2472
+ let featuresMd = "";
2265
2473
  for (const id of openActionIds) {
2266
2474
  gaps.push({
2267
2475
  id: `open-action-${id}`,
@@ -2287,7 +2495,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2287
2495
  }
2288
2496
  }
2289
2497
  try {
2290
- const featuresMd = await readFile7(featuresPath, "utf8");
2498
+ featuresMd = await readFile7(featuresPath, "utf8");
2291
2499
  if (parseFeatureMapNames(featuresMd).length === 0) {
2292
2500
  gaps.push({
2293
2501
  id: "features-map-empty",
@@ -2311,6 +2519,23 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2311
2519
  try {
2312
2520
  const body = options.changelogBody ?? await (options.fetchText ?? defaultFetchText)(changelogUrl);
2313
2521
  latestCursorVersion = extractLatestCursorVersion(body);
2522
+ const haystack = `${inventoryMd}
2523
+ ${featuresMd}`;
2524
+ const missingKeywords = findMissingInventoryKeywords(
2525
+ extractChangelogFeatureKeywords(body),
2526
+ haystack
2527
+ );
2528
+ for (const keyword of missingKeywords.slice(0, MAX_FEATURE_KEYWORD_GAPS)) {
2529
+ const slug = slugifyFeatureKeyword(keyword);
2530
+ if (!slug) continue;
2531
+ gaps.push({
2532
+ id: `feature-keyword-miss:${slug}`,
2533
+ severity: "advisory",
2534
+ path: FEATURES_REL,
2535
+ evidence: `Changelog names "${keyword}" which is not listed in native-audit or cursor-3-features`,
2536
+ suggestedRoute: "backlog-add"
2537
+ });
2538
+ }
2314
2539
  if (latestCursorVersion && prefs.lastSeenCursorVersion && compareCursorVersion(latestCursorVersion, prefs.lastSeenCursorVersion) > 0) {
2315
2540
  gaps.push({
2316
2541
  id: "changelog-ahead",
@@ -2530,13 +2755,13 @@ function readPreferredBrowserFromConfig(configPath, fsHooks = {}) {
2530
2755
  }
2531
2756
 
2532
2757
  // src/commands/dashboard.ts
2533
- function readPreferredBrowserFromWorkspace(cwd, readFile28 = readFileSync2) {
2758
+ function readPreferredBrowserFromWorkspace(cwd, readFile31 = readFileSync2) {
2534
2759
  const resolved = resolveContextConfigPath(path11.resolve(cwd), {
2535
2760
  existsSync,
2536
2761
  realpathSync
2537
2762
  });
2538
2763
  if (!resolved.ok) return null;
2539
- const value = readPreferredBrowserFromConfig(resolved.path, { readFileSync: readFile28 });
2764
+ const value = readPreferredBrowserFromConfig(resolved.path, { readFileSync: readFile31 });
2540
2765
  return normalizePreferredBrowser(value);
2541
2766
  }
2542
2767
  function applyDashboardOpenEnv(env, opts) {
@@ -3659,21 +3884,81 @@ import { constants as constants3, access as access6, readFile as readFile10, sta
3659
3884
  import { homedir as homedir2 } from "os";
3660
3885
  import path16 from "path";
3661
3886
  var MIN_NODE_MAJOR = 20;
3662
- async function checkBinOnPath(binName, env, platform) {
3887
+ var CLI_PACKAGE_JSON_REL = path16.join(
3888
+ "lib",
3889
+ "node_modules",
3890
+ "@dadado",
3891
+ "agent-kit-cli",
3892
+ "package.json"
3893
+ );
3894
+ var DIST_INDEX_RE = /(?:\$\{basedir\}|\$basedir)?[^'"\s;]*dist\/index\.js/g;
3895
+ function extractDistIndexFromBinScript(contents) {
3896
+ const found = [];
3897
+ DIST_INDEX_RE.lastIndex = 0;
3898
+ for (const match of contents.matchAll(DIST_INDEX_RE)) {
3899
+ const raw = match[0]?.trim();
3900
+ if (raw) found.push(raw.replace(/\\/g, "/"));
3901
+ }
3902
+ return found;
3903
+ }
3904
+ function expandBinDirVars(raw, binDir) {
3905
+ return raw.replace(/\$\{basedir\}/g, binDir).replace(/\$basedir/g, binDir);
3906
+ }
3907
+ function packageJsonFromDistIndex(distIndex) {
3908
+ return path16.join(path16.dirname(distIndex), "..", "package.json");
3909
+ }
3910
+ function parsePackageVersion(contents) {
3911
+ try {
3912
+ const parsed = JSON.parse(contents);
3913
+ return typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : null;
3914
+ } catch {
3915
+ return null;
3916
+ }
3917
+ }
3918
+ async function readBinVersion(binPath, options = {}) {
3919
+ const readFileImpl = options.readFileImpl ?? ((filePath) => readFile10(filePath, "utf8"));
3920
+ const binDir = path16.dirname(binPath);
3921
+ const candidates = [
3922
+ path16.join(binDir, "..", CLI_PACKAGE_JSON_REL),
3923
+ path16.join(binDir, "..", "node_modules", "@dadado", "agent-kit-cli", "package.json")
3924
+ ];
3925
+ try {
3926
+ const contents = await readFileImpl(binPath);
3927
+ for (const raw of extractDistIndexFromBinScript(contents)) {
3928
+ const distIndex = path16.resolve(binDir, expandBinDirVars(raw, binDir));
3929
+ candidates.push(packageJsonFromDistIndex(distIndex));
3930
+ }
3931
+ } catch {
3932
+ }
3933
+ const seen = /* @__PURE__ */ new Set();
3934
+ for (const candidate2 of candidates) {
3935
+ const resolved = path16.resolve(candidate2);
3936
+ if (seen.has(resolved)) continue;
3937
+ seen.add(resolved);
3938
+ try {
3939
+ const version = parsePackageVersion(await readFileImpl(resolved));
3940
+ if (version) return version;
3941
+ } catch {
3942
+ }
3943
+ }
3944
+ return null;
3945
+ }
3946
+ async function resolveBinOnPath(binName, env, platform) {
3663
3947
  const pathVar = env.PATH ?? env.Path ?? "";
3664
- if (!pathVar) return false;
3948
+ if (!pathVar) return null;
3665
3949
  const dirs = pathVar.split(path16.delimiter).filter(Boolean);
3666
3950
  const candidates = platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.exe`, `${binName}.bat`] : [binName];
3667
3951
  for (const dir of dirs) {
3668
3952
  for (const candidate2 of candidates) {
3953
+ const abs = path16.join(dir, candidate2);
3669
3954
  try {
3670
- await access6(path16.join(dir, candidate2), platform === "win32" ? void 0 : constants3.X_OK);
3671
- return true;
3955
+ await access6(abs, platform === "win32" ? void 0 : constants3.X_OK);
3956
+ return abs;
3672
3957
  } catch {
3673
3958
  }
3674
3959
  }
3675
3960
  }
3676
- return false;
3961
+ return null;
3677
3962
  }
3678
3963
  function isNodeVersionOk(nodeVersion, minMajor = MIN_NODE_MAJOR) {
3679
3964
  const match = /^v?(\d+)/.exec(nodeVersion);
@@ -3769,8 +4054,8 @@ async function assessEnvironment(options = {}) {
3769
4054
  const nodeVersion = options.nodeVersion ?? process.version;
3770
4055
  const homeDir = options.homeDir ?? homedir2();
3771
4056
  const binName = options.binName ?? "agent-kit";
3772
- const [binOnPath, npmPrefix] = await Promise.all([
3773
- checkBinOnPath(binName, env, platform).catch(() => false),
4057
+ const [binPath, npmPrefix] = await Promise.all([
4058
+ resolveBinOnPath(binName, env, platform).catch(() => null),
3774
4059
  checkNpmPrefixWritable(options).catch(
3775
4060
  () => ({
3776
4061
  prefix: null,
@@ -3779,8 +4064,11 @@ async function assessEnvironment(options = {}) {
3779
4064
  })
3780
4065
  )
3781
4066
  ]);
4067
+ const binVersion = binPath ? await readBinVersion(binPath, options).catch(() => null) : null;
3782
4068
  return {
3783
- binOnPath,
4069
+ binOnPath: binPath != null,
4070
+ binPath,
4071
+ binVersion,
3784
4072
  npmPrefixWritable: npmPrefix.writable,
3785
4073
  npmPrefix,
3786
4074
  nodeVersionOk: isNodeVersionOk(nodeVersion),
@@ -3949,21 +4237,30 @@ function buildPillars(scan) {
3949
4237
  )
3950
4238
  ]),
3951
4239
  pillar("quality-ci", [
3952
- check(
3953
- "quality.validation",
3954
- "Tests and validation",
3955
- scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? "ready" : "manual",
3956
- false,
3957
- scan.infra.ciFiles.map((value) => ({ source: "file", value })),
3958
- scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? [] : [
3959
- action(
3960
- "document-validation",
3961
- "manual",
3962
- "Document a repeatable repository validation command",
3963
- "user"
3964
- )
3965
- ]
3966
- )
4240
+ (() => {
4241
+ const hasValidation = scan.quality.hasTests || scan.quality.validationCommands.length > 0 || scan.quality.ciRunCommands.length > 0;
4242
+ return check(
4243
+ "quality.validation",
4244
+ "Tests and validation",
4245
+ hasValidation ? "ready" : "manual",
4246
+ false,
4247
+ [
4248
+ ...scan.infra.ciFiles.map((value) => ({ source: "file", value })),
4249
+ ...scan.quality.ciRunCommands.map((value) => ({
4250
+ source: "file",
4251
+ value: `ci:${value}`
4252
+ }))
4253
+ ],
4254
+ hasValidation ? [] : [
4255
+ action(
4256
+ "document-validation",
4257
+ "manual",
4258
+ "Document a repeatable repository validation command",
4259
+ "user"
4260
+ )
4261
+ ]
4262
+ );
4263
+ })()
3967
4264
  ]),
3968
4265
  pillar("deploy-infrastructure", [
3969
4266
  check(
@@ -4037,7 +4334,7 @@ function createReadinessReport(scan, options) {
4037
4334
  }
4038
4335
 
4039
4336
  // src/scanner/safe-fixes.ts
4040
- import { readFile as readFile13, writeFile as writeFile5 } from "fs/promises";
4337
+ import { readFile as readFile15, writeFile as writeFile5 } from "fs/promises";
4041
4338
  import path24 from "path";
4042
4339
 
4043
4340
  // src/scanner/detect-repository.ts
@@ -4410,6 +4707,7 @@ async function detectIde(rootDir) {
4410
4707
  }
4411
4708
 
4412
4709
  // src/scanner/detect-infra.ts
4710
+ import { readFile as readFile12, readdir as readdir5 } from "fs/promises";
4413
4711
  import path20 from "path";
4414
4712
 
4415
4713
  // src/types.ts
@@ -4465,6 +4763,51 @@ var PM_TOOL_LABELS = {
4465
4763
  };
4466
4764
 
4467
4765
  // src/scanner/detect-infra.ts
4766
+ function extractRunCommands(yaml) {
4767
+ const lines = yaml.split(/\r?\n/);
4768
+ const commands = [];
4769
+ for (let i = 0; i < lines.length; i++) {
4770
+ const line = lines[i] ?? "";
4771
+ const match = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(line);
4772
+ if (!match) continue;
4773
+ const [, indent = "", inline = ""] = match;
4774
+ const trimmedInline = inline.trim();
4775
+ if (trimmedInline && !/^[|>][+-]?$/.test(trimmedInline)) {
4776
+ commands.push(trimmedInline.replace(/^['"]|['"]$/g, ""));
4777
+ continue;
4778
+ }
4779
+ const blockIndent = indent.length;
4780
+ for (let j = i + 1; j < lines.length; j++) {
4781
+ const blockLine = lines[j] ?? "";
4782
+ if (!blockLine.trim()) continue;
4783
+ const lineIndent = blockLine.length - blockLine.trimStart().length;
4784
+ if (lineIndent <= blockIndent) break;
4785
+ commands.push(blockLine.trim());
4786
+ }
4787
+ }
4788
+ return Array.from(new Set(commands));
4789
+ }
4790
+ async function detectCiRunCommands(rootDir) {
4791
+ const workflowsDir = path20.join(rootDir, ".github", "workflows");
4792
+ if (!await fileExists(workflowsDir)) return [];
4793
+ let entries;
4794
+ try {
4795
+ entries = await readdir5(workflowsDir);
4796
+ } catch {
4797
+ return [];
4798
+ }
4799
+ const workflowFiles = entries.filter((name) => /\.ya?ml$/.test(name));
4800
+ const perFile = await Promise.all(
4801
+ workflowFiles.map(async (name) => {
4802
+ try {
4803
+ return extractRunCommands(await readFile12(path20.join(workflowsDir, name), "utf8"));
4804
+ } catch {
4805
+ return [];
4806
+ }
4807
+ })
4808
+ );
4809
+ return Array.from(new Set(perFile.flat()));
4810
+ }
4468
4811
  async function detectInfra(rootDir) {
4469
4812
  const docker = await fileExists(path20.join(rootDir, "Dockerfile")) || await fileExists(path20.join(rootDir, "docker-compose.yml")) || await fileExists(path20.join(rootDir, "docker-compose.yaml"));
4470
4813
  const kubernetes = await fileExists(path20.join(rootDir, "k8s")) || await fileExists(path20.join(rootDir, "kubernetes"));
@@ -4504,11 +4847,12 @@ async function detectInfra(rootDir) {
4504
4847
  async (file) => await fileExists(path20.join(rootDir, file)) ? file : void 0
4505
4848
  )
4506
4849
  )).filter((file) => file !== void 0);
4507
- return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles };
4850
+ const ciRunCommands = ci === "github-actions" ? await detectCiRunCommands(rootDir) : [];
4851
+ return { docker, kubernetes, ci, ciFiles, infrastructureFiles, deploymentFiles, ciRunCommands };
4508
4852
  }
4509
4853
 
4510
4854
  // src/scanner/detect-services.ts
4511
- import { readFile as readFile12 } from "fs/promises";
4855
+ import { readFile as readFile13 } from "fs/promises";
4512
4856
  import path21 from "path";
4513
4857
  async function detectProjectManagement(rootDir) {
4514
4858
  const tools = [];
@@ -4519,7 +4863,7 @@ async function detectProjectManagement(rootDir) {
4519
4863
  for (const configPath of mcpConfigPaths) {
4520
4864
  if (!await fileExists(configPath)) continue;
4521
4865
  try {
4522
- const raw = await readFile12(configPath, "utf8");
4866
+ const raw = await readFile13(configPath, "utf8");
4523
4867
  const lower = raw.toLowerCase();
4524
4868
  if (lower.includes("clickup")) tools.push("clickup");
4525
4869
  if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
@@ -4555,6 +4899,7 @@ async function detectServices(rootDir) {
4555
4899
  }
4556
4900
 
4557
4901
  // src/scanner/detect-stack.ts
4902
+ import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
4558
4903
  import path22 from "path";
4559
4904
  var PROJECT_MARKERS = [
4560
4905
  "package.json",
@@ -4566,8 +4911,152 @@ var PROJECT_MARKERS = [
4566
4911
  "build.gradle",
4567
4912
  "composer.json",
4568
4913
  "Cargo.toml",
4569
- "pnpm-workspace.yaml"
4914
+ "pnpm-workspace.yaml",
4915
+ "pubspec.yaml"
4570
4916
  ];
4917
+ var SUBDIR_SCAN_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "build", ".dart_tool"]);
4918
+ async function scanSubdirectoryMarkers(rootDir) {
4919
+ let entries;
4920
+ try {
4921
+ entries = await readdir6(rootDir, { withFileTypes: true });
4922
+ } catch {
4923
+ return [];
4924
+ }
4925
+ const dirs = entries.filter((entry) => entry.isDirectory() && !SUBDIR_SCAN_SKIP.has(entry.name)).map((entry) => entry.name);
4926
+ const candidates = dirs.flatMap((dir) => PROJECT_MARKERS.map((marker) => ({ dir, marker })));
4927
+ const found = await Promise.all(
4928
+ candidates.map(async (candidate2) => {
4929
+ const exists2 = await fileExists(path22.join(rootDir, candidate2.dir, candidate2.marker));
4930
+ return exists2 ? candidate2 : null;
4931
+ })
4932
+ );
4933
+ return found.filter((hit) => hit !== null);
4934
+ }
4935
+ function isFlutterPubspec(content) {
4936
+ return /^\s*flutter:\s*$/m.test(content) || /sdk:\s*flutter\b/.test(content);
4937
+ }
4938
+ var DART_VALIDATION_COMMANDS = [
4939
+ "dart analyze --fatal-infos",
4940
+ "dart format --output=none --set-exit-if-changed ."
4941
+ ];
4942
+ async function detectDartFlavor(pubspecDir) {
4943
+ let content = "";
4944
+ try {
4945
+ content = await readFile14(path22.join(pubspecDir, "pubspec.yaml"), "utf8");
4946
+ } catch {
4947
+ }
4948
+ return { isFlutter: isFlutterPubspec(content) };
4949
+ }
4950
+ async function detectStackFromSubdirHits(rootDir, hits) {
4951
+ const packageManagerEvidence = hits.map((hit) => ({
4952
+ source: "file",
4953
+ value: `${hit.dir}/${hit.marker}`
4954
+ }));
4955
+ const dartHits = hits.filter((hit) => hit.marker === "pubspec.yaml");
4956
+ if (dartHits.length > 0) {
4957
+ const flavors = await Promise.all(
4958
+ dartHits.map((hit) => detectDartFlavor(path22.join(rootDir, hit.dir)))
4959
+ );
4960
+ const testCommands = Array.from(
4961
+ new Set(flavors.map((flavor) => flavor.isFlutter ? "flutter test" : "dart test"))
4962
+ );
4963
+ return {
4964
+ language: "dart",
4965
+ framework: flavors.some((flavor) => flavor.isFlutter) ? "flutter" : "dart",
4966
+ workspaces: true,
4967
+ testCommands,
4968
+ validationCommands: DART_VALIDATION_COMMANDS,
4969
+ hasProjectFiles: true,
4970
+ packageManagerEvidence
4971
+ };
4972
+ }
4973
+ const nodeHit = hits.find((hit) => hit.marker === "package.json");
4974
+ if (nodeHit) {
4975
+ const subRoot = path22.join(rootDir, nodeHit.dir);
4976
+ const packageJson = await readJson(path22.join(subRoot, "package.json")) ?? {};
4977
+ const scripts = packageJson.scripts ?? {};
4978
+ const packageManager = await detectPackageManager(subRoot, packageJson);
4979
+ const commands = commandsForScripts(scripts, packageManager.packageManager);
4980
+ return {
4981
+ language: "node",
4982
+ framework: "node",
4983
+ packageManager: packageManager.packageManager,
4984
+ packageManagerEvidence: [...packageManagerEvidence, ...packageManager.evidence],
4985
+ scripts,
4986
+ workspaces: true,
4987
+ ...commands,
4988
+ hasProjectFiles: true
4989
+ };
4990
+ }
4991
+ const languageByMarker = [
4992
+ { marker: "pyproject.toml", language: "python", testCommands: [], validationCommands: [] },
4993
+ { marker: "requirements.txt", language: "python", testCommands: [], validationCommands: [] },
4994
+ { marker: "go.mod", language: "go", testCommands: ["go test ./..."], validationCommands: [] },
4995
+ {
4996
+ marker: "Cargo.toml",
4997
+ language: "rust",
4998
+ testCommands: ["cargo test"],
4999
+ validationCommands: ["cargo check"]
5000
+ },
5001
+ { marker: "composer.json", language: "php", testCommands: [], validationCommands: [] }
5002
+ ];
5003
+ for (const candidate2 of languageByMarker) {
5004
+ if (hits.some((hit) => hit.marker === candidate2.marker)) {
5005
+ return {
5006
+ language: candidate2.language,
5007
+ framework: candidate2.language,
5008
+ workspaces: true,
5009
+ testCommands: candidate2.testCommands,
5010
+ validationCommands: candidate2.validationCommands,
5011
+ hasProjectFiles: true,
5012
+ packageManagerEvidence
5013
+ };
5014
+ }
5015
+ }
5016
+ return {
5017
+ language: "unknown",
5018
+ workspaces: true,
5019
+ testCommands: [],
5020
+ validationCommands: [],
5021
+ hasProjectFiles: true,
5022
+ packageManagerEvidence
5023
+ };
5024
+ }
5025
+ var KNOWN_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
5026
+ function isDetectionEvidenceArray(value) {
5027
+ return Array.isArray(value) && value.every(
5028
+ (item) => item && typeof item === "object" && typeof item.value === "string" && typeof item.source === "string"
5029
+ );
5030
+ }
5031
+ async function readConfirmedStack(rootDir) {
5032
+ const configuration = await readJson(
5033
+ path22.join(rootDir, ".cursor", "agent-kit.config.json")
5034
+ );
5035
+ const configured = configuration?.stack;
5036
+ const language = configured?.language;
5037
+ if (!configured || typeof language !== "string" || !language || language === "unknown") {
5038
+ return void 0;
5039
+ }
5040
+ const testCommands = Array.isArray(configured.testCommands) ? configured.testCommands.filter((item) => typeof item === "string") : [];
5041
+ const validationCommands = Array.isArray(configured.validationCommands) ? configured.validationCommands.filter((item) => typeof item === "string") : [];
5042
+ if (testCommands.length === 0 && validationCommands.length === 0) return void 0;
5043
+ const packageManager = typeof configured.packageManager === "string" && KNOWN_PACKAGE_MANAGERS.includes(configured.packageManager) ? configured.packageManager : void 0;
5044
+ const scripts = configured.scripts && typeof configured.scripts === "object" && !Array.isArray(configured.scripts) ? configured.scripts : void 0;
5045
+ const packageManagerEvidence = isDetectionEvidenceArray(
5046
+ configured.packageManagerEvidence
5047
+ ) ? configured.packageManagerEvidence : [{ source: "configuration", value: ".cursor/agent-kit.config.json#stack.language" }];
5048
+ return {
5049
+ language,
5050
+ framework: typeof configured.framework === "string" ? configured.framework : void 0,
5051
+ packageManager,
5052
+ scripts,
5053
+ workspaces: configured.workspaces === true,
5054
+ testCommands,
5055
+ validationCommands,
5056
+ hasProjectFiles: true,
5057
+ packageManagerEvidence
5058
+ };
5059
+ }
4571
5060
  var LOCKFILES = [
4572
5061
  ["pnpm-lock.yaml", "pnpm"],
4573
5062
  ["yarn.lock", "yarn"],
@@ -4601,7 +5090,15 @@ function commandsForScripts(scripts, packageManager) {
4601
5090
  return { testCommands, validationCommands };
4602
5091
  }
4603
5092
  async function detectStack(rootDir) {
5093
+ const confirmed = await readConfirmedStack(rootDir);
5094
+ if (confirmed) return confirmed;
4604
5095
  const hasAnyProjectMarker = (await Promise.all(PROJECT_MARKERS.map((item) => fileExists(path22.join(rootDir, item))))).some(Boolean);
5096
+ if (!hasAnyProjectMarker) {
5097
+ const subdirHits = await scanSubdirectoryMarkers(rootDir);
5098
+ if (subdirHits.length > 0) {
5099
+ return detectStackFromSubdirHits(rootDir, subdirHits);
5100
+ }
5101
+ }
4605
5102
  const hasPackageJson = await fileExists(path22.join(rootDir, "package.json"));
4606
5103
  if (hasPackageJson) {
4607
5104
  const packageJson = await readJson(path22.join(rootDir, "package.json")) ?? {};
@@ -4661,6 +5158,17 @@ async function detectStack(rootDir) {
4661
5158
  hasProjectFiles: hasAnyProjectMarker
4662
5159
  };
4663
5160
  }
5161
+ if (await fileExists(path22.join(rootDir, "pubspec.yaml"))) {
5162
+ const { isFlutter } = await detectDartFlavor(rootDir);
5163
+ return {
5164
+ language: "dart",
5165
+ framework: isFlutter ? "flutter" : "dart",
5166
+ workspaces: false,
5167
+ testCommands: [isFlutter ? "flutter test" : "dart test"],
5168
+ validationCommands: DART_VALIDATION_COMMANDS,
5169
+ hasProjectFiles: hasAnyProjectMarker
5170
+ };
5171
+ }
4664
5172
  return {
4665
5173
  language: "unknown",
4666
5174
  workspaces: false,
@@ -4715,7 +5223,8 @@ async function runScanner(rootDir) {
4715
5223
  testCommands: stack.testCommands,
4716
5224
  validationCommands: stack.validationCommands,
4717
5225
  ci: infra.ci,
4718
- hasTests: stack.testCommands.length > 0
5226
+ hasTests: stack.testCommands.length > 0,
5227
+ ciRunCommands: infra.ciRunCommands
4719
5228
  }
4720
5229
  };
4721
5230
  }
@@ -4788,9 +5297,12 @@ function createOnboardingState(report, generatedAt, deferredItems = report.defer
4788
5297
  const validDeferredCheckIds = new Set(
4789
5298
  deferredItems.filter((item) => item.reason.trim().length > 0).map((item) => item.checkId)
4790
5299
  );
4791
- const unresolvedEssential = report.pillars.flatMap((pillar2) => pillar2.checks).some(
4792
- (check2) => check2.essential && check2.status !== "ready" && (check2.status === "blocked" || !validDeferredCheckIds.has(check2.id))
4793
- );
5300
+ const unresolvedEssential = report.pillars.flatMap((pillar2) => pillar2.checks).some((check2) => {
5301
+ if (!check2.essential || check2.status === "ready") return false;
5302
+ if (check2.status === "blocked") return true;
5303
+ if (!validDeferredCheckIds.has(check2.id)) return true;
5304
+ return check2.actions.length > 0;
5305
+ });
4794
5306
  return {
4795
5307
  contractVersion: 1,
4796
5308
  status: unresolvedEssential ? "in_progress" : "completed",
@@ -4941,7 +5453,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
4941
5453
  }
4942
5454
  const gitignoreRelativePath = ".gitignore";
4943
5455
  const gitignorePath = path24.join(beforeScan.rootDir, gitignoreRelativePath);
4944
- const existingGitignore = await fileExists(gitignorePath) ? await readFile13(gitignorePath, "utf8") : "";
5456
+ const existingGitignore = await fileExists(gitignorePath) ? await readFile15(gitignorePath, "utf8") : "";
4945
5457
  const gitignoreAfterSecrets = mergeSecretIgnores(existingGitignore);
4946
5458
  const secretsChanged = gitignoreAfterSecrets !== existingGitignore;
4947
5459
  const mergedGitignore = mergeKitOwnedIgnores(gitignoreAfterSecrets);
@@ -5056,11 +5568,12 @@ var KIT_TIPS = [
5056
5568
  "agent-kit --help groups SETUP, MISSION, DASHBOARD, INTEGRITY."
5057
5569
  ];
5058
5570
  function shouldUseWelcomeColor(opts = {}) {
5571
+ const env = opts.env ?? process.env;
5059
5572
  if (opts.color === false) return false;
5060
- if (process.env.NO_COLOR) return false;
5061
- if (process.env.NODE_DISABLE_COLORS) return false;
5062
- if (process.env.FORCE_COLOR === "0") return false;
5063
- if (process.env.CI != null && process.env.CI !== "") return false;
5573
+ if (env.NO_COLOR) return false;
5574
+ if (env.NODE_DISABLE_COLORS) return false;
5575
+ if (env.FORCE_COLOR === "0") return false;
5576
+ if (env.CI != null && env.CI !== "") return false;
5064
5577
  if (opts.color === true) return true;
5065
5578
  const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
5066
5579
  return tty;
@@ -5213,6 +5726,19 @@ async function withCliProgress(label, fn, opts) {
5213
5726
  }
5214
5727
 
5215
5728
  // src/commands/doctor.ts
5729
+ async function readDeferredEssentials(rootDir, report) {
5730
+ const config = await readJson(
5731
+ path26.join(rootDir, ".cursor", "context", "config.json")
5732
+ );
5733
+ const deferredItems = config?.onboarding?.deferredItems;
5734
+ if (!Array.isArray(deferredItems)) return [];
5735
+ const essentialIds = new Set(
5736
+ report.pillars.flatMap((pillar2) => pillar2.checks).filter((check2) => check2.essential).map((check2) => check2.id)
5737
+ );
5738
+ return deferredItems.filter(
5739
+ (item) => typeof item?.checkId === "string" && typeof item?.reason === "string" && essentialIds.has(item.checkId)
5740
+ ).map((item) => ({ checkId: item.checkId, reason: item.reason }));
5741
+ }
5216
5742
  async function runDoctor(cwd, options = {}) {
5217
5743
  const rootDir = path26.resolve(cwd);
5218
5744
  const hooks = await assessHooksHealth(rootDir);
@@ -5223,7 +5749,13 @@ async function runDoctor(cwd, options = {}) {
5223
5749
  generatedAt: options.generatedAt
5224
5750
  });
5225
5751
  await writeReadinessSnapshot(rootDir, execution.after);
5226
- return { report: execution.after, safeChanges: execution.changes, hooks, env };
5752
+ return {
5753
+ report: execution.after,
5754
+ safeChanges: execution.changes,
5755
+ hooks,
5756
+ env,
5757
+ deferredEssentials: await readDeferredEssentials(rootDir, execution.after)
5758
+ };
5227
5759
  }
5228
5760
  if (options.refreshProfile) {
5229
5761
  const refresh = await refreshRepositoryProfile(rootDir, {
@@ -5236,7 +5768,14 @@ async function runDoctor(cwd, options = {}) {
5236
5768
  generatedAt: options.generatedAt
5237
5769
  });
5238
5770
  await writeReadinessSnapshot(rootDir, report2);
5239
- return { report: report2, safeChanges: [], hooks, env, profileRefreshed: refresh.changed };
5771
+ return {
5772
+ report: report2,
5773
+ safeChanges: [],
5774
+ hooks,
5775
+ env,
5776
+ profileRefreshed: refresh.changed,
5777
+ deferredEssentials: await readDeferredEssentials(rootDir, report2)
5778
+ };
5240
5779
  }
5241
5780
  const scan = await runScanner(rootDir);
5242
5781
  const report = createReadinessReport(scan, {
@@ -5244,7 +5783,13 @@ async function runDoctor(cwd, options = {}) {
5244
5783
  generatedAt: options.generatedAt
5245
5784
  });
5246
5785
  await writeReadinessSnapshot(rootDir, report);
5247
- return { report, safeChanges: [], hooks, env };
5786
+ return {
5787
+ report,
5788
+ safeChanges: [],
5789
+ hooks,
5790
+ env,
5791
+ deferredEssentials: await readDeferredEssentials(rootDir, report)
5792
+ };
5248
5793
  }
5249
5794
  function printDoctorSummary(result) {
5250
5795
  const { summary, pendingActions } = result.report;
@@ -5261,6 +5806,9 @@ function printDoctorSummary(result) {
5261
5806
  ` profile refreshed: ${result.profileRefreshed ? "yes (facts changed)" : "no (already current)"}`
5262
5807
  );
5263
5808
  }
5809
+ for (const item of result.deferredEssentials) {
5810
+ console.log(` Deferred essential: ${item.checkId} \u2014 ${item.reason}`);
5811
+ }
5264
5812
  console.log(`hooks: ${result.hooks.status}`);
5265
5813
  if (result.hooks.reasons.length > 0) {
5266
5814
  for (const reason of result.hooks.reasons.slice(0, 5)) {
@@ -5274,7 +5822,8 @@ function printDoctorSummary(result) {
5274
5822
  }
5275
5823
  }
5276
5824
  console.log("environment:");
5277
- console.log(` - bin on PATH (agent-kit): ${result.env.binOnPath ? "ok" : "MISSING"}`);
5825
+ const pathLabel = result.env.binOnPath ? `ok (v${result.env.binVersion ?? "unknown"} at ${result.env.binPath})` : "MISSING";
5826
+ console.log(` - bin on PATH (agent-kit): ${pathLabel}`);
5278
5827
  console.log(
5279
5828
  ` - npm prefix writable: ${result.env.npmPrefixWritable ? "ok" : "BLOCKED"}${result.env.npmPrefix.prefix ? ` (${result.env.npmPrefix.prefix})` : ""}`
5280
5829
  );
@@ -5719,9 +6268,187 @@ var guardCommand = defineCommand8({
5719
6268
 
5720
6269
  // src/commands/handoff.ts
5721
6270
  import { spawn as spawn4 } from "child_process";
5722
- import { readFile as readFile14, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
6271
+ import { appendFile, readFile as readFile16, readdir as readdir7, writeFile as writeFile6 } from "fs/promises";
5723
6272
  import path27 from "path";
5724
6273
  import { defineCommand as defineCommand9 } from "citty";
6274
+
6275
+ // src/invariants/handoff-prune.ts
6276
+ var HANDOFF_MACHINE_FIELD_LABELS = [
6277
+ "Plan",
6278
+ "Last updated",
6279
+ "Mode",
6280
+ "Phase completed",
6281
+ "Next phase",
6282
+ "Completed to-dos",
6283
+ "Next to-dos",
6284
+ "Parked plans",
6285
+ "Backlog plans",
6286
+ "Run queue",
6287
+ "Queue cursor",
6288
+ "Queue status",
6289
+ "Queue outcomes",
6290
+ "Gaps",
6291
+ "Instruction for the next agent"
6292
+ ];
6293
+ var HANDOFF_FIXED_SECTION_HEADING_PREFIXES = [
6294
+ "## Work Status",
6295
+ "## Session Context",
6296
+ "## Files Touched",
6297
+ "## Decisions Made",
6298
+ "## Issues Found",
6299
+ "## Run queue (multi-plan only)"
6300
+ ];
6301
+ var HANDOFF_MAX_LINES = 200;
6302
+ var HANDOFF_MAX_LINE_LENGTH = 2e3;
6303
+ var MACHINE_FIELD_RE = new RegExp(
6304
+ `^- \\*\\*(${HANDOFF_MACHINE_FIELD_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):\\*\\*`
6305
+ );
6306
+ function isChunkStart(line) {
6307
+ return /^- \*\*/.test(line) || /^## /.test(line);
6308
+ }
6309
+ function isFixedSectionHeading(line) {
6310
+ return HANDOFF_FIXED_SECTION_HEADING_PREFIXES.some((p) => line.startsWith(p));
6311
+ }
6312
+ function splitIntoChunks(lines) {
6313
+ const firstStart = lines.findIndex(isChunkStart);
6314
+ if (firstStart === -1) return { preamble: lines, chunks: [] };
6315
+ const preamble = lines.slice(0, firstStart);
6316
+ const chunks = [];
6317
+ let current = [lines[firstStart]];
6318
+ for (const line of lines.slice(firstStart + 1)) {
6319
+ if (isChunkStart(line)) {
6320
+ chunks.push({ lines: current, isMachineField: MACHINE_FIELD_RE.test(current[0] ?? "") });
6321
+ current = [line];
6322
+ } else {
6323
+ current.push(line);
6324
+ }
6325
+ }
6326
+ chunks.push({ lines: current, isMachineField: MACHINE_FIELD_RE.test(current[0] ?? "") });
6327
+ return { preamble, chunks };
6328
+ }
6329
+ function pruneHandoffText(text, keep) {
6330
+ if (!text.trim()) {
6331
+ return { ok: false, reason: "empty file" };
6332
+ }
6333
+ const lines = text.split("\n");
6334
+ const titleIdx = lines.findIndex((l) => /^# /.test(l));
6335
+ if (titleIdx === -1) {
6336
+ return { ok: false, reason: "no H1 title line found" };
6337
+ }
6338
+ let fixedStartIdx = lines.findIndex((l) => isFixedSectionHeading(l));
6339
+ if (fixedStartIdx === -1) fixedStartIdx = lines.length;
6340
+ const head = lines.slice(0, titleIdx + 1);
6341
+ const narrativeLines = lines.slice(titleIdx + 1, fixedStartIdx);
6342
+ const trailingLines = lines.slice(fixedStartIdx);
6343
+ const { preamble, chunks } = splitIntoChunks(narrativeLines);
6344
+ const machineFieldCount = chunks.filter((c) => c.isMachineField).length;
6345
+ if (machineFieldCount === 0) {
6346
+ return { ok: false, reason: "no machine-field bullets found; refusing to prune" };
6347
+ }
6348
+ if (fixedStartIdx === lines.length) {
6349
+ return { ok: false, reason: "no recognized fixed-section heading found; refusing to prune" };
6350
+ }
6351
+ const narrativeChunks = chunks.filter((c) => !c.isMachineField);
6352
+ if (narrativeChunks.length <= keep) {
6353
+ return { ok: true, text, archived: [], keptCount: narrativeChunks.length };
6354
+ }
6355
+ const keptNarrativeIdx = /* @__PURE__ */ new Set();
6356
+ let seen = 0;
6357
+ const chunkIsNarrative = chunks.map((c) => !c.isMachineField);
6358
+ for (let i = 0; i < chunks.length; i++) {
6359
+ if (chunkIsNarrative[i]) {
6360
+ if (seen < keep) keptNarrativeIdx.add(i);
6361
+ seen++;
6362
+ }
6363
+ }
6364
+ const keptChunks = [];
6365
+ const archivedChunks = [];
6366
+ chunks.forEach((c, i) => {
6367
+ if (c.isMachineField || keptNarrativeIdx.has(i)) keptChunks.push(c);
6368
+ else archivedChunks.push(c);
6369
+ });
6370
+ const newNarrativeLines = [...preamble, ...keptChunks.flatMap((c) => c.lines)];
6371
+ const newLines = [...head, ...newNarrativeLines, ...trailingLines];
6372
+ return {
6373
+ ok: true,
6374
+ text: newLines.join("\n"),
6375
+ archived: archivedChunks.map((c) => c.lines.join("\n")),
6376
+ keptCount: keptNarrativeIdx.size
6377
+ };
6378
+ }
6379
+ function checkHandoffGrowth(text) {
6380
+ if (!text.trim()) return [];
6381
+ const lines = text.split("\n");
6382
+ const warnings = [];
6383
+ if (lines.length > HANDOFF_MAX_LINES) {
6384
+ warnings.push({
6385
+ code: "file-over-limit",
6386
+ message: `HANDOFF.md is ${lines.length} lines, over the ${HANDOFF_MAX_LINES}-line growth-contract limit. Run \`agent-kit handoff --prune\` to archive older narrative entries.`
6387
+ });
6388
+ }
6389
+ let firstOffendingLine = -1;
6390
+ let overCount = 0;
6391
+ lines.forEach((l, i) => {
6392
+ if (l.length > HANDOFF_MAX_LINE_LENGTH) {
6393
+ overCount++;
6394
+ if (firstOffendingLine === -1) firstOffendingLine = i + 1;
6395
+ }
6396
+ });
6397
+ if (overCount > 0) {
6398
+ warnings.push({
6399
+ code: "line-over-limit",
6400
+ message: `HANDOFF.md has ${overCount} line(s) over the ${HANDOFF_MAX_LINE_LENGTH}-char growth-contract limit (first at line ${firstOffendingLine}). Wrap long paragraphs; do not widen the limit.`
6401
+ });
6402
+ }
6403
+ return warnings;
6404
+ }
6405
+
6406
+ // src/commands/handoff.ts
6407
+ var HANDOFF_PRUNE_DEFAULT_KEEP = 5;
6408
+ function pruneArchiveTimestamp(now2 = /* @__PURE__ */ new Date()) {
6409
+ const pad = (n) => String(n).padStart(2, "0");
6410
+ return `${now2.getFullYear()}-${pad(now2.getMonth() + 1)}-${pad(now2.getDate())}-${pad(now2.getHours())}${pad(now2.getMinutes())}`;
6411
+ }
6412
+ async function runPrune(cwd, keep) {
6413
+ const handoffPath = path27.join(cwd, ".cursor", "HANDOFF.md");
6414
+ if (!await fileExists(handoffPath)) {
6415
+ logger.warn("No .cursor/HANDOFF.md to prune.");
6416
+ return;
6417
+ }
6418
+ const original = await readFile16(handoffPath, "utf8");
6419
+ const result = pruneHandoffText(original, keep);
6420
+ if (!result.ok) {
6421
+ logger.warn(`Refusing to prune: ${result.reason}`);
6422
+ process.exitCode = 1;
6423
+ return;
6424
+ }
6425
+ if (!result.archived || result.archived.length === 0) {
6426
+ logger.info(
6427
+ `HANDOFF.md already at or under ${keep} narrative ${keep === 1 ? "entry" : "entries"}; nothing to prune.`
6428
+ );
6429
+ return;
6430
+ }
6431
+ const archiveDir = path27.join(cwd, ".cursor", "context", "archive");
6432
+ await ensureDir(archiveDir);
6433
+ const archivePath = path27.join(archiveDir, `handoff-${pruneArchiveTimestamp()}.md`);
6434
+ const header = [
6435
+ `<!-- Pruned from .cursor/HANDOFF.md at ${(/* @__PURE__ */ new Date()).toISOString()} -->`,
6436
+ `<!-- ${result.archived.length} narrative ${result.archived.length === 1 ? "entry" : "entries"} archived, keep=${keep} -->`,
6437
+ ""
6438
+ ].join("\n");
6439
+ const body = `${result.archived.join("\n\n")}
6440
+ `;
6441
+ if (await fileExists(archivePath)) {
6442
+ await appendFile(archivePath, `
6443
+ ${header}${body}`, "utf8");
6444
+ } else {
6445
+ await writeFile6(archivePath, `${header}${body}`, "utf8");
6446
+ }
6447
+ await writeFile6(handoffPath, result.text ?? original, "utf8");
6448
+ logger.success(
6449
+ `HANDOFF.md pruned: kept ${result.keptCount} narrative ${result.keptCount === 1 ? "entry" : "entries"}, archived ${result.archived.length} to ${path27.relative(cwd, archivePath)}.`
6450
+ );
6451
+ }
5725
6452
  function parsePlanFrontmatter(raw) {
5726
6453
  const match = raw.match(/^---\n([\s\S]*?)\n---/);
5727
6454
  if (!match?.[1]) return null;
@@ -5738,15 +6465,15 @@ function parsePlanFrontmatter(raw) {
5738
6465
  }
5739
6466
  async function findActivePlan(plansDir) {
5740
6467
  if (!await fileExists(plansDir)) return null;
5741
- const files = (await readdir5(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
6468
+ const files = (await readdir7(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
5742
6469
  for (const file of files) {
5743
- const raw = await readFile14(path27.join(plansDir, file), "utf8");
6470
+ const raw = await readFile16(path27.join(plansDir, file), "utf8");
5744
6471
  const fm = parsePlanFrontmatter(raw);
5745
6472
  if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
5746
6473
  return { file, raw };
5747
6474
  }
5748
6475
  }
5749
- return files[0] ? { file: files[0], raw: await readFile14(path27.join(plansDir, files[0]), "utf8") } : null;
6476
+ return files[0] ? { file: files[0], raw: await readFile16(path27.join(plansDir, files[0]), "utf8") } : null;
5750
6477
  }
5751
6478
  function now() {
5752
6479
  return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
@@ -5848,9 +6575,24 @@ var handoffCommand = defineCommand9({
5848
6575
  type: "string",
5849
6576
  description: "Project root directory",
5850
6577
  default: process.cwd()
6578
+ },
6579
+ prune: {
6580
+ type: "boolean",
6581
+ description: "Prune .cursor/HANDOFF.md in place instead of regenerating it: keep the most recent narrative entries, archive the rest to .cursor/context/archive/. Never auto-triggered.",
6582
+ default: false
6583
+ },
6584
+ keep: {
6585
+ type: "string",
6586
+ description: `With --prune, how many recent narrative entries to keep in place (default ${HANDOFF_PRUNE_DEFAULT_KEEP}).`
5851
6587
  }
5852
6588
  },
5853
6589
  async run({ args }) {
6590
+ if (args.prune) {
6591
+ const keepArg = typeof args.keep === "string" ? Number.parseInt(args.keep, 10) : void 0;
6592
+ const keep = keepArg !== void 0 && Number.isFinite(keepArg) && keepArg >= 0 ? keepArg : HANDOFF_PRUNE_DEFAULT_KEEP;
6593
+ await runPrune(args.cwd, keep);
6594
+ return;
6595
+ }
5854
6596
  const profile = await loadProfile(args.cwd);
5855
6597
  const plansDir = path27.join(args.cwd, ".cursor", "plans");
5856
6598
  const handoffPath = path27.join(args.cwd, ".cursor", "HANDOFF.md");
@@ -5929,7 +6671,7 @@ function buildPreCompactUserMessage(payload = {}) {
5929
6671
 
5930
6672
  // src/hooks/session-start.ts
5931
6673
  import { spawn as spawn5 } from "child_process";
5932
- import { access as access7, readFile as readFile16, stat as stat4 } from "fs/promises";
6674
+ import { access as access7, readFile as readFile18, stat as stat4 } from "fs/promises";
5933
6675
  import path29 from "path";
5934
6676
 
5935
6677
  // src/invariants/handoff-schema.ts
@@ -5972,7 +6714,7 @@ function validateHandoffText(text) {
5972
6714
  }
5973
6715
 
5974
6716
  // src/plan-index/plan-index.ts
5975
- import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
6717
+ import { readFile as readFile17, writeFile as writeFile7 } from "fs/promises";
5976
6718
  import path28 from "path";
5977
6719
  var PLAN_INDEX_REL = ".cursor/context/plan-index.json";
5978
6720
  var NONE = /^(none|n\/a|empty|nil)$/i;
@@ -5981,7 +6723,7 @@ function defaultPlanIndexIo() {
5981
6723
  return {
5982
6724
  async readFile(absPath) {
5983
6725
  try {
5984
- return await readFile15(absPath, "utf8");
6726
+ return await readFile17(absPath, "utf8");
5985
6727
  } catch {
5986
6728
  return null;
5987
6729
  }
@@ -6152,6 +6894,137 @@ function extractPlanBasenames(raw) {
6152
6894
  return ids;
6153
6895
  }
6154
6896
 
6897
+ // src/hooks/handoff-excerpt.ts
6898
+ var HANDOFF_EXCERPT_MAX_LINES = 60;
6899
+ var HANDOFF_EXCERPT_MAX_BYTES = 12 * 1024;
6900
+ var HANDOFF_PROSE_LINE_MIN_BYTES = 160;
6901
+ var HANDOFF_MACHINE_FIELDS = /* @__PURE__ */ new Set([
6902
+ "Plan",
6903
+ "Last updated",
6904
+ "Mode",
6905
+ "In progress",
6906
+ "Next",
6907
+ "Next phase",
6908
+ "Next to-dos",
6909
+ "Backlog",
6910
+ "Backlog plans",
6911
+ "Parked plans",
6912
+ "Run queue",
6913
+ "Queue cursor",
6914
+ "Queue status"
6915
+ ]);
6916
+ var FIELD_BULLET = /^- \*\*([^*\n]+?):\*\*/;
6917
+ function byteLength(s) {
6918
+ return Buffer.byteLength(s, "utf8");
6919
+ }
6920
+ function classify(lines) {
6921
+ const entries = [];
6922
+ let lastBullet = "structural";
6923
+ for (const text of lines) {
6924
+ let kind;
6925
+ if (!text.trim() || /^#{1,6}\s/.test(text)) {
6926
+ kind = "structural";
6927
+ } else if (/^\s+\S/.test(text)) {
6928
+ kind = lastBullet;
6929
+ } else {
6930
+ const m = FIELD_BULLET.exec(text);
6931
+ kind = m?.[1] && HANDOFF_MACHINE_FIELDS.has(m[1].trim()) ? "machine" : "prose";
6932
+ lastBullet = kind;
6933
+ }
6934
+ entries.push({ text, kind, bytes: byteLength(text) });
6935
+ }
6936
+ return entries;
6937
+ }
6938
+ function truncateToBytes(text, maxBytes) {
6939
+ let used = 0;
6940
+ let kept = "";
6941
+ let keptChars = 0;
6942
+ const total = Array.from(text).length;
6943
+ for (const ch of text) {
6944
+ const b = byteLength(ch);
6945
+ if (used + b > maxBytes) break;
6946
+ used += b;
6947
+ kept += ch;
6948
+ keptChars += 1;
6949
+ }
6950
+ return { kept: kept.trimEnd(), removedChars: total - keptChars };
6951
+ }
6952
+ var MARKER_RESERVE_BYTES = 32;
6953
+ function truncationMarker(removedChars) {
6954
+ return ` \u2026[truncated ${removedChars} chars]`;
6955
+ }
6956
+ function droppedMarker(dropped) {
6957
+ return `\u2026[${dropped} prose ${dropped === 1 ? "line" : "lines"} omitted by the excerpt byte cap; read .cursor/HANDOFF.md]`;
6958
+ }
6959
+ function proseShare(proseBytes, budget, floor) {
6960
+ const n = proseBytes.length;
6961
+ if (n === 0) return budget;
6962
+ let share = Math.floor(budget / n);
6963
+ for (let i = 0; i < 8; i++) {
6964
+ const longer = proseBytes.filter((b) => b > share);
6965
+ if (longer.length === 0) break;
6966
+ const used = proseBytes.reduce((sum, b) => sum + Math.min(b, share), 0);
6967
+ const slack = budget - used;
6968
+ const step = Math.floor(slack / longer.length);
6969
+ if (step <= 0) break;
6970
+ share += step;
6971
+ }
6972
+ return Math.max(floor, share);
6973
+ }
6974
+ function capHandoffExcerpt(content, options = {}) {
6975
+ const maxLines = options.maxLines ?? HANDOFF_EXCERPT_MAX_LINES;
6976
+ const maxBytes = options.maxBytes ?? HANDOFF_EXCERPT_MAX_BYTES;
6977
+ const floor = options.proseLineMinBytes ?? HANDOFF_PROSE_LINE_MIN_BYTES;
6978
+ const lines = content.split(/\r?\n/).slice(0, maxLines);
6979
+ const entries = classify(lines);
6980
+ const join2 = (items) => items.map((e) => e.text).join("\n").trim();
6981
+ const untouched = join2(entries);
6982
+ const untouchedBytes = byteLength(untouched);
6983
+ if (untouchedBytes <= maxBytes) {
6984
+ return {
6985
+ text: untouched,
6986
+ bytes: untouchedBytes,
6987
+ lines: lines.length,
6988
+ truncatedLines: 0,
6989
+ droppedLines: 0
6990
+ };
6991
+ }
6992
+ const fixedBytes = entries.reduce((sum, e) => sum + (e.kind === "prose" ? 0 : e.bytes) + 1, 0);
6993
+ const proseBudget = Math.max(0, maxBytes - fixedBytes);
6994
+ const share = proseShare(
6995
+ entries.filter((e) => e.kind === "prose").map((e) => e.bytes),
6996
+ proseBudget,
6997
+ floor
6998
+ );
6999
+ let truncatedLines = 0;
7000
+ const shaped = entries.map((e) => {
7001
+ if (e.kind !== "prose" || e.bytes <= share) return e;
7002
+ const { kept, removedChars } = truncateToBytes(e.text, share - MARKER_RESERVE_BYTES);
7003
+ const text2 = kept + truncationMarker(removedChars);
7004
+ truncatedLines += 1;
7005
+ return { text: text2, kind: e.kind, bytes: byteLength(text2) };
7006
+ });
7007
+ let total = shaped.reduce((sum, e) => sum + e.bytes + 1, 0) - 1;
7008
+ let droppedLines = 0;
7009
+ if (total > maxBytes) {
7010
+ const keep = new Set(shaped.map((_, i) => i));
7011
+ const noteBytes = byteLength(droppedMarker(99)) + 1;
7012
+ for (let i = shaped.length - 1; i >= 0 && total + noteBytes > maxBytes; i--) {
7013
+ const e = shaped[i];
7014
+ if (!e || e.kind !== "prose") continue;
7015
+ keep.delete(i);
7016
+ total -= e.bytes + 1;
7017
+ droppedLines += 1;
7018
+ }
7019
+ const remaining = shaped.filter((_, i) => keep.has(i));
7020
+ remaining.push({ text: droppedMarker(droppedLines), kind: "structural", bytes: 0 });
7021
+ const text2 = join2(remaining);
7022
+ return { text: text2, bytes: byteLength(text2), lines: lines.length, truncatedLines, droppedLines };
7023
+ }
7024
+ const text = join2(shaped);
7025
+ return { text, bytes: byteLength(text), lines: lines.length, truncatedLines, droppedLines };
7026
+ }
7027
+
6155
7028
  // src/hooks/hard-rules.ts
6156
7029
  var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
6157
7030
 
@@ -6161,7 +7034,7 @@ var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
6161
7034
  4. **Read HANDOFF first** when resuming. Do not restart the plan from scratch.
6162
7035
  5. **Git:** suggest \`/git-staging\` after a phase with a diff; never \`/git-prod\` without explicit confirmation.
6163
7036
  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.
6164
- 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.
7037
+ 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 / write+backlog (\`Write plan and add to backlog\`) / modify / cancel): 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/skills/core/hitl-gates/SKILL.md\` and \`.cursor/rules/hitl-ask-questions.mdc\`. Fallback: numbered list is path 1.
6165
7038
  8. **\`/continue-plan\` waits for yes.** Summarize next \`[to-do-id]\`, then stop until the user confirms before editing.
6166
7039
  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.
6167
7040
  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.
@@ -6184,17 +7057,9 @@ Run \`/cursor-update-awareness\` (or \`agent-kit cursor-awareness --check\`) and
6184
7057
  // src/hooks/session-start.ts
6185
7058
  var NONE_PLACEHOLDERS = /* @__PURE__ */ new Set(["none", "n/a", "empty", "nil"]);
6186
7059
  var CURSOR_AWARENESS_SPAWN_TIMEOUT_MS = CHANGELOG_FETCH_TIMEOUT_MS + 3e3;
6187
- async function readTextLimited(filePath, limit = 60) {
6188
- try {
6189
- const lines = (await readFile16(filePath, "utf8")).split(/\r?\n/);
6190
- return lines.slice(0, limit).join("\n").trim();
6191
- } catch {
6192
- return "";
6193
- }
6194
- }
6195
7060
  async function readFull(filePath) {
6196
7061
  try {
6197
- return await readFile16(filePath, "utf8");
7062
+ return await readFile18(filePath, "utf8");
6198
7063
  } catch {
6199
7064
  return "";
6200
7065
  }
@@ -6311,7 +7176,7 @@ async function readinessSection(root) {
6311
7176
  const snapshotPath = path29.join(root, ".cursor", "context", "readiness.json");
6312
7177
  let data;
6313
7178
  try {
6314
- data = JSON.parse(await readFile16(snapshotPath, "utf8"));
7179
+ data = JSON.parse(await readFile18(snapshotPath, "utf8"));
6315
7180
  } catch {
6316
7181
  return null;
6317
7182
  }
@@ -6348,7 +7213,7 @@ async function dogfoodInboxSection(root) {
6348
7213
  for (const readme of candidateReadmes) {
6349
7214
  if (!await fileExists2(readme)) continue;
6350
7215
  try {
6351
- const text = await readFile16(readme, "utf8");
7216
+ const text = await readFile18(readme, "utf8");
6352
7217
  if (parseUnprocessedDogfoodItems(text).length) return DOGFOOD_INBOX_HINT;
6353
7218
  } catch {
6354
7219
  }
@@ -6358,7 +7223,7 @@ async function dogfoodInboxSection(root) {
6358
7223
  async function loadUpdateCheckPrefs(root) {
6359
7224
  try {
6360
7225
  const data = JSON.parse(
6361
- await readFile16(path29.join(root, ".cursor", "context", "config.json"), "utf8")
7226
+ await readFile18(path29.join(root, ".cursor", "context", "config.json"), "utf8")
6362
7227
  );
6363
7228
  const uc = data.updateCheck;
6364
7229
  if (!uc || typeof uc !== "object" || uc.enabled !== true) {
@@ -6436,7 +7301,7 @@ async function updateCheckSection(root) {
6436
7301
  async function loadCursorUpdateCheckPrefs(root) {
6437
7302
  try {
6438
7303
  const data = JSON.parse(
6439
- await readFile16(path29.join(root, ".cursor", "context", "config.json"), "utf8")
7304
+ await readFile18(path29.join(root, ".cursor", "context", "config.json"), "utf8")
6440
7305
  );
6441
7306
  const uc = data.cursorUpdateCheck;
6442
7307
  if (!uc || typeof uc !== "object" || uc.enabled !== true) {
@@ -6631,7 +7496,7 @@ async function buildSessionStartAdditionalContext(rootDir, _payload = {}) {
6631
7496
  const root = path29.resolve(rootDir);
6632
7497
  const handoffPath = path29.join(root, ".cursor", "HANDOFF.md");
6633
7498
  const handoffFull = await readFull(handoffPath);
6634
- const handoff = await readTextLimited(handoffPath);
7499
+ const handoff = capHandoffExcerpt(handoffFull);
6635
7500
  const parts = [HARD_RULES];
6636
7501
  if (await l0Present(root)) {
6637
7502
  const readiness = await readinessSection(root);
@@ -6656,10 +7521,14 @@ ${bullet}
6656
7521
  Rewrite machine lists as \`- **Field:**\` bullets before trusting Checklist / Current mission.`
6657
7522
  );
6658
7523
  }
6659
- if (handoff) {
7524
+ if (handoff.text) {
7525
+ const capped = handoff.truncatedLines > 0 || handoff.droppedLines > 0;
7526
+ const note = capped ? `
7527
+
7528
+ _Excerpt capped at ${Math.round(HANDOFF_EXCERPT_MAX_BYTES / 1024)} KB (${handoff.truncatedLines} lines truncated, ${handoff.droppedLines} dropped); read \`.cursor/HANDOFF.md\` for the full entries._` : "";
6660
7529
  parts.push(`## Current HANDOFF.md (excerpt)
6661
7530
 
6662
- ${handoff}`);
7531
+ ${handoff.text}${note}`);
6663
7532
  } else {
6664
7533
  parts.push(
6665
7534
  "## HANDOFF.md\n\nNo handoff file yet. If starting work, create a plan with to-dos first (`/start-project`)."
@@ -6743,8 +7612,12 @@ var hookCommand = defineCommand10({
6743
7612
  import { intro, outro } from "@clack/prompts";
6744
7613
  import { defineCommand as defineCommand12 } from "citty";
6745
7614
 
6746
- // src/utils/terminal.ts
6747
- import { readdir as readdir6 } from "fs/promises";
7615
+ // src/lifecycle/path-cli.ts
7616
+ import { spawn as spawn6 } from "child_process";
7617
+ import path32 from "path";
7618
+
7619
+ // src/utils/terminal.ts
7620
+ import { readdir as readdir8 } from "fs/promises";
6748
7621
  import { homedir as homedir3 } from "os";
6749
7622
  import path31 from "path";
6750
7623
  import { confirm, isCancel } from "@clack/prompts";
@@ -6758,7 +7631,7 @@ var NESTED_REPO_SCAN_LIMIT = 200;
6758
7631
  async function findNestedRepoChildren(resolved) {
6759
7632
  let entries;
6760
7633
  try {
6761
- entries = await readdir6(resolved, { withFileTypes: true });
7634
+ entries = await readdir8(resolved, { withFileTypes: true });
6762
7635
  } catch {
6763
7636
  return [];
6764
7637
  }
@@ -6928,18 +7801,122 @@ function classifyInstallError(err) {
6928
7801
  };
6929
7802
  }
6930
7803
 
7804
+ // src/lifecycle/path-cli.ts
7805
+ function npxPinned(runtimeVersion, subcommand) {
7806
+ return `npx -y ${pinnedCliSpec(runtimeVersion)} ${subcommand}`;
7807
+ }
7808
+ function isBinUnderPrefix(binPath, prefix) {
7809
+ const bin = path32.resolve(binPath);
7810
+ const root = path32.resolve(prefix);
7811
+ return bin === root || bin.startsWith(`${root}${path32.sep}`);
7812
+ }
7813
+ function pathCliStatus(env, runtimeVersion) {
7814
+ if (!env.binOnPath || !env.binPath) return "missing";
7815
+ const running = normalizeSemver(runtimeVersion);
7816
+ const pathVer = env.binVersion ? normalizeSemver(env.binVersion) : null;
7817
+ if (!pathVer || !running) return "unknown";
7818
+ try {
7819
+ return compareSemver(pathVer, running) >= 0 ? "current" : "behind";
7820
+ } catch {
7821
+ return "unknown";
7822
+ }
7823
+ }
7824
+ function driftLines(env, runtimeVersion) {
7825
+ const pathVer = env.binVersion ?? "unknown";
7826
+ const where = env.binPath ?? "(unresolved)";
7827
+ return [
7828
+ `This CLI is v${runtimeVersion}. PATH \`agent-kit\` is v${pathVer} at:`,
7829
+ ` ${where}`,
7830
+ "Do not run bare `agent-kit update` or `init` from that binary. It re-stamps the old version.",
7831
+ "Keep using the pinned npx form until PATH matches:",
7832
+ ` ${npxPinned(runtimeVersion, "update")}`,
7833
+ ` ${npxPinned(runtimeVersion, "status")}`
7834
+ ];
7835
+ }
7836
+ function isShadow(env) {
7837
+ if (!env.binPath || !env.npmPrefix.prefix) return false;
7838
+ return !isBinUnderPrefix(env.binPath, env.npmPrefix.prefix);
7839
+ }
7840
+ async function spawnNpmGlobalInstall(spec) {
7841
+ return new Promise((resolve3) => {
7842
+ const child = spawn6("npm", ["i", "-g", spec], { stdio: "inherit" });
7843
+ child.on("error", (error) => resolve3({ ok: false, error }));
7844
+ child.on("close", (code) => {
7845
+ if (code === 0) resolve3({ ok: true });
7846
+ else resolve3({ ok: false, error: new Error(`npm exited with code ${code ?? "unknown"}`) });
7847
+ });
7848
+ });
7849
+ }
7850
+ async function syncPathCliToRuntime(options = {}) {
7851
+ const runtimeVersion = options.runtimeVersion ?? KIT_VERSION;
7852
+ const assess = options.assessEnvironmentImpl ?? assessEnvironment;
7853
+ let env = options.env ?? await assess();
7854
+ const autoInstall = options.autoInstall ?? !isNonInteractive();
7855
+ const lines = [];
7856
+ let status = pathCliStatus(env, runtimeVersion);
7857
+ if (status === "current") {
7858
+ return { status, env, upgraded: false, lines };
7859
+ }
7860
+ if (status === "behind" || status === "unknown") {
7861
+ lines.push(...driftLines(env, runtimeVersion));
7862
+ if (isShadow(env)) {
7863
+ lines.push(
7864
+ "That PATH hit is not npm's global bin, so `npm i -g` may not replace it.",
7865
+ "Run `hash -r` and `which -a agent-kit`. Put npm's global bin first, or remove the stale shim."
7866
+ );
7867
+ }
7868
+ }
7869
+ if (!autoInstall) {
7870
+ return { status, env, upgraded: false, lines };
7871
+ }
7872
+ if (status === "missing") {
7873
+ lines.push("No bare `agent-kit` on PATH yet (npx is ephemeral).");
7874
+ }
7875
+ if (!env.npmPrefixWritable) {
7876
+ lines.push(
7877
+ "npm's global prefix is not writable, so this process cannot upgrade PATH.",
7878
+ `Run ${npxPinned(runtimeVersion, "setup-global")} (or fix prefix permissions).`
7879
+ );
7880
+ return { status, env, upgraded: false, lines };
7881
+ }
7882
+ const spec = pinnedCliSpec(runtimeVersion);
7883
+ const install = options.npmInstallImpl ?? spawnNpmGlobalInstall;
7884
+ lines.push(`Installing ${spec} into the writable npm prefix...`);
7885
+ const outcome = await install(spec);
7886
+ if (!outcome.ok) {
7887
+ const detail = outcome.error instanceof Error ? outcome.error.message : "npm i -g failed";
7888
+ lines.push(`Global install failed: ${detail}`);
7889
+ return { status, env, upgraded: false, lines };
7890
+ }
7891
+ env = await assess();
7892
+ status = pathCliStatus(env, runtimeVersion);
7893
+ if (status === "current") {
7894
+ lines.push(
7895
+ `PATH \`agent-kit\` is now v${env.binVersion ?? runtimeVersion}.`,
7896
+ "If this shell still shows the old version, run `hash -r` or open a new terminal."
7897
+ );
7898
+ return { status, env, upgraded: true, lines };
7899
+ }
7900
+ lines.push(
7901
+ `Installed ${spec}, but PATH still resolves to v${env.binVersion ?? "unknown"} at:`,
7902
+ ` ${env.binPath ?? "(unresolved)"}`,
7903
+ "Run `hash -r` and `which -a agent-kit`. Until PATH matches, keep using the npx pin above."
7904
+ );
7905
+ return { status, env, upgraded: true, lines };
7906
+ }
7907
+
6931
7908
  // src/commands/install.ts
6932
- import path38 from "path";
7909
+ import path39 from "path";
6933
7910
  import { defineCommand as defineCommand11 } from "citty";
6934
7911
  import { bold, cyan as cyan3, green as green2, options as koloristOptions2 } from "kolorist";
6935
7912
 
6936
7913
  // src/generator/personalization.ts
6937
- import { readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
6938
- import path36 from "path";
7914
+ import { readFile as readFile21, writeFile as writeFile12 } from "fs/promises";
7915
+ import path37 from "path";
6939
7916
 
6940
7917
  // src/generator/claude-command-adapters.ts
6941
- import { readFile as readFile17, readdir as readdir7, writeFile as writeFile8 } from "fs/promises";
6942
- import path32 from "path";
7918
+ import { readFile as readFile19, readdir as readdir9, writeFile as writeFile8 } from "fs/promises";
7919
+ import path33 from "path";
6943
7920
  var CURSOR_COMMANDS_DIR_REL = ".cursor/commands";
6944
7921
  var CLAUDE_COMMANDS_DIR_REL = ".claude/commands";
6945
7922
  var RESERVED_ADAPTER_NAMES = /* @__PURE__ */ new Set(["agent-kit"]);
@@ -6961,17 +7938,17 @@ description: ${command.description}
6961
7938
  Read \`.cursor/commands/${command.name}.md\` now and follow that contract exactly \u2014 it is the source of truth for /${command.name}; this file is only a thin adapter for Claude Code.
6962
7939
 
6963
7940
  Adapter rules (Claude Code CLI):
6964
- - Cursor "Ask questions" is unavailable here: use AskUserQuestion when possible, else present the same labels as one numbered list per message and WAIT for the answer.
7941
+ - Cursor "Ask questions" is unavailable here: use AskUserQuestion when possible, else print one line \`HITL_GATE: <ask-id> | <label 1> | <label 2> | ...\` immediately followed by the same labels as one numbered list, one list per message, and WAIT for the answer (ask-id and labels: \`.cursor/skills/core/hitl-gates/SKILL.md\`). A headless reply arrives as \`HITL_REPLY: <ask-id> | operator reply <n> | <label>\`; cite that line as the Ask provenance.
6965
7942
  - Skip or cancel means stop.
6966
7943
  - Never \`/git-prod\` without an explicit operator yes.
6967
7944
  - Do not clone Cursor hooks or invent behavior beyond the SoT file.
6968
7945
  `;
6969
7946
  }
6970
7947
  async function discoverInstalledCommands(rootDir) {
6971
- const dir = path32.join(rootDir, CURSOR_COMMANDS_DIR_REL);
7948
+ const dir = path33.join(rootDir, CURSOR_COMMANDS_DIR_REL);
6972
7949
  let entries;
6973
7950
  try {
6974
- entries = (await readdir7(dir)).filter((f) => f.endsWith(".md"));
7951
+ entries = (await readdir9(dir)).filter((f) => f.endsWith(".md"));
6975
7952
  } catch {
6976
7953
  return [];
6977
7954
  }
@@ -6980,7 +7957,7 @@ async function discoverInstalledCommands(rootDir) {
6980
7957
  const name = file.slice(0, -3);
6981
7958
  if (RESERVED_ADAPTER_NAMES.has(name)) continue;
6982
7959
  try {
6983
- const raw = await readFile17(path32.join(dir, file), "utf8");
7960
+ const raw = await readFile19(path33.join(dir, file), "utf8");
6984
7961
  const parsed = parseCommandFrontmatter(name, raw);
6985
7962
  if (parsed) commands.push(parsed);
6986
7963
  } catch {
@@ -6995,18 +7972,18 @@ async function generateClaudeCommandAdapters(rootDir) {
6995
7972
  const results = [];
6996
7973
  let ledgerDirty = false;
6997
7974
  for (const command of commands) {
6998
- const relPath = path32.posix.join(CLAUDE_COMMANDS_DIR_REL, `${command.name}.md`);
7975
+ const relPath = path33.posix.join(CLAUDE_COMMANDS_DIR_REL, `${command.name}.md`);
6999
7976
  const rendered = renderClaudeCommandAdapter(command);
7000
- const abs = path32.join(rootDir, relPath);
7977
+ const abs = path33.join(rootDir, relPath);
7001
7978
  if (!await fileExists(abs)) {
7002
- await ensureDir(path32.dirname(abs));
7979
+ await ensureDir(path33.dirname(abs));
7003
7980
  await writeFile8(abs, rendered, "utf8");
7004
7981
  ledger.hashes[relPath] = contentHash(rendered);
7005
7982
  ledgerDirty = true;
7006
7983
  results.push({ relativePath: relPath, status: "applied" });
7007
7984
  continue;
7008
7985
  }
7009
- const localContent = await readFile17(abs, "utf8");
7986
+ const localContent = await readFile19(abs, "utf8");
7010
7987
  if (localContent === rendered) {
7011
7988
  if (ledger.hashes[relPath] !== contentHash(rendered)) {
7012
7989
  ledger.hashes[relPath] = contentHash(rendered);
@@ -7030,7 +8007,7 @@ async function generateClaudeCommandAdapters(rootDir) {
7030
8007
 
7031
8008
  // src/generator/claude-kit-load.ts
7032
8009
  import { writeFile as writeFile9 } from "fs/promises";
7033
- import path33 from "path";
8010
+ import path34 from "path";
7034
8011
  var CLAUDE_MD_REL = "CLAUDE.md";
7035
8012
  var AGENT_KIT_COMMAND_REL = ".claude/commands/agent-kit.md";
7036
8013
  function renderClaudeMd() {
@@ -7086,11 +8063,11 @@ Non-goals: not audits / \`/plan-external-review\`, not a second tick dialect (\`
7086
8063
  `;
7087
8064
  }
7088
8065
  async function writeUnlessExists(rootDir, relativePath, content) {
7089
- const target = path33.join(rootDir, relativePath);
8066
+ const target = path34.join(rootDir, relativePath);
7090
8067
  if (await fileExists(target)) {
7091
8068
  return { relativePath, status: "skipped-customized" };
7092
8069
  }
7093
- await ensureDir(path33.dirname(target));
8070
+ await ensureDir(path34.dirname(target));
7094
8071
  await writeFile9(target, content, "utf8");
7095
8072
  return { relativePath, status: "applied" };
7096
8073
  }
@@ -7102,8 +8079,8 @@ async function generateClaudeKitLoadArtifacts(rootDir) {
7102
8079
  }
7103
8080
 
7104
8081
  // src/generator/claude-session-start-hook.ts
7105
- import { readFile as readFile18, writeFile as writeFile10 } from "fs/promises";
7106
- import path34 from "path";
8082
+ import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
8083
+ import path35 from "path";
7107
8084
  var CLAUDE_SETTINGS_REL = ".claude/settings.json";
7108
8085
  var RESOLVE_AGENT_KIT_REL = ".cursor/hooks/agent/resolve-agent-kit.sh";
7109
8086
  var SESSION_START_HOOK_MARKER = "hook session-start --format claude";
@@ -7178,10 +8155,10 @@ function instructionsBlock(entry) {
7178
8155
  ].join("\n");
7179
8156
  }
7180
8157
  async function writeClaudeSessionStartHook(rootDir) {
7181
- const abs = path34.join(rootDir, CLAUDE_SETTINGS_REL);
8158
+ const abs = path35.join(rootDir, CLAUDE_SETTINGS_REL);
7182
8159
  let existing = null;
7183
8160
  try {
7184
- existing = await readFile18(abs, "utf8");
8161
+ existing = await readFile20(abs, "utf8");
7185
8162
  } catch {
7186
8163
  existing = null;
7187
8164
  }
@@ -7196,14 +8173,14 @@ async function writeClaudeSessionStartHook(rootDir) {
7196
8173
  if (merged.status === "unchanged") {
7197
8174
  return { relativePath: CLAUDE_SETTINGS_REL, status: "unchanged" };
7198
8175
  }
7199
- await ensureDir(path34.dirname(abs));
8176
+ await ensureDir(path35.dirname(abs));
7200
8177
  await writeFile10(abs, merged.content, "utf8");
7201
8178
  return { relativePath: CLAUDE_SETTINGS_REL, status: merged.status };
7202
8179
  }
7203
8180
 
7204
8181
  // src/generator/vscode.ts
7205
8182
  import { writeFile as writeFile11 } from "fs/promises";
7206
- import path35 from "path";
8183
+ import path36 from "path";
7207
8184
 
7208
8185
  // src/generator/platform.ts
7209
8186
  function gitProviderLabel(profile) {
@@ -7227,10 +8204,10 @@ function prTerminology(profile) {
7227
8204
  // src/generator/vscode.ts
7228
8205
  async function generateVSCodeArtifacts(profile) {
7229
8206
  const results = [];
7230
- const vscodeDir = path35.join(profile.rootDir, ".vscode");
7231
- const githubDir = path35.join(profile.rootDir, ".github");
8207
+ const vscodeDir = path36.join(profile.rootDir, ".vscode");
8208
+ const githubDir = path36.join(profile.rootDir, ".github");
7232
8209
  await Promise.all([ensureDir(vscodeDir), ensureDir(githubDir)]);
7233
- const settingsPath = path35.join(vscodeDir, "settings.json");
8210
+ const settingsPath = path36.join(vscodeDir, "settings.json");
7234
8211
  if (await fileExists(settingsPath)) {
7235
8212
  results.push({ relativePath: ".vscode/settings.json", status: "skipped-customized" });
7236
8213
  } else {
@@ -7254,7 +8231,7 @@ async function generateVSCodeArtifacts(profile) {
7254
8231
  }
7255
8232
  const provider = gitProviderLabel(profile);
7256
8233
  const prTerm = prTerminology(profile);
7257
- const copilotPath = path35.join(githubDir, "copilot-instructions.md");
8234
+ const copilotPath = path36.join(githubDir, "copilot-instructions.md");
7258
8235
  if (await fileExists(copilotPath)) {
7259
8236
  results.push({ relativePath: ".github/copilot-instructions.md", status: "skipped-customized" });
7260
8237
  } else {
@@ -7272,7 +8249,7 @@ async function generateVSCodeArtifacts(profile) {
7272
8249
  results.push({ relativePath: ".github/copilot-instructions.md", status: "applied" });
7273
8250
  }
7274
8251
  if (profile.ide.plan === "vscode-pro") {
7275
- const securityPath = path35.join(vscodeDir, "security-review.agent.md");
8252
+ const securityPath = path36.join(vscodeDir, "security-review.agent.md");
7276
8253
  if (await fileExists(securityPath)) {
7277
8254
  results.push({
7278
8255
  relativePath: ".vscode/security-review.agent.md",
@@ -7484,7 +8461,7 @@ function renderProjectContext(profile, skillItems = []) {
7484
8461
  `;
7485
8462
  }
7486
8463
  async function createOwnedFile(rootDir, relativePath, content, evidence) {
7487
- const target = path36.join(rootDir, relativePath);
8464
+ const target = path37.join(rootDir, relativePath);
7488
8465
  if (await fileExists(target)) {
7489
8466
  return {
7490
8467
  kind: "file",
@@ -7494,7 +8471,7 @@ async function createOwnedFile(rootDir, relativePath, content, evidence) {
7494
8471
  evidence
7495
8472
  };
7496
8473
  }
7497
- await ensureDir(path36.dirname(target));
8474
+ await ensureDir(path37.dirname(target));
7498
8475
  await writeFile12(target, content, "utf8");
7499
8476
  return {
7500
8477
  kind: "file",
@@ -7511,7 +8488,7 @@ async function packTargets(registryRoot, packId) {
7511
8488
  async function existingTargets(projectRoot, targets) {
7512
8489
  const checks = await Promise.all(
7513
8490
  targets.map(
7514
- async (target) => await fileExists(path36.join(projectRoot, target)) ? target : null
8491
+ async (target) => await fileExists(path37.join(projectRoot, target)) ? target : null
7515
8492
  )
7516
8493
  );
7517
8494
  return checks.filter((target) => target !== null);
@@ -7533,14 +8510,14 @@ async function applyPersonalization(input) {
7533
8510
  componentResults.push({ ...item, status: "unavailable" });
7534
8511
  continue;
7535
8512
  }
7536
- const target = path36.posix.join(
8513
+ const target = path37.posix.join(
7537
8514
  ".cursor",
7538
8515
  "skills",
7539
8516
  skill.path.includes("/core/") ? "core" : "community",
7540
8517
  skill.id,
7541
8518
  "SKILL.md"
7542
8519
  );
7543
- if (await fileExists(path36.join(input.rootDir, target))) {
8520
+ if (await fileExists(path37.join(input.rootDir, target))) {
7544
8521
  componentResults.push({ ...item, status: "skipped-customized", path: target });
7545
8522
  protectedPaths.add(target);
7546
8523
  continue;
@@ -7685,7 +8662,7 @@ async function applyPersonalization(input) {
7685
8662
  protectedPaths: [...protectedPaths].sort(),
7686
8663
  ...claudeSessionStartInstructions ? { claudeSessionStartInstructions } : {}
7687
8664
  };
7688
- await writeJson(path36.join(input.rootDir, RESULT_PATH), result);
8665
+ await writeJson(path37.join(input.rootDir, RESULT_PATH), result);
7689
8666
  return {
7690
8667
  result,
7691
8668
  manifest: {
@@ -7703,26 +8680,26 @@ async function applyPersonalization(input) {
7703
8680
  };
7704
8681
  }
7705
8682
  async function readRepositoryProfile(rootDir) {
7706
- const target = path36.join(rootDir, ".cursor/agent-kit.config.json");
8683
+ const target = path37.join(rootDir, ".cursor/agent-kit.config.json");
7707
8684
  if (!await fileExists(target)) return null;
7708
- return JSON.parse(await readFile19(target, "utf8"));
8685
+ return JSON.parse(await readFile21(target, "utf8"));
7709
8686
  }
7710
8687
 
7711
8688
  // src/lifecycle/onboard-migration.ts
7712
8689
  import { createHash as createHash4 } from "crypto";
7713
- import { readFile as readFile20, unlink } from "fs/promises";
7714
- import path37 from "path";
8690
+ import { readFile as readFile22, unlink } from "fs/promises";
8691
+ import path38 from "path";
7715
8692
  var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
7716
8693
  var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
7717
8694
  var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
7718
8695
  "b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
7719
8696
  ]);
7720
8697
  async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
7721
- const legacyPath = path37.join(projectRoot, LEGACY_ONBOARD_PATH);
8698
+ const legacyPath = path38.join(projectRoot, LEGACY_ONBOARD_PATH);
7722
8699
  if (!await fileExists(legacyPath)) return "absent";
7723
- const namespacedPath = path37.join(projectRoot, NAMESPACED_ONBOARD_PATH);
8700
+ const namespacedPath = path38.join(projectRoot, NAMESPACED_ONBOARD_PATH);
7724
8701
  if (!await fileExists(namespacedPath)) return "preserved-customized";
7725
- const content = await readFile20(legacyPath);
8702
+ const content = await readFile22(legacyPath);
7726
8703
  const hash = createHash4("sha256").update(content).digest("hex");
7727
8704
  if (!managedHashes.has(hash)) return "preserved-customized";
7728
8705
  await unlink(legacyPath);
@@ -7810,8 +8787,15 @@ function paint(fn, text) {
7810
8787
  function printInstallEpilogue(env, options = {}) {
7811
8788
  const print = options.print ?? ((line) => console.log(line));
7812
8789
  const color = options.color ?? shouldUseWelcomeColor();
7813
- if (env.binOnPath) {
7814
- const line = "`agent-kit` is on PATH \u2014 run it directly, e.g. `agent-kit doctor`.";
8790
+ const runtimeVersion = options.runtimeVersion ?? KIT_VERSION;
8791
+ const status = pathCliStatus(env, runtimeVersion);
8792
+ if (status === "current") {
8793
+ const line = `PATH \`agent-kit\` is v${env.binVersion ?? runtimeVersion}. Run it directly, e.g. \`agent-kit doctor\`.`;
8794
+ print(color ? paint(green2, line) : line);
8795
+ return;
8796
+ }
8797
+ if (status === "behind" || status === "unknown") {
8798
+ const line = `Keep using ${npxPinned(runtimeVersion, "<subcommand>")} until PATH is v${runtimeVersion}.`;
7815
8799
  print(color ? paint(green2, line) : line);
7816
8800
  return;
7817
8801
  }
@@ -7821,11 +8805,11 @@ function printInstallEpilogue(env, options = {}) {
7821
8805
  'If you try `agent-kit <subcommand>` next, you will see "command not',
7822
8806
  'found". Pick one:',
7823
8807
  "",
7824
- " 1. Keep using npx \u2014 works right now, no action needed",
7825
- " npx @dadado/agent-kit-cli@latest <subcommand>",
8808
+ " 1. Keep using npx (works right now, no action needed)",
8809
+ ` ${npxPinned(runtimeVersion, "<subcommand>")}`,
7826
8810
  "",
7827
8811
  " 2. Put a bare `agent-kit` on PATH",
7828
- " npx @dadado/agent-kit-cli@latest setup-global",
8812
+ ` ${npxPinned(runtimeVersion, "setup-global")}`,
7829
8813
  " (fixes a root-owned npm prefix if that's the blocker, or just installs)",
7830
8814
  "",
7831
8815
  " 3. Manual steps",
@@ -7833,7 +8817,7 @@ function printInstallEpilogue(env, options = {}) {
7833
8817
  " mkdir -p ~/.npm-global",
7834
8818
  ' npm config set prefix "~/.npm-global"',
7835
8819
  ' export PATH="~/.npm-global/bin:$PATH"',
7836
- " npm i -g @dadado/agent-kit-cli@latest"
8820
+ ` npm i -g ${pinnedCliSpec(runtimeVersion)}`
7837
8821
  ];
7838
8822
  print(color ? paint(cyan3, divider) : divider);
7839
8823
  const heading = "Heads up: a bare `agent-kit` command won't work yet";
@@ -7844,10 +8828,12 @@ function printInstallEpilogue(env, options = {}) {
7844
8828
  async function printPostInstallSummary(result) {
7845
8829
  printReadinessNarrative(result);
7846
8830
  const env = await assessEnvironment();
7847
- printInstallEpilogue(env);
8831
+ const sync = await syncPathCliToRuntime({ runtimeVersion: KIT_VERSION, env });
8832
+ for (const line of sync.lines) console.log(line);
8833
+ printInstallEpilogue(sync.env, { runtimeVersion: KIT_VERSION });
7848
8834
  }
7849
8835
  async function performInstall(options) {
7850
- const projectRoot = path38.resolve(options.cwd);
8836
+ const projectRoot = path39.resolve(options.cwd);
7851
8837
  const packs = parsePackList(options.pack);
7852
8838
  const existing = await loadAgentKitManifest(projectRoot);
7853
8839
  const registry = await resolveRegistryFromCli({
@@ -7970,6 +8956,7 @@ ${err.recovery}
7970
8956
  throw err;
7971
8957
  }
7972
8958
  logger.info(`Installing into: ${projectRoot}`);
8959
+ logger.info(`CLI v${KIT_VERSION}`);
7973
8960
  await warnIfRunningCliBehindNpm(projectRoot, { warn: (message) => logger.warn(message) });
7974
8961
  const packs = parsePackList(args.pack);
7975
8962
  for (const id of packs) {
@@ -8072,12 +9059,13 @@ ${err.recovery}
8072
9059
  logger.success(`L0 and readiness prepared in ${result.projectRoot}`);
8073
9060
  const nextStep = nextStepAfterInstall(pending);
8074
9061
  const env = await assessEnvironment();
9062
+ const sync = await syncPathCliToRuntime({ runtimeVersion: KIT_VERSION, env });
9063
+ for (const line of sync.lines) console.log(line);
9064
+ printInstallEpilogue(sync.env, { runtimeVersion: KIT_VERSION });
8075
9065
  if (!nonInteractive) {
8076
- printInstallEpilogue(env);
8077
9066
  outro(nextStep);
8078
9067
  } else {
8079
9068
  logger.info(nextStep);
8080
- printInstallEpilogue(env);
8081
9069
  }
8082
9070
  } catch (err) {
8083
9071
  const hint = classifyInstallError(err);
@@ -8246,11 +9234,333 @@ function shouldLiveRefresh(opts) {
8246
9234
  const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
8247
9235
  return tty;
8248
9236
  }
9237
+ function renderProgressBar(opts) {
9238
+ const width = Math.max(4, Math.floor(opts.width ?? 20));
9239
+ const total = Math.max(0, Math.floor(opts.total));
9240
+ const done = Math.min(total, Math.max(0, Math.floor(opts.done)));
9241
+ const filled = total === 0 ? 0 : Math.round(done / total * width);
9242
+ const color = opts.color ?? false;
9243
+ const bar = `${hexPaint(HELMET_ACCENT, "\u2588".repeat(filled), color)}${hexPaint(
9244
+ LABEL_MUTED,
9245
+ "\u2591".repeat(width - filled),
9246
+ color
9247
+ )}`;
9248
+ return `${hexPaint(HELMET_OUTLINE, "[", color)}${bar}${hexPaint(HELMET_OUTLINE, "]", color)} ${hexPaint(
9249
+ HELMET_OUTLINE,
9250
+ `${done}/${total}`,
9251
+ color
9252
+ )}`;
9253
+ }
9254
+ var ScrollRegion = class {
9255
+ lines = [];
9256
+ offset = 0;
9257
+ capacity;
9258
+ constructor(capacity = 500) {
9259
+ this.capacity = Math.max(1, Math.floor(capacity));
9260
+ }
9261
+ get length() {
9262
+ return this.lines.length;
9263
+ }
9264
+ get scrolledUp() {
9265
+ return this.offset;
9266
+ }
9267
+ push(line) {
9268
+ this.lines.push(line);
9269
+ if (this.lines.length > this.capacity) {
9270
+ this.lines.splice(0, this.lines.length - this.capacity);
9271
+ }
9272
+ if (this.offset > 0) this.offset = Math.min(this.offset, Math.max(0, this.lines.length - 1));
9273
+ }
9274
+ scrollUp(n = 1) {
9275
+ this.offset = Math.min(Math.max(0, this.lines.length - 1), this.offset + Math.max(0, n));
9276
+ }
9277
+ scrollDown(n = 1) {
9278
+ this.offset = Math.max(0, this.offset - Math.max(0, n));
9279
+ }
9280
+ scrollToBottom() {
9281
+ this.offset = 0;
9282
+ }
9283
+ /** The last `height` lines (minus the scroll offset). */
9284
+ viewport(height) {
9285
+ const h = Math.max(0, Math.floor(height));
9286
+ const end = Math.max(0, this.lines.length - this.offset);
9287
+ const start = Math.max(0, end - h);
9288
+ return { lines: this.lines.slice(start, end), above: start, below: this.lines.length - end };
9289
+ }
9290
+ all() {
9291
+ return this.lines;
9292
+ }
9293
+ };
9294
+ var KEY_LEFT = `${ESC}[D`;
9295
+ var KEY_RIGHT = `${ESC}[C`;
9296
+ var KEY_HOME_SET = /* @__PURE__ */ new Set([`${ESC}[H`, `${ESC}[1~`, `${ESC}OH`]);
9297
+ var KEY_END_SET = /* @__PURE__ */ new Set([`${ESC}[F`, `${ESC}[4~`, `${ESC}OF`]);
9298
+ var KEY_DELETE = `${ESC}[3~`;
9299
+ function splitKeys(chunk) {
9300
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
9301
+ const keys = [];
9302
+ let i = 0;
9303
+ while (i < text.length) {
9304
+ const ch = text[i] ?? "";
9305
+ if (ch === ESC && i + 1 < text.length) {
9306
+ const next = text[i + 1];
9307
+ if (next === "[" || next === "O") {
9308
+ let j = i + 2;
9309
+ while (j < text.length && !/[A-Za-z~]/.test(text[j] ?? "")) j += 1;
9310
+ keys.push(text.slice(i, Math.min(text.length, j + 1)));
9311
+ i = j + 1;
9312
+ continue;
9313
+ }
9314
+ }
9315
+ keys.push(ch);
9316
+ i += 1;
9317
+ }
9318
+ return keys;
9319
+ }
9320
+ var InputLine = class {
9321
+ buffer = "";
9322
+ cursorPos = 0;
9323
+ get value() {
9324
+ return this.buffer;
9325
+ }
9326
+ get cursor() {
9327
+ return this.cursorPos;
9328
+ }
9329
+ reset() {
9330
+ this.buffer = "";
9331
+ this.cursorPos = 0;
9332
+ }
9333
+ handleKey(chunk) {
9334
+ let result = "ignored";
9335
+ for (const key of splitKeys(chunk)) {
9336
+ const r = this.handleOne(key);
9337
+ if (r === "submit" || r === "sigint" || r === "eof") return r;
9338
+ if (r === "edit") result = "edit";
9339
+ }
9340
+ return result;
9341
+ }
9342
+ handleOne(key) {
9343
+ if (key === "\r" || key === "\n") return "submit";
9344
+ if (key === "") return "sigint";
9345
+ if (key === "") return this.buffer.length === 0 ? "eof" : "ignored";
9346
+ if (key === "\x7F" || key === "\b") {
9347
+ if (this.cursorPos === 0) return "ignored";
9348
+ this.buffer = this.buffer.slice(0, this.cursorPos - 1) + this.buffer.slice(this.cursorPos);
9349
+ this.cursorPos -= 1;
9350
+ return "edit";
9351
+ }
9352
+ if (key === KEY_DELETE) {
9353
+ if (this.cursorPos >= this.buffer.length) return "ignored";
9354
+ this.buffer = this.buffer.slice(0, this.cursorPos) + this.buffer.slice(this.cursorPos + 1);
9355
+ return "edit";
9356
+ }
9357
+ if (key === KEY_LEFT) {
9358
+ if (this.cursorPos === 0) return "ignored";
9359
+ this.cursorPos -= 1;
9360
+ return "edit";
9361
+ }
9362
+ if (key === KEY_RIGHT) {
9363
+ if (this.cursorPos >= this.buffer.length) return "ignored";
9364
+ this.cursorPos += 1;
9365
+ return "edit";
9366
+ }
9367
+ if (KEY_HOME_SET.has(key) || key === "") {
9368
+ this.cursorPos = 0;
9369
+ return "edit";
9370
+ }
9371
+ if (KEY_END_SET.has(key) || key === "") {
9372
+ this.cursorPos = this.buffer.length;
9373
+ return "edit";
9374
+ }
9375
+ if (key === "") {
9376
+ if (this.buffer.length === 0) return "ignored";
9377
+ this.reset();
9378
+ return "edit";
9379
+ }
9380
+ if (key === "") {
9381
+ if (this.cursorPos === 0) return "ignored";
9382
+ const head = this.buffer.slice(0, this.cursorPos).replace(/\S*\s*$/, "");
9383
+ this.buffer = head + this.buffer.slice(this.cursorPos);
9384
+ this.cursorPos = head.length;
9385
+ return "edit";
9386
+ }
9387
+ if (key.startsWith(ESC)) return "ignored";
9388
+ const code = key.codePointAt(0) ?? 0;
9389
+ if (code < 32 || code === 127) return "ignored";
9390
+ this.buffer = this.buffer.slice(0, this.cursorPos) + key + this.buffer.slice(this.cursorPos);
9391
+ this.cursorPos += key.length;
9392
+ return "edit";
9393
+ }
9394
+ /** `> text` with the cursor cell marked by reverse video (plain: `_` at the end). */
9395
+ render(width, color, prompt = "> ") {
9396
+ const max = Math.max(4, width - prompt.length - 1);
9397
+ let start = 0;
9398
+ if (this.cursorPos >= max) start = this.cursorPos - max + 1;
9399
+ const visible = this.buffer.slice(start, start + max);
9400
+ const at = this.cursorPos - start;
9401
+ const before = visible.slice(0, at);
9402
+ const cell = visible.slice(at, at + 1) || " ";
9403
+ const after = visible.slice(at + 1);
9404
+ const painted = color ? `${hexPaint(HELMET_OUTLINE, before, true)}${ESC}[7m${cell}${ESC}[0m${hexPaint(HELMET_OUTLINE, after, true)}` : `${before}${cell === " " && after === "" ? "_" : cell}${after}`;
9405
+ return `${hexPaint(HELMET_ACCENT, prompt, color)}${painted}`;
9406
+ }
9407
+ };
9408
+ var LIVE_MIN_LOG_ROWS = 3;
9409
+ var LIVE_MAX_LOG_ROWS = 40;
9410
+ var LIVE_CHECKLIST_CAP = 4;
9411
+ function formatElapsed(ms) {
9412
+ const s = Math.max(0, Math.floor(ms / 1e3));
9413
+ const m = Math.floor(s / 60);
9414
+ const h = Math.floor(m / 60);
9415
+ if (h > 0) return `${h}h ${m % 60}m`;
9416
+ if (m > 0) return `${m}m ${s % 60}s`;
9417
+ return `${s}s`;
9418
+ }
9419
+ function liveLineHex(kind) {
9420
+ switch (kind) {
9421
+ case "tool":
9422
+ return HELMET_ACCENT;
9423
+ case "result":
9424
+ return STATUS_OK;
9425
+ case "error":
9426
+ return STATUS_ERR;
9427
+ case "rate_limit":
9428
+ return STATUS_WARN;
9429
+ case "hitl":
9430
+ return HELMET_ACCENT;
9431
+ case "hook":
9432
+ case "prompt":
9433
+ case "note":
9434
+ return LABEL_MUTED;
9435
+ default:
9436
+ return HELMET_OUTLINE;
9437
+ }
9438
+ }
9439
+ function renderMcLive(view, opts = {}) {
9440
+ const color = shouldUseWelcomeColor(opts);
9441
+ const motion = shouldUseVisualMotion(opts);
9442
+ const columns = Math.max(
9443
+ MIN_COLUMNS,
9444
+ Math.floor(opts.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)
9445
+ );
9446
+ const rows = Math.max(12, Math.floor(opts.rows ?? process.stdout.rows ?? 24));
9447
+ const inner = columns - 2 - INNER_PAD;
9448
+ const now2 = opts.now ?? Date.now();
9449
+ const running = view.crew.phase === "running" || view.crew.phase === "gate";
9450
+ const mark = motion && running ? spinnerFrame(opts.frameIndex ?? 0) : SPACE_MARKS.tick;
9451
+ const header = hexPaint(
9452
+ HELMET_ACCENT,
9453
+ `${mark} Mission Control ${SPACE_MARKS.star} live run ${SPACE_MARKS.tick} ${view.crew.backend ?? "agent"} ${SPACE_MARKS.tick} ${view.crew.phase}`,
9454
+ color
9455
+ );
9456
+ const barWidth = Math.max(8, Math.min(24, Math.floor(inner / 3)));
9457
+ const missionBody = [];
9458
+ if (view.error) {
9459
+ missionBody.push(
9460
+ hexPaint(STATUS_ERR, wrapNarrow(view.error, inner).split("\n")[0] ?? "", color)
9461
+ );
9462
+ } else {
9463
+ const m = view.mission;
9464
+ missionBody.push(
9465
+ `${labeled("plan ", lineOrQuiet(m.planFile, "none"), color)}${m.mode ? hexPaint(LABEL_MUTED, ` \xB7 ${m.mode}`, color) : ""}`
9466
+ );
9467
+ const todoTail = m.currentTodo ? ` now ${m.currentTodo}` : "";
9468
+ missionBody.push(
9469
+ `${hexPaint(LABEL_MUTED, "to-dos ", color)}${renderProgressBar({
9470
+ done: m.todos.done,
9471
+ total: m.todos.total,
9472
+ width: barWidth,
9473
+ color
9474
+ })}${hexPaint(HELMET_OUTLINE, todoTail, color)}`
9475
+ );
9476
+ if (m.queue) {
9477
+ const tail = m.queue.status ? ` ${m.queue.status}` : "";
9478
+ missionBody.push(
9479
+ `${hexPaint(LABEL_MUTED, "plans ", color)}${renderProgressBar({
9480
+ done: m.queue.done,
9481
+ total: m.queue.total,
9482
+ width: barWidth,
9483
+ color
9484
+ })}${hexPaint(HELMET_OUTLINE, tail, color)}`
9485
+ );
9486
+ }
9487
+ if (m.nextTodo) missionBody.push(labeled("next ", m.nextTodo, color));
9488
+ }
9489
+ if (view.gate) {
9490
+ const g = view.gate;
9491
+ const head = g.detection === "fallback" ? `${g.askId} (prose fallback)` : g.askId;
9492
+ missionBody.push(
9493
+ `${hexPaint(LABEL_MUTED, "gate ", color)}${hexPaint(STATUS_WARN, head, color)}${hexPaint(
9494
+ LABEL_MUTED,
9495
+ " \xB7 number or label replies; empty, skip, cancel or Ctrl-C stops",
9496
+ color
9497
+ )}`
9498
+ );
9499
+ g.labels.forEach((label, i) => {
9500
+ missionBody.push(hexPaint(HELMET_OUTLINE, ` ${i + 1}. ${label}`, color));
9501
+ });
9502
+ missionBody.push(` ${view.gate.input.render(inner - 9, color)}`);
9503
+ }
9504
+ const checklistBody = view.checklist.length === 0 ? [muted("no queue", color)] : view.checklist.slice(0, LIVE_CHECKLIST_CAP).map((row) => {
9505
+ const markOf = row.state === "done" ? SPACE_MARKS.star : row.state === "current" ? "\u25B6" : SPACE_MARKS.tick;
9506
+ const hex = row.state === "done" ? STATUS_OK : row.state === "current" ? HELMET_ACCENT : LABEL_MUTED;
9507
+ return hexPaint(hex, `${markOf} ${row.label}`, color);
9508
+ });
9509
+ const c = view.crew;
9510
+ const crewLine1 = [
9511
+ c.backend ?? "agent",
9512
+ c.pid !== void 0 ? `pid ${c.pid}` : "no pid",
9513
+ c.phase,
9514
+ formatElapsed(now2 - c.startedAt),
9515
+ c.costUsd !== void 0 ? `$${c.costUsd.toFixed(c.costUsd < 0.1 ? 4 : 2)}` : null,
9516
+ c.turns !== void 0 ? `${c.turns} turn${c.turns === 1 ? "" : "s"}` : null,
9517
+ `${c.toolCalls} tool call${c.toolCalls === 1 ? "" : "s"}`
9518
+ ].filter((part) => Boolean(part)).join(` ${SPACE_MARKS.tick} `);
9519
+ const crewBody = [hexPaint(HELMET_OUTLINE, crewLine1, color)];
9520
+ if (c.currentTool) {
9521
+ crewBody.push(
9522
+ `${hexPaint(LABEL_MUTED, "tool ", color)}${hexPaint(HELMET_ACCENT, `${c.currentTool.name} ${SPACE_MARKS.tick} ${c.currentTool.summary}`, color)}`
9523
+ );
9524
+ } else if (c.lastResult) {
9525
+ crewBody.push(
9526
+ `${hexPaint(LABEL_MUTED, "last ", color)}${hexPaint(c.lastResult.ok ? STATUS_OK : STATUS_ERR, `result ${c.lastResult.status}`, color)}`
9527
+ );
9528
+ }
9529
+ if (c.rateLimit) {
9530
+ crewBody.push(
9531
+ `${hexPaint(LABEL_MUTED, "limit ", color)}${hexPaint(c.rateLimit.severe ? STATUS_ERR : STATUS_WARN, c.rateLimit.line, color)}`
9532
+ );
9533
+ }
9534
+ if (c.logPath) crewBody.push(labeled("log ", c.logPath, color, LABEL_MUTED));
9535
+ let checklist = checklistBody;
9536
+ let crew = crewBody;
9537
+ const fixed = () => 1 + (missionBody.length + 2) + 2 + (checklist ? checklist.length + 2 : 0) + (crew.length + 2) + 1;
9538
+ if (fixed() + LIVE_MIN_LOG_ROWS > rows) checklist = null;
9539
+ if (fixed() + LIVE_MIN_LOG_ROWS > rows) crew = crew.slice(0, 1);
9540
+ const logRows = Math.max(1, Math.min(LIVE_MAX_LOG_ROWS, rows - fixed()));
9541
+ const viewport = view.flightLog.viewport(logRows);
9542
+ const flightBody = viewport.lines.map(
9543
+ (line) => hexPaint(liveLineHex(line.kind), line.text, color)
9544
+ );
9545
+ if (flightBody.length === 0) flightBody.push(muted("waiting for the agent stream", color));
9546
+ const flightTitle = viewport.above > 0 || viewport.below > 0 ? `Flight Log ${SPACE_MARKS.tick} ${viewport.above} above${viewport.below > 0 ? ` ${SPACE_MARKS.tick} ${viewport.below} below` : ""}` : "Flight Log";
9547
+ const panels = [
9548
+ ...box("Mission", missionBody, columns, color),
9549
+ ...box(flightTitle, flightBody, columns, color),
9550
+ ...checklist ? box("Checklist", checklist, columns, color) : [],
9551
+ ...box("Crew Monitor", crew, columns, color)
9552
+ ];
9553
+ const footer = muted(
9554
+ view.gate ? "Third surface \xB7 live feed \xB7 Enter sends the reply \xB7 Ctrl-C stops the run" : "Third surface \xB7 live feed \xB7 q / Ctrl-C stops the run \xB7 log keeps the full NDJSON",
9555
+ color
9556
+ );
9557
+ return [header, ...panels, footer].join("\n");
9558
+ }
8249
9559
 
8250
9560
  // src/mission-control/snapshot.ts
8251
9561
  import { execFile as execFile6 } from "child_process";
8252
9562
  import { access as access8 } from "fs/promises";
8253
- import path39 from "path";
9563
+ import path40 from "path";
8254
9564
 
8255
9565
  // ../../dashboard/lib/live-refresh.mjs
8256
9566
  import { join, resolve as resolve2 } from "path";
@@ -8283,7 +9593,7 @@ async function firstExisting2(candidates) {
8283
9593
  for (const candidate2 of candidates) {
8284
9594
  try {
8285
9595
  await access8(candidate2);
8286
- return path39.resolve(candidate2);
9596
+ return path40.resolve(candidate2);
8287
9597
  } catch {
8288
9598
  }
8289
9599
  }
@@ -8292,7 +9602,7 @@ async function firstExisting2(candidates) {
8292
9602
  async function findDashboardDataScript(cwd, env = process.env, options = {}) {
8293
9603
  const startPath = await findDashboardStart(cwd, env, options);
8294
9604
  if (startPath) {
8295
- const sibling = path39.join(path39.dirname(startPath), "dashboard-data.mjs");
9605
+ const sibling = path40.join(path40.dirname(startPath), "dashboard-data.mjs");
8296
9606
  try {
8297
9607
  await access8(sibling);
8298
9608
  return sibling;
@@ -8307,7 +9617,7 @@ async function loadDashboardSnapshot(opts) {
8307
9617
  const env = opts.env ?? process.env;
8308
9618
  const timeout = dataScriptTimeoutMs(env);
8309
9619
  const run = opts.execFileFn ?? execFile6;
8310
- const snapshotRoot = path39.resolve(opts.snapshotRoot);
9620
+ const snapshotRoot = path40.resolve(opts.snapshotRoot);
8311
9621
  return new Promise((resolve3) => {
8312
9622
  run(
8313
9623
  process.execPath,
@@ -8449,11 +9759,14 @@ function buildMcTuiView(snapshot, error = null) {
8449
9759
  var CLEAR_HOME = "\x1B[2J\x1B[H";
8450
9760
  var HIDE_CURSOR = "\x1B[?25l";
8451
9761
  var SHOW_CURSOR = "\x1B[?25h";
8452
- function isQuitKey(chunk) {
9762
+ function quitKeyOf(chunk) {
8453
9763
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
8454
- return text === "q" || text === "Q" || text === "";
9764
+ if (text === "q" || text === "Q") return "q";
9765
+ if (text === "") return "Ctrl-C";
9766
+ return null;
8455
9767
  }
8456
9768
  async function runMcTuiLoop(opts) {
9769
+ const render = opts.render ?? ((view, renderOpts) => renderMcTui(view, renderOpts));
8457
9770
  const live = shouldLiveRefresh({
8458
9771
  once: opts.once,
8459
9772
  stdoutIsTTY: opts.stdoutIsTTY,
@@ -8461,12 +9774,14 @@ async function runMcTuiLoop(opts) {
8461
9774
  });
8462
9775
  let frameIndex = 0;
8463
9776
  let inFlight = false;
8464
- const paint2 = async (clear) => {
8465
- if (inFlight) return;
9777
+ let stopped = false;
9778
+ const paint3 = async (clear) => {
9779
+ if (inFlight || stopped) return;
8466
9780
  inFlight = true;
8467
9781
  try {
8468
9782
  const view = await opts.loadView();
8469
- const frame = renderMcTui(view, {
9783
+ if (stopped) return;
9784
+ const frame = render(view, {
8470
9785
  ...opts.renderOpts,
8471
9786
  stdoutIsTTY: opts.stdoutIsTTY,
8472
9787
  frameIndex
@@ -8476,6 +9791,7 @@ async function runMcTuiLoop(opts) {
8476
9791
  ` : `${frame}
8477
9792
  `);
8478
9793
  } catch (err) {
9794
+ if (stopped) return;
8479
9795
  const message = err instanceof Error ? err.message : String(err);
8480
9796
  const frame = renderMcTui(buildMcTuiView(null, message), {
8481
9797
  ...opts.renderOpts,
@@ -8490,7 +9806,7 @@ async function runMcTuiLoop(opts) {
8490
9806
  inFlight = false;
8491
9807
  }
8492
9808
  };
8493
- await paint2(false);
9809
+ await paint3(false);
8494
9810
  if (!live) return null;
8495
9811
  opts.hooks.write(HIDE_CURSOR);
8496
9812
  const intervalMs = opts.intervalMs ?? PERIODIC_REFRESH_MS;
@@ -8499,7 +9815,7 @@ async function runMcTuiLoop(opts) {
8499
9815
  let timer;
8500
9816
  try {
8501
9817
  timer = setInt(() => {
8502
- void paint2(true);
9818
+ void paint3(true);
8503
9819
  }, intervalMs);
8504
9820
  } catch (err) {
8505
9821
  opts.hooks.write(SHOW_CURSOR);
@@ -8508,7 +9824,6 @@ async function runMcTuiLoop(opts) {
8508
9824
  const stdin = opts.hooks.stdin;
8509
9825
  const attachQuit = Boolean(stdin?.isTTY);
8510
9826
  let quitListener;
8511
- let stopped = false;
8512
9827
  const restoreStdin = () => {
8513
9828
  if (!stdin || !attachQuit) return;
8514
9829
  if (quitListener) {
@@ -8532,14 +9847,24 @@ async function runMcTuiLoop(opts) {
8532
9847
  stdin.setRawMode?.(true);
8533
9848
  stdin.resume?.();
8534
9849
  quitListener = (chunk) => {
8535
- if (!isQuitKey(chunk)) return;
9850
+ if (stopped) return;
9851
+ if (opts.hooks.onKey?.(chunk)) return;
9852
+ const key = quitKeyOf(chunk);
9853
+ if (!key) return;
8536
9854
  stop();
9855
+ if (opts.hooks.onQuit) {
9856
+ opts.hooks.onQuit(key);
9857
+ return;
9858
+ }
8537
9859
  const exitFn = opts.hooks.exit ?? ((code) => process.exit(code));
8538
9860
  exitFn(0);
8539
9861
  };
8540
9862
  stdin.on("data", quitListener);
8541
9863
  }
8542
- return { stop };
9864
+ return {
9865
+ stop,
9866
+ repaint: () => stopped ? Promise.resolve() : paint3(true)
9867
+ };
8543
9868
  }
8544
9869
 
8545
9870
  // src/commands/mission-control.ts
@@ -8615,13 +9940,13 @@ var missionControlCommand = defineCommand13({
8615
9940
  });
8616
9941
 
8617
9942
  // src/commands/monitors.ts
8618
- import path41 from "path";
9943
+ import path42 from "path";
8619
9944
  import { defineCommand as defineCommand14 } from "citty";
8620
9945
 
8621
9946
  // src/invariants/monitors-untriaged.ts
8622
9947
  import { execFile as execFile7 } from "child_process";
8623
- import { readFile as readFile21, readdir as readdir8, stat as stat5 } from "fs/promises";
8624
- import path40 from "path";
9948
+ import { readFile as readFile23, readdir as readdir10, stat as stat5 } from "fs/promises";
9949
+ import path41 from "path";
8625
9950
  import { promisify as promisify6 } from "util";
8626
9951
 
8627
9952
  // src/invariants/triage-heading.ts
@@ -8645,7 +9970,7 @@ function hasOpenGaps(content) {
8645
9970
  }
8646
9971
  async function listMonitorFiles(memoryDir) {
8647
9972
  try {
8648
- const names = await readdir8(memoryDir);
9973
+ const names = await readdir10(memoryDir);
8649
9974
  return names.filter((n) => n.startsWith("plan-monitor-") && n.endsWith(".md")).sort();
8650
9975
  } catch {
8651
9976
  return [];
@@ -8662,7 +9987,7 @@ async function gitFreshMonitorNames(rootDir) {
8662
9987
  for (const line of stdout.split("\n")) {
8663
9988
  if (!line.trim()) continue;
8664
9989
  const file = line.slice(3).trim().replace(/^.* -> /, "");
8665
- const base = path40.basename(file);
9990
+ const base = path41.basename(file);
8666
9991
  if (base.startsWith("plan-monitor-") && base.endsWith(".md")) {
8667
9992
  names.add(base);
8668
9993
  }
@@ -8685,15 +10010,15 @@ function monitorSlugFromName(fileName) {
8685
10010
  return fileName.replace(/^plan-monitor-/, "").replace(/\.md$/, "").toLowerCase();
8686
10011
  }
8687
10012
  async function selectUntriagedMonitors(rootDir) {
8688
- const root = path40.resolve(rootDir);
8689
- const memoryDir = path40.join(root, ".cursor", "memory");
10013
+ const root = path41.resolve(rootDir);
10014
+ const memoryDir = path41.join(root, ".cursor", "memory");
8690
10015
  const allNames = await listMonitorFiles(memoryDir);
8691
10016
  const selectionOrder = ["git-fresh", "handoff-aligned", "untriaged-scan"];
8692
10017
  const byName = /* @__PURE__ */ new Map();
8693
10018
  for (const name of allNames) {
8694
- const abs = path40.join(memoryDir, name);
10019
+ const abs = path41.join(memoryDir, name);
8695
10020
  try {
8696
- const [content, st] = await Promise.all([readFile21(abs, "utf8"), stat5(abs)]);
10021
+ const [content, st] = await Promise.all([readFile23(abs, "utf8"), stat5(abs)]);
8697
10022
  byName.set(name, { content, mtimeMs: st.mtimeMs });
8698
10023
  } catch {
8699
10024
  }
@@ -8706,7 +10031,7 @@ async function selectUntriagedMonitors(rootDir) {
8706
10031
  const gitFreshSet = [...gitFresh].filter(untriaged).sort();
8707
10032
  let handoff = "";
8708
10033
  try {
8709
- handoff = await readFile21(path40.join(root, ".cursor", "HANDOFF.md"), "utf8");
10034
+ handoff = await readFile23(path41.join(root, ".cursor", "HANDOFF.md"), "utf8");
8710
10035
  } catch {
8711
10036
  handoff = "";
8712
10037
  }
@@ -8726,8 +10051,8 @@ async function selectUntriagedMonitors(rootDir) {
8726
10051
  const row = byName.get(name);
8727
10052
  if (!row) continue;
8728
10053
  entries.push({
8729
- path: path40.join(memoryDir, name),
8730
- relativePath: path40.relative(root, path40.join(memoryDir, name)).split(path40.sep).join("/"),
10054
+ path: path41.join(memoryDir, name),
10055
+ relativePath: path41.relative(root, path41.join(memoryDir, name)).split(path41.sep).join("/"),
8731
10056
  mtimeMs: row.mtimeMs,
8732
10057
  hasTriageHeading: false,
8733
10058
  hasOpenGaps: hasOpenGaps(row.content),
@@ -8773,7 +10098,7 @@ var monitorsCommand = defineCommand14({
8773
10098
  process.exitCode = 2;
8774
10099
  return;
8775
10100
  }
8776
- const result = await selectUntriagedMonitors(path41.resolve(args.cwd));
10101
+ const result = await selectUntriagedMonitors(path42.resolve(args.cwd));
8777
10102
  if (args.json) {
8778
10103
  console.log(JSON.stringify(result, null, 2));
8779
10104
  return;
@@ -8789,7 +10114,7 @@ var monitorsCommand = defineCommand14({
8789
10114
  });
8790
10115
 
8791
10116
  // src/commands/plan-index.ts
8792
- import path42 from "path";
10117
+ import path43 from "path";
8793
10118
  import { defineCommand as defineCommand15 } from "citty";
8794
10119
  var planIndexCommand = defineCommand15({
8795
10120
  meta: {
@@ -8808,7 +10133,7 @@ var planIndexCommand = defineCommand15({
8808
10133
  }
8809
10134
  },
8810
10135
  async run({ args }) {
8811
- const root = path42.resolve(args.cwd);
10136
+ const root = path43.resolve(args.cwd);
8812
10137
  const index = await writePlanIndex(root);
8813
10138
  if (args.json) {
8814
10139
  console.log(JSON.stringify(index, null, 2));
@@ -8824,90 +10149,760 @@ var planIndexCommand = defineCommand15({
8824
10149
  });
8825
10150
 
8826
10151
  // src/commands/run-plan.ts
8827
- import path48 from "path";
10152
+ import path50 from "path";
8828
10153
  import { defineCommand as defineCommand16 } from "citty";
8829
10154
 
8830
10155
  // src/plan-loop/backends.ts
8831
- import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
10156
+ import { execFileSync as execFileSync2, spawn as spawn7 } from "child_process";
8832
10157
  import { createWriteStream } from "fs";
8833
10158
  import { access as access9 } from "fs/promises";
8834
- import path43 from "path";
10159
+ import path44 from "path";
10160
+ import { StringDecoder as StringDecoder3 } from "string_decoder";
10161
+
10162
+ // src/plan-loop/hitl-relay.ts
10163
+ import { createInterface } from "readline";
8835
10164
  import { StringDecoder } from "string_decoder";
8836
- var CLAUDE_ENV_PASSTHROUGH = [
8837
- "ANTHROPIC_BASE_URL",
8838
- "ANTHROPIC_AUTH_TOKEN",
8839
- "ANTHROPIC_API_KEY"
10165
+ var HITL_GATE_LINE_RE = /^HITL_GATE:\s*([a-z0-9-]+)\s*\|\s*(.+)$/;
10166
+ var HITL_REPLY_PREFIX = "HITL_REPLY:";
10167
+ var RESERVED_GATE_IDS = ["git-prod", "kit-prod"];
10168
+ var FALLBACK_ASK_ID = "prose-fallback";
10169
+ var HITL_UNANSWERED_EXIT_CODE = 4;
10170
+ var HITL_SIGINT_EXIT_CODE = 130;
10171
+ var NUMBERED_LINE_RE = /^\s*(\d+)\.\s+(.+?)\s*$/;
10172
+ var FALLBACK_WORDING_RES = [
10173
+ /reply with (?:the |a )?number/i,
10174
+ /ask questions is not available/i,
10175
+ /skip or cancel means stop/i
8840
10176
  ];
8841
- function redactSecrets(text, secrets) {
8842
- let out = text;
8843
- const ordered = secrets.filter((s) => s.value.length > 0).sort((a, b) => b.value.length - a.value.length);
8844
- for (const s of ordered) {
8845
- out = out.split(s.value).join(`[${s.label}]`);
10177
+ var TICK_SENTINEL_RE = /LOOP_TICK_RESULT:/;
10178
+ function parseHitlGateLine(line) {
10179
+ const m = HITL_GATE_LINE_RE.exec(line.trim());
10180
+ if (!m?.[1] || !m[2]) return null;
10181
+ const labels = m[2].split("|").map((s) => s.trim()).filter((s) => s.length > 0);
10182
+ if (labels.length === 0) return null;
10183
+ return { askId: m[1], labels, detection: "sentinel" };
10184
+ }
10185
+ function findHitlGateInText(text) {
10186
+ let last = null;
10187
+ for (const line of text.split(/\r?\n/)) {
10188
+ const gate = parseHitlGateLine(line);
10189
+ if (gate) last = gate;
8846
10190
  }
8847
- return out;
10191
+ return last;
8848
10192
  }
8849
- var REDACT_HOLD_MAX_CHARS = 64 * 1024;
8850
- var RedactingStreamBuffer = class {
10193
+ function cleanFallbackLabel(raw) {
10194
+ let label = raw.trim();
10195
+ const bold3 = /^\*\*(.+?)\*\*/.exec(label);
10196
+ if (bold3?.[1]) label = bold3[1];
10197
+ else label = label.replace(/\*\*/g, "").replace(/^`(.+)`$/, "$1");
10198
+ label = label.split(/\s+[—–]\s+/)[0] ?? label;
10199
+ return label.replace(/[.:]+$/, "").trim();
10200
+ }
10201
+ function detectFallbackGate(text) {
10202
+ if (!FALLBACK_WORDING_RES.some((re) => re.test(text))) return null;
10203
+ const labels = [];
10204
+ let expected = 1;
10205
+ for (const line of text.split(/\r?\n/)) {
10206
+ const m = NUMBERED_LINE_RE.exec(line);
10207
+ if (!m?.[1] || !m[2]) continue;
10208
+ const n = Number(m[1]);
10209
+ if (n === 1) {
10210
+ labels.length = 0;
10211
+ expected = 1;
10212
+ }
10213
+ if (n !== expected) continue;
10214
+ labels.push(cleanFallbackLabel(m[2]));
10215
+ expected += 1;
10216
+ }
10217
+ if (labels.length < 2) return null;
10218
+ return { askId: FALLBACK_ASK_ID, labels, detection: "fallback" };
10219
+ }
10220
+ function detectHitlGate(resultText, assistantTexts) {
10221
+ if (TICK_SENTINEL_RE.test(resultText)) return null;
10222
+ const fromResult = findHitlGateInText(resultText);
10223
+ if (fromResult) return fromResult;
10224
+ for (let i = assistantTexts.length - 1; i >= 0; i -= 1) {
10225
+ const text = assistantTexts[i] ?? "";
10226
+ if (TICK_SENTINEL_RE.test(text)) return null;
10227
+ const gate = findHitlGateInText(text);
10228
+ if (gate) return gate;
10229
+ }
10230
+ const fallback = detectFallbackGate(resultText);
10231
+ if (fallback) return fallback;
10232
+ for (let i = assistantTexts.length - 1; i >= 0; i -= 1) {
10233
+ const gate = detectFallbackGate(assistantTexts[i] ?? "");
10234
+ if (gate) return gate;
10235
+ }
10236
+ return null;
10237
+ }
10238
+ function isReservedGateId(askId) {
10239
+ return RESERVED_GATE_IDS.includes(askId);
10240
+ }
10241
+ var STOP_WORDS = /* @__PURE__ */ new Set(["skip", "cancel"]);
10242
+ function resolveOperatorAnswer(input, labels) {
10243
+ const text = input.trim();
10244
+ if (!text) return { kind: "stop", cause: "empty" };
10245
+ const exact = labels.indexOf(text);
10246
+ if (exact !== -1) return { kind: "reply", n: exact + 1, label: labels[exact] ?? text };
10247
+ if (/^\d+$/.test(text)) {
10248
+ const n = Number(text);
10249
+ const label = labels[n - 1];
10250
+ if (n >= 1 && label !== void 0) return { kind: "reply", n, label };
10251
+ }
10252
+ const lower = text.toLowerCase();
10253
+ if (STOP_WORDS.has(lower)) return { kind: "stop", cause: lower };
10254
+ return { kind: "other", text };
10255
+ }
10256
+ function formatHitlReply(askId, answer) {
10257
+ if (answer.kind === "reply") {
10258
+ return `${HITL_REPLY_PREFIX} ${askId} | operator reply ${answer.n} | ${answer.label}`;
10259
+ }
10260
+ if (answer.kind === "other") {
10261
+ return `${HITL_REPLY_PREFIX} ${askId} | operator reply other | ${answer.text}`;
10262
+ }
10263
+ throw new Error("a stop answer has no reply stamp");
10264
+ }
10265
+ function createTerminalAnswerReader(opts = {}) {
10266
+ return (_gate, readOpts) => new Promise((resolve3) => {
10267
+ const input = opts.input ?? process.stdin;
10268
+ const output = opts.output ?? process.stdout;
10269
+ const terminal = opts.terminal ?? Boolean(input.isTTY);
10270
+ const rl = createInterface({ input, output, terminal });
10271
+ let settled = false;
10272
+ const finish = (answer) => {
10273
+ if (settled) return;
10274
+ settled = true;
10275
+ readOpts.signal?.removeEventListener("abort", onAbort);
10276
+ rl.close();
10277
+ if (typeof input.pause === "function") {
10278
+ input.pause();
10279
+ }
10280
+ resolve3(answer);
10281
+ };
10282
+ const onAbort = () => finish({ kind: "eof" });
10283
+ if (readOpts.signal?.aborted) {
10284
+ onAbort();
10285
+ return;
10286
+ }
10287
+ readOpts.signal?.addEventListener("abort", onAbort, { once: true });
10288
+ rl.on("SIGINT", () => finish({ kind: "sigint" }));
10289
+ rl.on("close", () => finish({ kind: "eof" }));
10290
+ rl.question("> ", (line) => finish({ kind: "line", text: line }));
10291
+ });
10292
+ }
10293
+ function emptyHitlRecord() {
10294
+ return { replies: [], fallbackDetections: 0 };
10295
+ }
10296
+ function operatorStop(askId, cause) {
10297
+ return {
10298
+ askId,
10299
+ cause,
10300
+ message: `stopped-by-operator: gate ${askId}, no reply (${cause})`,
10301
+ exitCode: cause === "SIGINT" ? HITL_SIGINT_EXIT_CODE : HITL_UNANSWERED_EXIT_CODE
10302
+ };
10303
+ }
10304
+ function operatorQuitStop(key) {
10305
+ return {
10306
+ askId: "none",
10307
+ cause: "quit",
10308
+ message: `stopped-by-operator: quit (${key}), no gate open`,
10309
+ exitCode: HITL_SIGINT_EXIT_CODE
10310
+ };
10311
+ }
10312
+ function unansweredStop(askId, cause) {
10313
+ return {
10314
+ askId,
10315
+ cause,
10316
+ message: `stopped: unanswered gate (${cause})`,
10317
+ exitCode: HITL_UNANSWERED_EXIT_CODE
10318
+ };
10319
+ }
10320
+ function noRelayStop(askId, backend) {
10321
+ return {
10322
+ askId,
10323
+ cause: "no relay",
10324
+ message: `stopped: unanswered gate (backend ${backend} has no relay)`,
10325
+ exitCode: HITL_UNANSWERED_EXIT_CODE
10326
+ };
10327
+ }
10328
+ function reservedStop(askId) {
10329
+ return {
10330
+ askId,
10331
+ cause: "reserved",
10332
+ message: `stopped: gate ${askId} is operator-gated; run the /${askId} slash yourself (never headless)`,
10333
+ exitCode: HITL_UNANSWERED_EXIT_CODE
10334
+ };
10335
+ }
10336
+ function formatGatePrompt(gate) {
10337
+ const head = gate.detection === "fallback" ? `HITL gate (prose fallback, ask-id ${gate.askId})` : `HITL gate ${gate.askId}`;
10338
+ const lines = [
10339
+ "",
10340
+ `${head}: reply with the number or the label; anything else is sent as "other"; empty, skip, cancel or Ctrl-C stops the run.`,
10341
+ ...gate.labels.map((label, i) => ` ${i + 1}. ${label}`)
10342
+ ];
10343
+ return `${lines.join("\n")}
10344
+ `;
10345
+ }
10346
+ async function askOperator(gate, opts) {
10347
+ if (isReservedGateId(gate.askId)) return { kind: "stop", stop: reservedStop(gate.askId) };
10348
+ if (opts.policy === "off") return { kind: "stop", stop: unansweredStop(gate.askId, "--no-hitl") };
10349
+ if (!opts.isTTY) return { kind: "stop", stop: unansweredStop(gate.askId, "no TTY") };
10350
+ opts.onPromptStart?.();
10351
+ let raw;
10352
+ try {
10353
+ opts.write(formatGatePrompt(gate));
10354
+ raw = await opts.readAnswer(gate, { signal: opts.signal });
10355
+ } finally {
10356
+ opts.onPromptEnd?.();
10357
+ }
10358
+ if (opts.signal?.aborted) {
10359
+ return { kind: "stop", stop: unansweredStop(gate.askId, "child exited") };
10360
+ }
10361
+ if (raw.kind === "eof") return { kind: "stop", stop: operatorStop(gate.askId, "EOF") };
10362
+ if (raw.kind === "sigint") return { kind: "stop", stop: operatorStop(gate.askId, "SIGINT") };
10363
+ const answer = resolveOperatorAnswer(raw.text, gate.labels);
10364
+ if (answer.kind === "stop") return { kind: "stop", stop: operatorStop(gate.askId, answer.cause) };
10365
+ const line = formatHitlReply(gate.askId, answer);
10366
+ const stamp3 = {
10367
+ askId: gate.askId,
10368
+ reply: answer.kind === "reply" ? `operator reply ${answer.n}` : "operator reply other",
10369
+ label: answer.kind === "reply" ? answer.label : answer.text,
10370
+ line,
10371
+ at: (/* @__PURE__ */ new Date()).toISOString(),
10372
+ detection: gate.detection
10373
+ };
10374
+ return { kind: "reply", stamp: stamp3 };
10375
+ }
10376
+ function formatHitlSummary(record) {
10377
+ if (!record) return [];
10378
+ const lines = record.replies.map(
10379
+ (s) => `HITL ${s.askId}: ${s.reply} (${s.label}) ${s.at}${s.detection === "fallback" ? " [fallback]" : ""}`
10380
+ );
10381
+ if (record.fallbackDetections > 0) {
10382
+ lines.push(`hitl-detect: prose-fallback x${record.fallbackDetections}`);
10383
+ }
10384
+ if (record.stop) lines.push(record.stop.message);
10385
+ return lines;
10386
+ }
10387
+ function hitlExitCode(childExitCode, record) {
10388
+ if (record?.stop) return record.stop.exitCode;
10389
+ return childExitCode;
10390
+ }
10391
+ function userEventLine(text) {
10392
+ return `${JSON.stringify({ type: "user", message: { role: "user", content: text } })}
10393
+ `;
10394
+ }
10395
+ var TurnWatcher = class {
10396
+ constructor(onResult) {
10397
+ this.onResult = onResult;
10398
+ }
10399
+ onResult;
8851
10400
  pending = "";
10401
+ assistantTexts = [];
8852
10402
  decoder = new StringDecoder("utf8");
8853
- secrets;
8854
- keep;
8855
- constructor(secrets) {
8856
- this.secrets = secrets.filter((s) => s.value.length > 0);
8857
- this.keep = this.secrets.length > 0 ? Math.max(...this.secrets.map((s) => s.value.length)) - 1 : 0;
8858
- }
8859
- /** Append a chunk; returns the redacted text that is safe to emit now. */
8860
- push(chunk) {
10403
+ feed(chunk) {
8861
10404
  const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
8862
- if (this.secrets.length === 0) return text;
8863
- this.pending += text;
8864
- let cut = this.pending.lastIndexOf("\n") + 1;
8865
- if (cut === 0) {
8866
- if (this.pending.length < REDACT_HOLD_MAX_CHARS) return "";
8867
- cut = this.pending.length - this.keep;
10405
+ if (!text) return;
10406
+ const combined = this.pending + text;
10407
+ const lines = combined.split("\n");
10408
+ this.pending = lines.pop() ?? "";
10409
+ for (const line of lines) this.take(line);
10410
+ }
10411
+ end() {
10412
+ const rest = this.pending + this.decoder.end();
10413
+ this.pending = "";
10414
+ if (rest.trim()) this.take(rest);
10415
+ }
10416
+ take(line) {
10417
+ const trimmed = line.trim();
10418
+ if (!trimmed.startsWith("{")) return;
10419
+ let event;
10420
+ try {
10421
+ event = JSON.parse(trimmed);
10422
+ } catch {
10423
+ return;
8868
10424
  }
8869
- if (cut <= 0) return "";
8870
- let moved = true;
8871
- while (moved) {
8872
- moved = false;
8873
- for (const s of this.secrets) {
8874
- let idx = this.pending.indexOf(s.value, Math.max(0, cut - s.value.length + 1));
8875
- while (idx !== -1 && idx < cut) {
8876
- if (idx + s.value.length > cut) {
8877
- cut = idx;
8878
- moved = true;
8879
- break;
10425
+ if (typeof event !== "object" || event === null) return;
10426
+ const ev = event;
10427
+ if (ev.type === "assistant") {
10428
+ const content = ev.message?.content;
10429
+ if (typeof content === "string") {
10430
+ if (content) this.assistantTexts.push(content);
10431
+ } else if (Array.isArray(content)) {
10432
+ for (const part of content) {
10433
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
10434
+ this.assistantTexts.push(part.text);
8880
10435
  }
8881
- idx = this.pending.indexOf(s.value, idx + 1);
8882
10436
  }
8883
- if (moved) break;
8884
10437
  }
10438
+ return;
10439
+ }
10440
+ if (ev.type === "result") {
10441
+ const texts = this.assistantTexts;
10442
+ this.assistantTexts = [];
10443
+ this.onResult({
10444
+ resultText: typeof ev.result === "string" ? ev.result : "",
10445
+ assistantTexts: texts,
10446
+ isError: ev.is_error === true,
10447
+ subtype: typeof ev.subtype === "string" ? ev.subtype : null
10448
+ });
8885
10449
  }
8886
- if (cut <= 0) return "";
8887
- const out = redactSecrets(this.pending.slice(0, cut), this.secrets);
8888
- this.pending = this.pending.slice(cut);
8889
- return out;
8890
- }
8891
- /** Emit whatever is still held back (call once, on stream end). */
8892
- flush() {
8893
- const rest = this.pending + this.decoder.end();
8894
- this.pending = "";
8895
- return this.secrets.length === 0 ? rest : redactSecrets(rest, this.secrets);
8896
10450
  }
8897
10451
  };
8898
- function mergeChildEnv(overrides) {
8899
- const merged = { ...process.env };
8900
- if (!overrides) return merged;
8901
- for (const [key, value] of Object.entries(overrides)) {
8902
- if (value === void 0) delete merged[key];
8903
- else merged[key] = value;
8904
- }
8905
- return merged;
10452
+
10453
+ // src/plan-loop/stream-render.ts
10454
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
10455
+ import { trueColor as trueColor2 } from "kolorist";
10456
+
10457
+ // src/plan-loop/stream-events.ts
10458
+ function isRecord(value) {
10459
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8906
10460
  }
8907
- var CLAUDE_NESTED_SESSION_KEYS = [
8908
- "CLAUDECODE",
8909
- "CLAUDE_CODE_SESSION_ID",
8910
- "CLAUDE_CODE_CHILD_SESSION",
10461
+ function asString2(value) {
10462
+ return typeof value === "string" ? value : null;
10463
+ }
10464
+ function asNumber(value) {
10465
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
10466
+ }
10467
+ function oneLine(text) {
10468
+ return text.replace(/\s+/g, " ").trim();
10469
+ }
10470
+ var SUMMARY_KEYS = [
10471
+ "command",
10472
+ "file_path",
10473
+ "path",
10474
+ "pattern",
10475
+ "query",
10476
+ "url",
10477
+ "description",
10478
+ "prompt",
10479
+ "content"
10480
+ ];
10481
+ function summarizeToolInput(input) {
10482
+ if (typeof input === "string") return oneLine(input);
10483
+ if (!isRecord(input)) return input === void 0 ? "" : oneLine(JSON.stringify(input));
10484
+ for (const key of SUMMARY_KEYS) {
10485
+ const value = input[key];
10486
+ if (typeof value === "string" && value.trim()) return oneLine(value);
10487
+ }
10488
+ const keys = Object.keys(input);
10489
+ if (keys.length === 0) return "";
10490
+ return oneLine(JSON.stringify(input));
10491
+ }
10492
+ function firstLineOfContent(content) {
10493
+ if (typeof content === "string") return firstNonEmptyLine(content);
10494
+ if (Array.isArray(content)) {
10495
+ for (const part of content) {
10496
+ if (isRecord(part) && typeof part.text === "string") {
10497
+ const line = firstNonEmptyLine(part.text);
10498
+ if (line) return line;
10499
+ }
10500
+ }
10501
+ return "";
10502
+ }
10503
+ if (isRecord(content)) return oneLine(JSON.stringify(content));
10504
+ return "";
10505
+ }
10506
+ function firstNonEmptyLine(text) {
10507
+ for (const line of text.split(/\r?\n/)) {
10508
+ const trimmed = line.trim();
10509
+ if (trimmed) return trimmed;
10510
+ }
10511
+ return "";
10512
+ }
10513
+ function parseAssistant(event) {
10514
+ const message = event.message;
10515
+ const content = isRecord(message) ? message.content : void 0;
10516
+ if (typeof content === "string") return content ? [{ kind: "text", text: content }] : [];
10517
+ if (!Array.isArray(content)) return [{ kind: "silent" }];
10518
+ const out = [];
10519
+ for (const part of content) {
10520
+ if (!isRecord(part)) continue;
10521
+ const type = asString2(part.type);
10522
+ if (type === "text" && typeof part.text === "string") {
10523
+ if (part.text) out.push({ kind: "text", text: part.text });
10524
+ } else if (type === "tool_use" || type === "server_tool_use") {
10525
+ out.push({
10526
+ kind: "tool_use",
10527
+ name: asString2(part.name) ?? "tool",
10528
+ summary: summarizeToolInput(part.input)
10529
+ });
10530
+ }
10531
+ }
10532
+ return out.length > 0 ? out : [{ kind: "silent" }];
10533
+ }
10534
+ var HITL_REPLY_PREFIX2 = "HITL_REPLY:";
10535
+ function parseUser(event) {
10536
+ const message = event.message;
10537
+ const content = isRecord(message) ? message.content : void 0;
10538
+ if (typeof content === "string") {
10539
+ if (event.isReplay === true && content.startsWith(HITL_REPLY_PREFIX2)) {
10540
+ return [{ kind: "hitl_reply", line: oneLine(content) }];
10541
+ }
10542
+ return [{ kind: "prompt", chars: content.length }];
10543
+ }
10544
+ if (!Array.isArray(content)) return [{ kind: "silent" }];
10545
+ const out = [];
10546
+ let promptChars = 0;
10547
+ for (const part of content) {
10548
+ if (!isRecord(part)) continue;
10549
+ const type = asString2(part.type);
10550
+ if (type === "tool_result") {
10551
+ out.push({
10552
+ kind: "tool_result",
10553
+ ok: part.is_error !== true,
10554
+ line: firstLineOfContent(part.content)
10555
+ });
10556
+ } else if (type === "text" && typeof part.text === "string") {
10557
+ if (event.isReplay === true && part.text.startsWith(HITL_REPLY_PREFIX2)) {
10558
+ out.push({ kind: "hitl_reply", line: oneLine(part.text) });
10559
+ continue;
10560
+ }
10561
+ promptChars += part.text.length;
10562
+ }
10563
+ }
10564
+ if (promptChars > 0) out.push({ kind: "prompt", chars: promptChars });
10565
+ return out.length > 0 ? out : [{ kind: "silent" }];
10566
+ }
10567
+ function parseResult(event) {
10568
+ const isError = event.is_error === true;
10569
+ const subtype = asString2(event.subtype);
10570
+ return {
10571
+ kind: "result",
10572
+ ok: !isError,
10573
+ status: subtype ?? (isError ? "error" : "success"),
10574
+ costUsd: asNumber(event.total_cost_usd),
10575
+ durationMs: asNumber(event.duration_ms),
10576
+ apiMs: asNumber(event.duration_api_ms),
10577
+ turns: asNumber(event.num_turns)
10578
+ };
10579
+ }
10580
+ function parseSystem(event) {
10581
+ const subtype = asString2(event.subtype) ?? "";
10582
+ if (!subtype.startsWith("hook_")) return { kind: "silent" };
10583
+ const phase = subtype.slice("hook_".length);
10584
+ const name = asString2(event.hook_name) ?? asString2(event.hook_event) ?? "hook";
10585
+ const details = [];
10586
+ const outcome = asString2(event.outcome);
10587
+ if (outcome) details.push(outcome);
10588
+ const exitCode = asNumber(event.exit_code);
10589
+ if (exitCode !== void 0) details.push(`exit ${exitCode}`);
10590
+ return { kind: "hook", name, phase, detail: details.join(", ") };
10591
+ }
10592
+ function percent(value) {
10593
+ const n = asNumber(value);
10594
+ return n === void 0 ? null : `${Math.round(n * 100)}%`;
10595
+ }
10596
+ function parseRateLimit(event) {
10597
+ const info = isRecord(event.rate_limit_info) ? event.rate_limit_info : event;
10598
+ const status = asString2(info.status) ?? "unknown";
10599
+ const type = asString2(info.rateLimitType);
10600
+ const parts = [];
10601
+ if (type) parts.push(type);
10602
+ parts.push(status);
10603
+ const windows = isRecord(info.unifiedWindows) ? info.unifiedWindows : null;
10604
+ if (windows) {
10605
+ for (const [name, window] of Object.entries(windows)) {
10606
+ if (!isRecord(window)) continue;
10607
+ const used = percent(window.utilization);
10608
+ if (used) parts.push(`${name} ${used}`);
10609
+ }
10610
+ }
10611
+ return { kind: "rate_limit", line: parts.join(" \xB7 "), severe: status !== "allowed" };
10612
+ }
10613
+ function parseCursorToolCall(event) {
10614
+ const call = isRecord(event.tool_call) ? event.tool_call : null;
10615
+ const entry = call ? Object.entries(call).find(([, v]) => isRecord(v)) : void 0;
10616
+ const name = entry ? entry[0].replace(/ToolCall$/, "") : "tool";
10617
+ const body = entry && isRecord(entry[1]) ? entry[1] : {};
10618
+ const subtype = asString2(event.subtype) ?? "";
10619
+ if (subtype === "started") {
10620
+ return { kind: "tool_use", name, summary: summarizeToolInput(body.args) };
10621
+ }
10622
+ if (subtype === "completed") {
10623
+ const result = isRecord(body.result) ? body.result : null;
10624
+ const failed = result !== null && ("error" in result || "failure" in result || "rejected" in result);
10625
+ let line = "";
10626
+ if (result) {
10627
+ const payload = failed ? result.error ?? result.failure ?? result.rejected : result.success;
10628
+ line = firstLineOfContent(
10629
+ typeof payload === "string" ? payload : payload === void 0 ? result : payload
10630
+ );
10631
+ }
10632
+ return { kind: "tool_result", ok: !failed, line };
10633
+ }
10634
+ return { kind: "silent" };
10635
+ }
10636
+ function parseStreamLine(line) {
10637
+ const trimmed = line.trim();
10638
+ if (!trimmed || !trimmed.startsWith("{")) return [{ kind: "unparsed" }];
10639
+ let event;
10640
+ try {
10641
+ event = JSON.parse(trimmed);
10642
+ } catch {
10643
+ return [{ kind: "unparsed" }];
10644
+ }
10645
+ if (!isRecord(event)) return [{ kind: "unparsed" }];
10646
+ switch (asString2(event.type)) {
10647
+ case "assistant":
10648
+ return parseAssistant(event);
10649
+ case "user":
10650
+ return parseUser(event);
10651
+ case "result":
10652
+ return [parseResult(event)];
10653
+ case "system":
10654
+ return [parseSystem(event)];
10655
+ case "rate_limit_event":
10656
+ return [parseRateLimit(event)];
10657
+ case "tool_call":
10658
+ return [parseCursorToolCall(event)];
10659
+ default:
10660
+ return [{ kind: "silent" }];
10661
+ }
10662
+ }
10663
+
10664
+ // src/plan-loop/stream-render.ts
10665
+ var PENDING_MAX_CHARS = 4 * 1024 * 1024;
10666
+ var MIN_COLUMNS2 = 40;
10667
+ function paint2(hex, text, color) {
10668
+ if (!color) return text;
10669
+ return withKoloristColor(
10670
+ () => trueColor2(
10671
+ Number.parseInt(hex.slice(1, 3), 16),
10672
+ Number.parseInt(hex.slice(3, 5), 16),
10673
+ Number.parseInt(hex.slice(5, 7), 16)
10674
+ )(text)
10675
+ );
10676
+ }
10677
+ function truncate(text, width) {
10678
+ if (text.length <= width) return text;
10679
+ return `${text.slice(0, Math.max(1, width - 1))}\u2026`;
10680
+ }
10681
+ function formatDuration(ms) {
10682
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
10683
+ const seconds = ms / 1e3;
10684
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
10685
+ const minutes = Math.floor(seconds / 60);
10686
+ const rest = Math.round(seconds - minutes * 60);
10687
+ return `${minutes}m ${rest}s`;
10688
+ }
10689
+ function formatCost(usd) {
10690
+ return `$${usd.toFixed(usd < 0.1 ? 4 : 2)}`;
10691
+ }
10692
+ function renderStreamEvent(event, opts) {
10693
+ const width = Math.max(MIN_COLUMNS2, opts.columns);
10694
+ const color = opts.color;
10695
+ switch (event.kind) {
10696
+ case "text":
10697
+ return event.text;
10698
+ case "tool_use": {
10699
+ const head = `${SPACE_MARKS.diamond} ${event.name}`;
10700
+ const summary = truncate(event.summary, width - head.length - 3);
10701
+ return paint2(HELMET_ACCENT, summary ? `${head} ${SPACE_MARKS.tick} ${summary}` : head, color);
10702
+ }
10703
+ case "tool_result": {
10704
+ const label = event.ok ? "ok" : "error";
10705
+ const head = ` ${SPACE_MARKS.tick} ${label}`;
10706
+ const line = truncate(oneLine(event.line), width - head.length - 3);
10707
+ const text = line ? `${head} ${SPACE_MARKS.tick} ${line}` : head;
10708
+ return paint2(event.ok ? LABEL_MUTED : STATUS_ERR, text, color);
10709
+ }
10710
+ case "result": {
10711
+ const parts = [`${SPACE_MARKS.star} result ${event.status}`];
10712
+ if (event.costUsd !== void 0) parts.push(formatCost(event.costUsd));
10713
+ if (event.durationMs !== void 0) parts.push(formatDuration(event.durationMs));
10714
+ else if (event.apiMs !== void 0) parts.push(`${formatDuration(event.apiMs)} api`);
10715
+ if (event.turns !== void 0) {
10716
+ parts.push(`${event.turns} turn${event.turns === 1 ? "" : "s"}`);
10717
+ }
10718
+ return paint2(event.ok ? STATUS_OK : STATUS_ERR, parts.join(` ${SPACE_MARKS.tick} `), color);
10719
+ }
10720
+ case "hook": {
10721
+ const detail = event.detail ? ` (${event.detail})` : "";
10722
+ return paint2(
10723
+ LABEL_MUTED,
10724
+ truncate(`${SPACE_MARKS.tick} hook ${event.name} ${event.phase}${detail}`, width),
10725
+ color
10726
+ );
10727
+ }
10728
+ case "rate_limit":
10729
+ return paint2(
10730
+ event.severe ? STATUS_ERR : STATUS_WARN,
10731
+ truncate(`! rate limit ${SPACE_MARKS.tick} ${event.line}`, width),
10732
+ color
10733
+ );
10734
+ case "prompt":
10735
+ return paint2(
10736
+ LABEL_MUTED,
10737
+ `${SPACE_MARKS.tick} prompt sent ${SPACE_MARKS.tick} ${event.chars} chars`,
10738
+ color
10739
+ );
10740
+ case "hitl_reply":
10741
+ return paint2(HELMET_ACCENT, truncate(`\u21A9 ${event.line}`, width), color);
10742
+ default:
10743
+ return null;
10744
+ }
10745
+ }
10746
+ var StreamRenderer = class {
10747
+ pending = "";
10748
+ dropped = false;
10749
+ textOpen = false;
10750
+ decoder = new StringDecoder2("utf8");
10751
+ write;
10752
+ color;
10753
+ columns;
10754
+ constructor(opts = {}) {
10755
+ this.write = opts.write ?? ((text) => process.stdout.write(text));
10756
+ this.color = shouldUseWelcomeColor({
10757
+ stdoutIsTTY: opts.isTTY ?? Boolean(process.stdout.isTTY),
10758
+ env: opts.env
10759
+ });
10760
+ this.columns = opts.columns ?? process.stdout.columns ?? 80;
10761
+ }
10762
+ feed(chunk) {
10763
+ try {
10764
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
10765
+ if (!text) return;
10766
+ const combined = this.pending + text;
10767
+ const lines = combined.split("\n");
10768
+ this.pending = lines.pop() ?? "";
10769
+ for (const line of lines) {
10770
+ if (this.dropped) {
10771
+ this.dropped = false;
10772
+ continue;
10773
+ }
10774
+ this.renderLine(line);
10775
+ }
10776
+ if (this.pending.length > PENDING_MAX_CHARS) {
10777
+ this.pending = "";
10778
+ this.dropped = true;
10779
+ }
10780
+ } catch {
10781
+ }
10782
+ }
10783
+ end() {
10784
+ try {
10785
+ const rest = this.pending + this.decoder.end();
10786
+ this.pending = "";
10787
+ if (rest && !this.dropped) this.renderLine(rest);
10788
+ this.dropped = false;
10789
+ this.closeText();
10790
+ } catch {
10791
+ }
10792
+ }
10793
+ renderLine(line) {
10794
+ for (const event of parseStreamLine(line)) {
10795
+ const rendered = renderStreamEvent(event, { color: this.color, columns: this.columns });
10796
+ if (rendered === null) continue;
10797
+ if (event.kind === "text") {
10798
+ this.write(rendered);
10799
+ this.textOpen = !rendered.endsWith("\n");
10800
+ continue;
10801
+ }
10802
+ this.closeText();
10803
+ this.write(`${rendered}
10804
+ `);
10805
+ }
10806
+ }
10807
+ closeText() {
10808
+ if (!this.textOpen) return;
10809
+ this.textOpen = false;
10810
+ this.write("\n");
10811
+ }
10812
+ };
10813
+ function createStreamRenderer(opts = {}) {
10814
+ return new StreamRenderer(opts);
10815
+ }
10816
+
10817
+ // src/plan-loop/backends.ts
10818
+ var HITL_KILL_GRACE_MS = 5e3;
10819
+ function resolveHitlRunOptions(hitl, transport) {
10820
+ return {
10821
+ transport,
10822
+ policy: hitl?.policy ?? "prompt",
10823
+ isTTY: hitl?.isTTY ?? Boolean(process.stdin.isTTY),
10824
+ readAnswer: hitl?.readAnswer ?? createTerminalAnswerReader(),
10825
+ write: hitl?.write ?? ((text) => process.stdout.write(text)),
10826
+ onPromptStart: hitl?.onPromptStart,
10827
+ onPromptEnd: hitl?.onPromptEnd,
10828
+ killAfterMs: hitl?.killAfterMs
10829
+ };
10830
+ }
10831
+ var CLAUDE_ENV_PASSTHROUGH = [
10832
+ "ANTHROPIC_BASE_URL",
10833
+ "ANTHROPIC_AUTH_TOKEN",
10834
+ "ANTHROPIC_API_KEY"
10835
+ ];
10836
+ function redactSecrets(text, secrets) {
10837
+ let out = text;
10838
+ const ordered = secrets.filter((s) => s.value.length > 0).sort((a, b) => b.value.length - a.value.length);
10839
+ for (const s of ordered) {
10840
+ out = out.split(s.value).join(`[${s.label}]`);
10841
+ }
10842
+ return out;
10843
+ }
10844
+ var REDACT_HOLD_MAX_CHARS = 64 * 1024;
10845
+ var RedactingStreamBuffer = class {
10846
+ pending = "";
10847
+ decoder = new StringDecoder3("utf8");
10848
+ secrets;
10849
+ keep;
10850
+ constructor(secrets) {
10851
+ this.secrets = secrets.filter((s) => s.value.length > 0);
10852
+ this.keep = this.secrets.length > 0 ? Math.max(...this.secrets.map((s) => s.value.length)) - 1 : 0;
10853
+ }
10854
+ /** Append a chunk; returns the redacted text that is safe to emit now. */
10855
+ push(chunk) {
10856
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
10857
+ if (this.secrets.length === 0) return text;
10858
+ this.pending += text;
10859
+ let cut = this.pending.lastIndexOf("\n") + 1;
10860
+ if (cut === 0) {
10861
+ if (this.pending.length < REDACT_HOLD_MAX_CHARS) return "";
10862
+ cut = this.pending.length - this.keep;
10863
+ }
10864
+ if (cut <= 0) return "";
10865
+ let moved = true;
10866
+ while (moved) {
10867
+ moved = false;
10868
+ for (const s of this.secrets) {
10869
+ let idx = this.pending.indexOf(s.value, Math.max(0, cut - s.value.length + 1));
10870
+ while (idx !== -1 && idx < cut) {
10871
+ if (idx + s.value.length > cut) {
10872
+ cut = idx;
10873
+ moved = true;
10874
+ break;
10875
+ }
10876
+ idx = this.pending.indexOf(s.value, idx + 1);
10877
+ }
10878
+ if (moved) break;
10879
+ }
10880
+ }
10881
+ if (cut <= 0) return "";
10882
+ const out = redactSecrets(this.pending.slice(0, cut), this.secrets);
10883
+ this.pending = this.pending.slice(cut);
10884
+ return out;
10885
+ }
10886
+ /** Emit whatever is still held back (call once, on stream end). */
10887
+ flush() {
10888
+ const rest = this.pending + this.decoder.end();
10889
+ this.pending = "";
10890
+ return this.secrets.length === 0 ? rest : redactSecrets(rest, this.secrets);
10891
+ }
10892
+ };
10893
+ function mergeChildEnv(overrides) {
10894
+ const merged = { ...process.env };
10895
+ if (!overrides) return merged;
10896
+ for (const [key, value] of Object.entries(overrides)) {
10897
+ if (value === void 0) delete merged[key];
10898
+ else merged[key] = value;
10899
+ }
10900
+ return merged;
10901
+ }
10902
+ var CLAUDE_NESTED_SESSION_KEYS = [
10903
+ "CLAUDECODE",
10904
+ "CLAUDE_CODE_SESSION_ID",
10905
+ "CLAUDE_CODE_CHILD_SESSION",
8911
10906
  "CLAUDE_CODE_BRIDGE_SESSION_ID"
8912
10907
  ];
8913
10908
  function claudeChildEnv(overrides) {
@@ -8987,9 +10982,11 @@ async function whichBinary(bin) {
8987
10982
  }
8988
10983
  }
8989
10984
  function spawnLogged(command, args, logPath, options = {}) {
8990
- const spawnFn = options.spawnFn ?? spawn6;
10985
+ const spawnFn = options.spawnFn ?? spawn7;
8991
10986
  const redact = options.redact ?? [];
8992
10987
  const redactedError = (err) => new Error(redactSecrets(String(err), redact));
10988
+ const hitl = options.hitl;
10989
+ const stdinTransport = hitl?.transport.kind === "stdin-stream-json";
8993
10990
  return new Promise((resolve3, reject) => {
8994
10991
  let out;
8995
10992
  let child;
@@ -9001,7 +10998,7 @@ function spawnLogged(command, args, logPath, options = {}) {
9001
10998
  reject(redactedError(err));
9002
10999
  });
9003
11000
  child = spawnFn(command, args, {
9004
- stdio: ["ignore", "pipe", "pipe"],
11001
+ stdio: [stdinTransport ? "pipe" : "ignore", "pipe", "pipe"],
9005
11002
  ...options.cwd ? { cwd: options.cwd } : {},
9006
11003
  ...options.env ? { env: options.env } : {}
9007
11004
  });
@@ -9009,11 +11006,33 @@ function spawnLogged(command, args, logPath, options = {}) {
9009
11006
  reject(redactedError(err));
9010
11007
  return;
9011
11008
  }
11009
+ const render = options.render ?? createStreamRenderer();
11010
+ const relay = hitl ? createRelay(hitl, child, redactedError) : null;
9012
11011
  const emit = (text) => {
9013
11012
  if (text.length === 0) return;
9014
- process.stdout.write(text);
9015
11013
  out.write(text);
11014
+ render.feed(text);
11015
+ relay?.feed(text);
9016
11016
  };
11017
+ if (relay?.startError) {
11018
+ out.end();
11019
+ reject(relay.startError);
11020
+ return;
11021
+ }
11022
+ options.onSpawn?.({
11023
+ backend: options.backendId ?? "claude",
11024
+ pid: child.pid,
11025
+ stop: (stop) => {
11026
+ if (relay) {
11027
+ relay.stop(stop);
11028
+ return;
11029
+ }
11030
+ try {
11031
+ child.kill("SIGTERM");
11032
+ } catch {
11033
+ }
11034
+ }
11035
+ });
9017
11036
  const buffers = [];
9018
11037
  const tee = (stream) => {
9019
11038
  if (!stream) return;
@@ -9033,14 +11052,138 @@ function spawnLogged(command, args, logPath, options = {}) {
9033
11052
  });
9034
11053
  child.on("close", (code) => {
9035
11054
  for (const buffer of buffers) emit(buffer.flush());
9036
- out.end((err) => {
9037
- const failure = err ?? logError;
9038
- if (failure) reject(redactedError(failure));
9039
- else resolve3({ exitCode: code ?? 1 });
9040
- });
11055
+ render.end();
11056
+ const finish = () => {
11057
+ out.end((err) => {
11058
+ const failure = err ?? logError;
11059
+ if (failure) reject(redactedError(failure));
11060
+ else if (relay) resolve3({ exitCode: code ?? 1, hitl: relay.record });
11061
+ else resolve3({ exitCode: code ?? 1 });
11062
+ });
11063
+ };
11064
+ if (relay) relay.closed().then(finish, finish);
11065
+ else finish();
9041
11066
  });
9042
11067
  });
9043
11068
  }
11069
+ function createRelay(hitl, child, redactedError) {
11070
+ const record = emptyHitlRecord();
11071
+ const stdinTransport = hitl.transport.kind === "stdin-stream-json";
11072
+ const abort = new AbortController();
11073
+ const grace = hitl.killAfterMs ?? HITL_KILL_GRACE_MS;
11074
+ let startError = null;
11075
+ let closed = false;
11076
+ let stdinEnded = false;
11077
+ let pendingAsk = null;
11078
+ const timers = [];
11079
+ let resolveClosed = () => {
11080
+ };
11081
+ const closedPromise = new Promise((resolve3) => {
11082
+ resolveClosed = resolve3;
11083
+ });
11084
+ const stdin = child.stdin;
11085
+ if (hitl.transport.kind === "stdin-stream-json") {
11086
+ if (!stdin) {
11087
+ startError = new Error("claude child has no writable stdin (stream-json transport)");
11088
+ } else {
11089
+ stdin.on("error", () => {
11090
+ });
11091
+ try {
11092
+ stdin.write(userEventLine(hitl.transport.firstPrompt));
11093
+ } catch (err) {
11094
+ startError = redactedError(err);
11095
+ }
11096
+ }
11097
+ }
11098
+ const endStdin = () => {
11099
+ if (!stdinTransport || stdinEnded) return;
11100
+ stdinEnded = true;
11101
+ try {
11102
+ stdin?.end();
11103
+ } catch {
11104
+ }
11105
+ };
11106
+ const stopChild = () => {
11107
+ endStdin();
11108
+ const term = setTimeout(() => {
11109
+ if (closed) return;
11110
+ child.kill("SIGTERM");
11111
+ const kill = setTimeout(() => {
11112
+ if (!closed) child.kill("SIGKILL");
11113
+ }, grace);
11114
+ kill.unref?.();
11115
+ timers.push(kill);
11116
+ }, grace);
11117
+ term.unref?.();
11118
+ timers.push(term);
11119
+ };
11120
+ const watcher = new TurnWatcher(({ resultText, assistantTexts }) => {
11121
+ if (record.stop || closed) return;
11122
+ const gate = detectHitlGate(resultText, assistantTexts);
11123
+ if (!gate) {
11124
+ endStdin();
11125
+ return;
11126
+ }
11127
+ if (gate.detection === "fallback") record.fallbackDetections += 1;
11128
+ if (hitl.transport.kind === "none") {
11129
+ record.stop = noRelayStop(gate.askId, hitl.transport.backend);
11130
+ return;
11131
+ }
11132
+ const ask = askOperator(gate, {
11133
+ isTTY: hitl.isTTY,
11134
+ policy: hitl.policy,
11135
+ readAnswer: hitl.readAnswer,
11136
+ write: hitl.write,
11137
+ signal: abort.signal,
11138
+ onPromptStart: hitl.onPromptStart,
11139
+ onPromptEnd: hitl.onPromptEnd
11140
+ }).then(
11141
+ (outcome) => {
11142
+ if (record.stop) return;
11143
+ if (outcome.kind === "stop") {
11144
+ record.stop = outcome.stop;
11145
+ stopChild();
11146
+ return;
11147
+ }
11148
+ if (closed || !stdin) {
11149
+ record.stop = unansweredStop(gate.askId, "child exited");
11150
+ return;
11151
+ }
11152
+ record.replies.push(outcome.stamp);
11153
+ hitl.write(`\u2192 ${outcome.stamp.line}
11154
+ `);
11155
+ stdin.write(userEventLine(outcome.stamp.line));
11156
+ },
11157
+ (err) => {
11158
+ record.stop = unansweredStop(gate.askId, "child exited");
11159
+ hitl.write(`hitl relay: ${redactedError(err).message}
11160
+ `);
11161
+ stopChild();
11162
+ }
11163
+ );
11164
+ pendingAsk = ask;
11165
+ });
11166
+ child.on("close", () => {
11167
+ closed = true;
11168
+ for (const t of timers) clearTimeout(t);
11169
+ abort.abort();
11170
+ watcher.end();
11171
+ const settle = () => resolveClosed();
11172
+ if (pendingAsk) pendingAsk.then(settle, settle);
11173
+ else settle();
11174
+ });
11175
+ return {
11176
+ startError,
11177
+ record,
11178
+ feed: (text) => watcher.feed(text),
11179
+ closed: () => closedPromise,
11180
+ stop: (stop) => {
11181
+ if (closed || record.stop) return;
11182
+ record.stop = stop;
11183
+ stopChild();
11184
+ }
11185
+ };
11186
+ }
9044
11187
  var cursorAgentBackend = {
9045
11188
  id: "cursor-agent",
9046
11189
  async resolve() {
@@ -9065,7 +11208,11 @@ var cursorAgentBackend = {
9065
11208
  return spawnLogged("cursor-agent", args, opts.logPath, {
9066
11209
  spawnFn: opts.spawnFn,
9067
11210
  env,
9068
- redact: claudeRedactions(env ?? process.env)
11211
+ redact: claudeRedactions(env ?? process.env),
11212
+ hitl: resolveHitlRunOptions(opts.hitl, { kind: "none", backend: "cursor-agent" }),
11213
+ render: opts.render,
11214
+ onSpawn: opts.onSpawn,
11215
+ backendId: "cursor-agent"
9069
11216
  });
9070
11217
  }
9071
11218
  };
@@ -9116,7 +11263,7 @@ function checkClaudeVersion(bin, versionFn = defaultVersionFn) {
9116
11263
  }
9117
11264
  return { ok: true, version };
9118
11265
  }
9119
- function claudeHeadlessArgs(opts) {
11266
+ function claudeHeadlessArgs(opts = {}) {
9120
11267
  const args = [
9121
11268
  "-p",
9122
11269
  "--output-format",
@@ -9124,7 +11271,10 @@ function claudeHeadlessArgs(opts) {
9124
11271
  "--verbose",
9125
11272
  "--dangerously-skip-permissions",
9126
11273
  "--permission-prompts",
9127
- "none"
11274
+ "none",
11275
+ "--input-format",
11276
+ "stream-json",
11277
+ "--replay-user-messages"
9128
11278
  ];
9129
11279
  if (opts.maxTurns !== void 0) {
9130
11280
  args.push("--max-turns", String(opts.maxTurns));
@@ -9135,15 +11285,14 @@ function claudeHeadlessArgs(opts) {
9135
11285
  if (opts.model) {
9136
11286
  args.push("--model", opts.model);
9137
11287
  }
9138
- args.push(opts.prompt);
9139
11288
  return args;
9140
11289
  }
9141
11290
  async function missingClaudeAdapter(workspace, prompt) {
9142
11291
  const m = /^\/([A-Za-z0-9][A-Za-z0-9_-]*)/.exec(prompt.trimStart());
9143
11292
  if (!m?.[1]) return null;
9144
- const rel = path43.join(".claude", "commands", `${m[1]}.md`);
11293
+ const rel = path44.join(".claude", "commands", `${m[1]}.md`);
9145
11294
  try {
9146
- await access9(path43.join(workspace, rel));
11295
+ await access9(path44.join(workspace, rel));
9147
11296
  return null;
9148
11297
  } catch {
9149
11298
  return rel;
@@ -9180,15 +11329,22 @@ var claudeBackend = {
9180
11329
  (key) => `${key}=${env[key] ? "set" : "unset"}`
9181
11330
  ).join(" ");
9182
11331
  log(
9183
- `claude tick: claude -p (bypass permissions, stream-json${capText ? `, ${capText}` : ""}). env: ${presence}`
11332
+ `claude tick: claude -p (bypass permissions, stream-json, stdin relay${capText ? `, ${capText}` : ""}). env: ${presence}`
9184
11333
  );
9185
- const args = claudeHeadlessArgs({ prompt: opts.prompt, model: opts.model, ...caps });
11334
+ const args = claudeHeadlessArgs({ model: opts.model, ...caps });
9186
11335
  try {
9187
11336
  return await spawnLogged("claude", args, opts.logPath, {
9188
11337
  cwd: opts.workspace,
9189
11338
  env,
9190
11339
  spawnFn: opts.spawnFn,
9191
- redact
11340
+ redact,
11341
+ hitl: resolveHitlRunOptions(opts.hitl, {
11342
+ kind: "stdin-stream-json",
11343
+ firstPrompt: opts.prompt
11344
+ }),
11345
+ render: opts.render,
11346
+ onSpawn: opts.onSpawn,
11347
+ backendId: "claude"
9192
11348
  });
9193
11349
  } catch (err) {
9194
11350
  throw new Error(`claude backend failed to start: ${redactSecrets(String(err), redact)}`);
@@ -9258,14 +11414,464 @@ async function detectAgentBackend(requested, whichFn = whichBinary) {
9258
11414
  }
9259
11415
 
9260
11416
  // src/plan-loop/run-loop.ts
9261
- import { mkdir as mkdir6, readFile as readFile24, rm as rm2, unlink as unlink2 } from "fs/promises";
9262
- import path47 from "path";
11417
+ import { mkdir as mkdir6, readFile as readFile27, rm as rm2, unlink as unlink2 } from "fs/promises";
11418
+ import path49 from "path";
11419
+
11420
+ // src/mission-control/live-run.ts
11421
+ import { readFile as readFile25 } from "fs/promises";
11422
+ import path46 from "path";
11423
+
11424
+ // src/plan-loop/plan-state.ts
11425
+ import { readFile as readFile24, readdir as readdir11 } from "fs/promises";
11426
+ import path45 from "path";
11427
+ function countPendingTodos(raw) {
11428
+ const lines = raw.split(/\r?\n/);
11429
+ let inFront = 0;
11430
+ let cont = false;
11431
+ let n = 0;
11432
+ for (const line of lines) {
11433
+ if (line === "---") {
11434
+ inFront += 1;
11435
+ continue;
11436
+ }
11437
+ if (inFront !== 1) continue;
11438
+ const idMatch = line.match(/^- id:\s*(\S+)/);
11439
+ if (idMatch?.[1]) {
11440
+ cont = idMatch[1] === "dogfood-poc";
11441
+ }
11442
+ if (/content:.*[Cc]ont[ií]nuo/.test(line)) {
11443
+ cont = true;
11444
+ }
11445
+ if (/status:\s*(pending|in_progress)\b/.test(line) && !cont) {
11446
+ n += 1;
11447
+ }
11448
+ }
11449
+ return n;
11450
+ }
11451
+ async function findActivePlanFile(plansDir) {
11452
+ if (!await fileExists(plansDir)) return null;
11453
+ const files = (await readdir11(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
11454
+ return files[0] ? path45.join(plansDir, files[0]) : null;
11455
+ }
11456
+ async function readPlan(planPath) {
11457
+ return readFile24(planPath, "utf8");
11458
+ }
11459
+
11460
+ // src/mission-control/live-feed.ts
11461
+ import { StringDecoder as StringDecoder4 } from "string_decoder";
11462
+ var LIVE_LOG_CAPACITY = 500;
11463
+ var PENDING_MAX_CHARS2 = 4 * 1024 * 1024;
11464
+ var LiveRunFeed = class {
11465
+ log;
11466
+ crew;
11467
+ gate = null;
11468
+ hitlReplies = 0;
11469
+ pending = "";
11470
+ dropped = false;
11471
+ decoder = new StringDecoder4("utf8");
11472
+ columns;
11473
+ version = 0;
11474
+ constructor(opts = {}) {
11475
+ this.columns = Math.max(40, opts.columns ?? 80);
11476
+ this.log = new ScrollRegion(opts.capacity ?? LIVE_LOG_CAPACITY);
11477
+ this.crew = {
11478
+ backend: opts.backend ?? null,
11479
+ pid: void 0,
11480
+ phase: "starting",
11481
+ startedAt: (opts.now ?? Date.now)(),
11482
+ toolCalls: 0,
11483
+ currentTool: null,
11484
+ lastResult: null,
11485
+ rateLimit: null,
11486
+ logPath: opts.logPath ?? null
11487
+ };
11488
+ }
11489
+ /** Bumps on every change; a paint loop can skip a frame when nothing moved. */
11490
+ get revision() {
11491
+ return this.version;
11492
+ }
11493
+ setProcess(info) {
11494
+ this.crew.backend = info.backend;
11495
+ this.crew.pid = info.pid;
11496
+ if (this.crew.phase === "starting") this.crew.phase = "running";
11497
+ this.version += 1;
11498
+ }
11499
+ setPhase(phase) {
11500
+ this.crew.phase = phase;
11501
+ this.version += 1;
11502
+ }
11503
+ openGate(gate) {
11504
+ this.gate = gate;
11505
+ this.crew.phase = "gate";
11506
+ this.version += 1;
11507
+ }
11508
+ closeGate() {
11509
+ this.gate = null;
11510
+ if (this.crew.phase === "gate") this.crew.phase = "running";
11511
+ this.version += 1;
11512
+ }
11513
+ /** Append a status line the run lane produced itself (never from the stream). */
11514
+ note(text, kind = "note") {
11515
+ for (const line of text.split(/\r?\n/)) {
11516
+ const trimmed = line.trimEnd();
11517
+ if (trimmed) this.log.push({ kind, text: trimmed });
11518
+ }
11519
+ this.version += 1;
11520
+ }
11521
+ feed(chunk) {
11522
+ try {
11523
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
11524
+ if (!text) return;
11525
+ const combined = this.pending + text;
11526
+ const lines = combined.split("\n");
11527
+ this.pending = lines.pop() ?? "";
11528
+ for (const line of lines) {
11529
+ if (this.dropped) {
11530
+ this.dropped = false;
11531
+ continue;
11532
+ }
11533
+ this.takeLine(line);
11534
+ }
11535
+ if (this.pending.length > PENDING_MAX_CHARS2) {
11536
+ this.pending = "";
11537
+ this.dropped = true;
11538
+ }
11539
+ } catch {
11540
+ }
11541
+ }
11542
+ end() {
11543
+ try {
11544
+ const rest = this.pending + this.decoder.end();
11545
+ this.pending = "";
11546
+ if (rest && !this.dropped) this.takeLine(rest);
11547
+ this.dropped = false;
11548
+ this.crew.currentTool = null;
11549
+ if (this.crew.phase !== "done") this.crew.phase = "done";
11550
+ this.version += 1;
11551
+ } catch {
11552
+ }
11553
+ }
11554
+ takeLine(line) {
11555
+ for (const event of parseStreamLine(line)) {
11556
+ const rendered = renderStreamEvent(event, { color: false, columns: this.columns });
11557
+ if (rendered === null) continue;
11558
+ switch (event.kind) {
11559
+ case "text":
11560
+ this.note(rendered, "text");
11561
+ break;
11562
+ case "tool_use":
11563
+ this.crew.toolCalls += 1;
11564
+ this.crew.currentTool = { name: event.name, summary: event.summary };
11565
+ this.note(rendered, "tool");
11566
+ break;
11567
+ case "tool_result":
11568
+ this.crew.currentTool = null;
11569
+ this.note(rendered, event.ok ? "tool" : "error");
11570
+ break;
11571
+ case "result":
11572
+ this.crew.currentTool = null;
11573
+ this.crew.lastResult = { ok: event.ok, status: event.status };
11574
+ if (event.costUsd !== void 0) this.crew.costUsd = event.costUsd;
11575
+ if (event.turns !== void 0) this.crew.turns = event.turns;
11576
+ this.note(rendered, event.ok ? "result" : "error");
11577
+ break;
11578
+ case "hook":
11579
+ this.note(rendered, "hook");
11580
+ break;
11581
+ case "rate_limit":
11582
+ this.crew.rateLimit = { line: event.line, severe: event.severe };
11583
+ this.note(rendered, "rate_limit");
11584
+ break;
11585
+ case "prompt":
11586
+ this.note(rendered, "prompt");
11587
+ break;
11588
+ case "hitl_reply":
11589
+ this.hitlReplies += 1;
11590
+ this.note(rendered, "hitl");
11591
+ break;
11592
+ default:
11593
+ break;
11594
+ }
11595
+ }
11596
+ }
11597
+ };
11598
+ function emptyLiveMission() {
11599
+ return {
11600
+ planFile: null,
11601
+ mode: null,
11602
+ todos: { done: 0, total: 0 },
11603
+ currentTodo: null,
11604
+ nextTodo: null,
11605
+ queue: null
11606
+ };
11607
+ }
11608
+
11609
+ // src/mission-control/live-run.ts
11610
+ var LIVE_FRAME_MS = 120;
11611
+ var LIVE_PROGRESS_TTL_MS = 2e3;
11612
+ function shouldUseLiveTui(opts = {}) {
11613
+ if (opts.plain) return false;
11614
+ const env = opts.env ?? process.env;
11615
+ const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
11616
+ const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
11617
+ if (!stdoutIsTTY || !stdinIsTTY) return false;
11618
+ return shouldUseWelcomeColor({ stdoutIsTTY, env });
11619
+ }
11620
+ function defaultMissionIo() {
11621
+ return {
11622
+ readFile: async (file) => {
11623
+ try {
11624
+ return await readFile25(file, "utf8");
11625
+ } catch {
11626
+ return null;
11627
+ }
11628
+ },
11629
+ findActivePlan: findActivePlanFile
11630
+ };
11631
+ }
11632
+ var QUEUE_CURSOR_RE = /^- \*\*Queue cursor:\*\*\s*(.+)$/m;
11633
+ var QUEUE_STATUS_RE = /^- \*\*Queue status:\*\*\s*(.+)$/m;
11634
+ var MODE_RE = /^- \*\*Mode:\*\*\s*(.+)$/m;
11635
+ var QUEUE_DONE_STATUSES = /* @__PURE__ */ new Set(["exhausted", "completed", "done"]);
11636
+ function firstWord(raw) {
11637
+ const word = raw?.trim().split(/\s+/)[0]?.replace(/[`*]/g, "");
11638
+ return word ? word : null;
11639
+ }
11640
+ async function readLiveMission(root, io = defaultMissionIo()) {
11641
+ const mission = emptyLiveMission();
11642
+ const handoff = await io.readFile(path46.join(root, ".cursor", "HANDOFF.md")) ?? "";
11643
+ const named = extractHandoffNamedPlans(handoff);
11644
+ mission.mode = firstWord(MODE_RE.exec(handoff)?.[1]);
11645
+ let cursorIndex = null;
11646
+ let cursorPlan = null;
11647
+ const cursorRaw = QUEUE_CURSOR_RE.exec(handoff)?.[1]?.trim() ?? "";
11648
+ const indexMatch = /^(\d+)\b/.exec(cursorRaw);
11649
+ if (indexMatch?.[1]) cursorIndex = Number(indexMatch[1]);
11650
+ const currentMatch = /current:\s*`?([^`()]+?)`?\s*\)/i.exec(cursorRaw);
11651
+ if (currentMatch?.[1]) {
11652
+ const base = currentMatch[1].trim().split("/").pop() ?? "";
11653
+ if (/\.plan\.md$/i.test(base)) cursorPlan = base;
11654
+ }
11655
+ const queueStatus = firstWord(QUEUE_STATUS_RE.exec(handoff)?.[1]);
11656
+ const queue = named.runQueue;
11657
+ const finished = queueStatus !== null && QUEUE_DONE_STATUSES.has(queueStatus.toLowerCase());
11658
+ let checklist = [];
11659
+ if (queue.length > 0) {
11660
+ let cursor = null;
11661
+ if (cursorIndex !== null && cursorIndex >= 0 && cursorIndex < queue.length) {
11662
+ cursor = cursorIndex;
11663
+ } else if (cursorPlan && queue.includes(cursorPlan)) {
11664
+ cursor = queue.indexOf(cursorPlan);
11665
+ }
11666
+ const done = finished ? queue.length : cursor ?? 0;
11667
+ mission.queue = {
11668
+ done,
11669
+ total: queue.length,
11670
+ current: cursor !== null ? queue[cursor] ?? null : cursorPlan,
11671
+ status: queueStatus
11672
+ };
11673
+ checklist = queue.map((file, i) => ({
11674
+ label: file,
11675
+ state: finished || cursor !== null && i < cursor ? "done" : i === cursor ? "current" : "pending"
11676
+ }));
11677
+ }
11678
+ const plansDir = path46.join(root, ".cursor", "plans");
11679
+ const planBase = (mission.queue && !finished ? mission.queue.current : null) ?? named.active;
11680
+ const planPath = planBase ? path46.join(plansDir, planBase) : await io.findActivePlan(plansDir);
11681
+ if (planPath) {
11682
+ const raw = await io.readFile(planPath);
11683
+ if (raw !== null) {
11684
+ mission.planFile = path46.basename(planPath);
11685
+ const { todos } = parsePlanFrontmatter2(raw);
11686
+ const open = todos.filter((t) => t.status !== "cancelled");
11687
+ mission.todos = {
11688
+ done: open.filter((t) => t.status === "completed").length,
11689
+ total: open.length
11690
+ };
11691
+ mission.currentTodo = todos.find((t) => t.status === "in_progress")?.id ?? null;
11692
+ mission.nextTodo = todos.find((t) => t.status === "pending")?.id ?? null;
11693
+ if (checklist.length === 0) {
11694
+ checklist = open.map((t) => ({
11695
+ label: t.id,
11696
+ state: t.status === "completed" ? "done" : t.status === "in_progress" ? "current" : "pending"
11697
+ }));
11698
+ const current = checklist.findIndex((row) => row.state === "current");
11699
+ if (current > 1) checklist = checklist.slice(current - 1);
11700
+ }
11701
+ }
11702
+ }
11703
+ return { mission, checklist };
11704
+ }
11705
+ function createLiveMissionLoader(root, opts = {}) {
11706
+ const ttl = opts.ttlMs ?? LIVE_PROGRESS_TTL_MS;
11707
+ const now2 = opts.now ?? Date.now;
11708
+ let cached = null;
11709
+ let readAt = Number.NEGATIVE_INFINITY;
11710
+ let inFlight = null;
11711
+ return async () => {
11712
+ if (cached && now2() - readAt < ttl) return cached;
11713
+ if (inFlight) return inFlight;
11714
+ inFlight = readLiveMission(root, opts.io).then((state) => {
11715
+ cached = state;
11716
+ readAt = now2();
11717
+ return state;
11718
+ }).finally(() => {
11719
+ inFlight = null;
11720
+ });
11721
+ return cached ?? inFlight;
11722
+ };
11723
+ }
11724
+ async function withLiveTui(opts, run) {
11725
+ const write = opts.write ?? ((chunk) => process.stdout.write(chunk));
11726
+ const stdin = opts.stdin ?? process.stdin;
11727
+ const env = opts.env ?? process.env;
11728
+ const now2 = opts.now ?? Date.now;
11729
+ const columns = opts.columns ?? process.stdout.columns ?? 80;
11730
+ const rows = opts.rows ?? process.stdout.rows ?? 24;
11731
+ const feed = new LiveRunFeed({
11732
+ columns,
11733
+ now: now2,
11734
+ backend: opts.backend,
11735
+ logPath: path46.relative(opts.root, opts.logPath) || opts.logPath
11736
+ });
11737
+ opts.onFeed?.(feed);
11738
+ const missionLoader = opts.missionLoader ?? createLiveMissionLoader(opts.root, { now: now2 });
11739
+ const input = new InputLine();
11740
+ let pending = null;
11741
+ let spawnInfo = null;
11742
+ let quitRequested = null;
11743
+ let handle = null;
11744
+ const repaint = () => {
11745
+ void handle?.repaint();
11746
+ };
11747
+ const settleGate = (answer) => {
11748
+ const open = pending;
11749
+ if (!open) return;
11750
+ pending = null;
11751
+ feed.closeGate();
11752
+ open.settle(answer);
11753
+ };
11754
+ const readAnswer = (gate, readOpts) => new Promise((resolve3) => {
11755
+ const onAbort = () => settleGate({ kind: "eof" });
11756
+ if (readOpts.signal?.aborted) {
11757
+ resolve3({ kind: "eof" });
11758
+ return;
11759
+ }
11760
+ input.reset();
11761
+ pending = {
11762
+ gate,
11763
+ settle: (answer) => {
11764
+ readOpts.signal?.removeEventListener("abort", onAbort);
11765
+ resolve3(answer);
11766
+ }
11767
+ };
11768
+ readOpts.signal?.addEventListener("abort", onAbort, { once: true });
11769
+ feed.openGate(gate);
11770
+ repaint();
11771
+ });
11772
+ const stopRun = (stop) => {
11773
+ if (pending) {
11774
+ settleGate({ kind: "sigint" });
11775
+ return;
11776
+ }
11777
+ if (quitRequested) return;
11778
+ quitRequested = stop;
11779
+ feed.setPhase("stopping");
11780
+ feed.note(stop.message);
11781
+ spawnInfo?.stop(stop);
11782
+ };
11783
+ const hooks = {
11784
+ write,
11785
+ stdin,
11786
+ now: now2,
11787
+ setIntervalFn: opts.setIntervalFn,
11788
+ clearIntervalFn: opts.clearIntervalFn,
11789
+ onKey: (chunk) => {
11790
+ if (!pending) return false;
11791
+ const result = input.handleKey(chunk);
11792
+ if (result === "submit") settleGate({ kind: "line", text: input.value });
11793
+ else if (result === "sigint") settleGate({ kind: "sigint" });
11794
+ else if (result === "eof") settleGate({ kind: "eof" });
11795
+ else if (result === "edit") repaint();
11796
+ return true;
11797
+ },
11798
+ onQuit: (key) => {
11799
+ write("\n");
11800
+ stopRun(operatorQuitStop(key));
11801
+ }
11802
+ };
11803
+ const loadView = async () => {
11804
+ const state = await missionLoader();
11805
+ return {
11806
+ mission: state.mission,
11807
+ checklist: state.checklist,
11808
+ gate: pending ? {
11809
+ askId: pending.gate.askId,
11810
+ labels: pending.gate.labels,
11811
+ detection: pending.gate.detection,
11812
+ input
11813
+ } : null,
11814
+ flightLog: feed.log,
11815
+ crew: feed.crew,
11816
+ error: null
11817
+ };
11818
+ };
11819
+ const seams = {
11820
+ render: feed,
11821
+ log: (line) => feed.note(line),
11822
+ onSpawn: (info) => {
11823
+ spawnInfo = info;
11824
+ feed.setProcess({ backend: info.backend, pid: info.pid });
11825
+ if (quitRequested) info.stop(quitRequested);
11826
+ },
11827
+ hitl: {
11828
+ policy: opts.noHitl ? "off" : "prompt",
11829
+ isTTY: true,
11830
+ readAnswer,
11831
+ write: (text) => feed.note(text, "hitl")
11832
+ }
11833
+ };
11834
+ const onSignal = (signal) => {
11835
+ handle?.stop();
11836
+ stopRun(operatorQuitStop(signal));
11837
+ };
11838
+ const onSigint = () => onSignal("SIGINT");
11839
+ const onSigterm = () => onSignal("SIGTERM");
11840
+ const signals = opts.signals ?? true;
11841
+ if (signals) {
11842
+ process.prependListener("SIGINT", onSigint);
11843
+ process.prependListener("SIGTERM", onSigterm);
11844
+ }
11845
+ try {
11846
+ handle = await runMcTuiLoop({
11847
+ loadView,
11848
+ render: (view, renderOpts) => renderMcLive(view, { ...renderOpts, columns, rows, now: now2() }),
11849
+ stdoutIsTTY: true,
11850
+ env,
11851
+ intervalMs: opts.intervalMs ?? LIVE_FRAME_MS,
11852
+ renderOpts: { stdoutIsTTY: true, env, columns },
11853
+ hooks
11854
+ });
11855
+ const result = await run(seams);
11856
+ feed.setPhase("done");
11857
+ if (handle) await handle.repaint();
11858
+ return result;
11859
+ } finally {
11860
+ if (pending) settleGate({ kind: "eof" });
11861
+ handle?.stop();
11862
+ if (signals) {
11863
+ process.off("SIGINT", onSigint);
11864
+ process.off("SIGTERM", onSigterm);
11865
+ }
11866
+ write("\n");
11867
+ }
11868
+ }
9263
11869
 
9264
11870
  // src/plan-loop/external-review.ts
9265
- import { spawn as spawn7 } from "child_process";
9266
- import path44 from "path";
9267
- var CANONICAL_REL = path44.join(".cursor", "scripts", "plan-external-review.sh");
9268
- var FALLBACK_REL = path44.join("scripts", "plan-external-review.sh");
11871
+ import { spawn as spawn8 } from "child_process";
11872
+ import path47 from "path";
11873
+ var CANONICAL_REL = path47.join(".cursor", "scripts", "plan-external-review.sh");
11874
+ var FALLBACK_REL = path47.join("scripts", "plan-external-review.sh");
9269
11875
  function isPlanExhaustedReason(reason) {
9270
11876
  const r = reason.trim().toLowerCase();
9271
11877
  if (!r) return false;
@@ -9283,12 +11889,12 @@ function shouldArmExternalPlanReview(input) {
9283
11889
  return false;
9284
11890
  }
9285
11891
  async function armExternalPlanReview(root, options = {}) {
9286
- const spawnFn = options.spawnFn ?? spawn7;
11892
+ const spawnFn = options.spawnFn ?? spawn8;
9287
11893
  const existsFn = options.existsFn ?? fileExists;
9288
11894
  const log = options.log ?? ((line) => console.log(line));
9289
11895
  const force = options.force === true;
9290
- const canonicalPath = path44.join(root, CANONICAL_REL);
9291
- const fallbackPath = path44.join(root, FALLBACK_REL);
11896
+ const canonicalPath = path47.join(root, CANONICAL_REL);
11897
+ const fallbackPath = path47.join(root, FALLBACK_REL);
9292
11898
  let scriptPath = null;
9293
11899
  let scriptRel = CANONICAL_REL;
9294
11900
  if (await existsFn(canonicalPath)) {
@@ -9341,7 +11947,7 @@ async function armExternalPlanReview(root, options = {}) {
9341
11947
  }
9342
11948
 
9343
11949
  // src/plan-loop/persona-banners.ts
9344
- import path45 from "path";
11950
+ import path48 from "path";
9345
11951
  import {
9346
11952
  blue,
9347
11953
  cyan as cyan4,
@@ -9377,7 +11983,7 @@ function resolveColor(name, fallback) {
9377
11983
  async function resolveCliPersonaId(root) {
9378
11984
  try {
9379
11985
  const cfg = await readJson(
9380
- path45.join(root, ".cursor", "context", "config.json")
11986
+ path48.join(root, ".cursor", "context", "config.json")
9381
11987
  );
9382
11988
  const modes = cfg?.agentPersona?.modes ?? cfg?.workspaceSkin?.modes;
9383
11989
  const id = modes?.[CLI_RUN_PLAN_MODE];
@@ -9388,7 +11994,7 @@ async function resolveCliPersonaId(root) {
9388
11994
  }
9389
11995
  async function loadPersonaPack(root, personaId) {
9390
11996
  try {
9391
- const personaPath = path45.join(root, "registry", "personas", "core", personaId, "persona.json");
11997
+ const personaPath = path48.join(root, "registry", "personas", "core", personaId, "persona.json");
9392
11998
  const pack = await readJson(personaPath);
9393
11999
  if (!pack || typeof pack.id !== "string") return null;
9394
12000
  return pack;
@@ -9436,44 +12042,8 @@ function createPersonaBannerPrinter(persona) {
9436
12042
  };
9437
12043
  }
9438
12044
 
9439
- // src/plan-loop/plan-state.ts
9440
- import { readFile as readFile22, readdir as readdir9 } from "fs/promises";
9441
- import path46 from "path";
9442
- function countPendingTodos(raw) {
9443
- const lines = raw.split(/\r?\n/);
9444
- let inFront = 0;
9445
- let cont = false;
9446
- let n = 0;
9447
- for (const line of lines) {
9448
- if (line === "---") {
9449
- inFront += 1;
9450
- continue;
9451
- }
9452
- if (inFront !== 1) continue;
9453
- const idMatch = line.match(/^- id:\s*(\S+)/);
9454
- if (idMatch?.[1]) {
9455
- cont = idMatch[1] === "dogfood-poc";
9456
- }
9457
- if (/content:.*[Cc]ont[ií]nuo/.test(line)) {
9458
- cont = true;
9459
- }
9460
- if (/status:\s*(pending|in_progress)\b/.test(line) && !cont) {
9461
- n += 1;
9462
- }
9463
- }
9464
- return n;
9465
- }
9466
- async function findActivePlanFile(plansDir) {
9467
- if (!await fileExists(plansDir)) return null;
9468
- const files = (await readdir9(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
9469
- return files[0] ? path46.join(plansDir, files[0]) : null;
9470
- }
9471
- async function readPlan(planPath) {
9472
- return readFile22(planPath, "utf8");
9473
- }
9474
-
9475
12045
  // src/plan-loop/sentinel.ts
9476
- import { readFile as readFile23 } from "fs/promises";
12046
+ import { readFile as readFile26 } from "fs/promises";
9477
12047
  var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
9478
12048
  function takeFromText(text) {
9479
12049
  if (!text) return null;
@@ -9539,14 +12109,14 @@ function parseTickResultStatus(content) {
9539
12109
  }
9540
12110
  async function parseTickResultStatusFromLogFile(logPath) {
9541
12111
  try {
9542
- return parseTickResultStatus(await readFile23(logPath, "utf8"));
12112
+ return parseTickResultStatus(await readFile26(logPath, "utf8"));
9543
12113
  } catch {
9544
12114
  return null;
9545
12115
  }
9546
12116
  }
9547
12117
  async function parseSentinelFromLogFile(logPath) {
9548
12118
  try {
9549
- const content = await readFile23(logPath, "utf8");
12119
+ const content = await readFile26(logPath, "utf8");
9550
12120
  return parseSentinelFromLog(content);
9551
12121
  } catch {
9552
12122
  return { kind: "missing" };
@@ -9560,6 +12130,40 @@ function formatSentinelLine(sentinel) {
9560
12130
 
9561
12131
  // src/plan-loop/run-loop.ts
9562
12132
  var TICK_PROMPT = '/run-plan - single tick from the headless runner (agent-kit run-plan). Read .cursor/HANDOFF.md and the active plan in .cursor/plans/. Mark the next to-do as in_progress in the frontmatter, execute ONLY that to-do, mark completed and update HANDOFF. If there is a commitable diff: run /git-staging without asking for confirmation. NEVER /git-prod. Do NOT re-arm an internal Loop skill - the external runner starts the next agent. End the response with exactly one line: "LOOP_TICK_RESULT: continue" if implementable to-dos remain, or "LOOP_TICK_RESULT: stop - <reason>" (plan exhausted, external blocker, or human decision needed).';
12133
+ async function runTickWithSpinner(label, backend, opts, plain) {
12134
+ if (shouldUseLiveTui({ plain })) {
12135
+ return withLiveTui(
12136
+ {
12137
+ root: opts.workspace,
12138
+ backend: backend.id,
12139
+ logPath: opts.logPath,
12140
+ noHitl: opts.hitl?.policy === "off"
12141
+ },
12142
+ (seams) => backend.run({
12143
+ ...opts,
12144
+ hitl: seams.hitl,
12145
+ render: seams.render,
12146
+ onSpawn: seams.onSpawn,
12147
+ log: seams.log
12148
+ })
12149
+ );
12150
+ }
12151
+ if (!shouldUseVisualMotion()) return backend.run(opts);
12152
+ const spinner = new TtySpinner({ motion: true });
12153
+ spinner.start(label);
12154
+ try {
12155
+ return await backend.run({
12156
+ ...opts,
12157
+ hitl: {
12158
+ ...opts.hitl,
12159
+ onPromptStart: () => spinner.stop(),
12160
+ onPromptEnd: () => spinner.start(label)
12161
+ }
12162
+ });
12163
+ } finally {
12164
+ spinner.stop();
12165
+ }
12166
+ }
9563
12167
  function stamp() {
9564
12168
  const d = /* @__PURE__ */ new Date();
9565
12169
  const p = (n, w = 2) => String(n).padStart(w, "0");
@@ -9569,9 +12173,9 @@ function sleep(ms) {
9569
12173
  return new Promise((r) => setTimeout(r, ms));
9570
12174
  }
9571
12175
  async function runPlanLoop(opts) {
9572
- const plansDir = path47.join(opts.root, ".cursor", "plans");
9573
- const stopFile = path47.join(opts.root, ".cursor", "loop.stop");
9574
- const logDir = path47.join(opts.root, ".cursor", "loop-logs");
12176
+ const plansDir = path49.join(opts.root, ".cursor", "plans");
12177
+ const stopFile = path49.join(opts.root, ".cursor", "loop.stop");
12178
+ const logDir = path49.join(opts.root, ".cursor", "loop-logs");
9575
12179
  const planPath = await findActivePlanFile(plansDir);
9576
12180
  if (!planPath) {
9577
12181
  logger.error("No active plan in .cursor/plans/");
@@ -9592,7 +12196,7 @@ async function runPlanLoop(opts) {
9592
12196
  try {
9593
12197
  const persona = await loadCliRunPlanPersona(opts.root);
9594
12198
  const banners = createPersonaBannerPrinter(persona);
9595
- console.log(`Active plan: ${path47.basename(planPath)}`);
12199
+ console.log(`Active plan: ${path49.basename(planPath)}`);
9596
12200
  console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
9597
12201
  console.log(`Backend: ${opts.backend.id}`);
9598
12202
  if (persona) {
@@ -9611,6 +12215,7 @@ async function runPlanLoop(opts) {
9611
12215
  let tick = 0;
9612
12216
  let stopReason;
9613
12217
  let planExhausted = false;
12218
+ let exitCode = 0;
9614
12219
  while (true) {
9615
12220
  tick += 1;
9616
12221
  if (tick > opts.maxTicks) {
@@ -9635,30 +12240,47 @@ async function runPlanLoop(opts) {
9635
12240
  planExhausted = true;
9636
12241
  break;
9637
12242
  }
9638
- const logPath = path47.join(logDir, `tick-${stamp()}.log`);
9639
- const relLog = path47.relative(opts.root, logPath);
12243
+ const logPath = path49.join(logDir, `tick-${stamp()}.log`);
12244
+ const relLog = path49.relative(opts.root, logPath);
9640
12245
  console.log("");
9641
12246
  const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
9642
12247
  if (banners) banners.tickStart(tickLine);
9643
12248
  else console.log(tickLine);
9644
12249
  let agentExit = 0;
12250
+ let hitlStop;
9645
12251
  try {
9646
- const result = await withCliProgress(
12252
+ const result = await runTickWithSpinner(
9647
12253
  `tick ${tick}`,
9648
- () => opts.backend.run({
12254
+ opts.backend,
12255
+ {
9649
12256
  workspace: opts.root,
9650
12257
  prompt: TICK_PROMPT,
9651
12258
  model: opts.model,
9652
- logPath
9653
- })
12259
+ logPath,
12260
+ hitl: { policy: opts.noHitl ? "off" : "prompt" }
12261
+ },
12262
+ opts.plain === true
9654
12263
  );
9655
12264
  agentExit = result.exitCode;
12265
+ for (const line of formatHitlSummary(result.hitl)) console.log(line);
12266
+ hitlStop = result.hitl?.stop;
9656
12267
  } catch (err) {
9657
12268
  logger.error(String(err));
9658
12269
  return 1;
9659
12270
  }
12271
+ if (hitlStop) {
12272
+ const msg = `${opts.backend.id} tick ended at a HITL gate without a reply - stopping. See log: ${relLog}`;
12273
+ if (banners) {
12274
+ banners.tickEnd("hitl stop");
12275
+ banners.stop(msg);
12276
+ } else {
12277
+ console.log(msg);
12278
+ }
12279
+ exitCode = hitlStop.exitCode;
12280
+ break;
12281
+ }
9660
12282
  try {
9661
- const logText = await readFile24(logPath, "utf8");
12283
+ const logText = await readFile27(logPath, "utf8");
9662
12284
  if (logText.includes("Too many MCP tools")) {
9663
12285
  const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
9664
12286
  if (banners) banners.stop(msg);
@@ -9727,12 +12349,12 @@ async function runPlanLoop(opts) {
9727
12349
  const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
9728
12350
  if (banners) banners.phaseComplete(finishDetail);
9729
12351
  console.log(
9730
- `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path47.relative(opts.root, logDir)}/`
12352
+ `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path49.relative(opts.root, logDir)}/`
9731
12353
  );
9732
12354
  if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
9733
12355
  await armExternalPlanReview(opts.root);
9734
12356
  }
9735
- return 0;
12357
+ return exitCode;
9736
12358
  } finally {
9737
12359
  process.off("SIGINT", onSigInt);
9738
12360
  }
@@ -9774,6 +12396,16 @@ var runPlanCommand = defineCommand16({
9774
12396
  type: "boolean",
9775
12397
  description: "Print the tick prompt and exit without starting an agent",
9776
12398
  default: false
12399
+ },
12400
+ "no-hitl": {
12401
+ type: "boolean",
12402
+ description: `CI: never prompt; a HITL gate stops the run with exit ${HITL_UNANSWERED_EXIT_CODE} (no default answer)`,
12403
+ default: false
12404
+ },
12405
+ plain: {
12406
+ type: "boolean",
12407
+ description: "Status lines instead of the Mission Control live view (the default on a TTY; non-TTY, NO_COLOR and CI are always plain)",
12408
+ default: false
9777
12409
  }
9778
12410
  },
9779
12411
  async run({ args }) {
@@ -9809,12 +12441,14 @@ var runPlanCommand = defineCommand16({
9809
12441
  return;
9810
12442
  }
9811
12443
  const code = await runPlanLoop({
9812
- root: path48.resolve(args.cwd),
12444
+ root: path50.resolve(args.cwd),
9813
12445
  maxTicks,
9814
12446
  sleepSeconds,
9815
12447
  model: args.model ? String(args.model) : void 0,
9816
12448
  dryRun: Boolean(args["dry-run"]),
9817
- backend
12449
+ backend,
12450
+ noHitl: Boolean(args["no-hitl"]),
12451
+ plain: Boolean(args.plain)
9818
12452
  });
9819
12453
  process.exitCode = code;
9820
12454
  }
@@ -9822,12 +12456,12 @@ var runPlanCommand = defineCommand16({
9822
12456
 
9823
12457
  // src/commands/run.ts
9824
12458
  import { homedir as homedir4 } from "os";
9825
- import path50 from "path";
12459
+ import path52 from "path";
9826
12460
  import { defineCommand as defineCommand17 } from "citty";
9827
12461
 
9828
12462
  // src/plan-loop/dispatch.ts
9829
- import { mkdir as mkdir7, readFile as readFile25 } from "fs/promises";
9830
- import path49 from "path";
12463
+ import { mkdir as mkdir7, readFile as readFile28 } from "fs/promises";
12464
+ import path51 from "path";
9831
12465
  var RUN_CATALOG = [
9832
12466
  "run-plan",
9833
12467
  "run-plan-all",
@@ -9840,6 +12474,7 @@ var RUN_CATALOG = [
9840
12474
  "git-staging",
9841
12475
  "kit-staging"
9842
12476
  ];
12477
+ var FIRST_CLASS_SLASH_COMMANDS = ["run-plan", "run-plan-all"];
9843
12478
  var RUN_PROMOTE_BLOCKED = ["git-prod", "kit-prod"];
9844
12479
  function normalizeSlashName(raw) {
9845
12480
  return raw.replace(/^\//, "").replace(/\.md$/i, "").trim();
@@ -9851,12 +12486,30 @@ function classifySlash(name) {
9851
12486
  if (RUN_CATALOG.includes(n)) return "catalog";
9852
12487
  return "unknown";
9853
12488
  }
12489
+ function rewriteRootArgvToRun(argv) {
12490
+ const idx = argv.findIndex((arg) => arg.length > 0 && !arg.startsWith("-"));
12491
+ if (idx < 0) return argv;
12492
+ const token = argv[idx];
12493
+ if (!token || token === "run") return argv;
12494
+ const kind = classifySlash(token);
12495
+ if (kind === "unknown") return argv;
12496
+ const slash = normalizeSlashName(token);
12497
+ if (FIRST_CLASS_SLASH_COMMANDS.includes(slash)) {
12498
+ if (token === slash) return argv;
12499
+ const next2 = [...argv];
12500
+ next2[idx] = slash;
12501
+ return next2;
12502
+ }
12503
+ const next = [...argv];
12504
+ next.splice(idx, 1, "run", slash);
12505
+ return next;
12506
+ }
9854
12507
  function commandMarkdownPath(root, name) {
9855
- return path49.join(root, ".cursor", "commands", `${normalizeSlashName(name)}.md`);
12508
+ return path51.join(root, ".cursor", "commands", `${normalizeSlashName(name)}.md`);
9856
12509
  }
9857
12510
  function buildDispatchPrompt(commandBody) {
9858
12511
  return [
9859
- "Follow the Agent Kit command below. This session is headless: use numbered-list HITL (Ask questions is Cursor-only). Never /git-prod. No CLI --force promote.",
12512
+ "Follow the Agent Kit command below. This session is headless: use numbered-list HITL (Ask questions is Cursor-only). Before each numbered list print one line `HITL_GATE: <ask-id> | <label 1> | <label 2> | ...` (ask-id and labels from the hitl-gates skill), then wait; the operator's answer arrives as `HITL_REPLY: <ask-id> | operator reply <n> | <label>`. Never /git-prod. No CLI --force promote.",
9860
12513
  "",
9861
12514
  commandBody.trimEnd(),
9862
12515
  ""
@@ -9877,7 +12530,7 @@ async function readCommandFile(root, name) {
9877
12530
  message: `Command file not found: ${filePath}. Install or refresh Agent Kit L0 so .cursor/commands/${normalizeSlashName(name)}.md exists.`
9878
12531
  };
9879
12532
  }
9880
- const body = await readFile25(filePath, "utf8");
12533
+ const body = await readFile28(filePath, "utf8");
9881
12534
  return { ok: true, path: filePath, body };
9882
12535
  }
9883
12536
  function stamp2() {
@@ -9900,7 +12553,7 @@ function cursorAgentDispatchArgs(opts) {
9900
12553
  args.push(opts.prompt);
9901
12554
  return args;
9902
12555
  }
9903
- function claudeDispatchArgs(opts) {
12556
+ function claudeDispatchArgs(opts = {}) {
9904
12557
  return claudeHeadlessArgs(opts);
9905
12558
  }
9906
12559
  async function runHeadlessDispatch(opts) {
@@ -9917,9 +12570,21 @@ async function runHeadlessDispatch(opts) {
9917
12570
  try {
9918
12571
  return await spawnLogged(
9919
12572
  opts.bin,
9920
- claudeDispatchArgs({ prompt: opts.prompt, model: opts.model, ...caps }),
12573
+ claudeDispatchArgs({ model: opts.model, ...caps }),
9921
12574
  opts.logPath,
9922
- { cwd: opts.workspace, env, redact, spawnFn: opts.spawnFn }
12575
+ {
12576
+ cwd: opts.workspace,
12577
+ env,
12578
+ redact,
12579
+ spawnFn: opts.spawnFn,
12580
+ hitl: resolveHitlRunOptions(opts.hitl, {
12581
+ kind: "stdin-stream-json",
12582
+ firstPrompt: opts.prompt
12583
+ }),
12584
+ render: opts.render,
12585
+ onSpawn: opts.onSpawn,
12586
+ backendId: "claude"
12587
+ }
9923
12588
  );
9924
12589
  } catch (err) {
9925
12590
  throw new Error(`claude dispatch failed to start: ${redactSecrets(String(err), redact)}`);
@@ -9932,24 +12597,28 @@ async function runHeadlessDispatch(opts) {
9932
12597
  });
9933
12598
  return spawnLogged(opts.bin, args, opts.logPath, {
9934
12599
  spawnFn: opts.spawnFn,
9935
- redact: claudeRedactions(process.env)
12600
+ redact: claudeRedactions(process.env),
12601
+ hitl: resolveHitlRunOptions(opts.hitl, { kind: "none", backend: "cursor-agent" }),
12602
+ render: opts.render,
12603
+ onSpawn: opts.onSpawn,
12604
+ backendId: "cursor-agent"
9936
12605
  });
9937
12606
  }
9938
12607
  async function ensureDispatchLogPath(root) {
9939
- const logDir = path49.join(root, ".cursor", "loop-logs");
12608
+ const logDir = path51.join(root, ".cursor", "loop-logs");
9940
12609
  await mkdir7(logDir, { recursive: true });
9941
- return path49.join(logDir, `run-${stamp2()}.log`);
12610
+ return path51.join(logDir, `run-${stamp2()}.log`);
9942
12611
  }
9943
12612
 
9944
12613
  // src/commands/run.ts
9945
12614
  function displayPath(target, home = homedir4()) {
9946
- const resolvedHome = path50.resolve(home);
12615
+ const resolvedHome = path52.resolve(home);
9947
12616
  if (target === resolvedHome) return "~";
9948
- const prefix = resolvedHome.endsWith(path50.sep) ? resolvedHome : `${resolvedHome}${path50.sep}`;
9949
- return target.startsWith(prefix) ? `~${path50.sep}${target.slice(prefix.length)}` : target;
12617
+ const prefix = resolvedHome.endsWith(path52.sep) ? resolvedHome : `${resolvedHome}${path52.sep}`;
12618
+ return target.startsWith(prefix) ? `~${path52.sep}${target.slice(prefix.length)}` : target;
9950
12619
  }
9951
12620
  async function executeRun(input) {
9952
- const root = path50.resolve(input.cwd);
12621
+ const root = path52.resolve(input.cwd);
9953
12622
  const slash = normalizeSlashName(input.slash);
9954
12623
  if (!slash) {
9955
12624
  return { exitCode: 1, error: "Provide a slash: agent-kit run <slash>" };
@@ -9985,7 +12654,9 @@ async function executeRun(input) {
9985
12654
  sleepSeconds: input.sleepSeconds,
9986
12655
  model: input.model,
9987
12656
  dryRun: false,
9988
- backend
12657
+ backend,
12658
+ noHitl: input.noHitl === true,
12659
+ plain: input.plain === true
9989
12660
  });
9990
12661
  return { exitCode: code };
9991
12662
  }
@@ -9998,7 +12669,7 @@ async function executeRun(input) {
9998
12669
  const stdout = [
9999
12670
  "--dry-run: no agent will be started.",
10000
12671
  `Slash: ${slash}`,
10001
- `Command file: ${path50.relative(root, file.path)}`,
12672
+ `Command file: ${path52.relative(root, file.path)}`,
10002
12673
  `Backend: ${detected.id} (${displayPath(detected.bin)})`,
10003
12674
  "Prompt:",
10004
12675
  prompt
@@ -10007,93 +12678,135 @@ async function executeRun(input) {
10007
12678
  }
10008
12679
  const logPath = await ensureDispatchLogPath(root);
10009
12680
  console.log(`Dispatch: ${slash} via ${detected.id}`);
10010
- console.log(`Command file: ${path50.relative(root, file.path)}`);
10011
- console.log(`Log: ${path50.relative(root, logPath)}`);
12681
+ console.log(`Command file: ${path52.relative(root, file.path)}`);
12682
+ console.log(`Log: ${path52.relative(root, logPath)}`);
10012
12683
  try {
10013
- const result = await runHeadlessDispatch({
12684
+ const dispatch = {
10014
12685
  backendId: detected.id,
10015
12686
  bin: detected.bin,
10016
12687
  workspace: root,
10017
12688
  prompt,
10018
12689
  model: input.model,
10019
12690
  logPath
12691
+ };
12692
+ const result = shouldUseLiveTui({ plain: input.plain }) ? await withLiveTui(
12693
+ { root, backend: detected.id, logPath, noHitl: input.noHitl === true },
12694
+ (seams) => runHeadlessDispatch({
12695
+ ...dispatch,
12696
+ hitl: seams.hitl,
12697
+ render: seams.render,
12698
+ onSpawn: seams.onSpawn,
12699
+ log: seams.log
12700
+ })
12701
+ ) : await runHeadlessDispatch({
12702
+ ...dispatch,
12703
+ hitl: { policy: input.noHitl ? "off" : "prompt" }
10020
12704
  });
10021
- return { exitCode: result.exitCode };
12705
+ for (const line of formatHitlSummary(result.hitl)) console.log(line);
12706
+ return { exitCode: hitlExitCode(result.exitCode, result.hitl) };
10022
12707
  } catch (err) {
10023
12708
  return { exitCode: 1, error: String(err) };
10024
12709
  }
10025
12710
  }
12711
+ var runDispatchArgs = {
12712
+ cwd: {
12713
+ type: "string",
12714
+ description: "Project root (default: current directory)",
12715
+ default: process.cwd()
12716
+ },
12717
+ backend: {
12718
+ type: "string",
12719
+ description: `Agent CLI (${listDetectBackendIds().join(" | ")}; default: auto)`,
12720
+ default: "auto"
12721
+ },
12722
+ model: {
12723
+ type: "string",
12724
+ description: "Optional model id passed to the agent CLI",
12725
+ default: ""
12726
+ },
12727
+ "dry-run": {
12728
+ type: "boolean",
12729
+ description: "Print the resolved backend and prompt, then exit without starting an agent",
12730
+ default: false
12731
+ },
12732
+ "max-ticks": {
12733
+ type: "string",
12734
+ description: "For run-plan alias only: maximum ticks (default: 10)",
12735
+ default: "10"
12736
+ },
12737
+ sleep: {
12738
+ type: "string",
12739
+ description: "For run-plan alias only: seconds between ticks (default: 5)",
12740
+ default: "5"
12741
+ },
12742
+ "no-hitl": {
12743
+ type: "boolean",
12744
+ description: `CI: never prompt; a HITL gate stops the run with exit ${HITL_UNANSWERED_EXIT_CODE} (no default answer)`,
12745
+ default: false
12746
+ },
12747
+ plain: {
12748
+ type: "boolean",
12749
+ description: "Status lines instead of the Mission Control live view (the default on a TTY; non-TTY, NO_COLOR and CI are always plain)",
12750
+ default: false
12751
+ }
12752
+ };
12753
+ async function runSlashCli(slash, args) {
12754
+ const maxTicks = Number.parseInt(String(args["max-ticks"]), 10);
12755
+ const sleepSeconds = Number.parseFloat(String(args.sleep));
12756
+ if (!Number.isFinite(maxTicks) || maxTicks < 1) {
12757
+ logger.error("--max-ticks must be a positive integer");
12758
+ process.exitCode = 1;
12759
+ return;
12760
+ }
12761
+ if (!Number.isFinite(sleepSeconds) || sleepSeconds < 0) {
12762
+ logger.error("--sleep must be a non-negative number");
12763
+ process.exitCode = 1;
12764
+ return;
12765
+ }
12766
+ const result = await executeRun({
12767
+ slash,
12768
+ cwd: String(args.cwd),
12769
+ backend: String(args.backend),
12770
+ model: args.model ? String(args.model) : void 0,
12771
+ dryRun: Boolean(args["dry-run"]),
12772
+ maxTicks,
12773
+ sleepSeconds,
12774
+ noHitl: Boolean(args["no-hitl"]),
12775
+ plain: Boolean(args.plain)
12776
+ });
12777
+ if (result.stdout) {
12778
+ console.log(result.stdout);
12779
+ }
12780
+ if (result.error) {
12781
+ logger.error(result.error);
12782
+ }
12783
+ process.exitCode = result.exitCode;
12784
+ }
10026
12785
  var runCommand = defineCommand17({
10027
12786
  meta: {
10028
12787
  name: "run",
10029
- description: "Start one headless session from an L0 slash file. Numbered-list HITL. Never git-prod."
12788
+ description: "Start one headless session from an L0 slash file. Numbered-list HITL answered from the terminal (claude backend). Never git-prod."
10030
12789
  },
10031
12790
  args: {
10032
12791
  slash: {
10033
12792
  type: "positional",
10034
- description: "Slash name (e.g. backlog-add, run-plan, continue-plan)",
12793
+ description: "Slash name (e.g. run-plan-all, backlog-add, continue-plan)",
10035
12794
  required: true
10036
12795
  },
10037
- cwd: {
10038
- type: "string",
10039
- description: "Project root (default: current directory)",
10040
- default: process.cwd()
10041
- },
10042
- backend: {
10043
- type: "string",
10044
- description: `Agent CLI (${listDetectBackendIds().join(" | ")}; default: auto)`,
10045
- default: "auto"
10046
- },
10047
- model: {
10048
- type: "string",
10049
- description: "Optional model id passed to the agent CLI",
10050
- default: ""
10051
- },
10052
- "dry-run": {
10053
- type: "boolean",
10054
- description: "Print the resolved backend and prompt, then exit without starting an agent",
10055
- default: false
10056
- },
10057
- "max-ticks": {
10058
- type: "string",
10059
- description: "For run-plan alias only: maximum ticks (default: 10)",
10060
- default: "10"
10061
- },
10062
- sleep: {
10063
- type: "string",
10064
- description: "For run-plan alias only: seconds between ticks (default: 5)",
10065
- default: "5"
10066
- }
12796
+ ...runDispatchArgs
10067
12797
  },
10068
12798
  async run({ args }) {
10069
- const maxTicks = Number.parseInt(String(args["max-ticks"]), 10);
10070
- const sleepSeconds = Number.parseFloat(String(args.sleep));
10071
- if (!Number.isFinite(maxTicks) || maxTicks < 1) {
10072
- logger.error("--max-ticks must be a positive integer");
10073
- process.exitCode = 1;
10074
- return;
10075
- }
10076
- if (!Number.isFinite(sleepSeconds) || sleepSeconds < 0) {
10077
- logger.error("--sleep must be a non-negative number");
10078
- process.exitCode = 1;
10079
- return;
10080
- }
10081
- const result = await executeRun({
10082
- slash: String(args.slash ?? ""),
10083
- cwd: String(args.cwd),
10084
- backend: String(args.backend),
10085
- model: args.model ? String(args.model) : void 0,
10086
- dryRun: Boolean(args["dry-run"]),
10087
- maxTicks,
10088
- sleepSeconds
10089
- });
10090
- if (result.stdout) {
10091
- console.log(result.stdout);
10092
- }
10093
- if (result.error) {
10094
- logger.error(result.error);
10095
- }
10096
- process.exitCode = result.exitCode;
12799
+ await runSlashCli(String(args.slash ?? ""), args);
12800
+ }
12801
+ });
12802
+ var runPlanAllCommand = defineCommand17({
12803
+ meta: {
12804
+ name: "run-plan-all",
12805
+ description: "Headless /run-plan-all queue from the L0 file. Numbered-list HITL answered from the terminal (claude backend). Never git-prod."
12806
+ },
12807
+ args: runDispatchArgs,
12808
+ async run({ args }) {
12809
+ await runSlashCli("run-plan-all", args);
10097
12810
  }
10098
12811
  });
10099
12812
 
@@ -10120,22 +12833,21 @@ var scanCommand = defineCommand18({
10120
12833
  });
10121
12834
 
10122
12835
  // src/commands/setup-global.ts
10123
- import { spawn as spawn8 } from "child_process";
10124
- import { appendFile, mkdir as mkdir8, readFile as readFile26, writeFile as writeFile13 } from "fs/promises";
12836
+ import { spawn as spawn9 } from "child_process";
12837
+ import { appendFile as appendFile2, mkdir as mkdir8, readFile as readFile29, writeFile as writeFile13 } from "fs/promises";
10125
12838
  import { homedir as homedir5 } from "os";
10126
- import path51 from "path";
12839
+ import path53 from "path";
10127
12840
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
10128
12841
  import { defineCommand as defineCommand19 } from "citty";
10129
12842
  import { cyan as cyan5, green as green4, yellow as yellow3 } from "kolorist";
10130
12843
  var NPM_GLOBAL_DIR_NAME = ".npm-global";
10131
12844
  var SETUP_GLOBAL_MARKER = "# agent-kit setup-global";
10132
- var DEFAULT_PACKAGE_SPEC = "@dadado/agent-kit-cli";
10133
12845
  function planSetupGlobalSteps(env, options = {}) {
10134
12846
  const homeDir = options.homeDir ?? homedir5();
10135
- const packageSpec = options.packageSpec ?? DEFAULT_PACKAGE_SPEC;
10136
- const npmGlobalDir = path51.join(homeDir, NPM_GLOBAL_DIR_NAME);
10137
- const npmGlobalBin = path51.join(npmGlobalDir, "bin");
10138
- const npmrcPath = path51.join(homeDir, ".npmrc");
12847
+ const packageSpec = options.packageSpec ?? pinnedCliSpec();
12848
+ const npmGlobalDir = path53.join(homeDir, NPM_GLOBAL_DIR_NAME);
12849
+ const npmGlobalBin = path53.join(npmGlobalDir, "bin");
12850
+ const npmrcPath = path53.join(homeDir, ".npmrc");
10139
12851
  const npmrcPrefixValue = `~/${NPM_GLOBAL_DIR_NAME}`;
10140
12852
  const pathExportLine = `export PATH="${npmGlobalBin}:$PATH"`;
10141
12853
  const shellProfile = env.shellProfile;
@@ -10203,9 +12915,9 @@ var defaultFsImpl = {
10203
12915
  mkdir: async (dir) => {
10204
12916
  await mkdir8(dir, { recursive: true });
10205
12917
  },
10206
- readFile: (filePath) => readFile26(filePath, "utf8"),
12918
+ readFile: (filePath) => readFile29(filePath, "utf8"),
10207
12919
  writeFile: (filePath, content) => writeFile13(filePath, content, "utf8"),
10208
- appendFile: (filePath, content) => appendFile(filePath, content, "utf8")
12920
+ appendFile: (filePath, content) => appendFile2(filePath, content, "utf8")
10209
12921
  };
10210
12922
  async function safeReadFile(fs, filePath) {
10211
12923
  try {
@@ -10215,7 +12927,7 @@ async function safeReadFile(fs, filePath) {
10215
12927
  }
10216
12928
  }
10217
12929
  var defaultNpmInstallImpl = (packageSpec) => new Promise((resolve3) => {
10218
- const child = spawn8("npm", ["i", "-g", packageSpec], { stdio: "inherit" });
12930
+ const child = spawn9("npm", ["i", "-g", packageSpec], { stdio: "inherit" });
10219
12931
  child.on("error", (error) => resolve3({ ok: false, error }));
10220
12932
  child.on("close", (code) => {
10221
12933
  if (code === 0) resolve3({ ok: true });
@@ -10266,11 +12978,48 @@ async function runSetupGlobal(options = {}) {
10266
12978
  const homeDir = options.homeDir ?? homedir5();
10267
12979
  const env = await assessEnvironmentImpl(options);
10268
12980
  const plan = planSetupGlobalSteps(env, { homeDir, packageSpec: options.packageSpec });
10269
- if (plan.alreadyWritable) {
12981
+ if (plan.alreadyWritable && pathCliStatus(env, KIT_VERSION) === "current") {
10270
12982
  printHeader(env, print);
10271
- print(green4("npm's global prefix is already writable \u2014 nothing to fix."));
12983
+ print(green4("npm's global prefix is writable and PATH `agent-kit` matches this CLI."));
10272
12984
  return { exitCode: 0, mutated: false, outcome: "already-ok", env, plan };
10273
12985
  }
12986
+ if (plan.alreadyWritable) {
12987
+ printHeader(env, print);
12988
+ print(
12989
+ `PATH \`agent-kit\` is ${env.binVersion ? `v${env.binVersion}` : "missing or unreadable"}; this CLI is v${KIT_VERSION}.`
12990
+ );
12991
+ print(`Prefix is writable. Next step is only: npm i -g ${plan.packageSpec}`);
12992
+ if (options.dryRun) {
12993
+ print("Dry run: no changes.");
12994
+ print(` npm i -g ${plan.packageSpec}`);
12995
+ return { exitCode: 0, mutated: false, outcome: "dry-run", env, plan };
12996
+ }
12997
+ const nonInteractiveWritable = options.nonInteractive ?? isNonInteractive();
12998
+ if (nonInteractiveWritable) {
12999
+ print("No changes made. Run this yourself:");
13000
+ print(` npm i -g ${plan.packageSpec}`);
13001
+ print(" hash -r");
13002
+ print(" agent-kit --version");
13003
+ return { exitCode: 0, mutated: false, outcome: "manual-instructions", env, plan };
13004
+ }
13005
+ const confirmStep2 = options.confirmImpl ?? defaultConfirmImpl;
13006
+ const npmInstall2 = options.npmInstallImpl ?? defaultNpmInstallImpl;
13007
+ const proceedInstall2 = await confirmStep2(`Run: npm i -g ${plan.packageSpec}?`);
13008
+ if (!proceedInstall2) {
13009
+ print(yellow3("Cancelled: no changes made."));
13010
+ return { exitCode: 1, mutated: false, outcome: "cancelled", env, plan };
13011
+ }
13012
+ const installResult2 = await npmInstall2(plan.packageSpec);
13013
+ if (!installResult2.ok) {
13014
+ const hint = classifyInstallError(installResult2.error);
13015
+ print(` npm install failed: ${hint.message}`);
13016
+ print(hint.recovery);
13017
+ return { exitCode: 1, mutated: false, outcome: "error", env, plan };
13018
+ }
13019
+ print(green4(` done: ${plan.packageSpec} installed globally.`));
13020
+ print(" If this shell still shows the old version: hash -r (or open a new terminal).");
13021
+ return { exitCode: 0, mutated: true, outcome: "completed", env, plan };
13022
+ }
10274
13023
  if (options.dryRun) {
10275
13024
  printHeader(env, print);
10276
13025
  print("Dry run \u2014 no changes will be made. Steps that would run:");
@@ -10397,7 +13146,7 @@ var setupGlobalCommand = defineCommand19({
10397
13146
  });
10398
13147
 
10399
13148
  // src/commands/status.ts
10400
- import path52 from "path";
13149
+ import path54 from "path";
10401
13150
  import { defineCommand as defineCommand20 } from "citty";
10402
13151
  function profileStatus(profile) {
10403
13152
  if (!profile) return { origin: "none", evidence: [], profile: null };
@@ -10428,13 +13177,14 @@ var statusCommand = defineCommand20({
10428
13177
  }
10429
13178
  },
10430
13179
  async run({ args }) {
10431
- const rootDir = path52.resolve(args.cwd);
10432
- const [manifest, rawProfile, scan] = await Promise.all([
13180
+ const rootDir = path54.resolve(args.cwd);
13181
+ const [manifest, rawProfile, scan, env] = await Promise.all([
10433
13182
  loadAgentKitManifest(rootDir),
10434
13183
  readJson(
10435
- path52.join(rootDir, ".cursor", "agent-kit.config.json")
13184
+ path54.join(rootDir, ".cursor", "agent-kit.config.json")
10436
13185
  ),
10437
- runScanner(rootDir)
13186
+ runScanner(rootDir),
13187
+ assessEnvironment()
10438
13188
  ]);
10439
13189
  const readiness = createReadinessReport(scan, { generatorVersion: KIT_VERSION });
10440
13190
  const profile = profileStatus(rawProfile);
@@ -10470,6 +13220,17 @@ var statusCommand = defineCommand20({
10470
13220
  ` registry: ${manifest.registry?.url ?? "(default)"} @ ${manifest.registry?.ref ?? "(default)"}`
10471
13221
  );
10472
13222
  if (manifest.installedAt) console.log(` installed at: ${manifest.installedAt}`);
13223
+ if (manifest.version !== KIT_VERSION) {
13224
+ console.log(
13225
+ ` overlay: this CLI is v${KIT_VERSION}; apply with the same binary, not a stale PATH hit:`
13226
+ );
13227
+ console.log(` ${npxPinned(KIT_VERSION, "update")}`);
13228
+ }
13229
+ const pathStatus = pathCliStatus(env, KIT_VERSION);
13230
+ if (pathStatus === "behind" || pathStatus === "unknown") {
13231
+ console.log(` PATH bin: v${env.binVersion ?? "unknown"} at ${env.binPath}`);
13232
+ console.log(" bare `agent-kit update` will re-stamp that older version");
13233
+ }
10473
13234
  }
10474
13235
  console.log("Repository readiness");
10475
13236
  console.log(
@@ -10656,6 +13417,8 @@ ${err.recovery}
10656
13417
  logApplyStats(stats);
10657
13418
  const transition = next.version === existing.version ? `unchanged at v${next.version}` : `v${existing.version} \u2192 v${next.version}`;
10658
13419
  logger.success(`Update complete: ${transition} (L3 protected paths left untouched).`);
13420
+ const sync = await syncPathCliToRuntime({ runtimeVersion: KIT_VERSION });
13421
+ for (const line of sync.lines) console.log(line);
10659
13422
  } finally {
10660
13423
  await registry.unlock?.();
10661
13424
  }
@@ -10663,8 +13426,8 @@ ${err.recovery}
10663
13426
  });
10664
13427
 
10665
13428
  // src/commands/validate.ts
10666
- import { readFile as readFile27 } from "fs/promises";
10667
- import path53 from "path";
13429
+ import { readFile as readFile30 } from "fs/promises";
13430
+ import path55 from "path";
10668
13431
  import { defineCommand as defineCommand22 } from "citty";
10669
13432
 
10670
13433
  // src/invariants/plan-schema.ts
@@ -10697,7 +13460,7 @@ function validatePlanFrontmatterText(text) {
10697
13460
  cite: CITE5
10698
13461
  });
10699
13462
  }
10700
- const hasTodoItem = /^- id:\s*\S+/m.test(block);
13463
+ const hasTodoItem = /^\s*- id:\s*\S+/m.test(block);
10701
13464
  if (/^todos:/m.test(block) && !hasTodoItem && !/^todos:\s*\[\s*\]/m.test(block)) {
10702
13465
  warnings.push({
10703
13466
  code: "empty-todos",
@@ -10711,9 +13474,9 @@ function validatePlanFrontmatterText(text) {
10711
13474
  // src/commands/validate.ts
10712
13475
  async function resolveEditedPath(cwd, explicit) {
10713
13476
  if (explicit) {
10714
- const filePath2 = path53.resolve(cwd, explicit);
13477
+ const filePath2 = path55.resolve(cwd, explicit);
10715
13478
  try {
10716
- return { filePath: filePath2, content: await readFile27(filePath2, "utf8") };
13479
+ return { filePath: filePath2, content: await readFile30(filePath2, "utf8") };
10717
13480
  } catch {
10718
13481
  return null;
10719
13482
  }
@@ -10721,9 +13484,9 @@ async function resolveEditedPath(cwd, explicit) {
10721
13484
  const payload = await readStdinJson();
10722
13485
  const rel = typeof payload.file_path === "string" && payload.file_path || typeof payload.path === "string" && payload.path || typeof payload.file === "string" && payload.file || "";
10723
13486
  if (!rel) return null;
10724
- const filePath = path53.isAbsolute(rel) ? rel : path53.resolve(cwd, rel);
13487
+ const filePath = path55.isAbsolute(rel) ? rel : path55.resolve(cwd, rel);
10725
13488
  try {
10726
- return { filePath, content: await readFile27(filePath, "utf8") };
13489
+ return { filePath, content: await readFile30(filePath, "utf8") };
10727
13490
  } catch {
10728
13491
  return null;
10729
13492
  }
@@ -10751,15 +13514,21 @@ var validateCommand = defineCommand22({
10751
13514
  async run({ args }) {
10752
13515
  const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
10753
13516
  const fileArg = typeof args.file === "string" ? args.file : void 0;
10754
- const filePath = fileArg ? path53.resolve(cwd, fileArg) : path53.join(path53.resolve(cwd), ".cursor", "HANDOFF.md");
13517
+ const filePath = fileArg ? path55.resolve(cwd, fileArg) : path55.join(path55.resolve(cwd), ".cursor", "HANDOFF.md");
10755
13518
  let content = "";
10756
13519
  try {
10757
- content = await readFile27(filePath, "utf8");
13520
+ content = await readFile30(filePath, "utf8");
10758
13521
  } catch {
10759
13522
  console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
10760
13523
  return;
10761
13524
  }
10762
- const warnings = validateHandoffText(content);
13525
+ const warnings = [
13526
+ ...validateHandoffText(content),
13527
+ ...checkHandoffGrowth(content).map((w) => ({
13528
+ ...w,
13529
+ cite: "agent-kit handoff --prune (see .cursor/context/templates/handoff.md)"
13530
+ }))
13531
+ ];
10763
13532
  console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
10764
13533
  }
10765
13534
  }),
@@ -10778,10 +13547,10 @@ var validateCommand = defineCommand22({
10778
13547
  process.exitCode = 2;
10779
13548
  return;
10780
13549
  }
10781
- const filePath = path53.resolve(cwd, fileArg);
13550
+ const filePath = path55.resolve(cwd, fileArg);
10782
13551
  let content = "";
10783
13552
  try {
10784
- content = await readFile27(filePath, "utf8");
13553
+ content = await readFile30(filePath, "utf8");
10785
13554
  } catch {
10786
13555
  console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
10787
13556
  return;
@@ -10800,19 +13569,22 @@ var validateCommand = defineCommand22({
10800
13569
  },
10801
13570
  async run({ args }) {
10802
13571
  const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
10803
- const resolved = await resolveEditedPath(path53.resolve(cwd));
13572
+ const resolved = await resolveEditedPath(path55.resolve(cwd));
10804
13573
  if (!resolved) {
10805
13574
  console.log(JSON.stringify({}));
10806
13575
  return;
10807
13576
  }
10808
13577
  const { filePath, content } = resolved;
10809
13578
  if (isHandoffPath(filePath)) {
10810
- const warnings = validateHandoffText(content);
13579
+ const warnings = [
13580
+ ...validateHandoffText(content).map((w) => w.message),
13581
+ ...checkHandoffGrowth(content).map((w) => w.message)
13582
+ ];
10811
13583
  if (!warnings.length) {
10812
13584
  console.log(JSON.stringify({}));
10813
13585
  return;
10814
13586
  }
10815
- const msg = warnings.map((w) => w.message).join(" ");
13587
+ const msg = warnings.join(" ");
10816
13588
  console.log(
10817
13589
  JSON.stringify({
10818
13590
  user_message: msg,
@@ -10846,7 +13618,7 @@ var validateCommand = defineCommand22({
10846
13618
  import { bold as bold2, gray as gray4, underline } from "kolorist";
10847
13619
 
10848
13620
  // src/welcome/screen.ts
10849
- import { blue as blue2, cyan as cyan6, gray as gray3, lightCyan, trueColor as trueColor2, white as white2 } from "kolorist";
13621
+ import { blue as blue2, cyan as cyan6, gray as gray3, lightCyan, trueColor as trueColor3, white as white2 } from "kolorist";
10850
13622
  var HELMET_ASCII = [
10851
13623
  " ____",
10852
13624
  " .-' '-.",
@@ -10867,7 +13639,7 @@ function hexToRgb(hex) {
10867
13639
  }
10868
13640
  function outlineAnsi(line) {
10869
13641
  const [r, g, b] = hexToRgb(HELMET_OUTLINE);
10870
- return trueColor2(r, g, b)(line);
13642
+ return trueColor3(r, g, b)(line);
10871
13643
  }
10872
13644
  function hasCliSubcommand(rawArgs) {
10873
13645
  return Boolean(rawArgs?.some((arg) => !arg.startsWith("-")));
@@ -10913,7 +13685,7 @@ function renderWelcomeScreen(opts = {}) {
10913
13685
  })(),
10914
13686
  "",
10915
13687
  muted2(
10916
- "Chat HITL (start-project, git-staging/prod, run-plan-all) stays in Cursor slash commands."
13688
+ "Terminal: agent-kit run-plan-all (same as agent-kit run run-plan-all). Cursor chat: /run-plan-all."
10917
13689
  ),
10918
13690
  muted2(
10919
13691
  `${shouldUseVisualMotion(opts) ? SPACE_MARKS.star : SPACE_MARKS.tick} ${tipAt(
@@ -10938,7 +13710,7 @@ var CLI_HELP_GROUPS = [
10938
13710
  {
10939
13711
  id: "mission",
10940
13712
  title: "MISSION",
10941
- commands: ["handoff", "plan-index", "run", "run-plan"]
13713
+ commands: ["handoff", "plan-index", "run", "run-plan", "run-plan-all"]
10942
13714
  },
10943
13715
  {
10944
13716
  id: "dashboard",
@@ -11004,7 +13776,7 @@ async function renderGroupedRootHelp(cmd) {
11004
13776
  lines.push(
11005
13777
  g(`Use \`${name} <command> --help\` for more information about a command.`),
11006
13778
  g(
11007
- "Ask questions is Cursor-only. Use `agent-kit run <slash>` for headless numbered-list HITL. `/git-prod` stays operator-gated."
13779
+ "Ask questions is Cursor-only. Use `agent-kit run-plan-all` or `agent-kit run <slash>` for headless numbered-list HITL. `/git-prod` stays operator-gated."
11008
13780
  ),
11009
13781
  g(`${tipMark} ${tip}`),
11010
13782
  ""
@@ -11013,6 +13785,9 @@ async function renderGroupedRootHelp(cmd) {
11013
13785
  }
11014
13786
 
11015
13787
  // src/index.ts
13788
+ var rewrittenArgv = rewriteRootArgvToRun(process.argv.slice(2));
13789
+ process.argv.length = 2;
13790
+ process.argv.push(...rewrittenArgv);
11016
13791
  var main = defineCommand23({
11017
13792
  meta: {
11018
13793
  name: "agent-kit",
@@ -11035,6 +13810,7 @@ var main = defineCommand23({
11035
13810
  "plan-index": planIndexCommand,
11036
13811
  run: runCommand,
11037
13812
  "run-plan": runPlanCommand,
13813
+ "run-plan-all": runPlanAllCommand,
11038
13814
  dashboard: dashboardCommand,
11039
13815
  "dashboard-broadcast": dashboardBroadcastCommand,
11040
13816
  "mission-control": missionControlCommand,