@dadado/agent-kit-cli 5.9.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 (2) hide show
  1. package/dist/index.js +2750 -250
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -274,7 +274,73 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
274
274
  "481a9994ccecaa611173f9ef91a1dbe7458d89758c6a6b5e7c3bbbbf460888ce",
275
275
  "691d91fd1e9032b6f9315f29348d4473201917da257258afd74eb954298d8d71",
276
276
  "ab0808b7dbc333e523b87b126bb655353cde2574db30d849cc9c9037843bef8c",
277
- "85514e26af694bbe38e7ddf625710a32719ddf6afc173843c9cdfbffdad8e466"
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"
278
344
  ]);
279
345
 
280
346
  // src/lifecycle/paths.ts
@@ -1521,6 +1587,54 @@ var L0_ARTIFACTS = [
1521
1587
  source: ".cursor/rules/hitl-ask-questions.mdc",
1522
1588
  target: ".cursor/rules/hitl-ask-questions.mdc"
1523
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
+ },
1524
1638
  {
1525
1639
  source: "registry/rules/git-secrets-safety.mdc",
1526
1640
  target: ".cursor/rules/git-secrets-safety.mdc"
@@ -1590,11 +1704,8 @@ var L0_ARTIFACTS = [
1590
1704
  target: ".cursor/commands/git-staging.md"
1591
1705
  },
1592
1706
  { source: ".cursor/commands/git-prod.md", target: ".cursor/commands/git-prod.md" },
1593
- {
1594
- source: ".cursor/commands/kit-staging.md",
1595
- target: ".cursor/commands/kit-staging.md"
1596
- },
1597
- { 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).
1598
1709
  {
1599
1710
  source: ".cursor/commands/plan-external-review.md",
1600
1711
  target: ".cursor/commands/plan-external-review.md"
@@ -1618,6 +1729,10 @@ var L0_ARTIFACTS = [
1618
1729
  source: ".cursor/commands/cursor-update-awareness.md",
1619
1730
  target: ".cursor/commands/cursor-update-awareness.md"
1620
1731
  },
1732
+ {
1733
+ source: ".cursor/commands/qa.md",
1734
+ target: ".cursor/commands/qa.md"
1735
+ },
1621
1736
  {
1622
1737
  source: ".cursor/commands/update.md",
1623
1738
  target: ".cursor/commands/update.md"
@@ -2168,6 +2283,94 @@ function parseFeatureMapNames(markdown) {
2168
2283
  }
2169
2284
  return names;
2170
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
+ }
2171
2374
  async function defaultFetchText(url) {
2172
2375
  if (!url.startsWith("https://")) {
2173
2376
  throw new Error(`Refusing non-HTTPS changelog URL: ${url}`);
@@ -2266,6 +2469,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2266
2469
  const inventoryRefreshed = parseInventoryRefreshed(inventoryMd);
2267
2470
  const openActionIds = parseOpenActionIds(inventoryMd);
2268
2471
  const gaps = [];
2472
+ let featuresMd = "";
2269
2473
  for (const id of openActionIds) {
2270
2474
  gaps.push({
2271
2475
  id: `open-action-${id}`,
@@ -2291,7 +2495,7 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2291
2495
  }
2292
2496
  }
2293
2497
  try {
2294
- const featuresMd = await readFile7(featuresPath, "utf8");
2498
+ featuresMd = await readFile7(featuresPath, "utf8");
2295
2499
  if (parseFeatureMapNames(featuresMd).length === 0) {
2296
2500
  gaps.push({
2297
2501
  id: "features-map-empty",
@@ -2315,6 +2519,23 @@ async function checkCursorUpdateAwareness(cwd, options = {}) {
2315
2519
  try {
2316
2520
  const body = options.changelogBody ?? await (options.fetchText ?? defaultFetchText)(changelogUrl);
2317
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
+ }
2318
2539
  if (latestCursorVersion && prefs.lastSeenCursorVersion && compareCursorVersion(latestCursorVersion, prefs.lastSeenCursorVersion) > 0) {
2319
2540
  gaps.push({
2320
2541
  id: "changelog-ahead",
@@ -2534,13 +2755,13 @@ function readPreferredBrowserFromConfig(configPath, fsHooks = {}) {
2534
2755
  }
2535
2756
 
2536
2757
  // src/commands/dashboard.ts
2537
- function readPreferredBrowserFromWorkspace(cwd, readFile28 = readFileSync2) {
2758
+ function readPreferredBrowserFromWorkspace(cwd, readFile31 = readFileSync2) {
2538
2759
  const resolved = resolveContextConfigPath(path11.resolve(cwd), {
2539
2760
  existsSync,
2540
2761
  realpathSync
2541
2762
  });
2542
2763
  if (!resolved.ok) return null;
2543
- const value = readPreferredBrowserFromConfig(resolved.path, { readFileSync: readFile28 });
2764
+ const value = readPreferredBrowserFromConfig(resolved.path, { readFileSync: readFile31 });
2544
2765
  return normalizePreferredBrowser(value);
2545
2766
  }
2546
2767
  function applyDashboardOpenEnv(env, opts) {
@@ -4016,21 +4237,30 @@ function buildPillars(scan) {
4016
4237
  )
4017
4238
  ]),
4018
4239
  pillar("quality-ci", [
4019
- check(
4020
- "quality.validation",
4021
- "Tests and validation",
4022
- scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? "ready" : "manual",
4023
- false,
4024
- scan.infra.ciFiles.map((value) => ({ source: "file", value })),
4025
- scan.quality.hasTests || scan.quality.validationCommands.length > 0 ? [] : [
4026
- action(
4027
- "document-validation",
4028
- "manual",
4029
- "Document a repeatable repository validation command",
4030
- "user"
4031
- )
4032
- ]
4033
- )
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
+ })()
4034
4264
  ]),
4035
4265
  pillar("deploy-infrastructure", [
4036
4266
  check(
@@ -4104,7 +4334,7 @@ function createReadinessReport(scan, options) {
4104
4334
  }
4105
4335
 
4106
4336
  // src/scanner/safe-fixes.ts
4107
- import { readFile as readFile13, writeFile as writeFile5 } from "fs/promises";
4337
+ import { readFile as readFile15, writeFile as writeFile5 } from "fs/promises";
4108
4338
  import path24 from "path";
4109
4339
 
4110
4340
  // src/scanner/detect-repository.ts
@@ -4477,6 +4707,7 @@ async function detectIde(rootDir) {
4477
4707
  }
4478
4708
 
4479
4709
  // src/scanner/detect-infra.ts
4710
+ import { readFile as readFile12, readdir as readdir5 } from "fs/promises";
4480
4711
  import path20 from "path";
4481
4712
 
4482
4713
  // src/types.ts
@@ -4532,6 +4763,51 @@ var PM_TOOL_LABELS = {
4532
4763
  };
4533
4764
 
4534
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
+ }
4535
4811
  async function detectInfra(rootDir) {
4536
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"));
4537
4813
  const kubernetes = await fileExists(path20.join(rootDir, "k8s")) || await fileExists(path20.join(rootDir, "kubernetes"));
@@ -4571,11 +4847,12 @@ async function detectInfra(rootDir) {
4571
4847
  async (file) => await fileExists(path20.join(rootDir, file)) ? file : void 0
4572
4848
  )
4573
4849
  )).filter((file) => file !== void 0);
4574
- 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 };
4575
4852
  }
4576
4853
 
4577
4854
  // src/scanner/detect-services.ts
4578
- import { readFile as readFile12 } from "fs/promises";
4855
+ import { readFile as readFile13 } from "fs/promises";
4579
4856
  import path21 from "path";
4580
4857
  async function detectProjectManagement(rootDir) {
4581
4858
  const tools = [];
@@ -4586,7 +4863,7 @@ async function detectProjectManagement(rootDir) {
4586
4863
  for (const configPath of mcpConfigPaths) {
4587
4864
  if (!await fileExists(configPath)) continue;
4588
4865
  try {
4589
- const raw = await readFile12(configPath, "utf8");
4866
+ const raw = await readFile13(configPath, "utf8");
4590
4867
  const lower = raw.toLowerCase();
4591
4868
  if (lower.includes("clickup")) tools.push("clickup");
4592
4869
  if (lower.includes("jira") || lower.includes("atlassian")) tools.push("jira");
@@ -4622,6 +4899,7 @@ async function detectServices(rootDir) {
4622
4899
  }
4623
4900
 
4624
4901
  // src/scanner/detect-stack.ts
4902
+ import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
4625
4903
  import path22 from "path";
4626
4904
  var PROJECT_MARKERS = [
4627
4905
  "package.json",
@@ -4633,8 +4911,152 @@ var PROJECT_MARKERS = [
4633
4911
  "build.gradle",
4634
4912
  "composer.json",
4635
4913
  "Cargo.toml",
4636
- "pnpm-workspace.yaml"
4914
+ "pnpm-workspace.yaml",
4915
+ "pubspec.yaml"
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 ."
4637
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
+ }
4638
5060
  var LOCKFILES = [
4639
5061
  ["pnpm-lock.yaml", "pnpm"],
4640
5062
  ["yarn.lock", "yarn"],
@@ -4668,7 +5090,15 @@ function commandsForScripts(scripts, packageManager) {
4668
5090
  return { testCommands, validationCommands };
4669
5091
  }
4670
5092
  async function detectStack(rootDir) {
5093
+ const confirmed = await readConfirmedStack(rootDir);
5094
+ if (confirmed) return confirmed;
4671
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
+ }
4672
5102
  const hasPackageJson = await fileExists(path22.join(rootDir, "package.json"));
4673
5103
  if (hasPackageJson) {
4674
5104
  const packageJson = await readJson(path22.join(rootDir, "package.json")) ?? {};
@@ -4728,6 +5158,17 @@ async function detectStack(rootDir) {
4728
5158
  hasProjectFiles: hasAnyProjectMarker
4729
5159
  };
4730
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
+ }
4731
5172
  return {
4732
5173
  language: "unknown",
4733
5174
  workspaces: false,
@@ -4782,7 +5223,8 @@ async function runScanner(rootDir) {
4782
5223
  testCommands: stack.testCommands,
4783
5224
  validationCommands: stack.validationCommands,
4784
5225
  ci: infra.ci,
4785
- hasTests: stack.testCommands.length > 0
5226
+ hasTests: stack.testCommands.length > 0,
5227
+ ciRunCommands: infra.ciRunCommands
4786
5228
  }
4787
5229
  };
4788
5230
  }
@@ -4855,9 +5297,12 @@ function createOnboardingState(report, generatedAt, deferredItems = report.defer
4855
5297
  const validDeferredCheckIds = new Set(
4856
5298
  deferredItems.filter((item) => item.reason.trim().length > 0).map((item) => item.checkId)
4857
5299
  );
4858
- const unresolvedEssential = report.pillars.flatMap((pillar2) => pillar2.checks).some(
4859
- (check2) => check2.essential && check2.status !== "ready" && (check2.status === "blocked" || !validDeferredCheckIds.has(check2.id))
4860
- );
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
+ });
4861
5306
  return {
4862
5307
  contractVersion: 1,
4863
5308
  status: unresolvedEssential ? "in_progress" : "completed",
@@ -5008,7 +5453,7 @@ async function executeSafeReadinessFixes(rootDir, options) {
5008
5453
  }
5009
5454
  const gitignoreRelativePath = ".gitignore";
5010
5455
  const gitignorePath = path24.join(beforeScan.rootDir, gitignoreRelativePath);
5011
- const existingGitignore = await fileExists(gitignorePath) ? await readFile13(gitignorePath, "utf8") : "";
5456
+ const existingGitignore = await fileExists(gitignorePath) ? await readFile15(gitignorePath, "utf8") : "";
5012
5457
  const gitignoreAfterSecrets = mergeSecretIgnores(existingGitignore);
5013
5458
  const secretsChanged = gitignoreAfterSecrets !== existingGitignore;
5014
5459
  const mergedGitignore = mergeKitOwnedIgnores(gitignoreAfterSecrets);
@@ -5123,11 +5568,12 @@ var KIT_TIPS = [
5123
5568
  "agent-kit --help groups SETUP, MISSION, DASHBOARD, INTEGRITY."
5124
5569
  ];
5125
5570
  function shouldUseWelcomeColor(opts = {}) {
5571
+ const env = opts.env ?? process.env;
5126
5572
  if (opts.color === false) return false;
5127
- if (process.env.NO_COLOR) return false;
5128
- if (process.env.NODE_DISABLE_COLORS) return false;
5129
- if (process.env.FORCE_COLOR === "0") return false;
5130
- 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;
5131
5577
  if (opts.color === true) return true;
5132
5578
  const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
5133
5579
  return tty;
@@ -5280,6 +5726,19 @@ async function withCliProgress(label, fn, opts) {
5280
5726
  }
5281
5727
 
5282
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
+ }
5283
5742
  async function runDoctor(cwd, options = {}) {
5284
5743
  const rootDir = path26.resolve(cwd);
5285
5744
  const hooks = await assessHooksHealth(rootDir);
@@ -5290,7 +5749,13 @@ async function runDoctor(cwd, options = {}) {
5290
5749
  generatedAt: options.generatedAt
5291
5750
  });
5292
5751
  await writeReadinessSnapshot(rootDir, execution.after);
5293
- 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
+ };
5294
5759
  }
5295
5760
  if (options.refreshProfile) {
5296
5761
  const refresh = await refreshRepositoryProfile(rootDir, {
@@ -5303,7 +5768,14 @@ async function runDoctor(cwd, options = {}) {
5303
5768
  generatedAt: options.generatedAt
5304
5769
  });
5305
5770
  await writeReadinessSnapshot(rootDir, report2);
5306
- 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
+ };
5307
5779
  }
5308
5780
  const scan = await runScanner(rootDir);
5309
5781
  const report = createReadinessReport(scan, {
@@ -5311,7 +5783,13 @@ async function runDoctor(cwd, options = {}) {
5311
5783
  generatedAt: options.generatedAt
5312
5784
  });
5313
5785
  await writeReadinessSnapshot(rootDir, report);
5314
- return { report, safeChanges: [], hooks, env };
5786
+ return {
5787
+ report,
5788
+ safeChanges: [],
5789
+ hooks,
5790
+ env,
5791
+ deferredEssentials: await readDeferredEssentials(rootDir, report)
5792
+ };
5315
5793
  }
5316
5794
  function printDoctorSummary(result) {
5317
5795
  const { summary, pendingActions } = result.report;
@@ -5328,6 +5806,9 @@ function printDoctorSummary(result) {
5328
5806
  ` profile refreshed: ${result.profileRefreshed ? "yes (facts changed)" : "no (already current)"}`
5329
5807
  );
5330
5808
  }
5809
+ for (const item of result.deferredEssentials) {
5810
+ console.log(` Deferred essential: ${item.checkId} \u2014 ${item.reason}`);
5811
+ }
5331
5812
  console.log(`hooks: ${result.hooks.status}`);
5332
5813
  if (result.hooks.reasons.length > 0) {
5333
5814
  for (const reason of result.hooks.reasons.slice(0, 5)) {
@@ -5787,9 +6268,187 @@ var guardCommand = defineCommand8({
5787
6268
 
5788
6269
  // src/commands/handoff.ts
5789
6270
  import { spawn as spawn4 } from "child_process";
5790
- 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";
5791
6272
  import path27 from "path";
5792
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
+ }
5793
6452
  function parsePlanFrontmatter(raw) {
5794
6453
  const match = raw.match(/^---\n([\s\S]*?)\n---/);
5795
6454
  if (!match?.[1]) return null;
@@ -5806,15 +6465,15 @@ function parsePlanFrontmatter(raw) {
5806
6465
  }
5807
6466
  async function findActivePlan(plansDir) {
5808
6467
  if (!await fileExists(plansDir)) return null;
5809
- 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();
5810
6469
  for (const file of files) {
5811
- const raw = await readFile14(path27.join(plansDir, file), "utf8");
6470
+ const raw = await readFile16(path27.join(plansDir, file), "utf8");
5812
6471
  const fm = parsePlanFrontmatter(raw);
5813
6472
  if (fm?.todos?.some((t) => t.status !== "completed" && t.status !== "cancelled")) {
5814
6473
  return { file, raw };
5815
6474
  }
5816
6475
  }
5817
- 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;
5818
6477
  }
5819
6478
  function now() {
5820
6479
  return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16);
@@ -5916,9 +6575,24 @@ var handoffCommand = defineCommand9({
5916
6575
  type: "string",
5917
6576
  description: "Project root directory",
5918
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}).`
5919
6587
  }
5920
6588
  },
5921
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
+ }
5922
6596
  const profile = await loadProfile(args.cwd);
5923
6597
  const plansDir = path27.join(args.cwd, ".cursor", "plans");
5924
6598
  const handoffPath = path27.join(args.cwd, ".cursor", "HANDOFF.md");
@@ -5997,7 +6671,7 @@ function buildPreCompactUserMessage(payload = {}) {
5997
6671
 
5998
6672
  // src/hooks/session-start.ts
5999
6673
  import { spawn as spawn5 } from "child_process";
6000
- 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";
6001
6675
  import path29 from "path";
6002
6676
 
6003
6677
  // src/invariants/handoff-schema.ts
@@ -6040,7 +6714,7 @@ function validateHandoffText(text) {
6040
6714
  }
6041
6715
 
6042
6716
  // src/plan-index/plan-index.ts
6043
- import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
6717
+ import { readFile as readFile17, writeFile as writeFile7 } from "fs/promises";
6044
6718
  import path28 from "path";
6045
6719
  var PLAN_INDEX_REL = ".cursor/context/plan-index.json";
6046
6720
  var NONE = /^(none|n\/a|empty|nil)$/i;
@@ -6049,7 +6723,7 @@ function defaultPlanIndexIo() {
6049
6723
  return {
6050
6724
  async readFile(absPath) {
6051
6725
  try {
6052
- return await readFile15(absPath, "utf8");
6726
+ return await readFile17(absPath, "utf8");
6053
6727
  } catch {
6054
6728
  return null;
6055
6729
  }
@@ -6220,6 +6894,137 @@ function extractPlanBasenames(raw) {
6220
6894
  return ids;
6221
6895
  }
6222
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
+
6223
7028
  // src/hooks/hard-rules.ts
6224
7029
  var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
6225
7030
 
@@ -6229,7 +7034,7 @@ var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
6229
7034
  4. **Read HANDOFF first** when resuming. Do not restart the plan from scratch.
6230
7035
  5. **Git:** suggest \`/git-staging\` after a phase with a diff; never \`/git-prod\` without explicit confirmation.
6231
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.
6232
- 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.
6233
7038
  8. **\`/continue-plan\` waits for yes.** Summarize next \`[to-do-id]\`, then stop until the user confirms before editing.
6234
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.
6235
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.
@@ -6252,17 +7057,9 @@ Run \`/cursor-update-awareness\` (or \`agent-kit cursor-awareness --check\`) and
6252
7057
  // src/hooks/session-start.ts
6253
7058
  var NONE_PLACEHOLDERS = /* @__PURE__ */ new Set(["none", "n/a", "empty", "nil"]);
6254
7059
  var CURSOR_AWARENESS_SPAWN_TIMEOUT_MS = CHANGELOG_FETCH_TIMEOUT_MS + 3e3;
6255
- async function readTextLimited(filePath, limit = 60) {
6256
- try {
6257
- const lines = (await readFile16(filePath, "utf8")).split(/\r?\n/);
6258
- return lines.slice(0, limit).join("\n").trim();
6259
- } catch {
6260
- return "";
6261
- }
6262
- }
6263
7060
  async function readFull(filePath) {
6264
7061
  try {
6265
- return await readFile16(filePath, "utf8");
7062
+ return await readFile18(filePath, "utf8");
6266
7063
  } catch {
6267
7064
  return "";
6268
7065
  }
@@ -6379,7 +7176,7 @@ async function readinessSection(root) {
6379
7176
  const snapshotPath = path29.join(root, ".cursor", "context", "readiness.json");
6380
7177
  let data;
6381
7178
  try {
6382
- data = JSON.parse(await readFile16(snapshotPath, "utf8"));
7179
+ data = JSON.parse(await readFile18(snapshotPath, "utf8"));
6383
7180
  } catch {
6384
7181
  return null;
6385
7182
  }
@@ -6416,7 +7213,7 @@ async function dogfoodInboxSection(root) {
6416
7213
  for (const readme of candidateReadmes) {
6417
7214
  if (!await fileExists2(readme)) continue;
6418
7215
  try {
6419
- const text = await readFile16(readme, "utf8");
7216
+ const text = await readFile18(readme, "utf8");
6420
7217
  if (parseUnprocessedDogfoodItems(text).length) return DOGFOOD_INBOX_HINT;
6421
7218
  } catch {
6422
7219
  }
@@ -6426,7 +7223,7 @@ async function dogfoodInboxSection(root) {
6426
7223
  async function loadUpdateCheckPrefs(root) {
6427
7224
  try {
6428
7225
  const data = JSON.parse(
6429
- await readFile16(path29.join(root, ".cursor", "context", "config.json"), "utf8")
7226
+ await readFile18(path29.join(root, ".cursor", "context", "config.json"), "utf8")
6430
7227
  );
6431
7228
  const uc = data.updateCheck;
6432
7229
  if (!uc || typeof uc !== "object" || uc.enabled !== true) {
@@ -6504,7 +7301,7 @@ async function updateCheckSection(root) {
6504
7301
  async function loadCursorUpdateCheckPrefs(root) {
6505
7302
  try {
6506
7303
  const data = JSON.parse(
6507
- await readFile16(path29.join(root, ".cursor", "context", "config.json"), "utf8")
7304
+ await readFile18(path29.join(root, ".cursor", "context", "config.json"), "utf8")
6508
7305
  );
6509
7306
  const uc = data.cursorUpdateCheck;
6510
7307
  if (!uc || typeof uc !== "object" || uc.enabled !== true) {
@@ -6699,7 +7496,7 @@ async function buildSessionStartAdditionalContext(rootDir, _payload = {}) {
6699
7496
  const root = path29.resolve(rootDir);
6700
7497
  const handoffPath = path29.join(root, ".cursor", "HANDOFF.md");
6701
7498
  const handoffFull = await readFull(handoffPath);
6702
- const handoff = await readTextLimited(handoffPath);
7499
+ const handoff = capHandoffExcerpt(handoffFull);
6703
7500
  const parts = [HARD_RULES];
6704
7501
  if (await l0Present(root)) {
6705
7502
  const readiness = await readinessSection(root);
@@ -6724,10 +7521,14 @@ ${bullet}
6724
7521
  Rewrite machine lists as \`- **Field:**\` bullets before trusting Checklist / Current mission.`
6725
7522
  );
6726
7523
  }
6727
- 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._` : "";
6728
7529
  parts.push(`## Current HANDOFF.md (excerpt)
6729
7530
 
6730
- ${handoff}`);
7531
+ ${handoff.text}${note}`);
6731
7532
  } else {
6732
7533
  parts.push(
6733
7534
  "## HANDOFF.md\n\nNo handoff file yet. If starting work, create a plan with to-dos first (`/start-project`)."
@@ -6816,7 +7617,7 @@ import { spawn as spawn6 } from "child_process";
6816
7617
  import path32 from "path";
6817
7618
 
6818
7619
  // src/utils/terminal.ts
6819
- import { readdir as readdir6 } from "fs/promises";
7620
+ import { readdir as readdir8 } from "fs/promises";
6820
7621
  import { homedir as homedir3 } from "os";
6821
7622
  import path31 from "path";
6822
7623
  import { confirm, isCancel } from "@clack/prompts";
@@ -6830,7 +7631,7 @@ var NESTED_REPO_SCAN_LIMIT = 200;
6830
7631
  async function findNestedRepoChildren(resolved) {
6831
7632
  let entries;
6832
7633
  try {
6833
- entries = await readdir6(resolved, { withFileTypes: true });
7634
+ entries = await readdir8(resolved, { withFileTypes: true });
6834
7635
  } catch {
6835
7636
  return [];
6836
7637
  }
@@ -7110,11 +7911,11 @@ import { defineCommand as defineCommand11 } from "citty";
7110
7911
  import { bold, cyan as cyan3, green as green2, options as koloristOptions2 } from "kolorist";
7111
7912
 
7112
7913
  // src/generator/personalization.ts
7113
- import { readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
7914
+ import { readFile as readFile21, writeFile as writeFile12 } from "fs/promises";
7114
7915
  import path37 from "path";
7115
7916
 
7116
7917
  // src/generator/claude-command-adapters.ts
7117
- import { readFile as readFile17, readdir as readdir7, writeFile as writeFile8 } from "fs/promises";
7918
+ import { readFile as readFile19, readdir as readdir9, writeFile as writeFile8 } from "fs/promises";
7118
7919
  import path33 from "path";
7119
7920
  var CURSOR_COMMANDS_DIR_REL = ".cursor/commands";
7120
7921
  var CLAUDE_COMMANDS_DIR_REL = ".claude/commands";
@@ -7137,7 +7938,7 @@ description: ${command.description}
7137
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.
7138
7939
 
7139
7940
  Adapter rules (Claude Code CLI):
7140
- - 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.
7141
7942
  - Skip or cancel means stop.
7142
7943
  - Never \`/git-prod\` without an explicit operator yes.
7143
7944
  - Do not clone Cursor hooks or invent behavior beyond the SoT file.
@@ -7147,7 +7948,7 @@ async function discoverInstalledCommands(rootDir) {
7147
7948
  const dir = path33.join(rootDir, CURSOR_COMMANDS_DIR_REL);
7148
7949
  let entries;
7149
7950
  try {
7150
- entries = (await readdir7(dir)).filter((f) => f.endsWith(".md"));
7951
+ entries = (await readdir9(dir)).filter((f) => f.endsWith(".md"));
7151
7952
  } catch {
7152
7953
  return [];
7153
7954
  }
@@ -7156,7 +7957,7 @@ async function discoverInstalledCommands(rootDir) {
7156
7957
  const name = file.slice(0, -3);
7157
7958
  if (RESERVED_ADAPTER_NAMES.has(name)) continue;
7158
7959
  try {
7159
- const raw = await readFile17(path33.join(dir, file), "utf8");
7960
+ const raw = await readFile19(path33.join(dir, file), "utf8");
7160
7961
  const parsed = parseCommandFrontmatter(name, raw);
7161
7962
  if (parsed) commands.push(parsed);
7162
7963
  } catch {
@@ -7182,7 +7983,7 @@ async function generateClaudeCommandAdapters(rootDir) {
7182
7983
  results.push({ relativePath: relPath, status: "applied" });
7183
7984
  continue;
7184
7985
  }
7185
- const localContent = await readFile17(abs, "utf8");
7986
+ const localContent = await readFile19(abs, "utf8");
7186
7987
  if (localContent === rendered) {
7187
7988
  if (ledger.hashes[relPath] !== contentHash(rendered)) {
7188
7989
  ledger.hashes[relPath] = contentHash(rendered);
@@ -7278,7 +8079,7 @@ async function generateClaudeKitLoadArtifacts(rootDir) {
7278
8079
  }
7279
8080
 
7280
8081
  // src/generator/claude-session-start-hook.ts
7281
- import { readFile as readFile18, writeFile as writeFile10 } from "fs/promises";
8082
+ import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
7282
8083
  import path35 from "path";
7283
8084
  var CLAUDE_SETTINGS_REL = ".claude/settings.json";
7284
8085
  var RESOLVE_AGENT_KIT_REL = ".cursor/hooks/agent/resolve-agent-kit.sh";
@@ -7357,7 +8158,7 @@ async function writeClaudeSessionStartHook(rootDir) {
7357
8158
  const abs = path35.join(rootDir, CLAUDE_SETTINGS_REL);
7358
8159
  let existing = null;
7359
8160
  try {
7360
- existing = await readFile18(abs, "utf8");
8161
+ existing = await readFile20(abs, "utf8");
7361
8162
  } catch {
7362
8163
  existing = null;
7363
8164
  }
@@ -7881,12 +8682,12 @@ async function applyPersonalization(input) {
7881
8682
  async function readRepositoryProfile(rootDir) {
7882
8683
  const target = path37.join(rootDir, ".cursor/agent-kit.config.json");
7883
8684
  if (!await fileExists(target)) return null;
7884
- return JSON.parse(await readFile19(target, "utf8"));
8685
+ return JSON.parse(await readFile21(target, "utf8"));
7885
8686
  }
7886
8687
 
7887
8688
  // src/lifecycle/onboard-migration.ts
7888
8689
  import { createHash as createHash4 } from "crypto";
7889
- import { readFile as readFile20, unlink } from "fs/promises";
8690
+ import { readFile as readFile22, unlink } from "fs/promises";
7890
8691
  import path38 from "path";
7891
8692
  var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
7892
8693
  var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
@@ -7898,7 +8699,7 @@ async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_
7898
8699
  if (!await fileExists(legacyPath)) return "absent";
7899
8700
  const namespacedPath = path38.join(projectRoot, NAMESPACED_ONBOARD_PATH);
7900
8701
  if (!await fileExists(namespacedPath)) return "preserved-customized";
7901
- const content = await readFile20(legacyPath);
8702
+ const content = await readFile22(legacyPath);
7902
8703
  const hash = createHash4("sha256").update(content).digest("hex");
7903
8704
  if (!managedHashes.has(hash)) return "preserved-customized";
7904
8705
  await unlink(legacyPath);
@@ -8433,32 +9234,354 @@ function shouldLiveRefresh(opts) {
8433
9234
  const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
8434
9235
  return tty;
8435
9236
  }
8436
-
8437
- // src/mission-control/snapshot.ts
8438
- import { execFile as execFile6 } from "child_process";
8439
- import { access as access8 } from "fs/promises";
8440
- import path40 from "path";
8441
-
8442
- // ../../dashboard/lib/live-refresh.mjs
8443
- import { join, resolve as resolve2 } from "path";
8444
- var PERIODIC_REFRESH_MS = 15e3;
8445
- var SNAPSHOT_REPO_SOURCE_RELS = Object.freeze([
8446
- ".cursor/plans",
8447
- ".cursor/HANDOFF.md",
8448
- ".cursor/agents",
8449
- ".cursor/commands",
8450
- ".cursor/memory",
8451
- ".cursor/context/config.json",
8452
- ".cursor/context/current",
8453
- ".cursor/context/readiness.json",
8454
- ".cursor/context/field-report-dismissals.json",
8455
- ".cursor/context/mission-timing.json",
8456
- ".cursor/context/flight-log.json",
8457
- ".cursor/context/field-report-cadence.json",
8458
- ".cursor/skills",
8459
- "package.json"
8460
- ]);
8461
-
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
+ }
9559
+
9560
+ // src/mission-control/snapshot.ts
9561
+ import { execFile as execFile6 } from "child_process";
9562
+ import { access as access8 } from "fs/promises";
9563
+ import path40 from "path";
9564
+
9565
+ // ../../dashboard/lib/live-refresh.mjs
9566
+ import { join, resolve as resolve2 } from "path";
9567
+ var PERIODIC_REFRESH_MS = 15e3;
9568
+ var SNAPSHOT_REPO_SOURCE_RELS = Object.freeze([
9569
+ ".cursor/plans",
9570
+ ".cursor/HANDOFF.md",
9571
+ ".cursor/agents",
9572
+ ".cursor/commands",
9573
+ ".cursor/memory",
9574
+ ".cursor/context/config.json",
9575
+ ".cursor/context/current",
9576
+ ".cursor/context/readiness.json",
9577
+ ".cursor/context/field-report-dismissals.json",
9578
+ ".cursor/context/mission-timing.json",
9579
+ ".cursor/context/flight-log.json",
9580
+ ".cursor/context/field-report-cadence.json",
9581
+ ".cursor/skills",
9582
+ "package.json"
9583
+ ]);
9584
+
8462
9585
  // src/mission-control/snapshot.ts
8463
9586
  var DEFAULT_DATA_TIMEOUT_MS = 6e4;
8464
9587
  function dataScriptTimeoutMs(env = process.env) {
@@ -8636,11 +9759,14 @@ function buildMcTuiView(snapshot, error = null) {
8636
9759
  var CLEAR_HOME = "\x1B[2J\x1B[H";
8637
9760
  var HIDE_CURSOR = "\x1B[?25l";
8638
9761
  var SHOW_CURSOR = "\x1B[?25h";
8639
- function isQuitKey(chunk) {
9762
+ function quitKeyOf(chunk) {
8640
9763
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
8641
- return text === "q" || text === "Q" || text === "";
9764
+ if (text === "q" || text === "Q") return "q";
9765
+ if (text === "") return "Ctrl-C";
9766
+ return null;
8642
9767
  }
8643
9768
  async function runMcTuiLoop(opts) {
9769
+ const render = opts.render ?? ((view, renderOpts) => renderMcTui(view, renderOpts));
8644
9770
  const live = shouldLiveRefresh({
8645
9771
  once: opts.once,
8646
9772
  stdoutIsTTY: opts.stdoutIsTTY,
@@ -8648,12 +9774,14 @@ async function runMcTuiLoop(opts) {
8648
9774
  });
8649
9775
  let frameIndex = 0;
8650
9776
  let inFlight = false;
8651
- const paint2 = async (clear) => {
8652
- if (inFlight) return;
9777
+ let stopped = false;
9778
+ const paint3 = async (clear) => {
9779
+ if (inFlight || stopped) return;
8653
9780
  inFlight = true;
8654
9781
  try {
8655
9782
  const view = await opts.loadView();
8656
- const frame = renderMcTui(view, {
9783
+ if (stopped) return;
9784
+ const frame = render(view, {
8657
9785
  ...opts.renderOpts,
8658
9786
  stdoutIsTTY: opts.stdoutIsTTY,
8659
9787
  frameIndex
@@ -8663,6 +9791,7 @@ async function runMcTuiLoop(opts) {
8663
9791
  ` : `${frame}
8664
9792
  `);
8665
9793
  } catch (err) {
9794
+ if (stopped) return;
8666
9795
  const message = err instanceof Error ? err.message : String(err);
8667
9796
  const frame = renderMcTui(buildMcTuiView(null, message), {
8668
9797
  ...opts.renderOpts,
@@ -8677,7 +9806,7 @@ async function runMcTuiLoop(opts) {
8677
9806
  inFlight = false;
8678
9807
  }
8679
9808
  };
8680
- await paint2(false);
9809
+ await paint3(false);
8681
9810
  if (!live) return null;
8682
9811
  opts.hooks.write(HIDE_CURSOR);
8683
9812
  const intervalMs = opts.intervalMs ?? PERIODIC_REFRESH_MS;
@@ -8686,7 +9815,7 @@ async function runMcTuiLoop(opts) {
8686
9815
  let timer;
8687
9816
  try {
8688
9817
  timer = setInt(() => {
8689
- void paint2(true);
9818
+ void paint3(true);
8690
9819
  }, intervalMs);
8691
9820
  } catch (err) {
8692
9821
  opts.hooks.write(SHOW_CURSOR);
@@ -8695,7 +9824,6 @@ async function runMcTuiLoop(opts) {
8695
9824
  const stdin = opts.hooks.stdin;
8696
9825
  const attachQuit = Boolean(stdin?.isTTY);
8697
9826
  let quitListener;
8698
- let stopped = false;
8699
9827
  const restoreStdin = () => {
8700
9828
  if (!stdin || !attachQuit) return;
8701
9829
  if (quitListener) {
@@ -8719,14 +9847,24 @@ async function runMcTuiLoop(opts) {
8719
9847
  stdin.setRawMode?.(true);
8720
9848
  stdin.resume?.();
8721
9849
  quitListener = (chunk) => {
8722
- if (!isQuitKey(chunk)) return;
9850
+ if (stopped) return;
9851
+ if (opts.hooks.onKey?.(chunk)) return;
9852
+ const key = quitKeyOf(chunk);
9853
+ if (!key) return;
8723
9854
  stop();
9855
+ if (opts.hooks.onQuit) {
9856
+ opts.hooks.onQuit(key);
9857
+ return;
9858
+ }
8724
9859
  const exitFn = opts.hooks.exit ?? ((code) => process.exit(code));
8725
9860
  exitFn(0);
8726
9861
  };
8727
9862
  stdin.on("data", quitListener);
8728
9863
  }
8729
- return { stop };
9864
+ return {
9865
+ stop,
9866
+ repaint: () => stopped ? Promise.resolve() : paint3(true)
9867
+ };
8730
9868
  }
8731
9869
 
8732
9870
  // src/commands/mission-control.ts
@@ -8807,7 +9945,7 @@ import { defineCommand as defineCommand14 } from "citty";
8807
9945
 
8808
9946
  // src/invariants/monitors-untriaged.ts
8809
9947
  import { execFile as execFile7 } from "child_process";
8810
- import { readFile as readFile21, readdir as readdir8, stat as stat5 } from "fs/promises";
9948
+ import { readFile as readFile23, readdir as readdir10, stat as stat5 } from "fs/promises";
8811
9949
  import path41 from "path";
8812
9950
  import { promisify as promisify6 } from "util";
8813
9951
 
@@ -8832,7 +9970,7 @@ function hasOpenGaps(content) {
8832
9970
  }
8833
9971
  async function listMonitorFiles(memoryDir) {
8834
9972
  try {
8835
- const names = await readdir8(memoryDir);
9973
+ const names = await readdir10(memoryDir);
8836
9974
  return names.filter((n) => n.startsWith("plan-monitor-") && n.endsWith(".md")).sort();
8837
9975
  } catch {
8838
9976
  return [];
@@ -8880,7 +10018,7 @@ async function selectUntriagedMonitors(rootDir) {
8880
10018
  for (const name of allNames) {
8881
10019
  const abs = path41.join(memoryDir, name);
8882
10020
  try {
8883
- const [content, st] = await Promise.all([readFile21(abs, "utf8"), stat5(abs)]);
10021
+ const [content, st] = await Promise.all([readFile23(abs, "utf8"), stat5(abs)]);
8884
10022
  byName.set(name, { content, mtimeMs: st.mtimeMs });
8885
10023
  } catch {
8886
10024
  }
@@ -8893,7 +10031,7 @@ async function selectUntriagedMonitors(rootDir) {
8893
10031
  const gitFreshSet = [...gitFresh].filter(untriaged).sort();
8894
10032
  let handoff = "";
8895
10033
  try {
8896
- handoff = await readFile21(path41.join(root, ".cursor", "HANDOFF.md"), "utf8");
10034
+ handoff = await readFile23(path41.join(root, ".cursor", "HANDOFF.md"), "utf8");
8897
10035
  } catch {
8898
10036
  handoff = "";
8899
10037
  }
@@ -9011,7 +10149,7 @@ var planIndexCommand = defineCommand15({
9011
10149
  });
9012
10150
 
9013
10151
  // src/commands/run-plan.ts
9014
- import path49 from "path";
10152
+ import path50 from "path";
9015
10153
  import { defineCommand as defineCommand16 } from "citty";
9016
10154
 
9017
10155
  // src/plan-loop/backends.ts
@@ -9019,7 +10157,677 @@ import { execFileSync as execFileSync2, spawn as spawn7 } from "child_process";
9019
10157
  import { createWriteStream } from "fs";
9020
10158
  import { access as access9 } from "fs/promises";
9021
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";
9022
10164
  import { StringDecoder } from "string_decoder";
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
10176
+ ];
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;
10190
+ }
10191
+ return last;
10192
+ }
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;
10400
+ pending = "";
10401
+ assistantTexts = [];
10402
+ decoder = new StringDecoder("utf8");
10403
+ feed(chunk) {
10404
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
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;
10424
+ }
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);
10435
+ }
10436
+ }
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
+ });
10449
+ }
10450
+ }
10451
+ };
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);
10460
+ }
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
+ }
9023
10831
  var CLAUDE_ENV_PASSTHROUGH = [
9024
10832
  "ANTHROPIC_BASE_URL",
9025
10833
  "ANTHROPIC_AUTH_TOKEN",
@@ -9036,7 +10844,7 @@ function redactSecrets(text, secrets) {
9036
10844
  var REDACT_HOLD_MAX_CHARS = 64 * 1024;
9037
10845
  var RedactingStreamBuffer = class {
9038
10846
  pending = "";
9039
- decoder = new StringDecoder("utf8");
10847
+ decoder = new StringDecoder3("utf8");
9040
10848
  secrets;
9041
10849
  keep;
9042
10850
  constructor(secrets) {
@@ -9177,6 +10985,8 @@ function spawnLogged(command, args, logPath, options = {}) {
9177
10985
  const spawnFn = options.spawnFn ?? spawn7;
9178
10986
  const redact = options.redact ?? [];
9179
10987
  const redactedError = (err) => new Error(redactSecrets(String(err), redact));
10988
+ const hitl = options.hitl;
10989
+ const stdinTransport = hitl?.transport.kind === "stdin-stream-json";
9180
10990
  return new Promise((resolve3, reject) => {
9181
10991
  let out;
9182
10992
  let child;
@@ -9188,7 +10998,7 @@ function spawnLogged(command, args, logPath, options = {}) {
9188
10998
  reject(redactedError(err));
9189
10999
  });
9190
11000
  child = spawnFn(command, args, {
9191
- stdio: ["ignore", "pipe", "pipe"],
11001
+ stdio: [stdinTransport ? "pipe" : "ignore", "pipe", "pipe"],
9192
11002
  ...options.cwd ? { cwd: options.cwd } : {},
9193
11003
  ...options.env ? { env: options.env } : {}
9194
11004
  });
@@ -9196,11 +11006,33 @@ function spawnLogged(command, args, logPath, options = {}) {
9196
11006
  reject(redactedError(err));
9197
11007
  return;
9198
11008
  }
11009
+ const render = options.render ?? createStreamRenderer();
11010
+ const relay = hitl ? createRelay(hitl, child, redactedError) : null;
9199
11011
  const emit = (text) => {
9200
11012
  if (text.length === 0) return;
9201
- process.stdout.write(text);
9202
11013
  out.write(text);
11014
+ render.feed(text);
11015
+ relay?.feed(text);
9203
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
+ });
9204
11036
  const buffers = [];
9205
11037
  const tee = (stream) => {
9206
11038
  if (!stream) return;
@@ -9220,14 +11052,138 @@ function spawnLogged(command, args, logPath, options = {}) {
9220
11052
  });
9221
11053
  child.on("close", (code) => {
9222
11054
  for (const buffer of buffers) emit(buffer.flush());
9223
- out.end((err) => {
9224
- const failure = err ?? logError;
9225
- if (failure) reject(redactedError(failure));
9226
- else resolve3({ exitCode: code ?? 1 });
9227
- });
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();
9228
11066
  });
9229
11067
  });
9230
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
+ }
9231
11187
  var cursorAgentBackend = {
9232
11188
  id: "cursor-agent",
9233
11189
  async resolve() {
@@ -9252,7 +11208,11 @@ var cursorAgentBackend = {
9252
11208
  return spawnLogged("cursor-agent", args, opts.logPath, {
9253
11209
  spawnFn: opts.spawnFn,
9254
11210
  env,
9255
- 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"
9256
11216
  });
9257
11217
  }
9258
11218
  };
@@ -9303,7 +11263,7 @@ function checkClaudeVersion(bin, versionFn = defaultVersionFn) {
9303
11263
  }
9304
11264
  return { ok: true, version };
9305
11265
  }
9306
- function claudeHeadlessArgs(opts) {
11266
+ function claudeHeadlessArgs(opts = {}) {
9307
11267
  const args = [
9308
11268
  "-p",
9309
11269
  "--output-format",
@@ -9311,7 +11271,10 @@ function claudeHeadlessArgs(opts) {
9311
11271
  "--verbose",
9312
11272
  "--dangerously-skip-permissions",
9313
11273
  "--permission-prompts",
9314
- "none"
11274
+ "none",
11275
+ "--input-format",
11276
+ "stream-json",
11277
+ "--replay-user-messages"
9315
11278
  ];
9316
11279
  if (opts.maxTurns !== void 0) {
9317
11280
  args.push("--max-turns", String(opts.maxTurns));
@@ -9322,7 +11285,6 @@ function claudeHeadlessArgs(opts) {
9322
11285
  if (opts.model) {
9323
11286
  args.push("--model", opts.model);
9324
11287
  }
9325
- args.push(opts.prompt);
9326
11288
  return args;
9327
11289
  }
9328
11290
  async function missingClaudeAdapter(workspace, prompt) {
@@ -9367,15 +11329,22 @@ var claudeBackend = {
9367
11329
  (key) => `${key}=${env[key] ? "set" : "unset"}`
9368
11330
  ).join(" ");
9369
11331
  log(
9370
- `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}`
9371
11333
  );
9372
- const args = claudeHeadlessArgs({ prompt: opts.prompt, model: opts.model, ...caps });
11334
+ const args = claudeHeadlessArgs({ model: opts.model, ...caps });
9373
11335
  try {
9374
11336
  return await spawnLogged("claude", args, opts.logPath, {
9375
11337
  cwd: opts.workspace,
9376
11338
  env,
9377
11339
  spawnFn: opts.spawnFn,
9378
- 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"
9379
11348
  });
9380
11349
  } catch (err) {
9381
11350
  throw new Error(`claude backend failed to start: ${redactSecrets(String(err), redact)}`);
@@ -9445,14 +11414,464 @@ async function detectAgentBackend(requested, whichFn = whichBinary) {
9445
11414
  }
9446
11415
 
9447
11416
  // src/plan-loop/run-loop.ts
9448
- import { mkdir as mkdir6, readFile as readFile24, rm as rm2, unlink as unlink2 } from "fs/promises";
9449
- import path48 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
+ }
9450
11869
 
9451
11870
  // src/plan-loop/external-review.ts
9452
11871
  import { spawn as spawn8 } from "child_process";
9453
- import path45 from "path";
9454
- var CANONICAL_REL = path45.join(".cursor", "scripts", "plan-external-review.sh");
9455
- var FALLBACK_REL = path45.join("scripts", "plan-external-review.sh");
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");
9456
11875
  function isPlanExhaustedReason(reason) {
9457
11876
  const r = reason.trim().toLowerCase();
9458
11877
  if (!r) return false;
@@ -9474,8 +11893,8 @@ async function armExternalPlanReview(root, options = {}) {
9474
11893
  const existsFn = options.existsFn ?? fileExists;
9475
11894
  const log = options.log ?? ((line) => console.log(line));
9476
11895
  const force = options.force === true;
9477
- const canonicalPath = path45.join(root, CANONICAL_REL);
9478
- const fallbackPath = path45.join(root, FALLBACK_REL);
11896
+ const canonicalPath = path47.join(root, CANONICAL_REL);
11897
+ const fallbackPath = path47.join(root, FALLBACK_REL);
9479
11898
  let scriptPath = null;
9480
11899
  let scriptRel = CANONICAL_REL;
9481
11900
  if (await existsFn(canonicalPath)) {
@@ -9528,7 +11947,7 @@ async function armExternalPlanReview(root, options = {}) {
9528
11947
  }
9529
11948
 
9530
11949
  // src/plan-loop/persona-banners.ts
9531
- import path46 from "path";
11950
+ import path48 from "path";
9532
11951
  import {
9533
11952
  blue,
9534
11953
  cyan as cyan4,
@@ -9564,7 +11983,7 @@ function resolveColor(name, fallback) {
9564
11983
  async function resolveCliPersonaId(root) {
9565
11984
  try {
9566
11985
  const cfg = await readJson(
9567
- path46.join(root, ".cursor", "context", "config.json")
11986
+ path48.join(root, ".cursor", "context", "config.json")
9568
11987
  );
9569
11988
  const modes = cfg?.agentPersona?.modes ?? cfg?.workspaceSkin?.modes;
9570
11989
  const id = modes?.[CLI_RUN_PLAN_MODE];
@@ -9575,7 +11994,7 @@ async function resolveCliPersonaId(root) {
9575
11994
  }
9576
11995
  async function loadPersonaPack(root, personaId) {
9577
11996
  try {
9578
- const personaPath = path46.join(root, "registry", "personas", "core", personaId, "persona.json");
11997
+ const personaPath = path48.join(root, "registry", "personas", "core", personaId, "persona.json");
9579
11998
  const pack = await readJson(personaPath);
9580
11999
  if (!pack || typeof pack.id !== "string") return null;
9581
12000
  return pack;
@@ -9623,44 +12042,8 @@ function createPersonaBannerPrinter(persona) {
9623
12042
  };
9624
12043
  }
9625
12044
 
9626
- // src/plan-loop/plan-state.ts
9627
- import { readFile as readFile22, readdir as readdir9 } from "fs/promises";
9628
- import path47 from "path";
9629
- function countPendingTodos(raw) {
9630
- const lines = raw.split(/\r?\n/);
9631
- let inFront = 0;
9632
- let cont = false;
9633
- let n = 0;
9634
- for (const line of lines) {
9635
- if (line === "---") {
9636
- inFront += 1;
9637
- continue;
9638
- }
9639
- if (inFront !== 1) continue;
9640
- const idMatch = line.match(/^- id:\s*(\S+)/);
9641
- if (idMatch?.[1]) {
9642
- cont = idMatch[1] === "dogfood-poc";
9643
- }
9644
- if (/content:.*[Cc]ont[ií]nuo/.test(line)) {
9645
- cont = true;
9646
- }
9647
- if (/status:\s*(pending|in_progress)\b/.test(line) && !cont) {
9648
- n += 1;
9649
- }
9650
- }
9651
- return n;
9652
- }
9653
- async function findActivePlanFile(plansDir) {
9654
- if (!await fileExists(plansDir)) return null;
9655
- const files = (await readdir9(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
9656
- return files[0] ? path47.join(plansDir, files[0]) : null;
9657
- }
9658
- async function readPlan(planPath) {
9659
- return readFile22(planPath, "utf8");
9660
- }
9661
-
9662
12045
  // src/plan-loop/sentinel.ts
9663
- import { readFile as readFile23 } from "fs/promises";
12046
+ import { readFile as readFile26 } from "fs/promises";
9664
12047
  var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
9665
12048
  function takeFromText(text) {
9666
12049
  if (!text) return null;
@@ -9726,14 +12109,14 @@ function parseTickResultStatus(content) {
9726
12109
  }
9727
12110
  async function parseTickResultStatusFromLogFile(logPath) {
9728
12111
  try {
9729
- return parseTickResultStatus(await readFile23(logPath, "utf8"));
12112
+ return parseTickResultStatus(await readFile26(logPath, "utf8"));
9730
12113
  } catch {
9731
12114
  return null;
9732
12115
  }
9733
12116
  }
9734
12117
  async function parseSentinelFromLogFile(logPath) {
9735
12118
  try {
9736
- const content = await readFile23(logPath, "utf8");
12119
+ const content = await readFile26(logPath, "utf8");
9737
12120
  return parseSentinelFromLog(content);
9738
12121
  } catch {
9739
12122
  return { kind: "missing" };
@@ -9747,6 +12130,40 @@ function formatSentinelLine(sentinel) {
9747
12130
 
9748
12131
  // src/plan-loop/run-loop.ts
9749
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
+ }
9750
12167
  function stamp() {
9751
12168
  const d = /* @__PURE__ */ new Date();
9752
12169
  const p = (n, w = 2) => String(n).padStart(w, "0");
@@ -9756,9 +12173,9 @@ function sleep(ms) {
9756
12173
  return new Promise((r) => setTimeout(r, ms));
9757
12174
  }
9758
12175
  async function runPlanLoop(opts) {
9759
- const plansDir = path48.join(opts.root, ".cursor", "plans");
9760
- const stopFile = path48.join(opts.root, ".cursor", "loop.stop");
9761
- const logDir = path48.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");
9762
12179
  const planPath = await findActivePlanFile(plansDir);
9763
12180
  if (!planPath) {
9764
12181
  logger.error("No active plan in .cursor/plans/");
@@ -9779,7 +12196,7 @@ async function runPlanLoop(opts) {
9779
12196
  try {
9780
12197
  const persona = await loadCliRunPlanPersona(opts.root);
9781
12198
  const banners = createPersonaBannerPrinter(persona);
9782
- console.log(`Active plan: ${path48.basename(planPath)}`);
12199
+ console.log(`Active plan: ${path49.basename(planPath)}`);
9783
12200
  console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
9784
12201
  console.log(`Backend: ${opts.backend.id}`);
9785
12202
  if (persona) {
@@ -9798,6 +12215,7 @@ async function runPlanLoop(opts) {
9798
12215
  let tick = 0;
9799
12216
  let stopReason;
9800
12217
  let planExhausted = false;
12218
+ let exitCode = 0;
9801
12219
  while (true) {
9802
12220
  tick += 1;
9803
12221
  if (tick > opts.maxTicks) {
@@ -9822,30 +12240,47 @@ async function runPlanLoop(opts) {
9822
12240
  planExhausted = true;
9823
12241
  break;
9824
12242
  }
9825
- const logPath = path48.join(logDir, `tick-${stamp()}.log`);
9826
- const relLog = path48.relative(opts.root, logPath);
12243
+ const logPath = path49.join(logDir, `tick-${stamp()}.log`);
12244
+ const relLog = path49.relative(opts.root, logPath);
9827
12245
  console.log("");
9828
12246
  const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
9829
12247
  if (banners) banners.tickStart(tickLine);
9830
12248
  else console.log(tickLine);
9831
12249
  let agentExit = 0;
12250
+ let hitlStop;
9832
12251
  try {
9833
- const result = await withCliProgress(
12252
+ const result = await runTickWithSpinner(
9834
12253
  `tick ${tick}`,
9835
- () => opts.backend.run({
12254
+ opts.backend,
12255
+ {
9836
12256
  workspace: opts.root,
9837
12257
  prompt: TICK_PROMPT,
9838
12258
  model: opts.model,
9839
- logPath
9840
- })
12259
+ logPath,
12260
+ hitl: { policy: opts.noHitl ? "off" : "prompt" }
12261
+ },
12262
+ opts.plain === true
9841
12263
  );
9842
12264
  agentExit = result.exitCode;
12265
+ for (const line of formatHitlSummary(result.hitl)) console.log(line);
12266
+ hitlStop = result.hitl?.stop;
9843
12267
  } catch (err) {
9844
12268
  logger.error(String(err));
9845
12269
  return 1;
9846
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
+ }
9847
12282
  try {
9848
- const logText = await readFile24(logPath, "utf8");
12283
+ const logText = await readFile27(logPath, "utf8");
9849
12284
  if (logText.includes("Too many MCP tools")) {
9850
12285
  const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
9851
12286
  if (banners) banners.stop(msg);
@@ -9914,12 +12349,12 @@ async function runPlanLoop(opts) {
9914
12349
  const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
9915
12350
  if (banners) banners.phaseComplete(finishDetail);
9916
12351
  console.log(
9917
- `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path48.relative(opts.root, logDir)}/`
12352
+ `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path49.relative(opts.root, logDir)}/`
9918
12353
  );
9919
12354
  if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
9920
12355
  await armExternalPlanReview(opts.root);
9921
12356
  }
9922
- return 0;
12357
+ return exitCode;
9923
12358
  } finally {
9924
12359
  process.off("SIGINT", onSigInt);
9925
12360
  }
@@ -9961,6 +12396,16 @@ var runPlanCommand = defineCommand16({
9961
12396
  type: "boolean",
9962
12397
  description: "Print the tick prompt and exit without starting an agent",
9963
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
9964
12409
  }
9965
12410
  },
9966
12411
  async run({ args }) {
@@ -9996,12 +12441,14 @@ var runPlanCommand = defineCommand16({
9996
12441
  return;
9997
12442
  }
9998
12443
  const code = await runPlanLoop({
9999
- root: path49.resolve(args.cwd),
12444
+ root: path50.resolve(args.cwd),
10000
12445
  maxTicks,
10001
12446
  sleepSeconds,
10002
12447
  model: args.model ? String(args.model) : void 0,
10003
12448
  dryRun: Boolean(args["dry-run"]),
10004
- backend
12449
+ backend,
12450
+ noHitl: Boolean(args["no-hitl"]),
12451
+ plain: Boolean(args.plain)
10005
12452
  });
10006
12453
  process.exitCode = code;
10007
12454
  }
@@ -10009,12 +12456,12 @@ var runPlanCommand = defineCommand16({
10009
12456
 
10010
12457
  // src/commands/run.ts
10011
12458
  import { homedir as homedir4 } from "os";
10012
- import path51 from "path";
12459
+ import path52 from "path";
10013
12460
  import { defineCommand as defineCommand17 } from "citty";
10014
12461
 
10015
12462
  // src/plan-loop/dispatch.ts
10016
- import { mkdir as mkdir7, readFile as readFile25 } from "fs/promises";
10017
- import path50 from "path";
12463
+ import { mkdir as mkdir7, readFile as readFile28 } from "fs/promises";
12464
+ import path51 from "path";
10018
12465
  var RUN_CATALOG = [
10019
12466
  "run-plan",
10020
12467
  "run-plan-all",
@@ -10058,11 +12505,11 @@ function rewriteRootArgvToRun(argv) {
10058
12505
  return next;
10059
12506
  }
10060
12507
  function commandMarkdownPath(root, name) {
10061
- return path50.join(root, ".cursor", "commands", `${normalizeSlashName(name)}.md`);
12508
+ return path51.join(root, ".cursor", "commands", `${normalizeSlashName(name)}.md`);
10062
12509
  }
10063
12510
  function buildDispatchPrompt(commandBody) {
10064
12511
  return [
10065
- "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.",
10066
12513
  "",
10067
12514
  commandBody.trimEnd(),
10068
12515
  ""
@@ -10083,7 +12530,7 @@ async function readCommandFile(root, name) {
10083
12530
  message: `Command file not found: ${filePath}. Install or refresh Agent Kit L0 so .cursor/commands/${normalizeSlashName(name)}.md exists.`
10084
12531
  };
10085
12532
  }
10086
- const body = await readFile25(filePath, "utf8");
12533
+ const body = await readFile28(filePath, "utf8");
10087
12534
  return { ok: true, path: filePath, body };
10088
12535
  }
10089
12536
  function stamp2() {
@@ -10106,7 +12553,7 @@ function cursorAgentDispatchArgs(opts) {
10106
12553
  args.push(opts.prompt);
10107
12554
  return args;
10108
12555
  }
10109
- function claudeDispatchArgs(opts) {
12556
+ function claudeDispatchArgs(opts = {}) {
10110
12557
  return claudeHeadlessArgs(opts);
10111
12558
  }
10112
12559
  async function runHeadlessDispatch(opts) {
@@ -10123,9 +12570,21 @@ async function runHeadlessDispatch(opts) {
10123
12570
  try {
10124
12571
  return await spawnLogged(
10125
12572
  opts.bin,
10126
- claudeDispatchArgs({ prompt: opts.prompt, model: opts.model, ...caps }),
12573
+ claudeDispatchArgs({ model: opts.model, ...caps }),
10127
12574
  opts.logPath,
10128
- { 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
+ }
10129
12588
  );
10130
12589
  } catch (err) {
10131
12590
  throw new Error(`claude dispatch failed to start: ${redactSecrets(String(err), redact)}`);
@@ -10138,24 +12597,28 @@ async function runHeadlessDispatch(opts) {
10138
12597
  });
10139
12598
  return spawnLogged(opts.bin, args, opts.logPath, {
10140
12599
  spawnFn: opts.spawnFn,
10141
- 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"
10142
12605
  });
10143
12606
  }
10144
12607
  async function ensureDispatchLogPath(root) {
10145
- const logDir = path50.join(root, ".cursor", "loop-logs");
12608
+ const logDir = path51.join(root, ".cursor", "loop-logs");
10146
12609
  await mkdir7(logDir, { recursive: true });
10147
- return path50.join(logDir, `run-${stamp2()}.log`);
12610
+ return path51.join(logDir, `run-${stamp2()}.log`);
10148
12611
  }
10149
12612
 
10150
12613
  // src/commands/run.ts
10151
12614
  function displayPath(target, home = homedir4()) {
10152
- const resolvedHome = path51.resolve(home);
12615
+ const resolvedHome = path52.resolve(home);
10153
12616
  if (target === resolvedHome) return "~";
10154
- const prefix = resolvedHome.endsWith(path51.sep) ? resolvedHome : `${resolvedHome}${path51.sep}`;
10155
- return target.startsWith(prefix) ? `~${path51.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;
10156
12619
  }
10157
12620
  async function executeRun(input) {
10158
- const root = path51.resolve(input.cwd);
12621
+ const root = path52.resolve(input.cwd);
10159
12622
  const slash = normalizeSlashName(input.slash);
10160
12623
  if (!slash) {
10161
12624
  return { exitCode: 1, error: "Provide a slash: agent-kit run <slash>" };
@@ -10191,7 +12654,9 @@ async function executeRun(input) {
10191
12654
  sleepSeconds: input.sleepSeconds,
10192
12655
  model: input.model,
10193
12656
  dryRun: false,
10194
- backend
12657
+ backend,
12658
+ noHitl: input.noHitl === true,
12659
+ plain: input.plain === true
10195
12660
  });
10196
12661
  return { exitCode: code };
10197
12662
  }
@@ -10204,7 +12669,7 @@ async function executeRun(input) {
10204
12669
  const stdout = [
10205
12670
  "--dry-run: no agent will be started.",
10206
12671
  `Slash: ${slash}`,
10207
- `Command file: ${path51.relative(root, file.path)}`,
12672
+ `Command file: ${path52.relative(root, file.path)}`,
10208
12673
  `Backend: ${detected.id} (${displayPath(detected.bin)})`,
10209
12674
  "Prompt:",
10210
12675
  prompt
@@ -10213,18 +12678,32 @@ async function executeRun(input) {
10213
12678
  }
10214
12679
  const logPath = await ensureDispatchLogPath(root);
10215
12680
  console.log(`Dispatch: ${slash} via ${detected.id}`);
10216
- console.log(`Command file: ${path51.relative(root, file.path)}`);
10217
- console.log(`Log: ${path51.relative(root, logPath)}`);
12681
+ console.log(`Command file: ${path52.relative(root, file.path)}`);
12682
+ console.log(`Log: ${path52.relative(root, logPath)}`);
10218
12683
  try {
10219
- const result = await runHeadlessDispatch({
12684
+ const dispatch = {
10220
12685
  backendId: detected.id,
10221
12686
  bin: detected.bin,
10222
12687
  workspace: root,
10223
12688
  prompt,
10224
12689
  model: input.model,
10225
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" }
10226
12704
  });
10227
- return { exitCode: result.exitCode };
12705
+ for (const line of formatHitlSummary(result.hitl)) console.log(line);
12706
+ return { exitCode: hitlExitCode(result.exitCode, result.hitl) };
10228
12707
  } catch (err) {
10229
12708
  return { exitCode: 1, error: String(err) };
10230
12709
  }
@@ -10259,6 +12738,16 @@ var runDispatchArgs = {
10259
12738
  type: "string",
10260
12739
  description: "For run-plan alias only: seconds between ticks (default: 5)",
10261
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
10262
12751
  }
10263
12752
  };
10264
12753
  async function runSlashCli(slash, args) {
@@ -10281,7 +12770,9 @@ async function runSlashCli(slash, args) {
10281
12770
  model: args.model ? String(args.model) : void 0,
10282
12771
  dryRun: Boolean(args["dry-run"]),
10283
12772
  maxTicks,
10284
- sleepSeconds
12773
+ sleepSeconds,
12774
+ noHitl: Boolean(args["no-hitl"]),
12775
+ plain: Boolean(args.plain)
10285
12776
  });
10286
12777
  if (result.stdout) {
10287
12778
  console.log(result.stdout);
@@ -10294,7 +12785,7 @@ async function runSlashCli(slash, args) {
10294
12785
  var runCommand = defineCommand17({
10295
12786
  meta: {
10296
12787
  name: "run",
10297
- 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."
10298
12789
  },
10299
12790
  args: {
10300
12791
  slash: {
@@ -10311,7 +12802,7 @@ var runCommand = defineCommand17({
10311
12802
  var runPlanAllCommand = defineCommand17({
10312
12803
  meta: {
10313
12804
  name: "run-plan-all",
10314
- description: "Headless /run-plan-all queue from the L0 file. Numbered-list HITL. Never git-prod."
12805
+ description: "Headless /run-plan-all queue from the L0 file. Numbered-list HITL answered from the terminal (claude backend). Never git-prod."
10315
12806
  },
10316
12807
  args: runDispatchArgs,
10317
12808
  async run({ args }) {
@@ -10343,9 +12834,9 @@ var scanCommand = defineCommand18({
10343
12834
 
10344
12835
  // src/commands/setup-global.ts
10345
12836
  import { spawn as spawn9 } from "child_process";
10346
- import { appendFile, mkdir as mkdir8, readFile as readFile26, writeFile as writeFile13 } from "fs/promises";
12837
+ import { appendFile as appendFile2, mkdir as mkdir8, readFile as readFile29, writeFile as writeFile13 } from "fs/promises";
10347
12838
  import { homedir as homedir5 } from "os";
10348
- import path52 from "path";
12839
+ import path53 from "path";
10349
12840
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
10350
12841
  import { defineCommand as defineCommand19 } from "citty";
10351
12842
  import { cyan as cyan5, green as green4, yellow as yellow3 } from "kolorist";
@@ -10354,9 +12845,9 @@ var SETUP_GLOBAL_MARKER = "# agent-kit setup-global";
10354
12845
  function planSetupGlobalSteps(env, options = {}) {
10355
12846
  const homeDir = options.homeDir ?? homedir5();
10356
12847
  const packageSpec = options.packageSpec ?? pinnedCliSpec();
10357
- const npmGlobalDir = path52.join(homeDir, NPM_GLOBAL_DIR_NAME);
10358
- const npmGlobalBin = path52.join(npmGlobalDir, "bin");
10359
- const npmrcPath = path52.join(homeDir, ".npmrc");
12848
+ const npmGlobalDir = path53.join(homeDir, NPM_GLOBAL_DIR_NAME);
12849
+ const npmGlobalBin = path53.join(npmGlobalDir, "bin");
12850
+ const npmrcPath = path53.join(homeDir, ".npmrc");
10360
12851
  const npmrcPrefixValue = `~/${NPM_GLOBAL_DIR_NAME}`;
10361
12852
  const pathExportLine = `export PATH="${npmGlobalBin}:$PATH"`;
10362
12853
  const shellProfile = env.shellProfile;
@@ -10424,9 +12915,9 @@ var defaultFsImpl = {
10424
12915
  mkdir: async (dir) => {
10425
12916
  await mkdir8(dir, { recursive: true });
10426
12917
  },
10427
- readFile: (filePath) => readFile26(filePath, "utf8"),
12918
+ readFile: (filePath) => readFile29(filePath, "utf8"),
10428
12919
  writeFile: (filePath, content) => writeFile13(filePath, content, "utf8"),
10429
- appendFile: (filePath, content) => appendFile(filePath, content, "utf8")
12920
+ appendFile: (filePath, content) => appendFile2(filePath, content, "utf8")
10430
12921
  };
10431
12922
  async function safeReadFile(fs, filePath) {
10432
12923
  try {
@@ -10655,7 +13146,7 @@ var setupGlobalCommand = defineCommand19({
10655
13146
  });
10656
13147
 
10657
13148
  // src/commands/status.ts
10658
- import path53 from "path";
13149
+ import path54 from "path";
10659
13150
  import { defineCommand as defineCommand20 } from "citty";
10660
13151
  function profileStatus(profile) {
10661
13152
  if (!profile) return { origin: "none", evidence: [], profile: null };
@@ -10686,11 +13177,11 @@ var statusCommand = defineCommand20({
10686
13177
  }
10687
13178
  },
10688
13179
  async run({ args }) {
10689
- const rootDir = path53.resolve(args.cwd);
13180
+ const rootDir = path54.resolve(args.cwd);
10690
13181
  const [manifest, rawProfile, scan, env] = await Promise.all([
10691
13182
  loadAgentKitManifest(rootDir),
10692
13183
  readJson(
10693
- path53.join(rootDir, ".cursor", "agent-kit.config.json")
13184
+ path54.join(rootDir, ".cursor", "agent-kit.config.json")
10694
13185
  ),
10695
13186
  runScanner(rootDir),
10696
13187
  assessEnvironment()
@@ -10935,8 +13426,8 @@ ${err.recovery}
10935
13426
  });
10936
13427
 
10937
13428
  // src/commands/validate.ts
10938
- import { readFile as readFile27 } from "fs/promises";
10939
- import path54 from "path";
13429
+ import { readFile as readFile30 } from "fs/promises";
13430
+ import path55 from "path";
10940
13431
  import { defineCommand as defineCommand22 } from "citty";
10941
13432
 
10942
13433
  // src/invariants/plan-schema.ts
@@ -10969,7 +13460,7 @@ function validatePlanFrontmatterText(text) {
10969
13460
  cite: CITE5
10970
13461
  });
10971
13462
  }
10972
- const hasTodoItem = /^- id:\s*\S+/m.test(block);
13463
+ const hasTodoItem = /^\s*- id:\s*\S+/m.test(block);
10973
13464
  if (/^todos:/m.test(block) && !hasTodoItem && !/^todos:\s*\[\s*\]/m.test(block)) {
10974
13465
  warnings.push({
10975
13466
  code: "empty-todos",
@@ -10983,9 +13474,9 @@ function validatePlanFrontmatterText(text) {
10983
13474
  // src/commands/validate.ts
10984
13475
  async function resolveEditedPath(cwd, explicit) {
10985
13476
  if (explicit) {
10986
- const filePath2 = path54.resolve(cwd, explicit);
13477
+ const filePath2 = path55.resolve(cwd, explicit);
10987
13478
  try {
10988
- return { filePath: filePath2, content: await readFile27(filePath2, "utf8") };
13479
+ return { filePath: filePath2, content: await readFile30(filePath2, "utf8") };
10989
13480
  } catch {
10990
13481
  return null;
10991
13482
  }
@@ -10993,9 +13484,9 @@ async function resolveEditedPath(cwd, explicit) {
10993
13484
  const payload = await readStdinJson();
10994
13485
  const rel = typeof payload.file_path === "string" && payload.file_path || typeof payload.path === "string" && payload.path || typeof payload.file === "string" && payload.file || "";
10995
13486
  if (!rel) return null;
10996
- const filePath = path54.isAbsolute(rel) ? rel : path54.resolve(cwd, rel);
13487
+ const filePath = path55.isAbsolute(rel) ? rel : path55.resolve(cwd, rel);
10997
13488
  try {
10998
- return { filePath, content: await readFile27(filePath, "utf8") };
13489
+ return { filePath, content: await readFile30(filePath, "utf8") };
10999
13490
  } catch {
11000
13491
  return null;
11001
13492
  }
@@ -11023,15 +13514,21 @@ var validateCommand = defineCommand22({
11023
13514
  async run({ args }) {
11024
13515
  const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
11025
13516
  const fileArg = typeof args.file === "string" ? args.file : void 0;
11026
- const filePath = fileArg ? path54.resolve(cwd, fileArg) : path54.join(path54.resolve(cwd), ".cursor", "HANDOFF.md");
13517
+ const filePath = fileArg ? path55.resolve(cwd, fileArg) : path55.join(path55.resolve(cwd), ".cursor", "HANDOFF.md");
11027
13518
  let content = "";
11028
13519
  try {
11029
- content = await readFile27(filePath, "utf8");
13520
+ content = await readFile30(filePath, "utf8");
11030
13521
  } catch {
11031
13522
  console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
11032
13523
  return;
11033
13524
  }
11034
- 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
+ ];
11035
13532
  console.log(JSON.stringify({ ok: warnings.length === 0, warnings }));
11036
13533
  }
11037
13534
  }),
@@ -11050,10 +13547,10 @@ var validateCommand = defineCommand22({
11050
13547
  process.exitCode = 2;
11051
13548
  return;
11052
13549
  }
11053
- const filePath = path54.resolve(cwd, fileArg);
13550
+ const filePath = path55.resolve(cwd, fileArg);
11054
13551
  let content = "";
11055
13552
  try {
11056
- content = await readFile27(filePath, "utf8");
13553
+ content = await readFile30(filePath, "utf8");
11057
13554
  } catch {
11058
13555
  console.log(JSON.stringify({ ok: true, warnings: [], note: "file missing" }));
11059
13556
  return;
@@ -11072,19 +13569,22 @@ var validateCommand = defineCommand22({
11072
13569
  },
11073
13570
  async run({ args }) {
11074
13571
  const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
11075
- const resolved = await resolveEditedPath(path54.resolve(cwd));
13572
+ const resolved = await resolveEditedPath(path55.resolve(cwd));
11076
13573
  if (!resolved) {
11077
13574
  console.log(JSON.stringify({}));
11078
13575
  return;
11079
13576
  }
11080
13577
  const { filePath, content } = resolved;
11081
13578
  if (isHandoffPath(filePath)) {
11082
- const warnings = validateHandoffText(content);
13579
+ const warnings = [
13580
+ ...validateHandoffText(content).map((w) => w.message),
13581
+ ...checkHandoffGrowth(content).map((w) => w.message)
13582
+ ];
11083
13583
  if (!warnings.length) {
11084
13584
  console.log(JSON.stringify({}));
11085
13585
  return;
11086
13586
  }
11087
- const msg = warnings.map((w) => w.message).join(" ");
13587
+ const msg = warnings.join(" ");
11088
13588
  console.log(
11089
13589
  JSON.stringify({
11090
13590
  user_message: msg,
@@ -11118,7 +13618,7 @@ var validateCommand = defineCommand22({
11118
13618
  import { bold as bold2, gray as gray4, underline } from "kolorist";
11119
13619
 
11120
13620
  // src/welcome/screen.ts
11121
- 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";
11122
13622
  var HELMET_ASCII = [
11123
13623
  " ____",
11124
13624
  " .-' '-.",
@@ -11139,7 +13639,7 @@ function hexToRgb(hex) {
11139
13639
  }
11140
13640
  function outlineAnsi(line) {
11141
13641
  const [r, g, b] = hexToRgb(HELMET_OUTLINE);
11142
- return trueColor2(r, g, b)(line);
13642
+ return trueColor3(r, g, b)(line);
11143
13643
  }
11144
13644
  function hasCliSubcommand(rawArgs) {
11145
13645
  return Boolean(rawArgs?.some((arg) => !arg.startsWith("-")));