@ordewell/core 0.5.0 → 0.5.1

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-DXlJop_f.d.mts → ITerminalRunner-DCjsa2-I.d.mts} +6 -1
  2. package/dist/{ITerminalRunner-nupxhtPb.d.ts → ITerminalRunner-H1yT8LTy.d.ts} +6 -1
  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 +82 -57
  10. package/dist/index.d.ts +82 -57
  11. package/dist/index.js +658 -252
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +657 -264
  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;
@@ -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((resolve6) => {
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
+ resolve6(granted);
2781
2782
  };
2782
2783
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2783
2784
  timer.unref?.();
@@ -3475,6 +3476,10 @@ function normalizeGeminiModel(id) {
3475
3476
  function commaList(raw) {
3476
3477
  return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
3477
3478
  }
3479
+ function nonNegativeInt(raw, fallback) {
3480
+ const trimmed = (raw ?? "").trim();
3481
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
3482
+ }
3478
3483
  var BaseConfig = class {
3479
3484
  // --- Legacy named getters (backward compat) ---
3480
3485
  get openAiBaseUrl() {
@@ -3697,6 +3702,9 @@ var BaseConfig = class {
3697
3702
  get worktreeLinks() {
3698
3703
  return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
3699
3704
  }
3705
+ get conflictRepairAttempts() {
3706
+ return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
3707
+ }
3700
3708
  get approvalMode() {
3701
3709
  const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
3702
3710
  return raw === "allow" || raw === "deny" ? raw : "ask";
@@ -3756,13 +3764,17 @@ function landedNotice(landed) {
3756
3764
  if (landed.length === 0) return "Nothing was merged.";
3757
3765
  return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
3758
3766
  }
3759
- function describeMergeResult(result, branch, group) {
3767
+ function repairedNotice(repaired) {
3768
+ const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
3769
+ const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
3770
+ return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
3771
+ }
3772
+ function describeMergeResult(result, branch, group, repaired = []) {
3760
3773
  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
- };
3774
+ case "merged": {
3775
+ const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
3776
+ return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
3777
+ }
3766
3778
  case "blocked":
3767
3779
  return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
3768
3780
  case "conflict":
@@ -3783,7 +3795,7 @@ import { execFile, exec as exec2 } from "child_process";
3783
3795
  import { promisify as promisify2 } from "util";
3784
3796
  import { randomBytes } from "crypto";
3785
3797
  import * as fs4 from "fs";
3786
- import * as path7 from "path";
3798
+ import * as path8 from "path";
3787
3799
 
3788
3800
  // src/utils/shellPath.ts
3789
3801
  import { exec } from "child_process";
@@ -3812,28 +3824,28 @@ function posixBinDirs() {
3812
3824
  ];
3813
3825
  }
3814
3826
  function windowsBinDirs(env, home) {
3815
- const join21 = (...parts) => path4.win32.join(...parts);
3816
- const under = (base, ...parts) => base ? [join21(base, ...parts)] : [];
3827
+ const join22 = (...parts) => path4.win32.join(...parts);
3828
+ const under = (base, ...parts) => base ? [join22(base, ...parts)] : [];
3817
3829
  const appData = env.APPDATA;
3818
3830
  const localAppData = env.LOCALAPPDATA;
3819
3831
  const programData = env.ProgramData;
3820
3832
  return [
3821
3833
  // 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"),
3834
+ join22(home, ".local", "bin"),
3835
+ join22(home, ".opencode", "bin"),
3836
+ join22(home, ".claude", "bin"),
3837
+ join22(home, ".codex", "bin"),
3826
3838
  ...under(localAppData, "Programs", "claude"),
3827
3839
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
3828
3840
  // literal spelling is kept as a fallback for a host that does not export
3829
3841
  // APPDATA (a service account, a stripped CI container).
3830
3842
  ...under(appData, "npm"),
3831
- join21(home, "AppData", "Roaming", "npm"),
3843
+ join22(home, "AppData", "Roaming", "npm"),
3832
3844
  ...under(localAppData, "pnpm"),
3833
3845
  ...under(localAppData, "Yarn", "bin"),
3834
- join21(home, ".bun", "bin"),
3846
+ join22(home, ".bun", "bin"),
3835
3847
  // Windows package managers.
3836
- join21(home, "scoop", "shims"),
3848
+ join22(home, "scoop", "shims"),
3837
3849
  ...under(env.SCOOP, "shims"),
3838
3850
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
3839
3851
  ...under(programData, "chocolatey", "bin"),
@@ -3843,7 +3855,7 @@ function windowsBinDirs(env, home) {
3843
3855
  ...under(localAppData, "Microsoft", "WindowsApps"),
3844
3856
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
3845
3857
  ...under(localAppData, "Volta", "bin"),
3846
- join21(home, ".volta", "bin")
3858
+ join22(home, ".volta", "bin")
3847
3859
  ];
3848
3860
  }
3849
3861
  function wellKnownBinDirs(deps = {}) {
@@ -3947,6 +3959,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
3947
3959
 
3948
3960
  // src/services/worktreeLink.ts
3949
3961
  import * as fs3 from "fs";
3962
+ import * as path7 from "path";
3950
3963
  function linkPath(source, target, platform, hardLink = fs3.linkSync) {
3951
3964
  const isDir = fs3.statSync(source).isDirectory();
3952
3965
  if (platform !== "win32") {
@@ -3965,6 +3978,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
3965
3978
  return "copy";
3966
3979
  }
3967
3980
  }
3981
+ function mirrorDir(source, target, opts) {
3982
+ const copied = [];
3983
+ const links = [];
3984
+ const mirror = (from, to, prefix, top) => {
3985
+ fs3.mkdirSync(to, { recursive: true });
3986
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
3987
+ const name = path7.join(prefix, entry.name);
3988
+ const at = { from: path7.join(from, entry.name), to: path7.join(to, entry.name), name };
3989
+ if (entry.isSymbolicLink()) links.push(at);
3990
+ else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
3991
+ else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
3992
+ }
3993
+ };
3994
+ mirror(source, target, "", true);
3995
+ let pending = links;
3996
+ for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
3997
+ before = pending.length;
3998
+ pending = pending.filter((link) => {
3999
+ const kind = relink(link.from, link.to, opts);
4000
+ if (kind === "copy") copied.push(link.name);
4001
+ return kind === null;
4002
+ });
4003
+ }
4004
+ return copied;
4005
+ }
4006
+ function relink(from, to, opts) {
4007
+ const text = fs3.readlinkSync(from);
4008
+ const pointee = path7.isAbsolute(text) ? rebased(text, opts) : text;
4009
+ if (opts.platform !== "win32") {
4010
+ if (!fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) && fs3.existsSync(path7.resolve(path7.dirname(from), pointee))) {
4011
+ fs3.symlinkSync(rebased(path7.resolve(path7.dirname(from), pointee), opts), to);
4012
+ return "symlink";
4013
+ }
4014
+ fs3.symlinkSync(pointee, to);
4015
+ return "symlink";
4016
+ }
4017
+ const resolved = fs3.existsSync(path7.resolve(path7.dirname(to), pointee)) ? path7.resolve(path7.dirname(to), pointee) : rebased(path7.resolve(path7.dirname(from), pointee), opts);
4018
+ try {
4019
+ fs3.statSync(resolved);
4020
+ } catch {
4021
+ return null;
4022
+ }
4023
+ return linkPath(resolved, to, opts.platform, opts.hardLink);
4024
+ }
4025
+ function rebased(pointee, opts) {
4026
+ const rel = path7.relative(opts.from, pointee);
4027
+ const inside = rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4028
+ return inside && !rel.split(/[\\/]/).includes("node_modules") ? path7.join(opts.to, rel) : pointee;
4029
+ }
3968
4030
 
3969
4031
  // src/services/GitWorktreeIsolation.ts
3970
4032
  var execFileAsync = promisify2(execFile);
@@ -3979,15 +4041,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
3979
4041
  var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
3980
4042
  var isEnvFile = (name) => name.startsWith(".env");
3981
4043
  var INTEGRATION_DIR = "integration";
4044
+ var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
4045
+ var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
3982
4046
  var NESTED_REPO_DEPTH = 2;
3983
4047
  var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
3984
4048
  var GITLINK_MODE = "160000";
3985
4049
  function isInside(parent, child) {
3986
- const rel = path7.relative(parent, child);
3987
- return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4050
+ const rel = path8.relative(parent, child);
4051
+ return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
3988
4052
  }
3989
4053
  function hasGitEntry(dir) {
3990
- return fs4.existsSync(path7.join(dir, ".git"));
4054
+ return fs4.existsSync(path8.join(dir, ".git"));
3991
4055
  }
3992
4056
  function firstLine(text) {
3993
4057
  return text.split(/\r?\n/, 1)[0].trim();
@@ -4003,12 +4067,20 @@ function statusPaths(porcelain) {
4003
4067
  }
4004
4068
  return paths;
4005
4069
  }
4070
+ function leftoverMarkerFiles(check) {
4071
+ const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
4072
+ return [...new Set(files)];
4073
+ }
4074
+ function settleStatus(record, status) {
4075
+ record.status = status;
4076
+ delete record.repairBase;
4077
+ }
4006
4078
  function sameDir(a, b) {
4007
4079
  const real = (p) => {
4008
4080
  try {
4009
4081
  return fs4.realpathSync.native(p);
4010
4082
  } catch {
4011
- return path7.resolve(p);
4083
+ return path8.resolve(p);
4012
4084
  }
4013
4085
  };
4014
4086
  return real(a) === real(b);
@@ -4042,8 +4114,8 @@ function inactive(reason, repos) {
4042
4114
  }
4043
4115
  function groupPathOf(listed) {
4044
4116
  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(/\/+$/, "");
4117
+ if (path8.posix.isAbsolute(slashed) || path8.win32.isAbsolute(slashed)) return null;
4118
+ const rel = path8.posix.normalize(slashed).replace(/\/+$/, "");
4047
4119
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
4048
4120
  }
4049
4121
  function matchLinks(root, patterns) {
@@ -4059,9 +4131,26 @@ function matchLinks(root, patterns) {
4059
4131
  }
4060
4132
  function segmentMatches(root, base, segment) {
4061
4133
  const under = (name) => base ? `${base}/${name}` : name;
4062
- if (!/[*?]/.test(segment)) return lexists(path7.join(root, base, segment)) ? [under(segment)] : [];
4134
+ if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
4063
4135
  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);
4136
+ return listDir(path8.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4137
+ }
4138
+ function installDirs(root) {
4139
+ const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
4140
+ return ["node_modules", ...packages].filter((dir) => lexists(path8.join(root, dir)));
4141
+ }
4142
+ function workspaceGlobs(root) {
4143
+ let manifest;
4144
+ try {
4145
+ manifest = JSON.parse(fs4.readFileSync(path8.join(root, "package.json"), "utf8"));
4146
+ } catch {
4147
+ return [];
4148
+ }
4149
+ if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
4150
+ const { workspaces } = manifest;
4151
+ const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
4152
+ if (!Array.isArray(listed)) return [];
4153
+ return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
4065
4154
  }
4066
4155
  var GitWorktreeIsolation = class {
4067
4156
  constructor(deps) {
@@ -4115,7 +4204,7 @@ var GitWorktreeIsolation = class {
4115
4204
  groupPaths(workspaceRoot) {
4116
4205
  const listed = this.deps.config.workspaceRepos;
4117
4206
  if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
4118
- const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path7.join(workspaceRoot, rel)));
4207
+ const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
4119
4208
  return [...new Set(paths)].sort();
4120
4209
  }
4121
4210
  async committedRepos(workspaceRoot, paths) {
@@ -4131,7 +4220,7 @@ var GitWorktreeIsolation = class {
4131
4220
  return status.ok ? status.stdout.trim() !== "" : null;
4132
4221
  }
4133
4222
  reposDirectlyIn(dir) {
4134
- return this.childDirs(dir).filter((name) => hasGitEntry(path7.join(dir, name)));
4223
+ return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
4135
4224
  }
4136
4225
  childDirs(dir) {
4137
4226
  try {
@@ -4148,9 +4237,9 @@ var GitWorktreeIsolation = class {
4148
4237
  async nestedRepos(workspaceRoot, toplevel) {
4149
4238
  const found = [];
4150
4239
  const walk = (rel, depth) => {
4151
- for (const name of this.childDirs(path7.join(workspaceRoot, rel))) {
4240
+ for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
4152
4241
  const child = rel ? `${rel}/${name}` : name;
4153
- if (hasGitEntry(path7.join(workspaceRoot, child))) found.push(child);
4242
+ if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
4154
4243
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
4155
4244
  }
4156
4245
  };
@@ -4171,7 +4260,7 @@ var GitWorktreeIsolation = class {
4171
4260
  const [meta, file] = entry.split(" ");
4172
4261
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
4173
4262
  }
4174
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path7.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4263
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4175
4264
  for (const entry of declared.stdout.split("\0")) {
4176
4265
  const value = entry.split("\n")[1];
4177
4266
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -4227,7 +4316,7 @@ var GitWorktreeIsolation = class {
4227
4316
  async acceptsWorktree(run, repo) {
4228
4317
  const dir = this.integrationDir(run, repo);
4229
4318
  ensureStateDirIgnored(run.workspaceRoot);
4230
- fs4.mkdirSync(path7.dirname(dir), { recursive: true });
4319
+ fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4231
4320
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
4232
4321
  await this.removeWorktreeDir(run, repo, dir, []);
4233
4322
  return added.ok;
@@ -4242,12 +4331,12 @@ var GitWorktreeIsolation = class {
4242
4331
  if (isolated.includes(SELF_REPO)) return [];
4243
4332
  const shared = [];
4244
4333
  const walk = (rel) => {
4245
- for (const name of listDir(path7.join(workspaceRoot, rel))) {
4334
+ for (const name of listDir(path8.join(workspaceRoot, rel))) {
4246
4335
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
4247
4336
  const child = rel ? `${rel}/${name}` : name;
4248
4337
  if (isolated.includes(child)) continue;
4249
4338
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
4250
- else if (resolves(path7.join(workspaceRoot, child))) shared.push(child);
4339
+ else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
4251
4340
  }
4252
4341
  };
4253
4342
  walk("");
@@ -4264,17 +4353,17 @@ var GitWorktreeIsolation = class {
4264
4353
  const copied = [];
4265
4354
  try {
4266
4355
  for (const repo of run.repos) {
4267
- const worktree = path7.join(dir, repo.path);
4356
+ const worktree = path8.join(dir, repo.path);
4268
4357
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
4269
- fs4.mkdirSync(path7.dirname(worktree), { recursive: true });
4358
+ fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
4270
4359
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
4271
4360
  record.repos[repo.path] = { worktree, linked: [] };
4272
4361
  }
4273
4362
  run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
4274
4363
  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);
4364
+ const target = path8.join(dir, rel);
4365
+ fs4.mkdirSync(path8.dirname(target), { recursive: true });
4366
+ if (linkPath(path8.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
4278
4367
  }
4279
4368
  for (const repo of run.repos) {
4280
4369
  const entry = record.repos[repo.path];
@@ -4282,7 +4371,7 @@ var GitWorktreeIsolation = class {
4282
4371
  if (repo.path === SELF_REPO) cwd = inRepo;
4283
4372
  const boot = await this.bootstrap(repo, inRepo);
4284
4373
  entry.linked = boot.linked;
4285
- copied.push(...boot.copied.map((name) => path7.posix.join(repo.path, name)));
4374
+ copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
4286
4375
  }
4287
4376
  } catch (err) {
4288
4377
  record.status = "failed";
@@ -4293,6 +4382,53 @@ var GitWorktreeIsolation = class {
4293
4382
  return { cwd, branch, copied };
4294
4383
  });
4295
4384
  }
4385
+ reopen(task, run) {
4386
+ return this.admin(run.workspaceRoot, async () => {
4387
+ const record = run.tasks[task.id];
4388
+ if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
4389
+ let cwd = record.workspace;
4390
+ const repairBase = {};
4391
+ for (const repo of run.repos) {
4392
+ const entry = record.repos[repo.path];
4393
+ if (!entry) continue;
4394
+ if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
4395
+ if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
4396
+ if (entry.changed || repo.path === record.conflictRepo) {
4397
+ repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
4398
+ }
4399
+ }
4400
+ const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
4401
+ record.repairs = (record.repairs ?? 0) + 1;
4402
+ record.repairBase = repairBase;
4403
+ record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
4404
+ record.status = "repairing";
4405
+ return { cwd, branch: record.branch, copied: [] };
4406
+ });
4407
+ }
4408
+ async verifyRepair(task, run) {
4409
+ const record = run.tasks[task.id];
4410
+ if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
4411
+ for (const repo of run.repos) {
4412
+ const entry = record.repos[repo.path];
4413
+ if (!entry) continue;
4414
+ try {
4415
+ await this.commitWorktree(repo, record, entry);
4416
+ } catch {
4417
+ return { ok: false, reason: "failed", repo: repo.path };
4418
+ }
4419
+ }
4420
+ for (const repo of run.repos) {
4421
+ const base = record.repairBase[repo.path];
4422
+ if (base === void 0) continue;
4423
+ const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
4424
+ if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
4425
+ const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
4426
+ if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
4427
+ const files = leftoverMarkerFiles(check.stdout);
4428
+ if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
4429
+ }
4430
+ return { ok: true };
4431
+ }
4296
4432
  /**
4297
4433
  * The place in a repo's worktree that matches where the workspace sits in
4298
4434
  * the real repo: the worktree itself, or a subdirectory of it when the
@@ -4300,7 +4436,7 @@ var GitWorktreeIsolation = class {
4300
4436
  */
4301
4437
  async inWorkspacePlace(repo, worktree) {
4302
4438
  const prefix = await this.prefixOf(repo);
4303
- return prefix ? path7.join(worktree, prefix) : worktree;
4439
+ return prefix ? path8.join(worktree, prefix) : worktree;
4304
4440
  }
4305
4441
  async prefixOf(repo) {
4306
4442
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -4320,6 +4456,7 @@ var GitWorktreeIsolation = class {
4320
4456
  if (!record) return;
4321
4457
  if (opts.keep) {
4322
4458
  if (record.status === "active") record.status = "kept";
4459
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4323
4460
  return;
4324
4461
  }
4325
4462
  await this.removeTask(run, record, { dropRecord: true });
@@ -4339,6 +4476,7 @@ var GitWorktreeIsolation = class {
4339
4476
  for (const record of Object.values(run.tasks)) {
4340
4477
  if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
4341
4478
  else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
4479
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4342
4480
  }
4343
4481
  await this.removeUnowned(run);
4344
4482
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
@@ -4433,14 +4571,59 @@ var GitWorktreeIsolation = class {
4433
4571
  await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
4434
4572
  }
4435
4573
  await this.removeUnowned(run);
4436
- if (!opts.keepIntegration) {
4437
- for (const repo of run.repos) await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
4574
+ if (opts.integration !== "keep") {
4575
+ for (const repo of run.repos) {
4576
+ if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
4577
+ else await this.deleteIfMerged(repo.root, repo.integrationBranch);
4578
+ }
4438
4579
  run.tasks = {};
4439
4580
  this.removeIfEmpty(this.runRoot(run));
4440
4581
  }
4441
4582
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
4442
4583
  });
4443
4584
  }
4585
+ sweep(run) {
4586
+ return this.admin(run.workspaceRoot, async () => {
4587
+ const failed = [];
4588
+ for (const repo of run.repos) {
4589
+ if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
4590
+ }
4591
+ if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
4592
+ });
4593
+ }
4594
+ /** False when git failed anywhere; the other branches are still tried. */
4595
+ async sweepRepo(run, repo) {
4596
+ await this.tryGit(repo.root, ["worktree", "prune"]);
4597
+ const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
4598
+ const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
4599
+ if (!worktrees.ok || !refs.ok) return false;
4600
+ const checkedOut = /* @__PURE__ */ new Set();
4601
+ const liveRuns = /* @__PURE__ */ new Set();
4602
+ for (const line of worktrees.stdout.split(/\r?\n/)) {
4603
+ if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
4604
+ const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
4605
+ if (runId) liveRuns.add(runId);
4606
+ }
4607
+ let ok = true;
4608
+ for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
4609
+ const branch = ref.slice("refs/heads/".length);
4610
+ const runId = RUN_BRANCH.exec(branch)?.[1];
4611
+ if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
4612
+ if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
4613
+ }
4614
+ return ok;
4615
+ }
4616
+ /**
4617
+ * Delete `branch` only if the repo's checked-out HEAD already contains it —
4618
+ * HEAD, not an upstream: what the user has checked out is what they merged
4619
+ * into. `-d` rather than `-D`, so git also refuses a branch a worktree has
4620
+ * checked out.
4621
+ */
4622
+ async deleteIfMerged(root, branch) {
4623
+ const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
4624
+ if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
4625
+ return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
4626
+ }
4444
4627
  async drain() {
4445
4628
  try {
4446
4629
  while (this.waiting.length > 0) {
@@ -4493,21 +4676,24 @@ var GitWorktreeIsolation = class {
4493
4676
  return this.stopLanding(record, "failed");
4494
4677
  }
4495
4678
  for (const repo of changed) {
4496
- const outcome = await this.mergeTask(run, repo, record);
4679
+ const { outcome, files } = await this.mergeTask(run, repo, record);
4497
4680
  if (outcome === "merged") continue;
4498
- return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
4681
+ return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
4499
4682
  }
4500
4683
  }
4501
4684
  delete run.landing;
4502
- record.status = "merged";
4685
+ settleStatus(record, "merged");
4503
4686
  delete record.conflictRepo;
4687
+ delete record.conflictFiles;
4504
4688
  await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
4505
4689
  return "merged";
4506
4690
  }
4507
- stopLanding(record, outcome, repo) {
4508
- record.status = outcome;
4691
+ stopLanding(record, outcome, repo, files) {
4692
+ settleStatus(record, outcome);
4509
4693
  if (repo) record.conflictRepo = repo.path;
4510
4694
  else delete record.conflictRepo;
4695
+ if (files && files.length > 0) record.conflictFiles = files;
4696
+ else delete record.conflictFiles;
4511
4697
  return outcome;
4512
4698
  }
4513
4699
  /** 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 +4702,13 @@ var GitWorktreeIsolation = class {
4516
4702
  try {
4517
4703
  dir = await this.ensureIntegrationWorktree(run, repo);
4518
4704
  } catch {
4519
- return "failed";
4705
+ return { outcome: "failed", files: [] };
4520
4706
  }
4521
4707
  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;
4708
+ if (merge.ok) return { outcome: "merged", files: [] };
4709
+ const files = await this.unmergedPaths(dir);
4524
4710
  if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
4525
- return conflicted ? "conflict" : "failed";
4711
+ return { outcome: files.length > 0 ? "conflict" : "failed", files };
4526
4712
  }
4527
4713
  /**
4528
4714
  * Return every repo of the landing in flight to its recorded tip, then
@@ -4590,15 +4776,15 @@ var GitWorktreeIsolation = class {
4590
4776
  await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
4591
4777
  }
4592
4778
  integrationDir(run, repo) {
4593
- return path7.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
4779
+ return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
4594
4780
  }
4595
4781
  async ensureIntegrationWorktree(run, repo) {
4596
4782
  return this.admin(run.workspaceRoot, async () => {
4597
4783
  const dir = this.integrationDir(run, repo);
4598
- if (fs4.existsSync(path7.join(dir, ".git"))) return dir;
4784
+ if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
4599
4785
  await this.tryGit(repo.root, ["worktree", "prune"]);
4600
4786
  ensureStateDirIgnored(run.workspaceRoot);
4601
- fs4.mkdirSync(path7.dirname(dir), { recursive: true });
4787
+ fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4602
4788
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
4603
4789
  return dir;
4604
4790
  });
@@ -4633,20 +4819,20 @@ var GitWorktreeIsolation = class {
4633
4819
  return;
4634
4820
  }
4635
4821
  for (const entry of entries) {
4636
- const target = path7.join(dir, entry.name);
4822
+ const target = path8.join(dir, entry.name);
4637
4823
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
4638
4824
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
4639
4825
  }
4640
4826
  }
4641
4827
  /** Task workspaces and branches under this run that no record accounts for. */
4642
4828
  async removeUnowned(run) {
4643
- const owned = new Set(Object.values(run.tasks).map((r) => path7.resolve(r.workspace)));
4829
+ const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
4644
4830
  const root = this.runRoot(run);
4645
4831
  if (fs4.existsSync(root)) {
4646
4832
  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), []);
4833
+ const dir = path8.join(root, entry);
4834
+ if (entry === INTEGRATION_DIR || owned.has(path8.resolve(dir))) continue;
4835
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path8.join(dir, repo.path), []);
4650
4836
  this.removeTaskWorkspace(run, dir);
4651
4837
  }
4652
4838
  }
@@ -4660,14 +4846,18 @@ var GitWorktreeIsolation = class {
4660
4846
  }
4661
4847
  }
4662
4848
  async removeWorktreeDir(run, repo, dir, linked) {
4663
- for (const name of linked) this.unlinkIfLink(path7.join(dir, name));
4849
+ for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
4850
+ const target = path8.join(dir, name);
4851
+ this.unlinkIfLink(target);
4852
+ this.unlinkLinksUnder(target);
4853
+ }
4664
4854
  for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
4665
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path7.join(dir, name));
4855
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
4666
4856
  }
4667
4857
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
4668
4858
  if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
4669
4859
  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);
4860
+ for (let parent = path8.dirname(dir); isInside(this.runRoot(run), parent); parent = path8.dirname(parent)) this.removeIfEmpty(parent);
4671
4861
  }
4672
4862
  unlinkIfLink(target) {
4673
4863
  try {
@@ -4682,14 +4872,14 @@ var GitWorktreeIsolation = class {
4682
4872
  }
4683
4873
  }
4684
4874
  runRoot(run) {
4685
- return path7.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
4875
+ return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
4686
4876
  }
4687
4877
  namesFor(run, task) {
4688
4878
  const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
4689
4879
  let name = `${task.order}-${slug2}`;
4690
4880
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
4691
4881
  if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
4692
- return { branch: `ordewell/${run.id}/${name}`, dir: path7.join(this.runRoot(run), name) };
4882
+ return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
4693
4883
  }
4694
4884
  /**
4695
4885
  * Make one repo's worktree runnable: the default artifacts, unless a setup
@@ -4702,16 +4892,26 @@ var GitWorktreeIsolation = class {
4702
4892
  const linked = [];
4703
4893
  const copied = [];
4704
4894
  const link = (name) => {
4705
- const target = path7.join(cwd, name);
4895
+ const target = path8.join(cwd, name);
4706
4896
  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);
4897
+ fs4.mkdirSync(path8.dirname(target), { recursive: true });
4898
+ if (linkPath(path8.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
4899
+ linked.push(name);
4900
+ };
4901
+ const mirror = (name) => {
4902
+ const target = path8.join(cwd, name);
4903
+ if (lexists(target) || !fs4.existsSync(path8.dirname(target))) return;
4904
+ const source = path8.join(repo.root, name);
4905
+ if (!fs4.lstatSync(source).isDirectory()) return link(name);
4906
+ const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
4907
+ copied.push(...mirrored.map((entry) => path8.join(name, entry)));
4709
4908
  linked.push(name);
4710
4909
  };
4711
4910
  if (!setup) {
4712
4911
  for (const name of fs4.readdirSync(repo.root)) {
4713
- if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
4912
+ if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
4714
4913
  }
4914
+ for (const dir of installDirs(repo.root)) mirror(dir);
4715
4915
  }
4716
4916
  for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
4717
4917
  if (setup) await this.runSetup(setup, repo, cwd);
@@ -5159,6 +5359,7 @@ function researchPhaseBlock(harnessMode) {
5159
5359
  }
5160
5360
  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
5361
  var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
5362
+ 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
5363
  function repoGroupSection(isolated) {
5163
5364
  if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
5164
5365
  return [
@@ -5250,6 +5451,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
5250
5451
  "DEPENDENCY & PARALLELISM:",
5251
5452
  "- Independent slices should have NO dependencies \u2014 they run in parallel.",
5252
5453
  "- Only add dependencies when a slice truly depends on artifacts another slice creates.",
5454
+ SHARED_APPEND_FILE_RULE,
5253
5455
  ...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
5254
5456
  ...repoGroupSection(variant.isolatedExecution),
5255
5457
  "",
@@ -5351,6 +5553,7 @@ function corePlannerPrompt(isolatedExecution) {
5351
5553
  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
5554
  "- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
5353
5555
  "- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
5556
+ SHARED_APPEND_FILE_RULE,
5354
5557
  isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
5355
5558
  ...repoGroupSection(isolatedExecution),
5356
5559
  "",
@@ -5628,6 +5831,31 @@ ${task.prompt ?? task.description}`;
5628
5831
  asked
5629
5832
  ].join("\n");
5630
5833
  }
5834
+ function buildConflictRepairPrompt(task, conflict, integrationBranch) {
5835
+ const { branch } = conflict;
5836
+ const files = conflict.conflictFiles ?? [];
5837
+ const asked = `What the task was asked to do:
5838
+ ${task.prompt ?? task.description}`;
5839
+ const resolve6 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
5840
+ const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
5841
+ if (repos.length === 0) {
5842
+ return [
5843
+ `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.`,
5844
+ `This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve6} Never drop either side wholesale, and never abort the merge or reset it away.`,
5845
+ "Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
5846
+ "",
5847
+ asked
5848
+ ].join("\n");
5849
+ }
5850
+ const stoppedIn = conflict.conflictRepo ?? repos[0];
5851
+ return [
5852
+ `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.`,
5853
+ `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 ${resolve6} Never drop either side wholesale, and never abort a merge or reset it away.`,
5854
+ "Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
5855
+ "",
5856
+ asked
5857
+ ].join("\n");
5858
+ }
5631
5859
 
5632
5860
  // src/services/PlanRepair.ts
5633
5861
  async function repairLoop(opts) {
@@ -7641,9 +7869,9 @@ ${line}
7641
7869
  };
7642
7870
 
7643
7871
  // src/services/transcriptCapture.ts
7644
- import { readFileSync, readdirSync as readdirSync2, statSync as statSync4, existsSync as existsSync3 } from "fs";
7872
+ import { readFileSync as readFileSync2, readdirSync as readdirSync3, statSync as statSync4, existsSync as existsSync4 } from "fs";
7645
7873
  import * as os3 from "os";
7646
- import * as path8 from "path";
7874
+ import * as path9 from "path";
7647
7875
  var HomeTranscriptReader = class {
7648
7876
  home;
7649
7877
  constructor(opts = {}) {
@@ -7662,21 +7890,21 @@ var HomeTranscriptReader = class {
7662
7890
  };
7663
7891
  var CLAUDE_PROJECT_NAME_MAX = 200;
7664
7892
  function claudeProjectDirs(home, cwd) {
7665
- const projects = path8.join(home, ".claude", "projects");
7893
+ const projects = path9.join(home, ".claude", "projects");
7666
7894
  const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
7667
7895
  if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
7668
- const dir = path8.join(projects, munged);
7669
- return existsSync3(dir) ? [dir] : [];
7896
+ const dir = path9.join(projects, munged);
7897
+ return existsSync4(dir) ? [dir] : [];
7670
7898
  }
7671
- if (!existsSync3(projects)) return [];
7899
+ if (!existsSync4(projects)) return [];
7672
7900
  const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
7673
- return readdirSync2(projects).filter((name) => name.startsWith(prefix)).map((name) => path8.join(projects, name));
7901
+ return readdirSync3(projects).filter((name) => name.startsWith(prefix)).map((name) => path9.join(projects, name));
7674
7902
  }
7675
7903
  function claudeFinal(home, query, maxChars) {
7676
7904
  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);
7905
+ const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => readdirSync3(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path9.join(dir, f))).filter((f) => cutoff ? statSync4(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs);
7678
7906
  for (const file of candidates) {
7679
- const raw = readFileSync(file, "utf8");
7907
+ const raw = readFileSync2(file, "utf8");
7680
7908
  if (!raw.includes(query.marker)) continue;
7681
7909
  const text = claudeLastAssistant(raw);
7682
7910
  if (text) return clamp(text, maxChars);
@@ -7710,8 +7938,8 @@ async function opencodeFinal(home, query, maxChars) {
7710
7938
  } catch {
7711
7939
  return null;
7712
7940
  }
7713
- const dbPath = path8.join(home, ".local", "share", "opencode", "opencode.db");
7714
- if (!existsSync3(dbPath)) return null;
7941
+ const dbPath = path9.join(home, ".local", "share", "opencode", "opencode.db");
7942
+ if (!existsSync4(dbPath)) return null;
7715
7943
  let db;
7716
7944
  try {
7717
7945
  db = new DatabaseSync(dbPath, { open: true });
@@ -7763,8 +7991,8 @@ async function opencodeFinal(home, query, maxChars) {
7763
7991
  }
7764
7992
  }
7765
7993
  function codexFinal(home, query, maxChars) {
7766
- const root = path8.join(home, ".codex", "sessions");
7767
- if (!existsSync3(root)) return null;
7994
+ const root = path9.join(home, ".codex", "sessions");
7995
+ if (!existsSync4(root)) return null;
7768
7996
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
7769
7997
  const files = [];
7770
7998
  const stack = [root];
@@ -7772,12 +8000,12 @@ function codexFinal(home, query, maxChars) {
7772
8000
  const dir = stack.pop();
7773
8001
  let entries;
7774
8002
  try {
7775
- entries = readdirSync2(dir);
8003
+ entries = readdirSync3(dir);
7776
8004
  } catch {
7777
8005
  continue;
7778
8006
  }
7779
8007
  for (const e of entries) {
7780
- const full = path8.join(dir, e);
8008
+ const full = path9.join(dir, e);
7781
8009
  let st;
7782
8010
  try {
7783
8011
  st = statSync4(full);
@@ -7796,7 +8024,7 @@ function codexFinal(home, query, maxChars) {
7796
8024
  return null;
7797
8025
  }
7798
8026
  function codexLastAssistant(file, query) {
7799
- const raw = readFileSync(file, "utf8");
8027
+ const raw = readFileSync2(file, "utf8");
7800
8028
  if (!raw.includes(query.marker)) return null;
7801
8029
  const lines = raw.split("\n").filter((l) => l.trim());
7802
8030
  let sawCwd = false;
@@ -8415,7 +8643,7 @@ var TaskOrchestrator = class {
8415
8643
  if (!this.isolationRun) return null;
8416
8644
  const record = this.isolationRun.tasks[taskId];
8417
8645
  if (!record) return { state: "none" };
8418
- return taskIsolationOf(record);
8646
+ return taskIsolationOf(record, this.config.conflictRepairAttempts);
8419
8647
  }
8420
8648
  /**
8421
8649
  * The plan's isolation as a surface shows it, for one with no stream to have
@@ -8424,7 +8652,7 @@ var TaskOrchestrator = class {
8424
8652
  isolationView() {
8425
8653
  const run = this.isolationRun;
8426
8654
  if (!run) return null;
8427
- const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
8655
+ const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
8428
8656
  return { tasks, handoff: handoffOf(run) };
8429
8657
  }
8430
8658
  getAttemptSession(taskId) {
@@ -8460,24 +8688,43 @@ var TaskOrchestrator = class {
8460
8688
  async reviewRunDiff() {
8461
8689
  return this.isolation.reviewDiff(this.requireRun());
8462
8690
  }
8463
- /** "Merge all": the run's integration branches into whatever the user has checked out, in every repo or none. */
8691
+ /**
8692
+ * "Merge all": the run's integration branches into whatever the user has
8693
+ * checked out, in every repo or none. Once everything merged, the run has
8694
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
8695
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
8696
+ */
8464
8697
  async mergeRun() {
8465
8698
  const run = this.requireRun();
8466
8699
  const result = await this.isolation.mergeIntoCheckedOut(run);
8467
8700
  const branch = integrationBranchNameOf(run);
8468
8701
  const group = run.repos.some((r) => r.path !== SELF_REPO);
8469
- const { level, message } = describeMergeResult(result, branch, group);
8702
+ const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
8703
+ const { level, message } = describeMergeResult(result, branch, group, repaired);
8470
8704
  this.notifications[level](message);
8705
+ if (result.outcome === "merged") await this.clearMergedRun(run);
8471
8706
  return result;
8472
8707
  }
8708
+ async clearMergedRun(run) {
8709
+ try {
8710
+ await this.isolation.discard(run, { integration: "delete-merged" });
8711
+ } catch (err) {
8712
+ this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
8713
+ return;
8714
+ }
8715
+ this.forgetRun();
8716
+ }
8473
8717
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
8474
8718
  async cleanupRun() {
8475
- await this.isolation.discard(this.requireRun(), { keepIntegration: true });
8719
+ await this.isolation.discard(this.requireRun(), { integration: "keep" });
8476
8720
  this.emit("onIsolationChanged");
8477
8721
  }
8478
8722
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
8479
8723
  async discardRun() {
8480
- await this.isolation.discard(this.requireRun(), { keepIntegration: false });
8724
+ await this.isolation.discard(this.requireRun(), { integration: "delete" });
8725
+ this.forgetRun();
8726
+ }
8727
+ forgetRun() {
8481
8728
  this.isolationRun = null;
8482
8729
  this.resolvers = {};
8483
8730
  this.emit("onIsolationChanged");
@@ -8513,7 +8760,8 @@ var TaskOrchestrator = class {
8513
8760
  }
8514
8761
  loadPlan(tasks, planRunners = ["claude-code"]) {
8515
8762
  this.store.load(tasks, planRunners);
8516
- this.endAllAttempts("load");
8763
+ const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
8764
+ for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
8517
8765
  if (!this.running) this.runMode = null;
8518
8766
  this.blockedStart = null;
8519
8767
  this.planStatus = "approved";
@@ -8571,9 +8819,10 @@ var TaskOrchestrator = class {
8571
8819
  this.running = false;
8572
8820
  this.planStatus = "approved";
8573
8821
  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);
8822
+ for (const a of this.endAllAttempts("stop")) {
8823
+ if (a.repair) this.store.markAwaitingUser(a.taskId);
8824
+ if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
8825
+ }
8577
8826
  this.runMode = null;
8578
8827
  this.blockedStart = null;
8579
8828
  this.onHold.clear();
@@ -8589,6 +8838,7 @@ var TaskOrchestrator = class {
8589
8838
  console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
8590
8839
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
8591
8840
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
8841
+ if (attempt.repair) return this.settleRepair(task, attempt, verdict);
8592
8842
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
8593
8843
  const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
8594
8844
  if (this.attempts.get(taskId) !== attempt) return;
@@ -8609,6 +8859,9 @@ ${summary || "(empty \u2014 no output captured)"}`);
8609
8859
  }
8610
8860
  this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
8611
8861
  this.logAndArchive(task, verdict);
8862
+ await this.afterVerdict();
8863
+ }
8864
+ async afterVerdict() {
8612
8865
  this.emit("onTaskChanged");
8613
8866
  if (!this.running) {
8614
8867
  if (this.attempts.size === 0) {
@@ -8621,6 +8874,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
8621
8874
  }
8622
8875
  await this.tick();
8623
8876
  }
8877
+ /**
8878
+ * A repair's verdict decides only whether its work may try to land. It never
8879
+ * replaces the verdict the task's own work earned, and a repair that does not
8880
+ * land leaves the task waiting on the user — never a failed task, so never a
8881
+ * halted run.
8882
+ */
8883
+ async settleRepair(task, attempt, verdict) {
8884
+ const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
8885
+ if (this.attempts.get(task.id) !== attempt) return;
8886
+ this.endAttempt(task.id, "verdict");
8887
+ if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
8888
+ else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
8889
+ else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
8890
+ else this.landUnmerged(task, landed.outcome);
8891
+ await this.afterVerdict();
8892
+ }
8893
+ /**
8894
+ * Evidence before the queue: the repair's work is committed and checked, and
8895
+ * only then merged, through the same serialized landing as any other task —
8896
+ * so a repair the tip has moved past again is a fresh conflict, not a pass.
8897
+ */
8898
+ async landRepair(task, attempt) {
8899
+ const run = this.isolationRun;
8900
+ let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
8901
+ if (!run) return { evidence, outcome: "failed" };
8902
+ attempt.phase = "integrating";
8903
+ attempt.integration = (async () => {
8904
+ evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
8905
+ if (evidence.ok) return this.integrateWork(task);
8906
+ await this.releaseWorktree(task.id, { keep: true });
8907
+ return "conflict";
8908
+ })();
8909
+ const outcome = await attempt.integration;
8910
+ return { evidence, outcome };
8911
+ }
8912
+ async landedRepair(task, verdict) {
8913
+ const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
8914
+ this.store.markCompleted(task.id);
8915
+ this.store.unblockDependents(task.id);
8916
+ this.logAndArchive(task, task.verdict ?? verdict);
8917
+ this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
8918
+ await this.landResolved(task.id);
8919
+ }
8920
+ /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
8921
+ async unrepaired(task, why) {
8922
+ await this.releaseWorktree(task.id, { keep: true });
8923
+ this.store.markAwaitingUser(task.id);
8924
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
8925
+ 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.`);
8926
+ }
8927
+ describeEvidence(evidence) {
8928
+ const run = this.isolationRun;
8929
+ const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
8930
+ const branch = run ? integrationBranchNameOf(run) : "the integration branch";
8931
+ switch (evidence.reason) {
8932
+ case "not-merged":
8933
+ return `finished, but its branch${inRepo} does not contain ${branch}`;
8934
+ case "conflict-markers": {
8935
+ const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
8936
+ return `finished, but left conflict markers in ${capConflictFiles(files)}`;
8937
+ }
8938
+ case "failed":
8939
+ return `finished, but git could not check its work${inRepo}`;
8940
+ }
8941
+ }
8624
8942
  /**
8625
8943
  * Merge a passed attempt's worktree. The attempt stays live while it waits on
8626
8944
  * the module's merge queue, so a cancel, retry or stop in that window still
@@ -8654,11 +8972,25 @@ ${summary || "(empty \u2014 no output captured)"}`);
8654
8972
  }
8655
8973
  landUnmerged(task, landing) {
8656
8974
  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;
8975
+ const record = this.isolationRun?.tasks[task.id];
8976
+ const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
8659
8977
  if (landing === "conflict") {
8978
+ const repair = this.nextRepair(task.id);
8660
8979
  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.`);
8980
+ const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
8981
+ 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}.`;
8982
+ if (repair) {
8983
+ this.notifications.warn(conflicted);
8984
+ if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
8985
+ else {
8986
+ this.store.markPending(task.id);
8987
+ this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
8988
+ }
8989
+ return;
8990
+ }
8991
+ 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.`);
8992
+ const whyNot = this.noRepairReason(task);
8993
+ if (whyNot) this.tell("info", whyNot);
8662
8994
  } else {
8663
8995
  this.store.markFailed(task.id);
8664
8996
  this.running = false;
@@ -8666,6 +8998,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
8666
8998
  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
8999
  }
8668
9000
  }
9001
+ /** 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. */
9002
+ nextRepair(taskId) {
9003
+ const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
9004
+ const limit = this.config.conflictRepairAttempts;
9005
+ const spent = record?.repairs ?? 0;
9006
+ return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
9007
+ }
9008
+ noRepairReason(task) {
9009
+ const limit = this.config.conflictRepairAttempts;
9010
+ if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
9011
+ const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
9012
+ return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
9013
+ }
8669
9014
  /**
8670
9015
  * Mark which added task resolves which conflict. The resolver merges the
8671
9016
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -8955,6 +9300,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
8955
9300
  cwd: null,
8956
9301
  worktree: false,
8957
9302
  integration: null,
9303
+ repair: this.nextRepair(task.id),
8958
9304
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
8959
9305
  };
8960
9306
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -8965,12 +9311,13 @@ ${summary || "(empty \u2014 no output captured)"}`);
8965
9311
  const cwd = await this.resolveAttemptCwd(task, attempt);
8966
9312
  attempt.cwd = cwd;
8967
9313
  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);
9314
+ if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
9315
+ return this.abandonSpawn(task, attempt);
8970
9316
  }
8971
- const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
9317
+ const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
8972
9318
  planMapEnabled: this.config.planMapEnabled,
8973
- tddEnabled: this.tddEnabled()
9319
+ // A merge to resolve is not new behaviour to drive test-first.
9320
+ tddEnabled: !attempt.repair && this.tddEnabled()
8974
9321
  });
8975
9322
  const session = await this.terminalRunner.spawn({
8976
9323
  taskId: task.id,
@@ -8985,16 +9332,26 @@ ${summary || "(empty \u2014 no output captured)"}`);
8985
9332
  order: task.order,
8986
9333
  title: task.title
8987
9334
  });
8988
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
9335
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
8989
9336
  attempt.phase = "running";
8990
9337
  attempt.session = session;
8991
9338
  this.output.attach(task.id, session);
8992
9339
  this.verifier.watch(task, session);
8993
9340
  this.emit("onTaskChanged");
8994
- this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
9341
+ if (attempt.repair) {
9342
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
9343
+ this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
9344
+ } else {
9345
+ this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
9346
+ }
8995
9347
  } catch (err) {
8996
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
9348
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
8997
9349
  this.endAttempt(task.id, "spawn-failed");
9350
+ if (attempt.repair) {
9351
+ await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
9352
+ this.emit("onTaskChanged");
9353
+ return this.tick();
9354
+ }
8998
9355
  await this.releaseWorktree(task.id, { keep: false });
8999
9356
  this.store.markPending(task.id);
9000
9357
  this.onHold.add(task.id);
@@ -9008,17 +9365,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
9008
9365
  * and take back the claim it made. Only the claim — whatever ended the
9009
9366
  * attempt may have decided the task since (mark complete, cancel, retry).
9010
9367
  */
9011
- abandonSpawn(task, session) {
9368
+ abandonSpawn(task, attempt, session) {
9012
9369
  session?.kill();
9013
9370
  if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
9014
- this.store.markPending(task.id);
9371
+ if (attempt.repair) this.store.markAwaitingUser(task.id);
9372
+ else this.store.markPending(task.id);
9015
9373
  this.emit("onTaskChanged");
9016
9374
  }
9375
+ repairPrompt(task) {
9376
+ const run = this.requireRun();
9377
+ const record = run.tasks[task.id];
9378
+ const conflict = {
9379
+ branch: record?.branch ?? "",
9380
+ repos: Object.keys(record?.repairBase ?? {}),
9381
+ ...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
9382
+ ...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
9383
+ };
9384
+ return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
9385
+ }
9017
9386
  /**
9018
9387
  * The one place an attempt's working directory is decided. Async so a
9019
9388
  * per-attempt workspace (worktree isolation, #12) can be prepared here.
9020
9389
  */
9021
9390
  async resolveAttemptCwd(task, attempt) {
9391
+ if (attempt.repair && this.isolationRun) {
9392
+ const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
9393
+ attempt.worktree = true;
9394
+ this.emit("onIsolationChanged");
9395
+ return cwd2;
9396
+ }
9022
9397
  if (this.runMode !== "isolated" || !this.isolationRun) {
9023
9398
  await this.releaseWorktree(task.id, { keep: false });
9024
9399
  return this.workspaceRootFn();
@@ -9106,8 +9481,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
9106
9481
  }
9107
9482
  }
9108
9483
  this.runMode = "isolated";
9484
+ await this.sweep();
9109
9485
  return true;
9110
9486
  }
9487
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
9488
+ async sweep() {
9489
+ if (!this.isolationRun) return;
9490
+ try {
9491
+ await this.isolation.sweep(this.isolationRun);
9492
+ } catch (err) {
9493
+ this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
9494
+ }
9495
+ }
9111
9496
  /**
9112
9497
  * The plan's run carries on while anything has landed on its integration
9113
9498
  * branch: a resumed plan's dependents must start from a tip that holds their
@@ -9120,14 +9505,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
9120
9505
  /**
9121
9506
  * A run with nothing landed holds only superseded attempts, so it goes whole.
9122
9507
  * 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.
9508
+ * workspace path — keeps its integration branch in each repo that has not
9509
+ * merged it: only the user gives landed work up.
9125
9510
  */
9126
9511
  async mintRun(root) {
9127
9512
  const previous = this.isolationRun;
9128
9513
  if (previous) {
9129
9514
  const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
9130
- await this.isolation.discard(previous, { keepIntegration: landed }).catch(() => void 0);
9515
+ await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
9131
9516
  this.isolationRun = null;
9132
9517
  }
9133
9518
  this.isolationRun = await this.isolation.startRun(root);
@@ -9192,9 +9577,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
9192
9577
  return attempt;
9193
9578
  }
9194
9579
  endAllAttempts(reason) {
9195
- for (const taskId of [...this.attempts.keys()]) this.endAttempt(taskId, reason);
9580
+ const ended = [...this.attempts.values()];
9581
+ for (const { taskId } of ended) this.endAttempt(taskId, reason);
9196
9582
  this.verifier.reset();
9197
9583
  if (reason === "load") this.output.reset();
9584
+ return ended;
9198
9585
  }
9199
9586
  };
9200
9587
 
@@ -9324,7 +9711,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
9324
9711
  };
9325
9712
 
9326
9713
  // src/services/ModelDiscovery.ts
9327
- import * as path11 from "path";
9714
+ import * as path12 from "path";
9328
9715
  import { exec as exec3, spawn } from "child_process";
9329
9716
  import { promisify as promisify3 } from "util";
9330
9717
  import * as fsSync3 from "fs";
@@ -9332,17 +9719,17 @@ import * as osMod from "os";
9332
9719
 
9333
9720
  // src/utils/globalDataDir.ts
9334
9721
  import * as fs5 from "fs";
9335
- import * as path9 from "path";
9722
+ import * as path10 from "path";
9336
9723
  import * as os4 from "os";
9337
9724
  function globalDataDir() {
9338
- return path9.join(os4.homedir(), ".ordewell");
9725
+ return path10.join(os4.homedir(), ".ordewell");
9339
9726
  }
9340
9727
  function copyDirSync(src, dest) {
9341
9728
  fs5.mkdirSync(dest, { recursive: true });
9342
9729
  const entries = fs5.readdirSync(src, { withFileTypes: true });
9343
9730
  for (const entry of entries) {
9344
- const srcPath = path9.join(src, entry.name);
9345
- const destPath = path9.join(dest, entry.name);
9731
+ const srcPath = path10.join(src, entry.name);
9732
+ const destPath = path10.join(dest, entry.name);
9346
9733
  if (entry.isDirectory()) {
9347
9734
  copyDirSync(srcPath, destPath);
9348
9735
  } else {
@@ -9354,12 +9741,12 @@ var migrated = false;
9354
9741
  function migrateOldConfigDir() {
9355
9742
  if (migrated) return;
9356
9743
  migrated = true;
9357
- const oldDir = path9.join(os4.homedir(), ".config", "ordewell");
9744
+ const oldDir = path10.join(os4.homedir(), ".config", "ordewell");
9358
9745
  if (!fs5.existsSync(oldDir)) return;
9359
9746
  const newDir = globalDataDir();
9360
9747
  const liftFile = (name) => {
9361
- const oldFile = path9.join(oldDir, name);
9362
- const newFile = path9.join(newDir, name);
9748
+ const oldFile = path10.join(oldDir, name);
9749
+ const newFile = path10.join(newDir, name);
9363
9750
  if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
9364
9751
  fs5.mkdirSync(newDir, { recursive: true });
9365
9752
  fs5.copyFileSync(oldFile, newFile);
@@ -9367,8 +9754,8 @@ function migrateOldConfigDir() {
9367
9754
  };
9368
9755
  liftFile("settings.json");
9369
9756
  liftFile(".env");
9370
- const oldPlugins = path9.join(oldDir, "plugins");
9371
- const newPlugins = path9.join(newDir, "plugins");
9757
+ const oldPlugins = path10.join(oldDir, "plugins");
9758
+ const newPlugins = path10.join(newDir, "plugins");
9372
9759
  if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
9373
9760
  copyDirSync(oldPlugins, newPlugins);
9374
9761
  }
@@ -9376,7 +9763,7 @@ function migrateOldConfigDir() {
9376
9763
 
9377
9764
  // src/utils/launch.ts
9378
9765
  import * as fsSync2 from "fs";
9379
- import * as path10 from "path";
9766
+ import * as path11 from "path";
9380
9767
 
9381
9768
  // src/utils/shell.ts
9382
9769
  var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
@@ -9405,7 +9792,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
9405
9792
  var WINDOWS_MAX_COMMAND_LINE = 32767;
9406
9793
  function defaultPowerShell() {
9407
9794
  const root = process.env.SystemRoot || process.env.windir;
9408
- return root ? path10.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
9795
+ return root ? path11.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
9409
9796
  }
9410
9797
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
9411
9798
  var CommandLineTooLongError = class extends Error {
@@ -9457,7 +9844,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
9457
9844
  };
9458
9845
  function isResolvableOnPosixPath(command, PATH, exists) {
9459
9846
  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)));
9847
+ return PATH.split(path11.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path11.join(dir, command)));
9461
9848
  }
9462
9849
  function isExecutableResolved(command, plan, PATH, deps = {}) {
9463
9850
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -9497,9 +9884,9 @@ function cmdArgs(line) {
9497
9884
  return ["/d", "/s", "/c", `"${line}"`];
9498
9885
  }
9499
9886
  function candidateExtensions(command, pathExt) {
9500
- const declared = pathExt.split(path10.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
9887
+ const declared = pathExt.split(path11.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
9501
9888
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
9502
- const ext = path10.win32.extname(command).toLowerCase();
9889
+ const ext = path11.win32.extname(command).toLowerCase();
9503
9890
  if (ext && known.has(ext)) {
9504
9891
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
9505
9892
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -9520,13 +9907,13 @@ async function findWindowsExecutables(command, deps) {
9520
9907
  const exists = deps.exists ?? defaultExists2;
9521
9908
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
9522
9909
  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);
9910
+ const dirs = path11.win32.basename(command) !== command ? [path11.win32.dirname(path11.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path11.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
9911
+ const base = path11.win32.basename(command);
9525
9912
  const found = [];
9526
9913
  for (const route of ROUTES) {
9527
9914
  outer: for (const ext of byRoute[route]) {
9528
9915
  for (const dir of dirs) {
9529
- const candidate = path10.win32.join(dir, `${base}${ext}`);
9916
+ const candidate = path11.win32.join(dir, `${base}${ext}`);
9530
9917
  if (exists(candidate)) {
9531
9918
  found.push({ file: candidate, route });
9532
9919
  break outer;
@@ -9621,7 +10008,7 @@ var defaultAppServerClient = async (command, args, options) => {
9621
10008
  const PATH = await augmentedPath();
9622
10009
  const launch = await planDirectLaunch(command, args).catch(() => null);
9623
10010
  if (!launch) return null;
9624
- return new Promise((resolve5) => {
10011
+ return new Promise((resolve6) => {
9625
10012
  let child;
9626
10013
  try {
9627
10014
  child = spawn(launch.file, launch.args, {
@@ -9630,7 +10017,7 @@ var defaultAppServerClient = async (command, args, options) => {
9630
10017
  windowsVerbatimArguments: launch.verbatim
9631
10018
  });
9632
10019
  } catch {
9633
- resolve5(null);
10020
+ resolve6(null);
9634
10021
  return;
9635
10022
  }
9636
10023
  let settled = false;
@@ -9639,7 +10026,7 @@ var defaultAppServerClient = async (command, args, options) => {
9639
10026
  settled = true;
9640
10027
  clearTimeout(timer);
9641
10028
  killTree(child);
9642
- resolve5(value);
10029
+ resolve6(value);
9643
10030
  };
9644
10031
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
9645
10032
  child.on("error", () => finish(null));
@@ -9706,7 +10093,7 @@ function geminiDiscoveryCacheKey(apiKey) {
9706
10093
  return {
9707
10094
  key: `gemini-models:${hash}`,
9708
10095
  dir,
9709
- file: path11.join(dir, `gemini_models_${hash}.json`)
10096
+ file: path12.join(dir, `gemini_models_${hash}.json`)
9710
10097
  };
9711
10098
  }
9712
10099
  function readGeminiCache(apiKey) {
@@ -10230,7 +10617,7 @@ var ModelDiscovery = class {
10230
10617
 
10231
10618
  // src/services/ModelCatalog.ts
10232
10619
  import * as fs6 from "fs";
10233
- import * as path12 from "path";
10620
+ import * as path13 from "path";
10234
10621
  var CACHE_KEY = "model-catalog";
10235
10622
  var CACHE_TTL_MS = 60 * 60 * 1e3;
10236
10623
  function storageKey(baseUrl) {
@@ -10282,7 +10669,7 @@ var ModelCatalog = class {
10282
10669
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
10283
10670
  const key = storageKey(url);
10284
10671
  const cacheDir = globalCacheDir();
10285
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10672
+ const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10286
10673
  if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
10287
10674
  } catch {
10288
10675
  }
@@ -10290,7 +10677,7 @@ var ModelCatalog = class {
10290
10677
  static readCache(key) {
10291
10678
  try {
10292
10679
  const cacheDir = globalCacheDir();
10293
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10680
+ const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10294
10681
  if (!fs6.existsSync(cacheFile)) return null;
10295
10682
  const raw = fs6.readFileSync(cacheFile, "utf8");
10296
10683
  const data = JSON.parse(raw);
@@ -10304,7 +10691,7 @@ var ModelCatalog = class {
10304
10691
  try {
10305
10692
  const cacheDir = globalCacheDir();
10306
10693
  fs6.mkdirSync(cacheDir, { recursive: true });
10307
- const cacheFile = path12.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10694
+ const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
10308
10695
  fs6.writeFileSync(
10309
10696
  cacheFile,
10310
10697
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
@@ -10688,7 +11075,8 @@ var ORDEWELL_SETTABLE_ENV = [
10688
11075
  "ORDEWELL_SUBAGENT_MODEL",
10689
11076
  "ORDEWELL_PLANNER_EFFORT",
10690
11077
  "ORDEWELL_RESEARCH_ENABLED",
10691
- "ORDEWELL_MAX_PARALLEL"
11078
+ "ORDEWELL_MAX_PARALLEL",
11079
+ "ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
10692
11080
  ];
10693
11081
  var SETTINGS_ENV_REFUSED = [
10694
11082
  // Approval posture and pre-approved scope.
@@ -10745,7 +11133,7 @@ import { spawn as nodeSpawn } from "child_process";
10745
11133
 
10746
11134
  // src/utils/workspace.ts
10747
11135
  import * as fs7 from "fs";
10748
- import * as path13 from "path";
11136
+ import * as path14 from "path";
10749
11137
  var WorkspaceNotFoundError = class extends Error {
10750
11138
  constructor(workspace) {
10751
11139
  super(`Workspace "${workspace}" does not exist.`);
@@ -10801,7 +11189,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
10801
11189
  throw new WorkspaceNotAProjectError(workspace);
10802
11190
  }
10803
11191
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
10804
- (marker2) => exists(path13.join(resolved, marker2))
11192
+ (marker2) => exists(path14.join(resolved, marker2))
10805
11193
  );
10806
11194
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
10807
11195
  }
@@ -10869,8 +11257,8 @@ var StdioAgentAdapter = class {
10869
11257
  }
10870
11258
  async start(opts) {
10871
11259
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
10872
- this.processEnded = new Promise((resolve5) => {
10873
- this.markEnded = resolve5;
11260
+ this.processEnded = new Promise((resolve6) => {
11261
+ this.markEnded = resolve6;
10874
11262
  });
10875
11263
  const spec = this.spawnSpec(opts);
10876
11264
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
@@ -10921,7 +11309,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
10921
11309
  onEvent({ type: "error", message: this.exitMessage() });
10922
11310
  return;
10923
11311
  }
10924
- await new Promise((resolve5) => {
11312
+ await new Promise((resolve6) => {
10925
11313
  let settled = false;
10926
11314
  const finish = () => {
10927
11315
  if (settled) return;
@@ -10932,7 +11320,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
10932
11320
  signal?.removeEventListener("abort", onAbort);
10933
11321
  this.process?.removeListener("exit", onExit);
10934
11322
  this.process?.removeListener("error", onExit);
10935
- resolve5();
11323
+ resolve6();
10936
11324
  };
10937
11325
  const emit = (event) => {
10938
11326
  onEvent(event);
@@ -11147,14 +11535,14 @@ function codexSandboxUnavailableMessage() {
11147
11535
  ].join("\n");
11148
11536
  }
11149
11537
  function runProbe(deps, cwd, env, flags) {
11150
- return new Promise((resolve5) => {
11538
+ return new Promise((resolve6) => {
11151
11539
  let output = "";
11152
11540
  let settled = false;
11153
11541
  const finish = (code) => {
11154
11542
  if (settled) return;
11155
11543
  settled = true;
11156
11544
  clearTimeout(timer);
11157
- resolve5({ code, output });
11545
+ resolve6({ code, output });
11158
11546
  };
11159
11547
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
11160
11548
  timer.unref?.();
@@ -11237,8 +11625,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
11237
11625
  async handshake(opts) {
11238
11626
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
11239
11627
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
11240
- const ready = new Promise((resolve5) => {
11241
- this.settleHandshake = resolve5;
11628
+ const ready = new Promise((resolve6) => {
11629
+ this.settleHandshake = resolve6;
11242
11630
  });
11243
11631
  this.writeLine({
11244
11632
  jsonrpc: "2.0",
@@ -11251,8 +11639,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
11251
11639
  // A binary that rejects `app-server` outright dies immediately; waiting
11252
11640
  // out the timeout would turn a one-line diagnostic into a 30s stall.
11253
11641
  this.processEnded.then(() => false),
11254
- new Promise((resolve5) => {
11255
- const t = setTimeout(() => resolve5(false), HANDSHAKE_TIMEOUT_MS);
11642
+ new Promise((resolve6) => {
11643
+ const t = setTimeout(() => resolve6(false), HANDSHAKE_TIMEOUT_MS);
11256
11644
  t.unref?.();
11257
11645
  })
11258
11646
  ]);
@@ -11529,12 +11917,12 @@ function describeError(err) {
11529
11917
  return lines.join(": ");
11530
11918
  }
11531
11919
  function delay(ms, signal) {
11532
- return new Promise((resolve5) => {
11533
- const timer = setTimeout(resolve5, ms);
11920
+ return new Promise((resolve6) => {
11921
+ const timer = setTimeout(resolve6, ms);
11534
11922
  timer.unref?.();
11535
11923
  signal?.addEventListener("abort", () => {
11536
11924
  clearTimeout(timer);
11537
- resolve5();
11925
+ resolve6();
11538
11926
  }, { once: true });
11539
11927
  });
11540
11928
  }
@@ -11573,12 +11961,12 @@ var OpenCodeAdapter = class {
11573
11961
  cwd: opts.cwd,
11574
11962
  windowsVerbatimArguments: launch.verbatim
11575
11963
  });
11576
- const banner = new Promise((resolve5) => {
11964
+ const banner = new Promise((resolve6) => {
11577
11965
  let seen = "";
11578
11966
  const scan = (chunk) => {
11579
11967
  seen += chunk.toString();
11580
11968
  const match = seen.match(/https?:\/\/[^\s]+/);
11581
- if (match) resolve5(match[0].replace(/[.,)]$/, ""));
11969
+ if (match) resolve6(match[0].replace(/[.,)]$/, ""));
11582
11970
  };
11583
11971
  this.process.stdout?.on("data", scan);
11584
11972
  this.process.stderr?.on("data", (chunk) => {
@@ -11587,15 +11975,15 @@ var OpenCodeAdapter = class {
11587
11975
  });
11588
11976
  this.process.on("exit", () => {
11589
11977
  this.exited = true;
11590
- resolve5(null);
11978
+ resolve6(null);
11591
11979
  });
11592
11980
  this.process.on("error", (err) => {
11593
11981
  this.exited = true;
11594
11982
  this.stderrTail = (this.stderrTail + `
11595
11983
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
11596
- resolve5(null);
11984
+ resolve6(null);
11597
11985
  });
11598
- const timer = setTimeout(() => resolve5(null), SERVER_READY_TIMEOUT_MS);
11986
+ const timer = setTimeout(() => resolve6(null), SERVER_READY_TIMEOUT_MS);
11599
11987
  timer.unref?.();
11600
11988
  });
11601
11989
  this.baseUrl = await banner;
@@ -11628,8 +12016,8 @@ ${this.stderrTail.trim()}` : ""}`);
11628
12016
  const streamAbort = new AbortController();
11629
12017
  let connected = () => {
11630
12018
  };
11631
- const streamReady = new Promise((resolve5) => {
11632
- connected = resolve5;
12019
+ const streamReady = new Promise((resolve6) => {
12020
+ connected = resolve6;
11633
12021
  });
11634
12022
  const live = this.streamEvents(
11635
12023
  streamAbort.signal,
@@ -11833,14 +12221,14 @@ ${part.text}` : part.text });
11833
12221
  }
11834
12222
  }
11835
12223
  }
11836
- async json(method, path22, body, signal) {
11837
- const response = await this.deps.fetch(`${this.baseUrl}${path22}`, {
12224
+ async json(method, path23, body, signal) {
12225
+ const response = await this.deps.fetch(`${this.baseUrl}${path23}`, {
11838
12226
  method,
11839
12227
  headers: { "content-type": "application/json" },
11840
12228
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
11841
12229
  ...signal ? { signal } : {}
11842
12230
  });
11843
- if (!response.ok) throw new Error(`${method} ${path22} failed: ${response.status} ${response.statusText}`);
12231
+ if (!response.ok) throw new Error(`${method} ${path23} failed: ${response.status} ${response.statusText}`);
11844
12232
  return await response.json().catch(() => null);
11845
12233
  }
11846
12234
  nativeSessionId() {
@@ -12509,33 +12897,9 @@ var PlannerConversation = class {
12509
12897
  if (m.role !== "user" || index === 0) return [];
12510
12898
  const line = m.content.split("\n")[0];
12511
12899
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
12512
- return [{ index, preview, timestamp: m.timestamp }];
12900
+ return [{ index, preview, content: m.content, timestamp: m.timestamp }];
12513
12901
  });
12514
12902
  }
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
12903
  /**
12540
12904
  * Replace the transcript with a summary of it, keeping the last two
12541
12905
  * exchanges as they were, and drop the live context so the next message
@@ -12611,6 +12975,28 @@ var PlannerConversation = class {
12611
12975
  researchLog: plan?.researchLog ?? []
12612
12976
  });
12613
12977
  }
12978
+ /**
12979
+ * A copy of the dialogue as it stood just before the user message at
12980
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
12981
+ * from — plus that message's full text, for a surface to offer back. This
12982
+ * conversation is not touched. The research trace is cut at the same point
12983
+ * by time: its entries carry no link to the message that caused them.
12984
+ */
12985
+ cloneBefore(index) {
12986
+ this.assertIdle("rewind the conversation");
12987
+ if (!this.rewindTargets().some((t) => t.index === index)) {
12988
+ if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
12989
+ 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.");
12990
+ }
12991
+ const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
12992
+ return {
12993
+ dialogue: structuredClone({
12994
+ conversationHistory: this.transcript.slice(0, index),
12995
+ researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
12996
+ }),
12997
+ rewoundMessage
12998
+ };
12999
+ }
12614
13000
  /** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
12615
13001
  recordModification(request, requestedAt, taskCount) {
12616
13002
  this.append("user", request, { timestamp: requestedAt });
@@ -13197,17 +13583,17 @@ function retargetTaskRunner(task, runner, catalog) {
13197
13583
 
13198
13584
  // src/services/SkillsService.ts
13199
13585
  import * as fs8 from "fs";
13200
- import * as path15 from "path";
13586
+ import * as path16 from "path";
13201
13587
 
13202
13588
  // src/services/builtinSkills.ts
13203
- import * as path14 from "path";
13589
+ import * as path15 from "path";
13204
13590
  import { fileURLToPath } from "url";
13205
13591
  function builtinSkillsDir() {
13206
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path14.dirname(fileURLToPath(
13592
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path15.dirname(fileURLToPath(
13207
13593
  // @ts-expect-error - import.meta is only valid under an ESM module setting
13208
13594
  import.meta.url
13209
13595
  ));
13210
- return path14.join(dir, "..", "skills");
13596
+ return path15.join(dir, "..", "skills");
13211
13597
  }
13212
13598
 
13213
13599
  // src/services/SkillsService.ts
@@ -13246,8 +13632,8 @@ function copyDirSync2(src, dest) {
13246
13632
  fs8.mkdirSync(dest, { recursive: true });
13247
13633
  const entries = fs8.readdirSync(src, { withFileTypes: true });
13248
13634
  for (const entry of entries) {
13249
- const srcPath = path15.join(src, entry.name);
13250
- const destPath = path15.join(dest, entry.name);
13635
+ const srcPath = path16.join(src, entry.name);
13636
+ const destPath = path16.join(dest, entry.name);
13251
13637
  if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
13252
13638
  else fs8.copyFileSync(srcPath, destPath);
13253
13639
  }
@@ -13258,7 +13644,7 @@ function readDir(dir, source) {
13258
13644
  const skills = [];
13259
13645
  for (const entry of entries) {
13260
13646
  if (!entry.isDirectory()) continue;
13261
- const skillFile = path15.join(dir, entry.name, "SKILL.md");
13647
+ const skillFile = path16.join(dir, entry.name, "SKILL.md");
13262
13648
  if (!fs8.existsSync(skillFile)) continue;
13263
13649
  const parsed = parseSkillFile(skillFile, entry.name, source);
13264
13650
  if (parsed) skills.push(parsed);
@@ -13271,17 +13657,17 @@ var SkillsService = class {
13271
13657
  }
13272
13658
  workspaceRoot;
13273
13659
  localDir() {
13274
- return this.workspaceRoot ? path15.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
13660
+ return this.workspaceRoot ? path16.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
13275
13661
  }
13276
13662
  globalDir() {
13277
- return path15.join(globalDataDir(), "skills");
13663
+ return path16.join(globalDataDir(), "skills");
13278
13664
  }
13279
13665
  seed(name) {
13280
- const dest = path15.join(this.globalDir(), name);
13666
+ const dest = path16.join(this.globalDir(), name);
13281
13667
  if (fs8.existsSync(dest)) return false;
13282
13668
  let srcDir;
13283
13669
  try {
13284
- srcDir = path15.join(builtinSkillsDir(), name);
13670
+ srcDir = path16.join(builtinSkillsDir(), name);
13285
13671
  } catch (err) {
13286
13672
  console.warn(`Could not locate built-in skills dir: ${String(err)}`);
13287
13673
  return false;
@@ -13307,7 +13693,7 @@ var SkillsService = class {
13307
13693
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
13308
13694
  let raw;
13309
13695
  try {
13310
- raw = fs8.readFileSync(path15.join(dir, "SKILL.md"), "utf8");
13696
+ raw = fs8.readFileSync(path16.join(dir, "SKILL.md"), "utf8");
13311
13697
  } catch {
13312
13698
  return false;
13313
13699
  }
@@ -13324,7 +13710,7 @@ var SkillsService = class {
13324
13710
  }
13325
13711
  pruneRetired() {
13326
13712
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
13327
- const dest = path15.join(this.globalDir(), name);
13713
+ const dest = path16.join(this.globalDir(), name);
13328
13714
  if (!fs8.existsSync(dest)) continue;
13329
13715
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
13330
13716
  try {
@@ -13339,10 +13725,10 @@ var SkillsService = class {
13339
13725
  if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
13340
13726
  const localDir = this.localDir();
13341
13727
  if (localDir) {
13342
- const localFile = path15.join(localDir, name, "SKILL.md");
13728
+ const localFile = path16.join(localDir, name, "SKILL.md");
13343
13729
  if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
13344
13730
  }
13345
- const globalFile = path15.join(this.globalDir(), name, "SKILL.md");
13731
+ const globalFile = path16.join(this.globalDir(), name, "SKILL.md");
13346
13732
  if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
13347
13733
  return void 0;
13348
13734
  }
@@ -13371,7 +13757,7 @@ function createSkillsService(workspaceRoot) {
13371
13757
 
13372
13758
  // src/utils/sessionStore.ts
13373
13759
  import * as fs9 from "fs";
13374
- import * as path16 from "path";
13760
+ import * as path17 from "path";
13375
13761
 
13376
13762
  // src/utils/sessionId.ts
13377
13763
  import { randomBytes as randomBytes2 } from "crypto";
@@ -13382,7 +13768,7 @@ function mintSessionId() {
13382
13768
  // src/utils/sessionStore.ts
13383
13769
  var SESSIONS_SUBDIR = "sessions";
13384
13770
  function getSessionsDir(baseDir) {
13385
- return path16.join(getStateDir(baseDir), SESSIONS_SUBDIR);
13771
+ return path17.join(getStateDir(baseDir), SESSIONS_SUBDIR);
13386
13772
  }
13387
13773
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
13388
13774
  function hasScalarRunner(parsed) {
@@ -13404,8 +13790,8 @@ function legacySessionFilename(session) {
13404
13790
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
13405
13791
  }
13406
13792
  function dropLegacyFile(sessionsDir, meta) {
13407
- const legacy = path16.join(sessionsDir, legacySessionFilename(meta));
13408
- if (legacy === path16.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
13793
+ const legacy = path17.join(sessionsDir, legacySessionFilename(meta));
13794
+ if (legacy === path17.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
13409
13795
  try {
13410
13796
  const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
13411
13797
  if (held === meta.id) fs9.unlinkSync(legacy);
@@ -13431,7 +13817,7 @@ function saveSession(plan, goal, baseDir, id) {
13431
13817
  plan: JSON.parse(JSON.stringify(plan))
13432
13818
  };
13433
13819
  const filename = sessionFilename(meta);
13434
- const filePath = path16.join(sessionsDir, filename);
13820
+ const filePath = path17.join(sessionsDir, filename);
13435
13821
  fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
13436
13822
  dropLegacyFile(sessionsDir, meta);
13437
13823
  return meta;
@@ -13442,7 +13828,7 @@ function listSessions(baseDir, logger = defaultLogger) {
13442
13828
  const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
13443
13829
  return files.map((f) => {
13444
13830
  try {
13445
- const raw = fs9.readFileSync(path16.join(sessionsDir, f), "utf-8");
13831
+ const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
13446
13832
  const parsed = JSON.parse(raw);
13447
13833
  if (hasScalarRunner(parsed)) {
13448
13834
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -13460,7 +13846,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
13460
13846
  const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
13461
13847
  for (const f of files) {
13462
13848
  try {
13463
- const raw = fs9.readFileSync(path16.join(sessionsDir, f), "utf-8");
13849
+ const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
13464
13850
  const session = JSON.parse(raw);
13465
13851
  if (session.meta.id === sessionId) return f;
13466
13852
  } catch (err) {
@@ -13475,7 +13861,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
13475
13861
  try {
13476
13862
  const file = findSessionFile(sessionsDir, sessionId, logger);
13477
13863
  if (file) {
13478
- const raw = fs9.readFileSync(path16.join(sessionsDir, file), "utf-8");
13864
+ const raw = fs9.readFileSync(path17.join(sessionsDir, file), "utf-8");
13479
13865
  const parsed = JSON.parse(raw);
13480
13866
  if (hasScalarRunner(parsed)) {
13481
13867
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -13510,7 +13896,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
13510
13896
  try {
13511
13897
  const file = findSessionFile(sessionsDir, sessionId, logger);
13512
13898
  if (file) {
13513
- fs9.unlinkSync(path16.join(sessionsDir, file));
13899
+ fs9.unlinkSync(path17.join(sessionsDir, file));
13514
13900
  return true;
13515
13901
  }
13516
13902
  } catch (err) {
@@ -14042,26 +14428,30 @@ var Session = class {
14042
14428
  */
14043
14429
  forkConversation() {
14044
14430
  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 };
14431
+ return this.saveFork(this.conversation.clone());
14050
14432
  }
14051
14433
  /**
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.
14434
+ * Fork the conversation from just before a user message (a transcript
14435
+ * position from {@link rewindTargets}): the fork holds everything said
14436
+ * before it and the current task list, like {@link forkConversation}, and
14437
+ * this session keeps its whole history. The message's full text comes back
14438
+ * so a surface can offer it for resending.
14056
14439
  */
14057
14440
  rewindConversation(userMessageIndex) {
14058
14441
  if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
14059
- return this.conversation.rewind(userMessageIndex);
14442
+ const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
14443
+ return { ...this.saveFork(dialogue), rewoundMessage };
14444
+ }
14445
+ saveFork(dialogue) {
14446
+ const sessionId = mintSessionId();
14447
+ const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
14448
+ saveSession(plan, this.goal, this.workspace, sessionId);
14449
+ return { sessionId, goal: this.goal, workspace: this.workspace };
14060
14450
  }
14061
14451
  /**
14062
14452
  * Condense the conversation on the user's say-so: a hidden planner turn
14063
14453
  * summarises it, and the summary replaces everything but the last two
14064
- * exchanges. Conversation only, like a rewind — the tasks, and any run
14454
+ * exchanges. Conversation only — the tasks, and any run
14065
14455
  * executing them, are untouched. Atomic: a failed or stopped turn changes
14066
14456
  * nothing.
14067
14457
  */
@@ -14334,8 +14724,9 @@ var Session = class {
14334
14724
  * The opt-in way through a merge conflict: add an AI task, on the conflicted
14335
14725
  * task's own runner and model, that merges its branch by hand in a worktree
14336
14726
  * 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.
14727
+ * {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
14728
+ * conflict is a repair, a new attempt of the same task (ADR-0015); a task is
14729
+ * only ever added to the plan by this call.
14339
14730
  */
14340
14731
  async resolveConflictAsTask(taskId) {
14341
14732
  if (!this.plan) return null;
@@ -15088,8 +15479,8 @@ var HeadlessRunner = class extends AbstractRunner {
15088
15479
  import { execFile as execFile3 } from "child_process";
15089
15480
  import { promisify as promisify5 } from "util";
15090
15481
  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";
15482
+ import { join as join16 } from "path";
15483
+ import { existsSync as existsSync11, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
15093
15484
 
15094
15485
  // src/utils/tmux.ts
15095
15486
  import { execSync as execSync2 } from "child_process";
@@ -15196,7 +15587,7 @@ var TmuxSession = class extends AbstractTerminalSession {
15196
15587
  if (this.exited) return;
15197
15588
  let content;
15198
15589
  try {
15199
- content = existsSync10(this.logPath) ? readFileSync6(this.logPath, "utf8") : "";
15590
+ content = existsSync11(this.logPath) ? readFileSync7(this.logPath, "utf8") : "";
15200
15591
  } catch {
15201
15592
  return;
15202
15593
  }
@@ -15375,7 +15766,7 @@ var TmuxRunner = class extends AbstractRunner {
15375
15766
  windowName,
15376
15767
  this.execFileImpl,
15377
15768
  this.pollIntervalMs,
15378
- join15(this.logDir, `${id}.log`),
15769
+ join16(this.logDir, `${id}.log`),
15379
15770
  this.socket
15380
15771
  );
15381
15772
  invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
@@ -15388,8 +15779,8 @@ var TmuxRunner = class extends AbstractRunner {
15388
15779
 
15389
15780
  // src/utils/daemonToken.ts
15390
15781
  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";
15782
+ import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeSync } from "fs";
15783
+ import { join as join17 } from "path";
15393
15784
  var BEARER_PREFIX = "bearer ";
15394
15785
  var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
15395
15786
  var DAEMON_SUBPROTOCOL = "ordewell.v1";
@@ -15397,12 +15788,12 @@ function configDir() {
15397
15788
  return globalDataDir();
15398
15789
  }
15399
15790
  function daemonTokenPath(port) {
15400
- return join16(configDir(), `server-${port}.token`);
15791
+ return join17(configDir(), `server-${port}.token`);
15401
15792
  }
15402
15793
  function mintDaemonToken(port) {
15403
15794
  const token = randomBytes3(32).toString("base64url");
15404
15795
  const file = daemonTokenPath(port);
15405
- mkdirSync8(configDir(), { recursive: true });
15796
+ mkdirSync9(configDir(), { recursive: true });
15406
15797
  try {
15407
15798
  unlinkSync3(file);
15408
15799
  } catch {
@@ -15417,7 +15808,7 @@ function mintDaemonToken(port) {
15417
15808
  }
15418
15809
  function readDaemonToken(port) {
15419
15810
  try {
15420
- const token = readFileSync7(daemonTokenPath(port), "utf8").trim();
15811
+ const token = readFileSync8(daemonTokenPath(port), "utf8").trim();
15421
15812
  return token === "" ? void 0 : token;
15422
15813
  } catch {
15423
15814
  return void 0;
@@ -15459,17 +15850,17 @@ function tokensMatch(presented, expected) {
15459
15850
 
15460
15851
  // src/plugins/RunnerRegistry.ts
15461
15852
  import * as fs11 from "fs";
15462
- import * as path19 from "path";
15853
+ import * as path20 from "path";
15463
15854
  import * as os5 from "os";
15464
15855
  import { execFileSync } from "child_process";
15465
15856
  import { randomBytes as randomBytes4 } from "crypto";
15466
15857
 
15467
15858
  // src/plugins/FsPluginStore.ts
15468
15859
  import * as fs10 from "fs";
15469
- import * as path18 from "path";
15860
+ import * as path19 from "path";
15470
15861
 
15471
15862
  // src/plugins/pluginNames.ts
15472
- import * as path17 from "path";
15863
+ import * as path18 from "path";
15473
15864
  var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
15474
15865
  var MAX_PLUGIN_NAME_LENGTH = 64;
15475
15866
  function isPlainPluginName(name) {
@@ -15484,10 +15875,10 @@ function assertPlainPluginName(name) {
15484
15875
  }
15485
15876
  function resolvePluginInstallDir(pluginsDir, name) {
15486
15877
  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)) {
15878
+ const root = path18.resolve(pluginsDir);
15879
+ const dest = path18.resolve(root, name);
15880
+ const relative5 = path18.relative(root, dest);
15881
+ if (relative5 !== name || relative5 === "" || path18.isAbsolute(relative5)) {
15491
15882
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
15492
15883
  }
15493
15884
  return dest;
@@ -15502,10 +15893,10 @@ function isValidManifest(obj) {
15502
15893
 
15503
15894
  // src/plugins/FsPluginStore.ts
15504
15895
  function userPluginsDir() {
15505
- return path18.join(globalDataDir(), "plugins");
15896
+ return path19.join(globalDataDir(), "plugins");
15506
15897
  }
15507
15898
  function loadManifestFromDir(dir) {
15508
- const manifestPath = path18.join(dir, "manifest.json");
15899
+ const manifestPath = path19.join(dir, "manifest.json");
15509
15900
  if (!fs10.existsSync(manifestPath)) return null;
15510
15901
  try {
15511
15902
  const raw = fs10.readFileSync(manifestPath, "utf8");
@@ -15520,8 +15911,8 @@ function copyDirSync3(src, dest) {
15520
15911
  ensureDir(dest);
15521
15912
  const entries = fs10.readdirSync(src, { withFileTypes: true });
15522
15913
  for (const entry of entries) {
15523
- const srcPath = path18.join(src, entry.name);
15524
- const destPath = path18.join(dest, entry.name);
15914
+ const srcPath = path19.join(src, entry.name);
15915
+ const destPath = path19.join(dest, entry.name);
15525
15916
  if (entry.isDirectory()) {
15526
15917
  copyDirSync3(srcPath, destPath);
15527
15918
  } else {
@@ -15533,7 +15924,7 @@ function rmDirSync(dir) {
15533
15924
  if (!fs10.existsSync(dir)) return;
15534
15925
  const entries = fs10.readdirSync(dir, { withFileTypes: true });
15535
15926
  for (const entry of entries) {
15536
- const p = path18.join(dir, entry.name);
15927
+ const p = path19.join(dir, entry.name);
15537
15928
  if (entry.isDirectory()) {
15538
15929
  rmDirSync(p);
15539
15930
  } else {
@@ -15586,11 +15977,11 @@ var FsPluginStore = class {
15586
15977
  return null;
15587
15978
  }
15588
15979
  }
15589
- dirExists(path22) {
15590
- return fs10.existsSync(path22) && fs10.statSync(path22).isDirectory();
15980
+ dirExists(path23) {
15981
+ return fs10.existsSync(path23) && fs10.statSync(path23).isDirectory();
15591
15982
  }
15592
- exists(path22) {
15593
- return fs10.existsSync(path22);
15983
+ exists(path23) {
15984
+ return fs10.existsSync(path23);
15594
15985
  }
15595
15986
  };
15596
15987
 
@@ -16022,7 +16413,7 @@ var RunnerRegistry = class {
16022
16413
  return entry?.source === "builtin";
16023
16414
  }
16024
16415
  installFromPath(sourcePath) {
16025
- const absPath = path19.resolve(sourcePath);
16416
+ const absPath = path20.resolve(sourcePath);
16026
16417
  if (!this.store.dirExists(absPath)) {
16027
16418
  throw new Error(`Plugin source must be a directory: ${absPath}`);
16028
16419
  }
@@ -16034,7 +16425,7 @@ var RunnerRegistry = class {
16034
16425
  }
16035
16426
  installFromGit(url) {
16036
16427
  const validated = assertInstallablePluginUrl(url);
16037
- const tmpDir = path19.join(os5.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
16428
+ const tmpDir = path20.join(os5.tmpdir(), `ordewell-plugin-${randomBytes4(12).toString("hex")}`);
16038
16429
  this.store.ensureDir(tmpDir);
16039
16430
  try {
16040
16431
  try {
@@ -16047,7 +16438,7 @@ var RunnerRegistry = class {
16047
16438
  const hasManifest = contents.some((f) => f === "manifest.json");
16048
16439
  let sourceDir = tmpDir;
16049
16440
  if (!hasManifest) {
16050
- const onlyEntry = contents.length === 1 ? path19.join(tmpDir, contents[0]) : void 0;
16441
+ const onlyEntry = contents.length === 1 ? path20.join(tmpDir, contents[0]) : void 0;
16051
16442
  if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
16052
16443
  throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
16053
16444
  }
@@ -16095,7 +16486,7 @@ var RunnerRegistry = class {
16095
16486
  }
16096
16487
  createSkeleton(name, outputDir) {
16097
16488
  assertPlainPluginName(name);
16098
- const dir = path19.resolve(outputDir, name);
16489
+ const dir = path20.resolve(outputDir, name);
16099
16490
  this.store.ensureDir(dir);
16100
16491
  const manifest = {
16101
16492
  name,
@@ -16127,8 +16518,8 @@ var RunnerRegistry = class {
16127
16518
  ]
16128
16519
  }
16129
16520
  };
16130
- this.store.writeFile(path19.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
16131
- const runSh = path19.join(dir, "run.sh");
16521
+ this.store.writeFile(path20.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
16522
+ const runSh = path20.join(dir, "run.sh");
16132
16523
  const scriptContent = `#!/usr/bin/env bash
16133
16524
  # Custom runner script for ${name}
16134
16525
  # Receives:
@@ -16161,14 +16552,14 @@ function fsChmodSync(fpath, mode) {
16161
16552
 
16162
16553
  // src/utils/stateStore.ts
16163
16554
  import * as fs12 from "fs";
16164
- import * as path20 from "path";
16555
+ import * as path21 from "path";
16165
16556
  var STATE_FILE = "state.json";
16166
16557
  function getStatePath(baseDir) {
16167
- return path20.join(getStateDir(baseDir), STATE_FILE);
16558
+ return path21.join(getStateDir(baseDir), STATE_FILE);
16168
16559
  }
16169
16560
  function saveState(plan, baseDir) {
16170
16561
  const statePath = getStatePath(baseDir ?? process.cwd());
16171
- ensureDir(path20.dirname(statePath));
16562
+ ensureDir(path21.dirname(statePath));
16172
16563
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
16173
16564
  fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
16174
16565
  }
@@ -16203,7 +16594,7 @@ function stateExists(baseDir) {
16203
16594
 
16204
16595
  // src/services/SettingsService.ts
16205
16596
  import * as fs13 from "fs";
16206
- import * as path21 from "path";
16597
+ import * as path22 from "path";
16207
16598
  var DEFAULTS = {
16208
16599
  tdd: { enabled: true },
16209
16600
  verification: { enabled: false }
@@ -16211,7 +16602,7 @@ var DEFAULTS = {
16211
16602
  function getSettingsPath() {
16212
16603
  const override = process.env.ORDEWELL_SETTINGS_PATH;
16213
16604
  if (override && override.trim()) return override.trim();
16214
- return path21.join(globalDataDir(), "settings.json");
16605
+ return path22.join(globalDataDir(), "settings.json");
16215
16606
  }
16216
16607
  function readPlannerModels(raw) {
16217
16608
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -16339,7 +16730,7 @@ var SettingsService = class {
16339
16730
  return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
16340
16731
  }
16341
16732
  persist() {
16342
- const dir = path21.dirname(this.filePath);
16733
+ const dir = path22.dirname(this.filePath);
16343
16734
  if (!fs13.existsSync(dir)) {
16344
16735
  fs13.mkdirSync(dir, { recursive: true });
16345
16736
  }
@@ -16482,6 +16873,7 @@ export {
16482
16873
  augmentPromptWithPriorOutputs,
16483
16874
  augmentedPath,
16484
16875
  bearerHeaderValue,
16876
+ buildConflictRepairPrompt,
16485
16877
  buildConflictResolutionPrompt,
16486
16878
  buildConversationSystemPrompt,
16487
16879
  buildFallbackGrepArgs,
@@ -16501,6 +16893,7 @@ export {
16501
16893
  canMergeTasks,
16502
16894
  canSetDependencies,
16503
16895
  canSplitTask,
16896
+ capConflictFiles,
16504
16897
  checkDepsResolve,
16505
16898
  checkImmutableLog,
16506
16899
  checkInProgress,