@autopilot-harness/cli 0.2.12 → 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.
- package/dist/assets/vendor/runtime.mjs +1097 -980
- package/dist/init/types.d.ts +1 -1
- package/dist/init/types.js +1 -1
- package/package.json +5 -5
|
@@ -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"
|
|
@@ -2398,6 +2400,380 @@ function pendingRedeliverAllowed(lastRedeliverAt) {
|
|
|
2398
2400
|
return Date.now() - t >= PENDING_REDELIVER_COOLDOWN_MS;
|
|
2399
2401
|
}
|
|
2400
2402
|
|
|
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, "\\$&");
|
|
2509
|
+
}
|
|
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;
|
|
2536
|
+
}
|
|
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;
|
|
2548
|
+
}
|
|
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);
|
|
2557
|
+
}
|
|
2558
|
+
const body = globBodyToRegex(line);
|
|
2559
|
+
let regexSource;
|
|
2560
|
+
if (anchored) {
|
|
2561
|
+
regexSource = `^${body}`;
|
|
2562
|
+
if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
|
|
2563
|
+
regexSource += "(?:/.*)?";
|
|
2564
|
+
}
|
|
2565
|
+
regexSource += "$";
|
|
2566
|
+
} else if (line.includes("/")) {
|
|
2567
|
+
regexSource = `(?:^|.*/)${body}`;
|
|
2568
|
+
if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
|
|
2569
|
+
regexSource += "(?:/.*)?";
|
|
2570
|
+
}
|
|
2571
|
+
regexSource += "$";
|
|
2572
|
+
} else {
|
|
2573
|
+
regexSource = `(?:^|.*/)?${body}$`;
|
|
2574
|
+
}
|
|
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;
|
|
2584
|
+
}
|
|
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
|
+
|
|
2401
2777
|
// ../core/src/review-engine.ts
|
|
2402
2778
|
function defaultRender(kind, vars) {
|
|
2403
2779
|
switch (kind) {
|
|
@@ -2421,6 +2797,8 @@ function defaultRender(kind, vars) {
|
|
|
2421
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.`;
|
|
2422
2798
|
case "stuck":
|
|
2423
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.`;
|
|
2424
2802
|
case "verify_fix":
|
|
2425
2803
|
return `Verify failed (${vars.reason ?? "unknown"}). Fix verify commands and rewrite verify-last.json; do not advance.`;
|
|
2426
2804
|
case "need_evidence":
|
|
@@ -2527,6 +2905,31 @@ var ReviewEngine = class {
|
|
|
2527
2905
|
return fromPending;
|
|
2528
2906
|
});
|
|
2529
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
|
+
}
|
|
2530
2933
|
handleStop(input) {
|
|
2531
2934
|
if (!this.store.isConversationIdOk(input.conversationId)) {
|
|
2532
2935
|
return null;
|
|
@@ -2688,10 +3091,48 @@ var ReviewEngine = class {
|
|
|
2688
3091
|
return null;
|
|
2689
3092
|
}
|
|
2690
3093
|
}
|
|
2691
|
-
const inChain = chainNow.chain_pending === 1 || chainNow.fix_round > 0 && isChecklistExecuting(session);
|
|
2692
|
-
if (chainNow.confirm_left === null && chainNow.item_confirm_complete === 0 && inChain) {
|
|
2693
|
-
return this.e3ArmConfirm(session, chainNow);
|
|
2694
|
-
}
|
|
3094
|
+
const inChain = chainNow.chain_pending === 1 || chainNow.fix_round > 0 && isChecklistExecuting(session);
|
|
3095
|
+
if (chainNow.confirm_left === null && chainNow.item_confirm_complete === 0 && inChain) {
|
|
3096
|
+
return this.e3ArmConfirm(session, chainNow);
|
|
3097
|
+
}
|
|
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
|
+
}
|
|
2695
3136
|
return this.e0NoCodeContinue(session, chainNow);
|
|
2696
3137
|
} catch (err) {
|
|
2697
3138
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3207,10 +3648,10 @@ var ReviewEngine = class {
|
|
|
3207
3648
|
* salvage again — the prior tip does not cover the new failure.
|
|
3208
3649
|
*/
|
|
3209
3650
|
classifyCompletedOrphan(transcriptPath) {
|
|
3210
|
-
const
|
|
3211
|
-
if (!
|
|
3651
|
+
const path11 = transcriptPath?.trim();
|
|
3652
|
+
if (!path11) return "none";
|
|
3212
3653
|
try {
|
|
3213
|
-
const events = readTranscriptTail(
|
|
3654
|
+
const events = readTranscriptTail(path11);
|
|
3214
3655
|
const errIdx = latestUnresolvedTurnEndedErrorIndex(events);
|
|
3215
3656
|
if (errIdx < 0) return "none";
|
|
3216
3657
|
for (let i = events.length - 1; i > errIdx; i--) {
|
|
@@ -3924,28 +4365,16 @@ var ReviewEngine = class {
|
|
|
3924
4365
|
}
|
|
3925
4366
|
const nextIdle = sess.idle_stop_count + 1;
|
|
3926
4367
|
const nowStuck = nextIdle >= this.config.maxIdleStops;
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
paused: 1,
|
|
3934
|
-
paused_reason: "stuck",
|
|
3935
|
-
armed: 0
|
|
3936
|
-
});
|
|
3937
|
-
} else {
|
|
3938
|
-
this.store.upsertSession({
|
|
3939
|
-
conversation_id: cid2,
|
|
3940
|
-
project_root: sess.project_root,
|
|
3941
|
-
code_root: sess.code_root,
|
|
3942
|
-
idle_stop_count: nextIdle
|
|
3943
|
-
});
|
|
3944
|
-
}
|
|
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
|
+
});
|
|
3945
4374
|
const kind = nowStuck ? "stuck" : "need_evidence";
|
|
3946
4375
|
const title = (lockedItem.title ?? "").trim();
|
|
3947
4376
|
const message = this.render(
|
|
3948
|
-
|
|
4377
|
+
nowStuck ? "stuck_soft" : "need_evidence",
|
|
3949
4378
|
nowStuck ? {} : {
|
|
3950
4379
|
currentId: lockedItem.id,
|
|
3951
4380
|
currentTitle: title,
|
|
@@ -4304,8 +4733,8 @@ var ReviewEngine = class {
|
|
|
4304
4733
|
let unchecked = checklist.unchecked;
|
|
4305
4734
|
let next = checklist.next;
|
|
4306
4735
|
let targets = null;
|
|
4307
|
-
const
|
|
4308
|
-
const onChecklistPath = isChecklistExecuting(lockedSession) &&
|
|
4736
|
+
const path11 = lockedSession.checklist_path?.trim() ?? "";
|
|
4737
|
+
const onChecklistPath = isChecklistExecuting(lockedSession) && path11.length > 0;
|
|
4309
4738
|
if (onChecklistPath) {
|
|
4310
4739
|
const refreshed = this.parseSessionChecklist(lockedSession);
|
|
4311
4740
|
if (!refreshed?.checklist) {
|
|
@@ -4437,28 +4866,7 @@ var ReviewEngine = class {
|
|
|
4437
4866
|
} catch {
|
|
4438
4867
|
}
|
|
4439
4868
|
}
|
|
4440
|
-
|
|
4441
|
-
const next = session.idle_stop_count + 1;
|
|
4442
|
-
if (next >= this.config.maxIdleStops) {
|
|
4443
|
-
this.store.upsertSession({
|
|
4444
|
-
conversation_id: session.conversation_id,
|
|
4445
|
-
project_root: session.project_root,
|
|
4446
|
-
code_root: session.code_root,
|
|
4447
|
-
idle_stop_count: next,
|
|
4448
|
-
paused: 1,
|
|
4449
|
-
paused_reason: "stuck",
|
|
4450
|
-
armed: 0
|
|
4451
|
-
});
|
|
4452
|
-
} else {
|
|
4453
|
-
this.store.upsertSession({
|
|
4454
|
-
conversation_id: session.conversation_id,
|
|
4455
|
-
project_root: session.project_root,
|
|
4456
|
-
code_root: session.code_root,
|
|
4457
|
-
idle_stop_count: next
|
|
4458
|
-
});
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
|
-
/** 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. */
|
|
4462
4870
|
checkStuck(session) {
|
|
4463
4871
|
const fresh = this.store.getSession(session.conversation_id);
|
|
4464
4872
|
if (!fresh) return null;
|
|
@@ -4663,8 +5071,8 @@ function applyResumeReview(store, conversationId) {
|
|
|
4663
5071
|
}
|
|
4664
5072
|
|
|
4665
5073
|
// ../core/src/project-config.ts
|
|
4666
|
-
import
|
|
4667
|
-
import
|
|
5074
|
+
import fs8 from "node:fs";
|
|
5075
|
+
import path8 from "node:path";
|
|
4668
5076
|
var MAX_CONFIG_BYTES = 1e6;
|
|
4669
5077
|
var DEFAULT_PROJECT_REVIEW_CONFIG = {
|
|
4670
5078
|
confirmRounds: 5,
|
|
@@ -4824,28 +5232,28 @@ function loadProjectReviewConfig(projectRoot) {
|
|
|
4824
5232
|
if (!root) {
|
|
4825
5233
|
return cloneDefaultProjectReviewConfig();
|
|
4826
5234
|
}
|
|
4827
|
-
const configPath =
|
|
5235
|
+
const configPath = path8.join(root, ".autopilot", "config.yml");
|
|
4828
5236
|
try {
|
|
4829
|
-
const nofollow = typeof
|
|
5237
|
+
const nofollow = typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
|
|
4830
5238
|
if (nofollow === 0) {
|
|
4831
|
-
if (!
|
|
4832
|
-
if (
|
|
5239
|
+
if (!fs8.existsSync(configPath)) return cloneDefaultProjectReviewConfig();
|
|
5240
|
+
if (fs8.lstatSync(configPath).isSymbolicLink()) {
|
|
4833
5241
|
return cloneDefaultProjectReviewConfig();
|
|
4834
5242
|
}
|
|
4835
5243
|
}
|
|
4836
5244
|
let fd;
|
|
4837
5245
|
try {
|
|
4838
|
-
fd =
|
|
5246
|
+
fd = fs8.openSync(configPath, fs8.constants.O_RDONLY | nofollow);
|
|
4839
5247
|
} catch {
|
|
4840
5248
|
return cloneDefaultProjectReviewConfig();
|
|
4841
5249
|
}
|
|
4842
5250
|
let raw;
|
|
4843
5251
|
try {
|
|
4844
|
-
const st =
|
|
5252
|
+
const st = fs8.fstatSync(fd);
|
|
4845
5253
|
if (!st.isFile() || st.size > MAX_CONFIG_BYTES) {
|
|
4846
5254
|
return cloneDefaultProjectReviewConfig();
|
|
4847
5255
|
}
|
|
4848
|
-
const lst =
|
|
5256
|
+
const lst = fs8.lstatSync(configPath);
|
|
4849
5257
|
if (lst.isSymbolicLink() || !lst.isFile()) {
|
|
4850
5258
|
return cloneDefaultProjectReviewConfig();
|
|
4851
5259
|
}
|
|
@@ -4856,10 +5264,10 @@ function loadProjectReviewConfig(projectRoot) {
|
|
|
4856
5264
|
return cloneDefaultProjectReviewConfig();
|
|
4857
5265
|
}
|
|
4858
5266
|
const buf = Buffer.alloc(st.size);
|
|
4859
|
-
const n =
|
|
5267
|
+
const n = fs8.readSync(fd, buf, 0, st.size, 0);
|
|
4860
5268
|
raw = buf.subarray(0, n).toString("utf8");
|
|
4861
5269
|
} finally {
|
|
4862
|
-
|
|
5270
|
+
fs8.closeSync(fd);
|
|
4863
5271
|
}
|
|
4864
5272
|
if (Buffer.byteLength(raw, "utf8") > MAX_CONFIG_BYTES) {
|
|
4865
5273
|
return cloneDefaultProjectReviewConfig();
|
|
@@ -4959,6 +5367,12 @@ function createRenderFollowup(bundle) {
|
|
|
4959
5367
|
);
|
|
4960
5368
|
case "stuck":
|
|
4961
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
|
+
);
|
|
4962
5376
|
case "verify_fix":
|
|
4963
5377
|
return renderTemplate(
|
|
4964
5378
|
f.verify_fix ?? "Verify failed ({reason}). Fix verify commands and rewrite verify-last.json; do not advance.",
|
|
@@ -5011,1000 +5425,703 @@ function createConfiguredReviewEngine(store, projectRoot, localeBundle, preloade
|
|
|
5011
5425
|
}
|
|
5012
5426
|
|
|
5013
5427
|
// ../core/src/phase-actions.ts
|
|
5014
|
-
import
|
|
5015
|
-
import
|
|
5428
|
+
import fs10 from "node:fs";
|
|
5429
|
+
import path10 from "node:path";
|
|
5016
5430
|
|
|
5017
5431
|
// ../core/src/list-tracks.ts
|
|
5018
|
-
import
|
|
5019
|
-
import
|
|
5432
|
+
import fs9 from "node:fs";
|
|
5433
|
+
import path9 from "node:path";
|
|
5020
5434
|
function isRunnableTrack(t) {
|
|
5021
5435
|
if (t.paused) return false;
|
|
5022
5436
|
const unchecked = t.checklistTotal - t.checklistDone;
|
|
5023
5437
|
if (unchecked <= 0) return false;
|
|
5024
5438
|
return t.phase === "planning" || t.phase === "executing" || t.phase === "idle" || t.phase === "done";
|
|
5025
|
-
}
|
|
5026
|
-
function readPlansDir(root, plansDir = "plans") {
|
|
5027
|
-
if (typeof plansDir !== "string" || !root || root.includes("\0") || plansDir.includes("\0")) {
|
|
5028
|
-
return [];
|
|
5029
|
-
}
|
|
5030
|
-
const dir = path7.join(root, plansDir);
|
|
5031
|
-
try {
|
|
5032
|
-
const lst = fs8.lstatSync(dir);
|
|
5033
|
-
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5034
|
-
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5035
|
-
} catch {
|
|
5036
|
-
return [];
|
|
5037
|
-
}
|
|
5038
|
-
const names = fs8.readdirSync(dir, { withFileTypes: true }).filter(
|
|
5039
|
-
(d) => d.isDirectory() && !d.isSymbolicLink() && isSafeTrackSlug(d.name)
|
|
5040
|
-
).map((d) => d.name);
|
|
5041
|
-
try {
|
|
5042
|
-
const lst = fs8.lstatSync(dir);
|
|
5043
|
-
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5044
|
-
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5045
|
-
} catch {
|
|
5046
|
-
return [];
|
|
5047
|
-
}
|
|
5048
|
-
return names;
|
|
5049
|
-
}
|
|
5050
|
-
function titleFromPlan(planPath, slug, projectRoot) {
|
|
5051
|
-
if (!planPath || planPath.includes("\0")) return slug;
|
|
5052
|
-
const root = normalizeProjectRoot(projectRoot);
|
|
5053
|
-
if (!root) return slug;
|
|
5054
|
-
const maxBytes = 65536;
|
|
5055
|
-
try {
|
|
5056
|
-
const nofollow = typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
|
|
5057
|
-
if (nofollow === 0) {
|
|
5058
|
-
const lst = fs8.lstatSync(planPath);
|
|
5059
|
-
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5060
|
-
}
|
|
5061
|
-
const fd = fs8.openSync(planPath, fs8.constants.O_RDONLY | nofollow);
|
|
5062
|
-
try {
|
|
5063
|
-
const st = fs8.fstatSync(fd);
|
|
5064
|
-
if (!st.isFile() || st.size <= 0) return slug;
|
|
5065
|
-
const lst = fs8.lstatSync(planPath);
|
|
5066
|
-
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5067
|
-
if (lst.ino !== st.ino || lst.dev !== st.dev) return slug;
|
|
5068
|
-
if (!isRealpathInsideProject(root, planPath)) return slug;
|
|
5069
|
-
const len = Math.min(st.size, maxBytes);
|
|
5070
|
-
const buf = Buffer.alloc(len);
|
|
5071
|
-
const n = fs8.readSync(fd, buf, 0, len, 0);
|
|
5072
|
-
const first = buf.subarray(0, n).toString("utf8").split(/\r?\n/)[0] ?? "";
|
|
5073
|
-
const m = first.match(/^#\s+(.+)/);
|
|
5074
|
-
return m?.[1]?.trim() ?? slug;
|
|
5075
|
-
} finally {
|
|
5076
|
-
fs8.closeSync(fd);
|
|
5077
|
-
}
|
|
5078
|
-
} catch {
|
|
5079
|
-
return slug;
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5082
|
-
function listTracks(root, store, filter = "all", plansDir = "plans") {
|
|
5083
|
-
const normalized = normalizeProjectRoot(root);
|
|
5084
|
-
if (!normalized) return [];
|
|
5085
|
-
root = normalized;
|
|
5086
|
-
const safePlans = normalizeInProjectPlansDir(root, plansDir);
|
|
5087
|
-
if (!safePlans) return [];
|
|
5088
|
-
plansDir = safePlans;
|
|
5089
|
-
const slugs = readPlansDir(root, plansDir);
|
|
5090
|
-
const tracks = [];
|
|
5091
|
-
for (const slug of slugs) {
|
|
5092
|
-
const trackDir = path7.join(root, plansDir, slug);
|
|
5093
|
-
try {
|
|
5094
|
-
const lst = fs8.lstatSync(trackDir);
|
|
5095
|
-
if (lst.isSymbolicLink() || !lst.isDirectory()) continue;
|
|
5096
|
-
if (!isRealpathInsideProject(root, trackDir)) continue;
|
|
5097
|
-
} catch {
|
|
5098
|
-
continue;
|
|
5099
|
-
}
|
|
5100
|
-
const planPath = path7.join(trackDir, "plan.md");
|
|
5101
|
-
const checklistPath = path7.join(trackDir, "checklist.md");
|
|
5102
|
-
let checklistTotal = 0;
|
|
5103
|
-
let checklistDone = 0;
|
|
5104
|
-
const checklistInProject = isRealpathInsideProject(root, checklistPath);
|
|
5105
|
-
const planInProject = isRealpathInsideProject(root, planPath);
|
|
5106
|
-
if (checklistInProject) {
|
|
5107
|
-
try {
|
|
5108
|
-
const cl = parseChecklist(checklistPath, { projectRoot: root });
|
|
5109
|
-
checklistTotal = cl.items.length;
|
|
5110
|
-
checklistDone = cl.items.filter((i) => i.checked).length;
|
|
5111
|
-
} catch {
|
|
5112
|
-
}
|
|
5113
|
-
} else if (!planInProject) {
|
|
5114
|
-
continue;
|
|
5115
|
-
}
|
|
5116
|
-
let phase = "idle";
|
|
5117
|
-
let paused = false;
|
|
5118
|
-
let pausedReason;
|
|
5119
|
-
let updatedAt = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
5120
|
-
if (store) {
|
|
5121
|
-
const sessions = store.db.prepare(
|
|
5122
|
-
`SELECT * FROM sessions WHERE track_id = ? ORDER BY last_active_at DESC LIMIT 1`
|
|
5123
|
-
).all(slug);
|
|
5124
|
-
const latest = sessions[0];
|
|
5125
|
-
if (latest) {
|
|
5126
|
-
phase = latest.phase;
|
|
5127
|
-
paused = latest.paused === 1;
|
|
5128
|
-
if (latest.paused_reason === "stuck" || latest.paused_reason === "repeated_errors" || latest.paused_reason === "human_gate") {
|
|
5129
|
-
pausedReason = latest.paused_reason;
|
|
5130
|
-
}
|
|
5131
|
-
updatedAt = latest.last_active_at;
|
|
5132
|
-
} else {
|
|
5133
|
-
if (checklistTotal > 0 && checklistDone === checklistTotal) {
|
|
5134
|
-
phase = "done";
|
|
5135
|
-
} else if (checklistTotal - checklistDone > 0 && planInProject) {
|
|
5136
|
-
phase = "idle";
|
|
5137
|
-
}
|
|
5138
|
-
}
|
|
5139
|
-
} else {
|
|
5140
|
-
if (checklistTotal > 0 && checklistDone === checklistTotal) {
|
|
5141
|
-
phase = "done";
|
|
5142
|
-
} else if (checklistTotal - checklistDone > 0) {
|
|
5143
|
-
phase = "idle";
|
|
5144
|
-
}
|
|
5145
|
-
}
|
|
5146
|
-
tracks.push({
|
|
5147
|
-
slug,
|
|
5148
|
-
title: planInProject ? titleFromPlan(planPath, slug, root) : slug,
|
|
5149
|
-
phase,
|
|
5150
|
-
paused,
|
|
5151
|
-
pausedReason,
|
|
5152
|
-
checklistTotal,
|
|
5153
|
-
checklistDone,
|
|
5154
|
-
planPath,
|
|
5155
|
-
updatedAt
|
|
5156
|
-
});
|
|
5157
|
-
}
|
|
5158
|
-
if (filter === "all") return tracks;
|
|
5159
|
-
if (filter === "planning") {
|
|
5160
|
-
return tracks.filter((t) => t.phase === "planning");
|
|
5161
|
-
}
|
|
5162
|
-
return tracks.filter(isRunnableTrack);
|
|
5163
|
-
}
|
|
5164
|
-
function canEnterExecuting(options) {
|
|
5165
|
-
const { slug, checklistPath, paused, projectRoot } = options;
|
|
5166
|
-
if (!slug || slug === "_pending") {
|
|
5167
|
-
return { ok: false, reason: "no track slug" };
|
|
5168
|
-
}
|
|
5169
|
-
const root = normalizeProjectRoot(projectRoot);
|
|
5170
|
-
if (!root) {
|
|
5171
|
-
return { ok: false, reason: "invalid project root" };
|
|
5172
|
-
}
|
|
5173
|
-
if (!checklistPath || !fs8.existsSync(checklistPath)) {
|
|
5174
|
-
return { ok: false, reason: "checklist missing" };
|
|
5175
|
-
}
|
|
5176
|
-
if (!isRealpathInsideProject(root, checklistPath)) {
|
|
5177
|
-
return { ok: false, reason: "checklist outside project" };
|
|
5178
|
-
}
|
|
5179
|
-
let unchecked = 0;
|
|
5180
|
-
try {
|
|
5181
|
-
unchecked = countUnchecked(
|
|
5182
|
-
parseChecklist(checklistPath, { projectRoot: root })
|
|
5183
|
-
);
|
|
5184
|
-
} catch {
|
|
5185
|
-
return { ok: false, reason: "checklist unreadable" };
|
|
5186
|
-
}
|
|
5187
|
-
if (unchecked < 1) {
|
|
5188
|
-
return { ok: false, reason: "no unchecked items" };
|
|
5189
|
-
}
|
|
5190
|
-
if (paused) {
|
|
5191
|
-
return { ok: false, reason: "session paused" };
|
|
5192
|
-
}
|
|
5193
|
-
return { ok: true };
|
|
5194
|
-
}
|
|
5195
|
-
|
|
5196
|
-
// ../core/src/phase-actions.ts
|
|
5197
|
-
function displayUntrusted(raw, max = 64) {
|
|
5198
|
-
const text = typeof raw === "string" ? raw : String(raw ?? "");
|
|
5199
|
-
return sanitizeSessionDisplayText(text).slice(0, max) || "?";
|
|
5200
|
-
}
|
|
5201
|
-
function isChannelANeedPick(fail) {
|
|
5202
|
-
return fail.needPick === true && fail.busy !== true;
|
|
5203
|
-
}
|
|
5204
|
-
function formatOneExecutorBusyMessage(trackId, conversationId) {
|
|
5205
|
-
const occTrack = displayUntrusted(trackId || "(unknown)");
|
|
5206
|
-
const rawCid = typeof conversationId === "string" ? conversationId : String(conversationId ?? "");
|
|
5207
|
-
const occSession = displayUntrusted(shortConversationId(rawCid) || "?") || "?";
|
|
5208
|
-
return `Another session is already executing (track: ${occTrack}, session: ${occSession}). Send Autopilot OFF there or wait, then retry. If the host only shows opaque blocked, run: npx @autopilot-harness/cli status or npx @autopilot-harness/cli doctor`;
|
|
5209
|
-
}
|
|
5210
|
-
function logBusyOccupier(message) {
|
|
5211
|
-
try {
|
|
5212
|
-
process.stderr.write(`[autopilot] ${message}
|
|
5213
|
-
`);
|
|
5214
|
-
} catch {
|
|
5215
|
-
}
|
|
5216
|
-
}
|
|
5217
|
-
function nowIso2() {
|
|
5218
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
5219
|
-
}
|
|
5220
|
-
function checklistPathFor(projectRoot, slug, plansDir) {
|
|
5221
|
-
const root = normalizeProjectRoot(projectRoot);
|
|
5222
|
-
if (!root) return path8.join(plansDir, slug, "checklist.md");
|
|
5223
|
-
return path8.join(root, plansDir, slug, "checklist.md");
|
|
5224
|
-
}
|
|
5225
|
-
function sameChecklistBinding(stored, rebuilt, projectRoot) {
|
|
5226
|
-
if (stored === rebuilt) return true;
|
|
5227
|
-
if (!stored || !rebuilt || stored.includes("\0") || rebuilt.includes("\0")) {
|
|
5228
|
-
return false;
|
|
5439
|
+
}
|
|
5440
|
+
function readPlansDir(root, plansDir = "plans") {
|
|
5441
|
+
if (typeof plansDir !== "string" || !root || root.includes("\0") || plansDir.includes("\0")) {
|
|
5442
|
+
return [];
|
|
5229
5443
|
}
|
|
5230
|
-
const
|
|
5231
|
-
if (!root) return false;
|
|
5444
|
+
const dir = path9.join(root, plansDir);
|
|
5232
5445
|
try {
|
|
5233
|
-
const
|
|
5234
|
-
|
|
5235
|
-
|
|
5446
|
+
const lst = fs9.lstatSync(dir);
|
|
5447
|
+
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5448
|
+
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5236
5449
|
} catch {
|
|
5237
|
-
return
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
|
-
function trustedChecklistPath(projectRoot, plansDir, slug, storedPath, boundTrackId) {
|
|
5241
|
-
const rebuilt = checklistPathFor(projectRoot, slug, plansDir);
|
|
5242
|
-
if (storedPath && boundTrackId === slug) {
|
|
5243
|
-
if (isChecklistPathAllowed(projectRoot, storedPath)) {
|
|
5244
|
-
return storedPath;
|
|
5245
|
-
}
|
|
5450
|
+
return [];
|
|
5246
5451
|
}
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
if (!checklistPath || checklistPath.includes("\0")) return false;
|
|
5251
|
-
const root = normalizeProjectRoot(projectRoot);
|
|
5252
|
-
if (!root) return false;
|
|
5452
|
+
const names = fs9.readdirSync(dir, { withFileTypes: true }).filter(
|
|
5453
|
+
(d) => d.isDirectory() && !d.isSymbolicLink() && isSafeTrackSlug(d.name)
|
|
5454
|
+
).map((d) => d.name);
|
|
5253
5455
|
try {
|
|
5254
|
-
fs9.lstatSync(
|
|
5255
|
-
|
|
5256
|
-
);
|
|
5257
|
-
return isRealpathInsideProject(root, checklistPath);
|
|
5456
|
+
const lst = fs9.lstatSync(dir);
|
|
5457
|
+
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5458
|
+
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5258
5459
|
} catch {
|
|
5259
|
-
return
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5262
|
-
function ensureSession(store, conversationId, projectRoot, platform) {
|
|
5263
|
-
const existing = store.getSession(conversationId);
|
|
5264
|
-
if (existing) {
|
|
5265
|
-
const want = resolveSessionPlatform(platform, existing.platform);
|
|
5266
|
-
if (want !== existing.platform) {
|
|
5267
|
-
return store.upsertSession({
|
|
5268
|
-
conversation_id: conversationId,
|
|
5269
|
-
project_root: existing.project_root,
|
|
5270
|
-
code_root: existing.code_root,
|
|
5271
|
-
platform: want
|
|
5272
|
-
});
|
|
5273
|
-
}
|
|
5274
|
-
return existing;
|
|
5460
|
+
return [];
|
|
5275
5461
|
}
|
|
5276
|
-
return
|
|
5277
|
-
conversation_id: conversationId,
|
|
5278
|
-
project_root: projectRoot,
|
|
5279
|
-
code_root: projectRoot,
|
|
5280
|
-
platform: normalizeSessionPlatform(platform),
|
|
5281
|
-
phase: "idle",
|
|
5282
|
-
armed: 0,
|
|
5283
|
-
paused: 0,
|
|
5284
|
-
track_id: "_pending",
|
|
5285
|
-
checklist_path: ""
|
|
5286
|
-
});
|
|
5462
|
+
return names;
|
|
5287
5463
|
}
|
|
5288
|
-
function
|
|
5289
|
-
|
|
5464
|
+
function titleFromPlan(planPath, slug, projectRoot) {
|
|
5465
|
+
if (!planPath || planPath.includes("\0")) return slug;
|
|
5290
5466
|
const root = normalizeProjectRoot(projectRoot);
|
|
5291
|
-
if (!root) return;
|
|
5292
|
-
const
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
slug = excluded.slug,
|
|
5299
|
-
checklist_path = excluded.checklist_path,
|
|
5300
|
-
plan_path = excluded.plan_path,
|
|
5301
|
-
brief_path = excluded.brief_path,
|
|
5302
|
-
updated_at = excluded.updated_at`
|
|
5303
|
-
).run(
|
|
5304
|
-
slug,
|
|
5305
|
-
slug,
|
|
5306
|
-
checklistPath,
|
|
5307
|
-
fs9.existsSync(planPath) ? planPath : null,
|
|
5308
|
-
fs9.existsSync(briefPath) ? briefPath : null,
|
|
5309
|
-
ts
|
|
5310
|
-
);
|
|
5311
|
-
}
|
|
5312
|
-
function candidatePayload(tracks) {
|
|
5313
|
-
return JSON.stringify(
|
|
5314
|
-
tracks.map((t) => ({
|
|
5315
|
-
slug: t.slug,
|
|
5316
|
-
title: sanitizeSessionDisplayText(t.title || t.slug) || t.slug,
|
|
5317
|
-
phase: t.phase,
|
|
5318
|
-
progress: `${t.checklistDone}/${t.checklistTotal}`
|
|
5319
|
-
}))
|
|
5320
|
-
);
|
|
5321
|
-
}
|
|
5322
|
-
function resolveRunSlug(store, session, projectRoot, plansDir, requestedSlug) {
|
|
5323
|
-
const runnable = listTracks(projectRoot, store, "runnable", plansDir).filter(
|
|
5324
|
-
(t) => isSafeTrackSlug(t.slug)
|
|
5325
|
-
);
|
|
5326
|
-
if (requestedSlug) {
|
|
5327
|
-
if (!isSafeTrackSlug(requestedSlug)) {
|
|
5328
|
-
return {
|
|
5329
|
-
kind: "none",
|
|
5330
|
-
userMessage: `Invalid track slug "${displayUntrusted(requestedSlug)}".`
|
|
5331
|
-
};
|
|
5332
|
-
}
|
|
5333
|
-
const hit = runnable.find((t) => t.slug === requestedSlug);
|
|
5334
|
-
if (!hit) {
|
|
5335
|
-
const all = listTracks(projectRoot, store, "all", plansDir);
|
|
5336
|
-
if (!all.some((t) => t.slug === requestedSlug)) {
|
|
5337
|
-
return {
|
|
5338
|
-
kind: "none",
|
|
5339
|
-
userMessage: `Track "${displayUntrusted(requestedSlug)}" not found or has no unchecked checklist items.`
|
|
5340
|
-
};
|
|
5341
|
-
}
|
|
5342
|
-
return {
|
|
5343
|
-
kind: "none",
|
|
5344
|
-
userMessage: `Track "${displayUntrusted(requestedSlug)}" is not runnable (paused or no unchecked items).`
|
|
5345
|
-
};
|
|
5346
|
-
}
|
|
5347
|
-
return { kind: "slug", slug: requestedSlug };
|
|
5348
|
-
}
|
|
5349
|
-
const midPick = session.pending_action === "run" || session.pending_action === "replan";
|
|
5350
|
-
if (!midPick && isBoundRunTrackId(session.track_id)) {
|
|
5351
|
-
const bound = runnable.find((t) => t.slug === session.track_id);
|
|
5352
|
-
if (bound) {
|
|
5353
|
-
return { kind: "slug", slug: bound.slug };
|
|
5467
|
+
if (!root) return slug;
|
|
5468
|
+
const maxBytes = 65536;
|
|
5469
|
+
try {
|
|
5470
|
+
const nofollow = typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
|
|
5471
|
+
if (nofollow === 0) {
|
|
5472
|
+
const lst = fs9.lstatSync(planPath);
|
|
5473
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5354
5474
|
}
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5475
|
+
const fd = fs9.openSync(planPath, fs9.constants.O_RDONLY | nofollow);
|
|
5476
|
+
try {
|
|
5477
|
+
const st = fs9.fstatSync(fd);
|
|
5478
|
+
if (!st.isFile() || st.size <= 0) return slug;
|
|
5479
|
+
const lst = fs9.lstatSync(planPath);
|
|
5480
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5481
|
+
if (lst.ino !== st.ino || lst.dev !== st.dev) return slug;
|
|
5482
|
+
if (!isRealpathInsideProject(root, planPath)) return slug;
|
|
5483
|
+
const len = Math.min(st.size, maxBytes);
|
|
5484
|
+
const buf = Buffer.alloc(len);
|
|
5485
|
+
const n = fs9.readSync(fd, buf, 0, len, 0);
|
|
5486
|
+
const first = buf.subarray(0, n).toString("utf8").split(/\r?\n/)[0] ?? "";
|
|
5487
|
+
const m = first.match(/^#\s+(.+)/);
|
|
5488
|
+
return m?.[1]?.trim() ?? slug;
|
|
5489
|
+
} finally {
|
|
5490
|
+
fs9.closeSync(fd);
|
|
5365
5491
|
}
|
|
5366
|
-
|
|
5492
|
+
} catch {
|
|
5493
|
+
return slug;
|
|
5367
5494
|
}
|
|
5368
|
-
return { kind: "pick", candidates: runnable };
|
|
5369
|
-
}
|
|
5370
|
-
function requireProjectRoot(store, projectRoot) {
|
|
5371
|
-
return normalizeProjectRoot(store.projectRoot) ?? normalizeProjectRoot(projectRoot);
|
|
5372
5495
|
}
|
|
5373
|
-
function
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
const
|
|
5378
|
-
if (!
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
const
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
if (resolved.kind === "pick") {
|
|
5404
|
-
const clearingStaleBind = isBoundRunTrackId(session.track_id);
|
|
5405
|
-
const trackForPick = clearingStaleBind ? "_pending" : session.track_id;
|
|
5406
|
-
store.upsertSession({
|
|
5407
|
-
conversation_id: conversationId,
|
|
5408
|
-
project_root: session.project_root,
|
|
5409
|
-
code_root: session.code_root,
|
|
5410
|
-
track_id: trackForPick,
|
|
5411
|
-
// Drop checklist binding with the stale track so planning state stays consistent.
|
|
5412
|
-
...clearingStaleBind ? { checklist_path: "" } : {},
|
|
5413
|
-
pending_action: "run",
|
|
5414
|
-
track_candidates_json: candidatePayload(resolved.candidates),
|
|
5415
|
-
armed: 0
|
|
5416
|
-
// phase unchanged — do not write executing
|
|
5417
|
-
});
|
|
5418
|
-
const lines = resolved.candidates.map((t, i) => {
|
|
5419
|
-
const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
|
|
5420
|
-
const left = t.checklistTotal - t.checklistDone;
|
|
5421
|
-
return ` ${i + 1}. ${t.slug} \u2014 ${title} (${left}/${t.checklistTotal} left)`;
|
|
5422
|
-
}).join("\n");
|
|
5423
|
-
return {
|
|
5424
|
-
ok: false,
|
|
5425
|
-
needPick: true,
|
|
5426
|
-
candidates: resolved.candidates,
|
|
5427
|
-
userMessage: `Select a plan to execute:
|
|
5428
|
-
|
|
5429
|
-
${lines}
|
|
5430
|
-
|
|
5431
|
-
Reply with a number or /autopilot-run <slug>.`
|
|
5432
|
-
};
|
|
5433
|
-
}
|
|
5434
|
-
const slug = resolved.slug;
|
|
5435
|
-
if (!isSafeTrackSlug(slug)) {
|
|
5436
|
-
return {
|
|
5437
|
-
ok: false,
|
|
5438
|
-
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
5439
|
-
};
|
|
5440
|
-
}
|
|
5441
|
-
const checklistPath = checklistPathFor(projectRoot, slug, plansDir);
|
|
5442
|
-
const gate = canEnterExecuting({
|
|
5443
|
-
slug,
|
|
5444
|
-
checklistPath,
|
|
5445
|
-
paused: session.paused === 1,
|
|
5446
|
-
projectRoot
|
|
5447
|
-
});
|
|
5448
|
-
if (!gate.ok) {
|
|
5449
|
-
return {
|
|
5450
|
-
ok: false,
|
|
5451
|
-
userMessage: `Cannot start executing: ${gate.reason}.`
|
|
5452
|
-
};
|
|
5453
|
-
}
|
|
5454
|
-
try {
|
|
5455
|
-
const result = store.exclusiveWrite(() => {
|
|
5456
|
-
if (concurrencyMode === "one_executor") {
|
|
5457
|
-
const other = store.findExecutingSession(conversationId);
|
|
5458
|
-
if (other) {
|
|
5459
|
-
const userMessage = formatOneExecutorBusyMessage(
|
|
5460
|
-
other.track_id,
|
|
5461
|
-
other.conversation_id
|
|
5462
|
-
);
|
|
5463
|
-
return {
|
|
5464
|
-
commit: false,
|
|
5465
|
-
value: {
|
|
5466
|
-
ok: false,
|
|
5467
|
-
busy: true,
|
|
5468
|
-
userMessage
|
|
5469
|
-
}
|
|
5470
|
-
};
|
|
5471
|
-
}
|
|
5496
|
+
function listTracks(root, store, filter = "all", plansDir = "plans") {
|
|
5497
|
+
const normalized = normalizeProjectRoot(root);
|
|
5498
|
+
if (!normalized) return [];
|
|
5499
|
+
root = normalized;
|
|
5500
|
+
const safePlans = normalizeInProjectPlansDir(root, plansDir);
|
|
5501
|
+
if (!safePlans) return [];
|
|
5502
|
+
plansDir = safePlans;
|
|
5503
|
+
const slugs = readPlansDir(root, plansDir);
|
|
5504
|
+
const tracks = [];
|
|
5505
|
+
for (const slug of slugs) {
|
|
5506
|
+
const trackDir = path9.join(root, plansDir, slug);
|
|
5507
|
+
try {
|
|
5508
|
+
const lst = fs9.lstatSync(trackDir);
|
|
5509
|
+
if (lst.isSymbolicLink() || !lst.isDirectory()) continue;
|
|
5510
|
+
if (!isRealpathInsideProject(root, trackDir)) continue;
|
|
5511
|
+
} catch {
|
|
5512
|
+
continue;
|
|
5513
|
+
}
|
|
5514
|
+
const planPath = path9.join(trackDir, "plan.md");
|
|
5515
|
+
const checklistPath = path9.join(trackDir, "checklist.md");
|
|
5516
|
+
let checklistTotal = 0;
|
|
5517
|
+
let checklistDone = 0;
|
|
5518
|
+
const checklistInProject = isRealpathInsideProject(root, checklistPath);
|
|
5519
|
+
const planInProject = isRealpathInsideProject(root, planPath);
|
|
5520
|
+
if (checklistInProject) {
|
|
5521
|
+
try {
|
|
5522
|
+
const cl = parseChecklist(checklistPath, { projectRoot: root });
|
|
5523
|
+
checklistTotal = cl.items.length;
|
|
5524
|
+
checklistDone = cl.items.filter((i) => i.checked).length;
|
|
5525
|
+
} catch {
|
|
5472
5526
|
}
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
});
|
|
5491
|
-
if (!alreadyExecutingSameTrack) {
|
|
5492
|
-
let reviewingItemId = null;
|
|
5493
|
-
try {
|
|
5494
|
-
const cl = parseChecklist(checklistPath, { projectRoot });
|
|
5495
|
-
reviewingItemId = firstUnchecked(cl)?.id ?? null;
|
|
5496
|
-
} catch {
|
|
5527
|
+
} else if (!planInProject) {
|
|
5528
|
+
continue;
|
|
5529
|
+
}
|
|
5530
|
+
let phase = "idle";
|
|
5531
|
+
let paused = false;
|
|
5532
|
+
let pausedReason;
|
|
5533
|
+
let updatedAt = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
5534
|
+
if (store) {
|
|
5535
|
+
const sessions = store.db.prepare(
|
|
5536
|
+
`SELECT * FROM sessions WHERE track_id = ? ORDER BY last_active_at DESC LIMIT 1`
|
|
5537
|
+
).all(slug);
|
|
5538
|
+
const latest = sessions[0];
|
|
5539
|
+
if (latest) {
|
|
5540
|
+
phase = latest.phase;
|
|
5541
|
+
paused = latest.paused === 1;
|
|
5542
|
+
if (latest.paused_reason === "stuck" || latest.paused_reason === "repeated_errors" || latest.paused_reason === "human_gate") {
|
|
5543
|
+
pausedReason = latest.paused_reason;
|
|
5497
5544
|
}
|
|
5498
|
-
|
|
5499
|
-
fix_round: 0,
|
|
5500
|
-
confirm_left: null,
|
|
5501
|
-
chain_pending: 0,
|
|
5502
|
-
code_edited: 0,
|
|
5503
|
-
item_confirm_complete: 0,
|
|
5504
|
-
reviewing_item_id: reviewingItemId,
|
|
5505
|
-
pending_followup: null,
|
|
5506
|
-
pending_followup_at: null,
|
|
5507
|
-
pending_redeliver_at: null
|
|
5508
|
-
});
|
|
5545
|
+
updatedAt = latest.last_active_at;
|
|
5509
5546
|
} else {
|
|
5510
|
-
|
|
5547
|
+
if (checklistTotal > 0 && checklistDone === checklistTotal) {
|
|
5548
|
+
phase = "done";
|
|
5549
|
+
} else if (checklistTotal - checklistDone > 0 && planInProject) {
|
|
5550
|
+
phase = "idle";
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
} else {
|
|
5554
|
+
if (checklistTotal > 0 && checklistDone === checklistTotal) {
|
|
5555
|
+
phase = "done";
|
|
5556
|
+
} else if (checklistTotal - checklistDone > 0) {
|
|
5557
|
+
phase = "idle";
|
|
5511
5558
|
}
|
|
5512
|
-
return { commit: true, value: { ok: true, session: updated } };
|
|
5513
|
-
});
|
|
5514
|
-
if (!result.ok && result.busy === true) {
|
|
5515
|
-
logBusyOccupier(result.userMessage);
|
|
5516
|
-
}
|
|
5517
|
-
return result;
|
|
5518
|
-
} catch (err) {
|
|
5519
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5520
|
-
if (/busy|locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
|
|
5521
|
-
const userMessage = "State database is busy; retry Autopilot RUN in a moment.";
|
|
5522
|
-
logBusyOccupier(userMessage);
|
|
5523
|
-
return {
|
|
5524
|
-
ok: false,
|
|
5525
|
-
busy: true,
|
|
5526
|
-
userMessage
|
|
5527
|
-
};
|
|
5528
5559
|
}
|
|
5529
|
-
|
|
5560
|
+
tracks.push({
|
|
5561
|
+
slug,
|
|
5562
|
+
title: planInProject ? titleFromPlan(planPath, slug, root) : slug,
|
|
5563
|
+
phase,
|
|
5564
|
+
paused,
|
|
5565
|
+
pausedReason,
|
|
5566
|
+
checklistTotal,
|
|
5567
|
+
checklistDone,
|
|
5568
|
+
planPath,
|
|
5569
|
+
updatedAt
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5572
|
+
if (filter === "all") return tracks;
|
|
5573
|
+
if (filter === "planning") {
|
|
5574
|
+
return tracks.filter((t) => t.phase === "planning");
|
|
5530
5575
|
}
|
|
5576
|
+
return tracks.filter(isRunnableTrack);
|
|
5531
5577
|
}
|
|
5532
|
-
function
|
|
5533
|
-
const
|
|
5534
|
-
if (!
|
|
5535
|
-
return { ok: false,
|
|
5578
|
+
function canEnterExecuting(options) {
|
|
5579
|
+
const { slug, checklistPath, paused, projectRoot } = options;
|
|
5580
|
+
if (!slug || slug === "_pending") {
|
|
5581
|
+
return { ok: false, reason: "no track slug" };
|
|
5536
5582
|
}
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
return { ok: false, userMessage: "Invalid plans directory." };
|
|
5583
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
5584
|
+
if (!root) {
|
|
5585
|
+
return { ok: false, reason: "invalid project root" };
|
|
5541
5586
|
}
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
conversationId,
|
|
5545
|
-
projectRoot,
|
|
5546
|
-
opts?.platform
|
|
5547
|
-
);
|
|
5548
|
-
let slug = opts?.slug;
|
|
5549
|
-
if (!slug) {
|
|
5550
|
-
const tid = session.track_id;
|
|
5551
|
-
const midPick = session.pending_action === "run" || session.pending_action === "replan";
|
|
5552
|
-
slug = !midPick && isBoundRunTrackId(tid) ? tid : void 0;
|
|
5587
|
+
if (!checklistPath || !fs9.existsSync(checklistPath)) {
|
|
5588
|
+
return { ok: false, reason: "checklist missing" };
|
|
5553
5589
|
}
|
|
5554
|
-
if (
|
|
5555
|
-
return {
|
|
5556
|
-
ok: false,
|
|
5557
|
-
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
5558
|
-
};
|
|
5590
|
+
if (!isRealpathInsideProject(root, checklistPath)) {
|
|
5591
|
+
return { ok: false, reason: "checklist outside project" };
|
|
5559
5592
|
}
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5593
|
+
let unchecked = 0;
|
|
5594
|
+
try {
|
|
5595
|
+
unchecked = countUnchecked(
|
|
5596
|
+
parseChecklist(checklistPath, { projectRoot: root })
|
|
5563
5597
|
);
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
} else if (all.length > 1 || all.length === 1 && session.pending_action === "run") {
|
|
5567
|
-
const clearingStaleBind = isBoundRunTrackId(session.track_id);
|
|
5568
|
-
store.upsertSession({
|
|
5569
|
-
conversation_id: conversationId,
|
|
5570
|
-
project_root: session.project_root,
|
|
5571
|
-
code_root: session.code_root,
|
|
5572
|
-
track_id: clearingStaleBind ? "_pending" : session.track_id,
|
|
5573
|
-
...clearingStaleBind ? { checklist_path: "" } : {},
|
|
5574
|
-
pending_action: "replan",
|
|
5575
|
-
track_candidates_json: candidatePayload(all),
|
|
5576
|
-
armed: 0
|
|
5577
|
-
});
|
|
5578
|
-
const lines = all.map((t, i) => {
|
|
5579
|
-
const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
|
|
5580
|
-
return ` ${i + 1}. ${t.slug} \u2014 ${title}`;
|
|
5581
|
-
}).join("\n");
|
|
5582
|
-
return {
|
|
5583
|
-
ok: false,
|
|
5584
|
-
needPick: true,
|
|
5585
|
-
candidates: all,
|
|
5586
|
-
userMessage: `Select a plan to replan:
|
|
5587
|
-
|
|
5588
|
-
${lines}
|
|
5589
|
-
|
|
5590
|
-
Reply with a number or /autopilot-replan <slug>.`
|
|
5591
|
-
};
|
|
5592
|
-
} else {
|
|
5593
|
-
return {
|
|
5594
|
-
ok: false,
|
|
5595
|
-
userMessage: "No plan to replan. Use /autopilot-on first."
|
|
5596
|
-
};
|
|
5597
|
-
}
|
|
5598
|
+
} catch {
|
|
5599
|
+
return { ok: false, reason: "checklist unreadable" };
|
|
5598
5600
|
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
session
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
const updated = store.upsertSession({
|
|
5607
|
-
conversation_id: conversationId,
|
|
5608
|
-
project_root: projectRoot,
|
|
5609
|
-
code_root: projectRoot,
|
|
5610
|
-
track_id: slug,
|
|
5611
|
-
checklist_path: checklistPath,
|
|
5612
|
-
phase: "planning",
|
|
5613
|
-
armed: 0,
|
|
5614
|
-
paused: 0,
|
|
5615
|
-
paused_reason: null,
|
|
5616
|
-
pending_action: null,
|
|
5617
|
-
track_candidates_json: null
|
|
5618
|
-
});
|
|
5619
|
-
store.updateReviewChain(conversationId, {
|
|
5620
|
-
fix_round: 0,
|
|
5621
|
-
confirm_left: null,
|
|
5622
|
-
chain_pending: 0,
|
|
5623
|
-
code_edited: 0,
|
|
5624
|
-
item_confirm_complete: 0,
|
|
5625
|
-
reviewing_item_id: null,
|
|
5626
|
-
pending_followup: null,
|
|
5627
|
-
pending_followup_at: null,
|
|
5628
|
-
pending_redeliver_at: null
|
|
5629
|
-
});
|
|
5630
|
-
return { ok: true, session: updated };
|
|
5601
|
+
if (unchecked < 1) {
|
|
5602
|
+
return { ok: false, reason: "no unchecked items" };
|
|
5603
|
+
}
|
|
5604
|
+
if (paused) {
|
|
5605
|
+
return { ok: false, reason: "session paused" };
|
|
5606
|
+
}
|
|
5607
|
+
return { ok: true };
|
|
5631
5608
|
}
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5609
|
+
|
|
5610
|
+
// ../core/src/phase-actions.ts
|
|
5611
|
+
function displayUntrusted(raw, max = 64) {
|
|
5612
|
+
const text = typeof raw === "string" ? raw : String(raw ?? "");
|
|
5613
|
+
return sanitizeSessionDisplayText(text).slice(0, max) || "?";
|
|
5614
|
+
}
|
|
5615
|
+
function isChannelANeedPick(fail) {
|
|
5616
|
+
return fail.needPick === true && fail.busy !== true;
|
|
5617
|
+
}
|
|
5618
|
+
function formatOneExecutorBusyMessage(trackId, conversationId) {
|
|
5619
|
+
const occTrack = displayUntrusted(trackId || "(unknown)");
|
|
5620
|
+
const rawCid = typeof conversationId === "string" ? conversationId : String(conversationId ?? "");
|
|
5621
|
+
const occSession = displayUntrusted(shortConversationId(rawCid) || "?") || "?";
|
|
5622
|
+
return `Another session is already executing (track: ${occTrack}, session: ${occSession}). Send Autopilot OFF there or wait, then retry. If the host only shows opaque blocked, run: npx @autopilot-harness/cli status or npx @autopilot-harness/cli doctor`;
|
|
5623
|
+
}
|
|
5624
|
+
function logBusyOccupier(message) {
|
|
5625
|
+
try {
|
|
5626
|
+
process.stderr.write(`[autopilot] ${message}
|
|
5627
|
+
`);
|
|
5628
|
+
} catch {
|
|
5636
5629
|
}
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5630
|
+
}
|
|
5631
|
+
function nowIso2() {
|
|
5632
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
5633
|
+
}
|
|
5634
|
+
function checklistPathFor(projectRoot, slug, plansDir) {
|
|
5635
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
5636
|
+
if (!root) return path10.join(plansDir, slug, "checklist.md");
|
|
5637
|
+
return path10.join(root, plansDir, slug, "checklist.md");
|
|
5638
|
+
}
|
|
5639
|
+
function sameChecklistBinding(stored, rebuilt, projectRoot) {
|
|
5640
|
+
if (stored === rebuilt) return true;
|
|
5641
|
+
if (!stored || !rebuilt || stored.includes("\0") || rebuilt.includes("\0")) {
|
|
5642
|
+
return false;
|
|
5644
5643
|
}
|
|
5645
|
-
const
|
|
5646
|
-
if (
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5644
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
5645
|
+
if (!root) return false;
|
|
5646
|
+
try {
|
|
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);
|
|
5649
|
+
return absStored === absRebuilt;
|
|
5650
|
+
} catch {
|
|
5651
|
+
return false;
|
|
5651
5652
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5653
|
+
}
|
|
5654
|
+
function trustedChecklistPath(projectRoot, plansDir, slug, storedPath, boundTrackId) {
|
|
5655
|
+
const rebuilt = checklistPathFor(projectRoot, slug, plansDir);
|
|
5656
|
+
if (storedPath && boundTrackId === slug) {
|
|
5657
|
+
if (isChecklistPathAllowed(projectRoot, storedPath)) {
|
|
5658
|
+
return storedPath;
|
|
5659
|
+
}
|
|
5654
5660
|
}
|
|
5655
|
-
|
|
5661
|
+
return rebuilt;
|
|
5662
|
+
}
|
|
5663
|
+
function isChecklistPathAllowed(projectRoot, checklistPath) {
|
|
5664
|
+
if (!checklistPath || checklistPath.includes("\0")) return false;
|
|
5665
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
5666
|
+
if (!root) return false;
|
|
5656
5667
|
try {
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5660
|
-
}
|
|
5661
|
-
const hasValidSlug = parsed.some(
|
|
5662
|
-
(c) => !!c && typeof c === "object" && !Array.isArray(c) && typeof c.slug === "string" && isSafeTrackSlug(c.slug)
|
|
5668
|
+
fs10.lstatSync(
|
|
5669
|
+
path10.isAbsolute(checklistPath) ? checklistPath : path10.resolve(root, checklistPath)
|
|
5663
5670
|
);
|
|
5664
|
-
|
|
5665
|
-
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5666
|
-
}
|
|
5667
|
-
candidates = parsed;
|
|
5671
|
+
return isRealpathInsideProject(root, checklistPath);
|
|
5668
5672
|
} catch {
|
|
5669
|
-
return
|
|
5673
|
+
return isLexicallyInsideProject(root, checklistPath);
|
|
5670
5674
|
}
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
if (
|
|
5677
|
-
return {
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
if (!isSafeTrackSlug(pick)) {
|
|
5684
|
-
return {
|
|
5685
|
-
ok: false,
|
|
5686
|
-
userMessage: `Invalid track slug "${displayUntrusted(pick)}".`
|
|
5687
|
-
};
|
|
5688
|
-
}
|
|
5689
|
-
slug = pick;
|
|
5690
|
-
if (!candidates.some(
|
|
5691
|
-
(c) => !!c && typeof c === "object" && !Array.isArray(c) && c.slug === slug
|
|
5692
|
-
)) {
|
|
5693
|
-
return {
|
|
5694
|
-
ok: false,
|
|
5695
|
-
userMessage: `Unknown slug "${displayUntrusted(pick)}".`
|
|
5696
|
-
};
|
|
5675
|
+
}
|
|
5676
|
+
function ensureSession(store, conversationId, projectRoot, platform) {
|
|
5677
|
+
const existing = store.getSession(conversationId);
|
|
5678
|
+
if (existing) {
|
|
5679
|
+
const want = resolveSessionPlatform(platform, existing.platform);
|
|
5680
|
+
if (want !== existing.platform) {
|
|
5681
|
+
return store.upsertSession({
|
|
5682
|
+
conversation_id: conversationId,
|
|
5683
|
+
project_root: existing.project_root,
|
|
5684
|
+
code_root: existing.code_root,
|
|
5685
|
+
platform: want
|
|
5686
|
+
});
|
|
5697
5687
|
}
|
|
5688
|
+
return existing;
|
|
5698
5689
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
platform: opts?.platform
|
|
5690
|
+
return store.upsertSession({
|
|
5691
|
+
conversation_id: conversationId,
|
|
5692
|
+
project_root: projectRoot,
|
|
5693
|
+
code_root: projectRoot,
|
|
5694
|
+
platform: normalizeSessionPlatform(platform),
|
|
5695
|
+
phase: "idle",
|
|
5696
|
+
armed: 0,
|
|
5697
|
+
paused: 0,
|
|
5698
|
+
track_id: "_pending",
|
|
5699
|
+
checklist_path: ""
|
|
5710
5700
|
});
|
|
5711
5701
|
}
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
.
|
|
5738
|
-
.
|
|
5739
|
-
.
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
node_modules/**
|
|
5746
|
-
dist/**
|
|
5747
|
-
build/**
|
|
5748
|
-
out/**
|
|
5749
|
-
target/**
|
|
5750
|
-
.target/**
|
|
5751
|
-
coverage/**
|
|
5752
|
-
.venv/**
|
|
5753
|
-
venv/**
|
|
5754
|
-
__pycache__/**
|
|
5755
|
-
|
|
5756
|
-
# Lockfiles / package manager noise
|
|
5757
|
-
package-lock.json
|
|
5758
|
-
pnpm-lock.yaml
|
|
5759
|
-
yarn.lock
|
|
5760
|
-
bun.lock
|
|
5761
|
-
bun.lockb
|
|
5762
|
-
Cargo.lock
|
|
5763
|
-
poetry.lock
|
|
5764
|
-
composer.lock
|
|
5765
|
-
|
|
5766
|
-
# Media / binary (do not trigger self-review)
|
|
5767
|
-
*.png
|
|
5768
|
-
*.jpg
|
|
5769
|
-
*.jpeg
|
|
5770
|
-
*.gif
|
|
5771
|
-
*.webp
|
|
5772
|
-
*.ico
|
|
5773
|
-
*.svg
|
|
5774
|
-
*.bmp
|
|
5775
|
-
*.mp3
|
|
5776
|
-
*.mp4
|
|
5777
|
-
*.wav
|
|
5778
|
-
*.webm
|
|
5779
|
-
*.mov
|
|
5780
|
-
*.woff
|
|
5781
|
-
*.woff2
|
|
5782
|
-
*.ttf
|
|
5783
|
-
*.otf
|
|
5784
|
-
*.eot
|
|
5785
|
-
*.pdf
|
|
5786
|
-
*.zip
|
|
5787
|
-
*.gz
|
|
5788
|
-
*.tgz
|
|
5789
|
-
*.7z
|
|
5790
|
-
*.rar
|
|
5791
|
-
*.jar
|
|
5792
|
-
*.class
|
|
5793
|
-
*.o
|
|
5794
|
-
*.a
|
|
5795
|
-
*.so
|
|
5796
|
-
*.dylib
|
|
5797
|
-
*.dll
|
|
5798
|
-
*.exe
|
|
5799
|
-
*.wasm
|
|
5800
|
-
|
|
5801
|
-
# Prose / data noise
|
|
5802
|
-
*.txt
|
|
5803
|
-
*.html
|
|
5804
|
-
*.htm
|
|
5805
|
-
*.csv
|
|
5806
|
-
*.tsv
|
|
5807
|
-
*.log
|
|
5808
|
-
*.map
|
|
5809
|
-
*.min.js
|
|
5810
|
-
*.min.css
|
|
5811
|
-
`;
|
|
5812
|
-
var MAX_AUTOPILOT_IGNORE_BYTES = 1e6;
|
|
5813
|
-
var MAX_AUTOPILOT_IGNORE_LINE_CHARS = 4096;
|
|
5814
|
-
var MAX_AUTOPILOT_IGNORE_PATTERNS = 1e4;
|
|
5815
|
-
var ignoreCache = /* @__PURE__ */ new Map();
|
|
5816
|
-
function escapeRegexChar(ch) {
|
|
5817
|
-
return ch.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
5702
|
+
function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
|
|
5703
|
+
const ts = nowIso2();
|
|
5704
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
5705
|
+
if (!root) return;
|
|
5706
|
+
const planPath = path10.join(root, plansDir, slug, "plan.md");
|
|
5707
|
+
const briefPath = path10.join(root, plansDir, slug, "brief.md");
|
|
5708
|
+
store.db.prepare(
|
|
5709
|
+
`INSERT INTO tracks (track_id, slug, checklist_path, plan_path, brief_path, updated_at)
|
|
5710
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
5711
|
+
ON CONFLICT(track_id) DO UPDATE SET
|
|
5712
|
+
slug = excluded.slug,
|
|
5713
|
+
checklist_path = excluded.checklist_path,
|
|
5714
|
+
plan_path = excluded.plan_path,
|
|
5715
|
+
brief_path = excluded.brief_path,
|
|
5716
|
+
updated_at = excluded.updated_at`
|
|
5717
|
+
).run(
|
|
5718
|
+
slug,
|
|
5719
|
+
slug,
|
|
5720
|
+
checklistPath,
|
|
5721
|
+
fs10.existsSync(planPath) ? planPath : null,
|
|
5722
|
+
fs10.existsSync(briefPath) ? briefPath : null,
|
|
5723
|
+
ts
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
function candidatePayload(tracks) {
|
|
5727
|
+
return JSON.stringify(
|
|
5728
|
+
tracks.map((t) => ({
|
|
5729
|
+
slug: t.slug,
|
|
5730
|
+
title: sanitizeSessionDisplayText(t.title || t.slug) || t.slug,
|
|
5731
|
+
phase: t.phase,
|
|
5732
|
+
progress: `${t.checklistDone}/${t.checklistTotal}`
|
|
5733
|
+
}))
|
|
5734
|
+
);
|
|
5818
5735
|
}
|
|
5819
|
-
function
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5736
|
+
function resolveRunSlug(store, session, projectRoot, plansDir, requestedSlug) {
|
|
5737
|
+
const runnable = listTracks(projectRoot, store, "runnable", plansDir).filter(
|
|
5738
|
+
(t) => isSafeTrackSlug(t.slug)
|
|
5739
|
+
);
|
|
5740
|
+
if (requestedSlug) {
|
|
5741
|
+
if (!isSafeTrackSlug(requestedSlug)) {
|
|
5742
|
+
return {
|
|
5743
|
+
kind: "none",
|
|
5744
|
+
userMessage: `Invalid track slug "${displayUntrusted(requestedSlug)}".`
|
|
5745
|
+
};
|
|
5746
|
+
}
|
|
5747
|
+
const hit = runnable.find((t) => t.slug === requestedSlug);
|
|
5748
|
+
if (!hit) {
|
|
5749
|
+
const all = listTracks(projectRoot, store, "all", plansDir);
|
|
5750
|
+
if (!all.some((t) => t.slug === requestedSlug)) {
|
|
5751
|
+
return {
|
|
5752
|
+
kind: "none",
|
|
5753
|
+
userMessage: `Track "${displayUntrusted(requestedSlug)}" not found or has no unchecked checklist items.`
|
|
5754
|
+
};
|
|
5828
5755
|
}
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5756
|
+
return {
|
|
5757
|
+
kind: "none",
|
|
5758
|
+
userMessage: `Track "${displayUntrusted(requestedSlug)}" is not runnable (paused or no unchecked items).`
|
|
5759
|
+
};
|
|
5832
5760
|
}
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5761
|
+
return { kind: "slug", slug: requestedSlug };
|
|
5762
|
+
}
|
|
5763
|
+
const midPick = session.pending_action === "run" || session.pending_action === "replan";
|
|
5764
|
+
if (!midPick && isBoundRunTrackId(session.track_id)) {
|
|
5765
|
+
const bound = runnable.find((t) => t.slug === session.track_id);
|
|
5766
|
+
if (bound) {
|
|
5767
|
+
return { kind: "slug", slug: bound.slug };
|
|
5837
5768
|
}
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5769
|
+
}
|
|
5770
|
+
if (runnable.length === 0) {
|
|
5771
|
+
return {
|
|
5772
|
+
kind: "none",
|
|
5773
|
+
userMessage: "No runnable plan. Use /autopilot-on to plan, then finalize a checklist with unchecked items."
|
|
5774
|
+
};
|
|
5775
|
+
}
|
|
5776
|
+
if (runnable.length === 1) {
|
|
5777
|
+
if (session.pending_action === "replan") {
|
|
5778
|
+
return { kind: "pick", candidates: runnable };
|
|
5842
5779
|
}
|
|
5843
|
-
|
|
5844
|
-
i += 1;
|
|
5780
|
+
return { kind: "slug", slug: runnable[0].slug };
|
|
5845
5781
|
}
|
|
5846
|
-
return
|
|
5782
|
+
return { kind: "pick", candidates: runnable };
|
|
5847
5783
|
}
|
|
5848
|
-
function
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5784
|
+
function requireProjectRoot(store, projectRoot) {
|
|
5785
|
+
return normalizeProjectRoot(store.projectRoot) ?? normalizeProjectRoot(projectRoot);
|
|
5786
|
+
}
|
|
5787
|
+
function requirePlansDir(projectRoot, plansDir) {
|
|
5788
|
+
return normalizeInProjectPlansDir(projectRoot, plansDir);
|
|
5789
|
+
}
|
|
5790
|
+
function applyRun(store, conversationId, projectRoot, opts) {
|
|
5791
|
+
const root = requireProjectRoot(store, projectRoot);
|
|
5792
|
+
if (!root) {
|
|
5793
|
+
return { ok: false, userMessage: "Invalid project root." };
|
|
5857
5794
|
}
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
if (!
|
|
5861
|
-
|
|
5862
|
-
let anchored = false;
|
|
5863
|
-
if (line.startsWith("/")) {
|
|
5864
|
-
anchored = true;
|
|
5865
|
-
line = line.slice(1);
|
|
5795
|
+
projectRoot = root;
|
|
5796
|
+
const plansDir = requirePlansDir(projectRoot, opts?.config?.plansDir);
|
|
5797
|
+
if (!plansDir) {
|
|
5798
|
+
return { ok: false, userMessage: "Invalid plans directory." };
|
|
5866
5799
|
}
|
|
5867
|
-
const
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5800
|
+
const concurrencyMode = opts?.config?.concurrencyMode ?? "one_executor";
|
|
5801
|
+
const session = ensureSession(
|
|
5802
|
+
store,
|
|
5803
|
+
conversationId,
|
|
5804
|
+
projectRoot,
|
|
5805
|
+
opts?.platform
|
|
5806
|
+
);
|
|
5807
|
+
const resolved = resolveRunSlug(
|
|
5808
|
+
store,
|
|
5809
|
+
session,
|
|
5810
|
+
projectRoot,
|
|
5811
|
+
plansDir,
|
|
5812
|
+
opts?.slug
|
|
5813
|
+
);
|
|
5814
|
+
if (resolved.kind === "none") {
|
|
5815
|
+
return { ok: false, userMessage: resolved.userMessage };
|
|
5816
|
+
}
|
|
5817
|
+
if (resolved.kind === "pick") {
|
|
5818
|
+
const clearingStaleBind = isBoundRunTrackId(session.track_id);
|
|
5819
|
+
const trackForPick = clearingStaleBind ? "_pending" : session.track_id;
|
|
5820
|
+
store.upsertSession({
|
|
5821
|
+
conversation_id: conversationId,
|
|
5822
|
+
project_root: session.project_root,
|
|
5823
|
+
code_root: session.code_root,
|
|
5824
|
+
track_id: trackForPick,
|
|
5825
|
+
// Drop checklist binding with the stale track so planning state stays consistent.
|
|
5826
|
+
...clearingStaleBind ? { checklist_path: "" } : {},
|
|
5827
|
+
pending_action: "run",
|
|
5828
|
+
track_candidates_json: candidatePayload(resolved.candidates),
|
|
5829
|
+
armed: 0
|
|
5830
|
+
// phase unchanged — do not write executing
|
|
5831
|
+
});
|
|
5832
|
+
const lines = resolved.candidates.map((t, i) => {
|
|
5833
|
+
const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
|
|
5834
|
+
const left = t.checklistTotal - t.checklistDone;
|
|
5835
|
+
return ` ${i + 1}. ${t.slug} \u2014 ${title} (${left}/${t.checklistTotal} left)`;
|
|
5836
|
+
}).join("\n");
|
|
5837
|
+
return {
|
|
5838
|
+
ok: false,
|
|
5839
|
+
needPick: true,
|
|
5840
|
+
candidates: resolved.candidates,
|
|
5841
|
+
userMessage: `Select a plan to execute:
|
|
5842
|
+
|
|
5843
|
+
${lines}
|
|
5844
|
+
|
|
5845
|
+
Reply with a number or /autopilot-run <slug>.`
|
|
5846
|
+
};
|
|
5847
|
+
}
|
|
5848
|
+
const slug = resolved.slug;
|
|
5849
|
+
if (!isSafeTrackSlug(slug)) {
|
|
5850
|
+
return {
|
|
5851
|
+
ok: false,
|
|
5852
|
+
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
const checklistPath = checklistPathFor(projectRoot, slug, plansDir);
|
|
5856
|
+
const gate = canEnterExecuting({
|
|
5857
|
+
slug,
|
|
5858
|
+
checklistPath,
|
|
5859
|
+
paused: session.paused === 1,
|
|
5860
|
+
projectRoot
|
|
5861
|
+
});
|
|
5862
|
+
if (!gate.ok) {
|
|
5863
|
+
return {
|
|
5864
|
+
ok: false,
|
|
5865
|
+
userMessage: `Cannot start executing: ${gate.reason}.`
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
try {
|
|
5869
|
+
const result = store.exclusiveWrite(() => {
|
|
5870
|
+
if (concurrencyMode === "one_executor") {
|
|
5871
|
+
const other = store.findExecutingSession(conversationId);
|
|
5872
|
+
if (other) {
|
|
5873
|
+
const userMessage = formatOneExecutorBusyMessage(
|
|
5874
|
+
other.track_id,
|
|
5875
|
+
other.conversation_id
|
|
5876
|
+
);
|
|
5877
|
+
return {
|
|
5878
|
+
commit: false,
|
|
5879
|
+
value: {
|
|
5880
|
+
ok: false,
|
|
5881
|
+
busy: true,
|
|
5882
|
+
userMessage
|
|
5883
|
+
}
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
const fresh = store.getSession(conversationId);
|
|
5888
|
+
const alreadyExecutingSameTrack = fresh?.phase === "executing" && fresh.armed === 1 && fresh.paused === 0 && fresh.track_id === slug && sameChecklistBinding(fresh.checklist_path, checklistPath, projectRoot);
|
|
5889
|
+
upsertTrack(store, slug, checklistPath, plansDir, projectRoot);
|
|
5890
|
+
const updated = store.upsertSession({
|
|
5891
|
+
conversation_id: conversationId,
|
|
5892
|
+
project_root: projectRoot,
|
|
5893
|
+
code_root: projectRoot,
|
|
5894
|
+
track_id: slug,
|
|
5895
|
+
checklist_path: checklistPath,
|
|
5896
|
+
phase: "executing",
|
|
5897
|
+
armed: 1,
|
|
5898
|
+
paused: 0,
|
|
5899
|
+
paused_reason: null,
|
|
5900
|
+
pending_action: null,
|
|
5901
|
+
track_candidates_json: null,
|
|
5902
|
+
error_count: 0,
|
|
5903
|
+
idle_stop_count: 0
|
|
5904
|
+
});
|
|
5905
|
+
if (!alreadyExecutingSameTrack) {
|
|
5906
|
+
let reviewingItemId = null;
|
|
5907
|
+
try {
|
|
5908
|
+
const cl = parseChecklist(checklistPath, { projectRoot });
|
|
5909
|
+
reviewingItemId = firstUnchecked(cl)?.id ?? null;
|
|
5910
|
+
} catch {
|
|
5911
|
+
}
|
|
5912
|
+
store.updateReviewChain(conversationId, {
|
|
5913
|
+
fix_round: 0,
|
|
5914
|
+
confirm_left: null,
|
|
5915
|
+
chain_pending: 0,
|
|
5916
|
+
code_edited: 0,
|
|
5917
|
+
item_confirm_complete: 0,
|
|
5918
|
+
reviewing_item_id: reviewingItemId,
|
|
5919
|
+
pending_followup: null,
|
|
5920
|
+
pending_followup_at: null,
|
|
5921
|
+
pending_redeliver_at: null
|
|
5922
|
+
});
|
|
5923
|
+
} else {
|
|
5924
|
+
store.ensureReviewChain(conversationId);
|
|
5925
|
+
}
|
|
5926
|
+
return { commit: true, value: { ok: true, session: updated } };
|
|
5927
|
+
});
|
|
5928
|
+
if (!result.ok && result.busy === true) {
|
|
5929
|
+
logBusyOccupier(result.userMessage);
|
|
5873
5930
|
}
|
|
5874
|
-
|
|
5875
|
-
}
|
|
5876
|
-
|
|
5877
|
-
if (
|
|
5878
|
-
|
|
5931
|
+
return result;
|
|
5932
|
+
} catch (err) {
|
|
5933
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5934
|
+
if (/busy|locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
|
|
5935
|
+
const userMessage = "State database is busy; retry Autopilot RUN in a moment.";
|
|
5936
|
+
logBusyOccupier(userMessage);
|
|
5937
|
+
return {
|
|
5938
|
+
ok: false,
|
|
5939
|
+
busy: true,
|
|
5940
|
+
userMessage
|
|
5941
|
+
};
|
|
5879
5942
|
}
|
|
5880
|
-
|
|
5881
|
-
} else {
|
|
5882
|
-
regexSource = `(?:^|.*/)?${body}$`;
|
|
5943
|
+
throw err;
|
|
5883
5944
|
}
|
|
5884
|
-
return { negated, regex: new RegExp(regexSource) };
|
|
5885
5945
|
}
|
|
5886
|
-
function
|
|
5887
|
-
const
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
if (!compiled) continue;
|
|
5891
|
-
patterns.push(compiled);
|
|
5892
|
-
if (patterns.length >= MAX_AUTOPILOT_IGNORE_PATTERNS) break;
|
|
5946
|
+
function applyReplan(store, conversationId, projectRoot, opts) {
|
|
5947
|
+
const root = requireProjectRoot(store, projectRoot);
|
|
5948
|
+
if (!root) {
|
|
5949
|
+
return { ok: false, userMessage: "Invalid project root." };
|
|
5893
5950
|
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
);
|
|
5899
|
-
function normalizeRelativePath(filePath) {
|
|
5900
|
-
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5901
|
-
}
|
|
5902
|
-
function isAutopilotIgnoredPath(relativePath, patterns) {
|
|
5903
|
-
const norm = normalizeRelativePath(relativePath);
|
|
5904
|
-
let ignored = false;
|
|
5905
|
-
for (const pat of patterns) {
|
|
5906
|
-
if (pat.regex.test(norm)) {
|
|
5907
|
-
ignored = !pat.negated;
|
|
5908
|
-
}
|
|
5951
|
+
projectRoot = root;
|
|
5952
|
+
const plansDir = requirePlansDir(projectRoot, opts?.config?.plansDir);
|
|
5953
|
+
if (!plansDir) {
|
|
5954
|
+
return { ok: false, userMessage: "Invalid plans directory." };
|
|
5909
5955
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5956
|
+
const session = ensureSession(
|
|
5957
|
+
store,
|
|
5958
|
+
conversationId,
|
|
5959
|
+
projectRoot,
|
|
5960
|
+
opts?.platform
|
|
5961
|
+
);
|
|
5962
|
+
let slug = opts?.slug;
|
|
5963
|
+
if (!slug) {
|
|
5964
|
+
const tid = session.track_id;
|
|
5965
|
+
const midPick = session.pending_action === "run" || session.pending_action === "replan";
|
|
5966
|
+
slug = !midPick && isBoundRunTrackId(tid) ? tid : void 0;
|
|
5916
5967
|
}
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
const realDir = fs10.realpathSync(dir);
|
|
5923
|
-
return path9.join(realDir, ...missing, base);
|
|
5924
|
-
} catch {
|
|
5925
|
-
const parent = path9.dirname(dir);
|
|
5926
|
-
if (parent === dir) return absPath;
|
|
5927
|
-
missing.unshift(path9.basename(dir));
|
|
5928
|
-
dir = parent;
|
|
5929
|
-
}
|
|
5968
|
+
if (slug && !isSafeTrackSlug(slug)) {
|
|
5969
|
+
return {
|
|
5970
|
+
ok: false,
|
|
5971
|
+
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
5972
|
+
};
|
|
5930
5973
|
}
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5974
|
+
if (!slug) {
|
|
5975
|
+
const all = listTracks(projectRoot, store, "all", plansDir).filter(
|
|
5976
|
+
(t) => isSafeTrackSlug(t.slug)
|
|
5977
|
+
);
|
|
5978
|
+
if (all.length === 1 && session.pending_action !== "run") {
|
|
5979
|
+
slug = all[0].slug;
|
|
5980
|
+
} else if (all.length > 1 || all.length === 1 && session.pending_action === "run") {
|
|
5981
|
+
const clearingStaleBind = isBoundRunTrackId(session.track_id);
|
|
5982
|
+
store.upsertSession({
|
|
5983
|
+
conversation_id: conversationId,
|
|
5984
|
+
project_root: session.project_root,
|
|
5985
|
+
code_root: session.code_root,
|
|
5986
|
+
track_id: clearingStaleBind ? "_pending" : session.track_id,
|
|
5987
|
+
...clearingStaleBind ? { checklist_path: "" } : {},
|
|
5988
|
+
pending_action: "replan",
|
|
5989
|
+
track_candidates_json: candidatePayload(all),
|
|
5990
|
+
armed: 0
|
|
5991
|
+
});
|
|
5992
|
+
const lines = all.map((t, i) => {
|
|
5993
|
+
const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
|
|
5994
|
+
return ` ${i + 1}. ${t.slug} \u2014 ${title}`;
|
|
5995
|
+
}).join("\n");
|
|
5996
|
+
return {
|
|
5997
|
+
ok: false,
|
|
5998
|
+
needPick: true,
|
|
5999
|
+
candidates: all,
|
|
6000
|
+
userMessage: `Select a plan to replan:
|
|
6001
|
+
|
|
6002
|
+
${lines}
|
|
6003
|
+
|
|
6004
|
+
Reply with a number or /autopilot-replan <slug>.`
|
|
6005
|
+
};
|
|
6006
|
+
} else {
|
|
6007
|
+
return {
|
|
6008
|
+
ok: false,
|
|
6009
|
+
userMessage: "No plan to replan. Use /autopilot-on first."
|
|
6010
|
+
};
|
|
6011
|
+
}
|
|
5936
6012
|
}
|
|
5937
|
-
const
|
|
5938
|
-
|
|
5939
|
-
|
|
6013
|
+
const checklistPath = trustedChecklistPath(
|
|
6014
|
+
projectRoot,
|
|
6015
|
+
plansDir,
|
|
6016
|
+
slug,
|
|
6017
|
+
session.checklist_path,
|
|
6018
|
+
session.track_id
|
|
5940
6019
|
);
|
|
5941
|
-
const
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
6020
|
+
const updated = store.upsertSession({
|
|
6021
|
+
conversation_id: conversationId,
|
|
6022
|
+
project_root: projectRoot,
|
|
6023
|
+
code_root: projectRoot,
|
|
6024
|
+
track_id: slug,
|
|
6025
|
+
checklist_path: checklistPath,
|
|
6026
|
+
phase: "planning",
|
|
6027
|
+
armed: 0,
|
|
6028
|
+
paused: 0,
|
|
6029
|
+
paused_reason: null,
|
|
6030
|
+
pending_action: null,
|
|
6031
|
+
track_candidates_json: null
|
|
6032
|
+
});
|
|
6033
|
+
store.updateReviewChain(conversationId, {
|
|
6034
|
+
fix_round: 0,
|
|
6035
|
+
confirm_left: null,
|
|
6036
|
+
chain_pending: 0,
|
|
6037
|
+
code_edited: 0,
|
|
6038
|
+
item_confirm_complete: 0,
|
|
6039
|
+
reviewing_item_id: null,
|
|
6040
|
+
pending_followup: null,
|
|
6041
|
+
pending_followup_at: null,
|
|
6042
|
+
pending_redeliver_at: null
|
|
6043
|
+
});
|
|
6044
|
+
return { ok: true, session: updated };
|
|
5946
6045
|
}
|
|
5947
|
-
function
|
|
5948
|
-
const root =
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
try {
|
|
5952
|
-
st = fs10.lstatSync(filePath);
|
|
5953
|
-
} catch {
|
|
5954
|
-
return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
|
|
6046
|
+
function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
|
|
6047
|
+
const root = requireProjectRoot(store, projectRoot);
|
|
6048
|
+
if (!root) {
|
|
6049
|
+
return { ok: false, userMessage: "Invalid project root." };
|
|
5955
6050
|
}
|
|
5956
|
-
|
|
5957
|
-
|
|
6051
|
+
projectRoot = root;
|
|
6052
|
+
const session = store.getSession(conversationId);
|
|
6053
|
+
if (!session?.pending_action) {
|
|
6054
|
+
return {
|
|
6055
|
+
ok: false,
|
|
6056
|
+
userMessage: "No pending track selection."
|
|
6057
|
+
};
|
|
5958
6058
|
}
|
|
5959
|
-
|
|
5960
|
-
|
|
6059
|
+
const pending = session.pending_action;
|
|
6060
|
+
if (pending !== "run" && pending !== "replan") {
|
|
6061
|
+
return {
|
|
6062
|
+
ok: false,
|
|
6063
|
+
userMessage: `Unknown pending action "${displayUntrusted(pending)}".`
|
|
6064
|
+
};
|
|
5961
6065
|
}
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
return cached.patterns;
|
|
6066
|
+
if (!session.track_candidates_json) {
|
|
6067
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5965
6068
|
}
|
|
5966
|
-
let
|
|
6069
|
+
let candidates = [];
|
|
5967
6070
|
try {
|
|
5968
|
-
|
|
6071
|
+
const parsed = JSON.parse(session.track_candidates_json);
|
|
6072
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
6073
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
6074
|
+
}
|
|
6075
|
+
const hasValidSlug = parsed.some(
|
|
6076
|
+
(c) => !!c && typeof c === "object" && !Array.isArray(c) && typeof c.slug === "string" && isSafeTrackSlug(c.slug)
|
|
6077
|
+
);
|
|
6078
|
+
if (!hasValidSlug) {
|
|
6079
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
6080
|
+
}
|
|
6081
|
+
candidates = parsed;
|
|
5969
6082
|
} catch {
|
|
5970
|
-
return
|
|
6083
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5971
6084
|
}
|
|
5972
|
-
|
|
5973
|
-
|
|
6085
|
+
let slug;
|
|
6086
|
+
if (/^\d+$/.test(pick)) {
|
|
6087
|
+
const idx = Number.parseInt(pick, 10) - 1;
|
|
6088
|
+
const entry = candidates[idx];
|
|
6089
|
+
slug = entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.slug === "string" ? entry.slug : void 0;
|
|
6090
|
+
if (!slug || !isSafeTrackSlug(slug)) {
|
|
6091
|
+
return {
|
|
6092
|
+
ok: false,
|
|
6093
|
+
userMessage: `Invalid selection "${displayUntrusted(pick)}". Choose 1\u2013${candidates.length}.`
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
6096
|
+
} else {
|
|
6097
|
+
if (!isSafeTrackSlug(pick)) {
|
|
6098
|
+
return {
|
|
6099
|
+
ok: false,
|
|
6100
|
+
userMessage: `Invalid track slug "${displayUntrusted(pick)}".`
|
|
6101
|
+
};
|
|
6102
|
+
}
|
|
6103
|
+
slug = pick;
|
|
6104
|
+
if (!candidates.some(
|
|
6105
|
+
(c) => !!c && typeof c === "object" && !Array.isArray(c) && c.slug === slug
|
|
6106
|
+
)) {
|
|
6107
|
+
return {
|
|
6108
|
+
ok: false,
|
|
6109
|
+
userMessage: `Unknown slug "${displayUntrusted(pick)}".`
|
|
6110
|
+
};
|
|
6111
|
+
}
|
|
5974
6112
|
}
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
function isUntrackedGitIgnored(projectRoot, relativePath) {
|
|
5982
|
-
if (!relativePath || relativePath.includes("\0")) return false;
|
|
5983
|
-
try {
|
|
5984
|
-
const r = spawnSync(
|
|
5985
|
-
"git",
|
|
5986
|
-
["check-ignore", "-q", "--", relativePath],
|
|
5987
|
-
{
|
|
5988
|
-
cwd: projectRoot,
|
|
5989
|
-
encoding: "utf8",
|
|
5990
|
-
timeout: 5e3,
|
|
5991
|
-
windowsHide: true,
|
|
5992
|
-
shell: false
|
|
5993
|
-
}
|
|
5994
|
-
);
|
|
5995
|
-
return r.status === 0;
|
|
5996
|
-
} catch {
|
|
5997
|
-
return false;
|
|
6113
|
+
if (pending === "replan") {
|
|
6114
|
+
return applyReplan(store, conversationId, projectRoot, {
|
|
6115
|
+
slug,
|
|
6116
|
+
config: opts?.config,
|
|
6117
|
+
platform: opts?.platform
|
|
6118
|
+
});
|
|
5998
6119
|
}
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
if (isAutopilotIgnoredPath(relative, patterns)) return false;
|
|
6005
|
-
const root = opts?.projectRoot?.trim();
|
|
6006
|
-
if (root && isUntrackedGitIgnored(root, relative)) return false;
|
|
6007
|
-
return true;
|
|
6120
|
+
return applyRun(store, conversationId, projectRoot, {
|
|
6121
|
+
slug,
|
|
6122
|
+
config: opts?.config,
|
|
6123
|
+
platform: opts?.platform
|
|
6124
|
+
});
|
|
6008
6125
|
}
|
|
6009
6126
|
|
|
6010
6127
|
// ../ports/cursor/src/index.ts
|