@ordewell/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{ITerminalRunner-nupxhtPb.d.ts → ITerminalRunner-Bd-vAJnw.d.ts} +24 -2
  2. package/dist/{ITerminalRunner-DXlJop_f.d.mts → ITerminalRunner-ByeoLF57.d.mts} +24 -2
  3. package/dist/{ModeResolver-D1-2xJsE.d.mts → ModeResolver-DVJ7HV3k.d.mts} +1 -1
  4. package/dist/{ModeResolver-BBVktyiP.d.ts → ModeResolver-Dkig8ghQ.d.ts} +1 -1
  5. package/dist/{Task-CfTjRQeS.d.mts → Task-BxQkPlXO.d.mts} +88 -10
  6. package/dist/{Task-CfTjRQeS.d.ts → Task-BxQkPlXO.d.ts} +88 -10
  7. package/dist/{chunk-HQICT7HQ.mjs → chunk-T2S5O36I.mjs} +17 -4
  8. package/dist/chunk-T2S5O36I.mjs.map +1 -0
  9. package/dist/index.d.mts +151 -57
  10. package/dist/index.d.ts +151 -57
  11. package/dist/index.js +1179 -500
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1162 -498
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/order-labels.d.mts +1 -1
  16. package/dist/order-labels.d.ts +1 -1
  17. package/dist/{parsing-Bv3g2Tu_.d.ts → parsing-CF_grC29.d.ts} +2 -2
  18. package/dist/{parsing-BJECTMOD.d.mts → parsing-DRp4dPC0.d.mts} +2 -2
  19. package/dist/parsing.d.mts +3 -3
  20. package/dist/parsing.d.ts +3 -3
  21. package/dist/{plan-utils-Cy-RsBSV.d.ts → plan-utils-CkNbqAmS.d.ts} +36 -3
  22. package/dist/{plan-utils-B5oIZkuI.d.mts → plan-utils-CtB3_Ovf.d.mts} +36 -3
  23. package/dist/plan-utils.d.mts +3 -3
  24. package/dist/plan-utils.d.ts +3 -3
  25. package/dist/plan-utils.js +7 -0
  26. package/dist/plan-utils.js.map +1 -1
  27. package/dist/plan-utils.mjs +4 -0
  28. package/dist/testing.d.mts +20 -8
  29. package/dist/testing.d.ts +20 -8
  30. package/dist/testing.js +52 -4
  31. package/dist/testing.js.map +1 -1
  32. package/dist/testing.mjs +47 -4
  33. package/dist/testing.mjs.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-HQICT7HQ.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ __export(index_exports, {
56
56
  ConversationEditError: () => ConversationEditError,
57
57
  DAEMON_SUBPROTOCOL: () => DAEMON_SUBPROTOCOL,
58
58
  DAEMON_TOKEN_SUBPROTOCOL_PREFIX: () => DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
59
+ DEFAULT_MAX_PARALLEL: () => DEFAULT_MAX_PARALLEL,
59
60
  DENY_ALL: () => DENY_ALL,
60
61
  EmbeddedNewlineError: () => EmbeddedNewlineError,
61
62
  EnvConfig: () => EnvConfig,
@@ -129,6 +130,7 @@ __export(index_exports, {
129
130
  augmentPromptWithPriorOutputs: () => augmentPromptWithPriorOutputs,
130
131
  augmentedPath: () => augmentedPath,
131
132
  bearerHeaderValue: () => bearerHeaderValue,
133
+ buildConflictRepairPrompt: () => buildConflictRepairPrompt,
132
134
  buildConflictResolutionPrompt: () => buildConflictResolutionPrompt,
133
135
  buildConversationSystemPrompt: () => buildConversationSystemPrompt,
134
136
  buildFallbackGrepArgs: () => buildFallbackGrepArgs,
@@ -148,6 +150,7 @@ __export(index_exports, {
148
150
  canMergeTasks: () => canMergeTasks,
149
151
  canSetDependencies: () => canSetDependencies,
150
152
  canSplitTask: () => canSplitTask,
153
+ capConflictFiles: () => capConflictFiles,
151
154
  checkDepsResolve: () => checkDepsResolve,
152
155
  checkImmutableLog: () => checkImmutableLog,
153
156
  checkInProgress: () => checkInProgress,
@@ -236,6 +239,7 @@ __export(index_exports, {
236
239
  modifyValidationFeedback: () => modifyValidationFeedback,
237
240
  normalizeAgentArgs: () => normalizeAgentArgs,
238
241
  normalizeGeminiModel: () => normalizeGeminiModel,
242
+ parseMaxParallel: () => parseMaxParallel,
239
243
  parsePartialPlan: () => parsePartialPlan,
240
244
  parsePlanJson: () => parsePlanJson,
241
245
  parseTaskOpsJson: () => parseTaskOpsJson,
@@ -3189,33 +3193,33 @@ function grepOptions(args) {
3189
3193
  };
3190
3194
  }
3191
3195
  var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
3192
- async function executeTool(name, args, fs14, fetcher, signal) {
3193
- const outcome = await runTool(name, args, fs14, fetcher, signal);
3196
+ async function executeTool(name, args, fs15, fetcher, signal) {
3197
+ const outcome = await runTool(name, args, fs15, fetcher, signal);
3194
3198
  return { ...outcome, output: redactSecrets(outcome.output) };
3195
3199
  }
3196
- async function runTool(name, args, fs14, fetcher, signal) {
3200
+ async function runTool(name, args, fs15, fetcher, signal) {
3197
3201
  if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
3198
3202
  switch (name) {
3199
3203
  case "read_file": {
3200
3204
  const offset = num(args.offset);
3201
3205
  const limit = num(args.limit);
3202
3206
  const maxBytes = num(args.maxBytes);
3203
- return fs14.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
3207
+ return fs15.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
3204
3208
  }
3205
3209
  case "read_files": {
3206
3210
  const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
3207
- return fs14.readFiles(paths);
3211
+ return fs15.readFiles(paths);
3208
3212
  }
3209
3213
  case "glob":
3210
- return fs14.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
3214
+ return fs15.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
3211
3215
  case "grep":
3212
- return fs14.grep(String(args.pattern ?? ""), grepOptions(args));
3216
+ return fs15.grep(String(args.pattern ?? ""), grepOptions(args));
3213
3217
  case "find_symbol":
3214
- return fs14.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
3218
+ return fs15.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
3215
3219
  case "list_dir":
3216
- return fs14.listDir(String(args.path ?? ""), num(args.depth));
3220
+ return fs15.listDir(String(args.path ?? ""), num(args.depth));
3217
3221
  case "bash":
3218
- return fs14.bash(String(args.command ?? ""), signal);
3222
+ return fs15.bash(String(args.command ?? ""), signal);
3219
3223
  case "fetch": {
3220
3224
  if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
3221
3225
  const url = String(args.url ?? "");
@@ -3323,14 +3327,14 @@ var PendingApprovals = class {
3323
3327
  ask(request) {
3324
3328
  const id = `ap-${Date.now()}-${counter++}`;
3325
3329
  const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
3326
- return new Promise((resolve5) => {
3330
+ return new Promise((resolve7) => {
3327
3331
  const settle = (granted) => {
3328
3332
  const entry = this.entries.get(id);
3329
3333
  if (!entry) return;
3330
3334
  clearTimeout(entry.timer);
3331
3335
  this.entries.delete(id);
3332
3336
  this.opts.onSettled?.(id, granted);
3333
- resolve5(granted);
3337
+ resolve7(granted);
3334
3338
  };
3335
3339
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
3336
3340
  timer.unref?.();
@@ -3428,9 +3432,9 @@ function applyHeadLimit(stdout, headLimit) {
3428
3432
  }
3429
3433
  function formatSearchOutput(capped, root, opts) {
3430
3434
  if (capped.rows.length === 0) return opts.emptyMessage;
3431
- const sep = opts.sep ?? import_node_path.default.sep;
3432
- const prefix = root === sep || root.endsWith(sep) ? root : `${root}${sep}`;
3433
- const body = prefix === sep ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
3435
+ const sep2 = opts.sep ?? import_node_path.default.sep;
3436
+ const prefix = root === sep2 || root.endsWith(sep2) ? root : `${root}${sep2}`;
3437
+ const body = prefix === sep2 ? capped.rows.join("\n") : capped.rows.map((row) => row.startsWith(prefix) ? row.slice(prefix.length) : row).join("\n");
3434
3438
  if (!capped.truncated) return body;
3435
3439
  const hint = opts.hint ? ` ${opts.hint}` : "";
3436
3440
  return `${body}
@@ -4021,6 +4025,15 @@ function configuredProviders(config) {
4021
4025
  return out;
4022
4026
  }
4023
4027
 
4028
+ // src/utils/maxParallel.ts
4029
+ var DEFAULT_MAX_PARALLEL = 3;
4030
+ function parseMaxParallel(value) {
4031
+ const text = String(value ?? "").trim();
4032
+ const n = typeof value === "number" ? value : Number(text);
4033
+ if (text === "" || !Number.isInteger(n) || n < 1) return null;
4034
+ return n;
4035
+ }
4036
+
4024
4037
  // src/interfaces/BaseConfig.ts
4025
4038
  function normalizeGeminiModel(id) {
4026
4039
  return id.replace(/^gemini:/, "").replace(/^google\//, "");
@@ -4028,6 +4041,10 @@ function normalizeGeminiModel(id) {
4028
4041
  function commaList(raw) {
4029
4042
  return (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
4030
4043
  }
4044
+ function nonNegativeInt(raw, fallback) {
4045
+ const trimmed = (raw ?? "").trim();
4046
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : fallback;
4047
+ }
4031
4048
  var BaseConfig = class {
4032
4049
  // --- Legacy named getters (backward compat) ---
4033
4050
  get openAiBaseUrl() {
@@ -4221,7 +4238,7 @@ var BaseConfig = class {
4221
4238
  return "";
4222
4239
  }
4223
4240
  get maxParallelSessions() {
4224
- return parseInt(process.env.ORDEWELL_MAX_PARALLEL || "3", 10);
4241
+ return parseMaxParallel(process.env.ORDEWELL_MAX_PARALLEL) ?? DEFAULT_MAX_PARALLEL;
4225
4242
  }
4226
4243
  get researchEnabled() {
4227
4244
  return process.env.ORDEWELL_RESEARCH_ENABLED !== "false";
@@ -4250,6 +4267,9 @@ var BaseConfig = class {
4250
4267
  get worktreeLinks() {
4251
4268
  return commaList(process.env.ORDEWELL_WORKTREE_LINKS);
4252
4269
  }
4270
+ get conflictRepairAttempts() {
4271
+ return nonNegativeInt(process.env.ORDEWELL_CONFLICT_REPAIR_ATTEMPTS, 2);
4272
+ }
4253
4273
  get approvalMode() {
4254
4274
  const raw = (process.env.ORDEWELL_APPROVAL_MODE || "").trim().toLowerCase();
4255
4275
  return raw === "allow" || raw === "deny" ? raw : "ask";
@@ -4309,21 +4329,33 @@ var ISOLATION_STATE = {
4309
4329
  active: "active",
4310
4330
  merged: "integrated",
4311
4331
  conflict: "conflict",
4332
+ repairing: "repairing",
4312
4333
  kept: "kept",
4313
4334
  failed: "kept"
4314
4335
  };
4315
- function taskIsolationOf(record) {
4336
+ function taskIsolationOf(record, repairLimit) {
4316
4337
  return {
4317
4338
  state: ISOLATION_STATE[record.status],
4318
4339
  branch: record.branch,
4319
4340
  worktree: record.workspace,
4320
4341
  repos: Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath),
4321
- ...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {}
4342
+ ...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
4343
+ ...record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {},
4344
+ ...record.repairs ? { repair: { attempt: record.repairs, limit: repairLimit } } : {},
4345
+ ...record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}
4322
4346
  };
4323
4347
  }
4348
+ function capConflictFiles(files, max = 5) {
4349
+ return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
4350
+ }
4324
4351
  function handoffOf(run) {
4325
4352
  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 });
4353
+ const entry = (r) => ({
4354
+ taskId: r.taskId,
4355
+ order: r.order,
4356
+ title: r.title,
4357
+ ...r.repairedFiles?.length ? { repairedFiles: r.repairedFiles } : {}
4358
+ });
4327
4359
  return {
4328
4360
  repos: run.repos.map((repo) => ({
4329
4361
  path: repo.path,
@@ -4387,13 +4419,17 @@ function landedNotice(landed) {
4387
4419
  if (landed.length === 0) return "Nothing was merged.";
4388
4420
  return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
4389
4421
  }
4390
- function describeMergeResult(result, branch, group) {
4422
+ function repairedNotice(repaired) {
4423
+ const named = repaired.map((t) => `${t.title} (${capConflictFiles(t.repairedFiles ?? [])})`);
4424
+ const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
4425
+ return `${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
4426
+ }
4427
+ function describeMergeResult(result, branch, group, repaired = []) {
4391
4428
  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
- };
4429
+ case "merged": {
4430
+ const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
4431
+ return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
4432
+ }
4397
4433
  case "blocked":
4398
4434
  return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.` };
4399
4435
  case "conflict":
@@ -4414,7 +4450,7 @@ var import_child_process2 = require("child_process");
4414
4450
  var import_util2 = require("util");
4415
4451
  var import_crypto = require("crypto");
4416
4452
  var fs4 = __toESM(require("fs"));
4417
- var path8 = __toESM(require("path"));
4453
+ var path9 = __toESM(require("path"));
4418
4454
 
4419
4455
  // src/utils/shellPath.ts
4420
4456
  var import_child_process = require("child_process");
@@ -4443,28 +4479,28 @@ function posixBinDirs() {
4443
4479
  ];
4444
4480
  }
4445
4481
  function windowsBinDirs(env, home) {
4446
- const join22 = (...parts) => path5.win32.join(...parts);
4447
- const under = (base, ...parts) => base ? [join22(base, ...parts)] : [];
4482
+ const join24 = (...parts) => path5.win32.join(...parts);
4483
+ const under = (base, ...parts) => base ? [join24(base, ...parts)] : [];
4448
4484
  const appData = env.APPDATA;
4449
4485
  const localAppData = env.LOCALAPPDATA;
4450
4486
  const programData = env.ProgramData;
4451
4487
  return [
4452
4488
  // 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"),
4489
+ join24(home, ".local", "bin"),
4490
+ join24(home, ".opencode", "bin"),
4491
+ join24(home, ".claude", "bin"),
4492
+ join24(home, ".codex", "bin"),
4457
4493
  ...under(localAppData, "Programs", "claude"),
4458
4494
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
4459
4495
  // literal spelling is kept as a fallback for a host that does not export
4460
4496
  // APPDATA (a service account, a stripped CI container).
4461
4497
  ...under(appData, "npm"),
4462
- join22(home, "AppData", "Roaming", "npm"),
4498
+ join24(home, "AppData", "Roaming", "npm"),
4463
4499
  ...under(localAppData, "pnpm"),
4464
4500
  ...under(localAppData, "Yarn", "bin"),
4465
- join22(home, ".bun", "bin"),
4501
+ join24(home, ".bun", "bin"),
4466
4502
  // Windows package managers.
4467
- join22(home, "scoop", "shims"),
4503
+ join24(home, "scoop", "shims"),
4468
4504
  ...under(env.SCOOP, "shims"),
4469
4505
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
4470
4506
  ...under(programData, "chocolatey", "bin"),
@@ -4474,7 +4510,7 @@ function windowsBinDirs(env, home) {
4474
4510
  ...under(localAppData, "Microsoft", "WindowsApps"),
4475
4511
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
4476
4512
  ...under(localAppData, "Volta", "bin"),
4477
- join22(home, ".volta", "bin")
4513
+ join24(home, ".volta", "bin")
4478
4514
  ];
4479
4515
  }
4480
4516
  function wellKnownBinDirs(deps = {}) {
@@ -4578,6 +4614,7 @@ function savePrdMarkdown(workspace, slug2, markdown) {
4578
4614
 
4579
4615
  // src/services/worktreeLink.ts
4580
4616
  var fs3 = __toESM(require("fs"));
4617
+ var path8 = __toESM(require("path"));
4581
4618
  function linkPath(source, target, platform, hardLink = fs3.linkSync) {
4582
4619
  const isDir = fs3.statSync(source).isDirectory();
4583
4620
  if (platform !== "win32") {
@@ -4596,6 +4633,55 @@ function linkPath(source, target, platform, hardLink = fs3.linkSync) {
4596
4633
  return "copy";
4597
4634
  }
4598
4635
  }
4636
+ function mirrorDir(source, target, opts) {
4637
+ const copied = [];
4638
+ const links = [];
4639
+ const mirror = (from, to, prefix, top) => {
4640
+ fs3.mkdirSync(to, { recursive: true });
4641
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
4642
+ const name = path8.join(prefix, entry.name);
4643
+ const at = { from: path8.join(from, entry.name), to: path8.join(to, entry.name), name };
4644
+ if (entry.isSymbolicLink()) links.push(at);
4645
+ else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
4646
+ else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
4647
+ }
4648
+ };
4649
+ mirror(source, target, "", true);
4650
+ let pending = links;
4651
+ for (let before = Infinity; pending.length > 0 && pending.length < before; ) {
4652
+ before = pending.length;
4653
+ pending = pending.filter((link) => {
4654
+ const kind = relink(link.from, link.to, opts);
4655
+ if (kind === "copy") copied.push(link.name);
4656
+ return kind === null;
4657
+ });
4658
+ }
4659
+ return copied;
4660
+ }
4661
+ function relink(from, to, opts) {
4662
+ const text = fs3.readlinkSync(from);
4663
+ const pointee = path8.isAbsolute(text) ? rebased(text, opts) : text;
4664
+ if (opts.platform !== "win32") {
4665
+ if (!fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) && fs3.existsSync(path8.resolve(path8.dirname(from), pointee))) {
4666
+ fs3.symlinkSync(rebased(path8.resolve(path8.dirname(from), pointee), opts), to);
4667
+ return "symlink";
4668
+ }
4669
+ fs3.symlinkSync(pointee, to);
4670
+ return "symlink";
4671
+ }
4672
+ const resolved = fs3.existsSync(path8.resolve(path8.dirname(to), pointee)) ? path8.resolve(path8.dirname(to), pointee) : rebased(path8.resolve(path8.dirname(from), pointee), opts);
4673
+ try {
4674
+ fs3.statSync(resolved);
4675
+ } catch {
4676
+ return null;
4677
+ }
4678
+ return linkPath(resolved, to, opts.platform, opts.hardLink);
4679
+ }
4680
+ function rebased(pointee, opts) {
4681
+ const rel = path8.relative(opts.from, pointee);
4682
+ const inside = rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
4683
+ return inside && !rel.split(/[\\/]/).includes("node_modules") ? path8.join(opts.to, rel) : pointee;
4684
+ }
4599
4685
 
4600
4686
  // src/services/GitWorktreeIsolation.ts
4601
4687
  var execFileAsync = (0, import_util2.promisify)(import_child_process2.execFile);
@@ -4610,15 +4696,17 @@ var INHERITED_GIT_VARS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COM
4610
4696
  var LINKED_ARTIFACTS = /* @__PURE__ */ new Set(["node_modules", "vendor", ".venv", ".claude", ".opencode", ".codegraph", ".envrc"]);
4611
4697
  var isEnvFile = (name) => name.startsWith(".env");
4612
4698
  var INTEGRATION_DIR = "integration";
4699
+ var RUN_BRANCH = /^ordewell\/([^/]+)\/[^/]+$/;
4700
+ var RUN_WORKTREE = /.*[\\/]\.ordewell[\\/]worktrees[\\/]([^\\/]+)[\\/]/;
4613
4701
  var NESTED_REPO_DEPTH = 2;
4614
4702
  var NEVER_SCANNED = /* @__PURE__ */ new Set([".git", STATE_DIR, "node_modules"]);
4615
4703
  var GITLINK_MODE = "160000";
4616
4704
  function isInside(parent, child) {
4617
- const rel = path8.relative(parent, child);
4618
- return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
4705
+ const rel = path9.relative(parent, child);
4706
+ return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
4619
4707
  }
4620
4708
  function hasGitEntry(dir) {
4621
- return fs4.existsSync(path8.join(dir, ".git"));
4709
+ return fs4.existsSync(path9.join(dir, ".git"));
4622
4710
  }
4623
4711
  function firstLine(text) {
4624
4712
  return text.split(/\r?\n/, 1)[0].trim();
@@ -4634,12 +4722,20 @@ function statusPaths(porcelain) {
4634
4722
  }
4635
4723
  return paths;
4636
4724
  }
4725
+ function leftoverMarkerFiles(check) {
4726
+ const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
4727
+ return [...new Set(files)];
4728
+ }
4729
+ function settleStatus(record, status) {
4730
+ record.status = status;
4731
+ delete record.repairBase;
4732
+ }
4637
4733
  function sameDir(a, b) {
4638
4734
  const real = (p) => {
4639
4735
  try {
4640
4736
  return fs4.realpathSync.native(p);
4641
4737
  } catch {
4642
- return path8.resolve(p);
4738
+ return path9.resolve(p);
4643
4739
  }
4644
4740
  };
4645
4741
  return real(a) === real(b);
@@ -4673,8 +4769,8 @@ function inactive(reason, repos) {
4673
4769
  }
4674
4770
  function groupPathOf(listed) {
4675
4771
  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(/\/+$/, "");
4772
+ if (path9.posix.isAbsolute(slashed) || path9.win32.isAbsolute(slashed)) return null;
4773
+ const rel = path9.posix.normalize(slashed).replace(/\/+$/, "");
4678
4774
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
4679
4775
  }
4680
4776
  function matchLinks(root, patterns) {
@@ -4690,9 +4786,26 @@ function matchLinks(root, patterns) {
4690
4786
  }
4691
4787
  function segmentMatches(root, base, segment) {
4692
4788
  const under = (name) => base ? `${base}/${name}` : name;
4693
- if (!/[*?]/.test(segment)) return lexists(path8.join(root, base, segment)) ? [under(segment)] : [];
4789
+ if (!/[*?]/.test(segment)) return lexists(path9.join(root, base, segment)) ? [under(segment)] : [];
4694
4790
  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);
4791
+ return listDir(path9.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
4792
+ }
4793
+ function installDirs(root) {
4794
+ const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
4795
+ return ["node_modules", ...packages].filter((dir) => lexists(path9.join(root, dir)));
4796
+ }
4797
+ function workspaceGlobs(root) {
4798
+ let manifest;
4799
+ try {
4800
+ manifest = JSON.parse(fs4.readFileSync(path9.join(root, "package.json"), "utf8"));
4801
+ } catch {
4802
+ return [];
4803
+ }
4804
+ if (typeof manifest !== "object" || manifest === null || !("workspaces" in manifest)) return [];
4805
+ const { workspaces } = manifest;
4806
+ const listed = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
4807
+ if (!Array.isArray(listed)) return [];
4808
+ return listed.filter((glob) => typeof glob === "string" && !glob.startsWith("!"));
4696
4809
  }
4697
4810
  var GitWorktreeIsolation = class {
4698
4811
  constructor(deps) {
@@ -4746,7 +4859,7 @@ var GitWorktreeIsolation = class {
4746
4859
  groupPaths(workspaceRoot) {
4747
4860
  const listed = this.deps.config.workspaceRepos;
4748
4861
  if (listed.length === 0) return this.reposDirectlyIn(workspaceRoot);
4749
- const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path8.join(workspaceRoot, rel)));
4862
+ const paths = listed.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path9.join(workspaceRoot, rel)));
4750
4863
  return [...new Set(paths)].sort();
4751
4864
  }
4752
4865
  async committedRepos(workspaceRoot, paths) {
@@ -4762,7 +4875,7 @@ var GitWorktreeIsolation = class {
4762
4875
  return status.ok ? status.stdout.trim() !== "" : null;
4763
4876
  }
4764
4877
  reposDirectlyIn(dir) {
4765
- return this.childDirs(dir).filter((name) => hasGitEntry(path8.join(dir, name)));
4878
+ return this.childDirs(dir).filter((name) => hasGitEntry(path9.join(dir, name)));
4766
4879
  }
4767
4880
  childDirs(dir) {
4768
4881
  try {
@@ -4779,9 +4892,9 @@ var GitWorktreeIsolation = class {
4779
4892
  async nestedRepos(workspaceRoot, toplevel) {
4780
4893
  const found = [];
4781
4894
  const walk = (rel, depth) => {
4782
- for (const name of this.childDirs(path8.join(workspaceRoot, rel))) {
4895
+ for (const name of this.childDirs(path9.join(workspaceRoot, rel))) {
4783
4896
  const child = rel ? `${rel}/${name}` : name;
4784
- if (hasGitEntry(path8.join(workspaceRoot, child))) found.push(child);
4897
+ if (hasGitEntry(path9.join(workspaceRoot, child))) found.push(child);
4785
4898
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
4786
4899
  }
4787
4900
  };
@@ -4802,7 +4915,7 @@ var GitWorktreeIsolation = class {
4802
4915
  const [meta, file] = entry.split(" ");
4803
4916
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
4804
4917
  }
4805
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path8.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4918
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path9.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
4806
4919
  for (const entry of declared.stdout.split("\0")) {
4807
4920
  const value = entry.split("\n")[1];
4808
4921
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -4858,7 +4971,7 @@ var GitWorktreeIsolation = class {
4858
4971
  async acceptsWorktree(run, repo) {
4859
4972
  const dir = this.integrationDir(run, repo);
4860
4973
  ensureStateDirIgnored(run.workspaceRoot);
4861
- fs4.mkdirSync(path8.dirname(dir), { recursive: true });
4974
+ fs4.mkdirSync(path9.dirname(dir), { recursive: true });
4862
4975
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
4863
4976
  await this.removeWorktreeDir(run, repo, dir, []);
4864
4977
  return added.ok;
@@ -4873,12 +4986,12 @@ var GitWorktreeIsolation = class {
4873
4986
  if (isolated.includes(SELF_REPO)) return [];
4874
4987
  const shared = [];
4875
4988
  const walk = (rel) => {
4876
- for (const name of listDir(path8.join(workspaceRoot, rel))) {
4989
+ for (const name of listDir(path9.join(workspaceRoot, rel))) {
4877
4990
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
4878
4991
  const child = rel ? `${rel}/${name}` : name;
4879
4992
  if (isolated.includes(child)) continue;
4880
4993
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
4881
- else if (resolves(path8.join(workspaceRoot, child))) shared.push(child);
4994
+ else if (resolves(path9.join(workspaceRoot, child))) shared.push(child);
4882
4995
  }
4883
4996
  };
4884
4997
  walk("");
@@ -4895,17 +5008,17 @@ var GitWorktreeIsolation = class {
4895
5008
  const copied = [];
4896
5009
  try {
4897
5010
  for (const repo of run.repos) {
4898
- const worktree = path8.join(dir, repo.path);
5011
+ const worktree = path9.join(dir, repo.path);
4899
5012
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
4900
- fs4.mkdirSync(path8.dirname(worktree), { recursive: true });
5013
+ fs4.mkdirSync(path9.dirname(worktree), { recursive: true });
4901
5014
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
4902
5015
  record.repos[repo.path] = { worktree, linked: [] };
4903
5016
  }
4904
5017
  run.shared = this.sharedPaths(run.workspaceRoot, run.repos.map((r) => r.path));
4905
5018
  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);
5019
+ const target = path9.join(dir, rel);
5020
+ fs4.mkdirSync(path9.dirname(target), { recursive: true });
5021
+ if (linkPath(path9.join(run.workspaceRoot, rel), target, this.platform) === "copy") copied.push(rel);
4909
5022
  }
4910
5023
  for (const repo of run.repos) {
4911
5024
  const entry = record.repos[repo.path];
@@ -4913,7 +5026,7 @@ var GitWorktreeIsolation = class {
4913
5026
  if (repo.path === SELF_REPO) cwd = inRepo;
4914
5027
  const boot = await this.bootstrap(repo, inRepo);
4915
5028
  entry.linked = boot.linked;
4916
- copied.push(...boot.copied.map((name) => path8.posix.join(repo.path, name)));
5029
+ copied.push(...boot.copied.map((name) => path9.posix.join(repo.path, name)));
4917
5030
  }
4918
5031
  } catch (err) {
4919
5032
  record.status = "failed";
@@ -4924,6 +5037,53 @@ var GitWorktreeIsolation = class {
4924
5037
  return { cwd, branch, copied };
4925
5038
  });
4926
5039
  }
5040
+ reopen(task, run) {
5041
+ return this.admin(run.workspaceRoot, async () => {
5042
+ const record = run.tasks[task.id];
5043
+ if (record?.status !== "conflict") throw new Error(`Task ${task.order} has no conflict to repair`);
5044
+ let cwd = record.workspace;
5045
+ const repairBase = {};
5046
+ for (const repo of run.repos) {
5047
+ const entry = record.repos[repo.path];
5048
+ if (!entry) continue;
5049
+ if (!fs4.existsSync(entry.worktree)) throw new Error(`The worktree of task ${task.order} is gone: ${entry.worktree}`);
5050
+ if (repo.path === SELF_REPO) cwd = await this.inWorkspacePlace(repo, entry.worktree);
5051
+ if (entry.changed || repo.path === record.conflictRepo) {
5052
+ repairBase[repo.path] = (await this.git(repo.root, ["rev-parse", "--verify", repo.integrationBranch])).trim();
5053
+ }
5054
+ }
5055
+ const files = (record.conflictFiles ?? []).map((file) => record.conflictRepo && record.conflictRepo !== SELF_REPO ? `${record.conflictRepo}/${file}` : file);
5056
+ record.repairs = (record.repairs ?? 0) + 1;
5057
+ record.repairBase = repairBase;
5058
+ record.repairedFiles = [.../* @__PURE__ */ new Set([...record.repairedFiles ?? [], ...files])];
5059
+ record.status = "repairing";
5060
+ return { cwd, branch: record.branch, copied: [] };
5061
+ });
5062
+ }
5063
+ async verifyRepair(task, run) {
5064
+ const record = run.tasks[task.id];
5065
+ if (!record?.repairBase) return { ok: false, reason: "failed", repo: record?.conflictRepo ?? SELF_REPO };
5066
+ for (const repo of run.repos) {
5067
+ const entry = record.repos[repo.path];
5068
+ if (!entry) continue;
5069
+ try {
5070
+ await this.commitWorktree(repo, record, entry);
5071
+ } catch {
5072
+ return { ok: false, reason: "failed", repo: repo.path };
5073
+ }
5074
+ }
5075
+ for (const repo of run.repos) {
5076
+ const base = record.repairBase[repo.path];
5077
+ if (base === void 0) continue;
5078
+ const contains = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", base, record.branch]);
5079
+ if (!contains.ok) return { ok: false, reason: contains.code === 1 ? "not-merged" : "failed", repo: repo.path };
5080
+ const check = await this.tryGit(repo.root, ["-c", "core.quotePath=false", "diff", "--check", base, record.branch]);
5081
+ if (!check.ok && check.code !== 2) return { ok: false, reason: "failed", repo: repo.path };
5082
+ const files = leftoverMarkerFiles(check.stdout);
5083
+ if (files.length > 0) return { ok: false, reason: "conflict-markers", repo: repo.path, files };
5084
+ }
5085
+ return { ok: true };
5086
+ }
4927
5087
  /**
4928
5088
  * The place in a repo's worktree that matches where the workspace sits in
4929
5089
  * the real repo: the worktree itself, or a subdirectory of it when the
@@ -4931,7 +5091,7 @@ var GitWorktreeIsolation = class {
4931
5091
  */
4932
5092
  async inWorkspacePlace(repo, worktree) {
4933
5093
  const prefix = await this.prefixOf(repo);
4934
- return prefix ? path8.join(worktree, prefix) : worktree;
5094
+ return prefix ? path9.join(worktree, prefix) : worktree;
4935
5095
  }
4936
5096
  async prefixOf(repo) {
4937
5097
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -4951,6 +5111,7 @@ var GitWorktreeIsolation = class {
4951
5111
  if (!record) return;
4952
5112
  if (opts.keep) {
4953
5113
  if (record.status === "active") record.status = "kept";
5114
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4954
5115
  return;
4955
5116
  }
4956
5117
  await this.removeTask(run, record, { dropRecord: true });
@@ -4970,6 +5131,7 @@ var GitWorktreeIsolation = class {
4970
5131
  for (const record of Object.values(run.tasks)) {
4971
5132
  if (record.status === "active") await this.removeTask(run, record, { dropRecord: true });
4972
5133
  else if (record.status === "merged") await this.removeTask(run, record, { dropRecord: false });
5134
+ else if (record.status === "repairing") settleStatus(record, "conflict");
4973
5135
  }
4974
5136
  await this.removeUnowned(run);
4975
5137
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
@@ -5064,14 +5226,59 @@ var GitWorktreeIsolation = class {
5064
5226
  await this.removeTask(run, record, { dropRecord: record.status !== "merged" });
5065
5227
  }
5066
5228
  await this.removeUnowned(run);
5067
- if (!opts.keepIntegration) {
5068
- for (const repo of run.repos) await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
5229
+ if (opts.integration !== "keep") {
5230
+ for (const repo of run.repos) {
5231
+ if (opts.integration === "delete") await this.tryGit(repo.root, ["branch", "-D", repo.integrationBranch]);
5232
+ else await this.deleteIfMerged(repo.root, repo.integrationBranch);
5233
+ }
5069
5234
  run.tasks = {};
5070
5235
  this.removeIfEmpty(this.runRoot(run));
5071
5236
  }
5072
5237
  for (const repo of run.repos) await this.tryGit(repo.root, ["worktree", "prune"]);
5073
5238
  });
5074
5239
  }
5240
+ sweep(run) {
5241
+ return this.admin(run.workspaceRoot, async () => {
5242
+ const failed = [];
5243
+ for (const repo of run.repos) {
5244
+ if (!await this.sweepRepo(run, repo)) failed.push(repo.path);
5245
+ }
5246
+ if (failed.length > 0) throw new Error(`git failed clearing merged branches of earlier runs in ${failed.join(", ")}`);
5247
+ });
5248
+ }
5249
+ /** False when git failed anywhere; the other branches are still tried. */
5250
+ async sweepRepo(run, repo) {
5251
+ await this.tryGit(repo.root, ["worktree", "prune"]);
5252
+ const worktrees = await this.tryGit(repo.root, ["worktree", "list", "--porcelain"]);
5253
+ const refs = await this.tryGit(repo.root, ["for-each-ref", "--format=%(refname)", "refs/heads/ordewell/"]);
5254
+ if (!worktrees.ok || !refs.ok) return false;
5255
+ const checkedOut = /* @__PURE__ */ new Set();
5256
+ const liveRuns = /* @__PURE__ */ new Set();
5257
+ for (const line of worktrees.stdout.split(/\r?\n/)) {
5258
+ if (line.startsWith("branch refs/heads/")) checkedOut.add(line.slice("branch refs/heads/".length));
5259
+ const runId = line.startsWith("worktree ") ? RUN_WORKTREE.exec(line)?.[1] : void 0;
5260
+ if (runId) liveRuns.add(runId);
5261
+ }
5262
+ let ok = true;
5263
+ for (const ref of refs.stdout.split(/\r?\n/).filter(Boolean)) {
5264
+ const branch = ref.slice("refs/heads/".length);
5265
+ const runId = RUN_BRANCH.exec(branch)?.[1];
5266
+ if (!runId || runId === run.id || liveRuns.has(runId) || checkedOut.has(branch)) continue;
5267
+ if (await this.deleteIfMerged(repo.root, branch) === "failed") ok = false;
5268
+ }
5269
+ return ok;
5270
+ }
5271
+ /**
5272
+ * Delete `branch` only if the repo's checked-out HEAD already contains it —
5273
+ * HEAD, not an upstream: what the user has checked out is what they merged
5274
+ * into. `-d` rather than `-D`, so git also refuses a branch a worktree has
5275
+ * checked out.
5276
+ */
5277
+ async deleteIfMerged(root, branch) {
5278
+ const merged = await this.tryGit(root, ["merge-base", "--is-ancestor", branch, "HEAD"]);
5279
+ if (!merged.ok) return merged.code === 1 ? "kept" : "failed";
5280
+ return (await this.tryGit(root, ["branch", "-d", branch])).ok ? "deleted" : "failed";
5281
+ }
5075
5282
  async drain() {
5076
5283
  try {
5077
5284
  while (this.waiting.length > 0) {
@@ -5124,21 +5331,24 @@ var GitWorktreeIsolation = class {
5124
5331
  return this.stopLanding(record, "failed");
5125
5332
  }
5126
5333
  for (const repo of changed) {
5127
- const outcome = await this.mergeTask(run, repo, record);
5334
+ const { outcome, files } = await this.mergeTask(run, repo, record);
5128
5335
  if (outcome === "merged") continue;
5129
- return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo) : this.stopLanding(record, "failed", repo);
5336
+ return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files) : this.stopLanding(record, "failed", repo);
5130
5337
  }
5131
5338
  }
5132
5339
  delete run.landing;
5133
- record.status = "merged";
5340
+ settleStatus(record, "merged");
5134
5341
  delete record.conflictRepo;
5342
+ delete record.conflictFiles;
5135
5343
  await this.admin(run.workspaceRoot, () => this.removeTask(run, record, { dropRecord: false })).catch(() => void 0);
5136
5344
  return "merged";
5137
5345
  }
5138
- stopLanding(record, outcome, repo) {
5139
- record.status = outcome;
5346
+ stopLanding(record, outcome, repo, files) {
5347
+ settleStatus(record, outcome);
5140
5348
  if (repo) record.conflictRepo = repo.path;
5141
5349
  else delete record.conflictRepo;
5350
+ if (files && files.length > 0) record.conflictFiles = files;
5351
+ else delete record.conflictFiles;
5142
5352
  return outcome;
5143
5353
  }
5144
5354
  /** 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 +5357,13 @@ var GitWorktreeIsolation = class {
5147
5357
  try {
5148
5358
  dir = await this.ensureIntegrationWorktree(run, repo);
5149
5359
  } catch {
5150
- return "failed";
5360
+ return { outcome: "failed", files: [] };
5151
5361
  }
5152
5362
  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;
5363
+ if (merge.ok) return { outcome: "merged", files: [] };
5364
+ const files = await this.unmergedPaths(dir);
5155
5365
  if (await this.mergeInProgress(dir)) await this.abortMerge(dir);
5156
- return conflicted ? "conflict" : "failed";
5366
+ return { outcome: files.length > 0 ? "conflict" : "failed", files };
5157
5367
  }
5158
5368
  /**
5159
5369
  * Return every repo of the landing in flight to its recorded tip, then
@@ -5221,15 +5431,15 @@ var GitWorktreeIsolation = class {
5221
5431
  await this.git(entry.worktree, ["commit", "-q", "-m", `ordewell: task ${record.order} ${firstLine(record.title)}`]);
5222
5432
  }
5223
5433
  integrationDir(run, repo) {
5224
- return path8.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
5434
+ return path9.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
5225
5435
  }
5226
5436
  async ensureIntegrationWorktree(run, repo) {
5227
5437
  return this.admin(run.workspaceRoot, async () => {
5228
5438
  const dir = this.integrationDir(run, repo);
5229
- if (fs4.existsSync(path8.join(dir, ".git"))) return dir;
5439
+ if (fs4.existsSync(path9.join(dir, ".git"))) return dir;
5230
5440
  await this.tryGit(repo.root, ["worktree", "prune"]);
5231
5441
  ensureStateDirIgnored(run.workspaceRoot);
5232
- fs4.mkdirSync(path8.dirname(dir), { recursive: true });
5442
+ fs4.mkdirSync(path9.dirname(dir), { recursive: true });
5233
5443
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
5234
5444
  return dir;
5235
5445
  });
@@ -5264,20 +5474,20 @@ var GitWorktreeIsolation = class {
5264
5474
  return;
5265
5475
  }
5266
5476
  for (const entry of entries) {
5267
- const target = path8.join(dir, entry.name);
5477
+ const target = path9.join(dir, entry.name);
5268
5478
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
5269
5479
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
5270
5480
  }
5271
5481
  }
5272
5482
  /** Task workspaces and branches under this run that no record accounts for. */
5273
5483
  async removeUnowned(run) {
5274
- const owned = new Set(Object.values(run.tasks).map((r) => path8.resolve(r.workspace)));
5484
+ const owned = new Set(Object.values(run.tasks).map((r) => path9.resolve(r.workspace)));
5275
5485
  const root = this.runRoot(run);
5276
5486
  if (fs4.existsSync(root)) {
5277
5487
  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), []);
5488
+ const dir = path9.join(root, entry);
5489
+ if (entry === INTEGRATION_DIR || owned.has(path9.resolve(dir))) continue;
5490
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path9.join(dir, repo.path), []);
5281
5491
  this.removeTaskWorkspace(run, dir);
5282
5492
  }
5283
5493
  }
@@ -5291,14 +5501,18 @@ var GitWorktreeIsolation = class {
5291
5501
  }
5292
5502
  }
5293
5503
  async removeWorktreeDir(run, repo, dir, linked) {
5294
- for (const name of linked) this.unlinkIfLink(path8.join(dir, name));
5504
+ for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
5505
+ const target = path9.join(dir, name);
5506
+ this.unlinkIfLink(target);
5507
+ this.unlinkLinksUnder(target);
5508
+ }
5295
5509
  for (const name of fs4.existsSync(dir) ? fs4.readdirSync(dir) : []) {
5296
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path8.join(dir, name));
5510
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path9.join(dir, name));
5297
5511
  }
5298
5512
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
5299
5513
  if (fs4.existsSync(dir) && isInside(this.runRoot(run), dir)) fs4.rmSync(dir, { recursive: true, force: true });
5300
5514
  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);
5515
+ for (let parent = path9.dirname(dir); isInside(this.runRoot(run), parent); parent = path9.dirname(parent)) this.removeIfEmpty(parent);
5302
5516
  }
5303
5517
  unlinkIfLink(target) {
5304
5518
  try {
@@ -5313,14 +5527,14 @@ var GitWorktreeIsolation = class {
5313
5527
  }
5314
5528
  }
5315
5529
  runRoot(run) {
5316
- return path8.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
5530
+ return path9.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
5317
5531
  }
5318
5532
  namesFor(run, task) {
5319
5533
  const slug2 = sanitizeSlug(task.title).slice(0, 40).replace(/-+$/, "") || "task";
5320
5534
  let name = `${task.order}-${slug2}`;
5321
5535
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task.id && r.branch === `ordewell/${run.id}/${name}`);
5322
5536
  if (clash) name = `${name}-${sanitizeSlug(task.id) || "x"}`;
5323
- return { branch: `ordewell/${run.id}/${name}`, dir: path8.join(this.runRoot(run), name) };
5537
+ return { branch: `ordewell/${run.id}/${name}`, dir: path9.join(this.runRoot(run), name) };
5324
5538
  }
5325
5539
  /**
5326
5540
  * Make one repo's worktree runnable: the default artifacts, unless a setup
@@ -5333,16 +5547,26 @@ var GitWorktreeIsolation = class {
5333
5547
  const linked = [];
5334
5548
  const copied = [];
5335
5549
  const link = (name) => {
5336
- const target = path8.join(cwd, name);
5550
+ const target = path9.join(cwd, name);
5337
5551
  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);
5552
+ fs4.mkdirSync(path9.dirname(target), { recursive: true });
5553
+ if (linkPath(path9.join(repo.root, name), target, this.platform) === "copy") copied.push(name);
5554
+ linked.push(name);
5555
+ };
5556
+ const mirror = (name) => {
5557
+ const target = path9.join(cwd, name);
5558
+ if (lexists(target) || !fs4.existsSync(path9.dirname(target))) return;
5559
+ const source = path9.join(repo.root, name);
5560
+ if (!fs4.lstatSync(source).isDirectory()) return link(name);
5561
+ const mirrored = mirrorDir(source, target, { platform: this.platform, from: repo.root, to: cwd });
5562
+ copied.push(...mirrored.map((entry) => path9.join(name, entry)));
5340
5563
  linked.push(name);
5341
5564
  };
5342
5565
  if (!setup) {
5343
5566
  for (const name of fs4.readdirSync(repo.root)) {
5344
- if (name !== STATE_DIR && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
5567
+ if (name !== STATE_DIR && name !== "node_modules" && (LINKED_ARTIFACTS.has(name) || isEnvFile(name))) link(name);
5345
5568
  }
5569
+ for (const dir of installDirs(repo.root)) mirror(dir);
5346
5570
  }
5347
5571
  for (const name of matchLinks(repo.root, this.deps.config.worktreeLinks)) link(name);
5348
5572
  if (setup) await this.runSetup(setup, repo, cwd);
@@ -6004,6 +6228,7 @@ function researchPhaseBlock(harnessMode) {
6004
6228
  }
6005
6229
  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
6230
  var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
6231
+ 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
6232
  function repoGroupSection(isolated) {
6008
6233
  if (!isolated || isolated.repos.length === 1 && isolated.repos[0] === SELF_REPO) return [];
6009
6234
  return [
@@ -6095,6 +6320,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
6095
6320
  "DEPENDENCY & PARALLELISM:",
6096
6321
  "- Independent slices should have NO dependencies \u2014 they run in parallel.",
6097
6322
  "- Only add dependencies when a slice truly depends on artifacts another slice creates.",
6323
+ SHARED_APPEND_FILE_RULE,
6098
6324
  ...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
6099
6325
  ...repoGroupSection(variant.isolatedExecution),
6100
6326
  "",
@@ -6196,6 +6422,7 @@ function corePlannerPrompt(isolatedExecution) {
6196
6422
  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
6423
  "- Only add dependencies when the second slice truly depends on artifacts (files, APIs) that the first slice creates.",
6198
6424
  "- Prefer parallelism over serial chains. A plan with 3 independent slices running in parallel is better than 3 sequential tasks.",
6425
+ SHARED_APPEND_FILE_RULE,
6199
6426
  isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
6200
6427
  ...repoGroupSection(isolatedExecution),
6201
6428
  "",
@@ -6473,6 +6700,31 @@ ${task.prompt ?? task.description}`;
6473
6700
  asked
6474
6701
  ].join("\n");
6475
6702
  }
6703
+ function buildConflictRepairPrompt(task, conflict, integrationBranch) {
6704
+ const { branch } = conflict;
6705
+ const files = conflict.conflictFiles ?? [];
6706
+ const asked = `What the task was asked to do:
6707
+ ${task.prompt ?? task.description}`;
6708
+ const resolve7 = "resolve every conflict so that both sides' intent survives: keep the work already integrated and add what the task contributed.";
6709
+ const repos = conflict.repos.filter((repo) => repo !== SELF_REPO);
6710
+ if (repos.length === 0) {
6711
+ return [
6712
+ `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.`,
6713
+ `This is the task's own worktree, on \`${branch}\`, with its work committed. Run \`git merge --no-edit ${integrationBranch}\` here and ${resolve7} Never drop either side wholesale, and never abort the merge or reset it away.`,
6714
+ "Build and test the result the way this project does, commit the merge, and only then print the completion marker.",
6715
+ "",
6716
+ asked
6717
+ ].join("\n");
6718
+ }
6719
+ const stoppedIn = conflict.conflictRepo ?? repos[0];
6720
+ return [
6721
+ `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.`,
6722
+ `This is the task's own workspace, with each repository at its usual path on \`${branch}\` and the task's work committed. In each repository the task changed \u2014 ${repos.join(", ")} \u2014 run \`git merge --no-edit ${integrationBranch}\` inside that repository's directory, and ${resolve7} Never drop either side wholesale, and never abort a merge or reset it away.`,
6723
+ "Build and test the result the way this project does, commit the merge in each repository, and only then print the completion marker.",
6724
+ "",
6725
+ asked
6726
+ ].join("\n");
6727
+ }
6476
6728
 
6477
6729
  // src/services/PlanValidator.ts
6478
6730
  function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
@@ -7826,8 +8078,8 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
7826
8078
 
7827
8079
  // src/services/ContextCollector.ts
7828
8080
  var ORDEWELL_CONTEXT_MAX = 8e3;
7829
- async function collectResearchContext(fs14, runners) {
7830
- const collector = new ContextCollector(fs14);
8081
+ async function collectResearchContext(fs15, runners) {
8082
+ const collector = new ContextCollector(fs15);
7831
8083
  let contextStr = "";
7832
8084
  let firstContext = null;
7833
8085
  for (const r of runners) {
@@ -7854,8 +8106,8 @@ ${firstContext.dirStructure}
7854
8106
  return contextStr;
7855
8107
  }
7856
8108
  var ContextCollector = class {
7857
- constructor(fs14, registry) {
7858
- this.fs = fs14;
8109
+ constructor(fs15, registry) {
8110
+ this.fs = fs15;
7859
8111
  this.registry = registry;
7860
8112
  }
7861
8113
  fs;
@@ -7936,10 +8188,10 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
7936
8188
  const scope = args.include ? ` in ${args.include}` : "";
7937
8189
  return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
7938
8190
  }
7939
- const path23 = args.filePath || args.file_path || args.path;
7940
- if (path23) {
7941
- const basename = String(path23).split("/").pop() || String(path23);
7942
- return `${display} ${basename}`;
8191
+ const path26 = args.filePath || args.file_path || args.path;
8192
+ if (path26) {
8193
+ const basename2 = String(path26).split("/").pop() || String(path26);
8194
+ return `${display} ${basename2}`;
7943
8195
  }
7944
8196
  if (args.pattern) {
7945
8197
  return `${display} ${args.pattern}`;
@@ -7972,22 +8224,22 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
7972
8224
  function refused(output) {
7973
8225
  return { success: false, output, truncated: false };
7974
8226
  }
7975
- function nonPromptingFs(fs14) {
7976
- const withinWorkspace = (p) => resolveWithin(fs14.getWorkspaceRoot(), p ?? ".").inside;
8227
+ function nonPromptingFs(fs15) {
8228
+ const withinWorkspace = (p) => resolveWithin(fs15.getWorkspaceRoot(), p ?? ".").inside;
7977
8229
  const outsideRefusal = (p) => refused(
7978
8230
  `Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
7979
8231
  );
7980
8232
  return {
7981
- getWorkspaceRoot: () => fs14.getWorkspaceRoot(),
7982
- readFile: (p, opts) => withinWorkspace(p) ? fs14.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
8233
+ getWorkspaceRoot: () => fs15.getWorkspaceRoot(),
8234
+ readFile: (p, opts) => withinWorkspace(p) ? fs15.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
7983
8235
  readFiles: (paths) => {
7984
8236
  const escaping = paths.find((p) => !withinWorkspace(p));
7985
- return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs14.readFiles(paths);
8237
+ return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs15.readFiles(paths);
7986
8238
  },
7987
- glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs14.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
7988
- grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs14.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
7989
- findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs14.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
7990
- listDir: (p, depth) => withinWorkspace(p) ? fs14.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
8239
+ glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
8240
+ grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs15.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
8241
+ findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs15.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
8242
+ listDir: (p, depth) => withinWorkspace(p) ? fs15.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
7991
8243
  bash: async (command, signal) => {
7992
8244
  const dialect = resolveResearchShell().dialect;
7993
8245
  const { tier, reason } = classifyCommand(command, { dialect });
@@ -7998,13 +8250,13 @@ function nonPromptingFs(fs14) {
7998
8250
  }
7999
8251
  const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
8000
8252
  if (escaping) return outsideRefusal(escaping);
8001
- return fs14.bash(command, signal);
8253
+ return fs15.bash(command, signal);
8002
8254
  }
8003
8255
  };
8004
8256
  }
8005
8257
  async function runLoop(prompt, deps) {
8006
8258
  if (deps.signal?.aborted) return "[research agent aborted before starting]";
8007
- const fs14 = nonPromptingFs(deps.fs);
8259
+ const fs15 = nonPromptingFs(deps.fs);
8008
8260
  const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
8009
8261
  let turn = await chat.sendMessage(prompt, deps.signal);
8010
8262
  for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
@@ -8021,7 +8273,7 @@ ${turn.text}`;
8021
8273
  if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
8022
8274
  const toolArgs = JSON.stringify(tc.args);
8023
8275
  deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
8024
- const res = await executeTool(tc.name, args, fs14, void 0, deps.signal);
8276
+ const res = await executeTool(tc.name, args, fs15, void 0, deps.signal);
8025
8277
  const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
8026
8278
  [... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
8027
8279
  const stepEntry = {
@@ -8137,7 +8389,7 @@ var BaseAiService = class _BaseAiService {
8137
8389
  * steering/failed result and the loop continues sequentially: a subagent can
8138
8390
  * never fail a plan or a turn.
8139
8391
  */
8140
- async executeSpawnAgent(args, fs14, onProgress, signal, subagentId) {
8392
+ async executeSpawnAgent(args, fs15, onProgress, signal, subagentId) {
8141
8393
  const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
8142
8394
  if (!prompt) {
8143
8395
  return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
@@ -8148,20 +8400,20 @@ var BaseAiService = class _BaseAiService {
8148
8400
  if (!chat) throw new Error("subagent chats are not available for this provider");
8149
8401
  return chat;
8150
8402
  },
8151
- fs: fs14,
8403
+ fs: fs15,
8152
8404
  signal,
8153
8405
  onProgress: (progress) => onProgress({ ...progress, subagentId })
8154
8406
  });
8155
8407
  }
8156
8408
  /** Collect project context for the planning phase. Shared with the harness backend. */
8157
- static async collectResearchContext(fs14, runners) {
8158
- return collectResearchContext(fs14, runners);
8409
+ static async collectResearchContext(fs15, runners) {
8410
+ return collectResearchContext(fs15, runners);
8159
8411
  }
8160
8412
  /**
8161
8413
  * Execute one round of tool calls and report each through onProgress.
8162
8414
  * Returns the results to feed back plus the log entries produced.
8163
8415
  */
8164
- async executeToolCalls(toolCalls, fs14, onProgress, fetcher, stepIndex, thinking, signal) {
8416
+ async executeToolCalls(toolCalls, fs15, onProgress, fetcher, stepIndex, thinking, signal) {
8165
8417
  const toolResults = [];
8166
8418
  const logEntries = [];
8167
8419
  let resultsText = "";
@@ -8180,18 +8432,18 @@ var BaseAiService = class _BaseAiService {
8180
8432
  const subagentId = (0, import_uuid3.v4)();
8181
8433
  spawnSubagentIds.set(i, subagentId);
8182
8434
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
8183
- precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs14, onProgress, signal, subagentId));
8435
+ precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs15, onProgress, signal, subagentId));
8184
8436
  }) : Promise.resolve(),
8185
8437
  parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
8186
8438
  onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
8187
- precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs14, fetcher, signal));
8439
+ precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs15, fetcher, signal));
8188
8440
  }) : Promise.resolve()
8189
8441
  ]);
8190
8442
  for (const [index, tc] of toolCalls.entries()) {
8191
8443
  const toolArgs = normalizedArgs[index];
8192
8444
  const preResult = precomputed.get(index);
8193
8445
  if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
8194
- const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs14, fetcher, signal);
8446
+ const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs15, fetcher, signal);
8195
8447
  const MAX_RESPONSE_CHARS = 5e4;
8196
8448
  const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
8197
8449
  const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
@@ -8377,7 +8629,7 @@ ${llmOutput}
8377
8629
  * parsed tasks if a plan was emitted mid-loop, or null if the loop
8378
8630
  * exhausted without a valid plan.
8379
8631
  */
8380
- async runResearchLoop(chat, firstMessage, fs14, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
8632
+ async runResearchLoop(chat, firstMessage, fs15, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
8381
8633
  const researchLog = initialLog ? [...initialLog] : [];
8382
8634
  if (!initialLog && userGoal) {
8383
8635
  researchLog.push({
@@ -8404,7 +8656,7 @@ ${llmOutput}
8404
8656
  if (!turn.hasToolCalls) break;
8405
8657
  const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
8406
8658
  turn.toolCalls,
8407
- fs14,
8659
+ fs15,
8408
8660
  onProgress,
8409
8661
  fetcher,
8410
8662
  step,
@@ -8637,11 +8889,11 @@ ${repairHint}` }] : parts;
8637
8889
  return this.consumePlanStream(result, onToken);
8638
8890
  }
8639
8891
  // --- IAiService implementation ---
8640
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
8892
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
8641
8893
  this.ensureInit();
8642
8894
  const { autonomousDefault } = modes;
8643
8895
  const genModel = this.getPlanningModel();
8644
- const contextStr = await BaseAiService.collectResearchContext(fs14, runners);
8896
+ const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
8645
8897
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
8646
8898
  const firstMessage = `${buildResearchToolsPrompt()}
8647
8899
 
@@ -8652,17 +8904,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
8652
8904
  generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
8653
8905
  });
8654
8906
  const researchChat = new GeminiResearchChat(chat);
8655
- const result = await this.runResearchLoop(researchChat, firstMessage, fs14, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
8907
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
8656
8908
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
8657
8909
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
8658
8910
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
8659
8911
  }
8660
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
8912
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
8661
8913
  this.ensureInit();
8662
8914
  const { autonomousDefault } = modes;
8663
8915
  let ctx = "";
8664
- if (fs14) {
8665
- const collected = await new ContextCollector(fs14).collect(runners[0] ?? "claude-code");
8916
+ if (fs15) {
8917
+ const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
8666
8918
  if (collected.aiflowContext) ctx = `
8667
8919
  <aiflow_context>
8668
8920
  ${collected.aiflowContext}
@@ -8678,12 +8930,12 @@ ${collected.aiflowContext}
8678
8930
  autonomousDefault
8679
8931
  );
8680
8932
  }
8681
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
8933
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
8682
8934
  this.ensureInit();
8683
8935
  const { autonomousDefault } = modes;
8684
8936
  let aiflowContext;
8685
- if (fs14) {
8686
- const collected = await new ContextCollector(fs14).collect(existingPlan.runners[0] ?? "claude-code");
8937
+ if (fs15) {
8938
+ const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
8687
8939
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
8688
8940
  }
8689
8941
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -8919,11 +9171,11 @@ ${buildResearchToolsPrompt()}` }
8919
9171
  }
8920
9172
  }
8921
9173
  // --- IAiService implementation ---
8922
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
9174
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
8923
9175
  this.ensureInit();
8924
9176
  const client = this.getClient();
8925
9177
  const { autonomousDefault } = modes;
8926
- const contextStr = await BaseAiService.collectResearchContext(fs14, runners);
9178
+ const contextStr = await BaseAiService.collectResearchContext(fs15, runners);
8927
9179
  const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
8928
9180
  const userText = `User goal: ${userDescription}`;
8929
9181
  const firstMessage = `${buildResearchToolsPrompt()}
@@ -8941,17 +9193,17 @@ ${userText}`;
8941
9193
  toOpenAiTools(),
8942
9194
  (delta) => onProgress({ type: "thinking", text: delta })
8943
9195
  );
8944
- const result = await this.runResearchLoop(researchChat, firstMessage, fs14, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
9196
+ const result = await this.runResearchLoop(researchChat, firstMessage, fs15, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
8945
9197
  if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
8946
9198
  const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
8947
9199
  return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
8948
9200
  }
8949
- async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
9201
+ async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
8950
9202
  this.ensureInit();
8951
9203
  const { autonomousDefault } = modes;
8952
9204
  let ctx = "";
8953
- if (fs14) {
8954
- const collected = await new ContextCollector(fs14).collect(runners[0] ?? "claude-code");
9205
+ if (fs15) {
9206
+ const collected = await new ContextCollector(fs15).collect(runners[0] ?? "claude-code");
8955
9207
  if (collected.aiflowContext) ctx = `
8956
9208
  <aiflow_context>
8957
9209
  ${collected.aiflowContext}
@@ -8967,12 +9219,12 @@ ${collected.aiflowContext}
8967
9219
  autonomousDefault
8968
9220
  );
8969
9221
  }
8970
- async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs14, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
9222
+ async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs15, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
8971
9223
  this.ensureInit();
8972
9224
  const { autonomousDefault } = modes;
8973
9225
  let aiflowContext;
8974
- if (fs14) {
8975
- const collected = await new ContextCollector(fs14).collect(existingPlan.runners[0] ?? "claude-code");
9226
+ if (fs15) {
9227
+ const collected = await new ContextCollector(fs15).collect(existingPlan.runners[0] ?? "claude-code");
8976
9228
  if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
8977
9229
  }
8978
9230
  const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
@@ -8991,6 +9243,9 @@ ${collected.aiflowContext}
8991
9243
  }
8992
9244
  };
8993
9245
 
9246
+ // src/services/TaskOrchestrator.ts
9247
+ var path12 = __toESM(require("path"));
9248
+
8994
9249
  // src/services/promptAugment.ts
8995
9250
  var MAX_TAIL_CHARS = 500;
8996
9251
  var DEFAULT_PLAN_MAP_MAX_ENTRIES = 30;
@@ -9577,17 +9832,229 @@ ${line}
9577
9832
 
9578
9833
  // src/services/transcriptCapture.ts
9579
9834
  var import_fs = require("fs");
9835
+ var os4 = __toESM(require("os"));
9836
+ var path11 = __toESM(require("path"));
9837
+
9838
+ // src/services/workspaceEnv.ts
9839
+ var import_child_process3 = require("child_process");
9840
+ var fs5 = __toESM(require("fs"));
9580
9841
  var os3 = __toESM(require("os"));
9581
- var path9 = __toESM(require("path"));
9842
+ var path10 = __toESM(require("path"));
9843
+
9844
+ // src/services/settingsEnvAllowlist.ts
9845
+ var ORDEWELL_SETTABLE_ENV = [
9846
+ "AI_PROVIDER",
9847
+ "ORCHESTRATOR_MODEL",
9848
+ "ORDEWELL_SUBAGENT_MODEL",
9849
+ "ORDEWELL_PLANNER_EFFORT",
9850
+ "ORDEWELL_RESEARCH_ENABLED",
9851
+ "ORDEWELL_MAX_PARALLEL",
9852
+ "ORDEWELL_CONFLICT_REPAIR_ATTEMPTS"
9853
+ ];
9854
+ var SETTINGS_ENV_REFUSED = [
9855
+ // Approval posture and pre-approved scope.
9856
+ "ORDEWELL_AUTONOMOUS_MODE",
9857
+ "ORDEWELL_APPROVAL_MODE",
9858
+ "ORDEWELL_APPROVAL_ALLOW",
9859
+ // Where settings are persisted.
9860
+ "ORDEWELL_SETTINGS_PATH",
9861
+ // Runtime options and dynamic loaders.
9862
+ "NODE_OPTIONS",
9863
+ "NODE_REPL_EXTERNAL_MODULE",
9864
+ "NODE_PATH",
9865
+ "ELECTRON_RUN_AS_NODE",
9866
+ "LD_PRELOAD",
9867
+ "LD_LIBRARY_PATH",
9868
+ "LD_AUDIT",
9869
+ "DYLD_INSERT_LIBRARIES",
9870
+ "DYLD_LIBRARY_PATH",
9871
+ "DYLD_FRAMEWORK_PATH",
9872
+ // Executable resolution.
9873
+ "PATH",
9874
+ "PATHEXT",
9875
+ "NODE"
9876
+ ];
9877
+ var REFUSED = new Set(SETTINGS_ENV_REFUSED);
9878
+ function providerEnvKeys() {
9879
+ const keys = [];
9880
+ for (const meta of Object.values(ALL_PROVIDERS)) {
9881
+ if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
9882
+ if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
9883
+ for (const detect of meta.detectEnvVars) keys.push(detect);
9884
+ }
9885
+ return keys;
9886
+ }
9887
+ var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
9888
+ var SETTINGS_ENV_ALLOWLIST = new Set(
9889
+ [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
9890
+ );
9891
+ function admitSettingsEnv(env) {
9892
+ const accepted = {};
9893
+ const rejected = [];
9894
+ for (const [key, value] of Object.entries(env)) {
9895
+ if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
9896
+ rejected.push(key);
9897
+ continue;
9898
+ }
9899
+ accepted[key] = value;
9900
+ }
9901
+ return { accepted, rejected };
9902
+ }
9903
+
9904
+ // src/services/workspaceEnv.ts
9905
+ var WORKSPACE_ENV_FILE = "env";
9906
+ var DIRENV_TIMEOUT_MS = 5e3;
9907
+ var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
9908
+ function defaultDeps() {
9909
+ return {
9910
+ run: (command, args, opts) => new Promise((resolve7) => {
9911
+ (0, import_child_process3.execFile)(command, args, { ...opts, timeout: DIRENV_TIMEOUT_MS, encoding: "utf8" }, (err, stdout, stderr) => {
9912
+ const code = err ? typeof err.code === "number" ? Number(err.code) : -1 : 0;
9913
+ resolve7({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
9914
+ });
9915
+ }),
9916
+ readFile: (file) => {
9917
+ try {
9918
+ return fs5.readFileSync(file, "utf8");
9919
+ } catch {
9920
+ return null;
9921
+ }
9922
+ },
9923
+ isDirectory: (dir) => {
9924
+ try {
9925
+ return fs5.statSync(dir).isDirectory();
9926
+ } catch {
9927
+ return false;
9928
+ }
9929
+ },
9930
+ baseEnv: process.env,
9931
+ home: os3.homedir()
9932
+ };
9933
+ }
9934
+ var REFUSED2 = new Set(SETTINGS_ENV_REFUSED);
9935
+ async function resolveWorkspaceEnv(cwd, overrides = {}) {
9936
+ const deps = { ...defaultDeps(), ...overrides };
9937
+ const result = { env: {}, blockedEnvrc: null, refused: [], trackedEnvFile: null };
9938
+ if (!deps.isDirectory(cwd)) return result;
9939
+ const collected = {};
9940
+ const direnv = await fromDirenv(checkoutDirOf(cwd, deps), deps);
9941
+ result.blockedEnvrc = direnv.blocked;
9942
+ Object.assign(collected, direnv.env);
9943
+ const file = findUp(cwd, path10.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
9944
+ if (file) {
9945
+ const content = deps.readFile(file);
9946
+ if (content !== null) {
9947
+ if (await isTracked(file, deps)) result.trackedEnvFile = file;
9948
+ else Object.assign(collected, parseEnvFile(content, { ...deps.baseEnv, ...collected }, deps.home));
9949
+ }
9950
+ }
9951
+ for (const [key, value] of Object.entries(collected)) {
9952
+ if (!KEY.test(key)) continue;
9953
+ if (REFUSED2.has(key) || key.toUpperCase() === "PATH") result.refused.push(key);
9954
+ else result.env[key] = value;
9955
+ }
9956
+ return result;
9957
+ }
9958
+ async function workspaceEnvOf(cwd) {
9959
+ return (await resolveWorkspaceEnv(cwd)).env;
9960
+ }
9961
+ function checkoutDirOf(cwd, deps) {
9962
+ const parts = path10.resolve(cwd).split(path10.sep);
9963
+ const at = parts.findIndex((part, i) => part === STATE_DIR && parts[i + 1] === "worktrees");
9964
+ if (at < 0 || parts.length < at + 4) return cwd;
9965
+ const root = parts.slice(0, at).join(path10.sep) || path10.sep;
9966
+ const mapped = path10.join(root, ...parts.slice(at + 4));
9967
+ return deps.isDirectory(mapped) ? mapped : root;
9968
+ }
9969
+ async function fromDirenv(cwd, deps) {
9970
+ const setting = deps.baseEnv.ORDEWELL_DIRENV;
9971
+ if (setting === "false" || setting === "0") return { env: {}, blocked: null };
9972
+ const baseEnv = Object.fromEntries(Object.entries(deps.baseEnv).filter(([key]) => !key.startsWith("DIRENV_")));
9973
+ let out;
9974
+ try {
9975
+ out = await deps.run("direnv", ["export", "json"], { cwd, env: baseEnv });
9976
+ } catch {
9977
+ return { env: {}, blocked: null };
9978
+ }
9979
+ const blocked = out.stderr.match(/error (\S*\.envrc) is blocked/);
9980
+ if (blocked) return { env: {}, blocked: blocked[1] };
9981
+ if (out.code !== 0 || !out.stdout.trim()) return { env: {}, blocked: null };
9982
+ let parsed;
9983
+ try {
9984
+ parsed = JSON.parse(out.stdout);
9985
+ } catch {
9986
+ return { env: {}, blocked: null };
9987
+ }
9988
+ if (!parsed || typeof parsed !== "object") return { env: {}, blocked: null };
9989
+ const env = {};
9990
+ for (const [key, value] of Object.entries(parsed)) {
9991
+ if (typeof value === "string" && !key.startsWith("DIRENV_")) env[key] = value;
9992
+ }
9993
+ return { env, blocked: null };
9994
+ }
9995
+ function findUp(start, relative5, deps) {
9996
+ let dir = path10.resolve(start);
9997
+ for (; ; ) {
9998
+ const candidate = path10.join(dir, relative5);
9999
+ if (deps.readFile(candidate) !== null) return candidate;
10000
+ const parent = path10.dirname(dir);
10001
+ if (parent === dir) return null;
10002
+ dir = parent;
10003
+ }
10004
+ }
10005
+ async function isTracked(file, deps) {
10006
+ try {
10007
+ const out = await deps.run("git", ["ls-files", "--error-unmatch", path10.basename(file)], { cwd: path10.dirname(file), env: deps.baseEnv });
10008
+ return out.code === 0;
10009
+ } catch {
10010
+ return false;
10011
+ }
10012
+ }
10013
+ function parseEnvFile(content, context, home) {
10014
+ const env = {};
10015
+ const lookup2 = (name) => env[name] ?? context[name] ?? "";
10016
+ const expand = (value) => {
10017
+ const tilde = value.startsWith("~/") ? path10.join(home, value.slice(2)) : value;
10018
+ return tilde.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => lookup2(braced ?? bare));
10019
+ };
10020
+ for (const raw of content.split(/\r?\n/)) {
10021
+ const line = raw.trim().replace(/^export\s+/, "");
10022
+ if (!line || line.startsWith("#")) continue;
10023
+ const eq = line.indexOf("=");
10024
+ if (eq <= 0) continue;
10025
+ const key = line.slice(0, eq).trim();
10026
+ if (!KEY.test(key)) continue;
10027
+ const rest = line.slice(eq + 1).trim();
10028
+ if (rest.startsWith("'")) {
10029
+ const end = rest.indexOf("'", 1);
10030
+ env[key] = end > 0 ? rest.slice(1, end) : rest.slice(1);
10031
+ } else if (rest.startsWith('"')) {
10032
+ const end = rest.lastIndexOf('"');
10033
+ env[key] = expand((end > 0 ? rest.slice(1, end) : rest.slice(1)).replace(/\\n/g, "\n").replace(/\\"/g, '"'));
10034
+ } else {
10035
+ env[key] = expand(rest.replace(/\s+#.*$/, ""));
10036
+ }
10037
+ }
10038
+ return env;
10039
+ }
10040
+
10041
+ // src/services/transcriptCapture.ts
9582
10042
  var HomeTranscriptReader = class {
9583
10043
  home;
10044
+ claudeConfigDirs;
9584
10045
  constructor(opts = {}) {
9585
10046
  const { homeDir } = opts;
9586
- this.home = homeDir ? () => homeDir : () => process.env.HOME || os3.homedir();
10047
+ this.home = homeDir ? () => homeDir : () => process.env.HOME || os4.homedir();
10048
+ const workspaceEnv = opts.workspaceEnv ?? workspaceEnvOf;
10049
+ this.claudeConfigDirs = async (cwd) => [...new Set([
10050
+ (await workspaceEnv(cwd)).CLAUDE_CONFIG_DIR,
10051
+ homeDir ? void 0 : process.env.CLAUDE_CONFIG_DIR,
10052
+ path11.join(this.home(), ".claude")
10053
+ ].filter((dir) => Boolean(dir)))];
9587
10054
  }
9588
10055
  async finalAssistantText(query, maxChars = 4e3) {
9589
10056
  try {
9590
- if (query.runner === "claude-code") return claudeFinal(this.home(), query, maxChars);
10057
+ if (query.runner === "claude-code") return claudeFinal(await this.claudeConfigDirs(query.cwd), query, maxChars);
9591
10058
  if (query.runner === "opencode") return await opencodeFinal(this.home(), query, maxChars);
9592
10059
  if (query.runner === "codex") return codexFinal(this.home(), query, maxChars);
9593
10060
  } catch {
@@ -9596,20 +10063,20 @@ var HomeTranscriptReader = class {
9596
10063
  }
9597
10064
  };
9598
10065
  var CLAUDE_PROJECT_NAME_MAX = 200;
9599
- function claudeProjectDirs(home, cwd) {
9600
- const projects = path9.join(home, ".claude", "projects");
10066
+ function claudeProjectDirs(configDir2, cwd) {
10067
+ const projects = path11.join(configDir2, "projects");
9601
10068
  const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
9602
10069
  if (munged.length <= CLAUDE_PROJECT_NAME_MAX) {
9603
- const dir = path9.join(projects, munged);
10070
+ const dir = path11.join(projects, munged);
9604
10071
  return (0, import_fs.existsSync)(dir) ? [dir] : [];
9605
10072
  }
9606
10073
  if (!(0, import_fs.existsSync)(projects)) return [];
9607
10074
  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));
10075
+ return (0, import_fs.readdirSync)(projects).filter((name) => name.startsWith(prefix)).map((name) => path11.join(projects, name));
9609
10076
  }
9610
- function claudeFinal(home, query, maxChars) {
10077
+ function claudeFinal(configDirs, query, maxChars) {
9611
10078
  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);
10079
+ const candidates = configDirs.flatMap((dir) => claudeProjectDirs(dir, query.cwd)).flatMap((dir) => (0, import_fs.readdirSync)(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path11.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
10080
  for (const file of candidates) {
9614
10081
  const raw = (0, import_fs.readFileSync)(file, "utf8");
9615
10082
  if (!raw.includes(query.marker)) continue;
@@ -9645,7 +10112,7 @@ async function opencodeFinal(home, query, maxChars) {
9645
10112
  } catch {
9646
10113
  return null;
9647
10114
  }
9648
- const dbPath = path9.join(home, ".local", "share", "opencode", "opencode.db");
10115
+ const dbPath = path11.join(home, ".local", "share", "opencode", "opencode.db");
9649
10116
  if (!(0, import_fs.existsSync)(dbPath)) return null;
9650
10117
  let db;
9651
10118
  try {
@@ -9698,7 +10165,7 @@ async function opencodeFinal(home, query, maxChars) {
9698
10165
  }
9699
10166
  }
9700
10167
  function codexFinal(home, query, maxChars) {
9701
- const root = path9.join(home, ".codex", "sessions");
10168
+ const root = path11.join(home, ".codex", "sessions");
9702
10169
  if (!(0, import_fs.existsSync)(root)) return null;
9703
10170
  const cutoff = query.startedAt ? Date.parse(query.startedAt) : 0;
9704
10171
  const files = [];
@@ -9712,7 +10179,7 @@ function codexFinal(home, query, maxChars) {
9712
10179
  continue;
9713
10180
  }
9714
10181
  for (const e of entries) {
9715
- const full = path9.join(dir, e);
10182
+ const full = path11.join(dir, e);
9716
10183
  let st;
9717
10184
  try {
9718
10185
  st = (0, import_fs.statSync)(full);
@@ -10163,6 +10630,43 @@ ${taskB.description}`,
10163
10630
  }
10164
10631
  };
10165
10632
 
10633
+ // src/utils/shell.ts
10634
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
10635
+ function stripAnsi(text) {
10636
+ return text.replace(ANSI_RE, "");
10637
+ }
10638
+ function posixShellQuote(s) {
10639
+ return `'${s.replace(/'/g, `'\\''`)}'`;
10640
+ }
10641
+ function buildShellInvocation(command, args) {
10642
+ const inner = [command, ...args].map(posixShellQuote).join(" ");
10643
+ return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
10644
+ }
10645
+ function wrapWithPty(command, args, opts = {}) {
10646
+ const inner = [command, ...args].map(posixShellQuote).join(" ");
10647
+ const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
10648
+ const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
10649
+ return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
10650
+ }
10651
+
10652
+ // src/services/blockingPrompts.ts
10653
+ var TAIL = 4096;
10654
+ var squash = (text) => stripAnsi(text).replace(/\s+/g, "").toLowerCase();
10655
+ function watchBlockingPrompts(session, prompts, onPrompt) {
10656
+ if (prompts.length === 0) return;
10657
+ const pending = prompts.map((prompt) => ({ prompt, phrase: squash(prompt.phrase) }));
10658
+ let tail = "";
10659
+ session.onOutput((text) => {
10660
+ if (pending.length === 0) return;
10661
+ tail = (tail + squash(text)).slice(-TAIL);
10662
+ for (let i = pending.length - 1; i >= 0; i--) {
10663
+ if (!tail.includes(pending[i].phrase)) continue;
10664
+ onPrompt(pending[i].prompt);
10665
+ pending.splice(i, 1);
10666
+ }
10667
+ });
10668
+ }
10669
+
10166
10670
  // src/services/TaskOrchestrator.ts
10167
10671
  var SHARED_ROOT_TAIL = "tasks run in the workspace root without worktree isolation.";
10168
10672
  function sharedRootNotice(reason, repos) {
@@ -10226,6 +10730,25 @@ var TaskOrchestrator = class {
10226
10730
  */
10227
10731
  retryCounts = /* @__PURE__ */ new Map();
10228
10732
  spawnCounts = /* @__PURE__ */ new Map();
10733
+ /**
10734
+ * The terminal a verdict left open, by task. It stays so the user can read
10735
+ * the agent's output or keep talking to it — but only while its worktree
10736
+ * does: once that is removed the agent sits in a deleted directory, and a
10737
+ * newer attempt in the same worktree would share it with a second agent.
10738
+ * Without this, every task of every run left one agent process running
10739
+ * until the daemon stopped.
10740
+ */
10741
+ lingering = /* @__PURE__ */ new Map();
10742
+ /**
10743
+ * A full-plan run a failure paused. Retrying the failed task is the explicit
10744
+ * resume the pause waits for — without this a retry only reset the task to
10745
+ * pending and nothing ran until the user also re-ran the whole plan.
10746
+ */
10747
+ haltedByFailure = false;
10748
+ /** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
10749
+ workspaceEnv = (cwd) => resolveWorkspaceEnv(cwd);
10750
+ /** What the workspace env has already warned about, so a run says it once, not per task. */
10751
+ envWarnings = /* @__PURE__ */ new Set();
10229
10752
  /**
10230
10753
  * Tasks pulled out of auto-scheduling (user-cancelled or failed to spawn).
10231
10754
  * They stay 'pending' — "not executed" — but the scheduler skips them until
@@ -10269,6 +10792,32 @@ var TaskOrchestrator = class {
10269
10792
  setWorkspaceRoot(fn) {
10270
10793
  this.workspaceRootFn = fn;
10271
10794
  }
10795
+ setWorkspaceEnvResolver(resolve7) {
10796
+ this.workspaceEnv = resolve7;
10797
+ }
10798
+ /**
10799
+ * The variables a task's agent gets from its workspace. What cannot be
10800
+ * applied is said once — silently starting without them is how agents ran
10801
+ * under the wrong account when an edited `.envrc` was left unallowed.
10802
+ */
10803
+ async envForTask(cwd) {
10804
+ const resolved = await this.workspaceEnv(cwd);
10805
+ const warn = (key, message) => {
10806
+ if (this.envWarnings.has(key)) return;
10807
+ this.envWarnings.add(key);
10808
+ this.notifications.warn(message);
10809
+ };
10810
+ if (resolved.blockedEnvrc) {
10811
+ warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path12.dirname(resolved.blockedEnvrc)} to use them.`);
10812
+ }
10813
+ if (resolved.trackedEnvFile) {
10814
+ warn(`tracked:${resolved.trackedEnvFile}`, `Ignored ${resolved.trackedEnvFile}: git tracks it, and a committed file must not choose the environment agents run in. Untrack it to use it.`);
10815
+ }
10816
+ if (resolved.refused.length > 0) {
10817
+ warn(`refused:${resolved.refused.join(",")}`, `Ignored ${resolved.refused.join(", ")} from the workspace environment: Ordewell never passes these to agents.`);
10818
+ }
10819
+ return resolved.env;
10820
+ }
10272
10821
  setRegistry(registry) {
10273
10822
  this.registry = registry;
10274
10823
  }
@@ -10350,7 +10899,7 @@ var TaskOrchestrator = class {
10350
10899
  if (!this.isolationRun) return null;
10351
10900
  const record = this.isolationRun.tasks[taskId];
10352
10901
  if (!record) return { state: "none" };
10353
- return taskIsolationOf(record);
10902
+ return taskIsolationOf(record, this.config.conflictRepairAttempts);
10354
10903
  }
10355
10904
  /**
10356
10905
  * The plan's isolation as a surface shows it, for one with no stream to have
@@ -10359,7 +10908,7 @@ var TaskOrchestrator = class {
10359
10908
  isolationView() {
10360
10909
  const run = this.isolationRun;
10361
10910
  if (!run) return null;
10362
- const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r)]));
10911
+ const tasks = Object.fromEntries(Object.values(run.tasks).map((r) => [r.taskId, taskIsolationOf(r, this.config.conflictRepairAttempts)]));
10363
10912
  return { tasks, handoff: handoffOf(run) };
10364
10913
  }
10365
10914
  getAttemptSession(taskId) {
@@ -10395,24 +10944,62 @@ var TaskOrchestrator = class {
10395
10944
  async reviewRunDiff() {
10396
10945
  return this.isolation.reviewDiff(this.requireRun());
10397
10946
  }
10398
- /** "Merge all": the run's integration branches into whatever the user has checked out, in every repo or none. */
10947
+ /**
10948
+ * "Merge all": the run's integration branches into whatever the user has
10949
+ * checked out, in every repo or none. Once everything merged, the run has
10950
+ * nothing left to hand over, so it is cleared up and forgotten; a branch
10951
+ * the user's HEAD somehow does not contain stays for the next run's sweep.
10952
+ */
10399
10953
  async mergeRun() {
10400
10954
  const run = this.requireRun();
10401
10955
  const result = await this.isolation.mergeIntoCheckedOut(run);
10402
10956
  const branch = integrationBranchNameOf(run);
10403
10957
  const group = run.repos.some((r) => r.path !== SELF_REPO);
10404
- const { level, message } = describeMergeResult(result, branch, group);
10958
+ const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
10959
+ const { level, message } = describeMergeResult(result, branch, group, repaired);
10405
10960
  this.notifications[level](message);
10961
+ if (result.outcome === "merged") await this.clearMergedRun(run);
10406
10962
  return result;
10407
10963
  }
10964
+ async clearMergedRun(run) {
10965
+ this.closeLingeringOf(run);
10966
+ try {
10967
+ await this.isolation.discard(run, { integration: "delete-merged" });
10968
+ } catch (err) {
10969
+ this.tell("warn", `Merged, but could not clean up the run's worktrees and branches: ${err instanceof Error ? err.message : String(err)}`);
10970
+ return;
10971
+ }
10972
+ this.forgetRun();
10973
+ }
10408
10974
  /** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
10409
10975
  async cleanupRun() {
10410
- await this.isolation.discard(this.requireRun(), { keepIntegration: true });
10976
+ this.closeLingeringOf(this.requireRun());
10977
+ await this.isolation.discard(this.requireRun(), { integration: "keep" });
10411
10978
  this.emit("onIsolationChanged");
10412
10979
  }
10413
10980
  /** The run and everything it made go, and the plan forgets it; the next run starts afresh. */
10414
10981
  async discardRun() {
10415
- await this.isolation.discard(this.requireRun(), { keepIntegration: false });
10982
+ this.closeLingeringOf(this.requireRun());
10983
+ await this.isolation.discard(this.requireRun(), { integration: "delete" });
10984
+ this.forgetRun();
10985
+ }
10986
+ watchBlockingPrompts(task, attempt, session) {
10987
+ const manifest = this.registry?.get(attempt.runner)?.manifest;
10988
+ watchBlockingPrompts(session, manifest?.runner.blockingPrompts ?? [], (prompt) => {
10989
+ if (this.attempts.get(task.id) !== attempt) return;
10990
+ this.notifications.warn(`Task "${task.title}" is waiting for you: ${manifest?.displayName ?? attempt.runner} is asking ${prompt.asks}. Answer it in the task's terminal.`);
10991
+ });
10992
+ }
10993
+ closeLingering(taskId) {
10994
+ const sessionId = this.lingering.get(taskId);
10995
+ if (sessionId === void 0) return;
10996
+ this.lingering.delete(taskId);
10997
+ this.terminalRunner.stop(sessionId);
10998
+ }
10999
+ closeLingeringOf(run) {
11000
+ for (const taskId of Object.keys(run.tasks)) this.closeLingering(taskId);
11001
+ }
11002
+ forgetRun() {
10416
11003
  this.isolationRun = null;
10417
11004
  this.resolvers = {};
10418
11005
  this.emit("onIsolationChanged");
@@ -10448,7 +11035,8 @@ var TaskOrchestrator = class {
10448
11035
  }
10449
11036
  loadPlan(tasks, planRunners = ["claude-code"]) {
10450
11037
  this.store.load(tasks, planRunners);
10451
- this.endAllAttempts("load");
11038
+ const repairs = this.endAllAttempts("load").filter((a) => a.repair && a.worktree);
11039
+ for (const a of repairs) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
10452
11040
  if (!this.running) this.runMode = null;
10453
11041
  this.blockedStart = null;
10454
11042
  this.planStatus = "approved";
@@ -10498,17 +11086,25 @@ var TaskOrchestrator = class {
10498
11086
  if (!await this.openRun(() => this.start()) || this.running) return;
10499
11087
  console.log(`[TaskOrchestrator] Starting with ${this.store.allTasks.length} tasks (${this.store.allTasks.filter((t) => t.type === "ai" && t.prompt).length} AI ready)`);
10500
11088
  this.running = true;
11089
+ this.haltedByFailure = false;
10501
11090
  this.planStatus = "running";
10502
11091
  this.emit("onTaskChanged");
10503
11092
  await this.tick();
10504
11093
  }
11094
+ haltOnFailure() {
11095
+ if (this.running) this.haltedByFailure = true;
11096
+ this.running = false;
11097
+ this.planStatus = "approved";
11098
+ }
10505
11099
  stop() {
11100
+ this.haltedByFailure = false;
10506
11101
  this.running = false;
10507
11102
  this.planStatus = "approved";
10508
11103
  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);
11104
+ for (const a of this.endAllAttempts("stop")) {
11105
+ if (a.repair) this.store.markAwaitingUser(a.taskId);
11106
+ if (a.worktree) void this.releaseWorktree(a.taskId, { keep: true }, a.integration);
11107
+ }
10512
11108
  this.runMode = null;
10513
11109
  this.blockedStart = null;
10514
11110
  this.onHold.clear();
@@ -10524,6 +11120,7 @@ var TaskOrchestrator = class {
10524
11120
  console.error(`[TaskOrchestrator] Task #${task.order} "${task.title}" verdict=${verdict.outcome}`);
10525
11121
  console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
10526
11122
  console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
11123
+ if (attempt.repair) return this.settleRepair(task, attempt, verdict);
10527
11124
  const doneToken = `<<<ORDEWELL_DONE_${task.completionMarker}>>>`;
10528
11125
  const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
10529
11126
  if (this.attempts.get(taskId) !== attempt) return;
@@ -10537,13 +11134,15 @@ ${summary || "(empty \u2014 no output captured)"}`);
10537
11134
  await this.landPassed(task, landing);
10538
11135
  } else {
10539
11136
  this.store.markFailed(taskId);
10540
- this.running = false;
10541
- this.planStatus = "approved";
11137
+ this.haltOnFailure();
10542
11138
  this.notifications.error(`Task "${task.title}" failed verification: ${verdict.reason}`);
10543
11139
  if (attempt.worktree) await this.releaseWorktree(taskId, { keep: true });
10544
11140
  }
10545
11141
  this.store.setTaskOutputSummary(taskId, summarizeOutput(verdict.reason, summary));
10546
11142
  this.logAndArchive(task, verdict);
11143
+ await this.afterVerdict();
11144
+ }
11145
+ async afterVerdict() {
10547
11146
  this.emit("onTaskChanged");
10548
11147
  if (!this.running) {
10549
11148
  if (this.attempts.size === 0) {
@@ -10556,6 +11155,71 @@ ${summary || "(empty \u2014 no output captured)"}`);
10556
11155
  }
10557
11156
  await this.tick();
10558
11157
  }
11158
+ /**
11159
+ * A repair's verdict decides only whether its work may try to land. It never
11160
+ * replaces the verdict the task's own work earned, and a repair that does not
11161
+ * land leaves the task waiting on the user — never a failed task, so never a
11162
+ * halted run.
11163
+ */
11164
+ async settleRepair(task, attempt, verdict) {
11165
+ const landed = verdict.outcome === "pass" ? await this.landRepair(task, attempt) : null;
11166
+ if (this.attempts.get(task.id) !== attempt) return;
11167
+ this.endAttempt(task.id, "verdict");
11168
+ if (!landed) await this.unrepaired(task, `did not finish (${verdict.reason})`);
11169
+ else if (!landed.evidence.ok) await this.unrepaired(task, this.describeEvidence(landed.evidence));
11170
+ else if (landed.outcome === "merged") await this.landedRepair(task, verdict);
11171
+ else this.landUnmerged(task, landed.outcome);
11172
+ await this.afterVerdict();
11173
+ }
11174
+ /**
11175
+ * Evidence before the queue: the repair's work is committed and checked, and
11176
+ * only then merged, through the same serialized landing as any other task —
11177
+ * so a repair the tip has moved past again is a fresh conflict, not a pass.
11178
+ */
11179
+ async landRepair(task, attempt) {
11180
+ const run = this.isolationRun;
11181
+ let evidence = { ok: false, reason: "failed", repo: SELF_REPO };
11182
+ if (!run) return { evidence, outcome: "failed" };
11183
+ attempt.phase = "integrating";
11184
+ attempt.integration = (async () => {
11185
+ evidence = await this.isolation.verifyRepair(task, run).catch(() => ({ ok: false, reason: "failed", repo: run.tasks[task.id]?.conflictRepo ?? SELF_REPO }));
11186
+ if (evidence.ok) return this.integrateWork(task);
11187
+ await this.releaseWorktree(task.id, { keep: true });
11188
+ return "conflict";
11189
+ })();
11190
+ const outcome = await attempt.integration;
11191
+ return { evidence, outcome };
11192
+ }
11193
+ async landedRepair(task, verdict) {
11194
+ const files = this.isolationRun?.tasks[task.id]?.repairedFiles ?? [];
11195
+ this.store.markCompleted(task.id);
11196
+ this.store.unblockDependents(task.id);
11197
+ this.logAndArchive(task, task.verdict ?? verdict);
11198
+ this.tell("info", `Task "${task.title}" landed after repairing a conflict${files.length > 0 ? ` in ${capConflictFiles(files)}` : ""}.`);
11199
+ await this.landResolved(task.id);
11200
+ }
11201
+ /** A repair that did not land leaves the task as its conflict did: waiting on the user, worktree and refs kept. */
11202
+ async unrepaired(task, why) {
11203
+ await this.releaseWorktree(task.id, { keep: true });
11204
+ this.store.markAwaitingUser(task.id);
11205
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
11206
+ 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.`);
11207
+ }
11208
+ describeEvidence(evidence) {
11209
+ const run = this.isolationRun;
11210
+ const inRepo = evidence.repo !== SELF_REPO ? ` in ${evidence.repo}` : "";
11211
+ const branch = run ? integrationBranchNameOf(run) : "the integration branch";
11212
+ switch (evidence.reason) {
11213
+ case "not-merged":
11214
+ return `finished, but its branch${inRepo} does not contain ${branch}`;
11215
+ case "conflict-markers": {
11216
+ const files = (evidence.files ?? []).map((file) => evidence.repo !== SELF_REPO ? `${evidence.repo}/${file}` : file);
11217
+ return `finished, but left conflict markers in ${capConflictFiles(files)}`;
11218
+ }
11219
+ case "failed":
11220
+ return `finished, but git could not check its work${inRepo}`;
11221
+ }
11222
+ }
10559
11223
  /**
10560
11224
  * Merge a passed attempt's worktree. The attempt stays live while it waits on
10561
11225
  * the module's merge queue, so a cancel, retry or stop in that window still
@@ -10589,18 +11253,44 @@ ${summary || "(empty \u2014 no output captured)"}`);
10589
11253
  }
10590
11254
  landUnmerged(task, landing) {
10591
11255
  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;
11256
+ const record = this.isolationRun?.tasks[task.id];
11257
+ const inRepo = record?.conflictRepo && record.conflictRepo !== SELF_REPO ? record.conflictRepo : null;
10594
11258
  if (landing === "conflict") {
11259
+ const repair = this.nextRepair(task.id);
10595
11260
  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.`);
11261
+ const files = record?.conflictFiles?.length ? ` (${capConflictFiles(record.conflictFiles)})` : "";
11262
+ 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}.`;
11263
+ if (repair) {
11264
+ this.notifications.warn(conflicted);
11265
+ if (this.attempts.size < this.config.maxParallelSessions) void this.startTask(task);
11266
+ else {
11267
+ this.store.markPending(task.id);
11268
+ this.tell("info", `Task "${task.title}" is repaired once a slot is free.`);
11269
+ }
11270
+ return;
11271
+ }
11272
+ 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.`);
11273
+ const whyNot = this.noRepairReason(task);
11274
+ if (whyNot) this.tell("info", whyNot);
10597
11275
  } else {
10598
11276
  this.store.markFailed(task.id);
10599
- this.running = false;
10600
- this.planStatus = "approved";
11277
+ this.haltOnFailure();
10601
11278
  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
11279
  }
10603
11280
  }
11281
+ /** 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. */
11282
+ nextRepair(taskId) {
11283
+ const record = this.runMode === "isolated" ? this.isolationRun?.tasks[taskId] : void 0;
11284
+ const limit = this.config.conflictRepairAttempts;
11285
+ const spent = record?.repairs ?? 0;
11286
+ return record?.status === "conflict" && spent < limit ? { n: spent + 1, limit } : null;
11287
+ }
11288
+ noRepairReason(task) {
11289
+ const limit = this.config.conflictRepairAttempts;
11290
+ if (limit === 0) return `Conflict repair is off (conflictRepairAttempts is 0), so task "${task.title}" waits for you.`;
11291
+ const spent = this.isolationRun?.tasks[task.id]?.repairs ?? 0;
11292
+ return spent >= limit ? `Task "${task.title}" has had ${spent} of its ${limit} conflict repairs, so its conflict waits for you.` : null;
11293
+ }
10604
11294
  /**
10605
11295
  * Mark which added task resolves which conflict. The resolver merges the
10606
11296
  * conflicted task's branch by hand in its own worktree; once that lands, the
@@ -10635,6 +11325,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
10635
11325
  await integration;
10636
11326
  const run = this.isolationRun;
10637
11327
  if (!run?.tasks[taskId]) return;
11328
+ if (!opts.keep) this.closeLingering(taskId);
10638
11329
  try {
10639
11330
  await this.isolation.release(run, taskId, opts);
10640
11331
  } catch (err) {
@@ -10783,7 +11474,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
10783
11474
  this.onHold.delete(taskId);
10784
11475
  this.emit("onTaskChanged");
10785
11476
  await this.releaseWorktree(taskId, { keep: false }, ended?.integration);
10786
- await this.tick();
11477
+ if (this.haltedByFailure && !this.running) await this.start();
11478
+ else await this.tick();
10787
11479
  }
10788
11480
  /**
10789
11481
  * Manually start a single AI task right now, bypassing dependency/readiness
@@ -10890,6 +11582,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
10890
11582
  cwd: null,
10891
11583
  worktree: false,
10892
11584
  integration: null,
11585
+ repair: this.nextRepair(task.id),
10893
11586
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
10894
11587
  };
10895
11588
  this.spawnCounts.set(task.id, attempt.attempt);
@@ -10900,13 +11593,16 @@ ${summary || "(empty \u2014 no output captured)"}`);
10900
11593
  const cwd = await this.resolveAttemptCwd(task, attempt);
10901
11594
  attempt.cwd = cwd;
10902
11595
  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);
11596
+ if (attempt.worktree && !this.attempts.has(task.id)) await this.releaseWorktree(task.id, { keep: attempt.repair !== null });
11597
+ return this.abandonSpawn(task, attempt);
10905
11598
  }
10906
- const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
11599
+ const finalPrompt = composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.repairPrompt(task) } : task, this.store.planTasks, {
10907
11600
  planMapEnabled: this.config.planMapEnabled,
10908
- tddEnabled: this.tddEnabled()
11601
+ // A merge to resolve is not new behaviour to drive test-first.
11602
+ tddEnabled: !attempt.repair && this.tddEnabled()
10909
11603
  });
11604
+ this.closeLingering(task.id);
11605
+ const env = await this.envForTask(cwd);
10910
11606
  const session = await this.terminalRunner.spawn({
10911
11607
  taskId: task.id,
10912
11608
  runner: attempt.runner,
@@ -10918,18 +11614,30 @@ ${summary || "(empty \u2014 no output captured)"}`);
10918
11614
  cwd,
10919
11615
  registry: this.registry ?? void 0,
10920
11616
  order: task.order,
10921
- title: task.title
11617
+ title: task.title,
11618
+ env
10922
11619
  });
10923
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, session);
11620
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt, session);
10924
11621
  attempt.phase = "running";
10925
11622
  attempt.session = session;
10926
11623
  this.output.attach(task.id, session);
10927
11624
  this.verifier.watch(task, session);
11625
+ this.watchBlockingPrompts(task, attempt, session);
10928
11626
  this.emit("onTaskChanged");
10929
- this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
11627
+ if (attempt.repair) {
11628
+ const group = this.isolationRun?.repos.some((r) => r.path !== SELF_REPO);
11629
+ this.tell("info", `Repairing the conflict of task "${task.title}" in its own ${group ? "worktrees" : "worktree"} (repair ${attempt.repair.n} of ${attempt.repair.limit}).`);
11630
+ } else {
11631
+ this.notifications.info(`Task "${task.title}" started (${attempt.runner})`);
11632
+ }
10930
11633
  } catch (err) {
10931
- if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task);
11634
+ if (this.attempts.get(task.id) !== attempt) return this.abandonSpawn(task, attempt);
10932
11635
  this.endAttempt(task.id, "spawn-failed");
11636
+ if (attempt.repair) {
11637
+ await this.unrepaired(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
11638
+ this.emit("onTaskChanged");
11639
+ return this.tick();
11640
+ }
10933
11641
  await this.releaseWorktree(task.id, { keep: false });
10934
11642
  this.store.markPending(task.id);
10935
11643
  this.onHold.add(task.id);
@@ -10943,17 +11651,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
10943
11651
  * and take back the claim it made. Only the claim — whatever ended the
10944
11652
  * attempt may have decided the task since (mark complete, cancel, retry).
10945
11653
  */
10946
- abandonSpawn(task, session) {
11654
+ abandonSpawn(task, attempt, session) {
10947
11655
  session?.kill();
10948
11656
  if (this.attempts.has(task.id) || this.store.get(task.id)?.status !== "in_progress") return;
10949
- this.store.markPending(task.id);
11657
+ if (attempt.repair) this.store.markAwaitingUser(task.id);
11658
+ else this.store.markPending(task.id);
10950
11659
  this.emit("onTaskChanged");
10951
11660
  }
11661
+ repairPrompt(task) {
11662
+ const run = this.requireRun();
11663
+ const record = run.tasks[task.id];
11664
+ const conflict = {
11665
+ branch: record?.branch ?? "",
11666
+ repos: Object.keys(record?.repairBase ?? {}),
11667
+ ...record?.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
11668
+ ...record?.conflictFiles ? { conflictFiles: record.conflictFiles } : {}
11669
+ };
11670
+ return buildConflictRepairPrompt(task, conflict, integrationBranchNameOf(run));
11671
+ }
10952
11672
  /**
10953
11673
  * The one place an attempt's working directory is decided. Async so a
10954
11674
  * per-attempt workspace (worktree isolation, #12) can be prepared here.
10955
11675
  */
10956
11676
  async resolveAttemptCwd(task, attempt) {
11677
+ if (attempt.repair && this.isolationRun) {
11678
+ const { cwd: cwd2 } = await this.isolation.reopen(task, this.isolationRun);
11679
+ attempt.worktree = true;
11680
+ this.emit("onIsolationChanged");
11681
+ return cwd2;
11682
+ }
10957
11683
  if (this.runMode !== "isolated" || !this.isolationRun) {
10958
11684
  await this.releaseWorktree(task.id, { keep: false });
10959
11685
  return this.workspaceRootFn();
@@ -11041,8 +11767,18 @@ ${summary || "(empty \u2014 no output captured)"}`);
11041
11767
  }
11042
11768
  }
11043
11769
  this.runMode = "isolated";
11770
+ await this.sweep();
11044
11771
  return true;
11045
11772
  }
11773
+ /** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
11774
+ async sweep() {
11775
+ if (!this.isolationRun) return;
11776
+ try {
11777
+ await this.isolation.sweep(this.isolationRun);
11778
+ } catch (err) {
11779
+ this.tell("warn", `Could not clear merged branches of earlier runs: ${err instanceof Error ? err.message : String(err)}`);
11780
+ }
11781
+ }
11046
11782
  /**
11047
11783
  * The plan's run carries on while anything has landed on its integration
11048
11784
  * branch: a resumed plan's dependents must start from a tip that holds their
@@ -11055,14 +11791,14 @@ ${summary || "(empty \u2014 no output captured)"}`);
11055
11791
  /**
11056
11792
  * A run with nothing landed holds only superseded attempts, so it goes whole.
11057
11793
  * 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.
11794
+ * workspace path — keeps its integration branch in each repo that has not
11795
+ * merged it: only the user gives landed work up.
11060
11796
  */
11061
11797
  async mintRun(root) {
11062
11798
  const previous = this.isolationRun;
11063
11799
  if (previous) {
11064
11800
  const landed = Object.values(previous.tasks).some((r) => r.status === "merged");
11065
- await this.isolation.discard(previous, { keepIntegration: landed }).catch(() => void 0);
11801
+ await this.isolation.discard(previous, { integration: landed ? "delete-merged" : "delete" }).catch(() => void 0);
11066
11802
  this.isolationRun = null;
11067
11803
  }
11068
11804
  this.isolationRun = await this.isolation.startRun(root);
@@ -11119,6 +11855,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
11119
11855
  const attempt = this.attempts.get(taskId);
11120
11856
  this.attempts.delete(taskId);
11121
11857
  if (attempt) this.output.detach(taskId);
11858
+ if (reason === "verdict" && attempt?.session) this.lingering.set(taskId, attempt.session.id);
11122
11859
  if (reason === "cancel" || reason === "release" || reason === "complete" || reason === "retry") {
11123
11860
  const task = this.store.get(taskId);
11124
11861
  if (task) this.verifier.clear(task);
@@ -11127,9 +11864,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
11127
11864
  return attempt;
11128
11865
  }
11129
11866
  endAllAttempts(reason) {
11130
- for (const taskId of [...this.attempts.keys()]) this.endAttempt(taskId, reason);
11867
+ const ended = [...this.attempts.values()];
11868
+ for (const { taskId } of ended) this.endAttempt(taskId, reason);
11131
11869
  this.verifier.reset();
11132
11870
  if (reason === "load") this.output.reset();
11871
+ return ended;
11133
11872
  }
11134
11873
  };
11135
11874
 
@@ -11259,29 +11998,29 @@ ${errors.map((e) => `- ${e}`).join("\n")}`
11259
11998
  };
11260
11999
 
11261
12000
  // src/services/ModelDiscovery.ts
11262
- var path12 = __toESM(require("path"));
11263
- var import_child_process4 = require("child_process");
12001
+ var path15 = __toESM(require("path"));
12002
+ var import_child_process5 = require("child_process");
11264
12003
  var import_util3 = require("util");
11265
12004
  var fsSync3 = __toESM(require("fs"));
11266
12005
  var osMod = __toESM(require("os"));
11267
12006
 
11268
12007
  // src/utils/globalDataDir.ts
11269
- var fs5 = __toESM(require("fs"));
11270
- var path10 = __toESM(require("path"));
11271
- var os4 = __toESM(require("os"));
12008
+ var fs6 = __toESM(require("fs"));
12009
+ var path13 = __toESM(require("path"));
12010
+ var os5 = __toESM(require("os"));
11272
12011
  function globalDataDir() {
11273
- return path10.join(os4.homedir(), ".ordewell");
12012
+ return path13.join(os5.homedir(), ".ordewell");
11274
12013
  }
11275
12014
  function copyDirSync(src, dest) {
11276
- fs5.mkdirSync(dest, { recursive: true });
11277
- const entries = fs5.readdirSync(src, { withFileTypes: true });
12015
+ fs6.mkdirSync(dest, { recursive: true });
12016
+ const entries = fs6.readdirSync(src, { withFileTypes: true });
11278
12017
  for (const entry of entries) {
11279
- const srcPath = path10.join(src, entry.name);
11280
- const destPath = path10.join(dest, entry.name);
12018
+ const srcPath = path13.join(src, entry.name);
12019
+ const destPath = path13.join(dest, entry.name);
11281
12020
  if (entry.isDirectory()) {
11282
12021
  copyDirSync(srcPath, destPath);
11283
12022
  } else {
11284
- fs5.copyFileSync(srcPath, destPath);
12023
+ fs6.copyFileSync(srcPath, destPath);
11285
12024
  }
11286
12025
  }
11287
12026
  }
@@ -11289,50 +12028,29 @@ var migrated = false;
11289
12028
  function migrateOldConfigDir() {
11290
12029
  if (migrated) return;
11291
12030
  migrated = true;
11292
- const oldDir = path10.join(os4.homedir(), ".config", "ordewell");
11293
- if (!fs5.existsSync(oldDir)) return;
12031
+ const oldDir = path13.join(os5.homedir(), ".config", "ordewell");
12032
+ if (!fs6.existsSync(oldDir)) return;
11294
12033
  const newDir = globalDataDir();
11295
12034
  const liftFile = (name) => {
11296
- const oldFile = path10.join(oldDir, name);
11297
- const newFile = path10.join(newDir, name);
11298
- if (fs5.existsSync(oldFile) && !fs5.existsSync(newFile)) {
11299
- fs5.mkdirSync(newDir, { recursive: true });
11300
- fs5.copyFileSync(oldFile, newFile);
12035
+ const oldFile = path13.join(oldDir, name);
12036
+ const newFile = path13.join(newDir, name);
12037
+ if (fs6.existsSync(oldFile) && !fs6.existsSync(newFile)) {
12038
+ fs6.mkdirSync(newDir, { recursive: true });
12039
+ fs6.copyFileSync(oldFile, newFile);
11301
12040
  }
11302
12041
  };
11303
12042
  liftFile("settings.json");
11304
12043
  liftFile(".env");
11305
- const oldPlugins = path10.join(oldDir, "plugins");
11306
- const newPlugins = path10.join(newDir, "plugins");
11307
- if (fs5.existsSync(oldPlugins) && !fs5.existsSync(newPlugins)) {
12044
+ const oldPlugins = path13.join(oldDir, "plugins");
12045
+ const newPlugins = path13.join(newDir, "plugins");
12046
+ if (fs6.existsSync(oldPlugins) && !fs6.existsSync(newPlugins)) {
11308
12047
  copyDirSync(oldPlugins, newPlugins);
11309
12048
  }
11310
12049
  }
11311
12050
 
11312
12051
  // src/utils/launch.ts
11313
12052
  var fsSync2 = __toESM(require("fs"));
11314
- var path11 = __toESM(require("path"));
11315
-
11316
- // src/utils/shell.ts
11317
- var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\r/g;
11318
- function stripAnsi(text) {
11319
- return text.replace(ANSI_RE, "");
11320
- }
11321
- function posixShellQuote(s) {
11322
- return `'${s.replace(/'/g, `'\\''`)}'`;
11323
- }
11324
- function buildShellInvocation(command, args) {
11325
- const inner = [command, ...args].map(posixShellQuote).join(" ");
11326
- return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
11327
- }
11328
- function wrapWithPty(command, args, opts = {}) {
11329
- const inner = [command, ...args].map(posixShellQuote).join(" ");
11330
- const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
11331
- const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
11332
- return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
11333
- }
11334
-
11335
- // src/utils/launch.ts
12053
+ var path14 = __toESM(require("path"));
11336
12054
  var DIRECT_EXTENSIONS = [".exe", ".com"];
11337
12055
  var BATCH_EXTENSIONS = [".cmd", ".bat"];
11338
12056
  var POWERSHELL_EXTENSIONS = [".ps1"];
@@ -11340,7 +12058,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
11340
12058
  var WINDOWS_MAX_COMMAND_LINE = 32767;
11341
12059
  function defaultPowerShell() {
11342
12060
  const root = process.env.SystemRoot || process.env.windir;
11343
- return root ? path11.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
12061
+ return root ? path14.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
11344
12062
  }
11345
12063
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
11346
12064
  var CommandLineTooLongError = class extends Error {
@@ -11392,7 +12110,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
11392
12110
  };
11393
12111
  function isResolvableOnPosixPath(command, PATH, exists) {
11394
12112
  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)));
12113
+ return PATH.split(path14.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path14.join(dir, command)));
11396
12114
  }
11397
12115
  function isExecutableResolved(command, plan, PATH, deps = {}) {
11398
12116
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -11432,9 +12150,9 @@ function cmdArgs(line) {
11432
12150
  return ["/d", "/s", "/c", `"${line}"`];
11433
12151
  }
11434
12152
  function candidateExtensions(command, pathExt) {
11435
- const declared = pathExt.split(path11.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
12153
+ const declared = pathExt.split(path14.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
11436
12154
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
11437
- const ext = path11.win32.extname(command).toLowerCase();
12155
+ const ext = path14.win32.extname(command).toLowerCase();
11438
12156
  if (ext && known.has(ext)) {
11439
12157
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
11440
12158
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -11455,13 +12173,13 @@ async function findWindowsExecutables(command, deps) {
11455
12173
  const exists = deps.exists ?? defaultExists2;
11456
12174
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
11457
12175
  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);
12176
+ const dirs = path14.win32.basename(command) !== command ? [path14.win32.dirname(path14.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path14.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
12177
+ const base = path14.win32.basename(command);
11460
12178
  const found = [];
11461
12179
  for (const route of ROUTES) {
11462
12180
  outer: for (const ext of byRoute[route]) {
11463
12181
  for (const dir of dirs) {
11464
- const candidate = path11.win32.join(dir, `${base}${ext}`);
12182
+ const candidate = path14.win32.join(dir, `${base}${ext}`);
11465
12183
  if (exists(candidate)) {
11466
12184
  found.push({ file: candidate, route });
11467
12185
  break outer;
@@ -11507,10 +12225,10 @@ async function planShellLaunch(command, args, deps = {}) {
11507
12225
  }
11508
12226
 
11509
12227
  // src/utils/processTree.ts
11510
- var import_child_process3 = require("child_process");
12228
+ var import_child_process4 = require("child_process");
11511
12229
  var HARD_KILL_DELAY_MS = 5e3;
11512
12230
  function defaultExecFile2(file, args, cb) {
11513
- (0, import_child_process3.execFile)(file, args, (err) => cb(err ?? null));
12231
+ (0, import_child_process4.execFile)(file, args, (err) => cb(err ?? null));
11514
12232
  }
11515
12233
  function killTree(proc, deps = {}) {
11516
12234
  if (!proc || proc.killed || typeof proc.exitCode === "number") return;
@@ -11546,7 +12264,7 @@ function killTree(proc, deps = {}) {
11546
12264
  }
11547
12265
 
11548
12266
  // src/services/ModelDiscovery.ts
11549
- var execAsync3 = (0, import_util3.promisify)(import_child_process4.exec);
12267
+ var execAsync3 = (0, import_util3.promisify)(import_child_process5.exec);
11550
12268
  var defaultExec2 = async (command, options) => {
11551
12269
  const PATH = await augmentedPath();
11552
12270
  const { stdout } = await execAsync3(command, { ...options, maxBuffer: 32 * 1024 * 1024, env: withPath(process.env, PATH) });
@@ -11556,16 +12274,16 @@ var defaultAppServerClient = async (command, args, options) => {
11556
12274
  const PATH = await augmentedPath();
11557
12275
  const launch = await planDirectLaunch(command, args).catch(() => null);
11558
12276
  if (!launch) return null;
11559
- return new Promise((resolve5) => {
12277
+ return new Promise((resolve7) => {
11560
12278
  let child;
11561
12279
  try {
11562
- child = (0, import_child_process4.spawn)(launch.file, launch.args, {
12280
+ child = (0, import_child_process5.spawn)(launch.file, launch.args, {
11563
12281
  env: withPath(process.env, PATH),
11564
12282
  stdio: ["pipe", "pipe", "ignore"],
11565
12283
  windowsVerbatimArguments: launch.verbatim
11566
12284
  });
11567
12285
  } catch {
11568
- resolve5(null);
12286
+ resolve7(null);
11569
12287
  return;
11570
12288
  }
11571
12289
  let settled = false;
@@ -11574,7 +12292,7 @@ var defaultAppServerClient = async (command, args, options) => {
11574
12292
  settled = true;
11575
12293
  clearTimeout(timer);
11576
12294
  killTree(child);
11577
- resolve5(value);
12295
+ resolve7(value);
11578
12296
  };
11579
12297
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
11580
12298
  child.on("error", () => finish(null));
@@ -11641,7 +12359,7 @@ function geminiDiscoveryCacheKey(apiKey) {
11641
12359
  return {
11642
12360
  key: `gemini-models:${hash}`,
11643
12361
  dir,
11644
- file: path12.join(dir, `gemini_models_${hash}.json`)
12362
+ file: path15.join(dir, `gemini_models_${hash}.json`)
11645
12363
  };
11646
12364
  }
11647
12365
  function readGeminiCache(apiKey) {
@@ -12164,8 +12882,8 @@ var ModelDiscovery = class {
12164
12882
  };
12165
12883
 
12166
12884
  // src/services/ModelCatalog.ts
12167
- var fs6 = __toESM(require("fs"));
12168
- var path13 = __toESM(require("path"));
12885
+ var fs7 = __toESM(require("fs"));
12886
+ var path16 = __toESM(require("path"));
12169
12887
  var CACHE_KEY = "model-catalog";
12170
12888
  var CACHE_TTL_MS = 60 * 60 * 1e3;
12171
12889
  function storageKey(baseUrl) {
@@ -12217,17 +12935,17 @@ var ModelCatalog = class {
12217
12935
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
12218
12936
  const key = storageKey(url);
12219
12937
  const cacheDir = globalCacheDir();
12220
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12221
- if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
12938
+ const cacheFile = path16.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12939
+ if (fs7.existsSync(cacheFile)) fs7.rmSync(cacheFile);
12222
12940
  } catch {
12223
12941
  }
12224
12942
  }
12225
12943
  static readCache(key) {
12226
12944
  try {
12227
12945
  const cacheDir = globalCacheDir();
12228
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12229
- if (!fs6.existsSync(cacheFile)) return null;
12230
- const raw = fs6.readFileSync(cacheFile, "utf8");
12946
+ const cacheFile = path16.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12947
+ if (!fs7.existsSync(cacheFile)) return null;
12948
+ const raw = fs7.readFileSync(cacheFile, "utf8");
12231
12949
  const data = JSON.parse(raw);
12232
12950
  if (Date.now() - data.cachedAt > CACHE_TTL_MS) return null;
12233
12951
  return JSON.stringify(data.models);
@@ -12238,9 +12956,9 @@ var ModelCatalog = class {
12238
12956
  static writeCache(key, models) {
12239
12957
  try {
12240
12958
  const cacheDir = globalCacheDir();
12241
- fs6.mkdirSync(cacheDir, { recursive: true });
12242
- const cacheFile = path13.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12243
- fs6.writeFileSync(
12959
+ fs7.mkdirSync(cacheDir, { recursive: true });
12960
+ const cacheFile = path16.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
12961
+ fs7.writeFileSync(
12244
12962
  cacheFile,
12245
12963
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
12246
12964
  );
@@ -12507,9 +13225,9 @@ var ModelResolver = class {
12507
13225
  };
12508
13226
 
12509
13227
  // src/services/RunnerInstallation.ts
12510
- var import_child_process5 = require("child_process");
13228
+ var import_child_process6 = require("child_process");
12511
13229
  var import_util4 = require("util");
12512
- var execAsync4 = (0, import_util4.promisify)(import_child_process5.exec);
13230
+ var execAsync4 = (0, import_util4.promisify)(import_child_process6.exec);
12513
13231
  var defaultExec3 = async (command, options) => {
12514
13232
  const PATH = await augmentedPath();
12515
13233
  const { stdout } = await execAsync4(command, { ...options, env: withPath(process.env, PATH) });
@@ -12616,71 +13334,12 @@ var RunnerInstallation = class {
12616
13334
  }
12617
13335
  };
12618
13336
 
12619
- // src/services/settingsEnvAllowlist.ts
12620
- var ORDEWELL_SETTABLE_ENV = [
12621
- "AI_PROVIDER",
12622
- "ORCHESTRATOR_MODEL",
12623
- "ORDEWELL_SUBAGENT_MODEL",
12624
- "ORDEWELL_PLANNER_EFFORT",
12625
- "ORDEWELL_RESEARCH_ENABLED",
12626
- "ORDEWELL_MAX_PARALLEL"
12627
- ];
12628
- var SETTINGS_ENV_REFUSED = [
12629
- // Approval posture and pre-approved scope.
12630
- "ORDEWELL_AUTONOMOUS_MODE",
12631
- "ORDEWELL_APPROVAL_MODE",
12632
- "ORDEWELL_APPROVAL_ALLOW",
12633
- // Where settings are persisted.
12634
- "ORDEWELL_SETTINGS_PATH",
12635
- // Runtime options and dynamic loaders.
12636
- "NODE_OPTIONS",
12637
- "NODE_REPL_EXTERNAL_MODULE",
12638
- "NODE_PATH",
12639
- "ELECTRON_RUN_AS_NODE",
12640
- "LD_PRELOAD",
12641
- "LD_LIBRARY_PATH",
12642
- "LD_AUDIT",
12643
- "DYLD_INSERT_LIBRARIES",
12644
- "DYLD_LIBRARY_PATH",
12645
- "DYLD_FRAMEWORK_PATH",
12646
- // Executable resolution.
12647
- "PATH",
12648
- "PATHEXT",
12649
- "NODE"
12650
- ];
12651
- var REFUSED = new Set(SETTINGS_ENV_REFUSED);
12652
- function providerEnvKeys() {
12653
- const keys = [];
12654
- for (const meta of Object.values(ALL_PROVIDERS)) {
12655
- if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
12656
- if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
12657
- for (const detect of meta.detectEnvVars) keys.push(detect);
12658
- }
12659
- return keys;
12660
- }
12661
- var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
12662
- var SETTINGS_ENV_ALLOWLIST = new Set(
12663
- [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
12664
- );
12665
- function admitSettingsEnv(env) {
12666
- const accepted = {};
12667
- const rejected = [];
12668
- for (const [key, value] of Object.entries(env)) {
12669
- if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
12670
- rejected.push(key);
12671
- continue;
12672
- }
12673
- accepted[key] = value;
12674
- }
12675
- return { accepted, rejected };
12676
- }
12677
-
12678
13337
  // src/services/harness/CliAgentAiService.ts
12679
- var import_child_process6 = require("child_process");
13338
+ var import_child_process7 = require("child_process");
12680
13339
 
12681
13340
  // src/utils/workspace.ts
12682
- var fs7 = __toESM(require("fs"));
12683
- var path14 = __toESM(require("path"));
13341
+ var fs8 = __toESM(require("fs"));
13342
+ var path17 = __toESM(require("path"));
12684
13343
  var WorkspaceNotFoundError = class extends Error {
12685
13344
  constructor(workspace) {
12686
13345
  super(`Workspace "${workspace}" does not exist.`);
@@ -12691,7 +13350,7 @@ var WorkspaceNotFoundError = class extends Error {
12691
13350
  };
12692
13351
  function defaultIsDirectory(candidate) {
12693
13352
  try {
12694
- return fs7.statSync(candidate).isDirectory();
13353
+ return fs8.statSync(candidate).isDirectory();
12695
13354
  } catch {
12696
13355
  return false;
12697
13356
  }
@@ -12727,8 +13386,8 @@ var PROJECT_MARKER_FILES = [
12727
13386
  "CMakeLists.txt"
12728
13387
  ];
12729
13388
  function assertWorkspaceIsProject(workspace, deps = {}) {
12730
- const realpath = deps.realpath ?? ((candidate) => fs7.realpathSync(candidate));
12731
- const exists = deps.exists ?? ((candidate) => fs7.existsSync(candidate));
13389
+ const realpath = deps.realpath ?? ((candidate) => fs8.realpathSync(candidate));
13390
+ const exists = deps.exists ?? ((candidate) => fs8.existsSync(candidate));
12732
13391
  let resolved;
12733
13392
  try {
12734
13393
  resolved = realpath(workspace);
@@ -12736,7 +13395,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
12736
13395
  throw new WorkspaceNotAProjectError(workspace);
12737
13396
  }
12738
13397
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
12739
- (marker2) => exists(path14.join(resolved, marker2))
13398
+ (marker2) => exists(path17.join(resolved, marker2))
12740
13399
  );
12741
13400
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
12742
13401
  }
@@ -12804,13 +13463,14 @@ var StdioAgentAdapter = class {
12804
13463
  }
12805
13464
  async start(opts) {
12806
13465
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
12807
- this.processEnded = new Promise((resolve5) => {
12808
- this.markEnded = resolve5;
13466
+ this.processEnded = new Promise((resolve7) => {
13467
+ this.markEnded = resolve7;
12809
13468
  });
12810
13469
  const spec = this.spawnSpec(opts);
12811
13470
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
12812
13471
  const PATH = await resolvePath();
12813
- this.spawnEnv = withPath(process.env, PATH, spec.env);
13472
+ const workspace = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
13473
+ this.spawnEnv = withPath(process.env, PATH, { ...workspace, ...spec.env });
12814
13474
  const launch = await planDirectLaunch(spec.command, spec.args, {
12815
13475
  platform: this.deps.platform,
12816
13476
  resolvePath
@@ -12856,7 +13516,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
12856
13516
  onEvent({ type: "error", message: this.exitMessage() });
12857
13517
  return;
12858
13518
  }
12859
- await new Promise((resolve5) => {
13519
+ await new Promise((resolve7) => {
12860
13520
  let settled = false;
12861
13521
  const finish = () => {
12862
13522
  if (settled) return;
@@ -12867,7 +13527,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
12867
13527
  signal?.removeEventListener("abort", onAbort);
12868
13528
  this.process?.removeListener("exit", onExit);
12869
13529
  this.process?.removeListener("error", onExit);
12870
- resolve5();
13530
+ resolve7();
12871
13531
  };
12872
13532
  const emit = (event) => {
12873
13533
  onEvent(event);
@@ -13082,14 +13742,14 @@ function codexSandboxUnavailableMessage() {
13082
13742
  ].join("\n");
13083
13743
  }
13084
13744
  function runProbe(deps, cwd, env, flags) {
13085
- return new Promise((resolve5) => {
13745
+ return new Promise((resolve7) => {
13086
13746
  let output = "";
13087
13747
  let settled = false;
13088
13748
  const finish = (code) => {
13089
13749
  if (settled) return;
13090
13750
  settled = true;
13091
13751
  clearTimeout(timer);
13092
- resolve5({ code, output });
13752
+ resolve7({ code, output });
13093
13753
  };
13094
13754
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
13095
13755
  timer.unref?.();
@@ -13172,8 +13832,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
13172
13832
  async handshake(opts) {
13173
13833
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
13174
13834
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
13175
- const ready = new Promise((resolve5) => {
13176
- this.settleHandshake = resolve5;
13835
+ const ready = new Promise((resolve7) => {
13836
+ this.settleHandshake = resolve7;
13177
13837
  });
13178
13838
  this.writeLine({
13179
13839
  jsonrpc: "2.0",
@@ -13186,8 +13846,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
13186
13846
  // A binary that rejects `app-server` outright dies immediately; waiting
13187
13847
  // out the timeout would turn a one-line diagnostic into a 30s stall.
13188
13848
  this.processEnded.then(() => false),
13189
- new Promise((resolve5) => {
13190
- const t = setTimeout(() => resolve5(false), HANDSHAKE_TIMEOUT_MS);
13849
+ new Promise((resolve7) => {
13850
+ const t = setTimeout(() => resolve7(false), HANDSHAKE_TIMEOUT_MS);
13191
13851
  t.unref?.();
13192
13852
  })
13193
13853
  ]);
@@ -13464,12 +14124,12 @@ function describeError(err) {
13464
14124
  return lines.join(": ");
13465
14125
  }
13466
14126
  function delay(ms, signal) {
13467
- return new Promise((resolve5) => {
13468
- const timer = setTimeout(resolve5, ms);
14127
+ return new Promise((resolve7) => {
14128
+ const timer = setTimeout(resolve7, ms);
13469
14129
  timer.unref?.();
13470
14130
  signal?.addEventListener("abort", () => {
13471
14131
  clearTimeout(timer);
13472
- resolve5();
14132
+ resolve7();
13473
14133
  }, { once: true });
13474
14134
  });
13475
14135
  }
@@ -13503,17 +14163,17 @@ var OpenCodeAdapter = class {
13503
14163
  throw new ExecutableNotFoundError("opencode", PATH);
13504
14164
  }
13505
14165
  this.process = this.deps.spawn(launch.file, launch.args, {
13506
- env: withPath(process.env, PATH),
14166
+ env: withPath(process.env, PATH, await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd)),
13507
14167
  stdio: ["pipe", "pipe", "pipe"],
13508
14168
  cwd: opts.cwd,
13509
14169
  windowsVerbatimArguments: launch.verbatim
13510
14170
  });
13511
- const banner = new Promise((resolve5) => {
14171
+ const banner = new Promise((resolve7) => {
13512
14172
  let seen = "";
13513
14173
  const scan = (chunk) => {
13514
14174
  seen += chunk.toString();
13515
14175
  const match = seen.match(/https?:\/\/[^\s]+/);
13516
- if (match) resolve5(match[0].replace(/[.,)]$/, ""));
14176
+ if (match) resolve7(match[0].replace(/[.,)]$/, ""));
13517
14177
  };
13518
14178
  this.process.stdout?.on("data", scan);
13519
14179
  this.process.stderr?.on("data", (chunk) => {
@@ -13522,15 +14182,15 @@ var OpenCodeAdapter = class {
13522
14182
  });
13523
14183
  this.process.on("exit", () => {
13524
14184
  this.exited = true;
13525
- resolve5(null);
14185
+ resolve7(null);
13526
14186
  });
13527
14187
  this.process.on("error", (err) => {
13528
14188
  this.exited = true;
13529
14189
  this.stderrTail = (this.stderrTail + `
13530
14190
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
13531
- resolve5(null);
14191
+ resolve7(null);
13532
14192
  });
13533
- const timer = setTimeout(() => resolve5(null), SERVER_READY_TIMEOUT_MS);
14193
+ const timer = setTimeout(() => resolve7(null), SERVER_READY_TIMEOUT_MS);
13534
14194
  timer.unref?.();
13535
14195
  });
13536
14196
  this.baseUrl = await banner;
@@ -13563,8 +14223,8 @@ ${this.stderrTail.trim()}` : ""}`);
13563
14223
  const streamAbort = new AbortController();
13564
14224
  let connected = () => {
13565
14225
  };
13566
- const streamReady = new Promise((resolve5) => {
13567
- connected = resolve5;
14226
+ const streamReady = new Promise((resolve7) => {
14227
+ connected = resolve7;
13568
14228
  });
13569
14229
  const live = this.streamEvents(
13570
14230
  streamAbort.signal,
@@ -13768,14 +14428,14 @@ ${part.text}` : part.text });
13768
14428
  }
13769
14429
  }
13770
14430
  }
13771
- async json(method, path23, body, signal) {
13772
- const response = await this.deps.fetch(`${this.baseUrl}${path23}`, {
14431
+ async json(method, path26, body, signal) {
14432
+ const response = await this.deps.fetch(`${this.baseUrl}${path26}`, {
13773
14433
  method,
13774
14434
  headers: { "content-type": "application/json" },
13775
14435
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
13776
14436
  ...signal ? { signal } : {}
13777
14437
  });
13778
- if (!response.ok) throw new Error(`${method} ${path23} failed: ${response.status} ${response.statusText}`);
14438
+ if (!response.ok) throw new Error(`${method} ${path26} failed: ${response.status} ${response.statusText}`);
13779
14439
  return await response.json().catch(() => null);
13780
14440
  }
13781
14441
  nativeSessionId() {
@@ -13868,7 +14528,7 @@ var CliAgentAiService = class {
13868
14528
  if (!runner) throw new Error(`${config.aiProvider} is not a coding-agent planner.`);
13869
14529
  this.runner = runner;
13870
14530
  this.processDeps = {
13871
- spawn: deps.spawn ?? import_child_process6.spawn,
14531
+ spawn: deps.spawn ?? import_child_process7.spawn,
13872
14532
  fetch: deps.fetch ?? globalThis.fetch,
13873
14533
  resolvePath: deps.resolvePath,
13874
14534
  platform: deps.platform,
@@ -14229,8 +14889,8 @@ var CliAgentAiService = class {
14229
14889
  this.lastNativeSessionId = previousSessionId;
14230
14890
  }
14231
14891
  }
14232
- async researchAndPlan(userDescription, runners, modelsByRunner, fs14, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
14233
- const contextStr = await collectResearchContext(fs14, runners);
14892
+ async researchAndPlan(userDescription, runners, modelsByRunner, fs15, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
14893
+ const contextStr = await collectResearchContext(fs15, runners);
14234
14894
  const researchLog = [
14235
14895
  { id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
14236
14896
  ];
@@ -14444,33 +15104,9 @@ var PlannerConversation = class {
14444
15104
  if (m.role !== "user" || index === 0) return [];
14445
15105
  const line = m.content.split("\n")[0];
14446
15106
  const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
14447
- return [{ index, preview, timestamp: m.timestamp }];
15107
+ return [{ index, preview, content: m.content, timestamp: m.timestamp }];
14448
15108
  });
14449
15109
  }
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
15110
  /**
14475
15111
  * Replace the transcript with a summary of it, keeping the last two
14476
15112
  * exchanges as they were, and drop the live context so the next message
@@ -14546,6 +15182,28 @@ var PlannerConversation = class {
14546
15182
  researchLog: plan?.researchLog ?? []
14547
15183
  });
14548
15184
  }
15185
+ /**
15186
+ * A copy of the dialogue as it stood just before the user message at
15187
+ * `index` (a position from {@link rewindTargets}) — what a rewind forks
15188
+ * from — plus that message's full text, for a surface to offer back. This
15189
+ * conversation is not touched. The research trace is cut at the same point
15190
+ * by time: its entries carry no link to the message that caused them.
15191
+ */
15192
+ cloneBefore(index) {
15193
+ this.assertIdle("rewind the conversation");
15194
+ if (!this.rewindTargets().some((t) => t.index === index)) {
15195
+ if (index > 0) throw new ConversationEditError(`No user message at position ${index} to rewind to.`);
15196
+ 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.");
15197
+ }
15198
+ const { content: rewoundMessage, timestamp: cutoff } = this.transcript[index];
15199
+ return {
15200
+ dialogue: structuredClone({
15201
+ conversationHistory: this.transcript.slice(0, index),
15202
+ researchLog: (this.host.plan()?.researchLog ?? []).filter((e) => e.timestamp < cutoff)
15203
+ }),
15204
+ rewoundMessage
15205
+ };
15206
+ }
14549
15207
  /** A one-shot `modifyPlan` exchange. Call inside the host's mutation ritual. */
14550
15208
  recordModification(request, requestedAt, taskCount) {
14551
15209
  this.append("user", request, { timestamp: requestedAt });
@@ -15131,26 +15789,26 @@ function retargetTaskRunner(task, runner, catalog) {
15131
15789
  }
15132
15790
 
15133
15791
  // src/services/SkillsService.ts
15134
- var fs8 = __toESM(require("fs"));
15135
- var path16 = __toESM(require("path"));
15792
+ var fs9 = __toESM(require("fs"));
15793
+ var path19 = __toESM(require("path"));
15136
15794
 
15137
15795
  // src/services/builtinSkills.ts
15138
- var path15 = __toESM(require("path"));
15796
+ var path18 = __toESM(require("path"));
15139
15797
  var import_url = require("url");
15140
15798
  var import_meta = {};
15141
15799
  function builtinSkillsDir() {
15142
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path15.dirname((0, import_url.fileURLToPath)(
15800
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path18.dirname((0, import_url.fileURLToPath)(
15143
15801
  // @ts-expect-error - import.meta is only valid under an ESM module setting
15144
15802
  import_meta.url
15145
15803
  ));
15146
- return path15.join(dir, "..", "skills");
15804
+ return path18.join(dir, "..", "skills");
15147
15805
  }
15148
15806
 
15149
15807
  // src/services/SkillsService.ts
15150
15808
  var BUILTIN_SKILL_NAMES = ["grilling", "to-spec", "improve-codebase-architecture"];
15151
15809
  var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
15152
15810
  function parseSkillFile(filePath, name, source) {
15153
- const raw = fs8.readFileSync(filePath, "utf8");
15811
+ const raw = fs9.readFileSync(filePath, "utf8");
15154
15812
  const frontmatter = {};
15155
15813
  let content = raw;
15156
15814
  if (raw.startsWith("---\n")) {
@@ -15179,23 +15837,23 @@ function parseSkillFile(filePath, name, source) {
15179
15837
  return { name, description: metadata.description, metadata, content, path: filePath, source };
15180
15838
  }
15181
15839
  function copyDirSync2(src, dest) {
15182
- fs8.mkdirSync(dest, { recursive: true });
15183
- const entries = fs8.readdirSync(src, { withFileTypes: true });
15840
+ fs9.mkdirSync(dest, { recursive: true });
15841
+ const entries = fs9.readdirSync(src, { withFileTypes: true });
15184
15842
  for (const entry of entries) {
15185
- const srcPath = path16.join(src, entry.name);
15186
- const destPath = path16.join(dest, entry.name);
15843
+ const srcPath = path19.join(src, entry.name);
15844
+ const destPath = path19.join(dest, entry.name);
15187
15845
  if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
15188
- else fs8.copyFileSync(srcPath, destPath);
15846
+ else fs9.copyFileSync(srcPath, destPath);
15189
15847
  }
15190
15848
  }
15191
15849
  function readDir(dir, source) {
15192
- if (!fs8.existsSync(dir)) return [];
15193
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
15850
+ if (!fs9.existsSync(dir)) return [];
15851
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
15194
15852
  const skills = [];
15195
15853
  for (const entry of entries) {
15196
15854
  if (!entry.isDirectory()) continue;
15197
- const skillFile = path16.join(dir, entry.name, "SKILL.md");
15198
- if (!fs8.existsSync(skillFile)) continue;
15855
+ const skillFile = path19.join(dir, entry.name, "SKILL.md");
15856
+ if (!fs9.existsSync(skillFile)) continue;
15199
15857
  const parsed = parseSkillFile(skillFile, entry.name, source);
15200
15858
  if (parsed) skills.push(parsed);
15201
15859
  }
@@ -15207,22 +15865,22 @@ var SkillsService = class {
15207
15865
  }
15208
15866
  workspaceRoot;
15209
15867
  localDir() {
15210
- return this.workspaceRoot ? path16.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
15868
+ return this.workspaceRoot ? path19.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
15211
15869
  }
15212
15870
  globalDir() {
15213
- return path16.join(globalDataDir(), "skills");
15871
+ return path19.join(globalDataDir(), "skills");
15214
15872
  }
15215
15873
  seed(name) {
15216
- const dest = path16.join(this.globalDir(), name);
15217
- if (fs8.existsSync(dest)) return false;
15874
+ const dest = path19.join(this.globalDir(), name);
15875
+ if (fs9.existsSync(dest)) return false;
15218
15876
  let srcDir;
15219
15877
  try {
15220
- srcDir = path16.join(builtinSkillsDir(), name);
15878
+ srcDir = path19.join(builtinSkillsDir(), name);
15221
15879
  } catch (err) {
15222
15880
  console.warn(`Could not locate built-in skills dir: ${String(err)}`);
15223
15881
  return false;
15224
15882
  }
15225
- if (!fs8.existsSync(srcDir)) {
15883
+ if (!fs9.existsSync(srcDir)) {
15226
15884
  console.warn(`Built-in skill not found in package: ${name}`);
15227
15885
  return false;
15228
15886
  }
@@ -15236,14 +15894,14 @@ var SkillsService = class {
15236
15894
  isUnmodifiedRetiredSeed(dir, expectedName) {
15237
15895
  let entries;
15238
15896
  try {
15239
- entries = fs8.readdirSync(dir, { withFileTypes: true });
15897
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
15240
15898
  } catch {
15241
15899
  return false;
15242
15900
  }
15243
15901
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
15244
15902
  let raw;
15245
15903
  try {
15246
- raw = fs8.readFileSync(path16.join(dir, "SKILL.md"), "utf8");
15904
+ raw = fs9.readFileSync(path19.join(dir, "SKILL.md"), "utf8");
15247
15905
  } catch {
15248
15906
  return false;
15249
15907
  }
@@ -15260,11 +15918,11 @@ var SkillsService = class {
15260
15918
  }
15261
15919
  pruneRetired() {
15262
15920
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
15263
- const dest = path16.join(this.globalDir(), name);
15264
- if (!fs8.existsSync(dest)) continue;
15921
+ const dest = path19.join(this.globalDir(), name);
15922
+ if (!fs9.existsSync(dest)) continue;
15265
15923
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
15266
15924
  try {
15267
- fs8.rmSync(dest, { recursive: true, force: true });
15925
+ fs9.rmSync(dest, { recursive: true, force: true });
15268
15926
  } catch (err) {
15269
15927
  console.warn(`Could not prune retired skill ${name}: ${String(err)}`);
15270
15928
  }
@@ -15275,11 +15933,11 @@ var SkillsService = class {
15275
15933
  if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
15276
15934
  const localDir = this.localDir();
15277
15935
  if (localDir) {
15278
- const localFile = path16.join(localDir, name, "SKILL.md");
15279
- if (fs8.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
15936
+ const localFile = path19.join(localDir, name, "SKILL.md");
15937
+ if (fs9.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
15280
15938
  }
15281
- const globalFile = path16.join(this.globalDir(), name, "SKILL.md");
15282
- if (fs8.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
15939
+ const globalFile = path19.join(this.globalDir(), name, "SKILL.md");
15940
+ if (fs9.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
15283
15941
  return void 0;
15284
15942
  }
15285
15943
  listSkills() {
@@ -15360,8 +16018,8 @@ function truncateCheckpointSummary(summary) {
15360
16018
  }
15361
16019
 
15362
16020
  // src/utils/sessionStore.ts
15363
- var fs9 = __toESM(require("fs"));
15364
- var path17 = __toESM(require("path"));
16021
+ var fs10 = __toESM(require("fs"));
16022
+ var path20 = __toESM(require("path"));
15365
16023
 
15366
16024
  // src/utils/sessionId.ts
15367
16025
  var import_crypto2 = require("crypto");
@@ -15372,7 +16030,7 @@ function mintSessionId() {
15372
16030
  // src/utils/sessionStore.ts
15373
16031
  var SESSIONS_SUBDIR = "sessions";
15374
16032
  function getSessionsDir(baseDir) {
15375
- return path17.join(getStateDir(baseDir), SESSIONS_SUBDIR);
16033
+ return path20.join(getStateDir(baseDir), SESSIONS_SUBDIR);
15376
16034
  }
15377
16035
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
15378
16036
  function hasScalarRunner(parsed) {
@@ -15394,11 +16052,11 @@ function legacySessionFilename(session) {
15394
16052
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
15395
16053
  }
15396
16054
  function dropLegacyFile(sessionsDir, meta) {
15397
- const legacy = path17.join(sessionsDir, legacySessionFilename(meta));
15398
- if (legacy === path17.join(sessionsDir, sessionFilename(meta)) || !fs9.existsSync(legacy)) return;
16055
+ const legacy = path20.join(sessionsDir, legacySessionFilename(meta));
16056
+ if (legacy === path20.join(sessionsDir, sessionFilename(meta)) || !fs10.existsSync(legacy)) return;
15399
16057
  try {
15400
- const held = JSON.parse(fs9.readFileSync(legacy, "utf-8")).meta?.id;
15401
- if (held === meta.id) fs9.unlinkSync(legacy);
16058
+ const held = JSON.parse(fs10.readFileSync(legacy, "utf-8")).meta?.id;
16059
+ if (held === meta.id) fs10.unlinkSync(legacy);
15402
16060
  } catch {
15403
16061
  }
15404
16062
  }
@@ -15421,18 +16079,18 @@ function saveSession(plan, goal, baseDir, id) {
15421
16079
  plan: JSON.parse(JSON.stringify(plan))
15422
16080
  };
15423
16081
  const filename = sessionFilename(meta);
15424
- const filePath = path17.join(sessionsDir, filename);
15425
- fs9.writeFileSync(filePath, JSON.stringify(session, null, 2));
16082
+ const filePath = path20.join(sessionsDir, filename);
16083
+ fs10.writeFileSync(filePath, JSON.stringify(session, null, 2));
15426
16084
  dropLegacyFile(sessionsDir, meta);
15427
16085
  return meta;
15428
16086
  }
15429
16087
  function listSessions(baseDir, logger = defaultLogger) {
15430
16088
  const sessionsDir = getSessionsDir(baseDir);
15431
- if (!fs9.existsSync(sessionsDir)) return [];
15432
- const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
16089
+ if (!fs10.existsSync(sessionsDir)) return [];
16090
+ const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
15433
16091
  return files.map((f) => {
15434
16092
  try {
15435
- const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
16093
+ const raw = fs10.readFileSync(path20.join(sessionsDir, f), "utf-8");
15436
16094
  const parsed = JSON.parse(raw);
15437
16095
  if (hasScalarRunner(parsed)) {
15438
16096
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -15447,10 +16105,10 @@ function listSessions(baseDir, logger = defaultLogger) {
15447
16105
  }).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
15448
16106
  }
15449
16107
  function findSessionFile(sessionsDir, sessionId, logger) {
15450
- const files = fs9.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
16108
+ const files = fs10.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
15451
16109
  for (const f of files) {
15452
16110
  try {
15453
- const raw = fs9.readFileSync(path17.join(sessionsDir, f), "utf-8");
16111
+ const raw = fs10.readFileSync(path20.join(sessionsDir, f), "utf-8");
15454
16112
  const session = JSON.parse(raw);
15455
16113
  if (session.meta.id === sessionId) return f;
15456
16114
  } catch (err) {
@@ -15461,11 +16119,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
15461
16119
  }
15462
16120
  function loadSession(sessionId, baseDir, logger = defaultLogger) {
15463
16121
  const sessionsDir = getSessionsDir(baseDir);
15464
- if (!fs9.existsSync(sessionsDir)) return null;
16122
+ if (!fs10.existsSync(sessionsDir)) return null;
15465
16123
  try {
15466
16124
  const file = findSessionFile(sessionsDir, sessionId, logger);
15467
16125
  if (file) {
15468
- const raw = fs9.readFileSync(path17.join(sessionsDir, file), "utf-8");
16126
+ const raw = fs10.readFileSync(path20.join(sessionsDir, file), "utf-8");
15469
16127
  const parsed = JSON.parse(raw);
15470
16128
  if (hasScalarRunner(parsed)) {
15471
16129
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -15496,11 +16154,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
15496
16154
  }
15497
16155
  function deleteSession(sessionId, baseDir, logger = defaultLogger) {
15498
16156
  const sessionsDir = getSessionsDir(baseDir);
15499
- if (!fs9.existsSync(sessionsDir)) return false;
16157
+ if (!fs10.existsSync(sessionsDir)) return false;
15500
16158
  try {
15501
16159
  const file = findSessionFile(sessionsDir, sessionId, logger);
15502
16160
  if (file) {
15503
- fs9.unlinkSync(path17.join(sessionsDir, file));
16161
+ fs10.unlinkSync(path20.join(sessionsDir, file));
15504
16162
  return true;
15505
16163
  }
15506
16164
  } catch (err) {
@@ -16032,26 +16690,30 @@ var Session = class {
16032
16690
  */
16033
16691
  forkConversation() {
16034
16692
  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 };
16693
+ return this.saveFork(this.conversation.clone());
16040
16694
  }
16041
16695
  /**
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.
16696
+ * Fork the conversation from just before a user message (a transcript
16697
+ * position from {@link rewindTargets}): the fork holds everything said
16698
+ * before it and the current task list, like {@link forkConversation}, and
16699
+ * this session keeps its whole history. The message's full text comes back
16700
+ * so a surface can offer it for resending.
16046
16701
  */
16047
16702
  rewindConversation(userMessageIndex) {
16048
16703
  if (!this.plan) throw new ConversationEditError("No planning conversation to rewind");
16049
- return this.conversation.rewind(userMessageIndex);
16704
+ const { dialogue, rewoundMessage } = this.conversation.cloneBefore(userMessageIndex);
16705
+ return { ...this.saveFork(dialogue), rewoundMessage };
16706
+ }
16707
+ saveFork(dialogue) {
16708
+ const sessionId = mintSessionId();
16709
+ const plan = forkPlanState(this.plan, this.store.planTasks, dialogue, (/* @__PURE__ */ new Date()).toISOString());
16710
+ saveSession(plan, this.goal, this.workspace, sessionId);
16711
+ return { sessionId, goal: this.goal, workspace: this.workspace };
16050
16712
  }
16051
16713
  /**
16052
16714
  * Condense the conversation on the user's say-so: a hidden planner turn
16053
16715
  * summarises it, and the summary replaces everything but the last two
16054
- * exchanges. Conversation only, like a rewind — the tasks, and any run
16716
+ * exchanges. Conversation only — the tasks, and any run
16055
16717
  * executing them, are untouched. Atomic: a failed or stopped turn changes
16056
16718
  * nothing.
16057
16719
  */
@@ -16177,6 +16839,10 @@ var Session = class {
16177
16839
  }
16178
16840
  await this.orchestrator.runTask(taskId);
16179
16841
  }
16842
+ /** Start whatever the scheduler can now fit — after the parallel limit was raised mid-run, say. */
16843
+ async reschedule() {
16844
+ await this.orchestrator.tick();
16845
+ }
16180
16846
  async retryTask(taskId) {
16181
16847
  await this.orchestrator.retryTask(taskId);
16182
16848
  this.persist();
@@ -16324,8 +16990,9 @@ var Session = class {
16324
16990
  * The opt-in way through a merge conflict: add an AI task, on the conflicted
16325
16991
  * task's own runner and model, that merges its branch by hand in a worktree
16326
16992
  * 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.
16993
+ * {@link TaskOrchestrator.linkConflictResolver}). The automatic answer to a
16994
+ * conflict is a repair, a new attempt of the same task (ADR-0015); a task is
16995
+ * only ever added to the plan by this call.
16329
16996
  */
16330
16997
  async resolveConflictAsTask(taskId) {
16331
16998
  if (!this.plan) return null;
@@ -16933,7 +17600,7 @@ function buildRunnerInvocation(opts) {
16933
17600
  }
16934
17601
 
16935
17602
  // src/services/HeadlessRunner.ts
16936
- var import_child_process7 = require("child_process");
17603
+ var import_child_process8 = require("child_process");
16937
17604
 
16938
17605
  // src/services/AbstractRunner.ts
16939
17606
  var import_events = require("events");
@@ -16988,7 +17655,7 @@ var AbstractRunner = class {
16988
17655
  function defaultHasScriptCmd() {
16989
17656
  if (process.platform === "win32") return false;
16990
17657
  try {
16991
- (0, import_child_process7.execSync)("which script", { stdio: "ignore" });
17658
+ (0, import_child_process8.execSync)("which script", { stdio: "ignore" });
16992
17659
  return true;
16993
17660
  } catch {
16994
17661
  return false;
@@ -17078,7 +17745,7 @@ var HeadlessRunner = class extends AbstractRunner {
17078
17745
  defaultInteractive = false;
17079
17746
  constructor(deps = {}) {
17080
17747
  super();
17081
- this.spawnImpl = deps.spawnImpl ?? import_child_process7.spawn;
17748
+ this.spawnImpl = deps.spawnImpl ?? import_child_process8.spawn;
17082
17749
  this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
17083
17750
  this.resolvePath = deps.resolvePath ?? augmentedPath;
17084
17751
  this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
@@ -17118,7 +17785,7 @@ var HeadlessRunner = class extends AbstractRunner {
17118
17785
  return {
17119
17786
  launch: await planDirectLaunch(command, args, this.launchDeps),
17120
17787
  resolvedPath,
17121
- env: invocation.env,
17788
+ env: { ...opts.env, ...invocation.env },
17122
17789
  pty,
17123
17790
  submitPromptKey: invocation.submitPromptKey
17124
17791
  };
@@ -17137,14 +17804,14 @@ var HeadlessRunner = class extends AbstractRunner {
17137
17804
  };
17138
17805
 
17139
17806
  // src/services/TmuxRunner.ts
17140
- var import_child_process9 = require("child_process");
17807
+ var import_child_process10 = require("child_process");
17141
17808
  var import_util5 = require("util");
17142
17809
  var import_os = require("os");
17143
17810
  var import_path = require("path");
17144
17811
  var import_fs2 = require("fs");
17145
17812
 
17146
17813
  // src/utils/tmux.ts
17147
- var import_child_process8 = require("child_process");
17814
+ var import_child_process9 = require("child_process");
17148
17815
  function tmuxSessionName(port) {
17149
17816
  return `ordewell-${port}`;
17150
17817
  }
@@ -17157,7 +17824,7 @@ function tmuxWindowName(taskId, planSessionId) {
17157
17824
  return `t-${scope}${slug(taskId).slice(0, 16)}`;
17158
17825
  }
17159
17826
  var defaultProbe = () => {
17160
- (0, import_child_process8.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
17827
+ (0, import_child_process9.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
17161
17828
  };
17162
17829
  function hasTmux(probe = defaultProbe) {
17163
17830
  try {
@@ -17169,7 +17836,7 @@ function hasTmux(probe = defaultProbe) {
17169
17836
  }
17170
17837
  var defaultHasBin = (bin) => {
17171
17838
  try {
17172
- (0, import_child_process8.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
17839
+ (0, import_child_process9.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
17173
17840
  return true;
17174
17841
  } catch {
17175
17842
  return false;
@@ -17192,7 +17859,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
17192
17859
 
17193
17860
  // src/services/TmuxRunner.ts
17194
17861
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
17195
- var execFileAsync2 = (0, import_util5.promisify)(import_child_process9.execFile);
17862
+ var execFileAsync2 = (0, import_util5.promisify)(import_child_process10.execFile);
17196
17863
  var defaultExecFile3 = async (command, args) => {
17197
17864
  const { stdout, stderr } = await execFileAsync2(command, args);
17198
17865
  return { stdout: String(stdout), stderr: String(stderr) };
@@ -17430,7 +18097,7 @@ var TmuxRunner = class extends AbstractRunner {
17430
18097
  (0, import_path.join)(this.logDir, `${id}.log`),
17431
18098
  this.socket
17432
18099
  );
17433
- invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
18100
+ invocation.env = { ...opts.env, ...invocation.env, PATH: await this.resolvePath() };
17434
18101
  await session.start(invocation.command, invocation.args, opts.cwd, invocation.env);
17435
18102
  if (invocation.submitPromptKey) session.write("\r");
17436
18103
  this.registerSession(id, session);
@@ -17510,18 +18177,18 @@ function tokensMatch(presented, expected) {
17510
18177
  }
17511
18178
 
17512
18179
  // src/plugins/RunnerRegistry.ts
17513
- var fs11 = __toESM(require("fs"));
17514
- var path20 = __toESM(require("path"));
17515
- var os5 = __toESM(require("os"));
17516
- var import_child_process10 = require("child_process");
18180
+ var fs12 = __toESM(require("fs"));
18181
+ var path23 = __toESM(require("path"));
18182
+ var os6 = __toESM(require("os"));
18183
+ var import_child_process11 = require("child_process");
17517
18184
  var import_crypto4 = require("crypto");
17518
18185
 
17519
18186
  // src/plugins/FsPluginStore.ts
17520
- var fs10 = __toESM(require("fs"));
17521
- var path19 = __toESM(require("path"));
18187
+ var fs11 = __toESM(require("fs"));
18188
+ var path22 = __toESM(require("path"));
17522
18189
 
17523
18190
  // src/plugins/pluginNames.ts
17524
- var path18 = __toESM(require("path"));
18191
+ var path21 = __toESM(require("path"));
17525
18192
  var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
17526
18193
  var MAX_PLUGIN_NAME_LENGTH = 64;
17527
18194
  function isPlainPluginName(name) {
@@ -17536,10 +18203,10 @@ function assertPlainPluginName(name) {
17536
18203
  }
17537
18204
  function resolvePluginInstallDir(pluginsDir, name) {
17538
18205
  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)) {
18206
+ const root = path21.resolve(pluginsDir);
18207
+ const dest = path21.resolve(root, name);
18208
+ const relative5 = path21.relative(root, dest);
18209
+ if (relative5 !== name || relative5 === "" || path21.isAbsolute(relative5)) {
17543
18210
  throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
17544
18211
  }
17545
18212
  return dest;
@@ -17554,13 +18221,13 @@ function isValidManifest(obj) {
17554
18221
 
17555
18222
  // src/plugins/FsPluginStore.ts
17556
18223
  function userPluginsDir() {
17557
- return path19.join(globalDataDir(), "plugins");
18224
+ return path22.join(globalDataDir(), "plugins");
17558
18225
  }
17559
18226
  function loadManifestFromDir(dir) {
17560
- const manifestPath = path19.join(dir, "manifest.json");
17561
- if (!fs10.existsSync(manifestPath)) return null;
18227
+ const manifestPath = path22.join(dir, "manifest.json");
18228
+ if (!fs11.existsSync(manifestPath)) return null;
17562
18229
  try {
17563
- const raw = fs10.readFileSync(manifestPath, "utf8");
18230
+ const raw = fs11.readFileSync(manifestPath, "utf8");
17564
18231
  const parsed = JSON.parse(raw);
17565
18232
  if (!isValidManifest(parsed)) return null;
17566
18233
  return parsed;
@@ -17570,29 +18237,29 @@ function loadManifestFromDir(dir) {
17570
18237
  }
17571
18238
  function copyDirSync3(src, dest) {
17572
18239
  ensureDir(dest);
17573
- const entries = fs10.readdirSync(src, { withFileTypes: true });
18240
+ const entries = fs11.readdirSync(src, { withFileTypes: true });
17574
18241
  for (const entry of entries) {
17575
- const srcPath = path19.join(src, entry.name);
17576
- const destPath = path19.join(dest, entry.name);
18242
+ const srcPath = path22.join(src, entry.name);
18243
+ const destPath = path22.join(dest, entry.name);
17577
18244
  if (entry.isDirectory()) {
17578
18245
  copyDirSync3(srcPath, destPath);
17579
18246
  } else {
17580
- fs10.copyFileSync(srcPath, destPath);
18247
+ fs11.copyFileSync(srcPath, destPath);
17581
18248
  }
17582
18249
  }
17583
18250
  }
17584
18251
  function rmDirSync(dir) {
17585
- if (!fs10.existsSync(dir)) return;
17586
- const entries = fs10.readdirSync(dir, { withFileTypes: true });
18252
+ if (!fs11.existsSync(dir)) return;
18253
+ const entries = fs11.readdirSync(dir, { withFileTypes: true });
17587
18254
  for (const entry of entries) {
17588
- const p = path19.join(dir, entry.name);
18255
+ const p = path22.join(dir, entry.name);
17589
18256
  if (entry.isDirectory()) {
17590
18257
  rmDirSync(p);
17591
18258
  } else {
17592
- fs10.unlinkSync(p);
18259
+ fs11.unlinkSync(p);
17593
18260
  }
17594
18261
  }
17595
- fs10.rmdirSync(dir);
18262
+ fs11.rmdirSync(dir);
17596
18263
  }
17597
18264
  var FsPluginStore = class {
17598
18265
  getUserPluginsDir() {
@@ -17600,10 +18267,10 @@ var FsPluginStore = class {
17600
18267
  }
17601
18268
  listUserPluginDirs() {
17602
18269
  const dir = userPluginsDir();
17603
- if (!fs10.existsSync(dir)) return [];
18270
+ if (!fs11.existsSync(dir)) return [];
17604
18271
  let entries;
17605
18272
  try {
17606
- entries = fs10.readdirSync(dir, { withFileTypes: true });
18273
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
17607
18274
  } catch {
17608
18275
  return [];
17609
18276
  }
@@ -17611,7 +18278,7 @@ var FsPluginStore = class {
17611
18278
  }
17612
18279
  listDir(dir) {
17613
18280
  try {
17614
- return fs10.readdirSync(dir);
18281
+ return fs11.readdirSync(dir);
17615
18282
  } catch {
17616
18283
  return [];
17617
18284
  }
@@ -17629,20 +18296,20 @@ var FsPluginStore = class {
17629
18296
  ensureDir(dir);
17630
18297
  }
17631
18298
  writeFile(filePath, content) {
17632
- fs10.writeFileSync(filePath, content);
18299
+ fs11.writeFileSync(filePath, content);
17633
18300
  }
17634
18301
  readFile(filePath) {
17635
18302
  try {
17636
- return fs10.readFileSync(filePath, "utf8");
18303
+ return fs11.readFileSync(filePath, "utf8");
17637
18304
  } catch {
17638
18305
  return null;
17639
18306
  }
17640
18307
  }
17641
- dirExists(path23) {
17642
- return fs10.existsSync(path23) && fs10.statSync(path23).isDirectory();
18308
+ dirExists(path26) {
18309
+ return fs11.existsSync(path26) && fs11.statSync(path26).isDirectory();
17643
18310
  }
17644
- exists(path23) {
17645
- return fs10.existsSync(path23);
18311
+ exists(path26) {
18312
+ return fs11.existsSync(path26);
17646
18313
  }
17647
18314
  };
17648
18315
 
@@ -17669,7 +18336,15 @@ var CLAUDE_CODE_MANIFEST = {
17669
18336
  "{{/if}}",
17670
18337
  "{{prompt}}"
17671
18338
  ],
17672
- promptInArgs: true
18339
+ promptInArgs: true,
18340
+ // Interactive sessions show these even with --dangerously-skip-permissions:
18341
+ // a folder Claude Code has never been trusted in (a fresh clone, or a repo
18342
+ // only ever planned in headlessly), and the first Bypass Permissions run.
18343
+ blockingPrompts: [
18344
+ { phrase: "Is this a project you created or one you trust", asks: "whether to trust the task's folder" },
18345
+ { phrase: "Do you trust the files in this folder", asks: "whether to trust the task's folder" },
18346
+ { phrase: "running in Bypass Permissions mode", asks: "you to accept Bypass Permissions mode" }
18347
+ ]
17673
18348
  },
17674
18349
  features: {
17675
18350
  modelSelection: true,
@@ -18013,7 +18688,7 @@ function isReservedRunnerName(name) {
18013
18688
  return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
18014
18689
  }
18015
18690
  function gitClone(url, destDir) {
18016
- (0, import_child_process10.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
18691
+ (0, import_child_process11.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
18017
18692
  stdio: "pipe",
18018
18693
  timeout: 3e4,
18019
18694
  // A repository that demands credentials should fail, not hang on a prompt.
@@ -18074,7 +18749,7 @@ var RunnerRegistry = class {
18074
18749
  return entry?.source === "builtin";
18075
18750
  }
18076
18751
  installFromPath(sourcePath) {
18077
- const absPath = path20.resolve(sourcePath);
18752
+ const absPath = path23.resolve(sourcePath);
18078
18753
  if (!this.store.dirExists(absPath)) {
18079
18754
  throw new Error(`Plugin source must be a directory: ${absPath}`);
18080
18755
  }
@@ -18086,7 +18761,7 @@ var RunnerRegistry = class {
18086
18761
  }
18087
18762
  installFromGit(url) {
18088
18763
  const validated = assertInstallablePluginUrl(url);
18089
- const tmpDir = path20.join(os5.tmpdir(), `ordewell-plugin-${(0, import_crypto4.randomBytes)(12).toString("hex")}`);
18764
+ const tmpDir = path23.join(os6.tmpdir(), `ordewell-plugin-${(0, import_crypto4.randomBytes)(12).toString("hex")}`);
18090
18765
  this.store.ensureDir(tmpDir);
18091
18766
  try {
18092
18767
  try {
@@ -18099,7 +18774,7 @@ var RunnerRegistry = class {
18099
18774
  const hasManifest = contents.some((f) => f === "manifest.json");
18100
18775
  let sourceDir = tmpDir;
18101
18776
  if (!hasManifest) {
18102
- const onlyEntry = contents.length === 1 ? path20.join(tmpDir, contents[0]) : void 0;
18777
+ const onlyEntry = contents.length === 1 ? path23.join(tmpDir, contents[0]) : void 0;
18103
18778
  if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
18104
18779
  throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
18105
18780
  }
@@ -18147,7 +18822,7 @@ var RunnerRegistry = class {
18147
18822
  }
18148
18823
  createSkeleton(name, outputDir) {
18149
18824
  assertPlainPluginName(name);
18150
- const dir = path20.resolve(outputDir, name);
18825
+ const dir = path23.resolve(outputDir, name);
18151
18826
  this.store.ensureDir(dir);
18152
18827
  const manifest = {
18153
18828
  name,
@@ -18179,8 +18854,8 @@ var RunnerRegistry = class {
18179
18854
  ]
18180
18855
  }
18181
18856
  };
18182
- this.store.writeFile(path20.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
18183
- const runSh = path20.join(dir, "run.sh");
18857
+ this.store.writeFile(path23.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
18858
+ const runSh = path23.join(dir, "run.sh");
18184
18859
  const scriptContent = `#!/usr/bin/env bash
18185
18860
  # Custom runner script for ${name}
18186
18861
  # Receives:
@@ -18206,30 +18881,30 @@ echo "Running ${name} with prompt: $prompt"
18206
18881
  };
18207
18882
  function fsChmodSync(fpath, mode) {
18208
18883
  try {
18209
- fs11.chmodSync(fpath, mode);
18884
+ fs12.chmodSync(fpath, mode);
18210
18885
  } catch {
18211
18886
  }
18212
18887
  }
18213
18888
 
18214
18889
  // src/utils/stateStore.ts
18215
- var fs12 = __toESM(require("fs"));
18216
- var path21 = __toESM(require("path"));
18890
+ var fs13 = __toESM(require("fs"));
18891
+ var path24 = __toESM(require("path"));
18217
18892
  var STATE_FILE = "state.json";
18218
18893
  function getStatePath(baseDir) {
18219
- return path21.join(getStateDir(baseDir), STATE_FILE);
18894
+ return path24.join(getStateDir(baseDir), STATE_FILE);
18220
18895
  }
18221
18896
  function saveState(plan, baseDir) {
18222
18897
  const statePath = getStatePath(baseDir ?? process.cwd());
18223
- ensureDir(path21.dirname(statePath));
18898
+ ensureDir(path24.dirname(statePath));
18224
18899
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
18225
- fs12.writeFileSync(statePath, JSON.stringify(plan, null, 2));
18900
+ fs13.writeFileSync(statePath, JSON.stringify(plan, null, 2));
18226
18901
  }
18227
18902
  function loadState(baseDir, logger = defaultLogger) {
18228
18903
  const dir = baseDir ?? process.cwd();
18229
18904
  const statePath = getStatePath(dir);
18230
- if (!fs12.existsSync(statePath)) return null;
18905
+ if (!fs13.existsSync(statePath)) return null;
18231
18906
  try {
18232
- const raw = fs12.readFileSync(statePath, "utf-8");
18907
+ const raw = fs13.readFileSync(statePath, "utf-8");
18233
18908
  const parsed = JSON.parse(raw);
18234
18909
  if (!Array.isArray(parsed.runners)) {
18235
18910
  logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
@@ -18246,16 +18921,16 @@ function loadState(baseDir, logger = defaultLogger) {
18246
18921
  function clearState(baseDir) {
18247
18922
  const dir = baseDir ?? process.cwd();
18248
18923
  const statePath = getStatePath(dir);
18249
- if (fs12.existsSync(statePath)) fs12.unlinkSync(statePath);
18924
+ if (fs13.existsSync(statePath)) fs13.unlinkSync(statePath);
18250
18925
  }
18251
18926
  function stateExists(baseDir) {
18252
18927
  const dir = baseDir ?? process.cwd();
18253
- return fs12.existsSync(getStatePath(dir));
18928
+ return fs13.existsSync(getStatePath(dir));
18254
18929
  }
18255
18930
 
18256
18931
  // src/services/SettingsService.ts
18257
- var fs13 = __toESM(require("fs"));
18258
- var path22 = __toESM(require("path"));
18932
+ var fs14 = __toESM(require("fs"));
18933
+ var path25 = __toESM(require("path"));
18259
18934
  var DEFAULTS = {
18260
18935
  tdd: { enabled: true },
18261
18936
  verification: { enabled: false }
@@ -18263,7 +18938,7 @@ var DEFAULTS = {
18263
18938
  function getSettingsPath() {
18264
18939
  const override = process.env.ORDEWELL_SETTINGS_PATH;
18265
18940
  if (override && override.trim()) return override.trim();
18266
- return path22.join(globalDataDir(), "settings.json");
18941
+ return path25.join(globalDataDir(), "settings.json");
18267
18942
  }
18268
18943
  function readPlannerModels(raw) {
18269
18944
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -18296,7 +18971,7 @@ var SettingsService = class {
18296
18971
  }
18297
18972
  fileMtimeMs() {
18298
18973
  try {
18299
- return fs13.statSync(this.filePath).mtimeMs;
18974
+ return fs14.statSync(this.filePath).mtimeMs;
18300
18975
  } catch {
18301
18976
  return null;
18302
18977
  }
@@ -18368,8 +19043,8 @@ var SettingsService = class {
18368
19043
  load() {
18369
19044
  migrateOldConfigDir();
18370
19045
  try {
18371
- if (fs13.existsSync(this.filePath)) {
18372
- const raw = JSON.parse(fs13.readFileSync(this.filePath, "utf-8"));
19046
+ if (fs14.existsSync(this.filePath)) {
19047
+ const raw = JSON.parse(fs14.readFileSync(this.filePath, "utf-8"));
18373
19048
  const settings = {
18374
19049
  tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
18375
19050
  verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled }
@@ -18391,11 +19066,11 @@ var SettingsService = class {
18391
19066
  return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
18392
19067
  }
18393
19068
  persist() {
18394
- const dir = path22.dirname(this.filePath);
18395
- if (!fs13.existsSync(dir)) {
18396
- fs13.mkdirSync(dir, { recursive: true });
19069
+ const dir = path25.dirname(this.filePath);
19070
+ if (!fs14.existsSync(dir)) {
19071
+ fs14.mkdirSync(dir, { recursive: true });
18397
19072
  }
18398
- fs13.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
19073
+ fs14.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
18399
19074
  this.cachedMtimeMs = this.fileMtimeMs();
18400
19075
  }
18401
19076
  };
@@ -18462,6 +19137,7 @@ var PlannerModelMemory = class {
18462
19137
  ConversationEditError,
18463
19138
  DAEMON_SUBPROTOCOL,
18464
19139
  DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
19140
+ DEFAULT_MAX_PARALLEL,
18465
19141
  DENY_ALL,
18466
19142
  EmbeddedNewlineError,
18467
19143
  EnvConfig,
@@ -18535,6 +19211,7 @@ var PlannerModelMemory = class {
18535
19211
  augmentPromptWithPriorOutputs,
18536
19212
  augmentedPath,
18537
19213
  bearerHeaderValue,
19214
+ buildConflictRepairPrompt,
18538
19215
  buildConflictResolutionPrompt,
18539
19216
  buildConversationSystemPrompt,
18540
19217
  buildFallbackGrepArgs,
@@ -18554,6 +19231,7 @@ var PlannerModelMemory = class {
18554
19231
  canMergeTasks,
18555
19232
  canSetDependencies,
18556
19233
  canSplitTask,
19234
+ capConflictFiles,
18557
19235
  checkDepsResolve,
18558
19236
  checkImmutableLog,
18559
19237
  checkInProgress,
@@ -18642,6 +19320,7 @@ var PlannerModelMemory = class {
18642
19320
  modifyValidationFeedback,
18643
19321
  normalizeAgentArgs,
18644
19322
  normalizeGeminiModel,
19323
+ parseMaxParallel,
18645
19324
  parsePartialPlan,
18646
19325
  parsePlanJson,
18647
19326
  parseTaskOpsJson,