@autopilot-harness/cli 0.2.11 → 0.2.13

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.
@@ -42,6 +42,7 @@ var en_default = {
42
42
  recover_ambient: "Recover: the previous turn ended with an error. Continue your current work; Autopilot RUN is not active.",
43
43
  review_complete: "Review complete. All {total} confirm rounds passed; the review chain has ended. If the working tree still has uncommitted changes from this session, local commit only per the safe checklist (never stage .env/secrets/.autopilot runtime; no push unless the user asks); if clean, briefly confirm only.",
44
44
  stuck: "Stuck: no progress for several stops. Change strategy or send Autopilot RESUME after fixing.",
45
+ stuck_soft: "Stuck: no progress for several stops (missing completion evidence). Change strategy, write matching .autopilot/verify-last.json, then end the turn. Session stays armed \u2014 Autopilot RESUME is not required unless the session was paused.",
45
46
  verify_fix: "Verify failed ({reason}). Fix verify commands and rewrite verify-last.json; do not advance.",
46
47
  need_evidence: `Need evidence: no-code item {currentId}{currentTitleSuffix} cannot advance without matching soft completion evidence. Write .autopilot/verify-last.json with itemId "{currentId}" and ok: true (only after this item's work is done). Then end the turn so the stop hook can advance/done. Do not ask the user to continue; do not invent Advance/Done.`,
47
48
  track_pick: "Select a plan by number or slug."
@@ -118,6 +119,7 @@ var zh_CN_default = {
118
119
  recover_ambient: "\u6062\u590D\uFF1A\u4E0A\u4E00\u56DE\u5408\u51FA\u9519\u3002\u7EE7\u7EED\u5F53\u524D\u4EFB\u52A1\uFF08\u672A\u5728\u6267\u884C checklist\uFF09\u3002",
119
120
  review_complete: "\u81EA\u5BA1\u5B8C\u6210\u3002\u8FDE\u7EED {total} \u8F6E\u786E\u8BA4\u5DF2\u901A\u8FC7\uFF0C\u81EA\u5BA1\u94FE\u5DF2\u7ED3\u675F\u3002\u82E5\u5DE5\u4F5C\u533A\u4ECD\u6709\u672C\u4F1A\u8BDD\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u6309\u5B89\u5168\u6E05\u5355\u672C\u5730 commit\uFF08\u52FF stage .env/\u5BC6\u94A5/.autopilot \u8FD0\u884C\u65F6\uFF1B\u52FF push\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\uFF09\uFF1B\u5DF2\u5E72\u51C0\u5219\u53EA\u7B80\u77ED\u786E\u8BA4\u5373\u53EF\u3002",
120
121
  stuck: "\u5361\u4F4F\uFF1A\u8FDE\u7EED\u591A\u8F6E\u65E0\u8FDB\u5C55\u3002\u8BF7\u6362\u7B56\u7565\uFF0C\u6216\u4FEE\u597D\u540E\u53D1\u9001 Autopilot RESUME\u3002",
122
+ stuck_soft: "\u5361\u4F4F\uFF1A\u8FDE\u7EED\u591A\u8F6E\u65E0\u8FDB\u5C55\uFF08\u7F3A\u5B8C\u6210\u8BC1\u636E\uFF09\u3002\u8BF7\u6362\u7B56\u7565\u5E76\u5199\u5165\u5339\u914D\u7684 .autopilot/verify-last.json \u540E\u7ED3\u675F\u56DE\u5408\u3002\u4F1A\u8BDD\u4ECD\u5728\u8FD0\u884C\uFF0C\u65E0\u9700 RESUME\uFF1B\u4EC5\u5728\u5DF2 pause \u65F6\u624D\u53D1\u9001 Autopilot RESUME\u3002",
121
123
  verify_fix: "\u6821\u9A8C\u5931\u8D25\uFF08{reason}\uFF09\u3002\u8BF7\u4FEE\u590D verify \u547D\u4EE4\u5E76\u91CD\u5199 verify-last.json\uFF1B\u4E0D\u8981\u63A8\u8FDB\u3002",
122
124
  need_evidence: '\u9700\u8981\u5B8C\u6210\u8BC1\u636E\uFF1A\u65E0\u4EE3\u7801\u6539\u52A8\u9879 {currentId}{currentTitleSuffix} \u7F3A\u5C11\u5339\u914D\u7684 soft \u5B8C\u6210\u8BC1\u636E\uFF0C\u65E0\u6CD5\u63A8\u8FDB\u3002\u8BF7\u5199\u5165 .autopilot/verify-last.json\uFF08itemId \u4E3A "{currentId}"\uFF0Cok: true\uFF1B\u987B\u5728\u8BE5\u9879\u5DE5\u4F5C\u5B8C\u6210\u540E\uFF09\u3002\u7136\u540E\u7ED3\u675F\u672C\u56DE\u5408\uFF0C\u7531 stop hook \u63A8\u8FDB/\u5B8C\u6210\u3002\u4E0D\u8981\u8BA9\u7528\u6237\u8BF4\u300C\u7EE7\u7EED\u300D\uFF1B\u4E0D\u8981\u81EA\u884C\u53D1\u660E\u63A8\u8FDB/\u5B8C\u6210\u6307\u4EE4\u3002',
123
125
  track_pick: "\u8BF7\u7528\u6570\u5B57\u6216 slug \u9009\u62E9\u8981\u6267\u884C\u7684 plan\u3002"
@@ -2066,6 +2068,11 @@ function isRecoverOrStuckFollowupMessage(text) {
2066
2068
  if (!line) return false;
2067
2069
  return isRecoverFollowupMessage(line) || line.startsWith("Stuck:") || line.startsWith("\u5361\u4F4F\uFF1A") || line.startsWith("\u5361\u4F4F:");
2068
2070
  }
2071
+ function isTerminalFollowupMessage(text) {
2072
+ const line = firstSubstantiveLine(text);
2073
+ if (!line) return false;
2074
+ return line.startsWith("All checklist") || line.startsWith("\u5168\u90E8\u5B8C\u6210") || line.startsWith("Review complete") || line.startsWith("\u81EA\u5BA1\u5B8C\u6210");
2075
+ }
2069
2076
  var USER_ABORT_MARKERS = [
2070
2077
  "user aborted",
2071
2078
  "interrupted manually",
@@ -2393,93 +2400,469 @@ function pendingRedeliverAllowed(lastRedeliverAt) {
2393
2400
  return Date.now() - t >= PENDING_REDELIVER_COOLDOWN_MS;
2394
2401
  }
2395
2402
 
2396
- // ../core/src/review-engine.ts
2397
- function defaultRender(kind, vars) {
2398
- switch (kind) {
2399
- case "review.fix":
2400
- return `Review fix round ${vars.round} (no hard cap; confirm needs ${vars.total} consecutive no-edit rounds). Code changed this turn. Defect-first self-review and fix now: 1) when using git diff / git status, skip paths matching .autopilotignore, and skip untracked paths ignored by .gitignore; review only the remaining paths; 2) check correctness, null/boundaries, concurrency, security, regression, missing tests; 3) CRITICAL/HIGH must fix, MEDIUM preferably; 4) run relevant tests; 5) briefly state what you reviewed and changed (or "self-review clean"). Do not commit/push. If no further code changes, next stop enters multi-lens confirm.`;
2401
- case "review.confirm":
2402
- return `Review confirm ${vars.n}/${vars.total} (session round ${vars.sessionRound}; consecutive no-edit confirms, counted on the fix-round counter). Lens \u3010${vars.lensTitle}\u3011 (multi-lens confirm, not the same checklist again). ${vars.lensFocus} Previous turn had no further code edits. Recheck under this lens only: 1) git diff / git status \u2014 no new edits vs prior turn (or only already-reviewed edits); likewise skip paths matching .autopilotignore and untracked paths ignored by .gitignore; judge only from remaining paths; 2) dig into this lens only; ban vague "fully rechecked, all good"; 3) CRITICAL/HIGH under this lens must fix; MEDIUM preferably; 4) if you edit, fix and run related tests; 5) close with: "Lens (${vars.lensTitle}): self-review clean" or a short list of fixes; if clean, do not edit further. Do not commit/push.`;
2403
- case "review.confirm_final":
2404
- return `Review confirm ${vars.n}/${vars.total} (session round ${vars.sessionRound}; consecutive no-edit confirms, counted on the fix-round counter). Lens \u3010${vars.lensTitle}\u3011 (multi-lens confirm, not the same checklist again). ${vars.lensFocus} Previous turn had no further code edits. Recheck under this lens only: 1) git diff / git status \u2014 no new edits vs prior turn (or only already-reviewed edits); likewise skip paths matching .autopilotignore and untracked paths ignored by .gitignore; judge only from remaining paths; 2) dig into this lens only; ban vague "fully rechecked, all good"; 3) read-only: record CRITICAL/HIGH/missing tests \u2014 do not change code, add tests, or commit; if you already edited, accept returning to a fix round; never commit this turn; 4) do not run commands that mutate the repo; 5) close with: "Lens (${vars.lensTitle}): self-review clean" or list issues (no fixes); if clean, do not edit further. Do not commit/push. Handoff (commit) and next checklist item are handled by Advance/Done after the chain \u2014 not this turn.`;
2405
- case "advance":
2406
- return `Advance checklist: confirm chain passed cleanly (confirm rounds do not commit). First mark the completed current item ${vars.currentId ?? ""} [x] in checklist.md (do not mark the next item yet). Never mark an item [x] while you are still implementing it \u2014 only this Advance/Done followup checks off the completed item. Then, if the working tree still has uncommitted changes for this item (including checklist.md when plans/ is committed), local conventional commit only: git status/diff \u2192 stage only this checklist item's paths; never git add -A, never stage .env/secrets/.autopilot runtime; one conventional commit; no push/--no-verify/amend/force unless the user explicitly asks. If already clean after marking, skip commit. Then implement next: ${vars.nextId ?? ""} \u2014 ${vars.nextTitle ?? ""}.`;
2407
- case "done":
2408
- return `All checklist items done. Confirm chain passed (confirm rounds do not commit). Mark the last item [x]. If the working tree still has uncommitted changes for this item (including checklist.md when plans/ is committed), local conventional commit only (never stage .env/secrets/.autopilot runtime; no push unless the user asks); if clean, just confirm briefly. Phase is done.`;
2409
- case "recover":
2410
- return `Recover: the previous turn ended with an error. Continue the current task.`;
2411
- case "recover_planning":
2412
- return `Recover: the previous turn ended with an error. Continue planning; do not RUN or write product code.`;
2413
- case "recover_ambient":
2414
- return `Recover: the previous turn ended with an error. Continue your current work; Autopilot RUN is not active.`;
2415
- case "review_complete":
2416
- return `Review complete. All ${vars.total ?? 5} confirm rounds passed; the review chain has ended. If the working tree still has uncommitted changes from this session, local commit only per the safe checklist (never stage .env/secrets/.autopilot runtime; no push unless the user asks); if clean, briefly confirm only.`;
2417
- case "stuck":
2418
- return `Stuck: no progress for several stops. Change strategy or send Autopilot RESUME after fixing.`;
2419
- case "verify_fix":
2420
- return `Verify failed (${vars.reason ?? "unknown"}). Fix verify commands and rewrite verify-last.json; do not advance.`;
2421
- case "need_evidence":
2422
- return `Need evidence: no-code item ${vars.currentId ?? ""}${vars.currentTitle ? ` \u2014 ${vars.currentTitle}` : ""} cannot advance without matching soft completion evidence. Write .autopilot/verify-last.json with itemId "${vars.currentId ?? ""}" and ok: true (only after this item's work is done). Then end the turn so the stop hook can advance/done. Do not ask the user to continue; do not invent Advance/Done.`;
2423
- default:
2424
- return "";
2425
- }
2403
+ // ../core/src/code-edit-detector.ts
2404
+ import { spawnSync } from "node:child_process";
2405
+ import path7 from "node:path";
2406
+
2407
+ // ../core/src/autopilot-ignore.ts
2408
+ import fs7 from "node:fs";
2409
+ import path6 from "node:path";
2410
+ var DEFAULT_AUTOPILOT_IGNORE_TEXT = `# Autopilot \u2014 paths that do NOT trigger self-review (gitignore syntax).
2411
+ #
2412
+ # What this file is:
2413
+ # - Controls whether an afterFileEdit counts as "product code" (opens fix/confirm).
2414
+ # - Does NOT change \`git diff\` / \`git status\` output (that is \`.gitignore\`).
2415
+ # - Review followups ask the agent to skip these paths when reading diffs (soft).
2416
+ #
2417
+ # Semantics:
2418
+ # - Same glob rules as gitignore; last matching pattern wins.
2419
+ # - Use \`!\` to force-include an exception (e.g. \`!docs/feed/**/*.yml\`).
2420
+ # - Markdown (*.md / *.mdx) is NOT ignored by default \u2014 design docs can be reviewed.
2421
+ # - \`docs/**\` is NOT ignored by default.
2422
+ # - Also skip untracked paths ignored by \`.gitignore\` (tracked files still count).
2423
+ #
2424
+ # Later (not implemented): hard-filtered review-diff / path ledger \u2014 see
2425
+ # docs/autopilot/workflows/autopilot-executing.md (B2 strong).
2426
+
2427
+ # Runtime / editor (prefer also listing these in .gitignore)
2428
+ .autopilot/**
2429
+ .cursor/**
2430
+ .claude/**
2431
+
2432
+ # Planning artifacts
2433
+ plans/**
2434
+
2435
+ # Common build / vendor trees
2436
+ node_modules/**
2437
+ dist/**
2438
+ build/**
2439
+ out/**
2440
+ target/**
2441
+ .target/**
2442
+ coverage/**
2443
+ .venv/**
2444
+ venv/**
2445
+ __pycache__/**
2446
+
2447
+ # Lockfiles / package manager noise
2448
+ package-lock.json
2449
+ pnpm-lock.yaml
2450
+ yarn.lock
2451
+ bun.lock
2452
+ bun.lockb
2453
+ Cargo.lock
2454
+ poetry.lock
2455
+ composer.lock
2456
+
2457
+ # Media / binary (do not trigger self-review)
2458
+ *.png
2459
+ *.jpg
2460
+ *.jpeg
2461
+ *.gif
2462
+ *.webp
2463
+ *.ico
2464
+ *.svg
2465
+ *.bmp
2466
+ *.mp3
2467
+ *.mp4
2468
+ *.wav
2469
+ *.webm
2470
+ *.mov
2471
+ *.woff
2472
+ *.woff2
2473
+ *.ttf
2474
+ *.otf
2475
+ *.eot
2476
+ *.pdf
2477
+ *.zip
2478
+ *.gz
2479
+ *.tgz
2480
+ *.7z
2481
+ *.rar
2482
+ *.jar
2483
+ *.class
2484
+ *.o
2485
+ *.a
2486
+ *.so
2487
+ *.dylib
2488
+ *.dll
2489
+ *.exe
2490
+ *.wasm
2491
+
2492
+ # Prose / data noise
2493
+ *.txt
2494
+ *.html
2495
+ *.htm
2496
+ *.csv
2497
+ *.tsv
2498
+ *.log
2499
+ *.map
2500
+ *.min.js
2501
+ *.min.css
2502
+ `;
2503
+ var MAX_AUTOPILOT_IGNORE_BYTES = 1e6;
2504
+ var MAX_AUTOPILOT_IGNORE_LINE_CHARS = 4096;
2505
+ var MAX_AUTOPILOT_IGNORE_PATTERNS = 1e4;
2506
+ var ignoreCache = /* @__PURE__ */ new Map();
2507
+ function escapeRegexChar(ch) {
2508
+ return ch.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
2426
2509
  }
2427
- var ReviewEngine = class {
2428
- constructor(store, config) {
2429
- this.store = store;
2430
- this.config = config;
2431
- }
2432
- render(kind, vars) {
2433
- return (this.config.renderFollowup ?? defaultRender)(kind, vars);
2510
+ function globBodyToRegex(glob) {
2511
+ let out = "";
2512
+ for (let i = 0; i < glob.length; ) {
2513
+ const ch = glob[i];
2514
+ if (ch === "*" && glob[i + 1] === "*") {
2515
+ if (glob[i + 2] === "/") {
2516
+ out += "(?:.*/)?";
2517
+ i += 3;
2518
+ continue;
2519
+ }
2520
+ out += ".*";
2521
+ i += 2;
2522
+ continue;
2523
+ }
2524
+ if (ch === "*") {
2525
+ out += "[^/]*";
2526
+ i += 1;
2527
+ continue;
2528
+ }
2529
+ if (ch === "?") {
2530
+ out += "[^/]";
2531
+ i += 1;
2532
+ continue;
2533
+ }
2534
+ out += escapeRegexChar(ch);
2535
+ i += 1;
2434
2536
  }
2435
- lens(roundIndex) {
2436
- const rounds = this.config.confirmRounds;
2437
- return (this.config.resolveLens ?? getLens)(roundIndex, rounds);
2537
+ return out;
2538
+ }
2539
+ function compilePattern(raw) {
2540
+ let line = raw.trim();
2541
+ if (!line || line.startsWith("#")) return null;
2542
+ if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
2543
+ let negated = false;
2544
+ if (line.startsWith("!")) {
2545
+ negated = true;
2546
+ line = line.slice(1).trim();
2547
+ if (!line || line.startsWith("#")) return null;
2438
2548
  }
2439
- /**
2440
- * FS trust root: StateStore is authoritative.
2441
- * config.projectRoot must not widen the boundary (mismatched/evil config).
2442
- */
2443
- trustedProjectRoot() {
2444
- return normalizeProjectRoot(this.store.projectRoot) ?? normalizeProjectRoot(this.config.projectRoot);
2549
+ const dirOnly = line.endsWith("/");
2550
+ if (dirOnly) line = line.slice(0, -1);
2551
+ if (!line) return null;
2552
+ if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
2553
+ let anchored = false;
2554
+ if (line.startsWith("/")) {
2555
+ anchored = true;
2556
+ line = line.slice(1);
2445
2557
  }
2446
- /**
2447
- * Parse session checklist only when realpath stays under the project root.
2448
- * O_NOFOLLOW alone cannot stop intermediate directory symlink escapes or a
2449
- * poisoned absolute checklist_path in the session row.
2450
- */
2451
- parseSessionChecklist(session) {
2452
- const checklistPath = session.checklist_path;
2453
- if (!checklistPath) return null;
2454
- const root = this.trustedProjectRoot();
2455
- if (!root || !isRealpathInsideProject(root, checklistPath)) {
2456
- return null;
2558
+ const body = globBodyToRegex(line);
2559
+ let regexSource;
2560
+ if (anchored) {
2561
+ regexSource = `^${body}`;
2562
+ if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
2563
+ regexSource += "(?:/.*)?";
2457
2564
  }
2458
- try {
2459
- const cl = parseChecklist(checklistPath, { projectRoot: root });
2460
- return {
2461
- unchecked: countUnchecked(cl),
2462
- currentItem: firstUnchecked(cl),
2463
- followingItem: secondUnchecked(cl),
2464
- checklist: cl
2465
- };
2466
- } catch {
2467
- return null;
2565
+ regexSource += "$";
2566
+ } else if (line.includes("/")) {
2567
+ regexSource = `(?:^|.*/)${body}`;
2568
+ if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
2569
+ regexSource += "(?:/.*)?";
2468
2570
  }
2571
+ regexSource += "$";
2572
+ } else {
2573
+ regexSource = `(?:^|.*/)?${body}$`;
2469
2574
  }
2470
- reviewChainResetFields() {
2471
- return {
2472
- confirm_left: null,
2473
- fix_round: 0,
2474
- code_edited: 0,
2475
- item_confirm_complete: 0,
2476
- reviewing_item_id: null
2477
- };
2575
+ return { negated, regex: new RegExp(regexSource) };
2576
+ }
2577
+ function parseAutopilotIgnore(text) {
2578
+ const patterns = [];
2579
+ for (const rawLine of text.split(/\r?\n/)) {
2580
+ const compiled = compilePattern(rawLine);
2581
+ if (!compiled) continue;
2582
+ patterns.push(compiled);
2583
+ if (patterns.length >= MAX_AUTOPILOT_IGNORE_PATTERNS) break;
2478
2584
  }
2479
- renderAdvanceOrDone(targets) {
2480
- const isAdvance = targets.next != null;
2481
- const message = isAdvance ? this.render("advance", {
2482
- currentId: targets.current?.id ?? "",
2585
+ return patterns;
2586
+ }
2587
+ var DEFAULT_AUTOPILOT_IGNORE_PATTERNS = parseAutopilotIgnore(
2588
+ DEFAULT_AUTOPILOT_IGNORE_TEXT
2589
+ );
2590
+ function normalizeRelativePath(filePath) {
2591
+ return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
2592
+ }
2593
+ function isAutopilotIgnoredPath(relativePath, patterns) {
2594
+ const norm = normalizeRelativePath(relativePath);
2595
+ let ignored = false;
2596
+ for (const pat of patterns) {
2597
+ if (pat.regex.test(norm)) {
2598
+ ignored = !pat.negated;
2599
+ }
2600
+ }
2601
+ return ignored;
2602
+ }
2603
+ function realpathForCompare(absPath) {
2604
+ try {
2605
+ return fs7.realpathSync(absPath);
2606
+ } catch {
2607
+ }
2608
+ let dir = path6.dirname(absPath);
2609
+ const base = path6.basename(absPath);
2610
+ const missing = [];
2611
+ for (; ; ) {
2612
+ try {
2613
+ const realDir = fs7.realpathSync(dir);
2614
+ return path6.join(realDir, ...missing, base);
2615
+ } catch {
2616
+ const parent = path6.dirname(dir);
2617
+ if (parent === dir) return absPath;
2618
+ missing.unshift(path6.basename(dir));
2619
+ dir = parent;
2620
+ }
2621
+ }
2622
+ }
2623
+ function toProjectRelativePath(filePath, projectRoot) {
2624
+ const posix = filePath.replace(/\\/g, "/");
2625
+ if (!projectRoot?.trim()) {
2626
+ return normalizeRelativePath(posix);
2627
+ }
2628
+ const root = realpathForCompare(path6.resolve(projectRoot));
2629
+ const abs = realpathForCompare(
2630
+ path6.isAbsolute(posix) ? path6.resolve(posix) : path6.resolve(root, posix)
2631
+ );
2632
+ const rel = path6.relative(root, abs);
2633
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
2634
+ return "";
2635
+ }
2636
+ return normalizeRelativePath(rel.replace(/\\/g, "/"));
2637
+ }
2638
+ function loadAutopilotIgnorePatterns(projectRoot) {
2639
+ const root = path6.resolve(projectRoot);
2640
+ const filePath = path6.join(root, ".autopilotignore");
2641
+ let st;
2642
+ try {
2643
+ st = fs7.lstatSync(filePath);
2644
+ } catch {
2645
+ return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2646
+ }
2647
+ if (st.isSymbolicLink() || !st.isFile()) {
2648
+ return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2649
+ }
2650
+ if (st.size > MAX_AUTOPILOT_IGNORE_BYTES) {
2651
+ return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2652
+ }
2653
+ const cached = ignoreCache.get(root);
2654
+ if (cached && cached.mtimeMs === st.mtimeMs) {
2655
+ return cached.patterns;
2656
+ }
2657
+ let text;
2658
+ try {
2659
+ text = fs7.readFileSync(filePath, "utf8");
2660
+ } catch {
2661
+ return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2662
+ }
2663
+ if (text.length > MAX_AUTOPILOT_IGNORE_BYTES) {
2664
+ return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2665
+ }
2666
+ const patterns = parseAutopilotIgnore(text);
2667
+ ignoreCache.set(root, { mtimeMs: st.mtimeMs, patterns });
2668
+ return patterns;
2669
+ }
2670
+
2671
+ // ../core/src/code-edit-detector.ts
2672
+ function isUntrackedGitIgnored(projectRoot, relativePath) {
2673
+ if (!relativePath || relativePath.includes("\0")) return false;
2674
+ try {
2675
+ const r = spawnSync(
2676
+ "git",
2677
+ ["check-ignore", "-q", "--", relativePath],
2678
+ {
2679
+ cwd: projectRoot,
2680
+ encoding: "utf8",
2681
+ timeout: 5e3,
2682
+ windowsHide: true,
2683
+ shell: false
2684
+ }
2685
+ );
2686
+ return r.status === 0;
2687
+ } catch {
2688
+ return false;
2689
+ }
2690
+ }
2691
+ function isProductCodeEdit(filePath, opts) {
2692
+ const relative = toProjectRelativePath(filePath, opts?.projectRoot);
2693
+ if (!relative) return false;
2694
+ const patterns = opts?.projectRoot?.trim() ? loadAutopilotIgnorePatterns(opts.projectRoot) : DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
2695
+ if (isAutopilotIgnoredPath(relative, patterns)) return false;
2696
+ const root = opts?.projectRoot?.trim();
2697
+ if (root && isUntrackedGitIgnored(root, relative)) return false;
2698
+ return true;
2699
+ }
2700
+ var MAX_DIRTY_PATHS_COLLECT = 2e3;
2701
+ var MAX_DIRTY_PATHS_CLASSIFY = 200;
2702
+ function gitDirtyRelativePaths(projectRoot) {
2703
+ if (!projectRoot || projectRoot.includes("\0")) return [];
2704
+ const out = [];
2705
+ const seen = /* @__PURE__ */ new Set();
2706
+ const pushZ = (stdout) => {
2707
+ for (const raw of stdout.split("\0")) {
2708
+ const p = raw.replace(/\r$/, "");
2709
+ if (!p) continue;
2710
+ if (seen.has(p)) continue;
2711
+ seen.add(p);
2712
+ out.push(p);
2713
+ if (out.length >= MAX_DIRTY_PATHS_COLLECT) return;
2714
+ }
2715
+ };
2716
+ try {
2717
+ const tracked = spawnSync(
2718
+ "git",
2719
+ ["diff", "--name-only", "-z", "HEAD", "--"],
2720
+ {
2721
+ cwd: projectRoot,
2722
+ encoding: "utf8",
2723
+ timeout: 5e3,
2724
+ windowsHide: true,
2725
+ shell: false
2726
+ }
2727
+ );
2728
+ if (tracked.error || tracked.status !== 0) {
2729
+ return [];
2730
+ }
2731
+ if (tracked.stdout) pushZ(tracked.stdout);
2732
+ if (out.length >= MAX_DIRTY_PATHS_COLLECT) return out;
2733
+ const untracked = spawnSync(
2734
+ "git",
2735
+ ["ls-files", "-o", "--exclude-standard", "-z", "--"],
2736
+ {
2737
+ cwd: projectRoot,
2738
+ encoding: "utf8",
2739
+ timeout: 5e3,
2740
+ windowsHide: true,
2741
+ shell: false
2742
+ }
2743
+ );
2744
+ if (!untracked.error && untracked.status === 0 && untracked.stdout) {
2745
+ pushZ(untracked.stdout);
2746
+ }
2747
+ } catch {
2748
+ return [];
2749
+ }
2750
+ return out;
2751
+ }
2752
+ function hasDirtyProductCode(projectRoot) {
2753
+ const root = normalizeProjectRoot(projectRoot);
2754
+ if (!root) return false;
2755
+ const relatives = gitDirtyRelativePaths(root);
2756
+ const patterns = loadAutopilotIgnorePatterns(root);
2757
+ let classified = 0;
2758
+ for (const rel of relatives) {
2759
+ if (!rel || rel.includes("\0") || path7.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
2760
+ continue;
2761
+ }
2762
+ const abs = path7.join(root, rel);
2763
+ const relative = toProjectRelativePath(abs, root);
2764
+ if (!relative) continue;
2765
+ if (isAutopilotIgnoredPath(relative, patterns)) continue;
2766
+ classified += 1;
2767
+ if (classified > MAX_DIRTY_PATHS_CLASSIFY) {
2768
+ return false;
2769
+ }
2770
+ if (isProductCodeEdit(abs, { projectRoot: root })) {
2771
+ return true;
2772
+ }
2773
+ }
2774
+ return false;
2775
+ }
2776
+
2777
+ // ../core/src/review-engine.ts
2778
+ function defaultRender(kind, vars) {
2779
+ switch (kind) {
2780
+ case "review.fix":
2781
+ return `Review fix round ${vars.round} (no hard cap; confirm needs ${vars.total} consecutive no-edit rounds). Code changed this turn. Defect-first self-review and fix now: 1) when using git diff / git status, skip paths matching .autopilotignore, and skip untracked paths ignored by .gitignore; review only the remaining paths; 2) check correctness, null/boundaries, concurrency, security, regression, missing tests; 3) CRITICAL/HIGH must fix, MEDIUM preferably; 4) run relevant tests; 5) briefly state what you reviewed and changed (or "self-review clean"). Do not commit/push. If no further code changes, next stop enters multi-lens confirm.`;
2782
+ case "review.confirm":
2783
+ return `Review confirm ${vars.n}/${vars.total} (session round ${vars.sessionRound}; consecutive no-edit confirms, counted on the fix-round counter). Lens \u3010${vars.lensTitle}\u3011 (multi-lens confirm, not the same checklist again). ${vars.lensFocus} Previous turn had no further code edits. Recheck under this lens only: 1) git diff / git status \u2014 no new edits vs prior turn (or only already-reviewed edits); likewise skip paths matching .autopilotignore and untracked paths ignored by .gitignore; judge only from remaining paths; 2) dig into this lens only; ban vague "fully rechecked, all good"; 3) CRITICAL/HIGH under this lens must fix; MEDIUM preferably; 4) if you edit, fix and run related tests; 5) close with: "Lens (${vars.lensTitle}): self-review clean" or a short list of fixes; if clean, do not edit further. Do not commit/push.`;
2784
+ case "review.confirm_final":
2785
+ return `Review confirm ${vars.n}/${vars.total} (session round ${vars.sessionRound}; consecutive no-edit confirms, counted on the fix-round counter). Lens \u3010${vars.lensTitle}\u3011 (multi-lens confirm, not the same checklist again). ${vars.lensFocus} Previous turn had no further code edits. Recheck under this lens only: 1) git diff / git status \u2014 no new edits vs prior turn (or only already-reviewed edits); likewise skip paths matching .autopilotignore and untracked paths ignored by .gitignore; judge only from remaining paths; 2) dig into this lens only; ban vague "fully rechecked, all good"; 3) read-only: record CRITICAL/HIGH/missing tests \u2014 do not change code, add tests, or commit; if you already edited, accept returning to a fix round; never commit this turn; 4) do not run commands that mutate the repo; 5) close with: "Lens (${vars.lensTitle}): self-review clean" or list issues (no fixes); if clean, do not edit further. Do not commit/push. Handoff (commit) and next checklist item are handled by Advance/Done after the chain \u2014 not this turn.`;
2786
+ case "advance":
2787
+ return `Advance checklist: confirm chain passed cleanly (confirm rounds do not commit). First mark the completed current item ${vars.currentId ?? ""} [x] in checklist.md (do not mark the next item yet). Never mark an item [x] while you are still implementing it \u2014 only this Advance/Done followup checks off the completed item. Then, if the working tree still has uncommitted changes for this item (including checklist.md when plans/ is committed), local conventional commit only: git status/diff \u2192 stage only this checklist item's paths; never git add -A, never stage .env/secrets/.autopilot runtime; one conventional commit; no push/--no-verify/amend/force unless the user explicitly asks. If already clean after marking, skip commit. Then implement next: ${vars.nextId ?? ""} \u2014 ${vars.nextTitle ?? ""}.`;
2788
+ case "done":
2789
+ return `All checklist items done. Confirm chain passed (confirm rounds do not commit). Mark the last item [x]. If the working tree still has uncommitted changes for this item (including checklist.md when plans/ is committed), local conventional commit only (never stage .env/secrets/.autopilot runtime; no push unless the user asks); if clean, just confirm briefly. Phase is done.`;
2790
+ case "recover":
2791
+ return `Recover: the previous turn ended with an error. Continue the current task.`;
2792
+ case "recover_planning":
2793
+ return `Recover: the previous turn ended with an error. Continue planning; do not RUN or write product code.`;
2794
+ case "recover_ambient":
2795
+ return `Recover: the previous turn ended with an error. Continue your current work; Autopilot RUN is not active.`;
2796
+ case "review_complete":
2797
+ return `Review complete. All ${vars.total ?? 5} confirm rounds passed; the review chain has ended. If the working tree still has uncommitted changes from this session, local commit only per the safe checklist (never stage .env/secrets/.autopilot runtime; no push unless the user asks); if clean, briefly confirm only.`;
2798
+ case "stuck":
2799
+ return `Stuck: no progress for several stops. Change strategy or send Autopilot RESUME after fixing.`;
2800
+ case "stuck_soft":
2801
+ return `Stuck: no progress for several stops (missing completion evidence). Change strategy, write matching .autopilot/verify-last.json, then end the turn. Session stays armed \u2014 Autopilot RESUME is not required unless the session was paused.`;
2802
+ case "verify_fix":
2803
+ return `Verify failed (${vars.reason ?? "unknown"}). Fix verify commands and rewrite verify-last.json; do not advance.`;
2804
+ case "need_evidence":
2805
+ return `Need evidence: no-code item ${vars.currentId ?? ""}${vars.currentTitle ? ` \u2014 ${vars.currentTitle}` : ""} cannot advance without matching soft completion evidence. Write .autopilot/verify-last.json with itemId "${vars.currentId ?? ""}" and ok: true (only after this item's work is done). Then end the turn so the stop hook can advance/done. Do not ask the user to continue; do not invent Advance/Done.`;
2806
+ default:
2807
+ return "";
2808
+ }
2809
+ }
2810
+ var ReviewEngine = class {
2811
+ constructor(store, config) {
2812
+ this.store = store;
2813
+ this.config = config;
2814
+ }
2815
+ render(kind, vars) {
2816
+ return (this.config.renderFollowup ?? defaultRender)(kind, vars);
2817
+ }
2818
+ lens(roundIndex) {
2819
+ const rounds = this.config.confirmRounds;
2820
+ return (this.config.resolveLens ?? getLens)(roundIndex, rounds);
2821
+ }
2822
+ /**
2823
+ * FS trust root: StateStore is authoritative.
2824
+ * config.projectRoot must not widen the boundary (mismatched/evil config).
2825
+ */
2826
+ trustedProjectRoot() {
2827
+ return normalizeProjectRoot(this.store.projectRoot) ?? normalizeProjectRoot(this.config.projectRoot);
2828
+ }
2829
+ /**
2830
+ * Parse session checklist only when realpath stays under the project root.
2831
+ * O_NOFOLLOW alone cannot stop intermediate directory symlink escapes or a
2832
+ * poisoned absolute checklist_path in the session row.
2833
+ */
2834
+ parseSessionChecklist(session) {
2835
+ const checklistPath = session.checklist_path;
2836
+ if (!checklistPath) return null;
2837
+ const root = this.trustedProjectRoot();
2838
+ if (!root || !isRealpathInsideProject(root, checklistPath)) {
2839
+ return null;
2840
+ }
2841
+ try {
2842
+ const cl = parseChecklist(checklistPath, { projectRoot: root });
2843
+ return {
2844
+ unchecked: countUnchecked(cl),
2845
+ currentItem: firstUnchecked(cl),
2846
+ followingItem: secondUnchecked(cl),
2847
+ checklist: cl
2848
+ };
2849
+ } catch {
2850
+ return null;
2851
+ }
2852
+ }
2853
+ reviewChainResetFields() {
2854
+ return {
2855
+ confirm_left: null,
2856
+ fix_round: 0,
2857
+ code_edited: 0,
2858
+ item_confirm_complete: 0,
2859
+ reviewing_item_id: null
2860
+ };
2861
+ }
2862
+ renderAdvanceOrDone(targets) {
2863
+ const isAdvance = targets.next != null;
2864
+ const message = isAdvance ? this.render("advance", {
2865
+ currentId: targets.current?.id ?? "",
2483
2866
  currentTitle: targets.current?.title ?? "",
2484
2867
  nextId: targets.next?.id ?? "",
2485
2868
  nextTitle: targets.next?.title ?? ""
@@ -2522,6 +2905,31 @@ var ReviewEngine = class {
2522
2905
  return fromPending;
2523
2906
  });
2524
2907
  }
2908
+ /**
2909
+ * Stop-time arm when Shell (or other non-afterFileEdit) dirtied product paths.
2910
+ * - `clean`: no product dirty → caller may soft E0
2911
+ * - `armed`: code_edited set → caller must prefer E2 (never soft)
2912
+ * - `dirty_unarmed`: dirty seen but arm failed → caller must not soft
2913
+ * (otherwise unreviewed product could soft-advance)
2914
+ */
2915
+ maybeArmCodeEditedFromDirtyTree(session) {
2916
+ const root = this.trustedProjectRoot();
2917
+ if (!root) return "clean";
2918
+ let dirty = false;
2919
+ try {
2920
+ dirty = hasDirtyProductCode(root);
2921
+ } catch {
2922
+ return "dirty_unarmed";
2923
+ }
2924
+ if (!dirty) return "clean";
2925
+ try {
2926
+ this.onCodeEdited(session.conversation_id);
2927
+ } catch {
2928
+ return "dirty_unarmed";
2929
+ }
2930
+ const live = this.store.getReviewChain(session.conversation_id);
2931
+ return live?.code_edited === 1 ? "armed" : "dirty_unarmed";
2932
+ }
2525
2933
  handleStop(input) {
2526
2934
  if (!this.store.isConversationIdOk(input.conversationId)) {
2527
2935
  return null;
@@ -2687,24 +3095,62 @@ var ReviewEngine = class {
2687
3095
  if (chainNow.confirm_left === null && chainNow.item_confirm_complete === 0 && inChain) {
2688
3096
  return this.e3ArmConfirm(session, chainNow);
2689
3097
  }
2690
- return this.e0NoCodeContinue(session, chainNow);
2691
- } catch (err) {
2692
- const msg = err instanceof Error ? err.message : String(err);
2693
- if (msg.includes("No session for conversation") || msg.includes("Invalid conversation id")) {
2694
- return null;
2695
- }
2696
- throw err;
2697
- }
2698
- }
2699
- pendingBlocksAdvance(conversationId, chain, events, transcriptPath) {
2700
- const pending = chain.pending_followup?.trim();
2701
- if (!pending) return false;
2702
- if (events.length > 0 && automationFollowupPresent(events, pending)) {
2703
- const cleared = this.clearMatchingPendingAndArmAmbient(
2704
- conversationId,
2705
- pending
2706
- );
2707
- if (!cleared) {
3098
+ if (chainNow.code_edited === 0 && isChecklistExecuting(session)) {
3099
+ const dirtyArm = this.maybeArmCodeEditedFromDirtyTree(session);
3100
+ if (dirtyArm === "dirty_unarmed") {
3101
+ return null;
3102
+ }
3103
+ if (dirtyArm === "armed") {
3104
+ const armed = this.store.getReviewChain(session.conversation_id) ?? chainNow;
3105
+ if (armed.code_edited === 1) {
3106
+ const tip = armed.pending_followup?.trim() ?? "";
3107
+ if (isRecoverOrStuckFollowupMessage(tip)) {
3108
+ const again = this.tryRedeliverPending(
3109
+ session.conversation_id,
3110
+ armed,
3111
+ events,
3112
+ transcriptPath
3113
+ );
3114
+ if (again) return again;
3115
+ return null;
3116
+ }
3117
+ const fix = this.e2Fix(session, armed);
3118
+ if (fix) return fix;
3119
+ const afterE2 = this.store.getReviewChain(session.conversation_id) ?? armed;
3120
+ const afterTip = afterE2.pending_followup?.trim() ?? "";
3121
+ if (isRecoverOrStuckFollowupMessage(afterTip)) {
3122
+ const again = this.tryRedeliverPending(
3123
+ session.conversation_id,
3124
+ afterE2,
3125
+ events,
3126
+ transcriptPath
3127
+ );
3128
+ if (again) return again;
3129
+ return null;
3130
+ }
3131
+ return null;
3132
+ }
3133
+ return null;
3134
+ }
3135
+ }
3136
+ return this.e0NoCodeContinue(session, chainNow);
3137
+ } catch (err) {
3138
+ const msg = err instanceof Error ? err.message : String(err);
3139
+ if (msg.includes("No session for conversation") || msg.includes("Invalid conversation id")) {
3140
+ return null;
3141
+ }
3142
+ throw err;
3143
+ }
3144
+ }
3145
+ pendingBlocksAdvance(conversationId, chain, events, transcriptPath) {
3146
+ const pending = chain.pending_followup?.trim();
3147
+ if (!pending) return false;
3148
+ if (events.length > 0 && automationFollowupPresent(events, pending)) {
3149
+ const cleared = this.clearMatchingPendingAndArmAmbient(
3150
+ conversationId,
3151
+ pending
3152
+ );
3153
+ if (!cleared) {
2708
3154
  try {
2709
3155
  const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
2710
3156
  if (live) return true;
@@ -3202,10 +3648,10 @@ var ReviewEngine = class {
3202
3648
  * salvage again — the prior tip does not cover the new failure.
3203
3649
  */
3204
3650
  classifyCompletedOrphan(transcriptPath) {
3205
- const path10 = transcriptPath?.trim();
3206
- if (!path10) return "none";
3651
+ const path11 = transcriptPath?.trim();
3652
+ if (!path11) return "none";
3207
3653
  try {
3208
- const events = readTranscriptTail(path10);
3654
+ const events = readTranscriptTail(path11);
3209
3655
  const errIdx = latestUnresolvedTurnEndedErrorIndex(events);
3210
3656
  if (errIdx < 0) return "none";
3211
3657
  for (let i = events.length - 1; i > errIdx; i--) {
@@ -3919,28 +4365,16 @@ var ReviewEngine = class {
3919
4365
  }
3920
4366
  const nextIdle = sess.idle_stop_count + 1;
3921
4367
  const nowStuck = nextIdle >= this.config.maxIdleStops;
3922
- if (nowStuck) {
3923
- this.store.upsertSession({
3924
- conversation_id: cid2,
3925
- project_root: sess.project_root,
3926
- code_root: sess.code_root,
3927
- idle_stop_count: nextIdle,
3928
- paused: 1,
3929
- paused_reason: "stuck",
3930
- armed: 0
3931
- });
3932
- } else {
3933
- this.store.upsertSession({
3934
- conversation_id: cid2,
3935
- project_root: sess.project_root,
3936
- code_root: sess.code_root,
3937
- idle_stop_count: nextIdle
3938
- });
3939
- }
4368
+ this.store.upsertSession({
4369
+ conversation_id: cid2,
4370
+ project_root: sess.project_root,
4371
+ code_root: sess.code_root,
4372
+ idle_stop_count: nextIdle
4373
+ });
3940
4374
  const kind = nowStuck ? "stuck" : "need_evidence";
3941
4375
  const title = (lockedItem.title ?? "").trim();
3942
4376
  const message = this.render(
3943
- kind,
4377
+ nowStuck ? "stuck_soft" : "need_evidence",
3944
4378
  nowStuck ? {} : {
3945
4379
  currentId: lockedItem.id,
3946
4380
  currentTitle: title,
@@ -4299,8 +4733,8 @@ var ReviewEngine = class {
4299
4733
  let unchecked = checklist.unchecked;
4300
4734
  let next = checklist.next;
4301
4735
  let targets = null;
4302
- const path10 = lockedSession.checklist_path?.trim() ?? "";
4303
- const onChecklistPath = isChecklistExecuting(lockedSession) && path10.length > 0;
4736
+ const path11 = lockedSession.checklist_path?.trim() ?? "";
4737
+ const onChecklistPath = isChecklistExecuting(lockedSession) && path11.length > 0;
4304
4738
  if (onChecklistPath) {
4305
4739
  const refreshed = this.parseSessionChecklist(lockedSession);
4306
4740
  if (!refreshed?.checklist) {
@@ -4432,28 +4866,7 @@ var ReviewEngine = class {
4432
4866
  } catch {
4433
4867
  }
4434
4868
  }
4435
- incrementIdle(session) {
4436
- const next = session.idle_stop_count + 1;
4437
- if (next >= this.config.maxIdleStops) {
4438
- this.store.upsertSession({
4439
- conversation_id: session.conversation_id,
4440
- project_root: session.project_root,
4441
- code_root: session.code_root,
4442
- idle_stop_count: next,
4443
- paused: 1,
4444
- paused_reason: "stuck",
4445
- armed: 0
4446
- });
4447
- } else {
4448
- this.store.upsertSession({
4449
- conversation_id: session.conversation_id,
4450
- project_root: session.project_root,
4451
- code_root: session.code_root,
4452
- idle_stop_count: next
4453
- });
4454
- }
4455
- }
4456
- /** After a no-progress stop that didn't inject, check stuck threshold. */
4869
+ /** After a hard-stuck pause that didn't inject, surface stuck tip. */
4457
4870
  checkStuck(session) {
4458
4871
  const fresh = this.store.getSession(session.conversation_id);
4459
4872
  if (!fresh) return null;
@@ -4507,10 +4920,11 @@ function applyOn(store, conversationId, projectRoot, opts) {
4507
4920
  }
4508
4921
  projectRoot = root;
4509
4922
  const session = store.getSession(conversationId);
4923
+ const onBlockedMsg = "Autopilot is executing. Send Autopilot OFF, REPLAN, or RESUME before ON.";
4510
4924
  if (session?.phase === "executing") {
4511
4925
  return {
4512
4926
  ok: false,
4513
- userMessage: "Autopilot is executing. Send Autopilot OFF, REPLAN, or RESUME before ON."
4927
+ userMessage: onBlockedMsg
4514
4928
  };
4515
4929
  }
4516
4930
  if (opts?.slug !== void 0 && !isSafeTrackSlug(opts.slug)) {
@@ -4520,46 +4934,39 @@ function applyOn(store, conversationId, projectRoot, opts) {
4520
4934
  userMessage: `Invalid track slug "${sanitizeSessionDisplayText(raw).slice(0, 64)}".`
4521
4935
  };
4522
4936
  }
4523
- const prevTid = session?.track_id ?? "_pending";
4524
- const trackId = opts?.slug ?? (session?.track_id && isBoundRunTrackId(session.track_id) ? session.track_id : "_pending");
4525
- const checklistPath = trackId === prevTid ? session?.checklist_path ?? "" : "";
4526
- const platform = resolveSessionPlatform(
4527
- opts?.platform,
4528
- session?.platform ?? "cursor"
4529
- );
4530
- if (session?.phase === "done") {
4531
- const s2 = store.upsertSession({
4937
+ return store.exclusiveWrite(() => {
4938
+ const live = store.getSession(conversationId);
4939
+ if (live?.phase === "executing") {
4940
+ return {
4941
+ commit: false,
4942
+ value: { ok: false, userMessage: onBlockedMsg }
4943
+ };
4944
+ }
4945
+ const prevTid = live?.track_id ?? "_pending";
4946
+ const trackId = opts?.slug ?? (live?.track_id && isBoundRunTrackId(live.track_id) ? live.track_id : "_pending");
4947
+ const checklistPath = trackId === prevTid ? live?.checklist_path ?? "" : "";
4948
+ const platform = resolveSessionPlatform(
4949
+ opts?.platform,
4950
+ live?.platform ?? "cursor"
4951
+ );
4952
+ const row = store.upsertSession({
4532
4953
  conversation_id: conversationId,
4533
4954
  project_root: projectRoot,
4534
4955
  code_root: projectRoot,
4956
+ platform,
4535
4957
  phase: "planning",
4536
4958
  armed: 0,
4537
4959
  paused: 0,
4538
4960
  paused_reason: null,
4539
4961
  track_id: trackId,
4540
4962
  checklist_path: checklistPath,
4963
+ // ON returns to planning — drop mid-flow run/replan pick state.
4541
4964
  pending_action: null,
4542
- track_candidates_json: null,
4543
- platform
4965
+ track_candidates_json: null
4544
4966
  });
4545
- return { ok: true, session: s2 };
4546
- }
4547
- const s = store.upsertSession({
4548
- conversation_id: conversationId,
4549
- project_root: projectRoot,
4550
- code_root: projectRoot,
4551
- platform,
4552
- phase: "planning",
4553
- armed: 0,
4554
- paused: 0,
4555
- paused_reason: null,
4556
- track_id: trackId,
4557
- checklist_path: checklistPath,
4558
- // ON returns to planning — drop mid-flow run/replan pick state.
4559
- pending_action: null,
4560
- track_candidates_json: null
4967
+ store.clearPendingFollowupIf(conversationId, isTerminalFollowupMessage);
4968
+ return { commit: true, value: { ok: true, session: row } };
4561
4969
  });
4562
- return { ok: true, session: s };
4563
4970
  }
4564
4971
  function finishLocalResume(store, conversationId, session) {
4565
4972
  const snap = store.getSession(conversationId);
@@ -4664,8 +5071,8 @@ function applyResumeReview(store, conversationId) {
4664
5071
  }
4665
5072
 
4666
5073
  // ../core/src/project-config.ts
4667
- import fs7 from "node:fs";
4668
- import path6 from "node:path";
5074
+ import fs8 from "node:fs";
5075
+ import path8 from "node:path";
4669
5076
  var MAX_CONFIG_BYTES = 1e6;
4670
5077
  var DEFAULT_PROJECT_REVIEW_CONFIG = {
4671
5078
  confirmRounds: 5,
@@ -4825,28 +5232,28 @@ function loadProjectReviewConfig(projectRoot) {
4825
5232
  if (!root) {
4826
5233
  return cloneDefaultProjectReviewConfig();
4827
5234
  }
4828
- const configPath = path6.join(root, ".autopilot", "config.yml");
5235
+ const configPath = path8.join(root, ".autopilot", "config.yml");
4829
5236
  try {
4830
- const nofollow = typeof fs7.constants.O_NOFOLLOW === "number" ? fs7.constants.O_NOFOLLOW : 0;
5237
+ const nofollow = typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
4831
5238
  if (nofollow === 0) {
4832
- if (!fs7.existsSync(configPath)) return cloneDefaultProjectReviewConfig();
4833
- if (fs7.lstatSync(configPath).isSymbolicLink()) {
5239
+ if (!fs8.existsSync(configPath)) return cloneDefaultProjectReviewConfig();
5240
+ if (fs8.lstatSync(configPath).isSymbolicLink()) {
4834
5241
  return cloneDefaultProjectReviewConfig();
4835
5242
  }
4836
5243
  }
4837
5244
  let fd;
4838
5245
  try {
4839
- fd = fs7.openSync(configPath, fs7.constants.O_RDONLY | nofollow);
5246
+ fd = fs8.openSync(configPath, fs8.constants.O_RDONLY | nofollow);
4840
5247
  } catch {
4841
5248
  return cloneDefaultProjectReviewConfig();
4842
5249
  }
4843
5250
  let raw;
4844
5251
  try {
4845
- const st = fs7.fstatSync(fd);
5252
+ const st = fs8.fstatSync(fd);
4846
5253
  if (!st.isFile() || st.size > MAX_CONFIG_BYTES) {
4847
5254
  return cloneDefaultProjectReviewConfig();
4848
5255
  }
4849
- const lst = fs7.lstatSync(configPath);
5256
+ const lst = fs8.lstatSync(configPath);
4850
5257
  if (lst.isSymbolicLink() || !lst.isFile()) {
4851
5258
  return cloneDefaultProjectReviewConfig();
4852
5259
  }
@@ -4857,10 +5264,10 @@ function loadProjectReviewConfig(projectRoot) {
4857
5264
  return cloneDefaultProjectReviewConfig();
4858
5265
  }
4859
5266
  const buf = Buffer.alloc(st.size);
4860
- const n = fs7.readSync(fd, buf, 0, st.size, 0);
5267
+ const n = fs8.readSync(fd, buf, 0, st.size, 0);
4861
5268
  raw = buf.subarray(0, n).toString("utf8");
4862
5269
  } finally {
4863
- fs7.closeSync(fd);
5270
+ fs8.closeSync(fd);
4864
5271
  }
4865
5272
  if (Buffer.byteLength(raw, "utf8") > MAX_CONFIG_BYTES) {
4866
5273
  return cloneDefaultProjectReviewConfig();
@@ -4960,6 +5367,12 @@ function createRenderFollowup(bundle) {
4960
5367
  );
4961
5368
  case "stuck":
4962
5369
  return renderTemplate(f.stuck ?? "", vars);
5370
+ case "stuck_soft":
5371
+ return renderTemplate(
5372
+ // Never fall back to hard `stuck` (RESUME-required) — that defeats C2.
5373
+ f.stuck_soft ?? "Stuck: no progress for several stops (missing completion evidence). Change strategy, write matching .autopilot/verify-last.json, then end the turn. Session stays armed \u2014 Autopilot RESUME is not required unless the session was paused.",
5374
+ vars
5375
+ );
4963
5376
  case "verify_fix":
4964
5377
  return renderTemplate(
4965
5378
  f.verify_fix ?? "Verify failed ({reason}). Fix verify commands and rewrite verify-last.json; do not advance.",
@@ -5012,12 +5425,12 @@ function createConfiguredReviewEngine(store, projectRoot, localeBundle, preloade
5012
5425
  }
5013
5426
 
5014
5427
  // ../core/src/phase-actions.ts
5015
- import fs9 from "node:fs";
5016
- import path8 from "node:path";
5428
+ import fs10 from "node:fs";
5429
+ import path10 from "node:path";
5017
5430
 
5018
5431
  // ../core/src/list-tracks.ts
5019
- import fs8 from "node:fs";
5020
- import path7 from "node:path";
5432
+ import fs9 from "node:fs";
5433
+ import path9 from "node:path";
5021
5434
  function isRunnableTrack(t) {
5022
5435
  if (t.paused) return false;
5023
5436
  const unchecked = t.checklistTotal - t.checklistDone;
@@ -5028,19 +5441,19 @@ function readPlansDir(root, plansDir = "plans") {
5028
5441
  if (typeof plansDir !== "string" || !root || root.includes("\0") || plansDir.includes("\0")) {
5029
5442
  return [];
5030
5443
  }
5031
- const dir = path7.join(root, plansDir);
5444
+ const dir = path9.join(root, plansDir);
5032
5445
  try {
5033
- const lst = fs8.lstatSync(dir);
5446
+ const lst = fs9.lstatSync(dir);
5034
5447
  if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
5035
5448
  if (!isRealpathInsideProject(root, dir)) return [];
5036
5449
  } catch {
5037
5450
  return [];
5038
5451
  }
5039
- const names = fs8.readdirSync(dir, { withFileTypes: true }).filter(
5452
+ const names = fs9.readdirSync(dir, { withFileTypes: true }).filter(
5040
5453
  (d) => d.isDirectory() && !d.isSymbolicLink() && isSafeTrackSlug(d.name)
5041
5454
  ).map((d) => d.name);
5042
5455
  try {
5043
- const lst = fs8.lstatSync(dir);
5456
+ const lst = fs9.lstatSync(dir);
5044
5457
  if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
5045
5458
  if (!isRealpathInsideProject(root, dir)) return [];
5046
5459
  } catch {
@@ -5054,27 +5467,27 @@ function titleFromPlan(planPath, slug, projectRoot) {
5054
5467
  if (!root) return slug;
5055
5468
  const maxBytes = 65536;
5056
5469
  try {
5057
- const nofollow = typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
5470
+ const nofollow = typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
5058
5471
  if (nofollow === 0) {
5059
- const lst = fs8.lstatSync(planPath);
5472
+ const lst = fs9.lstatSync(planPath);
5060
5473
  if (lst.isSymbolicLink() || !lst.isFile()) return slug;
5061
5474
  }
5062
- const fd = fs8.openSync(planPath, fs8.constants.O_RDONLY | nofollow);
5475
+ const fd = fs9.openSync(planPath, fs9.constants.O_RDONLY | nofollow);
5063
5476
  try {
5064
- const st = fs8.fstatSync(fd);
5477
+ const st = fs9.fstatSync(fd);
5065
5478
  if (!st.isFile() || st.size <= 0) return slug;
5066
- const lst = fs8.lstatSync(planPath);
5479
+ const lst = fs9.lstatSync(planPath);
5067
5480
  if (lst.isSymbolicLink() || !lst.isFile()) return slug;
5068
5481
  if (lst.ino !== st.ino || lst.dev !== st.dev) return slug;
5069
5482
  if (!isRealpathInsideProject(root, planPath)) return slug;
5070
5483
  const len = Math.min(st.size, maxBytes);
5071
5484
  const buf = Buffer.alloc(len);
5072
- const n = fs8.readSync(fd, buf, 0, len, 0);
5485
+ const n = fs9.readSync(fd, buf, 0, len, 0);
5073
5486
  const first = buf.subarray(0, n).toString("utf8").split(/\r?\n/)[0] ?? "";
5074
5487
  const m = first.match(/^#\s+(.+)/);
5075
5488
  return m?.[1]?.trim() ?? slug;
5076
5489
  } finally {
5077
- fs8.closeSync(fd);
5490
+ fs9.closeSync(fd);
5078
5491
  }
5079
5492
  } catch {
5080
5493
  return slug;
@@ -5090,16 +5503,16 @@ function listTracks(root, store, filter = "all", plansDir = "plans") {
5090
5503
  const slugs = readPlansDir(root, plansDir);
5091
5504
  const tracks = [];
5092
5505
  for (const slug of slugs) {
5093
- const trackDir = path7.join(root, plansDir, slug);
5506
+ const trackDir = path9.join(root, plansDir, slug);
5094
5507
  try {
5095
- const lst = fs8.lstatSync(trackDir);
5508
+ const lst = fs9.lstatSync(trackDir);
5096
5509
  if (lst.isSymbolicLink() || !lst.isDirectory()) continue;
5097
5510
  if (!isRealpathInsideProject(root, trackDir)) continue;
5098
5511
  } catch {
5099
5512
  continue;
5100
5513
  }
5101
- const planPath = path7.join(trackDir, "plan.md");
5102
- const checklistPath = path7.join(trackDir, "checklist.md");
5514
+ const planPath = path9.join(trackDir, "plan.md");
5515
+ const checklistPath = path9.join(trackDir, "checklist.md");
5103
5516
  let checklistTotal = 0;
5104
5517
  let checklistDone = 0;
5105
5518
  const checklistInProject = isRealpathInsideProject(root, checklistPath);
@@ -5171,7 +5584,7 @@ function canEnterExecuting(options) {
5171
5584
  if (!root) {
5172
5585
  return { ok: false, reason: "invalid project root" };
5173
5586
  }
5174
- if (!checklistPath || !fs8.existsSync(checklistPath)) {
5587
+ if (!checklistPath || !fs9.existsSync(checklistPath)) {
5175
5588
  return { ok: false, reason: "checklist missing" };
5176
5589
  }
5177
5590
  if (!isRealpathInsideProject(root, checklistPath)) {
@@ -5220,8 +5633,8 @@ function nowIso2() {
5220
5633
  }
5221
5634
  function checklistPathFor(projectRoot, slug, plansDir) {
5222
5635
  const root = normalizeProjectRoot(projectRoot);
5223
- if (!root) return path8.join(plansDir, slug, "checklist.md");
5224
- return path8.join(root, plansDir, slug, "checklist.md");
5636
+ if (!root) return path10.join(plansDir, slug, "checklist.md");
5637
+ return path10.join(root, plansDir, slug, "checklist.md");
5225
5638
  }
5226
5639
  function sameChecklistBinding(stored, rebuilt, projectRoot) {
5227
5640
  if (stored === rebuilt) return true;
@@ -5231,8 +5644,8 @@ function sameChecklistBinding(stored, rebuilt, projectRoot) {
5231
5644
  const root = normalizeProjectRoot(projectRoot);
5232
5645
  if (!root) return false;
5233
5646
  try {
5234
- const absStored = path8.isAbsolute(stored) ? path8.resolve(stored) : path8.resolve(root, stored);
5235
- const absRebuilt = path8.isAbsolute(rebuilt) ? path8.resolve(rebuilt) : path8.resolve(root, rebuilt);
5647
+ const absStored = path10.isAbsolute(stored) ? path10.resolve(stored) : path10.resolve(root, stored);
5648
+ const absRebuilt = path10.isAbsolute(rebuilt) ? path10.resolve(rebuilt) : path10.resolve(root, rebuilt);
5236
5649
  return absStored === absRebuilt;
5237
5650
  } catch {
5238
5651
  return false;
@@ -5252,8 +5665,8 @@ function isChecklistPathAllowed(projectRoot, checklistPath) {
5252
5665
  const root = normalizeProjectRoot(projectRoot);
5253
5666
  if (!root) return false;
5254
5667
  try {
5255
- fs9.lstatSync(
5256
- path8.isAbsolute(checklistPath) ? checklistPath : path8.resolve(root, checklistPath)
5668
+ fs10.lstatSync(
5669
+ path10.isAbsolute(checklistPath) ? checklistPath : path10.resolve(root, checklistPath)
5257
5670
  );
5258
5671
  return isRealpathInsideProject(root, checklistPath);
5259
5672
  } catch {
@@ -5290,8 +5703,8 @@ function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
5290
5703
  const ts = nowIso2();
5291
5704
  const root = normalizeProjectRoot(projectRoot);
5292
5705
  if (!root) return;
5293
- const planPath = path8.join(root, plansDir, slug, "plan.md");
5294
- const briefPath = path8.join(root, plansDir, slug, "brief.md");
5706
+ const planPath = path10.join(root, plansDir, slug, "plan.md");
5707
+ const briefPath = path10.join(root, plansDir, slug, "brief.md");
5295
5708
  store.db.prepare(
5296
5709
  `INSERT INTO tracks (track_id, slug, checklist_path, plan_path, brief_path, updated_at)
5297
5710
  VALUES (?, ?, ?, ?, ?, ?)
@@ -5305,8 +5718,8 @@ function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
5305
5718
  slug,
5306
5719
  slug,
5307
5720
  checklistPath,
5308
- fs9.existsSync(planPath) ? planPath : null,
5309
- fs9.existsSync(briefPath) ? briefPath : null,
5721
+ fs10.existsSync(planPath) ? planPath : null,
5722
+ fs10.existsSync(briefPath) ? briefPath : null,
5310
5723
  ts
5311
5724
  );
5312
5725
  }
@@ -5711,303 +6124,6 @@ function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
5711
6124
  });
5712
6125
  }
5713
6126
 
5714
- // ../core/src/code-edit-detector.ts
5715
- import { spawnSync } from "node:child_process";
5716
-
5717
- // ../core/src/autopilot-ignore.ts
5718
- import fs10 from "node:fs";
5719
- import path9 from "node:path";
5720
- var DEFAULT_AUTOPILOT_IGNORE_TEXT = `# Autopilot \u2014 paths that do NOT trigger self-review (gitignore syntax).
5721
- #
5722
- # What this file is:
5723
- # - Controls whether an afterFileEdit counts as "product code" (opens fix/confirm).
5724
- # - Does NOT change \`git diff\` / \`git status\` output (that is \`.gitignore\`).
5725
- # - Review followups ask the agent to skip these paths when reading diffs (soft).
5726
- #
5727
- # Semantics:
5728
- # - Same glob rules as gitignore; last matching pattern wins.
5729
- # - Use \`!\` to force-include an exception (e.g. \`!docs/feed/**/*.yml\`).
5730
- # - Markdown (*.md / *.mdx) is NOT ignored by default \u2014 design docs can be reviewed.
5731
- # - \`docs/**\` is NOT ignored by default.
5732
- # - Also skip untracked paths ignored by \`.gitignore\` (tracked files still count).
5733
- #
5734
- # Later (not implemented): hard-filtered review-diff / path ledger \u2014 see
5735
- # docs/autopilot/workflows/autopilot-executing.md (B2 strong).
5736
-
5737
- # Runtime / editor (prefer also listing these in .gitignore)
5738
- .autopilot/**
5739
- .cursor/**
5740
- .claude/**
5741
-
5742
- # Planning artifacts
5743
- plans/**
5744
-
5745
- # Common build / vendor trees
5746
- node_modules/**
5747
- dist/**
5748
- build/**
5749
- out/**
5750
- target/**
5751
- .target/**
5752
- coverage/**
5753
- .venv/**
5754
- venv/**
5755
- __pycache__/**
5756
-
5757
- # Lockfiles / package manager noise
5758
- package-lock.json
5759
- pnpm-lock.yaml
5760
- yarn.lock
5761
- bun.lock
5762
- bun.lockb
5763
- Cargo.lock
5764
- poetry.lock
5765
- composer.lock
5766
-
5767
- # Media / binary (do not trigger self-review)
5768
- *.png
5769
- *.jpg
5770
- *.jpeg
5771
- *.gif
5772
- *.webp
5773
- *.ico
5774
- *.svg
5775
- *.bmp
5776
- *.mp3
5777
- *.mp4
5778
- *.wav
5779
- *.webm
5780
- *.mov
5781
- *.woff
5782
- *.woff2
5783
- *.ttf
5784
- *.otf
5785
- *.eot
5786
- *.pdf
5787
- *.zip
5788
- *.gz
5789
- *.tgz
5790
- *.7z
5791
- *.rar
5792
- *.jar
5793
- *.class
5794
- *.o
5795
- *.a
5796
- *.so
5797
- *.dylib
5798
- *.dll
5799
- *.exe
5800
- *.wasm
5801
-
5802
- # Prose / data noise
5803
- *.txt
5804
- *.html
5805
- *.htm
5806
- *.csv
5807
- *.tsv
5808
- *.log
5809
- *.map
5810
- *.min.js
5811
- *.min.css
5812
- `;
5813
- var MAX_AUTOPILOT_IGNORE_BYTES = 1e6;
5814
- var MAX_AUTOPILOT_IGNORE_LINE_CHARS = 4096;
5815
- var MAX_AUTOPILOT_IGNORE_PATTERNS = 1e4;
5816
- var ignoreCache = /* @__PURE__ */ new Map();
5817
- function escapeRegexChar(ch) {
5818
- return ch.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
5819
- }
5820
- function globBodyToRegex(glob) {
5821
- let out = "";
5822
- for (let i = 0; i < glob.length; ) {
5823
- const ch = glob[i];
5824
- if (ch === "*" && glob[i + 1] === "*") {
5825
- if (glob[i + 2] === "/") {
5826
- out += "(?:.*/)?";
5827
- i += 3;
5828
- continue;
5829
- }
5830
- out += ".*";
5831
- i += 2;
5832
- continue;
5833
- }
5834
- if (ch === "*") {
5835
- out += "[^/]*";
5836
- i += 1;
5837
- continue;
5838
- }
5839
- if (ch === "?") {
5840
- out += "[^/]";
5841
- i += 1;
5842
- continue;
5843
- }
5844
- out += escapeRegexChar(ch);
5845
- i += 1;
5846
- }
5847
- return out;
5848
- }
5849
- function compilePattern(raw) {
5850
- let line = raw.trim();
5851
- if (!line || line.startsWith("#")) return null;
5852
- if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
5853
- let negated = false;
5854
- if (line.startsWith("!")) {
5855
- negated = true;
5856
- line = line.slice(1).trim();
5857
- if (!line || line.startsWith("#")) return null;
5858
- }
5859
- const dirOnly = line.endsWith("/");
5860
- if (dirOnly) line = line.slice(0, -1);
5861
- if (!line) return null;
5862
- if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
5863
- let anchored = false;
5864
- if (line.startsWith("/")) {
5865
- anchored = true;
5866
- line = line.slice(1);
5867
- }
5868
- const body = globBodyToRegex(line);
5869
- let regexSource;
5870
- if (anchored) {
5871
- regexSource = `^${body}`;
5872
- if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
5873
- regexSource += "(?:/.*)?";
5874
- }
5875
- regexSource += "$";
5876
- } else if (line.includes("/")) {
5877
- regexSource = `(?:^|.*/)${body}`;
5878
- if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
5879
- regexSource += "(?:/.*)?";
5880
- }
5881
- regexSource += "$";
5882
- } else {
5883
- regexSource = `(?:^|.*/)?${body}$`;
5884
- }
5885
- return { negated, regex: new RegExp(regexSource) };
5886
- }
5887
- function parseAutopilotIgnore(text) {
5888
- const patterns = [];
5889
- for (const rawLine of text.split(/\r?\n/)) {
5890
- const compiled = compilePattern(rawLine);
5891
- if (!compiled) continue;
5892
- patterns.push(compiled);
5893
- if (patterns.length >= MAX_AUTOPILOT_IGNORE_PATTERNS) break;
5894
- }
5895
- return patterns;
5896
- }
5897
- var DEFAULT_AUTOPILOT_IGNORE_PATTERNS = parseAutopilotIgnore(
5898
- DEFAULT_AUTOPILOT_IGNORE_TEXT
5899
- );
5900
- function normalizeRelativePath(filePath) {
5901
- return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
5902
- }
5903
- function isAutopilotIgnoredPath(relativePath, patterns) {
5904
- const norm = normalizeRelativePath(relativePath);
5905
- let ignored = false;
5906
- for (const pat of patterns) {
5907
- if (pat.regex.test(norm)) {
5908
- ignored = !pat.negated;
5909
- }
5910
- }
5911
- return ignored;
5912
- }
5913
- function realpathForCompare(absPath) {
5914
- try {
5915
- return fs10.realpathSync(absPath);
5916
- } catch {
5917
- }
5918
- let dir = path9.dirname(absPath);
5919
- const base = path9.basename(absPath);
5920
- const missing = [];
5921
- for (; ; ) {
5922
- try {
5923
- const realDir = fs10.realpathSync(dir);
5924
- return path9.join(realDir, ...missing, base);
5925
- } catch {
5926
- const parent = path9.dirname(dir);
5927
- if (parent === dir) return absPath;
5928
- missing.unshift(path9.basename(dir));
5929
- dir = parent;
5930
- }
5931
- }
5932
- }
5933
- function toProjectRelativePath(filePath, projectRoot) {
5934
- const posix = filePath.replace(/\\/g, "/");
5935
- if (!projectRoot?.trim()) {
5936
- return normalizeRelativePath(posix);
5937
- }
5938
- const root = realpathForCompare(path9.resolve(projectRoot));
5939
- const abs = realpathForCompare(
5940
- path9.isAbsolute(posix) ? path9.resolve(posix) : path9.resolve(root, posix)
5941
- );
5942
- const rel = path9.relative(root, abs);
5943
- if (rel.startsWith("..") || path9.isAbsolute(rel)) {
5944
- return "";
5945
- }
5946
- return normalizeRelativePath(rel.replace(/\\/g, "/"));
5947
- }
5948
- function loadAutopilotIgnorePatterns(projectRoot) {
5949
- const root = path9.resolve(projectRoot);
5950
- const filePath = path9.join(root, ".autopilotignore");
5951
- let st;
5952
- try {
5953
- st = fs10.lstatSync(filePath);
5954
- } catch {
5955
- return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
5956
- }
5957
- if (st.isSymbolicLink() || !st.isFile()) {
5958
- return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
5959
- }
5960
- if (st.size > MAX_AUTOPILOT_IGNORE_BYTES) {
5961
- return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
5962
- }
5963
- const cached = ignoreCache.get(root);
5964
- if (cached && cached.mtimeMs === st.mtimeMs) {
5965
- return cached.patterns;
5966
- }
5967
- let text;
5968
- try {
5969
- text = fs10.readFileSync(filePath, "utf8");
5970
- } catch {
5971
- return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
5972
- }
5973
- if (text.length > MAX_AUTOPILOT_IGNORE_BYTES) {
5974
- return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
5975
- }
5976
- const patterns = parseAutopilotIgnore(text);
5977
- ignoreCache.set(root, { mtimeMs: st.mtimeMs, patterns });
5978
- return patterns;
5979
- }
5980
-
5981
- // ../core/src/code-edit-detector.ts
5982
- function isUntrackedGitIgnored(projectRoot, relativePath) {
5983
- if (!relativePath || relativePath.includes("\0")) return false;
5984
- try {
5985
- const r = spawnSync(
5986
- "git",
5987
- ["check-ignore", "-q", "--", relativePath],
5988
- {
5989
- cwd: projectRoot,
5990
- encoding: "utf8",
5991
- timeout: 5e3,
5992
- windowsHide: true,
5993
- shell: false
5994
- }
5995
- );
5996
- return r.status === 0;
5997
- } catch {
5998
- return false;
5999
- }
6000
- }
6001
- function isProductCodeEdit(filePath, opts) {
6002
- const relative = toProjectRelativePath(filePath, opts?.projectRoot);
6003
- if (!relative) return false;
6004
- const patterns = opts?.projectRoot?.trim() ? loadAutopilotIgnorePatterns(opts.projectRoot) : DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
6005
- if (isAutopilotIgnoredPath(relative, patterns)) return false;
6006
- const root = opts?.projectRoot?.trim();
6007
- if (root && isUntrackedGitIgnored(root, relative)) return false;
6008
- return true;
6009
- }
6010
-
6011
6127
  // ../ports/cursor/src/index.ts
6012
6128
  function normalizeBlockSubmitMessage(message) {
6013
6129
  return typeof message === "string" && message.trim().length > 0 ? message : "Request blocked.";