@ordewell/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{ITerminalRunner-nupxhtPb.d.ts → ITerminalRunner-Bd-vAJnw.d.ts} +24 -2
  2. package/dist/{ITerminalRunner-DXlJop_f.d.mts → ITerminalRunner-ByeoLF57.d.mts} +24 -2
  3. package/dist/{ModeResolver-D1-2xJsE.d.mts → ModeResolver-DVJ7HV3k.d.mts} +1 -1
  4. package/dist/{ModeResolver-BBVktyiP.d.ts → ModeResolver-Dkig8ghQ.d.ts} +1 -1
  5. package/dist/{Task-CfTjRQeS.d.mts → Task-BxQkPlXO.d.mts} +88 -10
  6. package/dist/{Task-CfTjRQeS.d.ts → Task-BxQkPlXO.d.ts} +88 -10
  7. package/dist/{chunk-HQICT7HQ.mjs → chunk-T2S5O36I.mjs} +17 -4
  8. package/dist/chunk-T2S5O36I.mjs.map +1 -0
  9. package/dist/index.d.mts +151 -57
  10. package/dist/index.d.ts +151 -57
  11. package/dist/index.js +1179 -500
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1162 -498
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/order-labels.d.mts +1 -1
  16. package/dist/order-labels.d.ts +1 -1
  17. package/dist/{parsing-Bv3g2Tu_.d.ts → parsing-CF_grC29.d.ts} +2 -2
  18. package/dist/{parsing-BJECTMOD.d.mts → parsing-DRp4dPC0.d.mts} +2 -2
  19. package/dist/parsing.d.mts +3 -3
  20. package/dist/parsing.d.ts +3 -3
  21. package/dist/{plan-utils-Cy-RsBSV.d.ts → plan-utils-CkNbqAmS.d.ts} +36 -3
  22. package/dist/{plan-utils-B5oIZkuI.d.mts → plan-utils-CtB3_Ovf.d.mts} +36 -3
  23. package/dist/plan-utils.d.mts +3 -3
  24. package/dist/plan-utils.d.ts +3 -3
  25. package/dist/plan-utils.js +7 -0
  26. package/dist/plan-utils.js.map +1 -1
  27. package/dist/plan-utils.mjs +4 -0
  28. package/dist/testing.d.mts +20 -8
  29. package/dist/testing.d.ts +20 -8
  30. package/dist/testing.js +52 -4
  31. package/dist/testing.js.map +1 -1
  32. package/dist/testing.mjs +47 -4
  33. package/dist/testing.mjs.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-HQICT7HQ.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -70,6 +70,7 @@ import {
70
70
  } from "./chunk-HPNUFBIT.mjs";
71
71
  import {
72
72
  SELF_REPO,
73
+ capConflictFiles,
73
74
  handoffOf,
74
75
  integrationBranchFor,
75
76
  integrationBranchNameOf,
@@ -78,7 +79,7 @@ import {
78
79
  migratePlanStateIsolation,
79
80
  repoRootOf,
80
81
  taskIsolationOf
81
- } from "./chunk-HQICT7HQ.mjs";
82
+ } from "./chunk-T2S5O36I.mjs";
82
83
 
83
84
  // src/interfaces/IFileSystem.ts
84
85
  var GREP_DEFAULT_HEAD_LIMIT = 100;
@@ -2636,33 +2637,33 @@ function grepOptions(args) {
2636
2637
  };
2637
2638
  }
2638
2639
  var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
