@autopilot-harness/cli 0.2.12 → 0.2.14
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 +913 -712
- package/dist/bin.js +14 -0
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init/default-config.d.ts +1 -1
- package/dist/init/default-config.d.ts.map +1 -1
- package/dist/init/default-config.js +3 -2
- package/dist/init/default-config.js.map +1 -1
- package/dist/init/install.d.ts +8 -1
- package/dist/init/install.d.ts.map +1 -1
- package/dist/init/install.js +48 -4
- package/dist/init/install.js.map +1 -1
- package/dist/init/tui.js +8 -8
- package/dist/init/tui.js.map +1 -1
- package/dist/init/types.d.ts +2 -2
- package/dist/init/types.d.ts.map +1 -1
- package/dist/init/types.js +1 -1
- package/dist/init/wizard-helpers.d.ts +9 -1
- package/dist/init/wizard-helpers.d.ts.map +1 -1
- package/dist/init/wizard-helpers.js +22 -4
- package/dist/init/wizard-helpers.js.map +1 -1
- package/dist/locale-set.d.ts.map +1 -1
- package/dist/locale-set.js +23 -2
- package/dist/locale-set.js.map +1 -1
- package/dist/status-doctor.d.ts.map +1 -1
- package/dist/status-doctor.js +12 -10
- package/dist/status-doctor.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +3 -0
- package/dist/upgrade.js.map +1 -1
- package/package.json +5 -5
|
@@ -5,12 +5,12 @@ var en_default = {
|
|
|
5
5
|
help: "Autopilot Harness \u2014 Planning \u2192 Executing agent harness"
|
|
6
6
|
},
|
|
7
7
|
triggers: {
|
|
8
|
-
on: ["Autopilot ON", "Enable autopilot"],
|
|
9
|
-
run: ["Autopilot RUN", "Start execution"],
|
|
10
|
-
off: ["Autopilot OFF", "Disable autopilot"],
|
|
11
|
-
resume: ["Autopilot RESUME"],
|
|
12
|
-
replan: ["Autopilot REPLAN"],
|
|
13
|
-
resume_review: ["Resume review"]
|
|
8
|
+
on: ["Autopilot ON", "Enable autopilot", "\u5F00\u542F\u81EA\u52A8\u9A7E\u9A76"],
|
|
9
|
+
run: ["Autopilot RUN", "Start execution", "\u5F00\u59CB\u6267\u884C"],
|
|
10
|
+
off: ["Autopilot OFF", "Disable autopilot", "\u5173\u95ED\u81EA\u52A8\u9A7E\u9A76"],
|
|
11
|
+
resume: ["Autopilot RESUME", "\u7EE7\u7EED\u6267\u884C"],
|
|
12
|
+
replan: ["Autopilot REPLAN", "\u4FEE\u6539\u65B9\u6848"],
|
|
13
|
+
resume_review: ["Resume review", "\u7EE7\u7EED\u81EA\u5BA1"]
|
|
14
14
|
},
|
|
15
15
|
skill: {
|
|
16
16
|
autopilot_on: {
|
|
@@ -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."
|
|
@@ -81,12 +82,12 @@ var zh_CN_default = {
|
|
|
81
82
|
help: "Autopilot Harness \u2014 \u4E24\u9636\u6BB5 Agent \u5916\u9AA8\u9ABC\uFF1A\u89C4\u5212 \u2192 \u6267\u884C"
|
|
82
83
|
},
|
|
83
84
|
triggers: {
|
|
84
|
-
on: ["Autopilot ON", "\u5F00\u542F\u81EA\u52A8\u9A7E\u9A76"],
|
|
85
|
-
run: ["Autopilot RUN", "\u5F00\u59CB\u6267\u884C"],
|
|
86
|
-
off: ["Autopilot OFF", "\u5173\u95ED\u81EA\u52A8\u9A7E\u9A76"],
|
|
85
|
+
on: ["Autopilot ON", "Enable autopilot", "\u5F00\u542F\u81EA\u52A8\u9A7E\u9A76"],
|
|
86
|
+
run: ["Autopilot RUN", "Start execution", "\u5F00\u59CB\u6267\u884C"],
|
|
87
|
+
off: ["Autopilot OFF", "Disable autopilot", "\u5173\u95ED\u81EA\u52A8\u9A7E\u9A76"],
|
|
87
88
|
resume: ["Autopilot RESUME", "\u7EE7\u7EED\u6267\u884C"],
|
|
88
89
|
replan: ["Autopilot REPLAN", "\u4FEE\u6539\u65B9\u6848"],
|
|
89
|
-
resume_review: ["\u7EE7\u7EED\u81EA\u5BA1"
|
|
90
|
+
resume_review: ["Resume review", "\u7EE7\u7EED\u81EA\u5BA1"]
|
|
90
91
|
},
|
|
91
92
|
skill: {
|
|
92
93
|
autopilot_on: {
|
|
@@ -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,94 +2400,470 @@ function pendingRedeliverAllowed(lastRedeliverAt) {
|
|
|
2398
2400
|
return Date.now() - t >= PENDING_REDELIVER_COOLDOWN_MS;
|
|
2399
2401
|
}
|
|
2400
2402
|
|
|
2401
|
-
// ../core/src/
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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, "\\$&");
|
|
2431
2509
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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;
|
|
2439
2536
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
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;
|
|
2443
2548
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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);
|
|
2450
2557
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
const checklistPath = session.checklist_path;
|
|
2458
|
-
if (!checklistPath) return null;
|
|
2459
|
-
const root = this.trustedProjectRoot();
|
|
2460
|
-
if (!root || !isRealpathInsideProject(root, checklistPath)) {
|
|
2461
|
-
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 += "(?:/.*)?";
|
|
2462
2564
|
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
followingItem: secondUnchecked(cl),
|
|
2469
|
-
checklist: cl
|
|
2470
|
-
};
|
|
2471
|
-
} catch {
|
|
2472
|
-
return null;
|
|
2565
|
+
regexSource += "$";
|
|
2566
|
+
} else if (line.includes("/")) {
|
|
2567
|
+
regexSource = `(?:^|.*/)${body}`;
|
|
2568
|
+
if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
|
|
2569
|
+
regexSource += "(?:/.*)?";
|
|
2473
2570
|
}
|
|
2571
|
+
regexSource += "$";
|
|
2572
|
+
} else {
|
|
2573
|
+
regexSource = `(?:^|.*/)?${body}$`;
|
|
2474
2574
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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;
|
|
2483
2584
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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 ?? "",
|
|
2866
|
+
currentTitle: targets.current?.title ?? "",
|
|
2489
2867
|
nextId: targets.next?.id ?? "",
|
|
2490
2868
|
nextTitle: targets.next?.title ?? ""
|
|
2491
2869
|
}) : this.render("done", {});
|
|
@@ -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;
|
|
@@ -2692,23 +3095,61 @@ var ReviewEngine = class {
|
|
|
2692
3095
|
if (chainNow.confirm_left === null && chainNow.item_confirm_complete === 0 && inChain) {
|
|
2693
3096
|
return this.e3ArmConfirm(session, chainNow);
|
|
2694
3097
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
+
);
|
|
2712
3153
|
if (!cleared) {
|
|
2713
3154
|
try {
|
|
2714
3155
|
const live = this.store.getReviewChain(conversationId)?.pending_followup?.trim() ?? "";
|
|
@@ -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,
|
|
@@ -4676,6 +5084,31 @@ var DEFAULT_PROJECT_REVIEW_CONFIG = {
|
|
|
4676
5084
|
maxErrorsBeforePause: 0,
|
|
4677
5085
|
locale: "en"
|
|
4678
5086
|
};
|
|
5087
|
+
var TRIGGER_PHRASE_KEYS = [
|
|
5088
|
+
"on",
|
|
5089
|
+
"run",
|
|
5090
|
+
"off",
|
|
5091
|
+
"resume",
|
|
5092
|
+
"replan",
|
|
5093
|
+
"resume_review"
|
|
5094
|
+
];
|
|
5095
|
+
function cloneDefaultTriggers() {
|
|
5096
|
+
return {
|
|
5097
|
+
match: "line_start",
|
|
5098
|
+
on: [...DEFAULT_TRIGGERS.on],
|
|
5099
|
+
run: [...DEFAULT_TRIGGERS.run],
|
|
5100
|
+
off: [...DEFAULT_TRIGGERS.off],
|
|
5101
|
+
resume: [...DEFAULT_TRIGGERS.resume],
|
|
5102
|
+
replan: [...DEFAULT_TRIGGERS.replan],
|
|
5103
|
+
resume_review: [...DEFAULT_TRIGGERS.resume_review]
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
function cloneDefaultHookConfig() {
|
|
5107
|
+
return {
|
|
5108
|
+
triggers: cloneDefaultTriggers(),
|
|
5109
|
+
plansDir: "plans"
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
4679
5112
|
function parseReviewScope(raw) {
|
|
4680
5113
|
const s = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
4681
5114
|
if (s === "project" || s === "always" || s === "all") return "project";
|
|
@@ -4716,6 +5149,16 @@ function coerceScalar(value) {
|
|
|
4716
5149
|
if (value === "true") return true;
|
|
4717
5150
|
if (value === "false") return false;
|
|
4718
5151
|
if (value === "null" || value === "~") return null;
|
|
5152
|
+
const trimmed = value.trim();
|
|
5153
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
5154
|
+
try {
|
|
5155
|
+
const parsed = JSON.parse(trimmed);
|
|
5156
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
5157
|
+
return parsed;
|
|
5158
|
+
}
|
|
5159
|
+
} catch {
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
4719
5162
|
return unquote(value);
|
|
4720
5163
|
}
|
|
4721
5164
|
function lineIndent(line) {
|
|
@@ -4819,70 +5262,95 @@ function parseVerifyCommands(raw) {
|
|
|
4819
5262
|
}
|
|
4820
5263
|
return out;
|
|
4821
5264
|
}
|
|
4822
|
-
function
|
|
5265
|
+
function readProjectConfigYaml(projectRoot) {
|
|
4823
5266
|
const root = normalizeProjectRoot(projectRoot);
|
|
4824
|
-
if (!root)
|
|
4825
|
-
|
|
4826
|
-
}
|
|
4827
|
-
const configPath = path6.join(root, ".autopilot", "config.yml");
|
|
5267
|
+
if (!root) return null;
|
|
5268
|
+
const configPath = path8.join(root, ".autopilot", "config.yml");
|
|
4828
5269
|
try {
|
|
4829
|
-
const nofollow = typeof
|
|
5270
|
+
const nofollow = typeof fs8.constants.O_NOFOLLOW === "number" ? fs8.constants.O_NOFOLLOW : 0;
|
|
4830
5271
|
if (nofollow === 0) {
|
|
4831
|
-
if (!
|
|
4832
|
-
if (
|
|
4833
|
-
return cloneDefaultProjectReviewConfig();
|
|
4834
|
-
}
|
|
5272
|
+
if (!fs8.existsSync(configPath)) return null;
|
|
5273
|
+
if (fs8.lstatSync(configPath).isSymbolicLink()) return null;
|
|
4835
5274
|
}
|
|
4836
5275
|
let fd;
|
|
4837
5276
|
try {
|
|
4838
|
-
fd =
|
|
5277
|
+
fd = fs8.openSync(configPath, fs8.constants.O_RDONLY | nofollow);
|
|
4839
5278
|
} catch {
|
|
4840
|
-
return
|
|
5279
|
+
return null;
|
|
4841
5280
|
}
|
|
4842
5281
|
let raw;
|
|
4843
5282
|
try {
|
|
4844
|
-
const st =
|
|
4845
|
-
if (!st.isFile() || st.size > MAX_CONFIG_BYTES)
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
if (
|
|
4850
|
-
return cloneDefaultProjectReviewConfig();
|
|
4851
|
-
}
|
|
4852
|
-
if (lst.ino !== st.ino || lst.dev !== st.dev) {
|
|
4853
|
-
return cloneDefaultProjectReviewConfig();
|
|
4854
|
-
}
|
|
4855
|
-
if (!isRealpathInsideProject(root, configPath)) {
|
|
4856
|
-
return cloneDefaultProjectReviewConfig();
|
|
4857
|
-
}
|
|
5283
|
+
const st = fs8.fstatSync(fd);
|
|
5284
|
+
if (!st.isFile() || st.size > MAX_CONFIG_BYTES) return null;
|
|
5285
|
+
const lst = fs8.lstatSync(configPath);
|
|
5286
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return null;
|
|
5287
|
+
if (lst.ino !== st.ino || lst.dev !== st.dev) return null;
|
|
5288
|
+
if (!isRealpathInsideProject(root, configPath)) return null;
|
|
4858
5289
|
const buf = Buffer.alloc(st.size);
|
|
4859
|
-
const n =
|
|
5290
|
+
const n = fs8.readSync(fd, buf, 0, st.size, 0);
|
|
4860
5291
|
raw = buf.subarray(0, n).toString("utf8");
|
|
4861
5292
|
} finally {
|
|
4862
|
-
|
|
4863
|
-
}
|
|
4864
|
-
if (Buffer.byteLength(raw, "utf8") > MAX_CONFIG_BYTES) {
|
|
4865
|
-
return cloneDefaultProjectReviewConfig();
|
|
5293
|
+
fs8.closeSync(fd);
|
|
4866
5294
|
}
|
|
5295
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_CONFIG_BYTES) return null;
|
|
4867
5296
|
const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
4868
5297
|
const parsed = parseSimpleYaml(text);
|
|
4869
|
-
if (!isPlainObject(parsed)) return
|
|
4870
|
-
|
|
4871
|
-
const verify = isPlainObject(review.verify) ? review.verify : {};
|
|
4872
|
-
const stuck = isPlainObject(review.stuck) ? review.stuck : {};
|
|
4873
|
-
const errors = isPlainObject(review.errors) ? review.errors : {};
|
|
4874
|
-
return normalizeProjectReviewConfig({
|
|
4875
|
-
confirmRounds: review.confirm_rounds,
|
|
4876
|
-
reviewScope: review.scope,
|
|
4877
|
-
verifyEnabled: verify.enabled,
|
|
4878
|
-
verifyCommands: verify.commands,
|
|
4879
|
-
maxIdleStops: stuck.max_idle_stops,
|
|
4880
|
-
maxErrorsBeforePause: errors.max_before_pause,
|
|
4881
|
-
locale: parsed.locale
|
|
4882
|
-
});
|
|
5298
|
+
if (!isPlainObject(parsed)) return null;
|
|
5299
|
+
return { root, parsed };
|
|
4883
5300
|
} catch {
|
|
4884
|
-
return
|
|
5301
|
+
return null;
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
function nonEmptyPhraseList(raw) {
|
|
5305
|
+
if (!Array.isArray(raw)) return null;
|
|
5306
|
+
const out = [];
|
|
5307
|
+
for (const item of raw) {
|
|
5308
|
+
if (typeof item !== "string") continue;
|
|
5309
|
+
const t = item.trim();
|
|
5310
|
+
if (t) out.push(t);
|
|
5311
|
+
}
|
|
5312
|
+
return out.length > 0 ? out : null;
|
|
5313
|
+
}
|
|
5314
|
+
function triggersFromParsed(parsed) {
|
|
5315
|
+
const base = cloneDefaultTriggers();
|
|
5316
|
+
const triggers = isPlainObject(parsed.triggers) ? parsed.triggers : {};
|
|
5317
|
+
for (const key of TRIGGER_PHRASE_KEYS) {
|
|
5318
|
+
const phrases = nonEmptyPhraseList(triggers[key]);
|
|
5319
|
+
if (phrases) base[key] = phrases;
|
|
4885
5320
|
}
|
|
5321
|
+
return base;
|
|
5322
|
+
}
|
|
5323
|
+
function plansDirFromParsed(root, parsed) {
|
|
5324
|
+
const artifacts = isPlainObject(parsed.artifacts) ? parsed.artifacts : {};
|
|
5325
|
+
const raw = artifacts.plans_dir;
|
|
5326
|
+
const candidate = typeof raw === "string" ? raw : "plans";
|
|
5327
|
+
return normalizeInProjectPlansDir(root, candidate) ?? "plans";
|
|
5328
|
+
}
|
|
5329
|
+
function loadProjectReviewConfig(projectRoot) {
|
|
5330
|
+
const loaded = readProjectConfigYaml(projectRoot);
|
|
5331
|
+
if (!loaded) return cloneDefaultProjectReviewConfig();
|
|
5332
|
+
const { parsed } = loaded;
|
|
5333
|
+
const review = isPlainObject(parsed.review) ? parsed.review : {};
|
|
5334
|
+
const verify = isPlainObject(review.verify) ? review.verify : {};
|
|
5335
|
+
const stuck = isPlainObject(review.stuck) ? review.stuck : {};
|
|
5336
|
+
const errors = isPlainObject(review.errors) ? review.errors : {};
|
|
5337
|
+
return normalizeProjectReviewConfig({
|
|
5338
|
+
confirmRounds: review.confirm_rounds,
|
|
5339
|
+
reviewScope: review.scope,
|
|
5340
|
+
verifyEnabled: verify.enabled,
|
|
5341
|
+
verifyCommands: verify.commands,
|
|
5342
|
+
maxIdleStops: stuck.max_idle_stops,
|
|
5343
|
+
maxErrorsBeforePause: errors.max_before_pause,
|
|
5344
|
+
locale: parsed.locale
|
|
5345
|
+
});
|
|
5346
|
+
}
|
|
5347
|
+
function loadProjectHookConfig(projectRoot) {
|
|
5348
|
+
const loaded = readProjectConfigYaml(projectRoot);
|
|
5349
|
+
if (!loaded) return cloneDefaultHookConfig();
|
|
5350
|
+
return {
|
|
5351
|
+
triggers: triggersFromParsed(loaded.parsed),
|
|
5352
|
+
plansDir: plansDirFromParsed(loaded.root, loaded.parsed)
|
|
5353
|
+
};
|
|
4886
5354
|
}
|
|
4887
5355
|
function normalizeProjectReviewConfig(raw) {
|
|
4888
5356
|
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -4959,6 +5427,12 @@ function createRenderFollowup(bundle) {
|
|
|
4959
5427
|
);
|
|
4960
5428
|
case "stuck":
|
|
4961
5429
|
return renderTemplate(f.stuck ?? "", vars);
|
|
5430
|
+
case "stuck_soft":
|
|
5431
|
+
return renderTemplate(
|
|
5432
|
+
// Never fall back to hard `stuck` (RESUME-required) — that defeats C2.
|
|
5433
|
+
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.",
|
|
5434
|
+
vars
|
|
5435
|
+
);
|
|
4962
5436
|
case "verify_fix":
|
|
4963
5437
|
return renderTemplate(
|
|
4964
5438
|
f.verify_fix ?? "Verify failed ({reason}). Fix verify commands and rewrite verify-last.json; do not advance.",
|
|
@@ -5011,12 +5485,12 @@ function createConfiguredReviewEngine(store, projectRoot, localeBundle, preloade
|
|
|
5011
5485
|
}
|
|
5012
5486
|
|
|
5013
5487
|
// ../core/src/phase-actions.ts
|
|
5014
|
-
import
|
|
5015
|
-
import
|
|
5488
|
+
import fs10 from "node:fs";
|
|
5489
|
+
import path10 from "node:path";
|
|
5016
5490
|
|
|
5017
5491
|
// ../core/src/list-tracks.ts
|
|
5018
|
-
import
|
|
5019
|
-
import
|
|
5492
|
+
import fs9 from "node:fs";
|
|
5493
|
+
import path9 from "node:path";
|
|
5020
5494
|
function isRunnableTrack(t) {
|
|
5021
5495
|
if (t.paused) return false;
|
|
5022
5496
|
const unchecked = t.checklistTotal - t.checklistDone;
|
|
@@ -5027,19 +5501,19 @@ function readPlansDir(root, plansDir = "plans") {
|
|
|
5027
5501
|
if (typeof plansDir !== "string" || !root || root.includes("\0") || plansDir.includes("\0")) {
|
|
5028
5502
|
return [];
|
|
5029
5503
|
}
|
|
5030
|
-
const dir =
|
|
5504
|
+
const dir = path9.join(root, plansDir);
|
|
5031
5505
|
try {
|
|
5032
|
-
const lst =
|
|
5506
|
+
const lst = fs9.lstatSync(dir);
|
|
5033
5507
|
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5034
5508
|
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5035
5509
|
} catch {
|
|
5036
5510
|
return [];
|
|
5037
5511
|
}
|
|
5038
|
-
const names =
|
|
5512
|
+
const names = fs9.readdirSync(dir, { withFileTypes: true }).filter(
|
|
5039
5513
|
(d) => d.isDirectory() && !d.isSymbolicLink() && isSafeTrackSlug(d.name)
|
|
5040
5514
|
).map((d) => d.name);
|
|
5041
5515
|
try {
|
|
5042
|
-
const lst =
|
|
5516
|
+
const lst = fs9.lstatSync(dir);
|
|
5043
5517
|
if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
|
|
5044
5518
|
if (!isRealpathInsideProject(root, dir)) return [];
|
|
5045
5519
|
} catch {
|
|
@@ -5053,27 +5527,27 @@ function titleFromPlan(planPath, slug, projectRoot) {
|
|
|
5053
5527
|
if (!root) return slug;
|
|
5054
5528
|
const maxBytes = 65536;
|
|
5055
5529
|
try {
|
|
5056
|
-
const nofollow = typeof
|
|
5530
|
+
const nofollow = typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
|
|
5057
5531
|
if (nofollow === 0) {
|
|
5058
|
-
const lst =
|
|
5532
|
+
const lst = fs9.lstatSync(planPath);
|
|
5059
5533
|
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5060
5534
|
}
|
|
5061
|
-
const fd =
|
|
5535
|
+
const fd = fs9.openSync(planPath, fs9.constants.O_RDONLY | nofollow);
|
|
5062
5536
|
try {
|
|
5063
|
-
const st =
|
|
5537
|
+
const st = fs9.fstatSync(fd);
|
|
5064
5538
|
if (!st.isFile() || st.size <= 0) return slug;
|
|
5065
|
-
const lst =
|
|
5539
|
+
const lst = fs9.lstatSync(planPath);
|
|
5066
5540
|
if (lst.isSymbolicLink() || !lst.isFile()) return slug;
|
|
5067
5541
|
if (lst.ino !== st.ino || lst.dev !== st.dev) return slug;
|
|
5068
5542
|
if (!isRealpathInsideProject(root, planPath)) return slug;
|
|
5069
5543
|
const len = Math.min(st.size, maxBytes);
|
|
5070
5544
|
const buf = Buffer.alloc(len);
|
|
5071
|
-
const n =
|
|
5545
|
+
const n = fs9.readSync(fd, buf, 0, len, 0);
|
|
5072
5546
|
const first = buf.subarray(0, n).toString("utf8").split(/\r?\n/)[0] ?? "";
|
|
5073
5547
|
const m = first.match(/^#\s+(.+)/);
|
|
5074
5548
|
return m?.[1]?.trim() ?? slug;
|
|
5075
5549
|
} finally {
|
|
5076
|
-
|
|
5550
|
+
fs9.closeSync(fd);
|
|
5077
5551
|
}
|
|
5078
5552
|
} catch {
|
|
5079
5553
|
return slug;
|
|
@@ -5089,16 +5563,16 @@ function listTracks(root, store, filter = "all", plansDir = "plans") {
|
|
|
5089
5563
|
const slugs = readPlansDir(root, plansDir);
|
|
5090
5564
|
const tracks = [];
|
|
5091
5565
|
for (const slug of slugs) {
|
|
5092
|
-
const trackDir =
|
|
5566
|
+
const trackDir = path9.join(root, plansDir, slug);
|
|
5093
5567
|
try {
|
|
5094
|
-
const lst =
|
|
5568
|
+
const lst = fs9.lstatSync(trackDir);
|
|
5095
5569
|
if (lst.isSymbolicLink() || !lst.isDirectory()) continue;
|
|
5096
5570
|
if (!isRealpathInsideProject(root, trackDir)) continue;
|
|
5097
5571
|
} catch {
|
|
5098
5572
|
continue;
|
|
5099
5573
|
}
|
|
5100
|
-
const planPath =
|
|
5101
|
-
const checklistPath =
|
|
5574
|
+
const planPath = path9.join(trackDir, "plan.md");
|
|
5575
|
+
const checklistPath = path9.join(trackDir, "checklist.md");
|
|
5102
5576
|
let checklistTotal = 0;
|
|
5103
5577
|
let checklistDone = 0;
|
|
5104
5578
|
const checklistInProject = isRealpathInsideProject(root, checklistPath);
|
|
@@ -5170,7 +5644,7 @@ function canEnterExecuting(options) {
|
|
|
5170
5644
|
if (!root) {
|
|
5171
5645
|
return { ok: false, reason: "invalid project root" };
|
|
5172
5646
|
}
|
|
5173
|
-
if (!checklistPath || !
|
|
5647
|
+
if (!checklistPath || !fs9.existsSync(checklistPath)) {
|
|
5174
5648
|
return { ok: false, reason: "checklist missing" };
|
|
5175
5649
|
}
|
|
5176
5650
|
if (!isRealpathInsideProject(root, checklistPath)) {
|
|
@@ -5219,8 +5693,8 @@ function nowIso2() {
|
|
|
5219
5693
|
}
|
|
5220
5694
|
function checklistPathFor(projectRoot, slug, plansDir) {
|
|
5221
5695
|
const root = normalizeProjectRoot(projectRoot);
|
|
5222
|
-
if (!root) return
|
|
5223
|
-
return
|
|
5696
|
+
if (!root) return path10.join(plansDir, slug, "checklist.md");
|
|
5697
|
+
return path10.join(root, plansDir, slug, "checklist.md");
|
|
5224
5698
|
}
|
|
5225
5699
|
function sameChecklistBinding(stored, rebuilt, projectRoot) {
|
|
5226
5700
|
if (stored === rebuilt) return true;
|
|
@@ -5230,8 +5704,8 @@ function sameChecklistBinding(stored, rebuilt, projectRoot) {
|
|
|
5230
5704
|
const root = normalizeProjectRoot(projectRoot);
|
|
5231
5705
|
if (!root) return false;
|
|
5232
5706
|
try {
|
|
5233
|
-
const absStored =
|
|
5234
|
-
const absRebuilt =
|
|
5707
|
+
const absStored = path10.isAbsolute(stored) ? path10.resolve(stored) : path10.resolve(root, stored);
|
|
5708
|
+
const absRebuilt = path10.isAbsolute(rebuilt) ? path10.resolve(rebuilt) : path10.resolve(root, rebuilt);
|
|
5235
5709
|
return absStored === absRebuilt;
|
|
5236
5710
|
} catch {
|
|
5237
5711
|
return false;
|
|
@@ -5251,8 +5725,8 @@ function isChecklistPathAllowed(projectRoot, checklistPath) {
|
|
|
5251
5725
|
const root = normalizeProjectRoot(projectRoot);
|
|
5252
5726
|
if (!root) return false;
|
|
5253
5727
|
try {
|
|
5254
|
-
|
|
5255
|
-
|
|
5728
|
+
fs10.lstatSync(
|
|
5729
|
+
path10.isAbsolute(checklistPath) ? checklistPath : path10.resolve(root, checklistPath)
|
|
5256
5730
|
);
|
|
5257
5731
|
return isRealpathInsideProject(root, checklistPath);
|
|
5258
5732
|
} catch {
|
|
@@ -5289,8 +5763,8 @@ function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
|
|
|
5289
5763
|
const ts = nowIso2();
|
|
5290
5764
|
const root = normalizeProjectRoot(projectRoot);
|
|
5291
5765
|
if (!root) return;
|
|
5292
|
-
const planPath =
|
|
5293
|
-
const briefPath =
|
|
5766
|
+
const planPath = path10.join(root, plansDir, slug, "plan.md");
|
|
5767
|
+
const briefPath = path10.join(root, plansDir, slug, "brief.md");
|
|
5294
5768
|
store.db.prepare(
|
|
5295
5769
|
`INSERT INTO tracks (track_id, slug, checklist_path, plan_path, brief_path, updated_at)
|
|
5296
5770
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -5304,8 +5778,8 @@ function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
|
|
|
5304
5778
|
slug,
|
|
5305
5779
|
slug,
|
|
5306
5780
|
checklistPath,
|
|
5307
|
-
|
|
5308
|
-
|
|
5781
|
+
fs10.existsSync(planPath) ? planPath : null,
|
|
5782
|
+
fs10.existsSync(briefPath) ? briefPath : null,
|
|
5309
5783
|
ts
|
|
5310
5784
|
);
|
|
5311
5785
|
}
|
|
@@ -5514,497 +5988,200 @@ Reply with a number or /autopilot-run <slug>.`
|
|
|
5514
5988
|
if (!result.ok && result.busy === true) {
|
|
5515
5989
|
logBusyOccupier(result.userMessage);
|
|
5516
5990
|
}
|
|
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
|
-
}
|
|
5529
|
-
throw err;
|
|
5530
|
-
}
|
|
5531
|
-
}
|
|
5532
|
-
function applyReplan(store, conversationId, projectRoot, opts) {
|
|
5533
|
-
const root = requireProjectRoot(store, projectRoot);
|
|
5534
|
-
if (!root) {
|
|
5535
|
-
return { ok: false, userMessage: "Invalid project root." };
|
|
5536
|
-
}
|
|
5537
|
-
projectRoot = root;
|
|
5538
|
-
const plansDir = requirePlansDir(projectRoot, opts?.config?.plansDir);
|
|
5539
|
-
if (!plansDir) {
|
|
5540
|
-
return { ok: false, userMessage: "Invalid plans directory." };
|
|
5541
|
-
}
|
|
5542
|
-
const session = ensureSession(
|
|
5543
|
-
store,
|
|
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;
|
|
5553
|
-
}
|
|
5554
|
-
if (slug && !isSafeTrackSlug(slug)) {
|
|
5555
|
-
return {
|
|
5556
|
-
ok: false,
|
|
5557
|
-
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
5558
|
-
};
|
|
5559
|
-
}
|
|
5560
|
-
if (!slug) {
|
|
5561
|
-
const all = listTracks(projectRoot, store, "all", plansDir).filter(
|
|
5562
|
-
(t) => isSafeTrackSlug(t.slug)
|
|
5563
|
-
);
|
|
5564
|
-
if (all.length === 1 && session.pending_action !== "run") {
|
|
5565
|
-
slug = all[0].slug;
|
|
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
|
-
}
|
|
5599
|
-
const checklistPath = trustedChecklistPath(
|
|
5600
|
-
projectRoot,
|
|
5601
|
-
plansDir,
|
|
5602
|
-
slug,
|
|
5603
|
-
session.checklist_path,
|
|
5604
|
-
session.track_id
|
|
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 };
|
|
5631
|
-
}
|
|
5632
|
-
function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
|
|
5633
|
-
const root = requireProjectRoot(store, projectRoot);
|
|
5634
|
-
if (!root) {
|
|
5635
|
-
return { ok: false, userMessage: "Invalid project root." };
|
|
5636
|
-
}
|
|
5637
|
-
projectRoot = root;
|
|
5638
|
-
const session = store.getSession(conversationId);
|
|
5639
|
-
if (!session?.pending_action) {
|
|
5640
|
-
return {
|
|
5641
|
-
ok: false,
|
|
5642
|
-
userMessage: "No pending track selection."
|
|
5643
|
-
};
|
|
5644
|
-
}
|
|
5645
|
-
const pending = session.pending_action;
|
|
5646
|
-
if (pending !== "run" && pending !== "replan") {
|
|
5647
|
-
return {
|
|
5648
|
-
ok: false,
|
|
5649
|
-
userMessage: `Unknown pending action "${displayUntrusted(pending)}".`
|
|
5650
|
-
};
|
|
5651
|
-
}
|
|
5652
|
-
if (!session.track_candidates_json) {
|
|
5653
|
-
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5654
|
-
}
|
|
5655
|
-
let candidates = [];
|
|
5656
|
-
try {
|
|
5657
|
-
const parsed = JSON.parse(session.track_candidates_json);
|
|
5658
|
-
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
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)
|
|
5663
|
-
);
|
|
5664
|
-
if (!hasValidSlug) {
|
|
5665
|
-
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5666
|
-
}
|
|
5667
|
-
candidates = parsed;
|
|
5668
|
-
} catch {
|
|
5669
|
-
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5670
|
-
}
|
|
5671
|
-
let slug;
|
|
5672
|
-
if (/^\d+$/.test(pick)) {
|
|
5673
|
-
const idx = Number.parseInt(pick, 10) - 1;
|
|
5674
|
-
const entry = candidates[idx];
|
|
5675
|
-
slug = entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.slug === "string" ? entry.slug : void 0;
|
|
5676
|
-
if (!slug || !isSafeTrackSlug(slug)) {
|
|
5677
|
-
return {
|
|
5678
|
-
ok: false,
|
|
5679
|
-
userMessage: `Invalid selection "${displayUntrusted(pick)}". Choose 1\u2013${candidates.length}.`
|
|
5680
|
-
};
|
|
5681
|
-
}
|
|
5682
|
-
} else {
|
|
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
|
-
};
|
|
5697
|
-
}
|
|
5698
|
-
}
|
|
5699
|
-
if (pending === "replan") {
|
|
5700
|
-
return applyReplan(store, conversationId, projectRoot, {
|
|
5701
|
-
slug,
|
|
5702
|
-
config: opts?.config,
|
|
5703
|
-
platform: opts?.platform
|
|
5704
|
-
});
|
|
5705
|
-
}
|
|
5706
|
-
return applyRun(store, conversationId, projectRoot, {
|
|
5707
|
-
slug,
|
|
5708
|
-
config: opts?.config,
|
|
5709
|
-
platform: opts?.platform
|
|
5710
|
-
});
|
|
5711
|
-
}
|
|
5712
|
-
|
|
5713
|
-
// ../core/src/code-edit-detector.ts
|
|
5714
|
-
import { spawnSync } from "node:child_process";
|
|
5715
|
-
|
|
5716
|
-
// ../core/src/autopilot-ignore.ts
|
|
5717
|
-
import fs10 from "node:fs";
|
|
5718
|
-
import path9 from "node:path";
|
|
5719
|
-
var DEFAULT_AUTOPILOT_IGNORE_TEXT = `# Autopilot \u2014 paths that do NOT trigger self-review (gitignore syntax).
|
|
5720
|
-
#
|
|
5721
|
-
# What this file is:
|
|
5722
|
-
# - Controls whether an afterFileEdit counts as "product code" (opens fix/confirm).
|
|
5723
|
-
# - Does NOT change \`git diff\` / \`git status\` output (that is \`.gitignore\`).
|
|
5724
|
-
# - Review followups ask the agent to skip these paths when reading diffs (soft).
|
|
5725
|
-
#
|
|
5726
|
-
# Semantics:
|
|
5727
|
-
# - Same glob rules as gitignore; last matching pattern wins.
|
|
5728
|
-
# - Use \`!\` to force-include an exception (e.g. \`!docs/feed/**/*.yml\`).
|
|
5729
|
-
# - Markdown (*.md / *.mdx) is NOT ignored by default \u2014 design docs can be reviewed.
|
|
5730
|
-
# - \`docs/**\` is NOT ignored by default.
|
|
5731
|
-
# - Also skip untracked paths ignored by \`.gitignore\` (tracked files still count).
|
|
5732
|
-
#
|
|
5733
|
-
# Later (not implemented): hard-filtered review-diff / path ledger \u2014 see
|
|
5734
|
-
# docs/autopilot/workflows/autopilot-executing.md (B2 strong).
|
|
5735
|
-
|
|
5736
|
-
# Runtime / editor (prefer also listing these in .gitignore)
|
|
5737
|
-
.autopilot/**
|
|
5738
|
-
.cursor/**
|
|
5739
|
-
.claude/**
|
|
5740
|
-
|
|
5741
|
-
# Planning artifacts
|
|
5742
|
-
plans/**
|
|
5743
|
-
|
|
5744
|
-
# Common build / vendor trees
|
|
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, "\\$&");
|
|
5818
|
-
}
|
|
5819
|
-
function globBodyToRegex(glob) {
|
|
5820
|
-
let out = "";
|
|
5821
|
-
for (let i = 0; i < glob.length; ) {
|
|
5822
|
-
const ch = glob[i];
|
|
5823
|
-
if (ch === "*" && glob[i + 1] === "*") {
|
|
5824
|
-
if (glob[i + 2] === "/") {
|
|
5825
|
-
out += "(?:.*/)?";
|
|
5826
|
-
i += 3;
|
|
5827
|
-
continue;
|
|
5828
|
-
}
|
|
5829
|
-
out += ".*";
|
|
5830
|
-
i += 2;
|
|
5831
|
-
continue;
|
|
5832
|
-
}
|
|
5833
|
-
if (ch === "*") {
|
|
5834
|
-
out += "[^/]*";
|
|
5835
|
-
i += 1;
|
|
5836
|
-
continue;
|
|
5837
|
-
}
|
|
5838
|
-
if (ch === "?") {
|
|
5839
|
-
out += "[^/]";
|
|
5840
|
-
i += 1;
|
|
5841
|
-
continue;
|
|
5842
|
-
}
|
|
5843
|
-
out += escapeRegexChar(ch);
|
|
5844
|
-
i += 1;
|
|
5845
|
-
}
|
|
5846
|
-
return out;
|
|
5847
|
-
}
|
|
5848
|
-
function compilePattern(raw) {
|
|
5849
|
-
let line = raw.trim();
|
|
5850
|
-
if (!line || line.startsWith("#")) return null;
|
|
5851
|
-
if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
|
|
5852
|
-
let negated = false;
|
|
5853
|
-
if (line.startsWith("!")) {
|
|
5854
|
-
negated = true;
|
|
5855
|
-
line = line.slice(1).trim();
|
|
5856
|
-
if (!line || line.startsWith("#")) return null;
|
|
5857
|
-
}
|
|
5858
|
-
const dirOnly = line.endsWith("/");
|
|
5859
|
-
if (dirOnly) line = line.slice(0, -1);
|
|
5860
|
-
if (!line) return null;
|
|
5861
|
-
if (line.length > MAX_AUTOPILOT_IGNORE_LINE_CHARS) return null;
|
|
5862
|
-
let anchored = false;
|
|
5863
|
-
if (line.startsWith("/")) {
|
|
5864
|
-
anchored = true;
|
|
5865
|
-
line = line.slice(1);
|
|
5866
|
-
}
|
|
5867
|
-
const body = globBodyToRegex(line);
|
|
5868
|
-
let regexSource;
|
|
5869
|
-
if (anchored) {
|
|
5870
|
-
regexSource = `^${body}`;
|
|
5871
|
-
if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
|
|
5872
|
-
regexSource += "(?:/.*)?";
|
|
5873
|
-
}
|
|
5874
|
-
regexSource += "$";
|
|
5875
|
-
} else if (line.includes("/")) {
|
|
5876
|
-
regexSource = `(?:^|.*/)${body}`;
|
|
5877
|
-
if (dirOnly || line.endsWith("/**") || line.endsWith("/*")) {
|
|
5878
|
-
regexSource += "(?:/.*)?";
|
|
5879
|
-
}
|
|
5880
|
-
regexSource += "$";
|
|
5881
|
-
} else {
|
|
5882
|
-
regexSource = `(?:^|.*/)?${body}$`;
|
|
5883
|
-
}
|
|
5884
|
-
return { negated, regex: new RegExp(regexSource) };
|
|
5885
|
-
}
|
|
5886
|
-
function parseAutopilotIgnore(text) {
|
|
5887
|
-
const patterns = [];
|
|
5888
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
5889
|
-
const compiled = compilePattern(rawLine);
|
|
5890
|
-
if (!compiled) continue;
|
|
5891
|
-
patterns.push(compiled);
|
|
5892
|
-
if (patterns.length >= MAX_AUTOPILOT_IGNORE_PATTERNS) break;
|
|
5893
|
-
}
|
|
5894
|
-
return patterns;
|
|
5895
|
-
}
|
|
5896
|
-
var DEFAULT_AUTOPILOT_IGNORE_PATTERNS = parseAutopilotIgnore(
|
|
5897
|
-
DEFAULT_AUTOPILOT_IGNORE_TEXT
|
|
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
|
-
}
|
|
5909
|
-
}
|
|
5910
|
-
return ignored;
|
|
5911
|
-
}
|
|
5912
|
-
function realpathForCompare(absPath) {
|
|
5913
|
-
try {
|
|
5914
|
-
return fs10.realpathSync(absPath);
|
|
5915
|
-
} catch {
|
|
5916
|
-
}
|
|
5917
|
-
let dir = path9.dirname(absPath);
|
|
5918
|
-
const base = path9.basename(absPath);
|
|
5919
|
-
const missing = [];
|
|
5920
|
-
for (; ; ) {
|
|
5921
|
-
try {
|
|
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;
|
|
5991
|
+
return result;
|
|
5992
|
+
} catch (err) {
|
|
5993
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5994
|
+
if (/busy|locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
|
|
5995
|
+
const userMessage = "State database is busy; retry Autopilot RUN in a moment.";
|
|
5996
|
+
logBusyOccupier(userMessage);
|
|
5997
|
+
return {
|
|
5998
|
+
ok: false,
|
|
5999
|
+
busy: true,
|
|
6000
|
+
userMessage
|
|
6001
|
+
};
|
|
5929
6002
|
}
|
|
6003
|
+
throw err;
|
|
5930
6004
|
}
|
|
5931
6005
|
}
|
|
5932
|
-
function
|
|
5933
|
-
const
|
|
5934
|
-
if (!
|
|
5935
|
-
return
|
|
6006
|
+
function applyReplan(store, conversationId, projectRoot, opts) {
|
|
6007
|
+
const root = requireProjectRoot(store, projectRoot);
|
|
6008
|
+
if (!root) {
|
|
6009
|
+
return { ok: false, userMessage: "Invalid project root." };
|
|
5936
6010
|
}
|
|
5937
|
-
|
|
5938
|
-
const
|
|
5939
|
-
|
|
6011
|
+
projectRoot = root;
|
|
6012
|
+
const plansDir = requirePlansDir(projectRoot, opts?.config?.plansDir);
|
|
6013
|
+
if (!plansDir) {
|
|
6014
|
+
return { ok: false, userMessage: "Invalid plans directory." };
|
|
6015
|
+
}
|
|
6016
|
+
const session = ensureSession(
|
|
6017
|
+
store,
|
|
6018
|
+
conversationId,
|
|
6019
|
+
projectRoot,
|
|
6020
|
+
opts?.platform
|
|
5940
6021
|
);
|
|
5941
|
-
|
|
5942
|
-
if (
|
|
5943
|
-
|
|
6022
|
+
let slug = opts?.slug;
|
|
6023
|
+
if (!slug) {
|
|
6024
|
+
const tid = session.track_id;
|
|
6025
|
+
const midPick = session.pending_action === "run" || session.pending_action === "replan";
|
|
6026
|
+
slug = !midPick && isBoundRunTrackId(tid) ? tid : void 0;
|
|
5944
6027
|
}
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
let st;
|
|
5951
|
-
try {
|
|
5952
|
-
st = fs10.lstatSync(filePath);
|
|
5953
|
-
} catch {
|
|
5954
|
-
return DEFAULT_AUTOPILOT_IGNORE_PATTERNS;
|
|
6028
|
+
if (slug && !isSafeTrackSlug(slug)) {
|
|
6029
|
+
return {
|
|
6030
|
+
ok: false,
|
|
6031
|
+
userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
|
|
6032
|
+
};
|
|
5955
6033
|
}
|
|
5956
|
-
if (
|
|
5957
|
-
|
|
6034
|
+
if (!slug) {
|
|
6035
|
+
const all = listTracks(projectRoot, store, "all", plansDir).filter(
|
|
6036
|
+
(t) => isSafeTrackSlug(t.slug)
|
|
6037
|
+
);
|
|
6038
|
+
if (all.length === 1 && session.pending_action !== "run") {
|
|
6039
|
+
slug = all[0].slug;
|
|
6040
|
+
} else if (all.length > 1 || all.length === 1 && session.pending_action === "run") {
|
|
6041
|
+
const clearingStaleBind = isBoundRunTrackId(session.track_id);
|
|
6042
|
+
store.upsertSession({
|
|
6043
|
+
conversation_id: conversationId,
|
|
6044
|
+
project_root: session.project_root,
|
|
6045
|
+
code_root: session.code_root,
|
|
6046
|
+
track_id: clearingStaleBind ? "_pending" : session.track_id,
|
|
6047
|
+
...clearingStaleBind ? { checklist_path: "" } : {},
|
|
6048
|
+
pending_action: "replan",
|
|
6049
|
+
track_candidates_json: candidatePayload(all),
|
|
6050
|
+
armed: 0
|
|
6051
|
+
});
|
|
6052
|
+
const lines = all.map((t, i) => {
|
|
6053
|
+
const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
|
|
6054
|
+
return ` ${i + 1}. ${t.slug} \u2014 ${title}`;
|
|
6055
|
+
}).join("\n");
|
|
6056
|
+
return {
|
|
6057
|
+
ok: false,
|
|
6058
|
+
needPick: true,
|
|
6059
|
+
candidates: all,
|
|
6060
|
+
userMessage: `Select a plan to replan:
|
|
6061
|
+
|
|
6062
|
+
${lines}
|
|
6063
|
+
|
|
6064
|
+
Reply with a number or /autopilot-replan <slug>.`
|
|
6065
|
+
};
|
|
6066
|
+
} else {
|
|
6067
|
+
return {
|
|
6068
|
+
ok: false,
|
|
6069
|
+
userMessage: "No plan to replan. Use /autopilot-on first."
|
|
6070
|
+
};
|
|
6071
|
+
}
|
|
5958
6072
|
}
|
|
5959
|
-
|
|
5960
|
-
|
|
6073
|
+
const checklistPath = trustedChecklistPath(
|
|
6074
|
+
projectRoot,
|
|
6075
|
+
plansDir,
|
|
6076
|
+
slug,
|
|
6077
|
+
session.checklist_path,
|
|
6078
|
+
session.track_id
|
|
6079
|
+
);
|
|
6080
|
+
const updated = store.upsertSession({
|
|
6081
|
+
conversation_id: conversationId,
|
|
6082
|
+
project_root: projectRoot,
|
|
6083
|
+
code_root: projectRoot,
|
|
6084
|
+
track_id: slug,
|
|
6085
|
+
checklist_path: checklistPath,
|
|
6086
|
+
phase: "planning",
|
|
6087
|
+
armed: 0,
|
|
6088
|
+
paused: 0,
|
|
6089
|
+
paused_reason: null,
|
|
6090
|
+
pending_action: null,
|
|
6091
|
+
track_candidates_json: null
|
|
6092
|
+
});
|
|
6093
|
+
store.updateReviewChain(conversationId, {
|
|
6094
|
+
fix_round: 0,
|
|
6095
|
+
confirm_left: null,
|
|
6096
|
+
chain_pending: 0,
|
|
6097
|
+
code_edited: 0,
|
|
6098
|
+
item_confirm_complete: 0,
|
|
6099
|
+
reviewing_item_id: null,
|
|
6100
|
+
pending_followup: null,
|
|
6101
|
+
pending_followup_at: null,
|
|
6102
|
+
pending_redeliver_at: null
|
|
6103
|
+
});
|
|
6104
|
+
return { ok: true, session: updated };
|
|
6105
|
+
}
|
|
6106
|
+
function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
|
|
6107
|
+
const root = requireProjectRoot(store, projectRoot);
|
|
6108
|
+
if (!root) {
|
|
6109
|
+
return { ok: false, userMessage: "Invalid project root." };
|
|
5961
6110
|
}
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
6111
|
+
projectRoot = root;
|
|
6112
|
+
const session = store.getSession(conversationId);
|
|
6113
|
+
if (!session?.pending_action) {
|
|
6114
|
+
return {
|
|
6115
|
+
ok: false,
|
|
6116
|
+
userMessage: "No pending track selection."
|
|
6117
|
+
};
|
|
5965
6118
|
}
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
6119
|
+
const pending = session.pending_action;
|
|
6120
|
+
if (pending !== "run" && pending !== "replan") {
|
|
6121
|
+
return {
|
|
6122
|
+
ok: false,
|
|
6123
|
+
userMessage: `Unknown pending action "${displayUntrusted(pending)}".`
|
|
6124
|
+
};
|
|
5971
6125
|
}
|
|
5972
|
-
if (
|
|
5973
|
-
return
|
|
6126
|
+
if (!session.track_candidates_json) {
|
|
6127
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5974
6128
|
}
|
|
5975
|
-
|
|
5976
|
-
ignoreCache.set(root, { mtimeMs: st.mtimeMs, patterns });
|
|
5977
|
-
return patterns;
|
|
5978
|
-
}
|
|
5979
|
-
|
|
5980
|
-
// ../core/src/code-edit-detector.ts
|
|
5981
|
-
function isUntrackedGitIgnored(projectRoot, relativePath) {
|
|
5982
|
-
if (!relativePath || relativePath.includes("\0")) return false;
|
|
6129
|
+
let candidates = [];
|
|
5983
6130
|
try {
|
|
5984
|
-
const
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
timeout: 5e3,
|
|
5991
|
-
windowsHide: true,
|
|
5992
|
-
shell: false
|
|
5993
|
-
}
|
|
6131
|
+
const parsed = JSON.parse(session.track_candidates_json);
|
|
6132
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
6133
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
6134
|
+
}
|
|
6135
|
+
const hasValidSlug = parsed.some(
|
|
6136
|
+
(c) => !!c && typeof c === "object" && !Array.isArray(c) && typeof c.slug === "string" && isSafeTrackSlug(c.slug)
|
|
5994
6137
|
);
|
|
5995
|
-
|
|
6138
|
+
if (!hasValidSlug) {
|
|
6139
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
6140
|
+
}
|
|
6141
|
+
candidates = parsed;
|
|
5996
6142
|
} catch {
|
|
5997
|
-
return false;
|
|
6143
|
+
return { ok: false, userMessage: "Invalid track candidates JSON." };
|
|
5998
6144
|
}
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6145
|
+
let slug;
|
|
6146
|
+
if (/^\d+$/.test(pick)) {
|
|
6147
|
+
const idx = Number.parseInt(pick, 10) - 1;
|
|
6148
|
+
const entry = candidates[idx];
|
|
6149
|
+
slug = entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.slug === "string" ? entry.slug : void 0;
|
|
6150
|
+
if (!slug || !isSafeTrackSlug(slug)) {
|
|
6151
|
+
return {
|
|
6152
|
+
ok: false,
|
|
6153
|
+
userMessage: `Invalid selection "${displayUntrusted(pick)}". Choose 1\u2013${candidates.length}.`
|
|
6154
|
+
};
|
|
6155
|
+
}
|
|
6156
|
+
} else {
|
|
6157
|
+
if (!isSafeTrackSlug(pick)) {
|
|
6158
|
+
return {
|
|
6159
|
+
ok: false,
|
|
6160
|
+
userMessage: `Invalid track slug "${displayUntrusted(pick)}".`
|
|
6161
|
+
};
|
|
6162
|
+
}
|
|
6163
|
+
slug = pick;
|
|
6164
|
+
if (!candidates.some(
|
|
6165
|
+
(c) => !!c && typeof c === "object" && !Array.isArray(c) && c.slug === slug
|
|
6166
|
+
)) {
|
|
6167
|
+
return {
|
|
6168
|
+
ok: false,
|
|
6169
|
+
userMessage: `Unknown slug "${displayUntrusted(pick)}".`
|
|
6170
|
+
};
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
if (pending === "replan") {
|
|
6174
|
+
return applyReplan(store, conversationId, projectRoot, {
|
|
6175
|
+
slug,
|
|
6176
|
+
config: opts?.config,
|
|
6177
|
+
platform: opts?.platform
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
return applyRun(store, conversationId, projectRoot, {
|
|
6181
|
+
slug,
|
|
6182
|
+
config: opts?.config,
|
|
6183
|
+
platform: opts?.platform
|
|
6184
|
+
});
|
|
6008
6185
|
}
|
|
6009
6186
|
|
|
6010
6187
|
// ../ports/cursor/src/index.ts
|
|
@@ -6086,13 +6263,18 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
|
|
|
6086
6263
|
} catch {
|
|
6087
6264
|
}
|
|
6088
6265
|
const session = store.getSession(conversationId);
|
|
6266
|
+
const hookCfg = loadProjectHookConfig(projectRoot);
|
|
6089
6267
|
const trigger = parseTrigger({
|
|
6090
6268
|
prompt,
|
|
6091
6269
|
conversationId,
|
|
6092
6270
|
projectRoot,
|
|
6093
|
-
pendingAction: session?.pending_action
|
|
6271
|
+
pendingAction: session?.pending_action,
|
|
6272
|
+
triggers: hookCfg.triggers
|
|
6094
6273
|
});
|
|
6095
|
-
const actionConfig =
|
|
6274
|
+
const actionConfig = {
|
|
6275
|
+
...portConfig?.phaseActions,
|
|
6276
|
+
plansDir: portConfig?.phaseActions?.plansDir ?? hookCfg.plansDir
|
|
6277
|
+
};
|
|
6096
6278
|
if (trigger) {
|
|
6097
6279
|
if (trigger.kind === "off") {
|
|
6098
6280
|
applyOff(store, conversationId);
|
|
@@ -6172,7 +6354,14 @@ function handleAfterFileEdit(store, payload, projectRoot) {
|
|
|
6172
6354
|
const filePath = payload.file_path ?? payload.filePath ?? "";
|
|
6173
6355
|
if (!conversationId || !filePath) return;
|
|
6174
6356
|
try {
|
|
6175
|
-
|
|
6357
|
+
const hookCfg = loadProjectHookConfig(projectRoot);
|
|
6358
|
+
notePlansDirEdit(
|
|
6359
|
+
store,
|
|
6360
|
+
conversationId,
|
|
6361
|
+
projectRoot,
|
|
6362
|
+
filePath,
|
|
6363
|
+
hookCfg.plansDir
|
|
6364
|
+
);
|
|
6176
6365
|
} catch {
|
|
6177
6366
|
}
|
|
6178
6367
|
if (!isProductCodeEdit(filePath, { projectRoot })) return;
|
|
@@ -6369,13 +6558,18 @@ function handleUserPromptSubmit(store, payload, projectRoot, portConfig) {
|
|
|
6369
6558
|
} catch {
|
|
6370
6559
|
}
|
|
6371
6560
|
const session = store.getSession(conversationId);
|
|
6561
|
+
const hookCfg = loadProjectHookConfig(projectRoot);
|
|
6372
6562
|
const trigger = parseTrigger({
|
|
6373
6563
|
prompt,
|
|
6374
6564
|
conversationId,
|
|
6375
6565
|
projectRoot,
|
|
6376
|
-
pendingAction: session?.pending_action
|
|
6566
|
+
pendingAction: session?.pending_action,
|
|
6567
|
+
triggers: hookCfg.triggers
|
|
6377
6568
|
});
|
|
6378
|
-
const actionConfig =
|
|
6569
|
+
const actionConfig = {
|
|
6570
|
+
...portConfig?.phaseActions,
|
|
6571
|
+
plansDir: portConfig?.phaseActions?.plansDir ?? hookCfg.plansDir
|
|
6572
|
+
};
|
|
6379
6573
|
const gateFallback = "Autopilot rejected this prompt. Check `npx autopilot-harness status`.";
|
|
6380
6574
|
if (trigger) {
|
|
6381
6575
|
if (trigger.kind === "off") {
|
|
@@ -6485,7 +6679,14 @@ function handlePostToolUse(store, payload, projectRoot) {
|
|
|
6485
6679
|
const filePath = filePathFromClaudeEdit(payload);
|
|
6486
6680
|
if (!filePath) return;
|
|
6487
6681
|
try {
|
|
6488
|
-
|
|
6682
|
+
const hookCfg = loadProjectHookConfig(projectRoot);
|
|
6683
|
+
notePlansDirEdit(
|
|
6684
|
+
store,
|
|
6685
|
+
conversationId,
|
|
6686
|
+
projectRoot,
|
|
6687
|
+
filePath,
|
|
6688
|
+
hookCfg.plansDir
|
|
6689
|
+
);
|
|
6489
6690
|
} catch {
|
|
6490
6691
|
}
|
|
6491
6692
|
if (!isProductCodeEdit(filePath, { projectRoot })) return;
|