@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.js CHANGED
@@ -129,6 +129,7 @@ __export(index_exports, {
129
129
  augmentPromptWithPriorOutputs: () => augmentPromptWithPriorOutputs,
130
130
  augmentedPath: () => augmentedPath,
131
131
  bearerHeaderValue: () => bearerHeaderValue,
132
+ buildConflictRepairPrompt: () => buildConflictRepairPrompt,
132
133
  buildConflictResolutionPrompt: () => buildConflictResolutionPrompt,
133
134
  buildConversationSystemPrompt: () => buildConversationSystemPrompt,
134
135
  buildFallbackGrepArgs: () => buildFallbackGrepArgs,
@@ -148,6 +149,7 @@ __export(index_exports, {
148
149
  canMergeTasks: () => canMergeTasks,
149
150
  canSetDependencies: () => canSetDependencies,
150
151
  canSplitTask: () => canSplitTask,
152
+ capConflictFiles: () => capConflictFiles,
151
153
  checkDepsResolve: () => checkDepsResolve,
152
154
  checkImmutableLog: () => checkImmutableLog,
153
155
  checkInProgress: () => checkInProgress,
@@ -3323,14 +3325,14 @@ var PendingApprovals = class {
3323
3325
  ask(request) {
3324
3326
  const id = `ap-${Date.now()}-${counter++}`;
3325
3327
  const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
3326
- return new Promise((resolve5) => {
3328
+ return new Promise((resolve6) => {
3327
3329
  const settle = (granted) => {
3328
3330
  const entry = this.entries.get(id);
3329
3331
  if (!entry) return;
3330
3332
  clearTimeout(entry.timer);
3331
3333
  this.entries.delete(id);
3332
3334
  this.opts.onSettled?.(id, granted);
3333
- resolve5(granted);
3335
+ resolve6(granted);
3334
3336
  };
3335
3337
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
3336
3338
  timer.unref?.();
@@ -4028,6 +4030,10 @@ function normalizeGeminiModel(id) {
4028
4030
  function commaList(raw) {
4029
4031
  return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
4030
4032
  }
4033
+ function nonNegativeInt(raw, fallback) {
4034
+ const trimmed = (raw ?? "").trim();
4035
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
4036
+ }
4031
4037
  var BaseConfig = class {
4032
4038
  // --- Legacy named getters (backward compat) ---
4033
4039
  get openAiBaseUrl() {
@@ -4250,6 +4256,9 @@ var BaseConfig = class {
4250
4256
  get worktreeLinks() {
4251
4257
  return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
4252
4258
  }
4259
+ get conflictRepairAttempts() {
4260
+ return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
4261
+ }
4253
4262
  get approvalMode() {
4254
4263
  const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
4255
4264
  return raw === "allow" || raw === "deny" ? raw : "ask";
@@ -4309,21 +4318,33 @@ var ISOLATION_STATE = {
4309
4318
  active: "active",
4310
4319
  merged: "integrated",
4311
4320
  conflict: "conflict",
4321
+ repairing: "repairing",
4312
4322
  kept: "kept",
4313
4323
  failed: "kept"
4314
4324
  };
4315
- function taskIsolationOf(record) {
4325
+ function taskIsolationOf(record, repairLimit) {
4316
4326
  return {
4317
4327
  state: ISOLATION_STATE[record.status],
4318
4328
  branch: record.branch,
4319
4329
  worktree: record.workspace,
4320
4330
  repos: Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath),
4321
- ...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {}
4331
+ ...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
4332
+ ...record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {},
4333
+ ...record.repairs ? { repair: { attempt: record.repairs, limit: repairLimit } } : {},
4334
+ ...record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}
4322
4335
  };
4323
4336
  }
4337
+ function capConflictFiles(files, max = 5) {
4338
+ return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
4339
+ }
4324
4340
  function handoffOf(run) {
4325
4341
  const merged = Object.values(run.tasks).filter((r) => r.status === "merged").sort((a, b) => a.order - b.order);
4326
- const entry = (r) => ({ taskId: r.taskId, order: r.order, title: r.title });
4342
+ const entry = (r) => ({
4343
+ taskId: r.taskId,
4344
+ order: r.order,
4345
+ title: r.title,
4346
+ ...r.repairedFiles?.length ? { repairedFiles: r.repairedFiles } : {}
4347
+ });
4327
4348
  return {
4328
4349
  repos: run.repos.map((repo) => ({
4329
4350
  path: repo.path,
@@ -4387,13 +4408,17 @@ function landedNotice(landed) {
4387
4408
  if (landed.length === 0) return "Nothing was merged.";
4388
4409
  return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
4389
4410
  }
4390
- function describeMergeResult(result, branch, group) {
4411
+ function repairedNotice(repaired) {
4412
+ const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
4413
+ const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
4414
+ return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
4415
+ }
4416
+ function describeMergeResult(result, branch, group, repaired = []) {
4391
4417
  switch (result.outcome) {
4392
- case "merged":
4393
- return {
4394
- level: "info",
4395
- message: group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`
4396
- };
4418
+ case "merged": {
4419
+ const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
4420
+ return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
4421
+ }
4397
4422
  case "blocked":
4398
4423
  return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
4399
4424
  case "conflict":
@@ -4414,7 +4439,7 @@ var import_child_process2 = require("child_process");
4414
4439
  var import_util2 = require("util");
4415
4440
  var import_crypto = require("crypto");
4416
4441
  var fs4 = __toESM(require("fs"));
4417
- var path8 = __toESM(require("path"));
4442
+ var path9 = __toESM(require("path"));
4418
4443
 
4419
4444
  // src/utils/shellPath.ts
4420
4445
  var import_child_process = require("child_process");
@@ -4443,28 +4468,28 @@ function posixBinDirs() {
4443
4468
  ];
4444
4469
  }
4445
4470
  function windowsBinDirs(env, home) {
4446
- const join22 = (...parts) => path5.win32.join(...parts);
4447
- const under = (base, ...parts) => base ? [join22(base, ...parts)] : [];
4471
+ const join23 = (...parts) => path5.win32.join(...parts);
4472
+ const under = (base, ...parts) => base ? [join23(base, ...parts)] : [];
4448
4473
  const appData = env.APPDATA;
4449
4474
  const localAppData = env.LOCALAPPDATA;
4450
4475
  const programData = env.ProgramData;
4451
4476
  return [
4452
4477
  // Native per-user installers, PowerShell-driven and otherwise.
4453
- join22(home, ".local", "bin"),
4454
- join22(home, ".opencode", "bin"),
4455
- join22(home, ".claude", "bin"),
4456
- join22(home, ".codex", "bin"),
4478
+ join23(home, ".local", "bin"),
4479
+ join23(home, ".opencode", "bin"),
4480
+ join23(home, ".claude", "bin"),
4481
+ join23(home, ".codex", "bin"),
4457
4482
  ...under(localAppData, "Programs", "claude"),
4458
4483
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
4459
4484
  // literal spelling is kept as a fallback for a host that does not export
4460
4485
  // APPDATA (a service account, a stripped CI container).
4461
4486
  ...under(appData, "npm"),
4462
- join22(home, "AppData", "Roaming", "npm"),
4487
+ join23(home, "AppData", "Roaming", "npm"),
4463
4488
  ...under(localAppData, "pnpm"),
4464
4489
  ...under(localAppData, "Yarn", "bin"),
4465
- join22(home, ".bun", "bin"),
4490
+ join23(home, ".bun", "bin"),
4466
4491
  // Windows package managers.
4467
- join22(home, "scoop", "shims"),
4492
+ join23(home, "scoop", "shims"),
4468
4493
  ...under(env.SCOOP, "shims"),
4469
4494
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
4470
4495
  ...under(programData, "chocolatey", "bin"),
@@ -4474,7 +4499,7 @@ function windowsBinDirs(env, home) {
4474
4499
  ...under(localAppData, "Microsoft", "WindowsApps"),
4475
4500
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
4476
4501
  ...under(localAppData, "Volta", "bin"),
4477
- join22(home, ".volta", "bin")
4502
+ join23(home, ".volta", "bin")
4478
4503
  ];
4479
4504
  }
4480
4505
  function wellKnownBinDirs(deps = {}) {
@@ -4578,6 +4603,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
4578
4603
 
4579
4604
  // src/services/worktreeLink.ts
4580
4605
  var fs3 = __toESM(require("fs"));
4606
+ var path8 = __toESM(require("path"));
4581
4607
  function linkPath(source, target, platform, hardLink = fs3.linkSync) {
4582
4608
  const isDir = fs3.statSync(source).isDirectory();
4583
4609
  if (platform !== "win32") {
@@ -4596,6 +4622,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
4596
4622
  return "copy";
4597
4623
  }
4598
4624
  }
4625
+ function mirrorDir(source, target, opts) {
4626
+ const copied = [];
4627
+ const links = [];
4628
+ const mirror = (from, to, prefix, top) => {
4629
+ fs3.mkdirSync(to, { recursive: true });
4630
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
4631
+ const name = path8.join(prefix, entry.name);
4632
+ const at = { from: path8.join(from, entry.name), to: path8.join(to, entry.name), name };
4633
+ if (entry.isSymbolicLink()) links.push(at);
4634
+ else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
4635
+ else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
4636
+ }
4637
+ };
4638
+ mirror(source, target, "", true);
4639
+ let pending = links;
4640
+ for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
4641
+ before = pending.length;
4642
+ pending = pending.filter((link) => {
4643
+ const kind = relink(link.from, link.to, opts);
4644
+ if (kind === "copy") copied.push(link.name);
4645
+ return kind === null;
4646
+ });
4647
+ }
4648
+ return copied;
4649
+ }
4650
+ function relink(from, to, opts) {
4651
+ const text = fs3.readlinkSync(from);
4652
+ const pointee = path8.isAbsolute(text) ? rebased(text, opts) : text;
4653
+ if (opts.platform !== "win32") {
4654
+ if (!fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) && fs3.existsSync(path8.resolve(path8.dirname(from), pointee))) {
4655
+ fs3.symlinkSync(rebased(path8.resolve(path8.dirname(from), pointee), opts), to);
4656
+ return "symlink";
4657
+ }
4658
+ fs3.symlinkSync(pointee, to);
4659
+ return "symlink";
4660
+ }
4661
+ const resolved = fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) ? path8.resolve(path8.dirname(to), pointee) : rebased(path8.resolve(path8.dirname(from), pointee), opts);
4662
+ try {
4663
+ fs3.statSync(resolved);
4664
+ } catch {
4665
+ return null;
4666
+ }
4667
+ return linkPath(resolved, to, opts.platform, opts.hardLink);
4668
+ }
4669
+ function rebased(pointee, opts) {
4670
+ const rel = path8.relative(opts.from, pointee);
4671
+ const inside = rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
4672
+ return inside && !rel.split(/[\\/]/).includes("node_modules") ? path8.join(opts.to, rel) : pointee;
4673
+ }
4599
4674
 
4600
4675
  // src/services/GitWorktreeIsolation.ts
4601
4676
  var execFileAsync = (0, import_util2.promisify)(import_child_process2.execFile);
@@ -4610,15 +4685,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
4610
4685
  var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
4611
4686
  var isEnvFile = (name) => name.startsWith(".env");
4612
4687
  var INTEGRATION_DIR = "integration";
4688
+ var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
4689
+ var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
4613
4690
  var NESTED_REPO_DEPTH = 2;
4614
4691
  var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
4615
4692
  var GITLINK_MODE = "160000";
4616
4693
  function isInside(parent, child) {
4617
- const rel = path8.relative(parent, child);
4618
- return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
4694
+ const rel = path9.relative(parent, child);
4695
+ return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
4619
4696
  }
4620
4697
  function hasGitEntry(dir) {
4621
- return fs4.existsSync(path8.join(dir, ".git"));
4698
+ return fs4.existsSync(path9.join(dir, ".git"));
4622
4699
  }
4623
4700
  function firstLine(text) {
4624
4701
  return text.split(/\r?\n/, 1)[0].trim();
@@ -4634,12 +4711,20 @@ function statusPaths(porcelain) {
4634
4711
  }
4635
4712
  return paths;
4636
4713
  }
4714
+ function leftoverMarkerFiles(check) {
4715
+ const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
4716
+ return [...new Set(files)];
4717
+ }
4718
+ function settleStatus(record, status) {
4719
+ record.status = status;
4720
+ delete record.repairBase;
4721
+ }
4637
4722
  function sameDir(a, b) {
4638
4723
  const real = (p) => {
4639
4724
  try {
4640
4725
  return fs4.realpathSync.native(p);
4641
4726
  } catch {
4642
- return path8.resolve(p);
4727
+ return path9.resolve(p);
4643
4728
  }
4644
4729
  };
4645
4730
  return real(a) === real(b);
@@ -4673,8 +4758,8 @@ function inactive(reason, repos) {
4673
4758
  }
4674
4759
  function groupPathOf(listed) {
4675
4760
  const slashed = listed.trim().replace(/\\/g, "/");
4676
- if (path8.posix.isAbsolute(slashed) || path8.win32.isAbsolute(slashed)) return null;
4677
- const rel = path8.posix.normalize(slashed).replace(/\/+$/, "");
4761
+ if (path9.posix.isAbsolute(slashed) || path9.win32.isAbsolute(slashed)) return null;
4762
+ const rel = path9.posix.normalize(slashed).replace(/\/+$/, "");
4678
4763
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
4679
4764
  }
4680
4765
  function matchLinks(root, patterns) {
@@ -4690,9 +4775,26 @@ function matchLinks(root, patterns) {
4690
4775
  }
4691
4776
  function segmentMatches(root, base, segment) {
4692
4777
  const under = (name) => base ? `${base}/${name}` : name;
4693
- if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
4778
+ if (!/[*?]/.test(segment)) return lexists(path9.join(root, base, segment)) ? [under(segment)] : [];
4694
4779
  const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
4695
- return listDir(path8.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4780
+ return listDir(path9.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4781
+ }
4782
+ function installDirs(root) {
4783
+ const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
4784
+ return ["node_modules", ...packages].filter((dir) => lexists(path9.join(root, dir)));
4785
+ }
4786
+ function workspaceGlobs(root) {
4787
+ let manifest;
4788
+ try {
4789
+ manifest = JSON.parse(fs4.readFileSync(path9.join(root, "package.json"), "utf8"));
4790
+ } catch {
4791
+ return [];
4792
+ }
4793
+ if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
4794
+ const { workspaces } = manifest;
4795
+ const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
4796
+ if (!Array.isArray(listed)) return [];
4797
+ return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
4696
4798
  }
4697
4799
  var GitWorktreeIsolation = class {
4698
4800
  constructor(deps) {
@@ -4746,7 +4848,7 @@ var GitWorktreeIsolation = class {
4746
4848
  groupPaths(workspaceRoot) {
4747
4849
  const listed = this.deps.config.workspaceRepos;
4748
4850
  if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
4749
- const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
4851
+ const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path9.join(workspaceRoot, rel)));
4750
4852
  return [...new Set(paths)].sort();
4751
4853
  }
4752
4854
  async committedRepos(workspaceRoot, paths) {
@@ -4762,7 +4864,7 @@ var GitWorktreeIsolation = class {
4762
4864
  return status.ok ? status.stdout.trim() !== "" : null;
4763
4865
  }
4764
4866
  reposDirectlyIn(dir) {
4765
- return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
4867
+ return this.childDirs(dir).filter((name) => hasGitEntry(path9.join(dir, name)));
4766
4868
  }
4767
4869
  childDirs(dir) {
4768
4870
  try {
@@ -4779,9 +4881,9 @@ var GitWorktreeIsolation = class {
4779
4881
  async nestedRepos(workspaceRoot, toplevel) {
4780
4882
  const found = [];
4781
4883
  const walk = (rel, depth) => {
4782
- for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
4884
+ for (const name of this.childDirs(path9.join(workspaceRoot, rel))) {
4783
4885
  const child = rel ? `${rel}/${name}` : name;
4784
- if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
4886
+ if (hasGitEntry(path9.join(workspaceRoot, child))) found.push(child);
4785
4887
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
4786
4888
  }
4787
4889
  };
@@ -4802,7 +4904,7 @@ var GitWorktreeIsolation = class {
4802
4904
  const [meta, file] = entry.split(" ");
4803
4905
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
4804
4906
  }
4805
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4907
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path9.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4806
4908
  for (const entry of declared.stdout.split("\0")) {
4807
4909
  const value = entry.split("\n")[1];
4808
4910
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -4858,7 +4960,7 @@ var GitWorktreeIsolation = class {
4858
4960
  async acceptsWorktree(run, repo) {
4859
4961
  const dir = this.integrationDir(run, repo);
4860
4962
  ensureStateDirIgnored(run.workspaceRoot);
4861
- fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4963
+ fs4.mkdirSync(path9.dirname(dir), { recursive: true });
4862
4964
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
4863
4965
  await this.removeWorktreeDir(run, repo, dir, []);
4864
4966
  return added.ok;
@@ -4873,12 +4975,12 @@ var GitWorktreeIsolation = class {
4873
4975
  if (isolated.includes(SELF_REPO)) return [];
4874
4976
  const shared = [];
4875
4977
  const walk = (rel) => {
4876
- for (const name of listDir(path8.join(workspaceRoot, rel))) {
4978
+ for (const name of listDir(path9.join(workspaceRoot, rel))) {
4877
4979
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
4878
4980
  const child = rel ? `${rel}/${name}` : name;
4879
4981
  if (isolated.includes(child)) continue;
4880
4982
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
4881
- else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
4983
+ else if (resolves(path9.join(workspaceRoot, child))) shared.push(child);
4882
4984
  }
4883
4985
  };
4884
4986
  walk("");
@@ -4895,17 +4997,17 @@ var GitWorktreeIsolation = class {
4895
4997
  const copied = [];
4896
4998
  try {
4897
4999
  for (const repo of run.repos) {
4898
- const worktree = path8.join(dir, repo.path);
5000
+ const worktree = path9.join(dir, repo.path);
4899
5001
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
4900
- fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
5002
+ fs4.mkdirSync(path9.dirname(worktree), { recursive: true });
4901
5003
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
4902
5004
  record.repos[repo.path] = { worktree, linked: [] };
4903
5005
  }
4904
5006
  run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
4905
5007
  for (const rel of run.shared) {
4906
- const target = path8.join(dir, rel);
4907
- fs4.mkdirSync(path8.dirname(target), { recursive: true });
4908
- if (linkPath(path8.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
5008
+ const target = path9.join(dir, rel);
5009
+ fs4.mkdirSync(path9.dirname(target), { recursive: true });
5010
+ if (linkPath(path9.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
4909
5011
  }
4910
5012
  for (const repo of run.repos) {
4911
5013
  const entry = record.repos[repo.path];
@@ -4913,7 +5015,7 @@ var GitWorktreeIsolation = class {
4913
5015
  if (repo.path === SELF_REPO) cwd = inRepo;
4914
5016
  const boot = await this.bootstrap(repo, inRepo);
4915
5017
  entry.linked = boot.linked;
4916
- copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
5018
+ copied.push(...boot.copied.map((name) => path9.posix.join(repo.path, name)));
4917
5019
  }
4918
5020
  } catch (err) {
4919
5021
  record.status = "failed";
@@ -4924,6 +5026,53 @@ var GitWorktreeIsolation = class {
4924
5026
  return { cwd, branch, copied };
4925
5027
  });
4926
5028
  }
5029
+ reopen(task, run) {
5030
+ return this.admin(run.workspaceRoot, async () => {
5031
+ const record = run.tasks[task.id];
5032
+ if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
5033
+ let cwd = record.workspace;
5034
+ const repairBase = {};
5035
+ for (const repo of run.repos) {
5036
+ const entry = record.repos[repo.path];
5037
+ if (!entry) continue;
5038
+ if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
5039
+ if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
5040
+ if (entry.changed || repo.path === record.conflictRepo) {
5041
+ repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
5042
+ }
5043
+ }
5044
+ const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
5045
+ record.repairs = (record.repairs ?? 0) + 1;
5046
+ record.repairBase = repairBase;
5047
+ record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
5048
+ record.status = "repairing";
5049
+ return { cwd, branch: record.branch, copied: [] };
5050
+ });
5051
+ }
5052
+ async verifyRepair(task, run) {
5053
+ const record = run.tasks[task.id];
5054
+ if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
5055
+ for (const repo of run.repos) {
5056
+ const entry = record.repos[repo.path];
5057
+ if (!entry) continue;
5058
+ try {
5059
+ await this.commitWorktree(repo, record, entry);
5060
+ } catch {
5061
+ return { ok: false, reason: "failed", repo: repo.path };
5062
+ }
5063
+ }
5064
+ for (const repo of run.repos) {
5065
+ const base = record.repairBase[repo.path];
5066
+ if (base === void 0) continue;
5067
+ const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
5068
+ if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
5069
+ const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
5070
+ if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
5071
+ const files = leftoverMarkerFiles(check.stdout);
5072
+ if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
5073
+ }
5074
+ return { ok: true };
5075
+ }
4927
5076
  /**
4928
5077
  * The place in a repo's worktree that matches where the workspace sits in
4929
5078
  * the real repo: the worktree itself, or a subdirectory of it when the
@@ -4931,7 +5080,7 @@ var GitWorktreeIsolation = class {
4931
5080
  */
4932
5081
  async inWorkspacePlace(repo, worktree) {
4933
5082
  const prefix = await this.prefixOf(repo);
4934
- return prefix ? path8.join(worktree, prefix) : worktree;
5083
+ return prefix ? path9.join(worktree, prefix) : worktree;
4935
5084
  }
4936
5085
  async prefixOf(repo) {
4937
5086
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -4951,6 +5100,7 @@ var GitWorktreeIsolation = class {
4951
5100
  if (!record) return;
4952
5101
  if (opts.keep) {
4953
5102
  if (record.status === "active") record.status = "kept";
5103
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4954
5104
  return;
4955
5105
  }
4956
5106
  await this.removeTask(run, record, { dropRecord: true });
@@ -4970,6 +5120,7 @@ var GitWorktreeIsolation = class {
4970
5120
  for (const record of Object.values(run.tasks)) {
4971
5121
  if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
4972
5122
  else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
5123
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4973
5124
  }
4974
5125
  await this.removeUnowned(run);
4975
5126
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
@@ -5064,14 +5215,59 @@ var GitWorktreeIsolation = class {
5064
5215
  await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
5065
5216
  }
5066
5217
  await this.removeUnowned(run);
5067
- if (!opts.keepIntegration) {
5068
- for (const repo of run.repos) await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
5218
+ if (opts.integration !== "keep") {
5219
+ for (const repo of run.repos) {
5220
+ if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
5221
+ else await this.deleteIfMerged(repo.root, repo.integrationBranch);
5222
+ }
5069
5223
  run.tasks = {};
5070
5224
  this.removeIfEmpty(this.runRoot(run));
5071
5225
  }
5072
5226
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
5073
5227
  });
5074
5228
  }
5229
+ sweep(run) {
5230
+ return this.admin(run.workspaceRoot, async () => {
5231
+ const failed = [];
5232
+ for (const repo of run.repos) {
5233
+ if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
5234
+ }
5235
+ if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
5236
+ });
5237
+ }
5238
+ /** False when git failed anywhere; the other branches are still tried. */
5239
+ async sweepRepo(run, repo) {
5240
+ await this.tryGit(repo.root, ["worktree", "prune"]);
5241
+ const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
5242
+ const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
5243
+ if (!worktrees.ok || !refs.ok) return false;
5244
+ const checkedOut = /* @__PURE__ */ new Set();
5245
+ const liveRuns = /* @__PURE__ */ new Set();
5246
+ for (const line of worktrees.stdout.split(/\r?\n/)) {
5247
+ if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
5248
+ const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
5249
+ if (runId) liveRuns.add(runId);
5250
+ }
5251
+ let ok = true;
5252
+ for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
5253
+ const branch = ref.slice("refs/heads/".length);
5254
+ const runId = RUN_BRANCH.exec(branch)?.[1];
5255
+ if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
5256
+ if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
5257
+ }
5258
+ return ok;
5259
+ }
5260
+ /**
5261
+ * Delete `branch` only if the repo's checked-out HEAD already contains it —
5262
+ * HEAD, not an upstream: what the user has checked out is what they merged
5263
+ * into. `-d` rather than `-D`, so git also refuses a branch a worktree has
5264
+ * checked out.
5265
+ */
5266
+ async deleteIfMerged(root, branch) {
5267
+ const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
5268
+ if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
5269
+ return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
5270
+ }
5075
5271
  async drain() {
5076
5272
  try {
5077
5273
  while (this.waiting.length > 0) {
@@ -5124,21 +5320,24 @@ var GitWorktreeIsolation = class {
5124
5320
  return this.stopLanding(record, "failed");
5125
5321
  }
5126
5322
  for (const repo of changed) {
5127
- const outcome = await this.mergeTask(run, repo, record);
5323
+ const { outcome, files } = await this.mergeTask(run, repo, record);
5128
5324
  if (outcome === "merged") continue;
5129
- return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
5325
+ return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
5130
5326
  }
5131
5327
  }
5132
5328
  delete run.landing;
5133
- record.status = "merged";
5329
+ settleStatus(record, "merged");
5134
5330
  delete record.conflictRepo;
5331
+ delete record.conflictFiles;
5135
5332
  await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
5136
5333
  return "merged";
5137
5334
  }
5138
- stopLanding(record, outcome, repo) {
5139
- record.status = outcome;
5335
+ stopLanding(record, outcome, repo, files) {
5336
+ settleStatus(record, outcome);
5140
5337
  if (repo) record.conflictRepo = repo.path;
5141
5338
  else delete record.conflictRepo;
5339
+ if (files && files.length > 0) record.conflictFiles = files;
5340
+ else delete record.conflictFiles;
5142
5341
  return outcome;
5143
5342
  }
5144
5343
  /** Merge the task branch into one repo's integration branch. A merge that does not complete is aborted: it is Ordewell's own. */
@@ -5147,13 +5346,13 @@ var GitWorktreeIsolation = class {
5147
5346
  try {
5148
5347
  dir = await this.ensureIntegrationWorktree(run, repo);
5149
5348
  } catch {
5150
- return "failed";
5349
+ return { outcome: "failed", files: [] };
5151
5350
  }
5152
5351
  const merge = await this.tryGit(dir, ["merge", "--no-ff", "--no-edit", "-m", `Merge task ${record.order}: ${firstLine(record.title)}`, record.branch]);
5153
- if (merge.ok) return "merged";
5154
- const conflicted = (await this.unmergedPaths(dir)).length > 0;
5352
+ if (merge.ok) return { outcome: "merged", files: [] };
5353
+ const files = await this.unmergedPaths(dir);
5155
5354
  if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
5156
- return conflicted ? "conflict" : "failed";
5355
+ return { outcome: files.length > 0 ? "conflict" : "failed", files };
5157
5356
  }
5158
5357
  /**
5159
5358
  * Return every repo of the landing in flight to its recorded tip, then
@@ -5221,15 +5420,15 @@ var GitWorktreeIsolation = class {
5221
5420
  await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
5222
5421
  }
5223
5422
  integrationDir(run, repo) {
5224
- return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
5423
+ return path9.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
5225
5424
  }
5226
5425
  async ensureIntegrationWorktree(run, repo) {
5227
5426
  return this.admin(run.workspaceRoot, async () => {
5228
5427
  const dir = this.integrationDir(run, repo);
5229
- if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
5428
+ if (fs4.existsSync(path9.join(dir, ".git"))) return dir;
5230
5429
  await this.tryGit(repo.root, ["worktree", "prune"]);
5231
5430
  ensureStateDirIgnored(run.workspaceRoot);
5232
- fs4.mkdirSync(path8.dirname(dir), { recursive: true });
5431
+ fs4.mkdirSync(path9.dirname(dir), { recursive: true });
5233
5432
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
5234
5433
  return dir;
5235
5434
  });
@@ -5264,20 +5463,20 @@ var GitWorktreeIsolation = class {
5264
5463
  return;
5265
5464
  }
5266
5465
  for (const entry of entries) {
5267
- const target = path8.join(dir, entry.name);
5466
+ const target = path9.join(dir, entry.name);
5268
5467
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
5269
5468
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
5270
5469
  }
5271
5470
  }
5272
5471
  /** Task workspaces and branches under this run that no record accounts for. */
5273
5472
  async removeUnowned(run) {
5274
- const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
5473
+ const owned = new Set(Object.values(run.tasks).map((r) => path9.resolve(r.workspace)));
5275
5474
  const root = this.runRoot(run);
5276
5475
  if (fs4.existsSync(root)) {
5277
5476
  for (const entry of fs4.readdirSync(root)) {
5278
- const dir = path8.join(root, entry);
5279
- if (entry === INTEGRATION_DIR || owned.has(path8.resolve(dir))) continue;
5280
- for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path8.join(dir, repo.path), []);
5477
+ const dir = path9.join(root, entry);
5478
+ if (entry === INTEGRATION_DIR || owned.has(path9.resolve(dir))) continue;
5479
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path9.join(dir, repo.path), []);
5281
5480
  this.removeTaskWorkspace(run, dir);
5282
5481
  }
5283
5482
  }
@@ -5291,14 +5490,18 @@ var GitWorktreeIsolation = class {
5291
5490
  }
5292
5491
  }
5293
5492
  async removeWorktreeDir(run, repo, dir, linked) {
5294
- for (const name of linked) this.unlinkIfLink(path8.join(dir, name));
5493
+ for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
5494
+ const target = path9.join(dir, name);
5495
+ this.unlinkIfLink(target);
5496
+ this.unlinkLinksUnder(target);
5497
+ }
5295
5498
  for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
5296
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
5499
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path9.join(dir, name));
5297
5500
  }
5298
5501
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
5299
5502
  if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
5300
5503
  await this.tryGit(repo.root, ["worktree", "prune"]);
5301
- for (let parent = path8.dirname(dir); isInside(this.runRoot(run), parent); parent = path8.dirname(parent)) this.removeIfEmpty(parent);
5504
+ for (let parent = path9.dirname(dir); isInside(this.runRoot(run), parent); parent = path9.dirname(parent)) this.removeIfEmpty(parent);
5302
5505
  }
5303
5506
  unlinkIfLink(target) {
5304
5507
  try {
@@ -5313,14 +5516,14 @@ var GitWorktreeIsolation = class {
5313
5516
  }
5314
5517
  }
5315
5518
  runRoot(run) {
5316
- return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
5519
+ return path9.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
5317
5520
  }
5318
5521
  namesFor(run, task) {
5319
5522
  const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
5320
5523
  let name = `${task.order}-${slug2}`;
5321
5524
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
5322
5525
  if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
5323
- return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
5526
+ return { branch: `ordewell/${run.id}/${name}`, dir: path9.join(this.runRoot(run), name) };
5324
5527
  }
5325
5528
  /**
5326
5529
  * Make one repo's worktree runnable: the default artifacts, unless a setup
@@ -5333,16 +5536,26 @@ var GitWorktreeIsolation = class {
5333
5536
  const linked = [];
5334
5537
  const copied = [];
5335
5538
  const link = (name) => {
5336
- const target = path8.join(cwd, name);
5539
+ const target = path9.join(cwd, name);
5337
5540
  if (lexists(target)) return;
5338
- fs4.mkdirSync(path8.dirname(target), { recursive: true });
5339
- if (linkPath(path8.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
5541
+ fs4.mkdirSync(path9.dirname(target), { recursive: true });
5542
+ if (linkPath(path9.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
5543
+ linked.push(name);
5544
+ };
5545
+ const mirror = (name) => {
5546
+ const target = path9.join(cwd, name);
5547
+ if (lexists(target) || !fs4.existsSync(path9.dirname(target))) return;
5548
+ const source = path9.join(repo.root, name);
5549
+ if (!fs4.lstatSync(source).isDirectory()) return link(name);
5550
+ const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
5551
+ copied.push(...mirrored.map((entry) => path9.join(name, entry)));
5340
5552
  linked.push(name);
5341
5553
  };
5342
5554
  if (!setup) {
5343
5555
  for (const name of fs4.readdirSync(repo.root)) {
5344
- if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
5556
+ if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
5345
5557
  }
5558
+ for (const dir of installDirs(repo.root)) mirror(dir);
5346
5559
  }
5347
5560
  for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
5348
5561
  if (setup) await this.runSetup(setup, repo, cwd);
@@ -6004,6 +6217,7 @@ function researchPhaseBlock(harnessMode) {
6004
6217
  }
6005
6218
  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.";
6006
6219
  var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
6220
+ 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.";
6007
6221
  function repoGroupSection(isolated) {
6008
6222
  if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
6009
6223
  return [
@@ -6095,6 +6309,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6095
6309
  "DEPENDENCY & PARALLELISM:",
6096
6310
  "- Independent slices should have NO dependencies \u2014 they run in parallel.",
6097
6311
  "- Only add dependencies when a slice truly depends on artifacts another slice creates.",
6312
+ SHARED_APPEND_FILE_RULE,
6098
6313
  ...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
6099
6314
  ...repoGroupSection(variant.isolatedExecution),
6100
6315
  "",
@@ -6196,6 +6411,7 @@ function corePlannerPrompt(isolatedExecution) {
6196
6411
  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.",
6197
6412
  "- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
6198
6413
  "- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
6414
+ SHARED_APPEND_FILE_RULE,
6199
6415
  isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
6200
6416
  ...repoGroupSection(isolatedExecution),
6201
6417
  "",
@@ -6473,6 +6689,31 @@ ${task.prompt ?? task.description}`;
6473
6689
  asked
6474
6690
  ].join("\n");
6475
6691
  }
6692
+ function buildConflictRepairPrompt(task, conflict, integrationBranch) {
6693
+ const { branch } = conflict;
6694
+ const files = conflict.conflictFiles ?? [];
6695
+ const asked = `What the task was asked to do:
6696
+ ${task.prompt ?? task.description}`;
6697
+ const resolve6 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
6698
+ const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
6699
+ if (repos.length === 0) {
6700
+ return [
6701
+ `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.`,
6702
+ `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.`,
6703
+ "Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
6704
+ "",
6705
+ asked
6706
+ ].join("\n");
6707
+ }
6708
+ const stoppedIn = conflict.conflictRepo ?? repos[0];
6709
+ return [
6710
+ `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.`,
6711
+ `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.`,
6712
+ "Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
6713
+ "",
6714
+ asked
6715
+ ].join("\n");
6716
+ }
6476
6717
 
6477
6718
  // src/services/PlanValidator.ts
6478
6719
  function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
@@ -7936,9 +8177,9 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
7936
8177
  const scope = args.include ? ` in ${args.include}` : "";
7937
8178
  return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
7938
8179
  }
7939
- const path23 = args.filePath || args.file_path || args.path;
7940
- if (path23) {
7941
- const basename = String(path23).split("/").pop() || String(path23);
8180
+ const path24 = args.filePath || args.file_path || args.path;
8181
+ if (path24) {
8182
+ const basename = String(path24).split("/").pop() || String(path24);
7942
8183
  return `${display} ${basename}`;
7943
8184
  }
7944
8185
  if (args.pattern) {
@@ -9578,7 +9819,7 @@ ${line}
9578
9819
  // src/services/transcriptCapture.ts
9579
9820
  var import_fs = require("fs");
9580
9821
  var os3 = __toESM(require("os"));
9581
- var path9 = __toESM(require("path"));
9822
+ var path10 = __toESM(require("path"));
9582
9823
  var HomeTranscriptReader = class {
9583
9824
  home;
9584
9825
  constructor(opts = {}) {
@@ -9597,19 +9838,19 @@ var HomeTranscriptReader = class {
9597
9838
  };
9598
9839
  var CLAUDE_PROJECT_NAME_MAX = 200;
9599
9840
  function claudeProjectDirs(home, cwd) {
9600
- const projects = path9.join(home, ".claude", "projects");
9841
+ const projects = path10.join(home, ".claude", "projects");
9601
9842
  const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
9602
9843
  if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
9603
- const dir = path9.join(projects, munged);
9844
+ const dir = path10.join(projects, munged);
9604
9845
  return (0, import_fs.existsSync)(dir) ? [dir] : [];
9605
9846
  }
9606
9847
  if (!(0, import_fs.existsSync)(projects)) return [];
9607
9848
  const prefix = `${munged.slice(0, CLAUDE_PROJECT_NAME_MAX)}-`;
9608
- return (0, import_fs.readdirSync)(projects).filter((name) => name.startsWith(prefix)).map((name) => path9.join(projects, name));
9849
+ return (0, import_fs.readdirSync)(projects).filter((name) => name.startsWith(prefix)).map((name) => path10.join(projects, name));
9609
9850
  }
9610
9851
  function claudeFinal(home, query, maxChars) {
9611
9852
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
9612
- const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path9.join(dir, f))).filter((f) => cutoff ? (0, import_fs.statSync)(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => (0, import_fs.statSync)(b).mtimeMs - (0, import_fs.statSync)(a).mtimeMs);
9853
+ const candidates = claudeProjectDirs(home, query.cwd).flatMap((dir) => (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path10.join(dir, f))).filter((f) => cutoff ? (0, import_fs.statSync)(f).mtimeMs >= cutoff - 5e3 : true).sort((a, b) => (0, import_fs.statSync)(b).mtimeMs - (0, import_fs.statSync)(a).mtimeMs);
9613
9854
  for (const file of candidates) {
9614
9855
  const raw = (0, import_fs.readFileSync)(file, "utf8");
9615
9856
  if (!raw.includes(query.marker)) continue;
@@ -9645,7 +9886,7 @@ async function opencodeFinal(home, query, maxChars) {
9645
9886
  } catch {
9646
9887
  return null;
9647
9888
  }
9648
- const dbPath = path9.join(home, ".local", "share", "opencode", "opencode.db");
9889
+ const dbPath = path10.join(home, ".local", "share", "opencode", "opencode.db");
9649
9890
  if (!(0, import_fs.existsSync)(dbPath)) return null;
9650
9891
  let db;
9651
9892
  try {
@@ -9698,7 +9939,7 @@ async function opencodeFinal(home, query, maxChars) {
9698
9939
  }
9699
9940
  }
9700
9941
  function codexFinal(home, query, maxChars) {
9701
- const root = path9.join(home, ".codex", "sessions");
9942
+ const root = path10.join(home, ".codex", "sessions");
9702
9943
  if (!(0, import_fs.existsSync)(root)) return null;
9703
9944
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
9704
9945
  const files = [];
@@ -9712,7 +9953,7 @@ function codexFinal(home, query, maxChars) {
9712
9953
  continue;
9713
9954
  }
9714
9955
  for (const e of entries) {
9715
- const full = path9.join(dir, e);
9956
+ const full = path10.join(dir, e);
9716
9957
  let st;
9717
9958
  try {
9718
9959
  st = (0, import_fs.statSync)(full);
@@ -10350,7 +10591,7 @@ var TaskOrchestrator = class {
10350
10591
  if (!this.isolationRun) return null;
10351
10592
  const record = this.isolationRun.tasks[taskId];
10352
10593
  if (!record) return { state: "none" };
10353
- return taskIsolationOf(record);
10594
+ return taskIsolationOf(record, this.config.conflictRepairAttempts);
10354
10595
  }
10355
10596
  /**
10356
10597
  * The plan's isolation as a surface shows it, for one with no stream to have
@@ -10359,7 +10600,7 @@ var TaskOrchestrator = class {
10359
10600
  isolationView() {
10360
10601
  const run = this.isolationRun;
10361
10602
  if (!run) return null;
10362
- const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
10603
+ const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
10363
10604
  return { tasks, handoff: handoffOf(run) };
10364
10605
  }
10365
10606
  getAttemptSession(taskId) {
@@ -10395,24 +10636,43 @@ var TaskOrchestrator = class {
10395
10636
  async reviewRunDiff() {
10396
10637
  return this.isolation.reviewDiff(this.requireRun());
10397
10638
  }
10398
- /** "Merge all": the run's integration branches into whatever the user has checked out, in every repo or none. */
10639
+ /**
10640
+ * "Merge all": the run's integration branches into whatever the user has
10641
+ * checked out, in every repo or none. Once everything merged, the run has
10642
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
10643
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
10644
+ */
10399
10645
  async mergeRun() {
10400
10646
  const run = this.requireRun();
10401
10647
  const result = await this.isolation.mergeIntoCheckedOut(run);
10402
10648
  const branch = integrationBranchNameOf(run);
10403
10649
  const group = run.repos.some((r) => r.path !== SELF_REPO);
10404
- const { level, message } = describeMergeResult(result, branch, group);
10650
+ const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
10651
+ const { level, message } = describeMergeResult(result, branch, group, repaired);
10405
10652
  this.notifications[level](message);
10653
+ if (result.outcome === "merged") await this.clearMergedRun(run);
10406
10654
  return result;
10407
10655
  }
10656
+ async clearMergedRun(run) {
10657
+ try {
10658
+ await this.isolation.discard(run, { integration: "delete-merged" });
10659
+ } catch (err) {
10660
+ this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
10661
+ return;
10662
+ }
10663
+ this.forgetRun();
10664
+ }
10408
10665
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
10409
10666
  async cleanupRun() {
10410
- await this.isolation.discard(this.requireRun(), { keepIntegration: true });
10667
+ await this.isolation.discard(this.requireRun(), { integration: "keep" });
10411
10668
  this.emit("onIsolationChanged");
10412
10669
  }
10413
10670
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
10414
10671
  async discardRun() {
10415
- await this.isolation.discard(this.requireRun(), { keepIntegration: false });
10672
+ await this.isolation.discard(this.requireRun(), { integration: "delete" });
10673
+ this.forgetRun();
10674
+ }
10675
+ forgetRun() {
10416
10676
  this.isolationRun = null;
10417
10677
  this.resolvers = {};
10418
10678
  this.emit("onIsolationChanged");
@@ -10448,7 +10708,8 @@ var TaskOrchestrator = class {
10448
10708
  }
10449
10709
  loadPlan(tasks, planRunners = ["claude-code"]) {
10450
10710
  this.store.load(tasks, planRunners);
10451
- this.endAllAttempts("load");
10711
+ const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
10712
+ for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
10452
10713
  if (!this.running) this.runMode = null;
10453
10714
  this.blockedStart = null;
10454
10715
  this.planStatus = "approved";
@@ -10506,9 +10767,10 @@ var TaskOrchestrator = class {
10506
10767
  this.running = false;
10507
10768
  this.planStatus = "approved";
10508
10769
  this.terminalRunner.stopAll();
10509
- const interrupted = [...this.attempts.values()].filter((a) => a.worktree);
10510
- this.endAllAttempts("stop");
10511
- for (const a of interrupted) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
10770
+ for (const a of this.endAllAttempts("stop")) {
10771
+ if (a.repair) this.store.markAwaitingUser(a.taskId);
10772
+ if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
10773
+ }
10512
10774
  this.runMode = null;
10513
10775
  this.blockedStart = null;
10514
10776
  this.onHold.clear();
@@ -10524,6 +10786,7 @@ var TaskOrchestrator = class {
10524
10786
  console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
10525
10787
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
10526
10788
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
10789
+ if (attempt.repair) return this.settleRepair(task, attempt, verdict);
10527
10790
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
10528
10791
  const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
10529
10792
  if (this.attempts.get(taskId) !== attempt) return;
@@ -10544,6 +10807,9 @@ ${summary || "(empty \u2014 no output captured)"}`);
10544
10807
  }
10545
10808
  this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
10546
10809
  this.logAndArchive(task, verdict);
10810
+ await this.afterVerdict();
10811
+ }
10812
+ async afterVerdict() {
10547
10813
  this.emit("onTaskChanged");
10548
10814
  if (!this.running) {
10549
10815
  if (this.attempts.size === 0) {
@@ -10556,6 +10822,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
10556
10822
  }
10557
10823
  await this.tick();
10558
10824
  }
10825
+ /**
10826
+ * A repair's verdict decides only whether its work may try to land. It never
10827
+ * replaces the verdict the task's own work earned, and a repair that does not
10828
+ * land leaves the task waiting on the user — never a failed task, so never a
10829
+ * halted run.
10830
+ */
10831
+ async settleRepair(task, attempt, verdict) {
10832
+ const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
10833
+ if (this.attempts.get(task.id) !== attempt) return;
10834
+ this.endAttempt(task.id, "verdict");
10835
+ if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
10836
+ else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
10837
+ else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
10838
+ else this.landUnmerged(task, landed.outcome);
10839
+ await this.afterVerdict();
10840
+ }
10841
+ /**
10842
+ * Evidence before the queue: the repair's work is committed and checked, and
10843
+ * only then merged, through the same serialized landing as any other task —
10844
+ * so a repair the tip has moved past again is a fresh conflict, not a pass.
10845
+ */
10846
+ async landRepair(task, attempt) {
10847
+ const run = this.isolationRun;
10848
+ let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
10849
+ if (!run) return { evidence, outcome: "failed" };
10850
+ attempt.phase = "integrating";
10851
+ attempt.integration = (async () => {
10852
+ evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
10853
+ if (evidence.ok) return this.integrateWork(task);
10854
+ await this.releaseWorktree(task.id, { keep: true });
10855
+ return "conflict";
10856
+ })();
10857
+ const outcome = await attempt.integration;
10858
+ return { evidence, outcome };
10859
+ }
10860
+ async landedRepair(task, verdict) {
10861
+ const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
10862
+ this.store.markCompleted(task.id);
10863
+ this.store.unblockDependents(task.id);
10864
+ this.logAndArchive(task, task.verdict ?? verdict);
10865
+ this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
10866
+ await this.landResolved(task.id);
10867
+ }
10868
+ /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
10869
+ async unrepaired(task, why) {
10870
+ await this.releaseWorktree(task.id, { keep: true });
10871
+ this.store.markAwaitingUser(task.id);
10872
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
10873
+ 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.`);
10874
+ }
10875
+ describeEvidence(evidence) {
10876
+ const run = this.isolationRun;
10877
+ const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
10878
+ const branch = run ? integrationBranchNameOf(run) : "the integration branch";
10879
+ switch (evidence.reason) {
10880
+ case "not-merged":
10881
+ return `finished, but its branch${inRepo} does not contain ${branch}`;
10882
+ case "conflict-markers": {
10883
+ const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
10884
+ return `finished, but left conflict markers in ${capConflictFiles(files)}`;
10885
+ }
10886
+ case "failed":
10887
+ return `finished, but git could not check its work${inRepo}`;
10888
+ }
10889
+ }
10559
10890
  /**
10560
10891
  * Merge a passed attempt's worktree. The attempt stays live while it waits on
10561
10892
  * the module's merge queue, so a cancel, retry or stop in that window still
@@ -10589,11 +10920,25 @@ ${summary || "(empty \u2014 no output captured)"}`);
10589
10920
  }
10590
10921
  landUnmerged(task, landing) {
10591
10922
  const branch = this.isolationRun ? integrationBranchNameOf(this.isolationRun) : "the integration branch";
10592
- const repo = this.isolationRun?.tasks[task.id]?.conflictRepo;
10593
- const inRepo = repo && repo !== SELF_REPO ? repo : null;
10923
+ const record = this.isolationRun?.tasks[task.id];
10924
+ const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
10594
10925
  if (landing === "conflict") {
10926
+ const repair = this.nextRepair(task.id);
10595
10927
  this.store.markAwaitingUser(task.id);
10596
- 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.`);
10928
+ const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
10929
+ 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}.`;
10930
+ if (repair) {
10931
+ this.notifications.warn(conflicted);
10932
+ if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
10933
+ else {
10934
+ this.store.markPending(task.id);
10935
+ this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
10936
+ }
10937
+ return;
10938
+ }
10939
+ 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.`);
10940
+ const whyNot = this.noRepairReason(task);
10941
+ if (whyNot) this.tell("info", whyNot);
10597
10942
  } else {
10598
10943
  this.store.markFailed(task.id);
10599
10944
  this.running = false;
@@ -10601,6 +10946,19 @@ ${summary || "(empty \u2014 no output captured)"}`);
10601
10946
  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.`);
10602
10947
  }
10603
10948
  }
10949
+ /** 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. */
10950
+ nextRepair(taskId) {
10951
+ const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
10952
+ const limit = this.config.conflictRepairAttempts;
10953
+ const spent = record?.repairs ?? 0;
10954
+ return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
10955
+ }
10956
+ noRepairReason(task) {
10957
+ const limit = this.config.conflictRepairAttempts;
10958
+ if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
10959
+ const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
10960
+ return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
10961
+ }
10604
10962
  /**
10605
10963
  * Mark which added task resolves which conflict. The resolver merges the
10606
10964
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -10890,6 +11248,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
10890
11248
  cwd: null,
10891
11249
  worktree: false,
10892
11250
  integration: null,
11251
+ repair: this.nextRepair(task.id),
10893
11252
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
10894
11253
  };
10895
11254
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -10900,12 +11259,13 @@ ${summary || "(empty \u2014 no output captured)"}`);
10900
11259
  const cwd = await this.resolveAttemptCwd(task, attempt);
10901
11260
  attempt.cwd = cwd;
10902
11261
  if (this.attempts.get(task.id) !== attempt) {
10903
- if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: false });
10904
- return this.abandonSpawn(task);
11262
+ if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
11263
+ return this.abandonSpawn(task, attempt);
10905
11264
  }
10906
- const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
11265
+ const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
10907
11266
  planMapEnabled: this.config.planMapEnabled,
10908
- tddEnabled: this.tddEnabled()
11267
+ // A merge to resolve is not new behaviour to drive test-first.
11268
+ tddEnabled: !attempt.repair && this.tddEnabled()
10909
11269
  });
10910
11270
  const session = await this.terminalRunner.spawn({
10911
11271
  taskId: task.id,
@@ -10920,16 +11280,26 @@ ${summary || "(empty \u2014 no output captured)"}`);
10920
11280
  order: task.order,
10921
11281
  title: task.title
10922
11282
  });
10923
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
11283
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
10924
11284
  attempt.phase = "running";
10925
11285
  attempt.session = session;
10926
11286
  this.output.attach(task.id, session);
10927
11287
  this.verifier.watch(task, session);
10928
11288
  this.emit("onTaskChanged");
10929
- this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
11289
+ if (attempt.repair) {
11290
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
11291
+ this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
11292
+ } else {
11293
+ this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
11294
+ }
10930
11295
  } catch (err) {
10931
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
11296
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
10932
11297
  this.endAttempt(task.id, "spawn-failed");
11298
+ if (attempt.repair) {
11299
+ await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
11300
+ this.emit("onTaskChanged");
11301
+ return this.tick();
11302
+ }
10933
11303
  await this.releaseWorktree(task.id, { keep: false });
10934
11304
  this.store.markPending(task.id);
10935
11305
  this.onHold.add(task.id);
@@ -10943,17 +11313,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
10943
11313
  * and take back the claim it made. Only the claim — whatever ended the
10944
11314
  * attempt may have decided the task since (mark complete, cancel, retry).
10945
11315
  */
10946
- abandonSpawn(task, session) {
11316
+ abandonSpawn(task, attempt, session) {
10947
11317
  session?.kill();
10948
11318
  if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
10949
- this.store.markPending(task.id);
11319
+ if (attempt.repair) this.store.markAwaitingUser(task.id);
11320
+ else this.store.markPending(task.id);
10950
11321
  this.emit("onTaskChanged");
10951
11322
  }
11323
+ repairPrompt(task) {
11324
+ const run = this.requireRun();
11325
+ const record = run.tasks[task.id];
11326
+ const conflict = {
11327
+ branch: record?.branch ?? "",
11328
+ repos: Object.keys(record?.repairBase ?? {}),
11329
+ ...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
11330
+ ...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
11331
+ };
11332
+ return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
11333
+ }
10952
11334
  /**
10953
11335
  * The one place an attempt's working directory is decided. Async so a
10954
11336
  * per-attempt workspace (worktree isolation, #12) can be prepared here.
10955
11337
  */
10956
11338
  async resolveAttemptCwd(task, attempt) {
11339
+ if (attempt.repair && this.isolationRun) {
11340
+ const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
11341
+ attempt.worktree = true;
11342
+ this.emit("onIsolationChanged");
11343
+ return cwd2;
11344
+ }
10957
11345
  if (this.runMode !== "isolated" || !this.isolationRun) {
10958
11346
  await this.releaseWorktree(task.id, { keep: false });
10959
11347
  return this.workspaceRootFn();
@@ -11041,8 +11429,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
11041
11429
  }
11042
11430
  }
11043
11431
  this.runMode = "isolated";
11432
+ await this.sweep();
11044
11433
  return true;
11045
11434
  }
11435
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
11436
+ async sweep() {
11437
+ if (!this.isolationRun) return;
11438
+ try {
11439
+ await this.isolation.sweep(this.isolationRun);
11440
+ } catch (err) {
11441
+ this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
11442
+ }
11443
+ }
11046
11444
  /**
11047
11445
  * The plan's run carries on while anything has landed on its integration
11048
11446
  * branch: a resumed plan's dependents must start from a tip that holds their
@@ -11055,14 +11453,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
11055
11453
  /**
11056
11454
  * A run with nothing landed holds only superseded attempts, so it goes whole.
11057
11455
  * One that cannot be continued for another reason — it ran from a different
11058
- * workspace path — keeps its integration branch: only the user gives landed
11059
- * work up.
11456
+ * workspace path — keeps its integration branch in each repo that has not
11457
+ * merged it: only the user gives landed work up.
11060
11458
  */
11061
11459
  async mintRun(root) {
11062
11460
  const previous = this.isolationRun;
11063
11461
  if (previous) {
11064
11462
  const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
11065
- await this.isolation.discard(previous, { keepIntegration: landed }).catch(() => void 0);
11463
+ await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
11066
11464
  this.isolationRun = null;
11067
11465
  }
11068
11466
  this.isolationRun = await this.isolation.startRun(root);
@@ -11127,9 +11525,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
11127
11525
  return attempt;
11128
11526
  }
11129
11527
  endAllAttempts(reason) {
11130
- for (const taskId of [...this.attempts.keys()]) this.endAttempt(taskId, reason);
11528
+ const ended = [...this.attempts.values()];
11529
+ for (const { taskId } of ended) this.endAttempt(taskId, reason);
11131
11530
  this.verifier.reset();
11132
11531
  if (reason === "load") this.output.reset();
11532
+ return ended;
11133
11533
  }
11134
11534
  };
11135
11535
 
@@ -11259,7 +11659,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
11259
11659
  };
11260
11660
 
11261
11661
  // src/services/ModelDiscovery.ts
11262
- var path12 = __toESM(require("path"));
11662
+ var path13 = __toESM(require("path"));
11263
11663
  var import_child_process4 = require("child_process");
11264
11664
  var import_util3 = require("util");
11265
11665
  var fsSync3 = __toESM(require("fs"));
@@ -11267,17 +11667,17 @@ var osMod = __toESM(require("os"));
11267
11667
 
11268
11668
  // src/utils/globalDataDir.ts
11269
11669
  var fs5 = __toESM(require("fs"));
11270
- var path10 = __toESM(require("path"));
11670
+ var path11 = __toESM(require("path"));
11271
11671
  var os4 = __toESM(require("os"));
11272
11672
  function globalDataDir() {
11273
- return path10.join(os4.homedir(), ".ordewell");
11673
+ return path11.join(os4.homedir(), ".ordewell");
11274
11674
  }
11275
11675
  function copyDirSync(src, dest) {
11276
11676
  fs5.mkdirSync(dest, { recursive: true });
11277
11677
  const entries = fs5.readdirSync(src, { withFileTypes: true });
11278
11678
  for (const entry of entries) {
11279
- const srcPath = path10.join(src, entry.name);
11280
- const destPath = path10.join(dest, entry.name);
11679
+ const srcPath = path11.join(src, entry.name);
11680
+ const destPath = path11.join(dest, entry.name);
11281
11681
  if (entry.isDirectory()) {
11282
11682
  copyDirSync(srcPath, destPath);
11283
11683
  } else {
@@ -11289,12 +11689,12 @@ var migrated = false;
11289
11689
  function migrateOldConfigDir() {
11290
11690
  if (migrated) return;
11291
11691
  migrated = true;
11292
- const oldDir = path10.join(os4.homedir(), ".config", "ordewell");
11692
+ const oldDir = path11.join(os4.homedir(), ".config", "ordewell");
11293
11693
  if (!fs5.existsSync(oldDir)) return;
11294
11694
  const newDir = globalDataDir();
11295
11695
  const liftFile = (name) => {
11296
- const oldFile = path10.join(oldDir, name);
11297
- const newFile = path10.join(newDir, name);
11696
+ const oldFile = path11.join(oldDir, name);
11697
+ const newFile = path11.join(newDir, name);
11298
11698
  if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
11299
11699
  fs5.mkdirSync(newDir, { recursive: true });
11300
11700
  fs5.copyFileSync(oldFile, newFile);
@@ -11302,8 +11702,8 @@ function migrateOldConfigDir() {
11302
11702
  };
11303
11703
  liftFile("settings.json");
11304
11704
  liftFile(".env");
11305
- const oldPlugins = path10.join(oldDir, "plugins");
11306
- const newPlugins = path10.join(newDir, "plugins");
11705
+ const oldPlugins = path11.join(oldDir, "plugins");
11706
+ const newPlugins = path11.join(newDir, "plugins");
11307
11707
  if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
11308
11708
  copyDirSync(oldPlugins, newPlugins);
11309
11709
  }
@@ -11311,7 +11711,7 @@ function migrateOldConfigDir() {
11311
11711
 
11312
11712
  // src/utils/launch.ts
11313
11713
  var fsSync2 = __toESM(require("fs"));
11314
- var path11 = __toESM(require("path"));
11714
+ var path12 = __toESM(require("path"));
11315
11715
 
11316
11716
  // src/utils/shell.ts
11317
11717
  var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
@@ -11340,7 +11740,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
11340
11740
  var WINDOWS_MAX_COMMAND_LINE = 32767;
11341
11741
  function defaultPowerShell() {
11342
11742
  const root = process.env.SystemRoot || process.env.windir;
11343
- return root ? path11.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
11743
+ return root ? path12.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
11344
11744
  }
11345
11745
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
11346
11746
  var CommandLineTooLongError = class extends Error {
@@ -11392,7 +11792,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
11392
11792
  };
11393
11793
  function isResolvableOnPosixPath(command, PATH, exists) {
11394
11794
  if (command.includes("/")) return exists(command);
11395
- return PATH.split(path11.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path11.join(dir, command)));
11795
+ return PATH.split(path12.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path12.join(dir, command)));
11396
11796
  }
11397
11797
  function isExecutableResolved(command, plan, PATH, deps = {}) {
11398
11798
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -11432,9 +11832,9 @@ function cmdArgs(line) {
11432
11832
  return ["/d", "/s", "/c", `"${line}"`];
11433
11833
  }
11434
11834
  function candidateExtensions(command, pathExt) {
11435
- const declared = pathExt.split(path11.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
11835
+ const declared = pathExt.split(path12.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
11436
11836
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
11437
- const ext = path11.win32.extname(command).toLowerCase();
11837
+ const ext = path12.win32.extname(command).toLowerCase();
11438
11838
  if (ext && known.has(ext)) {
11439
11839
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
11440
11840
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -11455,13 +11855,13 @@ async function findWindowsExecutables(command, deps) {
11455
11855
  const exists = deps.exists ?? defaultExists2;
11456
11856
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
11457
11857
  const byRoute = candidateExtensions(command, pathExt);
11458
- 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);
11459
- const base = path11.win32.basename(command);
11858
+ const dirs = path12.win32.basename(command) !== command ? [path12.win32.dirname(path12.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path12.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
11859
+ const base = path12.win32.basename(command);
11460
11860
  const found = [];
11461
11861
  for (const route of ROUTES) {
11462
11862
  outer: for (const ext of byRoute[route]) {
11463
11863
  for (const dir of dirs) {
11464
- const candidate = path11.win32.join(dir, `${base}${ext}`);
11864
+ const candidate = path12.win32.join(dir, `${base}${ext}`);
11465
11865
  if (exists(candidate)) {
11466
11866
  found.push({ file: candidate, route });
11467
11867
  break outer;
@@ -11556,7 +11956,7 @@ var defaultAppServerClient = async (command, args, options) => {
11556
11956
  const PATH = await augmentedPath();
11557
11957
  const launch = await planDirectLaunch(command, args).catch(() => null);
11558
11958
  if (!launch) return null;
11559
- return new Promise((resolve5) => {
11959
+ return new Promise((resolve6) => {
11560
11960
  let child;
11561
11961
  try {
11562
11962
  child = (0, import_child_process4.spawn)(launch.file, launch.args, {
@@ -11565,7 +11965,7 @@ var defaultAppServerClient = async (command, args, options) => {
11565
11965
  windowsVerbatimArguments: launch.verbatim
11566
11966
  });
11567
11967
  } catch {
11568
- resolve5(null);
11968
+ resolve6(null);
11569
11969
  return;
11570
11970
  }
11571
11971
  let settled = false;
@@ -11574,7 +11974,7 @@ var defaultAppServerClient = async (command, args, options) => {
11574
11974
  settled = true;
11575
11975
  clearTimeout(timer);
11576
11976
  killTree(child);
11577
- resolve5(value);
11977
+ resolve6(value);
11578
11978
  };
11579
11979
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
11580
11980
  child.on("error", () => finish(null));
@@ -11641,7 +12041,7 @@ function geminiDiscoveryCacheKey(apiKey) {
11641
12041
  return {
11642
12042
  key: `gemini-models:${hash}`,
11643
12043
  dir,
11644
- file: path12.join(dir, `gemini_models_${hash}.json`)
12044
+ file: path13.join(dir, `gemini_models_${hash}.json`)
11645
12045
  };
11646
12046
  }
11647
12047
  function readGeminiCache(apiKey) {
@@ -12165,7 +12565,7 @@ var ModelDiscovery = class {
12165
12565
 
12166
12566
  // src/services/ModelCatalog.ts
12167
12567
  var fs6 = __toESM(require("fs"));
12168
- var path13 = __toESM(require("path"));
12568
+ var path14 = __toESM(require("path"));
12169
12569
  var CACHE_KEY = "model-catalog";
12170
12570
  var CACHE_TTL_MS = 60 * 60 * 1e3;
12171
12571
  function storageKey(baseUrl) {
@@ -12217,7 +12617,7 @@ var ModelCatalog = class {
12217
12617
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
12218
12618
  const key = storageKey(url);
12219
12619
  const cacheDir = globalCacheDir();
12220
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12620
+ const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12221
12621
  if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
12222
12622
  } catch {
12223
12623
  }
@@ -12225,7 +12625,7 @@ var ModelCatalog = class {
12225
12625
  static readCache(key) {
12226
12626
  try {
12227
12627
  const cacheDir = globalCacheDir();
12228
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12628
+ const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12229
12629
  if (!fs6.existsSync(cacheFile)) return null;
12230
12630
  const raw = fs6.readFileSync(cacheFile, "utf8");
12231
12631
  const data = JSON.parse(raw);
@@ -12239,7 +12639,7 @@ var ModelCatalog = class {
12239
12639
  try {
12240
12640
  const cacheDir = globalCacheDir();
12241
12641
  fs6.mkdirSync(cacheDir, { recursive: true });
12242
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12642
+ const cacheFile = path14.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12243
12643
  fs6.writeFileSync(
12244
12644
  cacheFile,
12245
12645
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
@@ -12623,7 +13023,8 @@ var ORDEWELL_SETTABLE_ENV = [
12623
13023
  "ORDEWELL_SUBAGENT_MODEL",
12624
13024
  "ORDEWELL_PLANNER_EFFORT",
12625
13025
  "ORDEWELL_RESEARCH_ENABLED",
12626
- "ORDEWELL_MAX_PARALLEL"
13026
+ "ORDEWELL_MAX_PARALLEL",
13027
+ "ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
12627
13028
  ];
12628
13029
  var SETTINGS_ENV_REFUSED = [
12629
13030
  // Approval posture and pre-approved scope.
@@ -12680,7 +13081,7 @@ var import_child_process6 = require("child_process");
12680
13081
 
12681
13082
  // src/utils/workspace.ts
12682
13083
  var fs7 = __toESM(require("fs"));
12683
- var path14 = __toESM(require("path"));
13084
+ var path15 = __toESM(require("path"));
12684
13085
  var WorkspaceNotFoundError = class extends Error {
12685
13086
  constructor(workspace) {
12686
13087
  super(`Workspace "${workspace}" does not exist.`);
@@ -12736,7 +13137,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
12736
13137
  throw new WorkspaceNotAProjectError(workspace);
12737
13138
  }
12738
13139
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
12739
- (marker2) => exists(path14.join(resolved, marker2))
13140
+ (marker2) => exists(path15.join(resolved, marker2))
12740
13141
  );
12741
13142
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
12742
13143
  }
@@ -12804,8 +13205,8 @@ var StdioAgentAdapter = class {
12804
13205
  }
12805
13206
  async start(opts) {
12806
13207
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
12807
- this.processEnded = new Promise((resolve5) => {
12808
- this.markEnded = resolve5;
13208
+ this.processEnded = new Promise((resolve6) => {
13209
+ this.markEnded = resolve6;
12809
13210
  });
12810
13211
  const spec = this.spawnSpec(opts);
12811
13212
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
@@ -12856,7 +13257,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
12856
13257
  onEvent({ type: "error", message: this.exitMessage() });
12857
13258
  return;
12858
13259
  }
12859
- await new Promise((resolve5) => {
13260
+ await new Promise((resolve6) => {
12860
13261
  let settled = false;
12861
13262
  const finish = () => {
12862
13263
  if (settled) return;
@@ -12867,7 +13268,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
12867
13268
  signal?.removeEventListener("abort", onAbort);
12868
13269
  this.process?.removeListener("exit", onExit);
12869
13270
  this.process?.removeListener("error", onExit);
12870
- resolve5();
13271
+ resolve6();
12871
13272
  };
12872
13273
  const emit = (event) => {
12873
13274
  onEvent(event);
@@ -13082,14 +13483,14 @@ function codexSandboxUnavailableMessage() {
13082
13483
  ].join("\n");
13083
13484
  }
13084
13485
  function runProbe(deps, cwd, env, flags) {
13085
- return new Promise((resolve5) => {
13486
+ return new Promise((resolve6) => {
13086
13487
  let output = "";
13087
13488
  let settled = false;
13088
13489
  const finish = (code) => {
13089
13490
  if (settled) return;
13090
13491
  settled = true;
13091
13492
  clearTimeout(timer);
13092
- resolve5({ code, output });
13493
+ resolve6({ code, output });
13093
13494
  };
13094
13495
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
13095
13496
  timer.unref?.();
@@ -13172,8 +13573,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
13172
13573
  async handshake(opts) {
13173
13574
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
13174
13575
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
13175
- const ready = new Promise((resolve5) => {
13176
- this.settleHandshake = resolve5;
13576
+ const ready = new Promise((resolve6) => {
13577
+ this.settleHandshake = resolve6;
13177
13578
  });
13178
13579
  this.writeLine({
13179
13580
  jsonrpc: "2.0",
@@ -13186,8 +13587,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
13186
13587
  // A binary that rejects `app-server` outright dies immediately; waiting
13187
13588
  // out the timeout would turn a one-line diagnostic into a 30s stall.
13188
13589
  this.processEnded.then(() => false),
13189
- new Promise((resolve5) => {
13190
- const t = setTimeout(() => resolve5(false), HANDSHAKE_TIMEOUT_MS);
13590
+ new Promise((resolve6) => {
13591
+ const t = setTimeout(() => resolve6(false), HANDSHAKE_TIMEOUT_MS);
13191
13592
  t.unref?.();
13192
13593
  })
13193
13594
  ]);
@@ -13464,12 +13865,12 @@ function describeError(err) {
13464
13865
  return lines.join(": ");
13465
13866
  }
13466
13867
  function delay(ms, signal) {
13467
- return new Promise((resolve5) => {
13468
- const timer = setTimeout(resolve5, ms);
13868
+ return new Promise((resolve6) => {
13869
+ const timer = setTimeout(resolve6, ms);
13469
13870
  timer.unref?.();
13470
13871
  signal?.addEventListener("abort", () => {
13471
13872
  clearTimeout(timer);
13472
- resolve5();
13873
+ resolve6();
13473
13874
  }, { once: true });
13474
13875
  });
13475
13876
  }
@@ -13508,12 +13909,12 @@ var OpenCodeAdapter = class {
13508
13909
  cwd: opts.cwd,
13509
13910
  windowsVerbatimArguments: launch.verbatim
13510
13911
  });
13511
- const banner = new Promise((resolve5) => {
13912
+ const banner = new Promise((resolve6) => {
13512
13913
  let seen = "";
13513
13914
  const scan = (chunk) => {
13514
13915
  seen += chunk.toString();
13515
13916
  const match = seen.match(/https?:\/\/[^\s]+/);
13516
- if (match) resolve5(match[0].replace(/[.,)]$/, ""));
13917
+ if (match) resolve6(match[0].replace(/[.,)]$/, ""));
13517
13918
  };
13518
13919
  this.process.stdout?.on("data", scan);
13519
13920
  this.process.stderr?.on("data", (chunk) => {
@@ -13522,15 +13923,15 @@ var OpenCodeAdapter = class {
13522
13923
  });
13523
13924
  this.process.on("exit", () => {
13524
13925
  this.exited = true;
13525
- resolve5(null);
13926
+ resolve6(null);
13526
13927
  });
13527
13928
  this.process.on("error", (err) => {
13528
13929
  this.exited = true;
13529
13930
  this.stderrTail = (this.stderrTail + `
13530
13931
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
13531
- resolve5(null);
13932
+ resolve6(null);
13532
13933
  });
13533
- const timer = setTimeout(() => resolve5(null), SERVER_READY_TIMEOUT_MS);
13934
+ const timer = setTimeout(() => resolve6(null), SERVER_READY_TIMEOUT_MS);
13534
13935
  timer.unref?.();
13535
13936
  });
13536
13937
  this.baseUrl = await banner;
@@ -13563,8 +13964,8 @@ ${this.stderrTail.trim()}` : ""}`);
13563
13964
  const streamAbort = new AbortController();
13564
13965
  let connected = () => {
13565
13966
  };
13566
- const streamReady = new Promise((resolve5) => {
13567
- connected = resolve5;
13967
+ const streamReady = new Promise((resolve6) => {
13968
+ connected = resolve6;
13568
13969
  });
13569
13970
  const live = this.streamEvents(
13570
13971
  streamAbort.signal,
@@ -13768,14 +14169,14 @@ ${part.text}` : part.text });
13768
14169
  }
13769
14170
  }
13770
14171
  }
13771
- async json(method, path23, body, signal) {
13772
- const response = await this.deps.fetch(`${this.baseUrl}${path23}`, {
14172
+ async json(method, path24, body, signal) {
14173
+ const response = await this.deps.fetch(`${this.baseUrl}${path24}`, {
13773
14174
  method,
13774
14175
  headers: { "content-type": "application/json" },
13775
14176
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
13776
14177
  ...signal ? { signal } : {}
13777
14178
  });
13778
- if (!response.ok) throw new Error(`${method} ${path23} failed: ${response.status} ${response.statusText}`);
14179
+ if (!response.ok) throw new Error(`${method} ${path24} failed: ${response.status} ${response.statusText}`);
13779
14180
  return await response.json().catch(() => null);
13780
14181
  }
13781
14182
  nativeSessionId() {
@@ -14444,33 +14845,9 @@ var PlannerConversation = class {
14444
14845
  if (m.role !== "user" || index === 0) return [];
14445
14846
  const line = m.content.split("\n")[0];
14446
14847
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
14447
- return [{ index, preview, timestamp: m.timestamp }];
14848
+ return [{ index, preview, content: m.content, timestamp: m.timestamp }];
14448
14849
  });
14449
14850
  }
14450
- /**
14451
- * Cut the dialogue back to just before the user message at `index` (a
14452
- * position in the transcript), discarding it and everything after. The task
14453
- * list is the host's and rides along untouched — rewinding moves where the
14454
- * conversation resumes, not what the plan is. The planner's research trace
14455
- * goes back to the same point, by time: its entries carry no link to the
14456
- * message that caused them.
14457
- */
14458
- rewind(index) {
14459
- this.assertIdle("rewind the conversation");
14460
- const plan = this.requirePlan();
14461
- if (!this.rewindTargets().some((t) => t.index === index)) {
14462
- if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
14463
- 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.");
14464
- }
14465
- const cutoff = this.transcript[index].timestamp;
14466
- const rewound = this.host.mutate(() => {
14467
- plan.conversationHistory = this.transcript.slice(0, index);
14468
- plan.researchLog = (plan.researchLog ?? []).filter((e) => e.timestamp < cutoff);
14469
- return true;
14470
- }, () => this.host.broadcastPlan());
14471
- this.reset();
14472
- return rewound;
14473
- }
14474
14851
  /**
14475
14852
  * Replace the transcript with a summary of it, keeping the last two
14476
14853
  * exchanges as they were, and drop the live context so the next message
@@ -14546,6 +14923,28 @@ var PlannerConversation = class {
14546
14923
  researchLog: plan?.researchLog ?? []
14547
14924
  });
14548
14925
  }
14926
+ /**
14927
+ * A copy of the dialogue as it stood just before the user message at
14928
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
14929
+ * from — plus that message's full text, for a surface to offer back. This
14930
+ * conversation is not touched. The research trace is cut at the same point
14931
+ * by time: its entries carry no link to the message that caused them.
14932
+ */
14933
+ cloneBefore(index) {
14934
+ this.assertIdle("rewind the conversation");
14935
+ if (!this.rewindTargets().some((t) => t.index === index)) {
14936
+ if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
14937
+ 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.");
14938
+ }
14939
+ const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
14940
+ return {
14941
+ dialogue: structuredClone({
14942
+ conversationHistory: this.transcript.slice(0, index),
14943
+ researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
14944
+ }),
14945
+ rewoundMessage
14946
+ };
14947
+ }
14549
14948
  /** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
14550
14949
  recordModification(request, requestedAt, taskCount) {
14551
14950
  this.append("user", request, { timestamp: requestedAt });
@@ -15132,18 +15531,18 @@ function retargetTaskRunner(task, runner, catalog) {
15132
15531
 
15133
15532
  // src/services/SkillsService.ts
15134
15533
  var fs8 = __toESM(require("fs"));
15135
- var path16 = __toESM(require("path"));
15534
+ var path17 = __toESM(require("path"));
15136
15535
 
15137
15536
  // src/services/builtinSkills.ts
15138
- var path15 = __toESM(require("path"));
15537
+ var path16 = __toESM(require("path"));
15139
15538
  var import_url = require("url");
15140
15539
  var import_meta = {};
15141
15540
  function builtinSkillsDir() {
15142
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path15.dirname((0, import_url.fileURLToPath)(
15541
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path16.dirname((0, import_url.fileURLToPath)(
15143
15542
  // @ts-expect-error - import.meta is only valid under an ESM module setting
15144
15543
  import_meta.url
15145
15544
  ));
15146
- return path15.join(dir, "..", "skills");
15545
+ return path16.join(dir, "..", "skills");
15147
15546
  }
15148
15547
 
15149
15548
  // src/services/SkillsService.ts
@@ -15182,8 +15581,8 @@ function copyDirSync2(src, dest) {
15182
15581
  fs8.mkdirSync(dest, { recursive: true });
15183
15582
  const entries = fs8.readdirSync(src, { withFileTypes: true });
15184
15583
  for (const entry of entries) {
15185
- const srcPath = path16.join(src, entry.name);
15186
- const destPath = path16.join(dest, entry.name);
15584
+ const srcPath = path17.join(src, entry.name);
15585
+ const destPath = path17.join(dest, entry.name);
15187
15586
  if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
15188
15587
  else fs8.copyFileSync(srcPath, destPath);
15189
15588
  }
@@ -15194,7 +15593,7 @@ function readDir(dir, source) {
15194
15593
  const skills = [];
15195
15594
  for (const entry of entries) {
15196
15595
  if (!entry.isDirectory()) continue;
15197
- const skillFile = path16.join(dir, entry.name, "SKILL.md");
15596
+ const skillFile = path17.join(dir, entry.name, "SKILL.md");
15198
15597
  if (!fs8.existsSync(skillFile)) continue;
15199
15598
  const parsed = parseSkillFile(skillFile, entry.name, source);
15200
15599
  if (parsed) skills.push(parsed);
@@ -15207,17 +15606,17 @@ var SkillsService = class {
15207
15606
  }
15208
15607
  workspaceRoot;
15209
15608
  localDir() {
15210
- return this.workspaceRoot ? path16.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
15609
+ return this.workspaceRoot ? path17.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
15211
15610
  }
15212
15611
  globalDir() {
15213
- return path16.join(globalDataDir(), "skills");
15612
+ return path17.join(globalDataDir(), "skills");
15214
15613
  }
15215
15614
  seed(name) {
15216
- const dest = path16.join(this.globalDir(), name);
15615
+ const dest = path17.join(this.globalDir(), name);
15217
15616
  if (fs8.existsSync(dest)) return false;
15218
15617
  let srcDir;
15219
15618
  try {
15220
- srcDir = path16.join(builtinSkillsDir(), name);
15619
+ srcDir = path17.join(builtinSkillsDir(), name);
15221
15620
  } catch (err) {
15222
15621
  console.warn(`Could not locate built-in skills dir: ${String(err)}`);
15223
15622
  return false;
@@ -15243,7 +15642,7 @@ var SkillsService = class {
15243
15642
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
15244
15643
  let raw;
15245
15644
  try {
15246
- raw = fs8.readFileSync(path16.join(dir, "SKILL.md"), "utf8");
15645
+ raw = fs8.readFileSync(path17.join(dir, "SKILL.md"), "utf8");
15247
15646
  } catch {
15248
15647
  return false;
15249
15648
  }
@@ -15260,7 +15659,7 @@ var SkillsService = class {
15260
15659
  }
15261
15660
  pruneRetired() {
15262
15661
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
15263
- const dest = path16.join(this.globalDir(), name);
15662
+ const dest = path17.join(this.globalDir(), name);
15264
15663
  if (!fs8.existsSync(dest)) continue;
15265
15664
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
15266
15665
  try {
@@ -15275,10 +15674,10 @@ var SkillsService = class {
15275
15674
  if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
15276
15675
  const localDir = this.localDir();
15277
15676
  if (localDir) {
15278
- const localFile = path16.join(localDir, name, "SKILL.md");
15677
+ const localFile = path17.join(localDir, name, "SKILL.md");
15279
15678
  if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
15280
15679
  }
15281
- const globalFile = path16.join(this.globalDir(), name, "SKILL.md");
15680
+ const globalFile = path17.join(this.globalDir(), name, "SKILL.md");
15282
15681
  if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
15283
15682
  return void 0;
15284
15683
  }
@@ -15361,7 +15760,7 @@ function truncateCheckpointSummary(summary) {
15361
15760
 
15362
15761
  // src/utils/sessionStore.ts
15363
15762
  var fs9 = __toESM(require("fs"));
15364
- var path17 = __toESM(require("path"));
15763
+ var path18 = __toESM(require("path"));
15365
15764
 
15366
15765
  // src/utils/sessionId.ts
15367
15766
  var import_crypto2 = require("crypto");
@@ -15372,7 +15771,7 @@ function mintSessionId() {
15372
15771
  // src/utils/sessionStore.ts
15373
15772
  var SESSIONS_SUBDIR = "sessions";
15374
15773
  function getSessionsDir(baseDir) {
15375
- return path17.join(getStateDir(baseDir), SESSIONS_SUBDIR);
15774
+ return path18.join(getStateDir(baseDir), SESSIONS_SUBDIR);
15376
15775
  }
15377
15776
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
15378
15777
  function hasScalarRunner(parsed) {
@@ -15394,8 +15793,8 @@ function legacySessionFilename(session) {
15394
15793
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
15395
15794
  }
15396
15795
  function dropLegacyFile(sessionsDir, meta) {
15397
- const legacy = path17.join(sessionsDir, legacySessionFilename(meta));
15398
- if (legacy === path17.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
15796
+ const legacy = path18.join(sessionsDir, legacySessionFilename(meta));
15797
+ if (legacy === path18.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
15399
15798
  try {
15400
15799
  const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
15401
15800
  if (held === meta.id) fs9.unlinkSync(legacy);
@@ -15421,7 +15820,7 @@ function saveSession(plan, goal, baseDir, id) {
15421
15820
  plan: JSON.parse(JSON.stringify(plan))
15422
15821
  };
15423
15822
  const filename = sessionFilename(meta);
15424
- const filePath = path17.join(sessionsDir, filename);
15823
+ const filePath = path18.join(sessionsDir, filename);
15425
15824
  fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
15426
15825
  dropLegacyFile(sessionsDir, meta);
15427
15826
  return meta;
@@ -15432,7 +15831,7 @@ function listSessions(baseDir, logger = defaultLogger) {
15432
15831
  const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
15433
15832
  return files.map((f) => {
15434
15833
  try {
15435
- const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
15834
+ const raw = fs9.readFileSync(path18.join(sessionsDir, f), "utf-8");
15436
15835
  const parsed = JSON.parse(raw);
15437
15836
  if (hasScalarRunner(parsed)) {
15438
15837
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -15450,7 +15849,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
15450
15849
  const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
15451
15850
  for (const f of files) {
15452
15851
  try {
15453
- const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
15852
+ const raw = fs9.readFileSync(path18.join(sessionsDir, f), "utf-8");
15454
15853
  const session = JSON.parse(raw);
15455
15854
  if (session.meta.id === sessionId) return f;
15456
15855
  } catch (err) {
@@ -15465,7 +15864,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
15465
15864
  try {
15466
15865
  const file = findSessionFile(sessionsDir, sessionId, logger);
15467
15866
  if (file) {
15468
- const raw = fs9.readFileSync(path17.join(sessionsDir, file), "utf-8");
15867
+ const raw = fs9.readFileSync(path18.join(sessionsDir, file), "utf-8");
15469
15868
  const parsed = JSON.parse(raw);
15470
15869
  if (hasScalarRunner(parsed)) {
15471
15870
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -15500,7 +15899,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
15500
15899
  try {
15501
15900
  const file = findSessionFile(sessionsDir, sessionId, logger);
15502
15901
  if (file) {
15503
- fs9.unlinkSync(path17.join(sessionsDir, file));
15902
+ fs9.unlinkSync(path18.join(sessionsDir, file));
15504
15903
  return true;
15505
15904
  }
15506
15905
  } catch (err) {
@@ -16032,26 +16431,30 @@ var Session = class {
16032
16431
  */
16033
16432
  forkConversation() {
16034
16433
  if (!this.plan) throw new ConversationEditError("No planning conversation to fork");
16035
- const dialogue = this.conversation.clone();
16036
- const sessionId = mintSessionId();
16037
- const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
16038
- saveSession(plan, this.goal, this.workspace, sessionId);
16039
- return { sessionId, goal: this.goal, workspace: this.workspace };
16434
+ return this.saveFork(this.conversation.clone());
16040
16435
  }
16041
16436
  /**
16042
- * Cut the conversation back to just before a user message (a transcript
16043
- * position from {@link rewindTargets}). Conversation only: the task list,
16044
- * and any run executing it, carry on as they are. The planner's live context
16045
- * is dropped, so the next message replays from the shortened transcript.
16437
+ * Fork the conversation from just before a user message (a transcript
16438
+ * position from {@link rewindTargets}): the fork holds everything said
16439
+ * before it and the current task list, like {@link forkConversation}, and
16440
+ * this session keeps its whole history. The message's full text comes back
16441
+ * so a surface can offer it for resending.
16046
16442
  */
16047
16443
  rewindConversation(userMessageIndex) {
16048
16444
  if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
16049
- return this.conversation.rewind(userMessageIndex);
16445
+ const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
16446
+ return { ...this.saveFork(dialogue), rewoundMessage };
16447
+ }
16448
+ saveFork(dialogue) {
16449
+ const sessionId = mintSessionId();
16450
+ const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
16451
+ saveSession(plan, this.goal, this.workspace, sessionId);
16452
+ return { sessionId, goal: this.goal, workspace: this.workspace };
16050
16453
  }
16051
16454
  /**
16052
16455
  * Condense the conversation on the user's say-so: a hidden planner turn
16053
16456
  * summarises it, and the summary replaces everything but the last two
16054
- * exchanges. Conversation only, like a rewind — the tasks, and any run
16457
+ * exchanges. Conversation only — the tasks, and any run
16055
16458
  * executing them, are untouched. Atomic: a failed or stopped turn changes
16056
16459
  * nothing.
16057
16460
  */
@@ -16324,8 +16727,9 @@ var Session = class {
16324
16727
  * The opt-in way through a merge conflict: add an AI task, on the conflicted
16325
16728
  * task's own runner and model, that merges its branch by hand in a worktree
16326
16729
  * of its own. When that task lands, the conflicted one lands through it (see
16327
- * {@link TaskOrchestrator.linkConflictResolver}). Never automatic — nothing
16328
- * but this call adds it.
16730
+ * {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
16731
+ * conflict is a repair, a new attempt of the same task (ADR-0015); a task is
16732
+ * only ever added to the plan by this call.
16329
16733
  */
16330
16734
  async resolveConflictAsTask(taskId) {
16331
16735
  if (!this.plan) return null;
@@ -17511,17 +17915,17 @@ function tokensMatch(presented, expected) {
17511
17915
 
17512
17916
  // src/plugins/RunnerRegistry.ts
17513
17917
  var fs11 = __toESM(require("fs"));
17514
- var path20 = __toESM(require("path"));
17918
+ var path21 = __toESM(require("path"));
17515
17919
  var os5 = __toESM(require("os"));
17516
17920
  var import_child_process10 = require("child_process");
17517
17921
  var import_crypto4 = require("crypto");
17518
17922
 
17519
17923
  // src/plugins/FsPluginStore.ts
17520
17924
  var fs10 = __toESM(require("fs"));
17521
- var path19 = __toESM(require("path"));
17925
+ var path20 = __toESM(require("path"));
17522
17926
 
17523
17927
  // src/plugins/pluginNames.ts
17524
- var path18 = __toESM(require("path"));
17928
+ var path19 = __toESM(require("path"));
17525
17929
  var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
17526
17930
  var MAX_PLUGIN_NAME_LENGTH = 64;
17527
17931
  function isPlainPluginName(name) {
@@ -17536,10 +17940,10 @@ function assertPlainPluginName(name) {
17536
17940
  }
17537
17941
  function resolvePluginInstallDir(pluginsDir, name) {
17538
17942
  assertPlainPluginName(name);
17539
- const root = path18.resolve(pluginsDir);
17540
- const dest = path18.resolve(root, name);
17541
- const relative4 = path18.relative(root, dest);
17542
- if (relative4 !== name || relative4 === "" || path18.isAbsolute(relative4)) {
17943
+ const root = path19.resolve(pluginsDir);
17944
+ const dest = path19.resolve(root, name);
17945
+ const relative5 = path19.relative(root, dest);
17946
+ if (relative5 !== name || relative5 === "" || path19.isAbsolute(relative5)) {
17543
17947
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
17544
17948
  }
17545
17949
  return dest;
@@ -17554,10 +17958,10 @@ function isValidManifest(obj) {
17554
17958
 
17555
17959
  // src/plugins/FsPluginStore.ts
17556
17960
  function userPluginsDir() {
17557
- return path19.join(globalDataDir(), "plugins");
17961
+ return path20.join(globalDataDir(), "plugins");
17558
17962
  }
17559
17963
  function loadManifestFromDir(dir) {
17560
- const manifestPath = path19.join(dir, "manifest.json");
17964
+ const manifestPath = path20.join(dir, "manifest.json");
17561
17965
  if (!fs10.existsSync(manifestPath)) return null;
17562
17966
  try {
17563
17967
  const raw = fs10.readFileSync(manifestPath, "utf8");
@@ -17572,8 +17976,8 @@ function copyDirSync3(src, dest) {
17572
17976
  ensureDir(dest);
17573
17977
  const entries = fs10.readdirSync(src, { withFileTypes: true });
17574
17978
  for (const entry of entries) {
17575
- const srcPath = path19.join(src, entry.name);
17576
- const destPath = path19.join(dest, entry.name);
17979
+ const srcPath = path20.join(src, entry.name);
17980
+ const destPath = path20.join(dest, entry.name);
17577
17981
  if (entry.isDirectory()) {
17578
17982
  copyDirSync3(srcPath, destPath);
17579
17983
  } else {
@@ -17585,7 +17989,7 @@ function rmDirSync(dir) {
17585
17989
  if (!fs10.existsSync(dir)) return;
17586
17990
  const entries = fs10.readdirSync(dir, { withFileTypes: true });
17587
17991
  for (const entry of entries) {
17588
- const p = path19.join(dir, entry.name);
17992
+ const p = path20.join(dir, entry.name);
17589
17993
  if (entry.isDirectory()) {
17590
17994
  rmDirSync(p);
17591
17995
  } else {
@@ -17638,11 +18042,11 @@ var FsPluginStore = class {
17638
18042
  return null;
17639
18043
  }
17640
18044
  }
17641
- dirExists(path23) {
17642
- return fs10.existsSync(path23) && fs10.statSync(path23).isDirectory();
18045
+ dirExists(path24) {
18046
+ return fs10.existsSync(path24) && fs10.statSync(path24).isDirectory();
17643
18047
  }
17644
- exists(path23) {
17645
- return fs10.existsSync(path23);
18048
+ exists(path24) {
18049
+ return fs10.existsSync(path24);
17646
18050
  }
17647
18051
  };
17648
18052
 
@@ -18074,7 +18478,7 @@ var RunnerRegistry = class {
18074
18478
  return entry?.source === "builtin";
18075
18479
  }
18076
18480
  installFromPath(sourcePath) {
18077
- const absPath = path20.resolve(sourcePath);
18481
+ const absPath = path21.resolve(sourcePath);
18078
18482
  if (!this.store.dirExists(absPath)) {
18079
18483
  throw new Error(`Plugin source must be a directory: ${absPath}`);
18080
18484
  }
@@ -18086,7 +18490,7 @@ var RunnerRegistry = class {
18086
18490
  }
18087
18491
  installFromGit(url) {
18088
18492
  const validated = assertInstallablePluginUrl(url);
18089
- const tmpDir = path20.join(os5.tmpdir(), `ordewell-plugin-${(0, import_crypto4.randomBytes)(12).toString("hex")}`);
18493
+ const tmpDir = path21.join(os5.tmpdir(), `ordewell-plugin-${(0, import_crypto4.randomBytes)(12).toString("hex")}`);
18090
18494
  this.store.ensureDir(tmpDir);
18091
18495
  try {
18092
18496
  try {
@@ -18099,7 +18503,7 @@ var RunnerRegistry = class {
18099
18503
  const hasManifest = contents.some((f) => f === "manifest.json");
18100
18504
  let sourceDir = tmpDir;
18101
18505
  if (!hasManifest) {
18102
- const onlyEntry = contents.length === 1 ? path20.join(tmpDir, contents[0]) : void 0;
18506
+ const onlyEntry = contents.length === 1 ? path21.join(tmpDir, contents[0]) : void 0;
18103
18507
  if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
18104
18508
  throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
18105
18509
  }
@@ -18147,7 +18551,7 @@ var RunnerRegistry = class {
18147
18551
  }
18148
18552
  createSkeleton(name, outputDir) {
18149
18553
  assertPlainPluginName(name);
18150
- const dir = path20.resolve(outputDir, name);
18554
+ const dir = path21.resolve(outputDir, name);
18151
18555
  this.store.ensureDir(dir);
18152
18556
  const manifest = {
18153
18557
  name,
@@ -18179,8 +18583,8 @@ var RunnerRegistry = class {
18179
18583
  ]
18180
18584
  }
18181
18585
  };
18182
- this.store.writeFile(path20.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
18183
- const runSh = path20.join(dir, "run.sh");
18586
+ this.store.writeFile(path21.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
18587
+ const runSh = path21.join(dir, "run.sh");
18184
18588
  const scriptContent = `#!/usr/bin/env bash
18185
18589
  # Custom runner script for ${name}
18186
18590
  # Receives:
@@ -18213,14 +18617,14 @@ function fsChmodSync(fpath, mode) {
18213
18617
 
18214
18618
  // src/utils/stateStore.ts
18215
18619
  var fs12 = __toESM(require("fs"));
18216
- var path21 = __toESM(require("path"));
18620
+ var path22 = __toESM(require("path"));
18217
18621
  var STATE_FILE = "state.json";
18218
18622
  function getStatePath(baseDir) {
18219
- return path21.join(getStateDir(baseDir), STATE_FILE);
18623
+ return path22.join(getStateDir(baseDir), STATE_FILE);
18220
18624
  }
18221
18625
  function saveState(plan, baseDir) {
18222
18626
  const statePath = getStatePath(baseDir ?? process.cwd());
18223
- ensureDir(path21.dirname(statePath));
18627
+ ensureDir(path22.dirname(statePath));
18224
18628
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
18225
18629
  fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
18226
18630
  }
@@ -18255,7 +18659,7 @@ function stateExists(baseDir) {
18255
18659
 
18256
18660
  // src/services/SettingsService.ts
18257
18661
  var fs13 = __toESM(require("fs"));
18258
- var path22 = __toESM(require("path"));
18662
+ var path23 = __toESM(require("path"));
18259
18663
  var DEFAULTS = {
18260
18664
  tdd: { enabled: true },
18261
18665
  verification: { enabled: false }
@@ -18263,7 +18667,7 @@ var DEFAULTS = {
18263
18667
  function getSettingsPath() {
18264
18668
  const override = process.env.ORDEWELL_SETTINGS_PATH;
18265
18669
  if (override && override.trim()) return override.trim();
18266
- return path22.join(globalDataDir(), "settings.json");
18670
+ return path23.join(globalDataDir(), "settings.json");
18267
18671
  }
18268
18672
  function readPlannerModels(raw) {
18269
18673
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -18391,7 +18795,7 @@ var SettingsService = class {
18391
18795
  return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
18392
18796
  }
18393
18797
  persist() {
18394
- const dir = path22.dirname(this.filePath);
18798
+ const dir = path23.dirname(this.filePath);
18395
18799
  if (!fs13.existsSync(dir)) {
18396
18800
  fs13.mkdirSync(dir, { recursive: true });
18397
18801
  }
@@ -18535,6 +18939,7 @@ var PlannerModelMemory = class {
18535
18939
  augmentPromptWithPriorOutputs,
18536
18940
  augmentedPath,
18537
18941
  bearerHeaderValue,
18942
+ buildConflictRepairPrompt,
18538
18943
  buildConflictResolutionPrompt,
18539
18944
  buildConversationSystemPrompt,
18540
18945
  buildFallbackGrepArgs,
@@ -18554,6 +18959,7 @@ var PlannerModelMemory = class {
18554
18959
  canMergeTasks,
18555
18960
  canSetDependencies,
18556
18961
  canSplitTask,
18962
+ capConflictFiles,
18557
18963
  checkDepsResolve,
18558
18964
  checkImmutableLog,
18559
18965
  checkInProgress,