2639
- async function executeTool(name, args, fs14, fetcher, signal) {
2640
- const outcome = await runTool(name, args, fs14, fetcher, signal);
2640
+ async function executeTool(name, args, fs15, fetcher, signal) {
2641
+ const outcome = await runTool(name, args, fs15, fetcher, signal);
2641
2642
  return { ...outcome, output: redactSecrets(outcome.output) };
2642
2643
  }
2643
- async function runTool(name, args, fs14, fetcher, signal) {
2644
+ async function runTool(name, args, fs15, fetcher, signal) {
2644
2645
  if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
2645
2646
  switch (name) {
2646
2647
  case "read_file": {
2647
2648
  const offset = num(args.offset);
2648
2649
  const limit = num(args.limit);
2649
2650
  const maxBytes = num(args.maxBytes);
2650
- return fs14.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
2651
+ return fs15.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
2651
2652
  }
2652
2653
  case "read_files": {
2653
2654
  const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
2654
- return fs14.readFiles(paths);
2655
+ return fs15.readFiles(paths);
2655
2656
  }
2656
2657
  case "glob":
2657
- return fs14.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
2658
+ return fs15.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
2658
2659
  case "grep":
2659
- return fs14.grep(String(args.pattern ?? ""), grepOptions(args));
2660
+ return fs15.grep(String(args.pattern ?? ""), grepOptions(args));
2660
2661
  case "find_symbol":
2661
- return fs14.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
2662
+ return fs15.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
2662
2663
  case "list_dir":
2663
- return fs14.listDir(String(args.path ?? ""), num(args.depth));
2664
+ return fs15.listDir(String(args.path ?? ""), num(args.depth));
2664
2665
  case "bash":
2665
- return fs14.bash(String(args.command ?? ""), signal);
2666
+ return fs15.bash(String(args.command ?? ""), signal);
2666
2667
  case "fetch": {
2667
2668
  if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
2668
2669
  const url = String(args.url ?? "");
@@ -2770,14 +2771,14 @@ var PendingApprovals = class {
2770
2771
  ask(request) {
2771
2772
  const id = `ap-${Date.now()}-${counter++}`;
2772
2773
  const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
2773
- return new Promise((resolve5) => {
2774
+ return new Promise((resolve7) => {
2774
2775
  const settle = (granted) => {
2775
2776
  const entry = this.entries.get(id);
2776
2777
  if (!entry) return;
2777
2778
  clearTimeout(entry.timer);
2778
2779
  this.entries.delete(id);
2779
2780
  this.opts.onSettled?.(id, granted);
2780
- resolve5(granted);
2781
+ resolve7(granted);
2781
2782
  };
2782
2783
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2783
2784
  timer.unref?.();
@@ -2875,9 +2876,9 @@ function applyHeadLimit(stdout, headLimit) {
2875
2876
  }
2876
2877
  function formatSearchOutput(capped, root, opts) {
2877
2878
  if (capped.rows.length === 0) return opts.emptyMessage;
2878
- const sep = opts.sep ?? path3.sep;
2879
- const prefix = root === sep || root.endsWith(sep) ? root : `${root}${sep}`;
2880
- const body = prefix === sep ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
2879
+ const sep2 = opts.sep ?? path3.sep;
2880
+ const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
2881
+ const body = prefix === sep2 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
2881
2882
  if (!capped.truncated) return body;
2882
2883
  const hint = opts.hint ? ` ${opts.hint}` : "";
2883
2884
  return `${body}
@@ -3468,6 +3469,15 @@ function configuredProviders(config) {
3468
3469
  return out;
3469
3470
  }
3470
3471
 
3472
+ // src/utils/maxParallel.ts
3473
+ var DEFAULT_MAX_PARALLEL = 3;
3474
+ function parseMaxParallel(value) {
3475
+ const text = String(value ?? "").trim();
3476
+ const n = typeof value === "number" ? value : Number(text);
3477
+ if (text === "" || !Number.isInteger(n) || n < 1) return null;
3478
+ return n;
3479
+ }
3480
+
3471
3481
  // src/interfaces/BaseConfig.ts
3472
3482
  function normalizeGeminiModel(id) {
3473
3483
  return id.replace(/^gemini:/, "").replace(/^google\//, "");
@@ -3475,6 +3485,10 @@ function normalizeGeminiModel(id) {
3475
3485
  function commaList(raw) {
3476
3486
  return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
3477
3487
  }
3488
+ function nonNegativeInt(raw, fallback) {
3489
+ const trimmed = (raw ?? "").trim();
3490
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
3491
+ }
3478
3492
  var BaseConfig = class {
3479
3493
  // --- Legacy named getters (backward compat) ---
3480
3494
  get openAiBaseUrl() {
@@ -3668,7 +3682,7 @@ var BaseConfig = class {
3668
3682
  return "";
3669
3683
  }
3670
3684
  get maxParallelSessions() {
3671
- return parseInt(process.env.ORDEWELL_MAX_PARALLEL || "3", 10);
3685
+ return parseMaxParallel(process.env.ORDEWELL_MAX_PARALLEL) ?? DEFAULT_MAX_PARALLEL;
3672
3686
  }
3673
3687
  get researchEnabled() {
3674
3688
  return process.env.ORDEWELL_RESEARCH_ENABLED !== "false";
@@ -3697,6 +3711,9 @@ var BaseConfig = class {
3697
3711
  get worktreeLinks() {
3698
3712
  return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
3699
3713
  }
3714
+ get conflictRepairAttempts() {
3715
+ return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
3716
+ }
3700
3717
  get approvalMode() {
3701
3718
  const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
3702
3719
  return raw === "allow" || raw === "deny" ? raw : "ask";
@@ -3756,13 +3773,17 @@ function landedNotice(landed) {
3756
3773
  if (landed.length === 0) return "Nothing was merged.";
3757
3774
  return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
3758
3775
  }
3759
- function describeMergeResult(result, branch, group) {
3776
+ function repairedNotice(repaired) {
3777
+ const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
3778
+ const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
3779
+ return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
3780
+ }
3781
+ function describeMergeResult(result, branch, group, repaired = []) {
3760
3782
  switch (result.outcome) {
3761
- case "merged":
3762
- return {
3763
- level: "info",
3764
- message: group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`
3765
- };
3783
+ case "merged": {
3784
+ const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
3785
+ return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
3786
+ }
3766
3787
  case "blocked":
3767
3788
  return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
3768
3789
  case "conflict":
@@ -3783,7 +3804,7 @@ import { execFile, exec as exec2 } from "child_process";
3783
3804
  import { promisify as promisify2 } from "util";
3784
3805
  import { randomBytes } from "crypto";
3785
3806
  import * as fs4 from "fs";
3786
- import * as path7 from "path";
3807
+ import * as path8 from "path";
3787
3808
 
3788
3809
  // src/utils/shellPath.ts
3789
3810
  import { exec } from "child_process";
@@ -3812,28 +3833,28 @@ function posixBinDirs() {
3812
3833
  ];
3813
3834
  }
3814
3835
  function windowsBinDirs(env, home) {
3815
- const join21 = (...parts) => path4.win32.join(...parts);
3816
- const under = (base, ...parts) => base ? [join21(base, ...parts)] : [];
3836
+ const join23 = (...parts) => path4.win32.join(...parts);
3837
+ const under = (base, ...parts) => base ? [join23(base, ...parts)] : [];
3817
3838
  const appData = env.APPDATA;
3818
3839
  const localAppData = env.LOCALAPPDATA;
3819
3840
  const programData = env.ProgramData;
3820
3841
  return [
3821
3842
  // Native per-user installers, PowerShell-driven and otherwise.
3822
- join21(home, ".local", "bin"),
3823
- join21(home, ".opencode", "bin"),
3824
- join21(home, ".claude", "bin"),
3825
- join21(home, ".codex", "bin"),
3843
+ join23(home, ".local", "bin"),
3844
+ join23(home, ".opencode", "bin"),
3845
+ join23(home, ".claude", "bin"),
3846
+ join23(home, ".codex", "bin"),
3826
3847
  ...under(localAppData, "Programs", "claude"),
3827
3848
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
3828
3849
  // literal spelling is kept as a fallback for a host that does not export
3829
3850
  // APPDATA (a service account, a stripped CI container).
3830
3851
  ...under(appData, "npm"),
3831
- join21(home, "AppData", "Roaming", "npm"),
3852
+ join23(home, "AppData", "Roaming", "npm"),
3832
3853
  ...under(localAppData, "pnpm"),
3833
3854
  ...under(localAppData, "Yarn", "bin"),
3834
- join21(home, ".bun", "bin"),
3855
+ join23(home, ".bun", "bin"),
3835
3856
  // Windows package managers.
3836
- join21(home, "scoop", "shims"),
3857
+ join23(home, "scoop", "shims"),
3837
3858
  ...under(env.SCOOP, "shims"),
3838
3859
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
3839
3860
  ...under(programData, "chocolatey", "bin"),
@@ -3843,7 +3864,7 @@ function windowsBinDirs(env, home) {
3843
3864
  ...under(localAppData, "Microsoft", "WindowsApps"),
3844
3865
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
3845
3866
  ...under(localAppData, "Volta", "bin"),
3846
- join21(home, ".volta", "bin")
3867
+ join23(home, ".volta", "bin")
3847
3868
  ];
3848
3869
  }
3849
3870
  function wellKnownBinDirs(deps = {}) {
@@ -3947,6 +3968,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
3947
3968
 
3948
3969
  // src/services/worktreeLink.ts
3949
3970
  import * as fs3 from "fs";
3971
+ import * as path7 from "path";
3950
3972
  function linkPath(source, target, platform, hardLink = fs3.linkSync) {
3951
3973
  const isDir = fs3.statSync(source).isDirectory();
3952
3974
  if (platform !== "win32") {
@@ -3965,6 +3987,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
3965
3987
  return "copy";
3966
3988
  }
3967
3989
  }
3990
+ function mirrorDir(source, target, opts) {
3991
+ const copied = [];
3992
+ const links = [];
3993
+ const mirror = (from, to, prefix, top) => {
3994
+ fs3.mkdirSync(to, { recursive: true });
3995
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
3996
+ const name = path7.join(prefix, entry.name);
3997
+ const at = { from: path7.join(from, entry.name), to: path7.join(to, entry.name), name };
3998
+ if (entry.isSymbolicLink()) links.push(at);
3999
+ else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
4000
+ else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
4001
+ }
4002
+ };
4003
+ mirror(source, target, "", true);
4004
+ let pending = links;
4005
+ for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
4006
+ before = pending.length;
4007
+ pending = pending.filter((link) => {
4008
+ const kind = relink(link.from, link.to, opts);
4009
+ if (kind === "copy") copied.push(link.name);
4010
+ return kind === null;
4011
+ });
4012
+ }
4013
+ return copied;
4014
+ }
4015
+ function relink(from, to, opts) {
4016
+ const text = fs3.readlinkSync(from);
4017
+ const pointee = path7.isAbsolute(text) ? rebased(text, opts) : text;
4018
+ if (opts.platform !== "win32") {
4019
+ if (!fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) && fs3.existsSync(path7.resolve(path7.dirname(from), pointee))) {
4020
+ fs3.symlinkSync(rebased(path7.resolve(path7.dirname(from), pointee), opts), to);
4021
+ return "symlink";
4022
+ }
4023
+ fs3.symlinkSync(pointee, to);
4024
+ return "symlink";
4025
+ }
4026
+ const resolved = fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) ? path7.resolve(path7.dirname(to), pointee) : rebased(path7.resolve(path7.dirname(from), pointee), opts);
4027
+ try {
4028
+ fs3.statSync(resolved);
4029
+ } catch {
4030
+ return null;
4031
+ }
4032
+ return linkPath(resolved, to, opts.platform, opts.hardLink);
4033
+ }
4034
+ function rebased(pointee, opts) {
4035
+ const rel = path7.relative(opts.from, pointee);
4036
+ const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4037
+ return inside && !rel.split(/[\\/]/).includes("node_modules") ? path7.join(opts.to, rel) : pointee;
4038
+ }
3968
4039
 
3969
4040
  // src/services/GitWorktreeIsolation.ts
3970
4041
  var execFileAsync = promisify2(execFile);
@@ -3979,15 +4050,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
3979
4050
  var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
3980
4051
  var isEnvFile = (name) => name.startsWith(".env");
3981
4052
  var INTEGRATION_DIR = "integration";
4053
+ var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
4054
+ var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
3982
4055
  var NESTED_REPO_DEPTH = 2;
3983
4056
  var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
3984
4057
  var GITLINK_MODE = "160000";
3985
4058
  function isInside(parent, child) {
3986
- const rel = path7.relative(parent, child);
3987
- return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4059
+ const rel = path8.relative(parent, child);
4060
+ return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
3988
4061
  }
3989
4062
  function hasGitEntry(dir) {
3990
- return fs4.existsSync(path7.join(dir, ".git"));
4063
+ return fs4.existsSync(path8.join(dir, ".git"));
3991
4064
  }
3992
4065
  function firstLine(text) {
3993
4066
  return text.split(/\r?\n/, 1)[0].trim();
@@ -4003,12 +4076,20 @@ function statusPaths(porcelain) {
4003
4076
  }
4004
4077
  return paths;
4005
4078
  }
4079
+ function leftoverMarkerFiles(check) {
4080
+ const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
4081
+ return [...new Set(files)];
4082
+ }
4083
+ function settleStatus(record, status) {
4084
+ record.status = status;
4085
+ delete record.repairBase;
4086
+ }
4006
4087
  function sameDir(a, b) {
4007
4088
  const real = (p) => {
4008
4089
  try {
4009
4090
  return fs4.realpathSync.native(p);
4010
4091
  } catch {
4011
- return path7.resolve(p);
4092
+ return path8.resolve(p);
4012
4093
  }
4013
4094
  };
4014
4095
  return real(a) === real(b);
@@ -4042,8 +4123,8 @@ function inactive(reason, repos) {
4042
4123
  }
4043
4124
  function groupPathOf(listed) {
4044
4125
  const slashed = listed.trim().replace(/\\/g, "/");
4045
- if (path7.posix.isAbsolute(slashed) || path7.win32.isAbsolute(slashed)) return null;
4046
- const rel = path7.posix.normalize(slashed).replace(/\/+$/, "");
4126
+ if (path8.posix.isAbsolute(slashed) || path8.win32.isAbsolute(slashed)) return null;
4127
+ const rel = path8.posix.normalize(slashed).replace(/\/+$/, "");
4047
4128
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
4048
4129
  }
4049
4130
  function matchLinks(root, patterns) {
@@ -4059,9 +4140,26 @@ function matchLinks(root, patterns) {
4059
4140
  }
4060
4141
  function segmentMatches(root, base, segment) {
4061
4142
  const under = (name) => base ? `${base}/${name}` : name;
4062
- if (!/[*?]/.test(segment)) return lexists(path7.join(root, base, segment)) ? [under(segment)] : [];
4143
+ if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
4063
4144
  const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
4064
- return listDir(path7.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4145
+ return listDir(path8.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4146
+ }
4147
+ function installDirs(root) {
4148
+ const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
4149
+ return ["node_modules", ...packages].filter((dir) => lexists(path8.join(root, dir)));
4150
+ }
4151
+ function workspaceGlobs(root) {
4152
+ let manifest;
4153
+ try {
4154
+ manifest = JSON.parse(fs4.readFileSync(path8.join(root, "package.json"), "utf8"));
4155
+ } catch {
4156
+ return [];
4157
+ }
4158
+ if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
4159
+ const { workspaces } = manifest;
4160
+ const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
4161
+ if (!Array.isArray(listed)) return [];
4162
+ return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
4065
4163
  }
4066
4164
  var GitWorktreeIsolation = class {
4067
4165
  constructor(deps) {
@@ -4115,7 +4213,7 @@ var GitWorktreeIsolation = class {
4115
4213
  groupPaths(workspaceRoot) {
4116
4214
  const listed = this.deps.config.workspaceRepos;
4117
4215
  if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
4118
- const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path7.join(workspaceRoot, rel)));
4216
+ const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
4119
4217
  return [...new Set(paths)].sort();
4120
4218
  }
4121
4219
  async committedRepos(workspaceRoot, paths) {
@@ -4131,7 +4229,7 @@ var GitWorktreeIsolation = class {
4131
4229
  return status.ok ? status.stdout.trim() !== "" : null;
4132
4230
  }
4133
4231
  reposDirectlyIn(dir) {
4134
- return this.childDirs(dir).filter((name) => hasGitEntry(path7.join(dir, name)));
4232
+ return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
4135
4233
  }
4136
4234
  childDirs(dir) {
4137
4235
  try {
@@ -4148,9 +4246,9 @@ var GitWorktreeIsolation = class {
4148
4246
  async nestedRepos(workspaceRoot, toplevel) {
4149
4247
  const found = [];
4150
4248
  const walk = (rel, depth) => {
4151
- for (const name of this.childDirs(path7.join(workspaceRoot, rel))) {
4249
+ for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
4152
4250
  const child = rel ? `${rel}/${name}` : name;
4153
- if (hasGitEntry(path7.join(workspaceRoot, child))) found.push(child);
4251
+ if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
4154
4252
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
4155
4253
  }
4156
4254
  };
@@ -4171,7 +4269,7 @@ var GitWorktreeIsolation = class {
4171
4269
  const [meta, file] = entry.split(" ");
4172
4270
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
4173
4271
  }
4174
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path7.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4272
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4175
4273
  for (const entry of declared.stdout.split("\0")) {
4176
4274
  const value = entry.split("\n")[1];
4177
4275
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -4227,7 +4325,7 @@ var GitWorktreeIsolation = class {
4227
4325
  async acceptsWorktree(run, repo) {
4228
4326
  const dir = this.integrationDir(run, repo);
4229
4327
  ensureStateDirIgnored(run.workspaceRoot);
4230
- fs4.mkdirSync(path7.dirname(dir), { recursive: true });
4328
+ fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4231
4329
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
4232
4330
  await this.removeWorktreeDir(run, repo, dir, []);
4233
4331
  return added.ok;
@@ -4242,12 +4340,12 @@ var GitWorktreeIsolation = class {
4242
4340
  if (isolated.includes(SELF_REPO)) return [];
4243
4341
  const shared = [];
4244
4342
  const walk = (rel) => {
4245
- for (const name of listDir(path7.join(workspaceRoot, rel))) {
4343
+ for (const name of listDir(path8.join(workspaceRoot, rel))) {
4246
4344
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
4247
4345
  const child = rel ? `${rel}/${name}` : name;
4248
4346
  if (isolated.includes(child)) continue;
4249
4347
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
4250
- else if (resolves(path7.join(workspaceRoot, child))) shared.push(child);
4348
+ else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
4251
4349
  }
4252
4350
  };
4253
4351
  walk("");
@@ -4264,17 +4362,17 @@ var GitWorktreeIsolation = class {
4264
4362
  const copied = [];
4265
4363
  try {
4266
4364
  for (const repo of run.repos) {
4267
- const worktree = path7.join(dir, repo.path);
4365
+ const worktree = path8.join(dir, repo.path);
4268
4366
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
4269
- fs4.mkdirSync(path7.dirname(worktree), { recursive: true });
4367
+ fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
4270
4368
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
4271
4369
  record.repos[repo.path] = { worktree, linked: [] };
4272
4370
  }
4273
4371
  run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
4274
4372
  for (const rel of run.shared) {
4275
- const target = path7.join(dir, rel);
4276
- fs4.mkdirSync(path7.dirname(target), { recursive: true });
4277
- if (linkPath(path7.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
4373
+ const target = path8.join(dir, rel);
4374
+ fs4.mkdirSync(path8.dirname(target), { recursive: true });
4375
+ if (linkPath(path8.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
4278
4376
  }
4279
4377
  for (const repo of run.repos) {
4280
4378
  const entry = record.repos[repo.path];
@@ -4282,7 +4380,7 @@ var GitWorktreeIsolation = class {
4282
4380
  if (repo.path === SELF_REPO) cwd = inRepo;
4283
4381
  const boot = await this.bootstrap(repo, inRepo);
4284
4382
  entry.linked = boot.linked;
4285
- copied.push(...boot.copied.map((name) => path7.posix.join(repo.path, name)));
4383
+ copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
4286
4384
  }
4287
4385
  } catch (err) {
4288
4386
  record.status = "failed";
@@ -4293,6 +4391,53 @@ var GitWorktreeIsolation = class {
4293
4391
  return { cwd, branch, copied };
4294
4392
  });
4295
4393
  }
4394
+ reopen(task, run) {
4395
+ return this.admin(run.workspaceRoot, async () => {
4396
+ const record = run.tasks[task.id];
4397
+ if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
4398
+ let cwd = record.workspace;
4399
+ const repairBase = {};
4400
+ for (const repo of run.repos) {
4401
+ const entry = record.repos[repo.path];
4402
+ if (!entry) continue;
4403
+ if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
4404
+ if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
4405
+ if (entry.changed || repo.path === record.conflictRepo) {
4406
+ repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
4407
+ }
4408
+ }
4409
+ const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
4410
+ record.repairs = (record.repairs ?? 0) + 1;
4411
+ record.repairBase = repairBase;
4412
+ record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
4413
+ record.status = "repairing";
4414
+ return { cwd, branch: record.branch, copied: [] };
4415
+ });
4416
+ }
4417
+ async verifyRepair(task, run) {
4418
+ const record = run.tasks[task.id];
4419
+ if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
4420
+ for (const repo of run.repos) {
4421
+ const entry = record.repos[repo.path];
4422
+ if (!entry) continue;
4423
+ try {
4424
+ await this.commitWorktree(repo, record, entry);
4425
+ } catch {
4426
+ return { ok: false, reason: "failed", repo: repo.path };
4427
+ }
4428
+ }
4429
+ for (const repo of run.repos) {
4430
+ const base = record.repairBase[repo.path];
4431
+ if (base === void 0) continue;
4432
+ const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
4433
+ if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
4434
+ const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
4435
+ if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
4436
+ const files = leftoverMarkerFiles(check.stdout);
4437
+ if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
4438
+ }
4439
+ return { ok: true };
4440
+ }
4296
4441
  /**
4297
4442
  * The place in a repo's worktree that matches where the workspace sits in
4298
4443
  * the real repo: the worktree itself, or a subdirectory of it when the
@@ -4300,7 +4445,7 @@ var GitWorktreeIsolation = class {
4300
4445
  */
4301
4446
  async inWorkspacePlace(repo, worktree) {
4302
4447
  const prefix = await this.prefixOf(repo);
4303
- return prefix ? path7.join(worktree, prefix) : worktree;
4448
+ return prefix ? path8.join(worktree, prefix) : worktree;
4304
4449
  }
4305
4450
  async prefixOf(repo) {
4306
4451
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -4320,6 +4465,7 @@ var GitWorktreeIsolation = class {
4320
4465
  if (!record) return;
4321
4466
  if (opts.keep) {
4322
4467
  if (record.status === "active") record.status = "kept";
4468
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4323
4469
  return;
4324
4470
  }
4325
4471
  await this.removeTask(run, record, { dropRecord: true });
@@ -4339,6 +4485,7 @@ var GitWorktreeIsolation = class {
4339
4485
  for (const record of Object.values(run.tasks)) {
4340
4486
  if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
4341
4487
  else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
4488
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4342
4489
  }
4343
4490
  await this.removeUnowned(run);
4344
4491
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
@@ -4433,14 +4580,59 @@ var GitWorktreeIsolation = class {
4433
4580
  await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
4434
4581
  }
4435
4582
  await this.removeUnowned(run);
4436
- if (!opts.keepIntegration) {
4437
- for (const repo of run.repos) await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
4583
+ if (opts.integration !== "keep") {
4584
+ for (const repo of run.repos) {
4585
+ if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
4586
+ else await this.deleteIfMerged(repo.root, repo.integrationBranch);
4587
+ }
4438
4588
  run.tasks = {};
4439
4589
  this.removeIfEmpty(this.runRoot(run));
4440
4590
  }
4441
4591
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
4442
4592
  });
4443
4593
  }
4594
+ sweep(run) {
4595
+ return this.admin(run.workspaceRoot, async () => {
4596
+ const failed = [];
4597
+ for (const repo of run.repos) {
4598
+ if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
4599
+ }
4600
+ if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
4601
+ });
4602
+ }
4603
+ /** False when git failed anywhere; the other branches are still tried. */
4604
+ async sweepRepo(run, repo) {
4605
+ await this.tryGit(repo.root, ["worktree", "prune"]);
4606
+ const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
4607
+ const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
4608
+ if (!worktrees.ok || !refs.ok) return false;
4609
+ const checkedOut = /* @__PURE__ */ new Set();
4610
+ const liveRuns = /* @__PURE__ */ new Set();
4611
+ for (const line of worktrees.stdout.split(/\r?\n/)) {
4612
+ if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
4613
+ const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
4614
+ if (runId) liveRuns.add(runId);
4615
+ }
4616
+ let ok = true;
4617
+ for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
4618
+ const branch = ref.slice("refs/heads/".length);
4619
+ const runId = RUN_BRANCH.exec(branch)?.[1];
4620
+ if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
4621
+ if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
4622
+ }
4623
+ return ok;
4624
+ }
4625
+ /**
4626
+ * Delete `branch` only if the repo's checked-out HEAD already contains it —
4627
+ * HEAD, not an upstream: what the user has checked out is what they merged
4628
+ * into. `-d` rather than `-D`, so git also refuses a branch a worktree has
4629
+ * checked out.
4630
+ */
4631
+ async deleteIfMerged(root, branch) {
4632
+ const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
4633
+ if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
4634
+ return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
4635
+ }
4444
4636
  async drain() {
4445
4637
  try {
4446
4638
  while (this.waiting.length > 0) {
@@ -4493,21 +4685,24 @@ var GitWorktreeIsolation = class {
4493
4685
  return this.stopLanding(record, "failed");
4494
4686
  }
4495
4687
  for (const repo of changed) {
4496
- const outcome = await this.mergeTask(run, repo, record);
4688
+ const { outcome, files } = await this.mergeTask(run, repo, record);
4497
4689
  if (outcome === "merged") continue;
4498
- return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
4690
+ return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
4499
4691
  }
4500
4692
  }
4501
4693
  delete run.landing;
4502
- record.status = "merged";
4694
+ settleStatus(record, "merged");
4503
4695
  delete record.conflictRepo;
4696
+ delete record.conflictFiles;
4504
4697
  await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
4505
4698
  return "merged";
4506
4699
  }
4507
- stopLanding(record, outcome, repo) {
4508
- record.status = outcome;
4700
+ stopLanding(record, outcome, repo, files) {
4701
+ settleStatus(record, outcome);
4509
4702
  if (repo) record.conflictRepo = repo.path;
4510
4703
  else delete record.conflictRepo;
4704
+ if (files && files.length > 0) record.conflictFiles = files;
4705
+ else delete record.conflictFiles;
4511
4706
  return outcome;
4512
4707
  }
4513
4708
  /** Merge the task branch into one repo's integration branch. A merge that does not complete is aborted: it is Ordewell's own. */
@@ -4516,13 +4711,13 @@ var GitWorktreeIsolation = class {
4516
4711
  try {
4517
4712
  dir = await this.ensureIntegrationWorktree(run, repo);
4518
4713
  } catch {
4519
- return "failed";
4714
+ return { outcome: "failed", files: [] };
4520
4715
  }
4521
4716
  const merge = await this.tryGit(dir, ["merge", "--no-ff", "--no-edit", "-m", `Merge task ${record.order}: ${firstLine(record.title)}`, record.branch]);
4522
- if (merge.ok) return "merged";
4523
- const conflicted = (await this.unmergedPaths(dir)).length > 0;
4717
+ if (merge.ok) return { outcome: "merged", files: [] };
4718
+ const files = await this.unmergedPaths(dir);
4524
4719
  if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
4525
- return conflicted ? "conflict" : "failed";
4720
+ return { outcome: files.length > 0 ? "conflict" : "failed", files };
4526
4721
  }
4527
4722
  /**
4528
4723
  * Return every repo of the landing in flight to its recorded tip, then
@@ -4590,15 +4785,15 @@ var GitWorktreeIsolation = class {
4590
4785
  await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
4591
4786
  }
4592
4787
  integrationDir(run, repo) {
4593
- return path7.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
4788
+ return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
4594
4789
  }
4595
4790
  async ensureIntegrationWorktree(run, repo) {
4596
4791
  return this.admin(run.workspaceRoot, async () => {
4597
4792
  const dir = this.integrationDir(run, repo);
4598
- if (fs4.existsSync(path7.join(dir, ".git"))) return dir;
4793
+ if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
4599
4794
  await this.tryGit(repo.root, ["worktree", "prune"]);
4600
4795
  ensureStateDirIgnored(run.workspaceRoot);
4601
- fs4.mkdirSync(path7.dirname(dir), { recursive: true });
4796
+ fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4602
4797
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
4603
4798
  return dir;
4604
4799
  });
@@ -4633,20 +4828,20 @@ var GitWorktreeIsolation = class {
4633
4828
  return;
4634
4829
  }
4635
4830
  for (const entry of entries) {
4636
- const target = path7.join(dir, entry.name);
4831
+ const target = path8.join(dir, entry.name);
4637
4832
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
4638
4833
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
4639
4834
  }
4640
4835
  }
4641
4836
  /** Task workspaces and branches under this run that no record accounts for. */
4642
4837
  async removeUnowned(run) {
4643
- const owned = new Set(Object.values(run.tasks).map((r) => path7.resolve(r.workspace)));
4838
+ const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
4644
4839
  const root = this.runRoot(run);
4645
4840
  if (fs4.existsSync(root)) {
4646
4841
  for (const entry of fs4.readdirSync(root)) {
4647
- const dir = path7.join(root, entry);
4648
- if (entry === INTEGRATION_DIR || owned.has(path7.resolve(dir))) continue;
4649
- for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path7.join(dir, repo.path), []);
4842
+ const dir = path8.join(root, entry);
4843
+ if (entry === INTEGRATION_DIR || owned.has(path8.resolve(dir))) continue;
4844
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path8.join(dir, repo.path), []);
4650
4845
  this.removeTaskWorkspace(run, dir);
4651
4846
  }
4652
4847
  }
@@ -4660,14 +4855,18 @@ var GitWorktreeIsolation = class {
4660
4855
  }
4661
4856
  }
4662
4857
  async removeWorktreeDir(run, repo, dir, linked) {
4663
- for (const name of linked) this.unlinkIfLink(path7.join(dir, name));
4858
+ for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
4859
+ const target = path8.join(dir, name);
4860
+ this.unlinkIfLink(target);
4861
+ this.unlinkLinksUnder(target);
4862
+ }
4664
4863
  for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
4665
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path7.join(dir, name));
4864
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
4666
4865
  }
4667
4866
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
4668
4867
  if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
4669
4868
  await this.tryGit(repo.root, ["worktree", "prune"]);
4670
- for (let parent = path7.dirname(dir); isInside(this.runRoot(run), parent); parent = path7.dirname(parent)) this.removeIfEmpty(parent);
4869
+ for (let parent = path8.dirname(dir); isInside(this.runRoot(run), parent); parent = path8.dirname(parent)) this.removeIfEmpty(parent);
4671
4870
  }
4672
4871
  unlinkIfLink(target) {
4673
4872
  try {
@@ -4682,14 +4881,14 @@ var GitWorktreeIsolation = class {
4682
4881
  }
4683
4882
  }
4684
4883
  runRoot(run) {
4685
- return path7.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
4884
+ return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
4686
4885
  }
4687
4886
  namesFor(run, task) {
4688
4887
  const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
4689
4888
  let name = `${task.order}-${slug2}`;
4690
4889
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
4691
4890
  if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
4692
- return { branch: `ordewell/${run.id}/${name}`, dir: path7.join(this.runRoot(run), name) };
4891
+ return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
4693
4892
  }
4694
4893
  /**
4695
4894
  * Make one repo's worktree runnable: the default artifacts, unless a setup
@@ -4702,16 +4901,26 @@ var GitWorktreeIsolation = class {
4702
4901
  const linked = [];
4703
4902
  const copied = [];
4704
4903
  const link = (name) => {
4705
- const target = path7.join(cwd, name);
4904
+ const target = path8.join(cwd, name);
4706
4905
  if (lexists(target)) return;
4707
- fs4.mkdirSync(path7.dirname(target), { recursive: true });
4708
- if (linkPath(path7.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
4906
+ fs4.mkdirSync(path8.dirname(target), { recursive: true });
4907
+ if (linkPath(path8.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
4908
+ linked.push(name);
4909
+ };
4910
+ const mirror = (name) => {
4911
+ const target = path8.join(cwd, name);
4912
+ if (lexists(target) || !fs4.existsSync(path8.dirname(target))) return;
4913
+ const source = path8.join(repo.root, name);
4914
+ if (!fs4.lstatSync(source).isDirectory()) return link(name);
4915
+ const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
4916
+ copied.push(...mirrored.map((entry) => path8.join(name, entry)));
4709
4917
  linked.push(name);
4710
4918
  };
4711
4919
  if (!setup) {
4712
4920
  for (const name of fs4.readdirSync(repo.root)) {
4713
- if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
4921
+ if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
4714
4922
  }
4923
+ for (const dir of installDirs(repo.root)) mirror(dir);
4715
4924
  }
4716
4925
  for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
4717
4926
  if (setup) await this.runSetup(setup, repo, cwd);
@@ -5159,6 +5368,7 @@ function researchPhaseBlock(harnessMode) {
5159
5368
  }
5160
5369
  var ISOLATED_PARALLELISM_RULE = "- Each AI task runs in its own git worktree, and its work is merged in afterwards, so tasks that edit the same files can still run in parallel. Never add a dependency just because two tasks touch the same file \u2014 keep dependencies for genuine logical ordering.";
5161
5370
  var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
5371
+ var SHARED_APPEND_FILE_RULE = "- Tasks that run in parallel must not each append to the same shared file (changelogs, registries, index lists) \u2014 give that edit to the final task, or to one task the others depend on.";
5162
5372
  function repoGroupSection(isolated) {
5163
5373
  if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
5164
5374
  return [
@@ -5250,6 +5460,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
5250
5460
  "DEPENDENCY & PARALLELISM:",
5251
5461
  "- Independent slices should have NO dependencies \u2014 they run in parallel.",
5252
5462
  "- Only add dependencies when a slice truly depends on artifacts another slice creates.",
5463
+ SHARED_APPEND_FILE_RULE,
5253
5464
  ...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
5254
5465
  ...repoGroupSection(variant.isolatedExecution),
5255
5466
  "",
@@ -5351,6 +5562,7 @@ function corePlannerPrompt(isolatedExecution) {
5351
5562
  isolatedExecution ? "- Independent vertical slices should have NO dependencies between them \u2014 they can run in parallel." : "- Independent vertical slices (no shared files/modules) should have NO dependencies between them \u2014 they can run in parallel.",
5352
5563
  "- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
5353
5564
  "- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
5565
+ SHARED_APPEND_FILE_RULE,
5354
5566
  isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
5355
5567
  ...repoGroupSection(isolatedExecution),
5356
5568
  "",
@@ -5628,6 +5840,31 @@ ${task.prompt ?? task.description}`;
5628
5840
  asked
5629
5841
  ].join("\n");
5630
5842
  }
5843
+ function buildConflictRepairPrompt(task, conflict, integrationBranch) {
5844
+ const { branch } = conflict;
5845
+ const files = conflict.conflictFiles ?? [];
5846
+ const asked = `What the task was asked to do:
5847
+ ${task.prompt ?? task.description}`;
5848
+ const resolve7 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
5849
+ const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
5850
+ if (repos.length === 0) {
5851
+ return [
5852
+ `Task #${task.order} "${task.title}" passed, but merging its branch \`${branch}\` into \`${integrationBranch}\` conflicted${files.length > 0 ? ` in ${files.join(", ")}` : ""}. None of its work has landed yet.`,
5853
+ `This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve7} Never drop either side wholesale, and never abort the merge or reset it away.`,
5854
+ "Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
5855
+ "",
5856
+ asked
5857
+ ].join("\n");
5858
+ }
5859
+ const stoppedIn = conflict.conflictRepo ?? repos[0];
5860
+ return [
5861
+ `Task #${task.order} "${task.title}" passed, but landing its branch \`${branch}\` on \`${integrationBranch}\` conflicted in ${stoppedIn}${files.length > 0 ? ` (${files.join(", ")})` : ""}. A task lands in every repository it changed or in none, so none of its work has landed yet.`,
5862
+ `This is the task's own workspace, with each repository at its usual path on \`${branch}\` and the task's work committed. In each repository the task changed \u2014 ${repos.join(", ")} \u2014 run \`git merge --no-edit ${integrationBranch}\` inside that repository's directory, and ${resolve7} Never drop either side wholesale, and never abort a merge or reset it away.`,
5863
+ "Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
5864
+ "",
5865
+ asked
5866
+ ].join("\n");
5867
+ }
5631
5868
 
5632
5869
  // src/services/PlanRepair.ts
5633
5870
  async function repairLoop(opts) {
@@ -5958,8 +6195,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
5958
6195
 
5959
6196
  // src/services/ContextCollector.ts
5960
6197
  var ORDEWELL_CONTEXT_MAX = 8e3;
5961
- async function collectResearchContext(fs14, runners) {
5962
- const collector = new ContextCollector(fs14);
6198
+ async function collectResearchContext(fs15, runners) {
6199
+ const collector = new ContextCollector(fs15);
5963
6200
  let contextStr = "";
5964
6201
  let firstContext = null;
5965
6202
  for (const r of runners) {
@@ -5986,8 +6223,8 @@ ${firstContext.dirStructure}
5986
6223
  return contextStr;
5987
6224
  }
5988
6225
  var ContextCollector = class {
5989
- constructor(fs14, registry) {
5990
- this.fs = fs14;
6226
+ constructor(fs15, registry) {
6227
+ this.fs = fs15;
5991
6228
  this.registry = registry;
5992
6229
  }
5993
6230
  fs;
@@ -6035,22 +6272,22 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
6035
6272
  function refused(output) {
6036
6273
  return { success: false, output, truncated: false };
6037
6274
  }
6038
- function nonPromptingFs(fs14) {
6039
- const withinWorkspace = (p) => resolveWithin(fs14.getWorkspaceRoot(), p ?? ".").inside;
6275
+ function nonPromptingFs(fs15) {
6276
+ const withinWorkspace = (p) => resolveWithin(fs15.getWorkspaceRoot(), p ?? ".").inside;
6040
6277
  const outsideRefusal = (p) => refused(
6041
6278
  `Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
6042
6279
  );
6043
6280
  return {
6044
- getWorkspaceRoot: () => fs14.getWorkspaceRoot(),
6045
- readFile: (p, opts) => withinWorkspace(p) ? fs14.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
6281
+ getWorkspaceRoot: () => fs15.getWorkspaceRoot(),
6282
+ readFile: (p, opts) => withinWorkspace(p) ? fs15.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
6046
6283
  readFiles: (paths) => {
6047
6284
  const escaping = paths.find((p) => !withinWorkspace(p));
6048
- return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs14.readFiles(paths);
6285
+ return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs15.readFiles(paths);
6049
6286
  },
6050
- glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs14.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
6051
- grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs14.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
6052
- findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs14.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
6053
- listDir: (p, depth) => withinWorkspace(p) ? fs14.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
6287
+ glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
6288
+ grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
6289
+ findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs15.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
6290
+ listDir: (p, depth) => withinWorkspace(p) ? fs15.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
6054
6291
  bash: async (command, signal) => {
6055
6292
  const dialect = resolveResearchShell().dialect;
6056
6293
  const { tier, reason } = classifyCommand(command, { dialect });
@@ -6061,13 +6298,13 @@ function nonPromptingFs(fs14) {
6061
6298
  }
6062
6299
  const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
6063
6300
  if (escaping) return outsideRefusal(escaping);
6064
- return fs14.bash(command, signal);
6301
+ return fs15.bash(command, signal);
6065
6302
  }
6066
6303
  };
6067
6304
  }
6068
6305
  async function runLoop(prompt, deps) {
6069
6306
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
6070
- const fs14 = nonPromptingFs(deps.fs);
6307
+ const fs15 = nonPromptingFs(deps.fs);
6071
6308
  const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
6072
6309
  let turn = await chat.sendMessage(prompt, deps.signal);
6073
6310
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -6084,7 +6321,7 @@ ${turn.text}`;
6084
6321
  if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
6085
6322
  const toolArgs = JSON.stringify(tc.args);
6086
6323
  deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
6087
- const res = await executeTool(tc.name, args, fs14, void 0, deps.signal);
6324
+ const res = await executeTool(tc.name, args, fs15, void 0, deps.signal);
6088
6325
  const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
6089
6326
  [... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
6090
6327
  const stepEntry = {
@@ -6200,7 +6437,7 @@ var BaseAiService = class _BaseAiService {
6200
6437
  * steering/failed result and the loop continues sequentially: a subagent can
6201
6438
  * never fail a plan or a turn.
6202
6439
  */
6203
- async executeSpawnAgent(args, fs14, onProgress, signal, subagentId) {
6440
+ async executeSpawnAgent(args, fs15, onProgress, signal, subagentId) {
6204
6441
  const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
6205
6442
  if (!prompt) {
6206
6443
  return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
@@ -6211,20 +6448,20 @@ var BaseAiService = class _BaseAiService {
6211
6448
  if (!chat) throw new Error("subagent chats are not available for this provider");
6212
6449
  return chat;
6213
6450
  },
6214
- fs: fs14,
6451
+ fs: fs15,
6215
6452
  signal,
6216
6453
  onProgress: (progress) => onProgress({ ...progress, subagentId })
6217
6454
  });
6218
6455
  }
6219
6456
  /** Collect project context for the planning phase. Shared with the harness backend. */
6220
- static async collectResearchContext(fs14, runners) {
6221
- return collectResearchContext(fs14, runners);
6457
+ static async collectResearchContext(fs15, runners) {
6458
+ return collectResearchContext(fs15, runners);
6222
6459
  }
6223
6460
  /**
6224
6461
  * Execute one round of tool calls and report each through onProgress.
6225
6462
  * Returns the results to feed back plus the log entries produced.
6226
6463
  */
6227
- async executeToolCalls(toolCalls, fs14, onProgress, fetcher, stepIndex, thinking, signal) {
6464
+ async executeToolCalls(toolCalls, fs15, onProgress, fetcher, stepIndex, thinking, signal) {
6228
6465
  const toolResults = [];
6229
6466
  const logEntries = [];
6230
6467
  let resultsText = "";
@@ -6243,18 +6480,18 @@ var BaseAiService = class _BaseAiService {
6243
6480
  const subagentId = uuidv4();
6244
6481
  spawnSubagentIds.set(i, subagentId);
6245
6482
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
6246
- precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs14, onProgress, signal, subagentId));
6483
+ precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs15, onProgress, signal, subagentId));
6247
6484
  }) : Promise.resolve(),
6248
6485
  parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
6249
6486
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
6250
- precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs14, fetcher, signal));
6487
+ precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs15, fetcher, signal));
6251
6488
  }) : Promise.resolve()
6252
6489
  ]);
6253
6490
  for (const [index, tc] of toolCalls.entries()) {
6254
6491
  const toolArgs = normalizedArgs[index];
6255
6492
  const preResult = precomputed.get(index);
6256
6493
  if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
6257
- const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs14, fetcher, signal);
6494
+ const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs15, fetcher, signal);
6258
6495
  const MAX_RESPONSE_CHARS = 5e4;
6259
6496
  const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
6260
6497
  const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
@@ -6440,7 +6677,7 @@ ${llmOutput}
6440
6677
  * parsed tasks if a plan was emitted mid-loop, or null if the loop
6441
6678
  * exhausted without a valid plan.
6442
6679
  */
6443
- async runResearchLoop(chat, firstMessage, fs14, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
6680
+ async runResearchLoop(chat, firstMessage, fs15, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
6444
6681
  const researchLog = initialLog ? [...initialLog] : [];
6445
6682
  if (!initialLog && userGoal) {
6446
6683
  researchLog.push({
@@ -6467,7 +6704,7 @@ ${llmOutput}
6467
6704
  if (!turn.hasToolCalls) break;
6468
6705
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
6469
6706
  turn.toolCalls,
6470
- fs14,
6707
+ fs15,
6471
6708
  onProgress,
6472
6709
  fetcher,
6473
6710
  step,
@@ -6702,11 +6939,11 @@ ${repairHint}` }] : parts;
6702
6939
  return this.consumePlanStream(result, onToken);
6703
6940
  }
6704
6941
  // --- IAiService implementation ---
6705
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
6942
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
6706
6943
  this.ensureInit();
6707
6944
  const { autonomousDefault } = modes;
6708
6945
  const genModel = this.getPlanningModel();
6709
- const contextStr = await BaseAiService.collectResearchContext(fs14, runners);
6946
+ const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
6710
6947
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
6711
6948
  const firstMessage = `${buildResearchToolsPrompt()}
6712
6949
 
@@ -6717,17 +6954,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
6717
6954
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
6718
6955
  });
6719
6956
  const researchChat = new GeminiResearchChat(chat);
6720
- const result = await this.runResearchLoop(researchChat, firstMessage, fs14, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
6957
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
6721
6958
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
6722
6959
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
6723
6960
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
6724
6961
  }
6725
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
6962
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
6726
6963
  this.ensureInit();
6727
6964
  const { autonomousDefault } = modes;
6728
6965
  let ctx = "";
6729
- if (fs14) {
6730
- const collected = await new ContextCollector(fs14).collect(runners[0] ?? "claude-code");
6966
+ if (fs15) {
6967
+ const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
6731
6968
  if (collected.aiflowContext) ctx = `
6732
6969
  <aiflow_context>
6733
6970
  ${collected.aiflowContext}
@@ -6743,12 +6980,12 @@ ${collected.aiflowContext}
6743
6980
  autonomousDefault
6744
6981
  );
6745
6982
  }
6746
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
6983
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
6747
6984
  this.ensureInit();
6748
6985
  const { autonomousDefault } = modes;
6749
6986
  let aiflowContext;
6750
- if (fs14) {
6751
- const collected = await new ContextCollector(fs14).collect(existingPlan.runners[0] ?? "claude-code");
6987
+ if (fs15) {
6988
+ const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
6752
6989
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
6753
6990
  }
6754
6991
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -6984,11 +7221,11 @@ ${buildResearchToolsPrompt()}` }
6984
7221
  }
6985
7222
  }
6986
7223
  // --- IAiService implementation ---
6987
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
7224
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
6988
7225
  this.ensureInit();
6989
7226
  const client = this.getClient();
6990
7227
  const { autonomousDefault } = modes;
6991
- const contextStr = await BaseAiService.collectResearchContext(fs14, runners);
7228
+ const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
6992
7229
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
6993
7230
  const userText = `User goal: ${userDescription}`;
6994
7231
  const firstMessage = `${buildResearchToolsPrompt()}
@@ -7006,17 +7243,17 @@ ${userText}`;
7006
7243
  toOpenAiTools(),
7007
7244
  (delta) => onProgress({ type: "thinking", text: delta })
7008
7245
  );
7009
- const result = await this.runResearchLoop(researchChat, firstMessage, fs14, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
7246
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
7010
7247
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
7011
7248
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
7012
7249
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
7013
7250
  }
7014
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
7251
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
7015
7252
  this.ensureInit();
7016
7253
  const { autonomousDefault } = modes;
7017
7254
  let ctx = "";
7018
- if (fs14) {
7019
- const collected = await new ContextCollector(fs14).collect(runners[0] ?? "claude-code");
7255
+ if (fs15) {
7256
+ const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
7020
7257
  if (collected.aiflowContext) ctx = `
7021
7258
  <aiflow_context>
7022
7259
  ${collected.aiflowContext}
@@ -7032,12 +7269,12 @@ ${collected.aiflowContext}
7032
7269
  autonomousDefault
7033
7270
  );
7034
7271
  }
7035
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
7272
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
7036
7273
  this.ensureInit();
7037
7274
  const { autonomousDefault } = modes;
7038
7275
  let aiflowContext;
7039
- if (fs14) {
7040
- const collected = await new ContextCollector(fs14).collect(existingPlan.runners[0] ?? "claude-code");
7276
+ if (fs15) {
7277
+ const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
7041
7278
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
7042
7279
  }
7043
7280
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -7056,6 +7293,9 @@ ${collected.aiflowContext}
7056
7293
  }
7057
7294
  };
7058
7295
 
7296
+ // src/services/TaskOrchestrator.ts
7297
+ import * as path11 from "path";
7298
+
7059
7299
  // src/services/promptAugment.ts
7060
7300
  var MAX_TAIL_CHARS = 500;
7061
7301
  var DEFAULT_PLAN_MAP_MAX_ENTRIES = 30;
@@ -7641,18 +7881,230 @@ ${line}
7641
7881
  };
7642
7882
 
7643
7883
  // src/services/transcriptCapture.ts
7644
- import { readFileSync, readdirSync as readdirSync2, statSync as statSync4, existsSync as existsSync3 } from "fs";
7884
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync4 } from "fs";
7885
+ import * as os4 from "os";
7886
+ import * as path10 from "path";
7887
+
7888
+ // src/services/workspaceEnv.ts
7889
+ import { execFile as execFile2 } from "child_process";
7890
+ import * as fs5 from "fs";
7645
7891
  import * as os3 from "os";
7646
- import * as path8 from "path";
7892
+ import * as path9 from "path";
7893
+
7894
+ // src/services/settingsEnvAllowlist.ts
7895
+ var ORDEWELL_SETTABLE_ENV = [
7896
+ "AI_PROVIDER",
7897
+ "ORCHESTRATOR_MODEL",
7898
+ "ORDEWELL_SUBAGENT_MODEL",
7899
+ "ORDEWELL_PLANNER_EFFORT",
7900
+ "ORDEWELL_RESEARCH_ENABLED",
7901
+ "ORDEWELL_MAX_PARALLEL",
7902
+ "ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
7903
+ ];
7904
+ var SETTINGS_ENV_REFUSED = [
7905
+ // Approval posture and pre-approved scope.
7906
+ "ORDEWELL_AUTONOMOUS_MODE",
7907
+ "ORDEWELL_APPROVAL_MODE",
7908
+ "ORDEWELL_APPROVAL_ALLOW",
7909
+ // Where settings are persisted.
7910
+ "ORDEWELL_SETTINGS_PATH",
7911
+ // Runtime options and dynamic loaders.
7912
+ "NODE_OPTIONS",
7913
+ "NODE_REPL_EXTERNAL_MODULE",
7914
+ "NODE_PATH",
7915
+ "ELECTRON_RUN_AS_NODE",
7916
+ "LD_PRELOAD",
7917
+ "LD_LIBRARY_PATH",
7918
+ "LD_AUDIT",
7919
+ "DYLD_INSERT_LIBRARIES",
7920
+ "DYLD_LIBRARY_PATH",
7921
+ "DYLD_FRAMEWORK_PATH",
7922
+ // Executable resolution.
7923
+ "PATH",
7924
+ "PATHEXT",
7925
+ "NODE"
7926
+ ];
7927
+ var REFUSED = new Set(SETTINGS_ENV_REFUSED);
7928
+ function providerEnvKeys() {
7929
+ const keys = [];
7930
+ for (const meta of Object.values(ALL_PROVIDERS)) {
7931
+ if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
7932
+ if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
7933
+ for (const detect of meta.detectEnvVars) keys.push(detect);
7934
+ }
7935
+ return keys;
7936
+ }
7937
+ var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
7938
+ var SETTINGS_ENV_ALLOWLIST = new Set(
7939
+ [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
7940
+ );
7941
+ function admitSettingsEnv(env) {
7942
+ const accepted = {};
7943
+ const rejected = [];
7944
+ for (const [key, value] of Object.entries(env)) {
7945
+ if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
7946
+ rejected.push(key);
7947
+ continue;
7948
+ }
7949
+ accepted[key] = value;
7950
+ }
7951
+ return { accepted, rejected };
7952
+ }
7953
+
7954
+ // src/services/workspaceEnv.ts
7955
+ var WORKSPACE_ENV_FILE = "env";
7956
+ var DIRENV_TIMEOUT_MS = 5e3;
7957
+ var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
7958
+ function defaultDeps() {
7959
+ return {
7960
+ run: (command, args, opts) => new Promise((resolve7) => {
7961
+ execFile2(command, args, { ...opts, timeout: DIRENV_TIMEOUT_MS, encoding: "utf8" }, (err, stdout, stderr) => {
7962
+ const code = err ? typeof err.code === "number" ? Number(err.code) : -1 : 0;
7963
+ resolve7({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
7964
+ });
7965
+ }),
7966
+ readFile: (file) => {
7967
+ try {
7968
+ return fs5.readFileSync(file, "utf8");
7969
+ } catch {
7970
+ return null;
7971
+ }
7972
+ },
7973
+ isDirectory: (dir) => {
7974
+ try {
7975
+ return fs5.statSync(dir).isDirectory();
7976
+ } catch {
7977
+ return false;
7978
+ }
7979
+ },
7980
+ baseEnv: process.env,
7981
+ home: os3.homedir()
7982
+ };
7983
+ }
7984
+ var REFUSED2 = new Set(SETTINGS_ENV_REFUSED);
7985
+ async function resolveWorkspaceEnv(cwd, overrides = {}) {
7986
+ const deps = { ...defaultDeps(), ...overrides };
7987
+ const result = { env: {}, blockedEnvrc: null, refused: [], trackedEnvFile: null };
7988
+ if (!deps.isDirectory(cwd)) return result;
7989
+ const collected = {};
7990
+ const direnv = await fromDirenv(checkoutDirOf(cwd, deps), deps);
7991
+ result.blockedEnvrc = direnv.blocked;
7992
+ Object.assign(collected, direnv.env);
7993
+ const file = findUp(cwd, path9.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
7994
+ if (file) {
7995
+ const content = deps.readFile(file);
7996
+ if (content !== null) {
7997
+ if (await isTracked(file, deps)) result.trackedEnvFile = file;
7998
+ else Object.assign(collected, parseEnvFile(content, { ...deps.baseEnv, ...collected }, deps.home));
7999
+ }
8000
+ }
8001
+ for (const [key, value] of Object.entries(collected)) {
8002
+ if (!KEY.test(key)) continue;
8003
+ if (REFUSED2.has(key) || key.toUpperCase() === "PATH") result.refused.push(key);
8004
+ else result.env[key] = value;
8005
+ }
8006
+ return result;
8007
+ }
8008
+ async function workspaceEnvOf(cwd) {
8009
+ return (await resolveWorkspaceEnv(cwd)).env;
8010
+ }
8011
+ function checkoutDirOf(cwd, deps) {
8012
+ const parts = path9.resolve(cwd).split(path9.sep);
8013
+ const at = parts.findIndex((part, i) => part === STATE_DIR && parts[i + 1] === "worktrees");
8014
+ if (at < 0 || parts.length < at + 4) return cwd;
8015
+ const root = parts.slice(0, at).join(path9.sep) || path9.sep;
8016
+ const mapped = path9.join(root, ...parts.slice(at + 4));
8017
+ return deps.isDirectory(mapped) ? mapped : root;
8018
+ }
8019
+ async function fromDirenv(cwd, deps) {
8020
+ const setting = deps.baseEnv.ORDEWELL_DIRENV;
8021
+ if (setting === "false" || setting === "0") return { env: {}, blocked: null };
8022
+ const baseEnv = Object.fromEntries(Object.entries(deps.baseEnv).filter(([key]) => !key.startsWith("DIRENV_")));
8023
+ let out;
8024
+ try {
8025
+ out = await deps.run("direnv", ["export", "json"], { cwd, env: baseEnv });
8026
+ } catch {
8027
+ return { env: {}, blocked: null };
8028
+ }
8029
+ const blocked = out.stderr.match(/error (\S*\.envrc) is blocked/);
8030
+ if (blocked) return { env: {}, blocked: blocked[1] };
8031
+ if (out.code !== 0 || !out.stdout.trim()) return { env: {}, blocked: null };
8032
+ let parsed;
8033
+ try {
8034
+ parsed = JSON.parse(out.stdout);
8035
+ } catch {
8036
+ return { env: {}, blocked: null };
8037
+ }
8038
+ if (!parsed || typeof parsed !== "object") return { env: {}, blocked: null };
8039
+ const env = {};
8040
+ for (const [key, value] of Object.entries(parsed)) {
8041
+ if (typeof value === "string" && !key.startsWith("DIRENV_")) env[key] = value;
8042
+ }
8043
+ return { env, blocked: null };
8044
+ }
8045
+ function findUp(start, relative5, deps) {
8046
+ let dir = path9.resolve(start);
8047
+ for (; ; ) {
8048
+ const candidate = path9.join(dir, relative5);
8049
+ if (deps.readFile(candidate) !== null) return candidate;
8050
+ const parent = path9.dirname(dir);
8051
+ if (parent === dir) return null;
8052
+ dir = parent;
8053
+ }
8054
+ }
8055
+ async function isTracked(file, deps) {
8056
+ try {
8057
+ const out = await deps.run("git", ["ls-files", "--error-unmatch", path9.basename(file)], { cwd: path9.dirname(file), env: deps.baseEnv });
8058
+ return out.code === 0;
8059
+ } catch {
8060
+ return false;
8061
+ }
8062
+ }
8063
+ function parseEnvFile(content, context, home) {
8064
+ const env = {};
8065
+ const lookup2 = (name) => env[name] ?? context[name] ?? "";
8066
+ const expand = (value) => {
8067
+ const tilde = value.startsWith("~/") ? path9.join(home, value.slice(2)) : value;
8068
+ return tilde.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => lookup2(braced ?? bare));
8069
+ };
8070
+ for (const raw of content.split(/\r?\n/)) {
8071
+ const line = raw.trim().replace(/^export\s+/, "");
8072
+ if (!line || line.startsWith("#")) continue;
8073
+ const eq = line.indexOf("=");
8074
+ if (eq <= 0) continue;
8075
+ const key = line.slice(0, eq).trim();
8076
+ if (!KEY.test(key)) continue;
8077
+ const rest = line.slice(eq + 1).trim();
8078
+ if (rest.startsWith("'")) {
8079
+ const end = rest.indexOf("'", 1);
8080
+ env[key] = end > 0 ? rest.slice(1, end) : rest.slice(1);
8081
+ } else if (rest.startsWith('"')) {
8082
+ const end = rest.lastIndexOf('"');
8083
+ env[key] = expand((end > 0 ? rest.slice(1, end) : rest.slice(1)).replace(/\\n/g, "\n").replace(/\\"/g, '"'));
8084
+ } else {
8085
+ env[key] = expand(rest.replace(/\s+#.*$/, ""));
8086
+ }
8087
+ }
8088
+ return env;
8089
+ }
8090
+
8091
+ // src/services/transcriptCapture.ts
7647
8092
  var HomeTranscriptReader = class {
7648
8093
  home;
8094
+ claudeConfigDirs;
7649
8095
  constructor(opts = {}) {
7650
8096
  const { homeDir } = opts;
7651
- this.home = homeDir ? () => homeDir : () => process.env.HOME || os3.homedir();
8097
+ this.home = homeDir ? () => homeDir : () => process.env.HOME || os4.homedir();
8098
+ const workspaceEnv = opts.workspaceEnv ?? workspaceEnvOf;
8099
+ this.claudeConfigDirs = async (cwd) => [...new Set([
8100
+ (await workspaceEnv(cwd)).CLAUDE_CONFIG_DIR,
8101
+ homeDir ? void 0 : process.env.CLAUDE_CONFIG_DIR,
8102
+ path10.join(this.home(), ".claude")
8103
+ ].filter((dir) => Boolean(dir)))];
7652
8104
  }
7653
8105
  async finalAssistantText(query, maxChars = 4e3) {
7654
8106
  try {
7655
- if (query.runner === "claude-code") return claudeFinal(this.home(), query, maxChars);
8107
+ if (query.runner === "claude-code") return claudeFinal(await this.claudeConfigDirs(query.cwd), query, maxChars);
7656
8108
  if (query.runner === "opencode") return await opencodeFinal(this.home(), query, maxChars);
7657
8109
  if (query.runner === "codex") return codexFinal(this.home(), query, maxChars);
7658
8110
  } catch {
@@ -7661,22 +8113,22 @@ var HomeTranscriptReader = class {
7661
8113
  }
7662
8114
  };
7663
8115
  var CLAUDE_PROJECT_NAME_MAX = 200;
7664
- function claudeProjectDirs(home, cwd) {
7665
- const projects = path8.join(home, ".claude", "projects");
8116
+ function claudeProjectDirs(configDir2, cwd) {
8117
+ const projects = path10.join(configDir2, "projects");
7666
8118
  const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
7667
8119
  if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
7668
- const dir = path8.join(projects, munged);
7669
- return existsSync3(dir) ? [dir] : [];
8120
+ const dir = path10.join(projects, munged);
8121
+ return existsSync4(dir) ? [dir] : [];
7670
8122
  }
7671
- if (!existsSync3(projects)) return [];
8123
+ if (!existsSync4(projects)) return [];
7672
8124
  const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
7673
- return readdirSync2(projects).filter((name) => name.startsWith(prefix)).map((name) => path8.join(projects, name));
8125
+ return readdirSync3(projects).filter((name) => name.startsWith(prefix)).map((name) => path10.join(projects, name));
7674
8126
  }
7675
- function claudeFinal(home, query, maxChars) {
8127
+ function claudeFinal(configDirs, query, maxChars) {
7676
8128
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
7677
- const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path8.join(dir, f))).filter((f) => cutoff ? statSync4(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs);
8129
+ const candidates = configDirs.flatMap((dir) => claudeProjectDirs(dir, query.cwd)).flatMap((dir) => readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path10.join(dir, f))).filter((f) => cutoff ? statSync5(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs);
7678
8130
  for (const file of candidates) {
7679
- const raw = readFileSync(file, "utf8");
8131
+ const raw = readFileSync3(file, "utf8");
7680
8132
  if (!raw.includes(query.marker)) continue;
7681
8133
  const text = claudeLastAssistant(raw);
7682
8134
  if (text) return clamp(text, maxChars);
@@ -7710,8 +8162,8 @@ async function opencodeFinal(home, query, maxChars) {
7710
8162
  } catch {
7711
8163
  return null;
7712
8164
  }
7713
- const dbPath = path8.join(home, ".local", "share", "opencode", "opencode.db");
7714
- if (!existsSync3(dbPath)) return null;
8165
+ const dbPath = path10.join(home, ".local", "share", "opencode", "opencode.db");
8166
+ if (!existsSync4(dbPath)) return null;
7715
8167
  let db;
7716
8168
  try {
7717
8169
  db = new DatabaseSync(dbPath, { open: true });
@@ -7763,8 +8215,8 @@ async function opencodeFinal(home, query, maxChars) {
7763
8215
  }
7764
8216
  }
7765
8217
  function codexFinal(home, query, maxChars) {
7766
- const root = path8.join(home, ".codex", "sessions");
7767
- if (!existsSync3(root)) return null;
8218
+ const root = path10.join(home, ".codex", "sessions");
8219
+ if (!existsSync4(root)) return null;
7768
8220
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
7769
8221
  const files = [];
7770
8222
  const stack = [root];
@@ -7772,15 +8224,15 @@ function codexFinal(home, query, maxChars) {
7772
8224
  const dir = stack.pop();
7773
8225
  let entries;
7774
8226
  try {
7775
- entries = readdirSync2(dir);
8227
+ entries = readdirSync3(dir);
7776
8228
  } catch {
7777
8229
  continue;
7778
8230
  }
7779
8231
  for (const e of entries) {
7780
- const full = path8.join(dir, e);
8232
+ const full = path10.join(dir, e);
7781
8233
  let st;
7782
8234
  try {
7783
- st = statSync4(full);
8235
+ st = statSync5(full);
7784
8236
  } catch {
7785
8237
  continue;
7786
8238
  }
@@ -7788,7 +8240,7 @@ function codexFinal(home, query, maxChars) {
7788
8240
  else if (e.startsWith("rollout-") && e.endsWith(".jsonl") && (!cutoff || st.mtimeMs >= cutoff - 5e3)) files.push(full);
7789
8241
  }
7790
8242
  }
7791
- files.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs);
8243
+ files.sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs);
7792
8244
  for (const file of cutoff ? files : files.slice(0, 10)) {
7793
8245
  const text = codexLastAssistant(file, query);
7794
8246
  if (text) return clamp(text, maxChars);
@@ -7796,7 +8248,7 @@ function codexFinal(home, query, maxChars) {
7796
8248
  return null;
7797
8249
  }
7798
8250
  function codexLastAssistant(file, query) {
7799
- const raw = readFileSync(file, "utf8");
8251
+ const raw = readFileSync3(file, "utf8");
7800
8252
  if (!raw.includes(query.marker)) return null;
7801
8253
  const lines = raw.split("\n").filter((l) => l.trim());
7802
8254
  let sawCwd = false;
@@ -8228,6 +8680,43 @@ ${taskB.description}`,
8228
8680
  }
8229
8681
  };
8230
8682
 
8683
+ // src/utils/shell.ts
8684
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
8685
+ function stripAnsi(text) {
8686
+ return text.replace(ANSI_RE, "");
8687
+ }
8688
+ function posixShellQuote(s) {
8689
+ return `'${s.replace(/'/g, `'\\''`)}'`;
8690
+ }
8691
+ function buildShellInvocation(command, args) {
8692
+ const inner = [command, ...args].map(posixShellQuote).join(" ");
8693
+ return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
8694
+ }
8695
+ function wrapWithPty(command, args, opts = {}) {
8696
+ const inner = [command, ...args].map(posixShellQuote).join(" ");
8697
+ const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
8698
+ const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
8699
+ return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
8700
+ }
8701
+
8702
+ // src/services/blockingPrompts.ts
8703
+ var TAIL = 4096;
8704
+ var squash = (text) => stripAnsi(text).replace(/\s+/g, "").toLowerCase();
8705
+ function watchBlockingPrompts(session, prompts, onPrompt) {
8706
+ if (prompts.length === 0) return;
8707
+ const pending = prompts.map((prompt) => ({ prompt, phrase: squash(prompt.phrase) }));
8708
+ let tail = "";
8709
+ session.onOutput((text) => {
8710
+ if (pending.length === 0) return;
8711
+ tail = (tail + squash(text)).slice(-TAIL);
8712
+ for (let i = pending.length - 1; i >= 0; i--) {
8713
+ if (!tail.includes(pending[i].phrase)) continue;
8714
+ onPrompt(pending[i].prompt);
8715
+ pending.splice(i, 1);
8716
+ }
8717
+ });
8718
+ }
8719
+
8231
8720
  // src/services/TaskOrchestrator.ts
8232
8721
  var SHARED_ROOT_TAIL = "tasks run in the workspace root without worktree isolation.";
8233
8722
  function sharedRootNotice(reason, repos) {
@@ -8291,6 +8780,25 @@ var TaskOrchestrator = class {
8291
8780
  */
8292
8781
  retryCounts = /* @__PURE__ */ new Map();
8293
8782
  spawnCounts = /* @__PURE__ */ new Map();
8783
+ /**
8784
+ * The terminal a verdict left open, by task. It stays so the user can read
8785
+ * the agent's output or keep talking to it — but only while its worktree
8786
+ * does: once that is removed the agent sits in a deleted directory, and a
8787
+ * newer attempt in the same worktree would share it with a second agent.
8788
+ * Without this, every task of every run left one agent process running
8789
+ * until the daemon stopped.
8790
+ */
8791
+ lingering = /* @__PURE__ */ new Map();
8792
+ /**
8793
+ * A full-plan run a failure paused. Retrying the failed task is the explicit
8794
+ * resume the pause waits for — without this a retry only reset the task to
8795
+ * pending and nothing ran until the user also re-ran the whole plan.
8796
+ */
8797
+ haltedByFailure = false;
8798
+ /** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
8799
+ workspaceEnv = (cwd) => resolveWorkspaceEnv(cwd);
8800
+ /** What the workspace env has already warned about, so a run says it once, not per task. */
8801
+ envWarnings = /* @__PURE__ */ new Set();
8294
8802
  /**
8295
8803
  * Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
8296
8804
  * They stay 'pending' — "not executed" — but the scheduler skips them until
@@ -8334,6 +8842,32 @@ var TaskOrchestrator = class {
8334
8842
  setWorkspaceRoot(fn) {
8335
8843
  this.workspaceRootFn = fn;
8336
8844
  }
8845
+ setWorkspaceEnvResolver(resolve7) {
8846
+ this.workspaceEnv = resolve7;
8847
+ }
8848
+ /**
8849
+ * The variables a task's agent gets from its workspace. What cannot be
8850
+ * applied is said once — silently starting without them is how agents ran
8851
+ * under the wrong account when an edited `.envrc` was left unallowed.
8852
+ */
8853
+ async envForTask(cwd) {
8854
+ const resolved = await this.workspaceEnv(cwd);
8855
+ const warn = (key, message) => {
8856
+ if (this.envWarnings.has(key)) return;
8857
+ this.envWarnings.add(key);
8858
+ this.notifications.warn(message);
8859
+ };
8860
+ if (resolved.blockedEnvrc) {
8861
+ warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path11.dirname(resolved.blockedEnvrc)} to use them.`);
8862
+ }
8863
+ if (resolved.trackedEnvFile) {
8864
+ warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
8865
+ }
8866
+ if (resolved.refused.length > 0) {
8867
+ warn(`refused:${resolved.refused.join(",")}`, `Ignored ${resolved.refused.join(", ")} from the workspace environment: Ordewell never passes these to agents.`);
8868
+ }
8869
+ return resolved.env;
8870
+ }
8337
8871
  setRegistry(registry) {
8338
8872
  this.registry = registry;
8339
8873
  }
@@ -8415,7 +8949,7 @@ var TaskOrchestrator = class {
8415
8949
  if (!this.isolationRun) return null;
8416
8950
  const record = this.isolationRun.tasks[taskId];
8417
8951
  if (!record) return { state: "none" };
8418
- return taskIsolationOf(record);
8952
+ return taskIsolationOf(record, this.config.conflictRepairAttempts);
8419
8953
  }
8420
8954
  /**
8421
8955
  * The plan's isolation as a surface shows it, for one with no stream to have
@@ -8424,7 +8958,7 @@ var TaskOrchestrator = class {
8424
8958
  isolationView() {
8425
8959
  const run = this.isolationRun;
8426
8960
  if (!run) return null;
8427
- const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
8961
+ const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
8428
8962
  return { tasks, handoff: handoffOf(run) };
8429
8963
  }
8430
8964
  getAttemptSession(taskId) {
@@ -8460,24 +8994,62 @@ var TaskOrchestrator = class {
8460
8994
  async reviewRunDiff() {
8461
8995
  return this.isolation.reviewDiff(this.requireRun());
8462
8996
  }
8463
- /** "Merge all": the run's integration branches into whatever the user has checked out, in every repo or none. */
8997
+ /**
8998
+ * "Merge all": the run's integration branches into whatever the user has
8999
+ * checked out, in every repo or none. Once everything merged, the run has
9000
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
9001
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
9002
+ */
8464
9003
  async mergeRun() {
8465
9004
  const run = this.requireRun();
8466
9005
  const result = await this.isolation.mergeIntoCheckedOut(run);
8467
9006
  const branch = integrationBranchNameOf(run);
8468
9007
  const group = run.repos.some((r) => r.path !== SELF_REPO);
8469
- const { level, message } = describeMergeResult(result, branch, group);
9008
+ const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
9009
+ const { level, message } = describeMergeResult(result, branch, group, repaired);
8470
9010
  this.notifications[level](message);
9011
+ if (result.outcome === "merged") await this.clearMergedRun(run);
8471
9012
  return result;
8472
9013
  }
9014
+ async clearMergedRun(run) {
9015
+ this.closeLingeringOf(run);
9016
+ try {
9017
+ await this.isolation.discard(run, { integration: "delete-merged" });
9018
+ } catch (err) {
9019
+ this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
9020
+ return;
9021
+ }
9022
+ this.forgetRun();
9023
+ }
8473
9024
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
8474
9025
  async cleanupRun() {
8475
- await this.isolation.discard(this.requireRun(), { keepIntegration: true });
9026
+ this.closeLingeringOf(this.requireRun());
9027
+ await this.isolation.discard(this.requireRun(), { integration: "keep" });
8476
9028
  this.emit("onIsolationChanged");
8477
9029
  }
8478
9030
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
8479
9031
  async discardRun() {
8480
- await this.isolation.discard(this.requireRun(), { keepIntegration: false });
9032
+ this.closeLingeringOf(this.requireRun());
9033
+ await this.isolation.discard(this.requireRun(), { integration: "delete" });
9034
+ this.forgetRun();
9035
+ }
9036
+ watchBlockingPrompts(task, attempt, session) {
9037
+ const manifest = this.registry?.get(attempt.runner)?.manifest;
9038
+ watchBlockingPrompts(session, manifest?.runner.blockingPrompts ?? [], (prompt) => {
9039
+ if (this.attempts.get(task.id) !== attempt) return;
9040
+ this.notifications.warn(`Task "${task.title}" is waiting for you: ${manifest?.displayName ?? attempt.runner} is asking ${prompt.asks}. Answer it in the task's terminal.`);
9041
+ });
9042
+ }
9043
+ closeLingering(taskId) {
9044
+ const sessionId = this.lingering.get(taskId);
9045
+ if (sessionId === void 0) return;
9046
+ this.lingering.delete(taskId);
9047
+ this.terminalRunner.stop(sessionId);
9048
+ }
9049
+ closeLingeringOf(run) {
9050
+ for (const taskId of Object.keys(run.tasks)) this.closeLingering(taskId);
9051
+ }
9052
+ forgetRun() {
8481
9053
  this.isolationRun = null;
8482
9054
  this.resolvers = {};
8483
9055
  this.emit("onIsolationChanged");
@@ -8513,7 +9085,8 @@ var TaskOrchestrator = class {
8513
9085
  }
8514
9086
  loadPlan(tasks, planRunners = ["claude-code"]) {
8515
9087
  this.store.load(tasks, planRunners);
8516
- this.endAllAttempts("load");
9088
+ const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
9089
+ for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
8517
9090
  if (!this.running) this.runMode = null;
8518
9091
  this.blockedStart = null;
8519
9092
  this.planStatus = "approved";
@@ -8563,17 +9136,25 @@ var TaskOrchestrator = class {
8563
9136
  if (!await this.openRun(() => this.start()) || this.running) return;
8564
9137
  console.log(`[TaskOrchestrator] Starting with ${this.store.allTasks.length} tasks (${this.store.allTasks.filter((t) => t.type === "ai" && t.prompt).length} AI ready)`);
8565
9138
  this.running = true;
9139
+ this.haltedByFailure = false;
8566
9140
  this.planStatus = "running";
8567
9141
  this.emit("onTaskChanged");
8568
9142
  await this.tick();
8569
9143
  }
9144
+ haltOnFailure() {
9145
+ if (this.running) this.haltedByFailure = true;
9146
+ this.running = false;
9147
+ this.planStatus = "approved";
9148
+ }
8570
9149
  stop() {
9150
+ this.haltedByFailure = false;
8571
9151
  this.running = false;
8572
9152
  this.planStatus = "approved";
8573
9153
  this.terminalRunner.stopAll();
8574
- const interrupted = [...this.attempts.values()].filter((a) => a.worktree);
8575
- this.endAllAttempts("stop");
8576
- for (const a of interrupted) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
9154
+ for (const a of this.endAllAttempts("stop")) {
9155
+ if (a.repair) this.store.markAwaitingUser(a.taskId);
9156
+ if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
9157
+ }
8577
9158
  this.runMode = null;
8578
9159
  this.blockedStart = null;
8579
9160
  this.onHold.clear();
@@ -8589,6 +9170,7 @@ var TaskOrchestrator = class {
8589
9170
  console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
8590
9171
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
8591
9172
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
9173
+ if (attempt.repair) return this.settleRepair(task, attempt, verdict);
8592
9174
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
8593
9175
  const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
8594
9176
  if (this.attempts.get(taskId) !== attempt) return;
@@ -8602,13 +9184,15 @@ ${summary || "(empty \u2014 no output captured)"}`);
8602
9184
  await this.landPassed(task, landing);
8603
9185
  } else {
8604
9186
  this.store.markFailed(taskId);
8605
- this.running = false;
8606
- this.planStatus = "approved";
9187
+ this.haltOnFailure();
8607
9188
  this.notifications.error(`Task "${task.title}" failed verification: ${verdict.reason}`);
8608
9189
  if (attempt.worktree) await this.releaseWorktree(taskId, { keep: true });
8609
9190
  }
8610
9191
  this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
8611
9192
  this.logAndArchive(task, verdict);
9193
+ await this.afterVerdict();
9194
+ }
9195
+ async afterVerdict() {
8612
9196
  this.emit("onTaskChanged");
8613
9197
  if (!this.running) {
8614
9198
  if (this.attempts.size === 0) {
@@ -8621,6 +9205,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
8621
9205
  }
8622
9206
  await this.tick();
8623
9207
  }
9208
+ /**
9209
+ * A repair's verdict decides only whether its work may try to land. It never
9210
+ * replaces the verdict the task's own work earned, and a repair that does not
9211
+ * land leaves the task waiting on the user — never a failed task, so never a
9212
+ * halted run.
9213
+ */
9214
+ async settleRepair(task, attempt, verdict) {
9215
+ const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
9216
+ if (this.attempts.get(task.id) !== attempt) return;
9217
+ this.endAttempt(task.id, "verdict");
9218
+ if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
9219
+ else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
9220
+ else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
9221
+ else this.landUnmerged(task, landed.outcome);
9222
+ await this.afterVerdict();
9223
+ }
9224
+ /**
9225
+ * Evidence before the queue: the repair's work is committed and checked, and
9226
+ * only then merged, through the same serialized landing as any other task —
9227
+ * so a repair the tip has moved past again is a fresh conflict, not a pass.
9228
+ */
9229
+ async landRepair(task, attempt) {
9230
+ const run = this.isolationRun;
9231
+ let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
9232
+ if (!run) return { evidence, outcome: "failed" };
9233
+ attempt.phase = "integrating";
9234
+ attempt.integration = (async () => {
9235
+ evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
9236
+ if (evidence.ok) return this.integrateWork(task);
9237
+ await this.releaseWorktree(task.id, { keep: true });
9238
+ return "conflict";
9239
+ })();
9240
+ const outcome = await attempt.integration;
9241
+ return { evidence, outcome };
9242
+ }
9243
+ async landedRepair(task, verdict) {
9244
+ const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
9245
+ this.store.markCompleted(task.id);
9246
+ this.store.unblockDependents(task.id);
9247
+ this.logAndArchive(task, task.verdict ?? verdict);
9248
+ this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
9249
+ await this.landResolved(task.id);
9250
+ }
9251
+ /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
9252
+ async unrepaired(task, why) {
9253
+ await this.releaseWorktree(task.id, { keep: true });
9254
+ this.store.markAwaitingUser(task.id);
9255
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
9256
+ this.tell("warn", `The conflict repair of task "${task.title}" ${why}, so it did not land. Its ${group ? "worktrees are" : "worktree is"} kept \u2014 resolve it by hand and mark it complete, retry it, or resolve it as a task.`);
9257
+ }
9258
+ describeEvidence(evidence) {
9259
+ const run = this.isolationRun;
9260
+ const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
9261
+ const branch = run ? integrationBranchNameOf(run) : "the integration branch";
9262
+ switch (evidence.reason) {
9263
+ case "not-merged":
9264
+ return `finished, but its branch${inRepo} does not contain ${branch}`;
9265
+ case "conflict-markers": {
9266
+ const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
9267
+ return `finished, but left conflict markers in ${capConflictFiles(files)}`;
9268
+ }
9269
+ case "failed":
9270
+ return `finished, but git could not check its work${inRepo}`;
9271
+ }
9272
+ }
8624
9273
  /**
8625
9274
  * Merge a passed attempt's worktree. The attempt stays live while it waits on
8626
9275
  * the module's merge queue, so a cancel, retry or stop in that window still
@@ -8654,18 +9303,44 @@ ${summary || "(empty \u2014 no output captured)"}`);
8654
9303
  }
8655
9304
  landUnmerged(task, landing) {
8656
9305
  const branch = this.isolationRun ? integrationBranchNameOf(this.isolationRun) : "the integration branch";
8657
- const repo = this.isolationRun?.tasks[task.id]?.conflictRepo;
8658
- const inRepo = repo && repo !== SELF_REPO ? repo : null;
9306
+ const record = this.isolationRun?.tasks[task.id];
9307
+ const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
8659
9308
  if (landing === "conflict") {
9309
+ const repair = this.nextRepair(task.id);
8660
9310
  this.store.markAwaitingUser(task.id);
8661
- this.notifications.warn(inRepo ? `Task "${task.title}" passed, but landing it on ${branch} conflicted in ${inRepo}, so none of it landed. Its worktrees are kept \u2014 resolve it by hand, retry it, or resolve it as a task.` : `Task "${task.title}" passed, but merging it into ${branch} conflicted. Its worktree is kept \u2014 resolve it by hand, retry it, or resolve it as a task.`);
9311
+ const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
9312
+ const conflicted = inRepo ? `Task "${task.title}" passed, but landing it on ${branch} conflicted in ${inRepo}${files}, so none of it landed.` : `Task "${task.title}" passed, but merging it into ${branch} conflicted${files}.`;
9313
+ if (repair) {
9314
+ this.notifications.warn(conflicted);
9315
+ if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
9316
+ else {
9317
+ this.store.markPending(task.id);
9318
+ this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
9319
+ }
9320
+ return;
9321
+ }
9322
+ this.notifications.warn(`${conflicted} ${inRepo ? "Its worktrees are" : "Its worktree is"} kept \u2014 resolve it by hand and mark it complete, retry it, or resolve it as a task.`);
9323
+ const whyNot = this.noRepairReason(task);
9324
+ if (whyNot) this.tell("info", whyNot);
8662
9325
  } else {
8663
9326
  this.store.markFailed(task.id);
8664
- this.running = false;
8665
- this.planStatus = "approved";
9327
+ this.haltOnFailure();
8666
9328
  this.notifications.error(inRepo ? `Task "${task.title}" passed, but git could not integrate its work in ${inRepo}, so none of it landed. Its worktrees are kept for inspection.` : `Task "${task.title}" passed, but git could not integrate its work. Its worktree is kept for inspection.`);
8667
9329
  }
8668
9330
  }
9331
+ /** The repair a conflicted task is owed next; null when repair is off, used up, or there is no isolated run to repair it in. */
9332
+ nextRepair(taskId) {
9333
+ const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
9334
+ const limit = this.config.conflictRepairAttempts;
9335
+ const spent = record?.repairs ?? 0;
9336
+ return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
9337
+ }
9338
+ noRepairReason(task) {
9339
+ const limit = this.config.conflictRepairAttempts;
9340
+ if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
9341
+ const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
9342
+ return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
9343
+ }
8669
9344
  /**
8670
9345
  * Mark which added task resolves which conflict. The resolver merges the
8671
9346
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -8700,6 +9375,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
8700
9375
  await integration;
8701
9376
  const run = this.isolationRun;
8702
9377
  if (!run?.tasks[taskId]) return;
9378
+ if (!opts.keep) this.closeLingering(taskId);
8703
9379
  try {
8704
9380
  await this.isolation.release(run, taskId, opts);
8705
9381
  } catch (err) {
@@ -8848,7 +9524,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
8848
9524
  this.onHold.delete(taskId);
8849
9525
  this.emit("onTaskChanged");
8850
9526
  await this.releaseWorktree(taskId, { keep: false }, ended?.integration);
8851
- await this.tick();
9527
+ if (this.haltedByFailure && !this.running) await this.start();
9528
+ else await this.tick();
8852
9529
  }
8853
9530
  /**
8854
9531
  * Manually start a single AI task right now, bypassing dependency/readiness
@@ -8955,6 +9632,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
8955
9632
  cwd: null,
8956
9633
  worktree: false,
8957
9634
  integration: null,
9635
+ repair: this.nextRepair(task.id),
8958
9636
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
8959
9637
  };
8960
9638
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -8965,13 +9643,16 @@ ${summary || "(empty \u2014 no output captured)"}`);
8965
9643
  const cwd = await this.resolveAttemptCwd(task, attempt);
8966
9644
  attempt.cwd = cwd;
8967
9645
  if (this.attempts.get(task.id) !== attempt) {
8968
- if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: false });
8969
- return this.abandonSpawn(task);
9646
+ if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
9647
+ return this.abandonSpawn(task, attempt);
8970
9648
  }
8971
- const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
9649
+ const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
8972
9650
  planMapEnabled: this.config.planMapEnabled,
8973
- tddEnabled: this.tddEnabled()
9651
+ // A merge to resolve is not new behaviour to drive test-first.
9652
+ tddEnabled: !attempt.repair && this.tddEnabled()
8974
9653
  });
9654
+ this.closeLingering(task.id);
9655
+ const env = await this.envForTask(cwd);
8975
9656
  const session = await this.terminalRunner.spawn({
8976
9657
  taskId: task.id,
8977
9658
  runner: attempt.runner,
@@ -8983,18 +9664,30 @@ ${summary || "(empty \u2014 no output captured)"}`);
8983
9664
  cwd,
8984
9665
  registry: this.registry ?? void 0,
8985
9666
  order: task.order,
8986
- title: task.title
9667
+ title: task.title,
9668
+ env
8987
9669
  });
8988
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
9670
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
8989
9671
  attempt.phase = "running";
8990
9672
  attempt.session = session;
8991
9673
  this.output.attach(task.id, session);
8992
9674
  this.verifier.watch(task, session);
9675
+ this.watchBlockingPrompts(task, attempt, session);
8993
9676
  this.emit("onTaskChanged");
8994
- this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
9677
+ if (attempt.repair) {
9678
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
9679
+ this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
9680
+ } else {
9681
+ this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
9682
+ }
8995
9683
  } catch (err) {
8996
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
9684
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
8997
9685
  this.endAttempt(task.id, "spawn-failed");
9686
+ if (attempt.repair) {
9687
+ await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
9688
+ this.emit("onTaskChanged");
9689
+ return this.tick();
9690
+ }
8998
9691
  await this.releaseWorktree(task.id, { keep: false });
8999
9692
  this.store.markPending(task.id);
9000
9693
  this.onHold.add(task.id);
@@ -9008,17 +9701,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
9008
9701
  * and take back the claim it made. Only the claim — whatever ended the
9009
9702
  * attempt may have decided the task since (mark complete, cancel, retry).
9010
9703
  */
9011
- abandonSpawn(task, session) {
9704
+ abandonSpawn(task, attempt, session) {
9012
9705
  session?.kill();
9013
9706
  if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
9014
- this.store.markPending(task.id);
9707
+ if (attempt.repair) this.store.markAwaitingUser(task.id);
9708
+ else this.store.markPending(task.id);
9015
9709
  this.emit("onTaskChanged");
9016
9710
  }
9711
+ repairPrompt(task) {
9712
+ const run = this.requireRun();
9713
+ const record = run.tasks[task.id];
9714
+ const conflict = {
9715
+ branch: record?.branch ?? "",
9716
+ repos: Object.keys(record?.repairBase ?? {}),
9717
+ ...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
9718
+ ...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
9719
+ };
9720
+ return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
9721
+ }
9017
9722
  /**
9018
9723
  * The one place an attempt's working directory is decided. Async so a
9019
9724
  * per-attempt workspace (worktree isolation, #12) can be prepared here.
9020
9725
  */
9021
9726
  async resolveAttemptCwd(task, attempt) {
9727
+ if (attempt.repair && this.isolationRun) {
9728
+ const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
9729
+ attempt.worktree = true;
9730
+ this.emit("onIsolationChanged");
9731
+ return cwd2;
9732
+ }
9022
9733
  if (this.runMode !== "isolated" || !this.isolationRun) {
9023
9734
  await this.releaseWorktree(task.id, { keep: false });
9024
9735
  return this.workspaceRootFn();
@@ -9106,8 +9817,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
9106
9817
  }
9107
9818
  }
9108
9819
  this.runMode = "isolated";
9820
+ await this.sweep();
9109
9821
  return true;
9110
9822
  }
9823
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
9824
+ async sweep() {
9825
+ if (!this.isolationRun) return;
9826
+ try {
9827
+ await this.isolation.sweep(this.isolationRun);
9828
+ } catch (err) {
9829
+ this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
9830
+ }
9831
+ }
9111
9832
  /**
9112
9833
  * The plan's run carries on while anything has landed on its integration
9113
9834
  * branch: a resumed plan's dependents must start from a tip that holds their
@@ -9120,14 +9841,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
9120
9841
  /**
9121
9842
  * A run with nothing landed holds only superseded attempts, so it goes whole.
9122
9843
  * One that cannot be continued for another reason — it ran from a different
9123
- * workspace path — keeps its integration branch: only the user gives landed
9124
- * work up.
9844
+ * workspace path — keeps its integration branch in each repo that has not
9845
+ * merged it: only the user gives landed work up.
9125
9846
  */
9126
9847
  async mintRun(root) {
9127
9848
  const previous = this.isolationRun;
9128
9849
  if (previous) {
9129
9850
  const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
9130
- await this.isolation.discard(previous, { keepIntegration: landed }).catch(() => void 0);
9851
+ await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
9131
9852
  this.isolationRun = null;
9132
9853
  }
9133
9854
  this.isolationRun = await this.isolation.startRun(root);
@@ -9184,6 +9905,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
9184
9905
  const attempt = this.attempts.get(taskId);
9185
9906
  this.attempts.delete(taskId);
9186
9907
  if (attempt) this.output.detach(taskId);
9908
+ if (reason === "verdict" && attempt?.session) this.lingering.set(taskId, attempt.session.id);
9187
9909
  if (reason === "cancel" || reason === "release" || reason === "complete" || reason === "retry") {
9188
9910
  const task = this.store.get(taskId);
9189
9911
  if (task) this.verifier.clear(task);
@@ -9192,9 +9914,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
9192
9914
  return attempt;
9193
9915
  }
9194
9916
  endAllAttempts(reason) {
9195
- for (const taskId of [...this.attempts.keys()]) this.endAttempt(taskId, reason);
9917
+ const ended = [...this.attempts.values()];
9918
+ for (const { taskId } of ended) this.endAttempt(taskId, reason);
9196
9919
  this.verifier.reset();
9197
9920
  if (reason === "load") this.output.reset();
9921
+ return ended;
9198
9922
  }
9199
9923
  };
9200
9924
 
@@ -9324,29 +10048,29 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
9324
10048
  };
9325
10049
 
9326
10050
  // src/services/ModelDiscovery.ts
9327
- import * as path11 from "path";
10051
+ import * as path14 from "path";
9328
10052
  import { exec as exec3, spawn } from "child_process";
9329
10053
  import { promisify as promisify3 } from "util";
9330
10054
  import * as fsSync3 from "fs";
9331
10055
  import * as osMod from "os";
9332
10056
 
9333
10057
  // src/utils/globalDataDir.ts
9334
- import * as fs5 from "fs";
9335
- import * as path9 from "path";
9336
- import * as os4 from "os";
10058
+ import * as fs6 from "fs";
10059
+ import * as path12 from "path";
10060
+ import * as os5 from "os";
9337
10061
  function globalDataDir() {
9338
- return path9.join(os4.homedir(), ".ordewell");
10062
+ return path12.join(os5.homedir(), ".ordewell");
9339
10063
  }
9340
10064
  function copyDirSync(src, dest) {
9341
- fs5.mkdirSync(dest, { recursive: true });
9342
- const entries = fs5.readdirSync(src, { withFileTypes: true });
10065
+ fs6.mkdirSync(dest, { recursive: true });
10066
+ const entries = fs6.readdirSync(src, { withFileTypes: true });
9343
10067
  for (const entry of entries) {
9344
- const srcPath = path9.join(src, entry.name);
9345
- const destPath = path9.join(dest, entry.name);
10068
+ const srcPath = path12.join(src, entry.name);
10069
+ const destPath = path12.join(dest, entry.name);
9346
10070
  if (entry.isDirectory()) {
9347
10071
  copyDirSync(srcPath, destPath);
9348
10072
  } else {
9349
- fs5.copyFileSync(srcPath, destPath);
10073
+ fs6.copyFileSync(srcPath, destPath);
9350
10074
  }
9351
10075
  }
9352
10076
  }
@@ -9354,50 +10078,29 @@ var migrated = false;
9354
10078
  function migrateOldConfigDir() {
9355
10079
  if (migrated) return;
9356
10080
  migrated = true;
9357
- const oldDir = path9.join(os4.homedir(), ".config", "ordewell");
9358
- if (!fs5.existsSync(oldDir)) return;
10081
+ const oldDir = path12.join(os5.homedir(), ".config", "ordewell");
10082
+ if (!fs6.existsSync(oldDir)) return;
9359
10083
  const newDir = globalDataDir();
9360
10084
  const liftFile = (name) => {
9361
- const oldFile = path9.join(oldDir, name);
9362
- const newFile = path9.join(newDir, name);
9363
- if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
9364
- fs5.mkdirSync(newDir, { recursive: true });
9365
- fs5.copyFileSync(oldFile, newFile);
10085
+ const oldFile = path12.join(oldDir, name);
10086
+ const newFile = path12.join(newDir, name);
10087
+ if (fs6.existsSync(oldFile) && !fs6.existsSync(newFile)) {
10088
+ fs6.mkdirSync(newDir, { recursive: true });
10089
+ fs6.copyFileSync(oldFile, newFile);
9366
10090
  }
9367
10091
  };
9368
10092
  liftFile("settings.json");
9369
10093
  liftFile(".env");
9370
- const oldPlugins = path9.join(oldDir, "plugins");
9371
- const newPlugins = path9.join(newDir, "plugins");
9372
- if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
10094
+ const oldPlugins = path12.join(oldDir, "plugins");
10095
+ const newPlugins = path12.join(newDir, "plugins");
10096
+ if (fs6.existsSync(oldPlugins) && !fs6.existsSync(newPlugins)) {
9373
10097
  copyDirSync(oldPlugins, newPlugins);
9374
10098
  }
9375
10099
  }
9376
10100
 
9377
10101
  // src/utils/launch.ts
9378
10102
  import * as fsSync2 from "fs";
9379
- import * as path10 from "path";
9380
-
9381
- // src/utils/shell.ts
9382
- var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
9383
- function stripAnsi(text) {
9384
- return text.replace(ANSI_RE, "");
9385
- }
9386
- function posixShellQuote(s) {
9387
- return `'${s.replace(/'/g, `'\\''`)}'`;
9388
- }
9389
- function buildShellInvocation(command, args) {
9390
- const inner = [command, ...args].map(posixShellQuote).join(" ");
9391
- return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
9392
- }
9393
- function wrapWithPty(command, args, opts = {}) {
9394
- const inner = [command, ...args].map(posixShellQuote).join(" ");
9395
- const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
9396
- const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
9397
- return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
9398
- }
9399
-
9400
- // src/utils/launch.ts
10103
+ import * as path13 from "path";
9401
10104
  var DIRECT_EXTENSIONS = [".exe", ".com"];
9402
10105
  var BATCH_EXTENSIONS = [".cmd", ".bat"];
9403
10106
  var POWERSHELL_EXTENSIONS = [".ps1"];
@@ -9405,7 +10108,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
9405
10108
  var WINDOWS_MAX_COMMAND_LINE = 32767;
9406
10109
  function defaultPowerShell() {
9407
10110
  const root = process.env.SystemRoot || process.env.windir;
9408
- return root ? path10.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
10111
+ return root ? path13.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
9409
10112
  }
9410
10113
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
9411
10114
  var CommandLineTooLongError = class extends Error {
@@ -9457,7 +10160,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
9457
10160
  };
9458
10161
  function isResolvableOnPosixPath(command, PATH, exists) {
9459
10162
  if (command.includes("/")) return exists(command);
9460
- return PATH.split(path10.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path10.join(dir, command)));
10163
+ return PATH.split(path13.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path13.join(dir, command)));
9461
10164
  }
9462
10165
  function isExecutableResolved(command, plan, PATH, deps = {}) {
9463
10166
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -9497,9 +10200,9 @@ function cmdArgs(line) {
9497
10200
  return ["/d", "/s", "/c", `"${line}"`];
9498
10201
  }
9499
10202
  function candidateExtensions(command, pathExt) {
9500
- const declared = pathExt.split(path10.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
10203
+ const declared = pathExt.split(path13.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
9501
10204
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
9502
- const ext = path10.win32.extname(command).toLowerCase();
10205
+ const ext = path13.win32.extname(command).toLowerCase();
9503
10206
  if (ext && known.has(ext)) {
9504
10207
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
9505
10208
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -9520,13 +10223,13 @@ async function findWindowsExecutables(command, deps) {
9520
10223
  const exists = deps.exists ?? defaultExists2;
9521
10224
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
9522
10225
  const byRoute = candidateExtensions(command, pathExt);
9523
- const dirs = path10.win32.basename(command) !== command ? [path10.win32.dirname(path10.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path10.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
9524
- const base = path10.win32.basename(command);
10226
+ const dirs = path13.win32.basename(command) !== command ? [path13.win32.dirname(path13.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path13.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
10227
+ const base = path13.win32.basename(command);
9525
10228
  const found = [];
9526
10229
  for (const route of ROUTES) {
9527
10230
  outer: for (const ext of byRoute[route]) {
9528
10231
  for (const dir of dirs) {
9529
- const candidate = path10.win32.join(dir, `${base}${ext}`);
10232
+ const candidate = path13.win32.join(dir, `${base}${ext}`);
9530
10233
  if (exists(candidate)) {
9531
10234
  found.push({ file: candidate, route });
9532
10235
  break outer;
@@ -9572,10 +10275,10 @@ async function planShellLaunch(command, args, deps = {}) {
9572
10275
  }
9573
10276
 
9574
10277
  // src/utils/processTree.ts
9575
- import { execFile as execFile2 } from "child_process";
10278
+ import { execFile as execFile3 } from "child_process";
9576
10279
  var HARD_KILL_DELAY_MS = 5e3;
9577
10280
  function defaultExecFile2(file, args, cb) {
9578
- execFile2(file, args, (err) => cb(err ?? null));
10281
+ execFile3(file, args, (err) => cb(err ?? null));
9579
10282
  }
9580
10283
  function killTree(proc, deps = {}) {
9581
10284
  if (!proc || proc.killed || typeof proc.exitCode === "number") return;
@@ -9621,7 +10324,7 @@ var defaultAppServerClient = async (command, args, options) => {
9621
10324
  const PATH = await augmentedPath();
9622
10325
  const launch = await planDirectLaunch(command, args).catch(() => null);
9623
10326
  if (!launch) return null;
9624
- return new Promise((resolve5) => {
10327
+ return new Promise((resolve7) => {
9625
10328
  let child;
9626
10329
  try {
9627
10330
  child = spawn(launch.file, launch.args, {
@@ -9630,7 +10333,7 @@ var defaultAppServerClient = async (command, args, options) => {
9630
10333
  windowsVerbatimArguments: launch.verbatim
9631
10334
  });
9632
10335
  } catch {
9633
- resolve5(null);
10336
+ resolve7(null);
9634
10337
  return;
9635
10338
  }
9636
10339
  let settled = false;
@@ -9639,7 +10342,7 @@ var defaultAppServerClient = async (command, args, options) => {
9639
10342
  settled = true;
9640
10343
  clearTimeout(timer);
9641
10344
  killTree(child);
9642
- resolve5(value);
10345
+ resolve7(value);
9643
10346
  };
9644
10347
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
9645
10348
  child.on("error", () => finish(null));
@@ -9706,7 +10409,7 @@ function geminiDiscoveryCacheKey(apiKey) {
9706
10409
  return {
9707
10410
  key: `gemini-models:${hash}`,
9708
10411
  dir,
9709
- file: path11.join(dir, `gemini_models_${hash}.json`)
10412
+ file: path14.join(dir, `gemini_models_${hash}.json`)
9710
10413
  };
9711
10414
  }
9712
10415
  function readGeminiCache(apiKey) {
@@ -10229,8 +10932,8 @@ var ModelDiscovery = class {
10229
10932
  };
10230
10933
 
10231
10934
  // src/services/ModelCatalog.ts
10232
- import * as fs6 from "fs";
10233
- import * as path12 from "path";
10935
+ import * as fs7 from "fs";
10936
+ import * as path15 from "path";
10234
10937
  var CACHE_KEY = "model-catalog";
10235
10938
  var CACHE_TTL_MS = 60 * 60 * 1e3;
10236
10939
  function storageKey(baseUrl) {
@@ -10282,17 +10985,17 @@ var ModelCatalog = class {
10282
10985
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
10283
10986
  const key = storageKey(url);
10284
10987
  const cacheDir = globalCacheDir();
10285
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10286
- if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
10988
+ const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10989
+ if (fs7.existsSync(cacheFile)) fs7.rmSync(cacheFile);
10287
10990
  } catch {
10288
10991
  }
10289
10992
  }
10290
10993
  static readCache(key) {
10291
10994
  try {
10292
10995
  const cacheDir = globalCacheDir();
10293
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10294
- if (!fs6.existsSync(cacheFile)) return null;
10295
- const raw = fs6.readFileSync(cacheFile, "utf8");
10996
+ const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10997
+ if (!fs7.existsSync(cacheFile)) return null;
10998
+ const raw = fs7.readFileSync(cacheFile, "utf8");
10296
10999
  const data = JSON.parse(raw);
10297
11000
  if (Date.now() - data.cachedAt > CACHE_TTL_MS) return null;
10298
11001
  return JSON.stringify(data.models);
@@ -10303,9 +11006,9 @@ var ModelCatalog = class {
10303
11006
  static writeCache(key, models) {
10304
11007
  try {
10305
11008
  const cacheDir = globalCacheDir();
10306
- fs6.mkdirSync(cacheDir, { recursive: true });
10307
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10308
- fs6.writeFileSync(
11009
+ fs7.mkdirSync(cacheDir, { recursive: true });
11010
+ const cacheFile = path15.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
11011
+ fs7.writeFileSync(
10309
11012
  cacheFile,
10310
11013
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
10311
11014
  );
@@ -10681,71 +11384,12 @@ var RunnerInstallation = class {
10681
11384
  }
10682
11385
  };
10683
11386
 
10684
- // src/services/settingsEnvAllowlist.ts
10685
- var ORDEWELL_SETTABLE_ENV = [
10686
- "AI_PROVIDER",
10687
- "ORCHESTRATOR_MODEL",
10688
- "ORDEWELL_SUBAGENT_MODEL",
10689
- "ORDEWELL_PLANNER_EFFORT",
10690
- "ORDEWELL_RESEARCH_ENABLED",
10691
- "ORDEWELL_MAX_PARALLEL"
10692
- ];
10693
- var SETTINGS_ENV_REFUSED = [
10694
- // Approval posture and pre-approved scope.
10695
- "ORDEWELL_AUTONOMOUS_MODE",
10696
- "ORDEWELL_APPROVAL_MODE",
10697
- "ORDEWELL_APPROVAL_ALLOW",
10698
- // Where settings are persisted.
10699
- "ORDEWELL_SETTINGS_PATH",
10700
- // Runtime options and dynamic loaders.
10701
- "NODE_OPTIONS",
10702
- "NODE_REPL_EXTERNAL_MODULE",
10703
- "NODE_PATH",
10704
- "ELECTRON_RUN_AS_NODE",
10705
- "LD_PRELOAD",
10706
- "LD_LIBRARY_PATH",
10707
- "LD_AUDIT",
10708
- "DYLD_INSERT_LIBRARIES",
10709
- "DYLD_LIBRARY_PATH",
10710
- "DYLD_FRAMEWORK_PATH",
10711
- // Executable resolution.
10712
- "PATH",
10713
- "PATHEXT",
10714
- "NODE"
10715
- ];
10716
- var REFUSED = new Set(SETTINGS_ENV_REFUSED);
10717
- function providerEnvKeys() {
10718
- const keys = [];
10719
- for (const meta of Object.values(ALL_PROVIDERS)) {
10720
- if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
10721
- if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
10722
- for (const detect of meta.detectEnvVars) keys.push(detect);
10723
- }
10724
- return keys;
10725
- }
10726
- var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
10727
- var SETTINGS_ENV_ALLOWLIST = new Set(
10728
- [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
10729
- );
10730
- function admitSettingsEnv(env) {
10731
- const accepted = {};
10732
- const rejected = [];
10733
- for (const [key, value] of Object.entries(env)) {
10734
- if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
10735
- rejected.push(key);
10736
- continue;
10737
- }
10738
- accepted[key] = value;
10739
- }
10740
- return { accepted, rejected };
10741
- }
10742
-
10743
11387
  // src/services/harness/CliAgentAiService.ts
10744
11388
  import { spawn as nodeSpawn } from "child_process";
10745
11389
 
10746
11390
  // src/utils/workspace.ts
10747
- import * as fs7 from "fs";
10748
- import * as path13 from "path";
11391
+ import * as fs8 from "fs";
11392
+ import * as path16 from "path";
10749
11393
  var WorkspaceNotFoundError = class extends Error {
10750
11394
  constructor(workspace) {
10751
11395
  super(`Workspace "${workspace}" does not exist.`);
@@ -10756,7 +11400,7 @@ var WorkspaceNotFoundError = class extends Error {
10756
11400
  };
10757
11401
  function defaultIsDirectory(candidate) {
10758
11402
  try {
10759
- return fs7.statSync(candidate).isDirectory();
11403
+ return fs8.statSync(candidate).isDirectory();
10760
11404
  } catch {
10761
11405
  return false;
10762
11406
  }
@@ -10792,8 +11436,8 @@ var PROJECT_MARKER_FILES = [
10792
11436
  "CMakeLists.txt"
10793
11437
  ];
10794
11438
  function assertWorkspaceIsProject(workspace, deps = {}) {
10795
- const realpath = deps.realpath ?? ((candidate) => fs7.realpathSync(candidate));
10796
- const exists = deps.exists ?? ((candidate) => fs7.existsSync(candidate));
11439
+ const realpath = deps.realpath ?? ((candidate) => fs8.realpathSync(candidate));
11440
+ const exists = deps.exists ?? ((candidate) => fs8.existsSync(candidate));
10797
11441
  let resolved;
10798
11442
  try {
10799
11443
  resolved = realpath(workspace);
@@ -10801,7 +11445,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
10801
11445
  throw new WorkspaceNotAProjectError(workspace);
10802
11446
  }
10803
11447
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
10804
- (marker2) => exists(path13.join(resolved, marker2))
11448
+ (marker2) => exists(path16.join(resolved, marker2))
10805
11449
  );
10806
11450
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
10807
11451
  }
@@ -10869,13 +11513,14 @@ var StdioAgentAdapter = class {
10869
11513
  }
10870
11514
  async start(opts) {
10871
11515
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
10872
- this.processEnded = new Promise((resolve5) => {
10873
- this.markEnded = resolve5;
11516
+ this.processEnded = new Promise((resolve7) => {
11517
+ this.markEnded = resolve7;
10874
11518
  });
10875
11519
  const spec = this.spawnSpec(opts);
10876
11520
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
10877
11521
  const PATH = await resolvePath();
10878
- this.spawnEnv = withPath(process.env, PATH, spec.env);
11522
+ const workspace = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
11523
+ this.spawnEnv = withPath(process.env, PATH, { ...workspace, ...spec.env });
10879
11524
  const launch = await planDirectLaunch(spec.command, spec.args, {
10880
11525
  platform: this.deps.platform,
10881
11526
  resolvePath
@@ -10921,7 +11566,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
10921
11566
  onEvent({ type: "error", message: this.exitMessage() });
10922
11567
  return;
10923
11568
  }
10924
- await new Promise((resolve5) => {
11569
+ await new Promise((resolve7) => {
10925
11570
  let settled = false;
10926
11571
  const finish = () => {
10927
11572
  if (settled) return;
@@ -10932,7 +11577,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
10932
11577
  signal?.removeEventListener("abort", onAbort);
10933
11578
  this.process?.removeListener("exit", onExit);
10934
11579
  this.process?.removeListener("error", onExit);
10935
- resolve5();
11580
+ resolve7();
10936
11581
  };
10937
11582
  const emit = (event) => {
10938
11583
  onEvent(event);
@@ -11147,14 +11792,14 @@ function codexSandboxUnavailableMessage() {
11147
11792
  ].join("\n");
11148
11793
  }
11149
11794
  function runProbe(deps, cwd, env, flags) {
11150
- return new Promise((resolve5) => {
11795
+ return new Promise((resolve7) => {
11151
11796
  let output = "";
11152
11797
  let settled = false;
11153
11798
  const finish = (code) => {
11154
11799
  if (settled) return;
11155
11800
  settled = true;
11156
11801
  clearTimeout(timer);
11157
- resolve5({ code, output });
11802
+ resolve7({ code, output });
11158
11803
  };
11159
11804
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
11160
11805
  timer.unref?.();
@@ -11237,8 +11882,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
11237
11882
  async handshake(opts) {
11238
11883
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
11239
11884
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
11240
- const ready = new Promise((resolve5) => {
11241
- this.settleHandshake = resolve5;
11885
+ const ready = new Promise((resolve7) => {
11886
+ this.settleHandshake = resolve7;
11242
11887
  });
11243
11888
  this.writeLine({
11244
11889
  jsonrpc: "2.0",
@@ -11251,8 +11896,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
11251
11896
  // A binary that rejects `app-server` outright dies immediately; waiting
11252
11897
  // out the timeout would turn a one-line diagnostic into a 30s stall.
11253
11898
  this.processEnded.then(() => false),
11254
- new Promise((resolve5) => {
11255
- const t = setTimeout(() => resolve5(false), HANDSHAKE_TIMEOUT_MS);
11899
+ new Promise((resolve7) => {
11900
+ const t = setTimeout(() => resolve7(false), HANDSHAKE_TIMEOUT_MS);
11256
11901
  t.unref?.();
11257
11902
  })
11258
11903
  ]);
@@ -11529,12 +12174,12 @@ function describeError(err) {
11529
12174
  return lines.join(": ");
11530
12175
  }
11531
12176
  function delay(ms, signal) {
11532
- return new Promise((resolve5) => {
11533
- const timer = setTimeout(resolve5, ms);
12177
+ return new Promise((resolve7) => {
12178
+ const timer = setTimeout(resolve7, ms);
11534
12179
  timer.unref?.();
11535
12180
  signal?.addEventListener("abort", () => {
11536
12181
  clearTimeout(timer);
11537
- resolve5();
12182
+ resolve7();
11538
12183
  }, { once: true });
11539
12184
  });
11540
12185
  }
@@ -11568,17 +12213,17 @@ var OpenCodeAdapter = class {
11568
12213
  throw new ExecutableNotFoundError("opencode", PATH);
11569
12214
  }
11570
12215
  this.process = this.deps.spawn(launch.file, launch.args, {
11571
- env: withPath(process.env, PATH),
12216
+ env: withPath(process.env, PATH, await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd)),
11572
12217
  stdio: ["pipe", "pipe", "pipe"],
11573
12218
  cwd: opts.cwd,
11574
12219
  windowsVerbatimArguments: launch.verbatim
11575
12220
  });
11576
- const banner = new Promise((resolve5) => {
12221
+ const banner = new Promise((resolve7) => {
11577
12222
  let seen = "";
11578
12223
  const scan = (chunk) => {
11579
12224
  seen += chunk.toString();
11580
12225
  const match = seen.match(/https?:\/\/[^\s]+/);
11581
- if (match) resolve5(match[0].replace(/[.,)]$/, ""));
12226
+ if (match) resolve7(match[0].replace(/[.,)]$/, ""));
11582
12227
  };
11583
12228
  this.process.stdout?.on("data", scan);
11584
12229
  this.process.stderr?.on("data", (chunk) => {
@@ -11587,15 +12232,15 @@ var OpenCodeAdapter = class {
11587
12232
  });
11588
12233
  this.process.on("exit", () => {
11589
12234
  this.exited = true;
11590
- resolve5(null);
12235
+ resolve7(null);
11591
12236
  });
11592
12237
  this.process.on("error", (err) => {
11593
12238
  this.exited = true;
11594
12239
  this.stderrTail = (this.stderrTail + `
11595
12240
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
11596
- resolve5(null);
12241
+ resolve7(null);
11597
12242
  });
11598
- const timer = setTimeout(() => resolve5(null), SERVER_READY_TIMEOUT_MS);
12243
+ const timer = setTimeout(() => resolve7(null), SERVER_READY_TIMEOUT_MS);
11599
12244
  timer.unref?.();
11600
12245
  });
11601
12246
  this.baseUrl = await banner;
@@ -11628,8 +12273,8 @@ ${this.stderrTail.trim()}` : ""}`);
11628
12273
  const streamAbort = new AbortController();
11629
12274
  let connected = () => {
11630
12275
  };
11631
- const streamReady = new Promise((resolve5) => {
11632
- connected = resolve5;
12276
+ const streamReady = new Promise((resolve7) => {
12277
+ connected = resolve7;
11633
12278
  });
11634
12279
  const live = this.streamEvents(
11635
12280
  streamAbort.signal,
@@ -11833,14 +12478,14 @@ ${part.text}` : part.text });
11833
12478
  }
11834
12479
  }
11835
12480
  }
11836
- async json(method, path22, body, signal) {
11837
- const response = await this.deps.fetch(`${this.baseUrl}${path22}`, {
12481
+ async json(method, path25, body, signal) {
12482
+ const response = await this.deps.fetch(`${this.baseUrl}${path25}`, {
11838
12483
  method,
11839
12484
  headers: { "content-type": "application/json" },
11840
12485
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
11841
12486
  ...signal ? { signal } : {}
11842
12487
  });
11843
- if (!response.ok) throw new Error(`${method} ${path22} failed: ${response.status} ${response.statusText}`);
12488
+ if (!response.ok) throw new Error(`${method} ${path25} failed: ${response.status} ${response.statusText}`);
11844
12489
  return await response.json().catch(() => null);
11845
12490
  }
11846
12491
  nativeSessionId() {
@@ -12294,8 +12939,8 @@ var CliAgentAiService = class {
12294
12939
  this.lastNativeSessionId = previousSessionId;
12295
12940
  }
12296
12941
  }
12297
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
12298
- const contextStr = await collectResearchContext(fs14, runners);
12942
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
12943
+ const contextStr = await collectResearchContext(fs15, runners);
12299
12944
  const researchLog = [
12300
12945
  { id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
12301
12946
  ];
@@ -12509,33 +13154,9 @@ var PlannerConversation = class {
12509
13154
  if (m.role !== "user" || index === 0) return [];
12510
13155
  const line = m.content.split("\n")[0];
12511
13156
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
12512
- return [{ index, preview, timestamp: m.timestamp }];
13157
+ return [{ index, preview, content: m.content, timestamp: m.timestamp }];
12513
13158
  });
12514
13159
  }
12515
- /**
12516
- * Cut the dialogue back to just before the user message at `index` (a
12517
- * position in the transcript), discarding it and everything after. The task
12518
- * list is the host's and rides along untouched — rewinding moves where the
12519
- * conversation resumes, not what the plan is. The planner's research trace
12520
- * goes back to the same point, by time: its entries carry no link to the
12521
- * message that caused them.
12522
- */
12523
- rewind(index) {
12524
- this.assertIdle("rewind the conversation");
12525
- const plan = this.requirePlan();
12526
- if (!this.rewindTargets().some((t) => t.index === index)) {
12527
- if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
12528
- throw new ConversationEditError(this.transcript[0]?.kind === "compaction" ? "The conversation was condensed there \u2014 a rewind cannot reach back past the summary." : "The first message is the goal \u2014 start a new session to change it.");
12529
- }
12530
- const cutoff = this.transcript[index].timestamp;
12531
- const rewound = this.host.mutate(() => {
12532
- plan.conversationHistory = this.transcript.slice(0, index);
12533
- plan.researchLog = (plan.researchLog ?? []).filter((e) => e.timestamp < cutoff);
12534
- return true;
12535
- }, () => this.host.broadcastPlan());
12536
- this.reset();
12537
- return rewound;
12538
- }
12539
13160
  /**
12540
13161
  * Replace the transcript with a summary of it, keeping the last two
12541
13162
  * exchanges as they were, and drop the live context so the next message
@@ -12611,6 +13232,28 @@ var PlannerConversation = class {
12611
13232
  researchLog: plan?.researchLog ?? []
12612
13233
  });
12613
13234
  }
13235
+ /**
13236
+ * A copy of the dialogue as it stood just before the user message at
13237
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
13238
+ * from — plus that message's full text, for a surface to offer back. This
13239
+ * conversation is not touched. The research trace is cut at the same point
13240
+ * by time: its entries carry no link to the message that caused them.
13241
+ */
13242
+ cloneBefore(index) {
13243
+ this.assertIdle("rewind the conversation");
13244
+ if (!this.rewindTargets().some((t) => t.index === index)) {
13245
+ if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
13246
+ throw new ConversationEditError(this.transcript[0]?.kind === "compaction" ? "The conversation was condensed there \u2014 a rewind cannot reach back past the summary." : "The first message is the goal \u2014 start a new session to change it.");
13247
+ }
13248
+ const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
13249
+ return {
13250
+ dialogue: structuredClone({
13251
+ conversationHistory: this.transcript.slice(0, index),
13252
+ researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
13253
+ }),
13254
+ rewoundMessage
13255
+ };
13256
+ }
12614
13257
  /** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
12615
13258
  recordModification(request, requestedAt, taskCount) {
12616
13259
  this.append("user", request, { timestamp: requestedAt });
@@ -13196,25 +13839,25 @@ function retargetTaskRunner(task, runner, catalog) {
13196
13839
  }
13197
13840
 
13198
13841
  // src/services/SkillsService.ts
13199
- import * as fs8 from "fs";
13200
- import * as path15 from "path";
13842
+ import * as fs9 from "fs";
13843
+ import * as path18 from "path";
13201
13844
 
13202
13845
  // src/services/builtinSkills.ts
13203
- import * as path14 from "path";
13846
+ import * as path17 from "path";
13204
13847
  import { fileURLToPath } from "url";
13205
13848
  function builtinSkillsDir() {
13206
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path14.dirname(fileURLToPath(
13849
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path17.dirname(fileURLToPath(
13207
13850
  // @ts-expect-error - import.meta is only valid under an ESM module setting
13208
13851
  import.meta.url
13209
13852
  ));
13210
- return path14.join(dir, "..", "skills");
13853
+ return path17.join(dir, "..", "skills");
13211
13854
  }
13212
13855
 
13213
13856
  // src/services/SkillsService.ts
13214
13857
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
13215
13858
  var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
13216
13859
  function parseSkillFile(filePath, name, source) {
13217
- const raw = fs8.readFileSync(filePath, "utf8");
13860
+ const raw = fs9.readFileSync(filePath, "utf8");
13218
13861
  const frontmatter = {};
13219
13862
  let content = raw;
13220
13863
  if (raw.startsWith("---\n")) {
@@ -13243,23 +13886,23 @@ function parseSkillFile(filePath, name, source) {
13243
13886
  return { name, description: metadata.description, metadata, content, path: filePath, source };
13244
13887
  }
13245
13888
  function copyDirSync2(src, dest) {
13246
- fs8.mkdirSync(dest, { recursive: true });
13247
- const entries = fs8.readdirSync(src, { withFileTypes: true });
13889
+ fs9.mkdirSync(dest, { recursive: true });
13890
+ const entries = fs9.readdirSync(src, { withFileTypes: true });
13248
13891
  for (const entry of entries) {
13249
- const srcPath = path15.join(src, entry.name);
13250
- const destPath = path15.join(dest, entry.name);
13892
+ const srcPath = path18.join(src, entry.name);
13893
+ const destPath = path18.join(dest, entry.name);
13251
13894
  if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
13252
- else fs8.copyFileSync(srcPath, destPath);
13895
+ else fs9.copyFileSync(srcPath, destPath);
13253
13896
  }
13254
13897
  }
13255
13898
  function readDir(dir, source) {
13256
- if (!fs8.existsSync(dir)) return [];
13257
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
13899
+ if (!fs9.existsSync(dir)) return [];
13900
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
13258
13901
  const skills = [];
13259
13902
  for (const entry of entries) {
13260
13903
  if (!entry.isDirectory()) continue;
13261
- const skillFile = path15.join(dir, entry.name, "SKILL.md");
13262
- if (!fs8.existsSync(skillFile)) continue;
13904
+ const skillFile = path18.join(dir, entry.name, "SKILL.md");
13905
+ if (!fs9.existsSync(skillFile)) continue;
13263
13906
  const parsed = parseSkillFile(skillFile, entry.name, source);
13264
13907
  if (parsed) skills.push(parsed);
13265
13908
  }
@@ -13271,22 +13914,22 @@ var SkillsService = class {
13271
13914
  }
13272
13915
  workspaceRoot;
13273
13916
  localDir() {
13274
- return this.workspaceRoot ? path15.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
13917
+ return this.workspaceRoot ? path18.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
13275
13918
  }
13276
13919
  globalDir() {
13277
- return path15.join(globalDataDir(), "skills");
13920
+ return path18.join(globalDataDir(), "skills");
13278
13921
  }
13279
13922
  seed(name) {
13280
- const dest = path15.join(this.globalDir(), name);
13281
- if (fs8.existsSync(dest)) return false;
13923
+ const dest = path18.join(this.globalDir(), name);
13924
+ if (fs9.existsSync(dest)) return false;
13282
13925
  let srcDir;
13283
13926
  try {
13284
- srcDir = path15.join(builtinSkillsDir(), name);
13927
+ srcDir = path18.join(builtinSkillsDir(), name);
13285
13928
  } catch (err) {
13286
13929
  console.warn(`Could not locate built-in skills dir: ${String(err)}`);
13287
13930
  return false;
13288
13931
  }
13289
- if (!fs8.existsSync(srcDir)) {
13932
+ if (!fs9.existsSync(srcDir)) {
13290
13933
  console.warn(`Built-in skill not found in package: ${name}`);
13291
13934
  return false;
13292
13935
  }
@@ -13300,14 +13943,14 @@ var SkillsService = class {
13300
13943
  isUnmodifiedRetiredSeed(dir, expectedName) {
13301
13944
  let entries;
13302
13945
  try {
13303
- entries = fs8.readdirSync(dir, { withFileTypes: true });
13946
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
13304
13947
  } catch {
13305
13948
  return false;
13306
13949
  }
13307
13950
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
13308
13951
  let raw;
13309
13952
  try {
13310
- raw = fs8.readFileSync(path15.join(dir, "SKILL.md"), "utf8");
13953
+ raw = fs9.readFileSync(path18.join(dir, "SKILL.md"), "utf8");
13311
13954
  } catch {
13312
13955
  return false;
13313
13956
  }
@@ -13324,11 +13967,11 @@ var SkillsService = class {
13324
13967
  }
13325
13968
  pruneRetired() {
13326
13969
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
13327
- const dest = path15.join(this.globalDir(), name);
13328
- if (!fs8.existsSync(dest)) continue;
13970
+ const dest = path18.join(this.globalDir(), name);
13971
+ if (!fs9.existsSync(dest)) continue;
13329
13972
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
13330
13973
  try {
13331
- fs8.rmSync(dest, { recursive: true, force: true });
13974
+ fs9.rmSync(dest, { recursive: true, force: true });
13332
13975
  } catch (err) {
13333
13976
  console.warn(`Could not prune retired skill ${name}: ${String(err)}`);
13334
13977
  }
@@ -13339,11 +13982,11 @@ var SkillsService = class {
13339
13982
  if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
13340
13983
  const localDir = this.localDir();
13341
13984
  if (localDir) {
13342
- const localFile = path15.join(localDir, name, "SKILL.md");
13343
- if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
13985
+ const localFile = path18.join(localDir, name, "SKILL.md");
13986
+ if (fs9.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
13344
13987
  }
13345
- const globalFile = path15.join(this.globalDir(), name, "SKILL.md");
13346
- if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
13988
+ const globalFile = path18.join(this.globalDir(), name, "SKILL.md");
13989
+ if (fs9.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
13347
13990
  return void 0;
13348
13991
  }
13349
13992
  listSkills() {
@@ -13370,8 +14013,8 @@ function createSkillsService(workspaceRoot) {
13370
14013
  }
13371
14014
 
13372
14015
  // src/utils/sessionStore.ts
13373
- import * as fs9 from "fs";
13374
- import * as path16 from "path";
14016
+ import * as fs10 from "fs";
14017
+ import * as path19 from "path";
13375
14018
 
13376
14019
  // src/utils/sessionId.ts
13377
14020
  import { randomBytes as randomBytes2 } from "crypto";
@@ -13382,7 +14025,7 @@ function mintSessionId() {
13382
14025
  // src/utils/sessionStore.ts
13383
14026
  var SESSIONS_SUBDIR = "sessions";
13384
14027
  function getSessionsDir(baseDir) {
13385
- return path16.join(getStateDir(baseDir), SESSIONS_SUBDIR);
14028
+ return path19.join(getStateDir(baseDir), SESSIONS_SUBDIR);
13386
14029
  }
13387
14030
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
13388
14031
  function hasScalarRunner(parsed) {
@@ -13404,11 +14047,11 @@ function legacySessionFilename(session) {
13404
14047
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
13405
14048
  }
13406
14049
  function dropLegacyFile(sessionsDir, meta) {
13407
- const legacy = path16.join(sessionsDir, legacySessionFilename(meta));
13408
- if (legacy === path16.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
14050
+ const legacy = path19.join(sessionsDir, legacySessionFilename(meta));
14051
+ if (legacy === path19.join(sessionsDir, sessionFilename(meta)) || !fs10.existsSync(legacy)) return;
13409
14052
  try {
13410
- const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
13411
- if (held === meta.id) fs9.unlinkSync(legacy);
14053
+ const held = JSON.parse(fs10.readFileSync(legacy, "utf-8")).meta?.id;
14054
+ if (held === meta.id) fs10.unlinkSync(legacy);
13412
14055
  } catch {
13413
14056
  }
13414
14057
  }
@@ -13431,18 +14074,18 @@ function saveSession(plan, goal, baseDir, id) {
13431
14074
  plan: JSON.parse(JSON.stringify(plan))
13432
14075
  };
13433
14076
  const filename = sessionFilename(meta);
13434
- const filePath = path16.join(sessionsDir, filename);
13435
- fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
14077
+ const filePath = path19.join(sessionsDir, filename);
14078
+ fs10.writeFileSync(filePath, JSON.stringify(session, null, 2));
13436
14079
  dropLegacyFile(sessionsDir, meta);
13437
14080
  return meta;
13438
14081
  }
13439
14082
  function listSessions(baseDir, logger = defaultLogger) {
13440
14083
  const sessionsDir = getSessionsDir(baseDir);
13441
- if (!fs9.existsSync(sessionsDir)) return [];
13442
- const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
14084
+ if (!fs10.existsSync(sessionsDir)) return [];
14085
+ const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
13443
14086
  return files.map((f) => {
13444
14087
  try {
13445
- const raw = fs9.readFileSync(path16.join(sessionsDir, f), "utf-8");
14088
+ const raw = fs10.readFileSync(path19.join(sessionsDir, f), "utf-8");
13446
14089
  const parsed = JSON.parse(raw);
13447
14090
  if (hasScalarRunner(parsed)) {
13448
14091
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -13457,10 +14100,10 @@ function listSessions(baseDir, logger = defaultLogger) {
13457
14100
  }).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
13458
14101
  }
13459
14102
  function findSessionFile(sessionsDir, sessionId, logger) {
13460
- const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
14103
+ const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
13461
14104
  for (const f of files) {
13462
14105
  try {
13463
- const raw = fs9.readFileSync(path16.join(sessionsDir, f), "utf-8");
14106
+ const raw = fs10.readFileSync(path19.join(sessionsDir, f), "utf-8");
13464
14107
  const session = JSON.parse(raw);
13465
14108
  if (session.meta.id === sessionId) return f;
13466
14109
  } catch (err) {
@@ -13471,11 +14114,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
13471
14114
  }
13472
14115
  function loadSession(sessionId, baseDir, logger = defaultLogger) {
13473
14116
  const sessionsDir = getSessionsDir(baseDir);
13474
- if (!fs9.existsSync(sessionsDir)) return null;
14117
+ if (!fs10.existsSync(sessionsDir)) return null;
13475
14118
  try {
13476
14119
  const file = findSessionFile(sessionsDir, sessionId, logger);
13477
14120
  if (file) {
13478
- const raw = fs9.readFileSync(path16.join(sessionsDir, file), "utf-8");
14121
+ const raw = fs10.readFileSync(path19.join(sessionsDir, file), "utf-8");
13479
14122
  const parsed = JSON.parse(raw);
13480
14123
  if (hasScalarRunner(parsed)) {
13481
14124
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -13506,11 +14149,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
13506
14149
  }
13507
14150
  function deleteSession(sessionId, baseDir, logger = defaultLogger) {
13508
14151
  const sessionsDir = getSessionsDir(baseDir);
13509
- if (!fs9.existsSync(sessionsDir)) return false;
14152
+ if (!fs10.existsSync(sessionsDir)) return false;
13510
14153
  try {
13511
14154
  const file = findSessionFile(sessionsDir, sessionId, logger);
13512
14155
  if (file) {
13513
- fs9.unlinkSync(path16.join(sessionsDir, file));
14156
+ fs10.unlinkSync(path19.join(sessionsDir, file));
13514
14157
  return true;
13515
14158
  }
13516
14159
  } catch (err) {
@@ -14042,26 +14685,30 @@ var Session = class {
14042
14685
  */
14043
14686
  forkConversation() {
14044
14687
  if (!this.plan) throw new ConversationEditError("No planning conversation to fork");
14045
- const dialogue = this.conversation.clone();
14046
- const sessionId = mintSessionId();
14047
- const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
14048
- saveSession(plan, this.goal, this.workspace, sessionId);
14049
- return { sessionId, goal: this.goal, workspace: this.workspace };
14688
+ return this.saveFork(this.conversation.clone());
14050
14689
  }
14051
14690
  /**
14052
- * Cut the conversation back to just before a user message (a transcript
14053
- * position from {@link rewindTargets}). Conversation only: the task list,
14054
- * and any run executing it, carry on as they are. The planner's live context
14055
- * is dropped, so the next message replays from the shortened transcript.
14691
+ * Fork the conversation from just before a user message (a transcript
14692
+ * position from {@link rewindTargets}): the fork holds everything said
14693
+ * before it and the current task list, like {@link forkConversation}, and
14694
+ * this session keeps its whole history. The message's full text comes back
14695
+ * so a surface can offer it for resending.
14056
14696
  */
14057
14697
  rewindConversation(userMessageIndex) {
14058
14698
  if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
14059
- return this.conversation.rewind(userMessageIndex);
14699
+ const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
14700
+ return { ...this.saveFork(dialogue), rewoundMessage };
14701
+ }
14702
+ saveFork(dialogue) {
14703
+ const sessionId = mintSessionId();
14704
+ const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
14705
+ saveSession(plan, this.goal, this.workspace, sessionId);
14706
+ return { sessionId, goal: this.goal, workspace: this.workspace };
14060
14707
  }
14061
14708
  /**
14062
14709
  * Condense the conversation on the user's say-so: a hidden planner turn
14063
14710
  * summarises it, and the summary replaces everything but the last two
14064
- * exchanges. Conversation only, like a rewind — the tasks, and any run
14711
+ * exchanges. Conversation only — the tasks, and any run
14065
14712
  * executing them, are untouched. Atomic: a failed or stopped turn changes
14066
14713
  * nothing.
14067
14714
  */
@@ -14187,6 +14834,10 @@ var Session = class {
14187
14834
  }
14188
14835
  await this.orchestrator.runTask(taskId);
14189
14836
  }
14837
+ /** Start whatever the scheduler can now fit — after the parallel limit was raised mid-run, say. */
14838
+ async reschedule() {
14839
+ await this.orchestrator.tick();
14840
+ }
14190
14841
  async retryTask(taskId) {
14191
14842
  await this.orchestrator.retryTask(taskId);
14192
14843
  this.persist();
@@ -14334,8 +14985,9 @@ var Session = class {
14334
14985
  * The opt-in way through a merge conflict: add an AI task, on the conflicted
14335
14986
  * task's own runner and model, that merges its branch by hand in a worktree
14336
14987
  * of its own. When that task lands, the conflicted one lands through it (see
14337
- * {@link TaskOrchestrator.linkConflictResolver}). Never automatic — nothing
14338
- * but this call adds it.
14988
+ * {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
14989
+ * conflict is a repair, a new attempt of the same task (ADR-0015); a task is
14990
+ * only ever added to the plan by this call.
14339
14991
  */
14340
14992
  async resolveConflictAsTask(taskId) {
14341
14993
  if (!this.plan) return null;
@@ -15066,7 +15718,7 @@ var HeadlessRunner = class extends AbstractRunner {
15066
15718
  return {
15067
15719
  launch: await planDirectLaunch(command, args, this.launchDeps),
15068
15720
  resolvedPath,
15069
- env: invocation.env,
15721
+ env: { ...opts.env, ...invocation.env },
15070
15722
  pty,
15071
15723
  submitPromptKey: invocation.submitPromptKey
15072
15724
  };
@@ -15085,11 +15737,11 @@ var HeadlessRunner = class extends AbstractRunner {
15085
15737
  };
15086
15738
 
15087
15739
  // src/services/TmuxRunner.ts
15088
- import { execFile as execFile3 } from "child_process";
15740
+ import { execFile as execFile4 } from "child_process";
15089
15741
  import { promisify as promisify5 } from "util";
15090
15742
  import { tmpdir } from "os";
15091
- import { join as join15 } from "path";
15092
- import { existsSync as existsSync10, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
15743
+ import { join as join17 } from "path";
15744
+ import { existsSync as existsSync11, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
15093
15745
 
15094
15746
  // src/utils/tmux.ts
15095
15747
  import { execSync as execSync2 } from "child_process";
@@ -15140,7 +15792,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
15140
15792
 
15141
15793
  // src/services/TmuxRunner.ts
15142
15794
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
15143
- var execFileAsync2 = promisify5(execFile3);
15795
+ var execFileAsync2 = promisify5(execFile4);
15144
15796
  var defaultExecFile3 = async (command, args) => {
15145
15797
  const { stdout, stderr } = await execFileAsync2(command, args);
15146
15798
  return { stdout: String(stdout), stderr: String(stderr) };
@@ -15196,7 +15848,7 @@ var TmuxSession = class extends AbstractTerminalSession {
15196
15848
  if (this.exited) return;
15197
15849
  let content;
15198
15850
  try {
15199
- content = existsSync10(this.logPath) ? readFileSync6(this.logPath, "utf8") : "";
15851
+ content = existsSync11(this.logPath) ? readFileSync8(this.logPath, "utf8") : "";
15200
15852
  } catch {
15201
15853
  return;
15202
15854
  }
@@ -15375,10 +16027,10 @@ var TmuxRunner = class extends AbstractRunner {
15375
16027
  windowName,
15376
16028
  this.execFileImpl,
15377
16029
  this.pollIntervalMs,
15378
- join15(this.logDir, `${id}.log`),
16030
+ join17(this.logDir, `${id}.log`),
15379
16031
  this.socket
15380
16032
  );
15381
- invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
16033
+ invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
15382
16034
  await session.start(invocation.command, invocation.args, opts.cwd, invocation.env);
15383
16035
  if (invocation.submitPromptKey) session.write("\r");
15384
16036
  this.registerSession(id, session);
@@ -15388,8 +16040,8 @@ var TmuxRunner = class extends AbstractRunner {
15388
16040
 
15389
16041
  // src/utils/daemonToken.ts
15390
16042
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
15391
- import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeSync } from "fs";
15392
- import { join as join16 } from "path";
16043
+ import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeSync } from "fs";
16044
+ import { join as join18 } from "path";
15393
16045
  var BEARER_PREFIX = "bearer ";
15394
16046
  var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
15395
16047
  var DAEMON_SUBPROTOCOL = "ordewell.v1";
@@ -15397,12 +16049,12 @@ function configDir() {
15397
16049
  return globalDataDir();
15398
16050
  }
15399
16051
  function daemonTokenPath(port) {
15400
- return join16(configDir(), `server-${port}.token`);
16052
+ return join18(configDir(), `server-${port}.token`);
15401
16053
  }
15402
16054
  function mintDaemonToken(port) {
15403
16055
  const token = randomBytes3(32).toString("base64url");
15404
16056
  const file = daemonTokenPath(port);
15405
- mkdirSync8(configDir(), { recursive: true });
16057
+ mkdirSync9(configDir(), { recursive: true });
15406
16058
  try {
15407
16059
  unlinkSync3(file);
15408
16060
  } catch {
@@ -15417,7 +16069,7 @@ function mintDaemonToken(port) {
15417
16069
  }
15418
16070
  function readDaemonToken(port) {
15419
16071
  try {
15420
- const token = readFileSync7(daemonTokenPath(port), "utf8").trim();
16072
+ const token = readFileSync9(daemonTokenPath(port), "utf8").trim();
15421
16073
  return token === "" ? void 0 : token;
15422
16074
  } catch {
15423
16075
  return void 0;
@@ -15458,18 +16110,18 @@ function tokensMatch(presented, expected) {
15458
16110
  }
15459
16111
 
15460
16112
  // src/plugins/RunnerRegistry.ts
15461
- import * as fs11 from "fs";
15462
- import * as path19 from "path";
15463
- import * as os5 from "os";
16113
+ import * as fs12 from "fs";
16114
+ import * as path22 from "path";
16115
+ import * as os6 from "os";
15464
16116
  import { execFileSync } from "child_process";
15465
16117
  import { randomBytes as randomBytes4 } from "crypto";
15466
16118
 
15467
16119
  // src/plugins/FsPluginStore.ts
15468
- import * as fs10 from "fs";
15469
- import * as path18 from "path";
16120
+ import * as fs11 from "fs";
16121
+ import * as path21 from "path";
15470
16122
 
15471
16123
  // src/plugins/pluginNames.ts
15472
- import * as path17 from "path";
16124
+ import * as path20 from "path";
15473
16125
  var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
15474
16126
  var MAX_PLUGIN_NAME_LENGTH = 64;
15475
16127
  function isPlainPluginName(name) {
@@ -15484,10 +16136,10 @@ function assertPlainPluginName(name) {
15484
16136
  }
15485
16137
  function resolvePluginInstallDir(pluginsDir, name) {
15486
16138
  assertPlainPluginName(name);
15487
- const root = path17.resolve(pluginsDir);
15488
- const dest = path17.resolve(root, name);
15489
- const relative4 = path17.relative(root, dest);
15490
- if (relative4 !== name || relative4 === "" || path17.isAbsolute(relative4)) {
16139
+ const root = path20.resolve(pluginsDir);
16140
+ const dest = path20.resolve(root, name);
16141
+ const relative5 = path20.relative(root, dest);
16142
+ if (relative5 !== name || relative5 === "" || path20.isAbsolute(relative5)) {
15491
16143
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
15492
16144
  }
15493
16145
  return dest;
@@ -15502,13 +16154,13 @@ function isValidManifest(obj) {
15502
16154
 
15503
16155
  // src/plugins/FsPluginStore.ts
15504
16156
  function userPluginsDir() {
15505
- return path18.join(globalDataDir(), "plugins");
16157
+ return path21.join(globalDataDir(), "plugins");
15506
16158
  }
15507
16159
  function loadManifestFromDir(dir) {
15508
- const manifestPath = path18.join(dir, "manifest.json");
15509
- if (!fs10.existsSync(manifestPath)) return null;
16160
+ const manifestPath = path21.join(dir, "manifest.json");
16161
+ if (!fs11.existsSync(manifestPath)) return null;
15510
16162
  try {
15511
- const raw = fs10.readFileSync(manifestPath, "utf8");
16163
+ const raw = fs11.readFileSync(manifestPath, "utf8");
15512
16164
  const parsed = JSON.parse(raw);
15513
16165
  if (!isValidManifest(parsed)) return null;
15514
16166
  return parsed;
@@ -15518,29 +16170,29 @@ function loadManifestFromDir(dir) {
15518
16170
  }
15519
16171
  function copyDirSync3(src, dest) {
15520
16172
  ensureDir(dest);
15521
- const entries = fs10.readdirSync(src, { withFileTypes: true });
16173
+ const entries = fs11.readdirSync(src, { withFileTypes: true });
15522
16174
  for (const entry of entries) {
15523
- const srcPath = path18.join(src, entry.name);
15524
- const destPath = path18.join(dest, entry.name);
16175
+ const srcPath = path21.join(src, entry.name);
16176
+ const destPath = path21.join(dest, entry.name);
15525
16177
  if (entry.isDirectory()) {
15526
16178
  copyDirSync3(srcPath, destPath);
15527
16179
  } else {
15528
- fs10.copyFileSync(srcPath, destPath);
16180
+ fs11.copyFileSync(srcPath, destPath);
15529
16181
  }
15530
16182
  }
15531
16183
  }
15532
16184
  function rmDirSync(dir) {
15533
- if (!fs10.existsSync(dir)) return;
15534
- const entries = fs10.readdirSync(dir, { withFileTypes: true });
16185
+ if (!fs11.existsSync(dir)) return;
16186
+ const entries = fs11.readdirSync(dir, { withFileTypes: true });
15535
16187
  for (const entry of entries) {
15536
- const p = path18.join(dir, entry.name);
16188
+ const p = path21.join(dir, entry.name);
15537
16189
  if (entry.isDirectory()) {
15538
16190
  rmDirSync(p);
15539
16191
  } else {
15540
- fs10.unlinkSync(p);
16192
+ fs11.unlinkSync(p);
15541
16193
  }
15542
16194
  }
15543
- fs10.rmdirSync(dir);
16195
+ fs11.rmdirSync(dir);
15544
16196
  }
15545
16197
  var FsPluginStore = class {
15546
16198
  getUserPluginsDir() {
@@ -15548,10 +16200,10 @@ var FsPluginStore = class {
15548
16200
  }
15549
16201
  listUserPluginDirs() {
15550
16202
  const dir = userPluginsDir();
15551
- if (!fs10.existsSync(dir)) return [];
16203
+ if (!fs11.existsSync(dir)) return [];
15552
16204
  let entries;
15553
16205
  try {
15554
- entries = fs10.readdirSync(dir, { withFileTypes: true });
16206
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
15555
16207
  } catch {
15556
16208
  return [];
15557
16209
  }
@@ -15559,7 +16211,7 @@ var FsPluginStore = class {
15559
16211
  }
15560
16212
  listDir(dir) {
15561
16213
  try {
15562
- return fs10.readdirSync(dir);
16214
+ return fs11.readdirSync(dir);
15563
16215
  } catch {
15564
16216
  return [];
15565
16217
  }
@@ -15577,20 +16229,20 @@ var FsPluginStore = class {
15577
16229
  ensureDir(dir);
15578
16230
  }
15579
16231
  writeFile(filePath, content) {
15580
- fs10.writeFileSync(filePath, content);
16232
+ fs11.writeFileSync(filePath, content);
15581
16233
  }
15582
16234
  readFile(filePath) {
15583
16235
  try {
15584
- return fs10.readFileSync(filePath, "utf8");
16236
+ return fs11.readFileSync(filePath, "utf8");
15585
16237
  } catch {
15586
16238
  return null;
15587
16239
  }
15588
16240
  }
15589
- dirExists(path22) {
15590
- return fs10.existsSync(path22) && fs10.statSync(path22).isDirectory();
16241
+ dirExists(path25) {
16242
+ return fs11.existsSync(path25) && fs11.statSync(path25).isDirectory();
15591
16243
  }
15592
- exists(path22) {
15593
- return fs10.existsSync(path22);
16244
+ exists(path25) {
16245
+ return fs11.existsSync(path25);
15594
16246
  }
15595
16247
  };
15596
16248
 
@@ -15617,7 +16269,15 @@ var CLAUDE_CODE_MANIFEST = {
15617
16269
  "{{/if}}",
15618
16270
  "{{prompt}}"
15619
16271
  ],
15620
- promptInArgs: true
16272
+ promptInArgs: true,
16273
+ // Interactive sessions show these even with --dangerously-skip-permissions:
16274
+ // a folder Claude Code has never been trusted in (a fresh clone, or a repo
16275
+ // only ever planned in headlessly), and the first Bypass Permissions run.
16276
+ blockingPrompts: [
16277
+ { phrase: "Is this a project you created or one you trust", asks: "whether to trust the task's folder" },
16278
+ { phrase: "Do you trust the files in this folder", asks: "whether to trust the task's folder" },
16279
+ { phrase: "running in Bypass Permissions mode", asks: "you to accept Bypass Permissions mode" }
16280
+ ]
15621
16281
  },
15622
16282
  features: {
15623
16283
  modelSelection: true,
@@ -16022,7 +16682,7 @@ var RunnerRegistry = class {
16022
16682
  return entry?.source === "builtin";
16023
16683
  }
16024
16684
  installFromPath(sourcePath) {
16025
- const absPath = path19.resolve(sourcePath);
16685
+ const absPath = path22.resolve(sourcePath);
16026
16686
  if (!this.store.dirExists(absPath)) {
16027
16687
  throw new Error(`Plugin source must be a directory: ${absPath}`);
16028
16688
  }
@@ -16034,7 +16694,7 @@ var RunnerRegistry = class {
16034
16694
  }
16035
16695
  installFromGit(url) {
16036
16696
  const validated = assertInstallablePluginUrl(url);
16037
- const tmpDir = path19.join(os5.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
16697
+ const tmpDir = path22.join(os6.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
16038
16698
  this.store.ensureDir(tmpDir);
16039
16699
  try {
16040
16700
  try {
@@ -16047,7 +16707,7 @@ var RunnerRegistry = class {
16047
16707
  const hasManifest = contents.some((f) => f === "manifest.json");
16048
16708
  let sourceDir = tmpDir;
16049
16709
  if (!hasManifest) {
16050
- const onlyEntry = contents.length === 1 ? path19.join(tmpDir, contents[0]) : void 0;
16710
+ const onlyEntry = contents.length === 1 ? path22.join(tmpDir, contents[0]) : void 0;
16051
16711
  if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
16052
16712
  throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
16053
16713
  }
@@ -16095,7 +16755,7 @@ var RunnerRegistry = class {
16095
16755
  }
16096
16756
  createSkeleton(name, outputDir) {
16097
16757
  assertPlainPluginName(name);
16098
- const dir = path19.resolve(outputDir, name);
16758
+ const dir = path22.resolve(outputDir, name);
16099
16759
  this.store.ensureDir(dir);
16100
16760
  const manifest = {
16101
16761
  name,
@@ -16127,8 +16787,8 @@ var RunnerRegistry = class {
16127
16787
  ]
16128
16788
  }
16129
16789
  };
16130
- this.store.writeFile(path19.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
16131
- const runSh = path19.join(dir, "run.sh");
16790
+ this.store.writeFile(path22.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
16791
+ const runSh = path22.join(dir, "run.sh");
16132
16792
  const scriptContent = `#!/usr/bin/env bash
16133
16793
  # Custom runner script for ${name}
16134
16794
  # Receives:
@@ -16154,30 +16814,30 @@ echo "Running ${name} with prompt: $prompt"
16154
16814
  };
16155
16815
  function fsChmodSync(fpath, mode) {
16156
16816
  try {
16157
- fs11.chmodSync(fpath, mode);
16817
+ fs12.chmodSync(fpath, mode);
16158
16818
  } catch {
16159
16819
  }
16160
16820
  }
16161
16821
 
16162
16822
  // src/utils/stateStore.ts
16163
- import * as fs12 from "fs";
16164
- import * as path20 from "path";
16823
+ import * as fs13 from "fs";
16824
+ import * as path23 from "path";
16165
16825
  var STATE_FILE = "state.json";
16166
16826
  function getStatePath(baseDir) {
16167
- return path20.join(getStateDir(baseDir), STATE_FILE);
16827
+ return path23.join(getStateDir(baseDir), STATE_FILE);
16168
16828
  }
16169
16829
  function saveState(plan, baseDir) {
16170
16830
  const statePath = getStatePath(baseDir ?? process.cwd());
16171
- ensureDir(path20.dirname(statePath));
16831
+ ensureDir(path23.dirname(statePath));
16172
16832
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
16173
- fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
16833
+ fs13.writeFileSync(statePath, JSON.stringify(plan, null, 2));
16174
16834
  }
16175
16835
  function loadState(baseDir, logger = defaultLogger) {
16176
16836
  const dir = baseDir ?? process.cwd();
16177
16837
  const statePath = getStatePath(dir);
16178
- if (!fs12.existsSync(statePath)) return null;
16838
+ if (!fs13.existsSync(statePath)) return null;
16179
16839
  try {
16180
- const raw = fs12.readFileSync(statePath, "utf-8");
16840
+ const raw = fs13.readFileSync(statePath, "utf-8");
16181
16841
  const parsed = JSON.parse(raw);
16182
16842
  if (!Array.isArray(parsed.runners)) {
16183
16843
  logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
@@ -16194,16 +16854,16 @@ function loadState(baseDir, logger = defaultLogger) {
16194
16854
  function clearState(baseDir) {
16195
16855
  const dir = baseDir ?? process.cwd();
16196
16856
  const statePath = getStatePath(dir);
16197
- if (fs12.existsSync(statePath)) fs12.unlinkSync(statePath);
16857
+ if (fs13.existsSync(statePath)) fs13.unlinkSync(statePath);
16198
16858
  }
16199
16859
  function stateExists(baseDir) {
16200
16860
  const dir = baseDir ?? process.cwd();
16201
- return fs12.existsSync(getStatePath(dir));
16861
+ return fs13.existsSync(getStatePath(dir));
16202
16862
  }
16203
16863
 
16204
16864
  // src/services/SettingsService.ts
16205
- import * as fs13 from "fs";
16206
- import * as path21 from "path";
16865
+ import * as fs14 from "fs";
16866
+ import * as path24 from "path";
16207
16867
  var DEFAULTS = {
16208
16868
  tdd: { enabled: true },
16209
16869
  verification: { enabled: false }
@@ -16211,7 +16871,7 @@ var DEFAULTS = {
16211
16871
  function getSettingsPath() {
16212
16872
  const override = process.env.ORDEWELL_SETTINGS_PATH;
16213
16873
  if (override && override.trim()) return override.trim();
16214
- return path21.join(globalDataDir(), "settings.json");
16874
+ return path24.join(globalDataDir(), "settings.json");
16215
16875
  }
16216
16876
  function readPlannerModels(raw) {
16217
16877
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -16244,7 +16904,7 @@ var SettingsService = class {
16244
16904
  }
16245
16905
  fileMtimeMs() {
16246
16906
  try {
16247
- return fs13.statSync(this.filePath).mtimeMs;
16907
+ return fs14.statSync(this.filePath).mtimeMs;
16248
16908
  } catch {
16249
16909
  return null;
16250
16910
  }
@@ -16316,8 +16976,8 @@ var SettingsService = class {
16316
16976
  load() {
16317
16977
  migrateOldConfigDir();
16318
16978
  try {
16319
- if (fs13.existsSync(this.filePath)) {
16320
- const raw = JSON.parse(fs13.readFileSync(this.filePath, "utf-8"));
16979
+ if (fs14.existsSync(this.filePath)) {
16980
+ const raw = JSON.parse(fs14.readFileSync(this.filePath, "utf-8"));
16321
16981
  const settings = {
16322
16982
  tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
16323
16983
  verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled }
@@ -16339,11 +16999,11 @@ var SettingsService = class {
16339
16999
  return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
16340
17000
  }
16341
17001
  persist() {
16342
- const dir = path21.dirname(this.filePath);
16343
- if (!fs13.existsSync(dir)) {
16344
- fs13.mkdirSync(dir, { recursive: true });
17002
+ const dir = path24.dirname(this.filePath);
17003
+ if (!fs14.existsSync(dir)) {
17004
+ fs14.mkdirSync(dir, { recursive: true });
16345
17005
  }
16346
- fs13.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
17006
+ fs14.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
16347
17007
  this.cachedMtimeMs = this.fileMtimeMs();
16348
17008
  }
16349
17009
  };
@@ -16409,6 +17069,7 @@ export {
16409
17069
  ConversationEditError,
16410
17070
  DAEMON_SUBPROTOCOL,
16411
17071
  DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
17072
+ DEFAULT_MAX_PARALLEL,
16412
17073
  DENY_ALL,
16413
17074
  EmbeddedNewlineError,
16414
17075
  EnvConfig,
@@ -16482,6 +17143,7 @@ export {
16482
17143
  augmentPromptWithPriorOutputs,
16483
17144
  augmentedPath,
16484
17145
  bearerHeaderValue,
17146
+ buildConflictRepairPrompt,
16485
17147
  buildConflictResolutionPrompt,
16486
17148
  buildConversationSystemPrompt,
16487
17149
  buildFallbackGrepArgs,
@@ -16501,6 +17163,7 @@ export {
16501
17163
  canMergeTasks,
16502
17164
  canSetDependencies,
16503
17165
  canSplitTask,
17166
+ capConflictFiles,
16504
17167
  checkDepsResolve,
16505
17168
  checkImmutableLog,
16506
17169
  checkInProgress,
@@ -16589,6 +17252,7 @@ export {
16589
17252
  modifyValidationFeedback,
16590
17253
  normalizeAgentArgs,
16591
17254
  normalizeGeminiModel,
17255
+ parseMaxParallel,
16592
17256
  parsePartialPlan,
16593
17257
  parsePlanJson,
16594
17258
  parseTaskOpsJson,