@ordewell/core 0.11.1 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -210,6 +210,7 @@ __export(index_exports, {
210
210
  assignedRunnerOf: () => assignedRunnerOf,
211
211
  assignmentIssue: () => assignmentIssue,
212
212
  assignmentIssueText: () => assignmentIssueText,
213
+ attachingLabel: () => attachingLabel,
213
214
  augmentedPath: () => augmentedPath,
214
215
  automergeSummary: () => automergeSummary,
215
216
  autonomyLevelLabel: () => autonomyLevelLabel,
@@ -471,6 +472,7 @@ __export(index_exports, {
471
472
  repoBaseLine: () => repoBaseLine,
472
473
  repoResultLines: () => repoResultLines,
473
474
  reposWithWork: () => reposWithWork,
475
+ repositoryRunnerConfigPaths: () => repositoryRunnerConfigPaths,
474
476
  researchShellWarning: () => researchShellWarning,
475
477
  researchToolsPath: () => researchToolsPath,
476
478
  resetInstalledPlanner: () => resetInstalledPlanner,
@@ -661,18 +663,18 @@ function dependencyTargetErrors(tasks, taskId, dependencies) {
661
663
  }
662
664
  function findDependencyCycle(tasks) {
663
665
  const byId = new Map([...walkPlan(tasks)].map(({ task: task2 }) => [task2.id, task2]));
664
- const path47 = [];
666
+ const path50 = [];
665
667
  const done = /* @__PURE__ */ new Set();
666
668
  const visit = (id2) => {
667
669
  if (done.has(id2)) return null;
668
- const at = path47.indexOf(id2);
669
- if (at >= 0) return path47.slice(at);
670
- path47.push(id2);
670
+ const at = path50.indexOf(id2);
671
+ if (at >= 0) return path50.slice(at);
672
+ path50.push(id2);
671
673
  for (const dep of byId.get(id2)?.dependencies ?? []) {
672
674
  const hit = visit(dep);
673
675
  if (hit) return hit;
674
676
  }
675
- path47.pop();
677
+ path50.pop();
676
678
  done.add(id2);
677
679
  return null;
678
680
  };
@@ -1737,7 +1739,7 @@ function lastServedModel(events) {
1737
1739
  return void 0;
1738
1740
  }
1739
1741
  function taskLogForSurface(events) {
1740
- return events.map((event) => event.type === "task_skills" ? { type: "task_skills", skills: event.skills.map(({ name, source, path: path47 }) => ({ name, source, path: path47 })) } : event);
1742
+ return events.map((event) => event.type === "task_skills" ? { type: "task_skills", skills: event.skills.map(({ name, source, path: path50 }) => ({ name, source, path: path50 })) } : event);
1741
1743
  }
1742
1744
  function coalesceTaskLog(events) {
1743
1745
  const out = [];
@@ -2780,9 +2782,9 @@ function literalCdTarget(seg) {
2780
2782
  return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
2781
2783
  }
2782
2784
  function followCd(segments) {
2783
- if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
2785
+ if (segments.some((s) => s.joinedBy === "other" || movesShell(s))) return;
2784
2786
  const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
2785
- if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
2787
+ if (segments.some((s, i) => s.binary.toLowerCase() === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
2786
2788
  let chain = [];
2787
2789
  segments.forEach((seg, i) => {
2788
2790
  seg.cwd = chain;
@@ -2791,7 +2793,7 @@ function followCd(segments) {
2791
2793
  });
2792
2794
  }
2793
2795
  function markShellState(segments, bareSegment) {
2794
- if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
2796
+ if (!bareSegment && !segments.some(movesShell)) return;
2795
2797
  for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
2796
2798
  }
2797
2799
  var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
@@ -2810,6 +2812,7 @@ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
2810
2812
  "exec",
2811
2813
  "pushd",
2812
2814
  "popd",
2815
+ "chdir",
2813
2816
  "alias",
2814
2817
  "unalias",
2815
2818
  "builtin",
@@ -2818,6 +2821,9 @@ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
2818
2821
  "hash",
2819
2822
  "trap"
2820
2823
  ]);
2824
+ function movesShell(seg) {
2825
+ return SHELL_STATE_COMMANDS.has(seg.binary.toLowerCase());
2826
+ }
2821
2827
 
2822
2828
  // src/services/commandRunners.ts
2823
2829
  var HELP_FLAGS = ["--help", "--version"];
@@ -3673,8 +3679,9 @@ var FLAG_POLICY = {
3673
3679
  sort: {
3674
3680
  cluster: true,
3675
3681
  // Absent on purpose: `-o`/`--output` writes a file with no redirect for the
3676
- // redirect check to see, and `--compress-program` names a program that sort
3677
- // runs over its own temporary files — a permitted binary as an interpreter.
3682
+ // redirect check to see, `-T`/`--temporary-directory` names where sort writes
3683
+ // its run files, and `--compress-program` names a program that sort runs
3684
+ // over its own temporary files — a permitted binary as an interpreter.
3678
3685
  booleans: [
3679
3686
  "-b",
3680
3687
  "-c",
@@ -3713,11 +3720,9 @@ var FLAG_POLICY = {
3713
3720
  "-k",
3714
3721
  "-t",
3715
3722
  "-S",
3716
- "-T",
3717
3723
  "--key",
3718
3724
  "--field-separator",
3719
3725
  "--buffer-size",
3720
- "--temporary-directory",
3721
3726
  "--random-source",
3722
3727
  "--batch-size",
3723
3728
  "--parallel",
@@ -4344,23 +4349,28 @@ function scanFlags(seg) {
4344
4349
  const booleans = /* @__PURE__ */ new Set([...spec.booleans ?? [], ...UNIVERSAL_FLAGS]);
4345
4350
  const values = new Set(spec.values ?? []);
4346
4351
  const operands = [];
4352
+ const operandAt = [];
4353
+ const operand = (at) => {
4354
+ operands.push(seg.args[at]);
4355
+ operandAt.push(at);
4356
+ };
4347
4357
  let i = 0;
4348
4358
  while (i < seg.args.length) {
4349
4359
  const token = seg.args[i];
4350
4360
  if (token === "--" && !spec.expressionArgs) {
4351
- operands.push(...seg.args.slice(i + 1));
4361
+ for (let j = i + 1; j < seg.args.length; j++) operand(j);
4352
4362
  break;
4353
4363
  }
4354
4364
  if (!/^-./.test(token)) {
4355
- operands.push(token);
4365
+ operand(i);
4356
4366
  i++;
4357
4367
  continue;
4358
4368
  }
4359
4369
  const shape = matchFlag(spec, booleans, values, token);
4360
- if (shape === void 0) return { unknown: flagLabel(token), operands };
4370
+ if (shape === void 0) return { unknown: flagLabel(token), operands, operandAt };
4361
4371
  i += shape === "value" && takesNextToken(seg.args[i + 1]) ? 2 : 1;
4362
4372
  }
4363
- return { operands };
4373
+ return { operands, operandAt };
4364
4374
  }
4365
4375
  function subcommandOf(seg) {
4366
4376
  const scan = scanFlags(seg);
@@ -4513,6 +4523,8 @@ function sedRefusalFor(binary, scripts) {
4513
4523
  return `The "${binary}" script runs a shell command (the "e" command or the "e" flag of "s"), which this classifier cannot inspect. ${INSPECT_OR_TASK}`;
4514
4524
  case "write":
4515
4525
  return `The "${binary}" script writes a file (the "w"/"W" command or the "w" flag of "s"). You are a read-only planner \u2014 print the output instead, or describe the write as a task.`;
4526
+ case "read":
4527
+ return `The "${binary}" script reads a file it names itself (the "r"/"R" command), which path confinement cannot see. Read that file directly instead.`;
4516
4528
  default:
4517
4529
  return `This classifier could not read the "${binary}" script well enough to rule out a shell command or a file write in it. Rewrite it with plain print, delete and substitute commands, or describe the work as a task.`;
4518
4530
  }
@@ -4633,6 +4645,7 @@ function sedScriptHazard(script) {
4633
4645
  if (cmd === void 0) return "unreadable";
4634
4646
  if (cmd === "e") return "exec";
4635
4647
  if (cmd === "w" || cmd === "W") return "write";
4648
+ if (cmd === "r" || cmd === "R") return "read";
4636
4649
  if (cmd === "{" || cmd === "}") continue;
4637
4650
  if (cmd === "#") {
4638
4651
  while (i < n && script[i] !== "\n") i++;
@@ -4654,7 +4667,7 @@ function sedScriptHazard(script) {
4654
4667
  if (!commandEnds()) return "unreadable";
4655
4668
  continue;
4656
4669
  }
4657
- if ("aicrR".includes(cmd)) {
4670
+ if ("aic".includes(cmd)) {
4658
4671
  while (i < n && script[i] !== "\n") i += script[i] === "\\" ? 2 : 1;
4659
4672
  continue;
4660
4673
  }
@@ -4754,6 +4767,12 @@ function awkRefusalFor(binary, programs) {
4754
4767
  return `The "${binary}" program pipes to or from a command ("|"), which runs it through a shell this classifier cannot inspect. ${INSPECT_OR_TASK}`;
4755
4768
  case "write":
4756
4769
  return `The "${binary}" program redirects its output to a file (">"). You are a read-only planner \u2014 print to standard output instead, or describe the write as a task.`;
4770
+ case "read":
4771
+ return `The "${binary}" program reads a file it names itself ("getline <" or an "ARGV" entry), which path confinement cannot see. Name the file as an operand instead.`;
4772
+ case "environment":
4773
+ return `The "${binary}" program reads the environment ("ENVIRON"), which holds credentials. Run printenv for the one variable you need, which asks first.`;
4774
+ case "symbol-table":
4775
+ return `The "${binary}" program uses "SYMTAB", which gawk reads as every global by name, "ENVIRON" and "ARGV" included. Name the variable you need directly.`;
4757
4776
  case "extension":
4758
4777
  return `The "${binary}" program uses an "@" form, which loads an extension, includes a source file or calls a function by name \u2014 none of which this classifier can inspect. ${INSPECT_OR_TASK}`;
4759
4778
  default:
@@ -4795,6 +4814,7 @@ function awkProgramHazard(program) {
4795
4814
  let slash = "regex";
4796
4815
  let depth = 0;
4797
4816
  let printDepth;
4817
+ let getlineDepth;
4798
4818
  let continues = false;
4799
4819
  const readRegex = () => {
4800
4820
  i++;
@@ -4861,7 +4881,10 @@ function awkProgramHazard(program) {
4861
4881
  continue;
4862
4882
  }
4863
4883
  if (c === "\n" || c === ";") {
4864
- if (c === ";" || !continues) printDepth = void 0;
4884
+ if (c === ";" || !continues) {
4885
+ printDepth = void 0;
4886
+ getlineDepth = void 0;
4887
+ }
4865
4888
  slash = "regex";
4866
4889
  i++;
4867
4890
  continue;
@@ -4912,6 +4935,12 @@ function awkProgramHazard(program) {
4912
4935
  slash = "unclear";
4913
4936
  continue;
4914
4937
  }
4938
+ if (c === "<") {
4939
+ if (getlineDepth !== void 0 && depth >= getlineDepth) return "read";
4940
+ i++;
4941
+ slash = "unclear";
4942
+ continue;
4943
+ }
4915
4944
  if (c === "(" || c === "[") {
4916
4945
  depth++;
4917
4946
  i++;
@@ -4920,12 +4949,16 @@ function awkProgramHazard(program) {
4920
4949
  }
4921
4950
  if (c === ")" || c === "]") {
4922
4951
  depth--;
4952
+ if (getlineDepth !== void 0 && depth < getlineDepth) getlineDepth = void 0;
4923
4953
  i++;
4924
4954
  slash = "divide";
4925
4955
  continue;
4926
4956
  }
4927
4957
  if (c === "{" || c === "}") {
4928
- if (c === "}") printDepth = void 0;
4958
+ if (c === "}") {
4959
+ printDepth = void 0;
4960
+ getlineDepth = void 0;
4961
+ }
4929
4962
  i++;
4930
4963
  slash = "regex";
4931
4964
  continue;
@@ -4946,6 +4979,10 @@ function awkProgramHazard(program) {
4946
4979
  while (i < n && /[A-Za-z0-9_.]/.test(program[i])) i++;
4947
4980
  const word = program.slice(start, i);
4948
4981
  if (word === "system") return "exec";
4982
+ if (word === "ENVIRON") return "environment";
4983
+ if (word === "ARGV") return "read";
4984
+ if (word === "SYMTAB") return "symbol-table";
4985
+ if (word === "getline") getlineDepth = depth;
4949
4986
  if (word === "print" || word === "printf") printDepth = depth;
4950
4987
  slash = AWK_KEYWORDS.includes(word) ? "unclear" : "divide";
4951
4988
  continue;
@@ -5832,6 +5869,36 @@ var GIT_READONLY_SUBCOMMANDS = [
5832
5869
  "whatchanged",
5833
5870
  "cat-file"
5834
5871
  ];
5872
+ var onlyAction = (...reads) => (after) => after[0] === void 0 || reads.includes(after[0]);
5873
+ var GIT_PROMPTABLE = {
5874
+ "ls-remote": () => true,
5875
+ "for-each-ref": () => true,
5876
+ "show-ref": () => true,
5877
+ "name-rev": () => true,
5878
+ "merge-base": () => true,
5879
+ "count-objects": () => true,
5880
+ "cherry": () => true,
5881
+ "range-diff": () => true,
5882
+ "diff-tree": () => true,
5883
+ "diff-files": () => true,
5884
+ "diff-index": () => true,
5885
+ "show-branch": () => true,
5886
+ "annotate": () => true,
5887
+ "check-ignore": () => true,
5888
+ "check-attr": () => true,
5889
+ "check-ref-format": () => true,
5890
+ "check-mailmap": () => true,
5891
+ "var": () => true,
5892
+ "version": () => true,
5893
+ "worktree": onlyAction("list"),
5894
+ "notes": onlyAction("list", "show"),
5895
+ "bisect": onlyAction("log"),
5896
+ "sparse-checkout": onlyAction("list"),
5897
+ "reflog": onlyAction("show", "exists"),
5898
+ "symbolic-ref": (after) => after.filter((a) => !a.startsWith("-")).length <= 1,
5899
+ // `-w` writes the object; git's shared flag set keeps it for `git diff -w`.
5900
+ "hash-object": (after) => !after.some((a) => /^-[^-]*w/.test(a))
5901
+ };
5835
5902
  var REFUSED_COMMANDS = [
5836
5903
  "rm",
5837
5904
  "rmdir",
@@ -5923,7 +5990,28 @@ var REFUSED_SUBCOMMANDS = {
5923
5990
  "clone",
5924
5991
  "fetch",
5925
5992
  "pull",
5926
- "submodule"
5993
+ "submodule",
5994
+ "add",
5995
+ "rm",
5996
+ "mv",
5997
+ "format-patch",
5998
+ "replace",
5999
+ "mergetool",
6000
+ "repack",
6001
+ "update-index",
6002
+ "read-tree",
6003
+ "checkout-index",
6004
+ "merge-file",
6005
+ "bundle",
6006
+ "commit-tree",
6007
+ "write-tree",
6008
+ "mktree",
6009
+ "mktag",
6010
+ "unpack-objects",
6011
+ "fast-import",
6012
+ "pack-refs",
6013
+ "prune-packed",
6014
+ "maintenance"
5927
6015
  ],
5928
6016
  npm: ["publish", "unpublish", "deprecate", "owner", "access", "token", "login", "logout"],
5929
6017
  yarn: ["publish", "npm"],
@@ -6133,7 +6221,7 @@ function pathRefs(command, opts = {}) {
6133
6221
  return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
6134
6222
  const patterns = patternArgs(seg);
6135
6223
  const cwd = seg.cwd ?? [];
6136
- return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path47) => ({ path: path47, cwd }));
6224
+ return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path50) => ({ path: path50, cwd }));
6137
6225
  });
6138
6226
  }
6139
6227
  function excludedBy(command, allow, opts = {}) {
@@ -6240,7 +6328,9 @@ function patternArgs(seg) {
6240
6328
  var SCOPE_LEAD_ARGS = 2;
6241
6329
  function scopeFor(seg) {
6242
6330
  const name = seg.binaryPath ?? seg.binary;
6243
- if (!MULTIPLEXERS.includes(seg.binary.toLowerCase())) return name;
6331
+ const program = programName(seg.binary);
6332
+ if (!MULTIPLEXERS.includes(program)) return name;
6333
+ if (program === "git") return gitScope(seg, name);
6244
6334
  const lead = [];
6245
6335
  for (const arg of seg.args) {
6246
6336
  if (arg.startsWith("-")) break;
@@ -6249,6 +6339,14 @@ function scopeFor(seg) {
6249
6339
  }
6250
6340
  return [name, ...lead].join(" ");
6251
6341
  }
6342
+ function gitScope(seg, name) {
6343
+ const scan = scanFlags({ ...seg, binary: "git" });
6344
+ if (!scan || scan.unknown || scan.operands[0] === "ls-remote") return [name, ...seg.args].map(scopeWord).join(" ");
6345
+ return [name, ...scan.operands.slice(0, SCOPE_LEAD_ARGS)].join(" ");
6346
+ }
6347
+ function scopeWord(word) {
6348
+ return word === "" || /[\s"'\\]/.test(word) ? JSON.stringify(word) : word;
6349
+ }
6252
6350
  function climbsByGlob(seg) {
6253
6351
  if (!seg.globbed) return false;
6254
6352
  const patterns = patternArgs(seg);
@@ -6270,6 +6368,27 @@ function swallowsRefName(arg) {
6270
6368
  const values = FLAG_POLICY.git.values ?? [];
6271
6369
  return [...arg.slice(1)].some((c) => c !== "n" && values.includes(`-${c}`));
6272
6370
  }
6371
+ function gitArgsAfterSubcommand(seg) {
6372
+ const at = scanFlags(seg)?.operandAt[0];
6373
+ return at === void 0 ? [] : seg.args.slice(at + 1);
6374
+ }
6375
+ var REMOTE_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
6376
+ function namesConfiguredRemote(seg, after) {
6377
+ const destination2 = scanFlags({ ...seg, args: after })?.operands[0];
6378
+ return destination2 === void 0 || REMOTE_NAME.test(destination2);
6379
+ }
6380
+ function programName(binary) {
6381
+ return binary.toLowerCase().replace(/\.exe$/, "");
6382
+ }
6383
+ function isRemoteListing(seg) {
6384
+ const git2 = { ...seg, binary: programName(seg.binary) };
6385
+ return git2.binary === "git" && subcommandOf(git2) === "ls-remote";
6386
+ }
6387
+ function remoteListingTexts(seg) {
6388
+ if (!isRemoteListing(seg)) return [];
6389
+ const git2 = { ...seg, binary: "git" };
6390
+ return scanFlags({ ...git2, args: gitArgsAfterSubcommand(git2) })?.operands ?? [];
6391
+ }
6273
6392
  function refusalFor(seg, trustedResolves) {
6274
6393
  const web = webCommand(seg, trustedResolves);
6275
6394
  if (web?.reason) return web.reason;
@@ -6307,8 +6426,24 @@ function refusalFor(seg, trustedResolves) {
6307
6426
  }
6308
6427
  if (seg.binary === "git") {
6309
6428
  const sub = subcommandOf(seg);
6429
+ const after = gitArgsAfterSubcommand(seg);
6430
+ if (sub !== void 0 && !GIT_READONLY_SUBCOMMANDS.includes(sub)) {
6431
+ const reads = GIT_PROMPTABLE[sub];
6432
+ if (!reads) {
6433
+ return `"git ${sub}" is not a git subcommand known only to read, so it may write the repository. You are a read-only planner: ${EFFECT_ROUTE}`;
6434
+ }
6435
+ if (!reads(after)) {
6436
+ return `"git ${[sub, ...after].join(" ")}" is not a read-only form of "git ${sub}". You are a read-only planner: ${EFFECT_ROUTE}`;
6437
+ }
6438
+ }
6439
+ if (sub === "ls-remote" && !namesConfiguredRemote(seg, after)) {
6440
+ return `"git ls-remote" given a URL, an address or a path reaches it through git's own transport, which resolves the host itself and follows a redirect, so the origin approval, address guard and pinned connection a web read gets (ADR-0027) cannot hold for it. Name a remote the repository configures, such as "git ls-remote origin", or fetch the page with your web fetch tool.`;
6441
+ }
6442
+ if (sub === "ls-remote" && (seg.expandable || seg.globbed || after.some((a) => a.includes("~")))) {
6443
+ return `The shell rewrites part of this "git ls-remote" before git sees it (a variable, substitution, glob or "~"), so what it sends the remote, and which repository it lists, cannot be checked for a secret or approved. Write the remote and patterns literally, in single quotes where they hold a "*", and re-run.`;
6444
+ }
6310
6445
  if (sub === "branch" || sub === "tag") {
6311
- const flag2 = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a)) ?? seg.args.slice(seg.args.indexOf(sub) + 1).find(swallowsRefName);
6446
+ const flag2 = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a)) ?? after.find(swallowsRefName);
6312
6447
  if (flag2) {
6313
6448
  return `"git ${sub} ${flag2}" mutates refs. You are a read-only planner: ${EFFECT_ROUTE}`;
6314
6449
  }
@@ -6447,10 +6582,10 @@ function classifyCommand(command, opts = {}) {
6447
6582
  return { tier: "refuse", scope: "", reason: `curl and wget are refused in Full and allowlist mode. ${web.effect ? EFFECT_REFUSAL_HINT : READ_REFUSAL_HINT}` };
6448
6583
  }
6449
6584
  const allow = opts.allow;
6450
- if (web && allow && web.urls.some((url) => scopeExcluded(allow, `${seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`))) {
6585
+ if (web && allow && web.urls.some((url) => scopeExcluded(allow, `${programName(seg.binary)} ${url.origin}/*`))) {
6451
6586
  return { tier: "refuse", scope: "", reason: `The web origin is excluded by the planner allowlist. ${READ_REFUSAL_HINT}` };
6452
6587
  }
6453
- const reason = refusalFor({ ...seg, binary: seg.binary.toLowerCase() }, opts.trustedWebResolves);
6588
+ const reason = refusalFor({ ...seg, binary: programName(seg.binary) }, opts.trustedWebResolves);
6454
6589
  if (reason) return { tier: "refuse", scope: "", reason: withWrapperNote(reason, wrappers) };
6455
6590
  if (runner && !XARGS_TARGETS.includes(seg.binary)) {
6456
6591
  return {
@@ -6460,23 +6595,24 @@ function classifyCommand(command, opts = {}) {
6460
6595
  };
6461
6596
  }
6462
6597
  }
6463
- if (unwrapped.some(({ seg }) => seg.binary.toLowerCase().replace(/\.exe$/, "") === "wget") && wgetVersionCommands(command, opts.dialect).includes(void 0)) {
6598
+ if (unwrapped.some(({ seg }) => programName(seg.binary) === "wget") && wgetVersionCommands(command, opts.dialect).includes(void 0)) {
6464
6599
  return { tier: "refuse", scope: "", reason: `The wget executable cannot be checked safely in this working directory. ${READ_REFUSAL_HINT}` };
6465
6600
  }
6466
6601
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || u.wrappers.some((w) => w !== w.toLowerCase()) || !isAuto(u.seg, opts));
6467
6602
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
6603
+ const changesDirectory = unwrapped.some(({ seg }) => seg.binary.toLowerCase() === "cd");
6468
6604
  const lineScoped = nonAuto.some((u) => {
6469
6605
  const name = u.seg.binary.toLowerCase();
6470
- return name === "cd" || name !== "command" && SHELL_STATE_COMMANDS.has(name) || climbsByGlob(u.seg);
6606
+ return name === "cd" || name !== "command" && SHELL_STATE_COMMANDS.has(name) || climbsByGlob(u.seg) || isRemoteListing(u.seg) && (changesDirectory || u.wrappers.length > 0 || nonAuto.length > 1);
6471
6607
  });
6472
6608
  const webUrls = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.urls ?? []);
6473
- const webOutboundTexts = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.outboundTexts ?? []);
6609
+ const outboundTexts = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.outboundTexts ?? remoteListingTexts(u.seg));
6474
6610
  if (lineScoped && webUrls.length === 0) {
6475
- return { tier: "ask", scope: trimmed2 };
6611
+ return { tier: "ask", scope: trimmed2, ...outboundTexts.length ? { outboundTexts } : {} };
6476
6612
  }
6477
6613
  const scopes = [...new Set(nonAuto.flatMap((u) => {
6478
6614
  const web = webCommand(u.seg, opts.trustedWebResolves);
6479
- return web ? web.urls.map((url) => `${u.seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`) : [lineScoped ? trimmed2 : u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)];
6615
+ return web ? web.urls.map((url) => `${programName(u.seg.binary)} ${url.origin}/*`) : [lineScoped ? trimmed2 : u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)];
6480
6616
  }))].sort();
6481
6617
  const scope = scopes.join(" + ");
6482
6618
  const perInvocation = nonAuto.some((u) => webCommand(u.seg, opts.trustedWebResolves)?.perInvocation);
@@ -6484,7 +6620,8 @@ function classifyCommand(command, opts = {}) {
6484
6620
  return {
6485
6621
  tier: "ask",
6486
6622
  scope,
6487
- ...webUrls.length ? { webUrls, webOutboundTexts } : {},
6623
+ ...webUrls.length ? { webUrls } : {},
6624
+ ...outboundTexts.length ? { outboundTexts } : {},
6488
6625
  ...scopes.length > 1 && webUrls.length ? { scopes } : {},
6489
6626
  ...perInvocation ? { perInvocation } : {},
6490
6627
  ...allowedBy ? { allowedBy } : {}
@@ -6628,10 +6765,10 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
6628
6765
  const scope = args.include ? ` in ${args.include}` : "";
6629
6766
  return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
6630
6767
  }
6631
- const path47 = args.filePath || args.file_path || args.path;
6632
- if (path47) {
6633
- const basename6 = String(path47).split("/").pop() || String(path47);
6634
- return `${display} ${basename6}`;
6768
+ const path50 = args.filePath || args.file_path || args.path;
6769
+ if (path50) {
6770
+ const basename7 = String(path50).split("/").pop() || String(path50);
6771
+ return `${display} ${basename7}`;
6635
6772
  }
6636
6773
  if (args.pattern) {
6637
6774
  return `${display} ${args.pattern}`;
@@ -8116,7 +8253,7 @@ ${TASK_CONTINUE}`);
8116
8253
  command = disableWebCommandStartup(command, this.researchShell.dialect);
8117
8254
  const { approval, root } = site;
8118
8255
  const allow = approval.allowlist?.();
8119
- const { tier, scope, reason, allowedBy, perInvocation, scopes: commandScopes, webUrls, webOutboundTexts } = classifyCommand(command, {
8256
+ const { tier, scope, reason, allowedBy, perInvocation, scopes: commandScopes, webUrls, outboundTexts } = classifyCommand(command, {
8120
8257
  ...this.policyOptions(),
8121
8258
  ...approval.commandPolicy?.(),
8122
8259
  ...allow ? { allow } : {}
@@ -8141,8 +8278,9 @@ ${TASK_CONTINUE}`);
8141
8278
  if (site.task?.sealed) return this.runSealed(site, command, request, "after", signal);
8142
8279
  if (tier === "refuse") return this.sealedInstead(site, command, request, refusal(`Command refused: ${reason}${site.task ? `
8143
8280
  ${TASK_UNSEALED} ${TASK_CONTINUE}` : ""}`), signal);
8144
- if (webUrls?.length && carriesSecret([...webOutboundTexts ?? [], ...webUrls.map((url) => url.href)], [...this.outboundSecrets(), ...sensitiveEnvValues(process.env)])) {
8145
- return refusal("Command refused: a web command contains what looks like a secret. Fetch the page with your web fetch tool, without credentials.");
8281
+ const outbound = [...outboundTexts ?? [], ...(webUrls ?? []).map((url) => url.href)];
8282
+ if (outbound.length && carriesSecret(outbound, [...this.outboundSecrets(), ...sensitiveEnvValues(process.env)])) {
8283
+ return refusal(webUrls?.length ? "Command refused: a web command contains what looks like a secret. Fetch the page with your web fetch tool, without credentials." : "Command refused: what this command sends over the network contains what looks like a secret. Run it without the value.");
8146
8284
  }
8147
8285
  const vettedAddresses = /* @__PURE__ */ new Map();
8148
8286
  for (const url of webUrls ?? []) {
@@ -8245,7 +8383,7 @@ ${outcome.output}${outcome.success ? "" : `
8245
8383
  }
8246
8384
  let shell;
8247
8385
  try {
8248
- const sealed2 = site.task?.gitBefore ? { ...site.sealed, gitEntries: site.task.gitBefore.entries.map(({ path: path47, directory }) => ({ path: path47, directory })) } : site.sealed;
8386
+ const sealed2 = site.task?.gitBefore ? { ...site.sealed, gitEntries: site.task.gitBefore.entries.map(({ path: path50, directory }) => ({ path: path50, directory })) } : site.sealed;
8249
8387
  shell = await site.sandbox.launchSealed(this.researchShell, sealed2);
8250
8388
  } catch (err) {
8251
8389
  return { refused: refusal(`Command not run: its sealed sandbox could not be prepared (${err instanceof Error ? err.message : String(err)}).`) };
@@ -9794,8 +9932,8 @@ function firstLineOf(command) {
9794
9932
  function text(value) {
9795
9933
  return typeof value === "string" ? value : "";
9796
9934
  }
9797
- function searchArg(pattern, path47, include = "") {
9798
- return `${pattern}${path47 ? ` in ${path47}` : ""}${include ? ` (${include})` : ""}`;
9935
+ function searchArg(pattern, path50, include = "") {
9936
+ return `${pattern}${path50 ? ` in ${path50}` : ""}${include ? ` (${include})` : ""}`;
9799
9937
  }
9800
9938
  function hintArg(args) {
9801
9939
  for (const field of HINT_FIELDS) {
@@ -9900,8 +10038,8 @@ function userMessage(id2, text6, skills, { turnId, forced } = {}) {
9900
10038
  ...forced ? { forced: true } : {}
9901
10039
  };
9902
10040
  }
9903
- function skillLoadNotice({ invokedBy, name, source, path: path47, attaches }) {
9904
- return { invokedBy, name, source, path: path47, ...attaches ? { attaches } : {} };
10041
+ function skillLoadNotice({ invokedBy, name, source, path: path50, attaches }) {
10042
+ return { invokedBy, name, source, path: path50, ...attaches ? { attaches } : {} };
9905
10043
  }
9906
10044
  var LOAD_SKILL_TOOL = /(?:^|_)load_skill$/;
9907
10045
  function surfaceStep(step) {
@@ -9918,8 +10056,8 @@ function loadedSkillName(args) {
9918
10056
  return void 0;
9919
10057
  }
9920
10058
  }
9921
- function skillLoadBlock(id2, { invokedBy, name, source, path: path47, attaches }, turnId) {
9922
- return { type: "skill_load", id: id2, invokedBy, name, source, path: path47, ...attaches ? { attaches } : {}, ...turnId ? { turnId } : {} };
10059
+ function skillLoadBlock(id2, { invokedBy, name, source, path: path50, attaches }, turnId) {
10060
+ return { type: "skill_load", id: id2, invokedBy, name, source, path: path50, ...attaches ? { attaches } : {}, ...turnId ? { turnId } : {} };
9923
10061
  }
9924
10062
  function subagentBlock(id2, { toolCallId, model, usage, turnId, ...fields }) {
9925
10063
  return {
@@ -10693,6 +10831,7 @@ function enabledRunners(cfg) {
10693
10831
  }
10694
10832
 
10695
10833
  // src/plugins/builtin/claudeModels.ts
10834
+ var path13 = __toESM(require("path"));
10696
10835
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
10697
10836
  var ANTHROPIC_VARIANT_LABELS = {
10698
10837
  adaptive: "Adaptive",
@@ -10786,6 +10925,53 @@ function parseClaudeHelp(stdout) {
10786
10925
  }
10787
10926
  return models;
10788
10927
  }
10928
+ function claudeConfigDir(env) {
10929
+ return env.CLAUDE_CONFIG_DIR?.trim() || "~/.claude";
10930
+ }
10931
+ function claudeManagedSettingsDir(platform = process.platform) {
10932
+ if (platform === "darwin") return "/Library/Application Support/ClaudeCode";
10933
+ if (platform === "win32") return "C:\\Program Files\\ClaudeCode";
10934
+ return "/etc/claude-code";
10935
+ }
10936
+ function readClaudeSettingsModels(files, platform = process.platform) {
10937
+ const managed = claudeManagedSettingsDir(platform);
10938
+ const join46 = platform === "win32" ? path13.win32.join : path13.posix.join;
10939
+ const dropIns = files.list(join46(managed, "managed-settings.d")).filter((name) => name.endsWith(".json") && !name.startsWith(".")).sort().map((name) => join46(managed, "managed-settings.d", name));
10940
+ const ranked = [...dropIns.reverse(), join46(managed, "managed-settings.json"), join46(claudeConfigDir(files.env), "settings.json")];
10941
+ for (const file of ranked) {
10942
+ const content = files.read(file);
10943
+ const lineup = content === null ? null : parseClaudeSettingsModels(content);
10944
+ if (lineup) return lineup;
10945
+ }
10946
+ return [];
10947
+ }
10948
+ function parseClaudeSettingsModels(content) {
10949
+ let parsed;
10950
+ try {
10951
+ parsed = JSON.parse(content);
10952
+ } catch {
10953
+ return null;
10954
+ }
10955
+ const picker = parsed && typeof parsed === "object" ? parsed.modelPicker : void 0;
10956
+ const options = picker && typeof picker === "object" ? picker.options : void 0;
10957
+ if (!Array.isArray(options)) return null;
10958
+ const seen = /* @__PURE__ */ new Set();
10959
+ const models = [];
10960
+ for (const row of options) {
10961
+ if (!row || typeof row !== "object") continue;
10962
+ const { model: raw, label } = row;
10963
+ const model = typeof raw === "string" ? raw.trim() : "";
10964
+ if (!model || seen.has(model)) continue;
10965
+ seen.add(model);
10966
+ models.push({
10967
+ modelId: model,
10968
+ modelLabel: typeof label === "string" && label.trim() ? label.trim() : model,
10969
+ runnerProvider: model.includes("/") ? model.split("/")[0] : void 0,
10970
+ variants: []
10971
+ });
10972
+ }
10973
+ return models;
10974
+ }
10789
10975
 
10790
10976
  // src/plugins/builtin/claude-code.manifest.ts
10791
10977
  var TURN_NOT_LOGGED_IN = /not logged in\s*·\s*please run \/login/i;
@@ -10824,18 +11010,24 @@ var CLAUDE_CODE_MANIFEST = {
10824
11010
  // 3. canonicalAliases — stable --model contracts (opus/sonnet/haiku/fable)
10825
11011
  // merged into any successful result to fill gaps, and used as the full
10826
11012
  // last-resort list when both API and CLI are unavailable.
11013
+ //
11014
+ // Whichever source answers, the models the user added to Claude's own
11015
+ // picker (`modelPicker.options` in its managed or user settings, usually
11016
+ // gateway IDs) are appended (settingsModels): the CLI accepts them as
11017
+ // `--model` values though no catalog above lists them.
10827
11018
  apiDiscovery: {
10828
11019
  url: "https://api.anthropic.com/v1/models?limit=100",
10829
11020
  headers: { "anthropic-version": "2023-06-01" },
10830
11021
  auth: [
10831
11022
  { type: "env", varName: "ANTHROPIC_API_KEY", header: "X-Api-Key" },
10832
- { type: "file", path: "~/.claude/.credentials.json", jsonPath: "claudeAiOauth.accessToken", header: "Authorization", prefix: "Bearer " }
11023
+ { type: "file", path: (env) => `${claudeConfigDir(env)}/.credentials.json`, jsonPath: "claudeAiOauth.accessToken", header: "Authorization", prefix: "Bearer " }
10833
11024
  ],
10834
11025
  parser: parseAnthropicModels
10835
11026
  },
10836
11027
  discoveryCommands: [
10837
11028
  { command: "claude", args: ["--help"], parser: parseClaudeHelp }
10838
11029
  ],
11030
+ settingsModels: (files) => readClaudeSettingsModels(files),
10839
11031
  canonicalAliases: [
10840
11032
  { modelId: "opus", modelLabel: "Opus" },
10841
11033
  { modelId: "sonnet", modelLabel: "Sonnet" },
@@ -10893,7 +11085,7 @@ var import_path5 = require("path");
10893
11085
 
10894
11086
  // src/services/harness/repoConfig.ts
10895
11087
  var import_fs6 = require("fs");
10896
- var path13 = __toESM(require("path"));
11088
+ var path14 = __toESM(require("path"));
10897
11089
 
10898
11090
  // src/services/mcp/OrdewellMcpServer.ts
10899
11091
  var import_http = require("http");
@@ -11857,12 +12049,12 @@ function checkpointReply(answer2) {
11857
12049
  return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
11858
12050
  }
11859
12051
  }
11860
- function fieldPath(path47) {
11861
- return path47.reduce((text6, part) => typeof part === "number" ? `${text6}[${part}]` : text6 ? `${text6}.${String(part)}` : String(part), "");
12052
+ function fieldPath(path50) {
12053
+ return path50.reduce((text6, part) => typeof part === "number" ? `${text6}[${part}]` : text6 ? `${text6}.${String(part)}` : String(part), "");
11862
12054
  }
11863
- function valueAt(value, path47) {
12055
+ function valueAt(value, path50) {
11864
12056
  let current = value;
11865
- for (const part of path47) {
12057
+ for (const part of path50) {
11866
12058
  if (typeof part === "number") {
11867
12059
  if (!Array.isArray(current)) return void 0;
11868
12060
  current = current[part];
@@ -11884,7 +12076,7 @@ function boundedRefusal(issue, args) {
11884
12076
  var LEAKED_PARAMETER = /<\/parameter>|<parameter\s+name\s*=\s*"([^"]*)">/g;
11885
12077
  function leakedMarkupIn(value) {
11886
12078
  const found = [];
11887
- const walk = (node, path47) => {
12079
+ const walk = (node, path50) => {
11888
12080
  if (typeof node === "string") {
11889
12081
  const names = [];
11890
12082
  let closing = false;
@@ -11892,25 +12084,25 @@ function leakedMarkupIn(value) {
11892
12084
  if (match[1] !== void 0) names.push(match[1]);
11893
12085
  else closing = true;
11894
12086
  }
11895
- if (names.length > 0 || closing) found.push({ path: path47, names: [...new Set(names)], closing });
12087
+ if (names.length > 0 || closing) found.push({ path: path50, names: [...new Set(names)], closing });
11896
12088
  return;
11897
12089
  }
11898
12090
  if (Array.isArray(node)) {
11899
- node.forEach((item, i) => walk(item, `${path47}[${i}]`));
12091
+ node.forEach((item, i) => walk(item, `${path50}[${i}]`));
11900
12092
  return;
11901
12093
  }
11902
12094
  if (node !== null && typeof node === "object") {
11903
- for (const [key, item] of Object.entries(node)) walk(item, path47 ? `${path47}.${key}` : key);
12095
+ for (const [key, item] of Object.entries(node)) walk(item, path50 ? `${path50}.${key}` : key);
11904
12096
  }
11905
12097
  };
11906
12098
  walk(value, "");
11907
12099
  return found;
11908
12100
  }
11909
12101
  function markupRefusal(leaks) {
11910
- return leaks.map(({ path: path47, names, closing }) => {
12102
+ return leaks.map(({ path: path50, names, closing }) => {
11911
12103
  const tags = [...names.map((n) => `<parameter name="${n}">`), ...closing ? ["</parameter>"] : []];
11912
- const guide = names.length > 0 ? ` The parameters ${names.map((n) => JSON.stringify(n)).join(", ")} are inside it; pass them as separate arguments, not inside ${path47}.` : " Pass each argument separately, not inside one string.";
11913
- return `${path47} contains leaked tool-call markup: ${tags.join(", ")}.${guide}`;
12104
+ const guide = names.length > 0 ? ` The parameters ${names.map((n) => JSON.stringify(n)).join(", ")} are inside it; pass them as separate arguments, not inside ${path50}.` : " Pass each argument separately, not inside one string.";
12105
+ return `${path50} contains leaked tool-call markup: ${tags.join(", ")}.${guide}`;
11914
12106
  }).join("\n");
11915
12107
  }
11916
12108
  function invalidArguments(name, error, args) {
@@ -12375,9 +12567,9 @@ function mcpClientConfig(credential) {
12375
12567
  }
12376
12568
  function ownerOnlyConfigFile(fileName, contents) {
12377
12569
  const dir = (0, import_fs5.mkdtempSync)((0, import_path4.join)((0, import_os.tmpdir)(), "ordewell-mcp-"));
12378
- const path47 = (0, import_path4.join)(dir, fileName);
12379
- (0, import_fs5.writeFileSync)(path47, contents, { mode: 384 });
12380
- return { path: path47, remove: () => (0, import_fs5.rmSync)(dir, { recursive: true, force: true }) };
12570
+ const path50 = (0, import_path4.join)(dir, fileName);
12571
+ (0, import_fs5.writeFileSync)(path50, contents, { mode: 384 });
12572
+ return { path: path50, remove: () => (0, import_fs5.rmSync)(dir, { recursive: true, force: true }) };
12381
12573
  }
12382
12574
 
12383
12575
  // src/services/harness/repoConfig.ts
@@ -12400,19 +12592,19 @@ function onDisk(target) {
12400
12592
  try {
12401
12593
  return import_fs6.realpathSync.native(target);
12402
12594
  } catch {
12403
- return path13.resolve(target);
12595
+ return path14.resolve(target);
12404
12596
  }
12405
12597
  }
12406
12598
  function insideOnDisk(root, target) {
12407
- const rel = path13.relative(onDisk(root), onDisk(target));
12408
- return rel === "" || !rel.startsWith("..") && !path13.isAbsolute(rel);
12599
+ const rel = path14.relative(onDisk(root), onDisk(target));
12600
+ return rel === "" || !rel.startsWith("..") && !path14.isAbsolute(rel);
12409
12601
  }
12410
12602
  function repoLevels(workspace) {
12411
12603
  const levels = [];
12412
- for (let dir = path13.resolve(workspace); ; dir = path13.dirname(dir)) {
12604
+ for (let dir = path14.resolve(workspace); ; dir = path14.dirname(dir)) {
12413
12605
  levels.unshift(dir);
12414
- if ((0, import_fs6.existsSync)(path13.join(dir, ".git"))) return levels;
12415
- if (path13.dirname(dir) === dir) return [path13.resolve(workspace)];
12606
+ if ((0, import_fs6.existsSync)(path14.join(dir, ".git"))) return levels;
12607
+ if (path14.dirname(dir) === dir) return [path14.resolve(workspace)];
12416
12608
  }
12417
12609
  }
12418
12610
  function repoRoot(workspace) {
@@ -12506,33 +12698,33 @@ var TomlReader = class {
12506
12698
  if (this.i >= this.src.length) return root;
12507
12699
  if (this.src.startsWith("[[", this.i)) {
12508
12700
  this.i += 2;
12509
- const path47 = this.key();
12701
+ const path50 = this.key();
12510
12702
  this.expect("]]");
12511
- const parent = this.tableAt(root, path47.slice(0, -1));
12512
- const last = path47[path47.length - 1];
12703
+ const parent = this.tableAt(root, path50.slice(0, -1));
12704
+ const last = path50[path50.length - 1];
12513
12705
  const list = Object.hasOwn(parent, last) ? parent[last] : parent[last] = [];
12514
- if (!Array.isArray(list)) throw this.fail(`${path47.join(".")} is not an array of tables`);
12706
+ if (!Array.isArray(list)) throw this.fail(`${path50.join(".")} is not an array of tables`);
12515
12707
  table = newTable();
12516
12708
  list.push(table);
12517
12709
  } else if (this.src[this.i] === "[") {
12518
12710
  this.i += 1;
12519
- const path47 = this.key();
12711
+ const path50 = this.key();
12520
12712
  this.expect("]");
12521
- table = this.tableAt(root, path47);
12713
+ table = this.tableAt(root, path50);
12522
12714
  } else {
12523
- const path47 = this.key();
12715
+ const path50 = this.key();
12524
12716
  this.skipBlank(false);
12525
12717
  this.expect("=");
12526
12718
  this.skipBlank(false);
12527
- const owner = this.tableAt(table, path47.slice(0, -1));
12528
- owner[path47[path47.length - 1]] = this.value();
12719
+ const owner = this.tableAt(table, path50.slice(0, -1));
12720
+ owner[path50[path50.length - 1]] = this.value();
12529
12721
  }
12530
12722
  this.endOfLine();
12531
12723
  }
12532
12724
  }
12533
- tableAt(from, path47) {
12725
+ tableAt(from, path50) {
12534
12726
  let table = from;
12535
- for (const part of path47) {
12727
+ for (const part of path50) {
12536
12728
  const next = Object.hasOwn(table, part) ? table[part] : void 0;
12537
12729
  if (next === void 0) table = table[part] = newTable();
12538
12730
  else if (isRecord2(next)) table = next;
@@ -12601,10 +12793,10 @@ var TomlReader = class {
12601
12793
  this.i++;
12602
12794
  return table;
12603
12795
  }
12604
- const path47 = this.key();
12796
+ const path50 = this.key();
12605
12797
  this.expect("=");
12606
12798
  this.skipBlank(false);
12607
- this.tableAt(table, path47.slice(0, -1))[path47[path47.length - 1]] = this.value();
12799
+ this.tableAt(table, path50.slice(0, -1))[path50[path50.length - 1]] = this.value();
12608
12800
  this.skipBlank(true);
12609
12801
  if (this.src[this.i] === ",") this.i++;
12610
12802
  else if (this.src[this.i] !== "}") throw this.fail("expected , or } in an inline table");
@@ -12749,7 +12941,8 @@ var CODEX_MANIFEST = {
12749
12941
  appServer: {
12750
12942
  command: "codex",
12751
12943
  args: ["app-server"],
12752
- cacheFile: "~/.codex/models_cache.json"
12944
+ // CODEX_HOME moves all of Codex's own files from `~/.codex`.
12945
+ cacheFile: (env) => `${env.CODEX_HOME?.trim() || "~/.codex"}/models_cache.json`
12753
12946
  },
12754
12947
  canonicalAliases: [
12755
12948
  { modelId: "gpt-5.6-sol", modelLabel: "GPT-5.6-Sol" },
@@ -12978,8 +13171,7 @@ var LineBuffer = class {
12978
13171
  // src/services/harness/ClaudeCodeAdapter.ts
12979
13172
  var import_crypto6 = require("crypto");
12980
13173
  var import_fs11 = require("fs");
12981
- var os9 = __toESM(require("os"));
12982
- var path21 = __toESM(require("path"));
13174
+ var path23 = __toESM(require("path"));
12983
13175
 
12984
13176
  // src/plugins/resolveArgs.ts
12985
13177
  function claudeThinkingArgs(effort) {
@@ -13017,14 +13209,29 @@ function resolveTaskRunnerFlags(manifest, ctx) {
13017
13209
  return flags;
13018
13210
  }
13019
13211
 
13212
+ // src/utils/runnerHome.ts
13213
+ var os5 = __toESM(require("os"));
13214
+ function runnerHomeDir(env, platform = process.platform) {
13215
+ const name = platform === "win32" ? "USERPROFILE" : "HOME";
13216
+ const key = platform === "win32" ? Object.keys(env).filter((k) => k.toUpperCase() === name).pop() : name;
13217
+ const home = key === void 0 ? void 0 : env[key];
13218
+ return home ?? os5.homedir();
13219
+ }
13220
+ function expandRunnerHome(filePath, env = process.env) {
13221
+ if (!filePath.startsWith("~")) return filePath;
13222
+ const home = runnerHomeDir(env);
13223
+ return home ? home + filePath.slice(1) : null;
13224
+ }
13225
+
13020
13226
  // src/services/harness/runnerProcess.ts
13227
+ var import_net = require("net");
13021
13228
  var import_string_decoder = require("string_decoder");
13022
13229
 
13023
13230
  // src/utils/shellPath.ts
13024
13231
  var import_child_process3 = require("child_process");
13025
13232
  var import_util = require("util");
13026
- var os5 = __toESM(require("os"));
13027
- var path14 = __toESM(require("path"));
13233
+ var os6 = __toESM(require("os"));
13234
+ var path15 = __toESM(require("path"));
13028
13235
  var execAsync = (0, import_util.promisify)(import_child_process3.exec);
13029
13236
  var LOGIN_SHELL_TIMEOUT_MS = 5e3;
13030
13237
  var PATH_MARKER = "__ORDEWELL_PATH__";
@@ -13033,42 +13240,42 @@ var defaultExec = async (command, options) => {
13033
13240
  return { stdout: String(stdout) };
13034
13241
  };
13035
13242
  function posixBinDirs() {
13036
- const home = os5.homedir();
13243
+ const home = os6.homedir();
13037
13244
  return [
13038
- path14.join(home, ".local", "bin"),
13039
- path14.join(home, ".opencode", "bin"),
13040
- path14.join(home, ".claude", "bin"),
13041
- path14.join(home, ".bun", "bin"),
13042
- path14.join(home, ".volta", "bin"),
13043
- path14.join(home, ".npm-global", "bin"),
13044
- path14.join(home, "bin"),
13245
+ path15.join(home, ".local", "bin"),
13246
+ path15.join(home, ".opencode", "bin"),
13247
+ path15.join(home, ".claude", "bin"),
13248
+ path15.join(home, ".bun", "bin"),
13249
+ path15.join(home, ".volta", "bin"),
13250
+ path15.join(home, ".npm-global", "bin"),
13251
+ path15.join(home, "bin"),
13045
13252
  "/usr/local/bin",
13046
13253
  "/opt/homebrew/bin"
13047
13254
  ];
13048
13255
  }
13049
13256
  function windowsBinDirs(env, home) {
13050
- const join44 = (...parts) => path14.win32.join(...parts);
13051
- const under = (base, ...parts) => base ? [join44(base, ...parts)] : [];
13257
+ const join46 = (...parts) => path15.win32.join(...parts);
13258
+ const under = (base, ...parts) => base ? [join46(base, ...parts)] : [];
13052
13259
  const appData = env.APPDATA;
13053
13260
  const localAppData = env.LOCALAPPDATA;
13054
13261
  const programData = env.ProgramData;
13055
13262
  return [
13056
13263
  // Native per-user installers, PowerShell-driven and otherwise.
13057
- join44(home, ".local", "bin"),
13058
- join44(home, ".opencode", "bin"),
13059
- join44(home, ".claude", "bin"),
13060
- join44(home, ".codex", "bin"),
13264
+ join46(home, ".local", "bin"),
13265
+ join46(home, ".opencode", "bin"),
13266
+ join46(home, ".claude", "bin"),
13267
+ join46(home, ".codex", "bin"),
13061
13268
  ...under(localAppData, "Programs", "claude"),
13062
13269
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
13063
13270
  // literal spelling is kept as a fallback for a host that does not export
13064
13271
  // APPDATA (a service account, a stripped CI container).
13065
13272
  ...under(appData, "npm"),
13066
- join44(home, "AppData", "Roaming", "npm"),
13273
+ join46(home, "AppData", "Roaming", "npm"),
13067
13274
  ...under(localAppData, "pnpm"),
13068
13275
  ...under(localAppData, "Yarn", "bin"),
13069
- join44(home, ".bun", "bin"),
13276
+ join46(home, ".bun", "bin"),
13070
13277
  // Windows package managers.
13071
- join44(home, "scoop", "shims"),
13278
+ join46(home, "scoop", "shims"),
13072
13279
  ...under(env.SCOOP, "shims"),
13073
13280
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
13074
13281
  ...under(programData, "chocolatey", "bin"),
@@ -13078,13 +13285,13 @@ function windowsBinDirs(env, home) {
13078
13285
  ...under(localAppData, "Microsoft", "WindowsApps"),
13079
13286
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
13080
13287
  ...under(localAppData, "Volta", "bin"),
13081
- join44(home, ".volta", "bin")
13288
+ join46(home, ".volta", "bin")
13082
13289
  ];
13083
13290
  }
13084
13291
  function wellKnownBinDirs(deps = {}) {
13085
13292
  const platform = deps.platform ?? process.platform;
13086
13293
  if (platform !== "win32") return posixBinDirs();
13087
- return [...new Set(windowsBinDirs(deps.env ?? process.env, deps.home ?? os5.homedir()))];
13294
+ return [...new Set(windowsBinDirs(deps.env ?? process.env, deps.home ?? os6.homedir()))];
13088
13295
  }
13089
13296
  async function loginShellPath(execImpl) {
13090
13297
  if (process.platform === "win32") return null;
@@ -13107,14 +13314,14 @@ function augmentedPath(execImpl = defaultExec) {
13107
13314
  const segments = [];
13108
13315
  const push = (p) => {
13109
13316
  if (!p) return;
13110
- for (const seg of p.split(path14.delimiter)) {
13317
+ for (const seg of p.split(path15.delimiter)) {
13111
13318
  if (seg && !segments.includes(seg)) segments.push(seg);
13112
13319
  }
13113
13320
  };
13114
13321
  push(process.env.PATH);
13115
13322
  push(await loginShellPath(execImpl));
13116
13323
  for (const dir of wellKnownBinDirs()) push(dir);
13117
- return segments.join(path14.delimiter);
13324
+ return segments.join(path15.delimiter);
13118
13325
  })();
13119
13326
  }
13120
13327
  return cachedPath;
@@ -13136,7 +13343,7 @@ function withPath(base, resolvedPath, overrides = {}) {
13136
13343
 
13137
13344
  // src/utils/launch.ts
13138
13345
  var fsSync4 = __toESM(require("fs"));
13139
- var path15 = __toESM(require("path"));
13346
+ var path16 = __toESM(require("path"));
13140
13347
  var DIRECT_EXTENSIONS = [".exe", ".com"];
13141
13348
  var BATCH_EXTENSIONS = [".cmd", ".bat"];
13142
13349
  var POWERSHELL_EXTENSIONS = [".ps1"];
@@ -13144,7 +13351,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
13144
13351
  var WINDOWS_MAX_COMMAND_LINE = 32767;
13145
13352
  function defaultPowerShell() {
13146
13353
  const root = process.env.SystemRoot || process.env.windir;
13147
- return root ? path15.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
13354
+ return root ? path16.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
13148
13355
  }
13149
13356
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
13150
13357
  var CommandLineTooLongError = class extends Error {
@@ -13210,7 +13417,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
13210
13417
  };
13211
13418
  function isResolvableOnPosixPath(command, PATH, exists) {
13212
13419
  if (command.includes("/")) return exists(command);
13213
- return PATH.split(path15.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path15.join(dir, command)));
13420
+ return PATH.split(path16.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path16.join(dir, command)));
13214
13421
  }
13215
13422
  function isExecutableResolved(command, plan, PATH, deps = {}) {
13216
13423
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -13250,9 +13457,9 @@ function cmdArgs(line) {
13250
13457
  return ["/d", "/s", "/c", `"${line}"`];
13251
13458
  }
13252
13459
  function candidateExtensions(command, pathExt) {
13253
- const declared = pathExt.split(path15.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
13460
+ const declared = pathExt.split(path16.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
13254
13461
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
13255
- const ext = path15.win32.extname(command).toLowerCase();
13462
+ const ext = path16.win32.extname(command).toLowerCase();
13256
13463
  if (ext && known.has(ext)) {
13257
13464
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
13258
13465
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -13273,13 +13480,13 @@ async function findWindowsExecutables(command, deps) {
13273
13480
  const exists = deps.exists ?? defaultExists3;
13274
13481
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
13275
13482
  const byRoute = candidateExtensions(command, pathExt);
13276
- const dirs = path15.win32.basename(command) !== command ? [path15.win32.dirname(path15.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path15.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
13277
- const base = path15.win32.basename(command);
13483
+ const dirs = path16.win32.basename(command) !== command ? [path16.win32.dirname(path16.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path16.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
13484
+ const base = path16.win32.basename(command);
13278
13485
  const found = [];
13279
13486
  for (const route of ROUTES) {
13280
13487
  outer: for (const ext of byRoute[route]) {
13281
13488
  for (const dir of dirs) {
13282
- const candidate = path15.win32.join(dir, `${base}${ext}`);
13489
+ const candidate = path16.win32.join(dir, `${base}${ext}`);
13283
13490
  if (exists(candidate)) {
13284
13491
  found.push({ file: candidate, route });
13285
13492
  break outer;
@@ -13327,7 +13534,7 @@ async function planDirectLaunch(command, args, deps = {}) {
13327
13534
 
13328
13535
  // src/utils/workspace.ts
13329
13536
  var fs3 = __toESM(require("fs"));
13330
- var path16 = __toESM(require("path"));
13537
+ var path17 = __toESM(require("path"));
13331
13538
  var WorkspaceNotFoundError = class extends Error {
13332
13539
  constructor(workspace) {
13333
13540
  super(`Workspace "${workspace}" does not exist.`);
@@ -13383,7 +13590,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
13383
13590
  throw new WorkspaceNotAProjectError(workspace);
13384
13591
  }
13385
13592
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
13386
- (marker2) => exists(path16.join(resolved, marker2))
13593
+ (marker2) => exists(path17.join(resolved, marker2))
13387
13594
  );
13388
13595
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
13389
13596
  }
@@ -13391,12 +13598,12 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
13391
13598
  // src/services/workspaceEnv.ts
13392
13599
  var import_child_process4 = require("child_process");
13393
13600
  var fs5 = __toESM(require("fs"));
13394
- var os6 = __toESM(require("os"));
13395
- var path18 = __toESM(require("path"));
13601
+ var os7 = __toESM(require("os"));
13602
+ var path19 = __toESM(require("path"));
13396
13603
 
13397
13604
  // src/utils/fsHelpers.ts
13398
13605
  var fs4 = __toESM(require("fs"));
13399
- var path17 = __toESM(require("path"));
13606
+ var path18 = __toESM(require("path"));
13400
13607
  var import_crypto5 = require("crypto");
13401
13608
  var STATE_DIR = ".ordewell";
13402
13609
  function ensureDir(dir) {
@@ -13405,9 +13612,9 @@ function ensureDir(dir) {
13405
13612
  }
13406
13613
  }
13407
13614
  function writeFileAtomic(filePath, content) {
13408
- const dir = path17.dirname(filePath);
13615
+ const dir = path18.dirname(filePath);
13409
13616
  ensureDir(dir);
13410
- const tempPath = path17.join(dir, `.${path17.basename(filePath)}.${(0, import_crypto5.randomUUID)()}.tmp`);
13617
+ const tempPath = path18.join(dir, `.${path18.basename(filePath)}.${(0, import_crypto5.randomUUID)()}.tmp`);
13411
13618
  fs4.writeFileSync(tempPath, content, { flag: "wx" });
13412
13619
  try {
13413
13620
  fs4.renameSync(tempPath, filePath);
@@ -13420,7 +13627,7 @@ function writeFileAtomic(filePath, content) {
13420
13627
  }
13421
13628
  }
13422
13629
  function getStateDir(baseDir) {
13423
- return path17.join(baseDir ?? process.cwd(), STATE_DIR);
13630
+ return path18.join(baseDir ?? process.cwd(), STATE_DIR);
13424
13631
  }
13425
13632
  var IGNORE_FILE = ".gitignore";
13426
13633
  var STATE_DIR_IGNORE = "*\n!.gitignore\n!skills/\n!skills/**\n";
@@ -13428,7 +13635,7 @@ var LEGACY_IGNORES = /* @__PURE__ */ new Set(["*", "*\n"]);
13428
13635
  function ensureStateDirIgnored(baseDir) {
13429
13636
  const dir = getStateDir(baseDir);
13430
13637
  ensureDir(dir);
13431
- const file = path17.join(dir, IGNORE_FILE);
13638
+ const file = path18.join(dir, IGNORE_FILE);
13432
13639
  try {
13433
13640
  fs4.writeFileSync(file, STATE_DIR_IGNORE, { flag: "wx" });
13434
13641
  return;
@@ -13963,7 +14170,7 @@ function defaultDeps() {
13963
14170
  }
13964
14171
  },
13965
14172
  baseEnv: process.env,
13966
- home: os6.homedir()
14173
+ home: os7.homedir()
13967
14174
  };
13968
14175
  }
13969
14176
  var REFUSED2 = new Set(SETTINGS_ENV_REFUSED);
@@ -13975,7 +14182,7 @@ async function resolveWorkspaceEnv(cwd, overrides = {}) {
13975
14182
  const direnv = await fromDirenv(checkoutDirOf(cwd, deps), deps);
13976
14183
  result.blockedEnvrc = direnv.blocked;
13977
14184
  Object.assign(collected, direnv.env);
13978
- const file = findUp(cwd, path18.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
14185
+ const file = findUp(cwd, path19.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
13979
14186
  if (file) {
13980
14187
  const content = deps.readFile(file);
13981
14188
  if (content !== null) {
@@ -13992,7 +14199,7 @@ async function resolveWorkspaceEnv(cwd, overrides = {}) {
13992
14199
  }
13993
14200
  var evaluating = /* @__PURE__ */ new Map();
13994
14201
  async function workspaceEnvOf(cwd) {
13995
- const key = path18.resolve(cwd);
14202
+ const key = path19.resolve(cwd);
13996
14203
  let running2 = evaluating.get(key);
13997
14204
  if (!running2) {
13998
14205
  running2 = resolveWorkspaceEnv(cwd).then((result) => result.env).finally(() => evaluating.delete(key));
@@ -14001,11 +14208,11 @@ async function workspaceEnvOf(cwd) {
14001
14208
  return { ...await running2 };
14002
14209
  }
14003
14210
  function checkoutDirOf(cwd, deps) {
14004
- const parts = path18.resolve(cwd).split(path18.sep);
14211
+ const parts = path19.resolve(cwd).split(path19.sep);
14005
14212
  const at = parts.findIndex((part, i) => part === STATE_DIR && parts[i + 1] === "worktrees");
14006
14213
  if (at < 0 || parts.length < at + 4) return cwd;
14007
- const root = parts.slice(0, at).join(path18.sep) || path18.sep;
14008
- const mapped = path18.join(root, ...parts.slice(at + 4));
14214
+ const root = parts.slice(0, at).join(path19.sep) || path19.sep;
14215
+ const mapped = path19.join(root, ...parts.slice(at + 4));
14009
14216
  return deps.isDirectory(mapped) ? mapped : root;
14010
14217
  }
14011
14218
  async function fromDirenv(cwd, deps) {
@@ -14035,18 +14242,18 @@ async function fromDirenv(cwd, deps) {
14035
14242
  return { env, blocked: null };
14036
14243
  }
14037
14244
  function findUp(start, relative17, deps) {
14038
- let dir = path18.resolve(start);
14245
+ let dir = path19.resolve(start);
14039
14246
  for (; ; ) {
14040
- const candidate = path18.join(dir, relative17);
14247
+ const candidate = path19.join(dir, relative17);
14041
14248
  if (deps.readFile(candidate) !== null) return candidate;
14042
- const parent = path18.dirname(dir);
14249
+ const parent = path19.dirname(dir);
14043
14250
  if (parent === dir) return null;
14044
14251
  dir = parent;
14045
14252
  }
14046
14253
  }
14047
14254
  async function isTracked(file, deps) {
14048
14255
  try {
14049
- const out = await deps.run("git", ["ls-files", "--error-unmatch", path18.basename(file)], { cwd: path18.dirname(file), env: deps.baseEnv });
14256
+ const out = await deps.run("git", ["ls-files", "--error-unmatch", path19.basename(file)], { cwd: path19.dirname(file), env: deps.baseEnv });
14050
14257
  return out.code === 0;
14051
14258
  } catch {
14052
14259
  return false;
@@ -14056,7 +14263,7 @@ function parseEnvFile(content, context, home) {
14056
14263
  const env = {};
14057
14264
  const lookup2 = (name) => env[name] ?? context[name] ?? "";
14058
14265
  const expand = (value) => {
14059
- const tilde = value.startsWith("~/") ? path18.join(home, value.slice(2)) : value;
14266
+ const tilde = value.startsWith("~/") ? path19.join(home, value.slice(2)) : value;
14060
14267
  return tilde.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => lookup2(braced ?? bare));
14061
14268
  };
14062
14269
  for (const raw of content.split(/\r?\n/)) {
@@ -14326,7 +14533,7 @@ function readOnlySessionRules(site) {
14326
14533
  real = import_fs8.realpathSync.native(root);
14327
14534
  } catch {
14328
14535
  }
14329
- for (const path47 of /* @__PURE__ */ new Set([root, real])) rules.push({ action: "external_directory", resource: `${path47.replace(/[\\/]$/, "")}/*`, effect: "allow" });
14536
+ for (const path50 of /* @__PURE__ */ new Set([root, real])) rules.push({ action: "external_directory", resource: `${path50.replace(/[\\/]$/, "")}/*`, effect: "allow" });
14330
14537
  }
14331
14538
  return rules;
14332
14539
  }
@@ -14452,6 +14659,14 @@ function runnerEnv(resolvedPath, workspace = {}, launch = {}, drop) {
14452
14659
  // src/services/harness/runnerProcess.ts
14453
14660
  var STDERR_TAIL_CHARS = 4e3;
14454
14661
  var CLOSE_GRACE_MS = 2e3;
14662
+ function releasePipes(child) {
14663
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
14664
+ if (stream instanceof import_net.Socket) stream.unref();
14665
+ }
14666
+ for (const stream of [child.stdout, child.stderr]) {
14667
+ if (stream instanceof import_net.Socket) stream.resume();
14668
+ }
14669
+ }
14455
14670
  var RunnerProcess = class {
14456
14671
  constructor(deps) {
14457
14672
  this.deps = deps;
@@ -14524,6 +14739,7 @@ var RunnerProcess = class {
14524
14739
  const grace = setTimeout(() => {
14525
14740
  for (const flush of [...this.stdoutEnds]) flush();
14526
14741
  this.end({ code, signal });
14742
+ releasePipes(child);
14527
14743
  }, CLOSE_GRACE_MS);
14528
14744
  grace.unref?.();
14529
14745
  void this.ended.then(() => clearTimeout(grace));
@@ -14622,11 +14838,11 @@ ${tail}` : ""}`;
14622
14838
 
14623
14839
  // src/services/sandbox/nativeShellLauncher.ts
14624
14840
  var import_fs9 = require("fs");
14625
- var os7 = __toESM(require("os"));
14626
- var path19 = __toESM(require("path"));
14841
+ var os8 = __toESM(require("os"));
14842
+ var path20 = __toESM(require("path"));
14627
14843
  function nativeShellInvocation(platform, env = process.env) {
14628
14844
  const shell = platform === "darwin" ? "/bin/zsh" : env.SHELL;
14629
- return { shell: shell && path19.posix.isAbsolute(shell) ? shell : "/bin/sh", args: ["-c"] };
14845
+ return { shell: shell && path20.posix.isAbsolute(shell) ? shell : "/bin/sh", args: ["-c"] };
14630
14846
  }
14631
14847
  var live = /* @__PURE__ */ new Set();
14632
14848
  var exitHook = false;
@@ -14658,10 +14874,10 @@ function launcherScript(launch) {
14658
14874
  }
14659
14875
  async function createNativeShellLauncher(opts) {
14660
14876
  const invocation = nativeShellInvocation(opts.platform ?? process.platform, opts.env);
14661
- const root = (0, import_fs9.realpathSync)((0, import_fs9.mkdtempSync)(path19.join(os7.tmpdir(), "ordewell-native-shell-")));
14877
+ const root = (0, import_fs9.realpathSync)((0, import_fs9.mkdtempSync)(path20.join(os8.tmpdir(), "ordewell-native-shell-")));
14662
14878
  let shortTmp = null;
14663
14879
  try {
14664
- shortTmp = (0, import_fs9.realpathSync)((0, import_fs9.mkdtempSync)(path19.join("/tmp", "ow-")));
14880
+ shortTmp = (0, import_fs9.realpathSync)((0, import_fs9.mkdtempSync)(path20.join("/tmp", "ow-")));
14665
14881
  const tmpdir7 = shortTmp;
14666
14882
  const launch = await opts.sandbox.launchNativeShell({
14667
14883
  workspace: opts.workspace,
@@ -14670,7 +14886,7 @@ async function createNativeShellLauncher(opts) {
14670
14886
  shellArgs: invocation.args,
14671
14887
  ...opts.env ? { env: opts.env } : {}
14672
14888
  });
14673
- const prefix = path19.join(root, "run");
14889
+ const prefix = path20.join(root, "run");
14674
14890
  (0, import_fs9.writeFileSync)(prefix, launcherScript(launch), { mode: 448 });
14675
14891
  (0, import_fs9.chmodSync)(prefix, 448);
14676
14892
  keep(root);
@@ -15039,6 +15255,20 @@ ${lines2.map((line) => `${line}
15039
15255
  }
15040
15256
 
15041
15257
  // src/services/harness/claudeOrdewell.ts
15258
+ var path21 = __toESM(require("path"));
15259
+ function assertNoClaudeTaskCollision(cwd) {
15260
+ const text6 = readText(path21.join(cwd, ".mcp.json"));
15261
+ if (text6 === void 0) return;
15262
+ let parsed;
15263
+ try {
15264
+ parsed = JSON.parse(text6);
15265
+ } catch {
15266
+ return;
15267
+ }
15268
+ if (isRecord2(parsed) && isRecord2(parsed.mcpServers) && Object.hasOwn(parsed.mcpServers, ORDEWELL_MCP_SERVER_NAME)) {
15269
+ throw new Error(".mcp.json declares an MCP server named ordewell; rename or remove the ordewell server before starting this task.");
15270
+ }
15271
+ }
15042
15272
  var CLAUDE_ORDEWELL = {
15043
15273
  toolName: MCP_CLIENT_TOOL_NAMES.toolName,
15044
15274
  toolNames: MCP_CLIENT_TOOL_NAMES.toolNames,
@@ -15186,9 +15416,8 @@ var ReplyRuns = class {
15186
15416
  };
15187
15417
 
15188
15418
  // src/services/harness/claudeRepoConfig.ts
15189
- var os8 = __toESM(require("os"));
15190
- var path20 = __toESM(require("path"));
15191
- var MEMORY_FILES = ["CLAUDE.md", path20.join(".claude", "CLAUDE.md"), "CLAUDE.local.md"];
15419
+ var path22 = __toESM(require("path"));
15420
+ var MEMORY_FILES = ["CLAUDE.md", path22.join(".claude", "CLAUDE.md"), "CLAUDE.local.md"];
15192
15421
  var MAX_IMPORT_DEPTH = 5;
15193
15422
  function importsOf(text6) {
15194
15423
  const prose = text6.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
@@ -15199,11 +15428,11 @@ function withImports(workspace, file, depth, seen, into) {
15199
15428
  seen.add(file);
15200
15429
  const text6 = readText(file);
15201
15430
  if (text6 === void 0) return;
15202
- into.push({ path: path20.relative(workspace, file), text: text6 });
15431
+ into.push({ path: path22.relative(workspace, file), text: text6 });
15203
15432
  if (depth >= MAX_IMPORT_DEPTH) return;
15204
15433
  for (const target of importsOf(text6)) {
15205
15434
  if (target.startsWith("~")) continue;
15206
- const abs = path20.resolve(path20.dirname(file), target);
15435
+ const abs = path22.resolve(path22.dirname(file), target);
15207
15436
  if (insideOnDisk(workspace, abs)) withImports(workspace, abs, depth + 1, seen, into);
15208
15437
  }
15209
15438
  }
@@ -15224,12 +15453,12 @@ var CLAUDE_REPO_CONFIG = {
15224
15453
  instructions(workspace) {
15225
15454
  const files = [];
15226
15455
  const seen = /* @__PURE__ */ new Set();
15227
- for (const name of MEMORY_FILES) withImports(workspace, path20.join(workspace, name), 0, seen, files);
15456
+ for (const name of MEMORY_FILES) withImports(workspace, path22.join(workspace, name), 0, seen, files);
15228
15457
  return files;
15229
15458
  },
15230
15459
  projectServers(workspace) {
15231
15460
  const source = ".mcp.json";
15232
- const text6 = readText(path20.join(workspace, source));
15461
+ const text6 = readText(path22.join(workspace, source));
15233
15462
  if (text6 === void 0) return NO_SERVERS;
15234
15463
  let parsed;
15235
15464
  try {
@@ -15247,13 +15476,14 @@ var CLAUDE_REPO_CONFIG = {
15247
15476
  return found;
15248
15477
  }
15249
15478
  };
15250
- function claudeLocalServers(configDir2, workspace) {
15251
- const file = configDir2 ? path20.join(configDir2, ".claude.json") : path20.join(os8.homedir(), ".claude.json");
15252
- const text6 = readText(file);
15479
+ function claudeLocalServers(env, workspace) {
15480
+ const configDir2 = env.CLAUDE_CONFIG_DIR?.trim();
15481
+ const file = expandRunnerHome(configDir2 ? path22.join(configDir2, ".claude.json") : "~/.claude.json", env);
15482
+ const text6 = file === null ? void 0 : readText(file);
15253
15483
  if (text6 === void 0) return {};
15254
15484
  try {
15255
15485
  const parsed = JSON.parse(text6);
15256
- const project = isRecord2(parsed) && isRecord2(parsed.projects) ? parsed.projects[path20.resolve(workspace)] : void 0;
15486
+ const project = isRecord2(parsed) && isRecord2(parsed.projects) ? parsed.projects[path22.resolve(workspace)] : void 0;
15257
15487
  return isRecord2(project) && isRecord2(project.mcpServers) ? project.mcpServers : {};
15258
15488
  } catch {
15259
15489
  return {};
@@ -15508,13 +15738,13 @@ var NO_AUTO_MEMORY = { CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
15508
15738
  function inSavedOutput(abs, configDir2, sessionId) {
15509
15739
  let projects;
15510
15740
  try {
15511
- projects = import_fs11.realpathSync.native(path21.join(configDir2, "projects"));
15741
+ projects = import_fs11.realpathSync.native(path23.join(configDir2, "projects"));
15512
15742
  } catch {
15513
15743
  return false;
15514
15744
  }
15515
- const rel = path21.relative(projects, abs);
15516
- if (rel.startsWith("..") || path21.isAbsolute(rel)) return false;
15517
- const [project, session, folder] = rel.split(path21.sep);
15745
+ const rel = path23.relative(projects, abs);
15746
+ if (rel.startsWith("..") || path23.isAbsolute(rel)) return false;
15747
+ const [project, session, folder] = rel.split(path23.sep);
15518
15748
  return Boolean(project) && session === sessionId && folder === "tool-results";
15519
15749
  }
15520
15750
  var PATH_TOOLS = {
@@ -15752,7 +15982,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
15752
15982
  "--append-system-prompt",
15753
15983
  opts.systemPrompt
15754
15984
  ];
15755
- if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner", this.plannerServers(opts, workspace)));
15985
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner", this.plannerServers(opts, { ...process.env, ...workspace })));
15756
15986
  if (opts.model) args.push("--model", opts.model);
15757
15987
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
15758
15988
  if (opts.resumeSessionId) {
@@ -15760,7 +15990,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
15760
15990
  this.countFrom(opts.resumeSessionId, opts.resumeCost);
15761
15991
  this.pendingResume = opts.resumeSessionId;
15762
15992
  }
15763
- this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path21.join(os9.homedir(), ".claude");
15993
+ const env = { ...process.env, ...workspace };
15994
+ this.configDir = expandRunnerHome(claudeConfigDir(env), env);
15764
15995
  return {
15765
15996
  command: "claude",
15766
15997
  args,
@@ -15804,6 +16035,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
15804
16035
  */
15805
16036
  taskSpawnSpec(opts) {
15806
16037
  const readOnly = readOnlyTask(opts);
16038
+ if (!readOnly) assertNoClaudeTaskCollision(opts.cwd);
15807
16039
  this.readOnlySite = readOnly ? opts : null;
15808
16040
  this.requestedMode = readOnly ? PLANNER_PERMISSION_MODE : opts.flags.permissionMode;
15809
16041
  this.followedModel = opts.model;
@@ -15830,13 +16062,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
15830
16062
  /**
15831
16063
  * What user-only settings stop the CLI loading that is not the
15832
16064
  * repository's to choose (ADR-0028): the user's own local-scope servers,
15833
- * under the `CLAUDE_CONFIG_DIR` the planner runs with, and the project
15834
- * servers approved for it.
16065
+ * from the account in the environment the planner runs with, and the
16066
+ * project servers approved for it.
15835
16067
  */
15836
- plannerServers(opts, workspace) {
15837
- const configDir2 = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR;
16068
+ plannerServers(opts, env) {
15838
16069
  return {
15839
- ...claudeLocalServers(configDir2, opts.cwd),
16070
+ ...claudeLocalServers(env, opts.cwd),
15840
16071
  ...Object.fromEntries((opts.projectServers ?? []).map((server4) => [server4.name, server4.definition]))
15841
16072
  };
15842
16073
  }
@@ -16600,8 +16831,8 @@ function runProbe(deps, cwd, env, flags) {
16600
16831
  }
16601
16832
 
16602
16833
  // src/services/harness/codexRepoConfig.ts
16603
- var path22 = __toESM(require("path"));
16604
- var CONFIG_FILE = path22.join(".codex", "config.toml");
16834
+ var path24 = __toESM(require("path"));
16835
+ var CONFIG_FILE = path24.join(".codex", "config.toml");
16605
16836
  function stringsOf(value) {
16606
16837
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
16607
16838
  }
@@ -16627,9 +16858,9 @@ var CODEX_REPO_CONFIG = {
16627
16858
  const files = [];
16628
16859
  for (const dir of repoLevels(workspace)) {
16629
16860
  for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
16630
- const text6 = readText(path22.join(dir, name));
16861
+ const text6 = readText(path24.join(dir, name));
16631
16862
  if (text6 === void 0) continue;
16632
- files.push({ path: path22.relative(workspace, path22.join(dir, name)) || name, text: text6 });
16863
+ files.push({ path: path24.relative(workspace, path24.join(dir, name)) || name, text: text6 });
16633
16864
  break;
16634
16865
  }
16635
16866
  }
@@ -16640,10 +16871,10 @@ var CODEX_REPO_CONFIG = {
16640
16871
  const byName = /* @__PURE__ */ new Map();
16641
16872
  const unreadable = [];
16642
16873
  for (const dir of repoLevels(workspace)) {
16643
- const file = path22.join(dir, CONFIG_FILE);
16874
+ const file = path24.join(dir, CONFIG_FILE);
16644
16875
  const text6 = readText(file);
16645
16876
  if (text6 === void 0) continue;
16646
- const source = path22.relative(workspace, file);
16877
+ const source = path24.relative(workspace, file);
16647
16878
  let parsed;
16648
16879
  try {
16649
16880
  parsed = parseToml(text6);
@@ -16663,9 +16894,9 @@ var CODEX_REPO_CONFIG = {
16663
16894
  };
16664
16895
  function untrustedProjects(workspace) {
16665
16896
  const projects = {};
16666
- for (let dir = path22.resolve(workspace); ; dir = path22.dirname(dir)) {
16897
+ for (let dir = path24.resolve(workspace); ; dir = path24.dirname(dir)) {
16667
16898
  projects[dir] = { trust_level: "untrusted" };
16668
- if (path22.dirname(dir) === dir) return projects;
16899
+ if (path24.dirname(dir) === dir) return projects;
16669
16900
  }
16670
16901
  }
16671
16902
 
@@ -17786,7 +18017,7 @@ ${this.exitMessage()}`);
17786
18017
  case "webSearch":
17787
18018
  return;
17788
18019
  case "fileChange": {
17789
- const paths = (item.changes ?? []).map((change) => change.path).filter((path47) => !!path47);
18020
+ const paths = (item.changes ?? []).map((change) => change.path).filter((path50) => !!path50);
17790
18021
  this.fileChangePaths.set(item.id, paths);
17791
18022
  if (this.startOpts?.kind === "task") {
17792
18023
  for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
@@ -17889,20 +18120,20 @@ ${this.exitMessage()}`);
17889
18120
  announceFileChange(itemId, change, emit, subagentId) {
17890
18121
  const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
17891
18122
  this.fileChangeRows.set(itemId, rows);
17892
- const path47 = this.shownChangePath(change);
17893
- const known = rows.get(path47);
18123
+ const path50 = this.shownChangePath(change);
18124
+ const known = rows.get(path50);
17894
18125
  if (known) return known;
17895
- const row = rows.size === 0 ? itemId : `${itemId}:${path47}`;
17896
- rows.set(path47, row);
17897
- emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path47 }, ...subagentId ? { subagentId } : {} });
18126
+ const row = rows.size === 0 ? itemId : `${itemId}:${path50}`;
18127
+ rows.set(path50, row);
18128
+ emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path50 }, ...subagentId ? { subagentId } : {} });
17898
18129
  return row;
17899
18130
  }
17900
18131
  /** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
17901
18132
  shownChangePath(change) {
17902
- const shown = (path47) => {
18133
+ const shown = (path50) => {
17903
18134
  const cwd = this.startOpts?.cwd;
17904
- const rel = cwd ? (0, import_path6.relative)(cwd, path47) : path47;
17905
- return rel && rel !== ".." && !rel.startsWith(`..${import_path6.sep}`) && !(0, import_path6.isAbsolute)(rel) ? rel : path47;
18135
+ const rel = cwd ? (0, import_path6.relative)(cwd, path50) : path50;
18136
+ return rel && rel !== ".." && !rel.startsWith(`..${import_path6.sep}`) && !(0, import_path6.isAbsolute)(rel) ? rel : path50;
17906
18137
  };
17907
18138
  const from = shown(change.path ?? "");
17908
18139
  return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
@@ -17953,7 +18184,7 @@ var import_fs12 = require("fs");
17953
18184
 
17954
18185
  // src/services/harness/projectServers.ts
17955
18186
  var import_crypto8 = require("crypto");
17956
- var path23 = __toESM(require("path"));
18187
+ var path25 = __toESM(require("path"));
17957
18188
  function canonical2(value) {
17958
18189
  if (Array.isArray(value)) return value.map(canonical2);
17959
18190
  if (!isRecord2(value)) return value;
@@ -17964,7 +18195,7 @@ function projectServerScope(server4) {
17964
18195
  return `mcp-server:${server4.name}@${hash}`;
17965
18196
  }
17966
18197
  var scopeName = (scope) => scope.slice(0, scope.lastIndexOf("@"));
17967
- function fileProjectServerApprovals(file = path23.join(globalDataDir(), "project-mcp-servers.json")) {
18198
+ function fileProjectServerApprovals(file = path25.join(globalDataDir(), "project-mcp-servers.json")) {
17968
18199
  const load = () => {
17969
18200
  const text6 = readText(file);
17970
18201
  if (text6 === void 0) return {};
@@ -19127,8 +19358,8 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
19127
19358
  this.v2 = new OpenCodeV2({
19128
19359
  version,
19129
19360
  nativeShell: this.nativeShell?.prefix ?? null,
19130
- request: (method, path47, body, signal, timeoutMs) => this.request(method, path47, body, signal, timeoutMs),
19131
- json: (method, path47, body, signal, timeoutMs) => this.json(method, path47, body, signal, timeoutMs),
19361
+ request: (method, path50, body, signal, timeoutMs) => this.request(method, path50, body, signal, timeoutMs),
19362
+ json: (method, path50, body, signal, timeoutMs) => this.json(method, path50, body, signal, timeoutMs),
19132
19363
  openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
19133
19364
  processEnded: this.processEnded,
19134
19365
  isExited: () => this.exited,
@@ -19765,15 +19996,15 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
19765
19996
  onActivity
19766
19997
  );
19767
19998
  }
19768
- async json(method, path47, body, signal, timeoutMs) {
19769
- const response = await this.request(method, path47, body, signal, timeoutMs);
19770
- if (!response.ok) throw new OpenCodeHttpError(`${method} ${path47} failed: ${response.status} ${response.statusText}`);
19999
+ async json(method, path50, body, signal, timeoutMs) {
20000
+ const response = await this.request(method, path50, body, signal, timeoutMs);
20001
+ if (!response.ok) throw new OpenCodeHttpError(`${method} ${path50} failed: ${response.status} ${response.statusText}`);
19771
20002
  return await response.json().catch(() => null);
19772
20003
  }
19773
20004
  /** `timeoutMs` null is for a turn's own POST, which the server answers when the turn is done. */
19774
- request(method, path47, body, signal, timeoutMs = REQUEST_TIMEOUT_MS) {
20005
+ request(method, path50, body, signal, timeoutMs = REQUEST_TIMEOUT_MS) {
19775
20006
  const deadline = withDeadline(signal, timeoutMs);
19776
- return this.deps.fetch(`${this.baseUrl}${path47}`, {
20007
+ return this.deps.fetch(`${this.baseUrl}${path50}`, {
19777
20008
  method,
19778
20009
  headers: this.headers({ "content-type": "application/json" }),
19779
20010
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
@@ -19834,8 +20065,8 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
19834
20065
  };
19835
20066
 
19836
20067
  // src/services/harness/openCodeRepoConfig.ts
19837
- var path24 = __toESM(require("path"));
19838
- var CONFIG_FILES = ["opencode.json", "opencode.jsonc", path24.join(".opencode", "opencode.json"), path24.join(".opencode", "opencode.jsonc")];
20068
+ var path26 = __toESM(require("path"));
20069
+ var CONFIG_FILES = ["opencode.json", "opencode.jsonc", path26.join(".opencode", "opencode.json"), path26.join(".opencode", "opencode.jsonc")];
19839
20070
  var INSTRUCTION_NAMES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
19840
20071
  function server3(name, entry, source) {
19841
20072
  if (!isRecord2(entry)) return "not an object";
@@ -19854,8 +20085,8 @@ var OPENCODE_REPO_CONFIG = {
19854
20085
  const levels = repoLevels(workspace);
19855
20086
  for (const name of INSTRUCTION_NAMES) {
19856
20087
  const files = levels.flatMap((dir) => {
19857
- const text6 = readText(path24.join(dir, name));
19858
- return text6 === void 0 ? [] : [{ path: path24.relative(workspace, path24.join(dir, name)) || name, text: text6 }];
20088
+ const text6 = readText(path26.join(dir, name));
20089
+ return text6 === void 0 ? [] : [{ path: path26.relative(workspace, path26.join(dir, name)) || name, text: text6 }];
19859
20090
  });
19860
20091
  if (files.length > 0) return files;
19861
20092
  }
@@ -19867,10 +20098,10 @@ var OPENCODE_REPO_CONFIG = {
19867
20098
  const unreadable = [];
19868
20099
  for (const dir of repoLevels(workspace)) {
19869
20100
  for (const name of CONFIG_FILES) {
19870
- const file = path24.join(dir, name);
20101
+ const file = path26.join(dir, name);
19871
20102
  const text6 = readText(file);
19872
20103
  if (text6 === void 0) continue;
19873
- const source = path24.relative(workspace, file);
20104
+ const source = path26.relative(workspace, file);
19874
20105
  let parsed;
19875
20106
  try {
19876
20107
  parsed = parseJsonc(text6);
@@ -20059,7 +20290,7 @@ function knownModelId(value, optionIds, shortcuts) {
20059
20290
 
20060
20291
  // src/services/ModelCatalog.ts
20061
20292
  var fs6 = __toESM(require("fs"));
20062
- var path25 = __toESM(require("path"));
20293
+ var path27 = __toESM(require("path"));
20063
20294
  var CACHE_KEY = "model-catalog";
20064
20295
  var CACHE_TTL_MS = 60 * 60 * 1e3;
20065
20296
  function storageKey(baseUrl) {
@@ -20112,7 +20343,7 @@ var ModelCatalog = class {
20112
20343
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
20113
20344
  const key = storageKey(url);
20114
20345
  const cacheDir = globalCacheDir();
20115
- const cacheFile = path25.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20346
+ const cacheFile = path27.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20116
20347
  if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
20117
20348
  } catch {
20118
20349
  }
@@ -20120,7 +20351,7 @@ var ModelCatalog = class {
20120
20351
  static readCache(key) {
20121
20352
  try {
20122
20353
  const cacheDir = globalCacheDir();
20123
- const cacheFile = path25.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20354
+ const cacheFile = path27.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20124
20355
  if (!fs6.existsSync(cacheFile)) return null;
20125
20356
  const raw = fs6.readFileSync(cacheFile, "utf8");
20126
20357
  const data = JSON.parse(raw);
@@ -20134,7 +20365,7 @@ var ModelCatalog = class {
20134
20365
  try {
20135
20366
  const cacheDir = globalCacheDir();
20136
20367
  fs6.mkdirSync(cacheDir, { recursive: true });
20137
- const cacheFile = path25.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20368
+ const cacheFile = path27.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
20138
20369
  fs6.writeFileSync(
20139
20370
  cacheFile,
20140
20371
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
@@ -20145,15 +20376,17 @@ var ModelCatalog = class {
20145
20376
  };
20146
20377
 
20147
20378
  // src/services/ModelDiscovery.ts
20148
- var path26 = __toESM(require("path"));
20379
+ var path28 = __toESM(require("path"));
20149
20380
  var import_child_process6 = require("child_process");
20150
20381
  var import_util2 = require("util");
20151
20382
  var fsSync5 = __toESM(require("fs"));
20152
- var osMod = __toESM(require("os"));
20153
20383
 
20154
20384
  // src/services/harness/appServerModels.ts
20155
20385
  var import_child_process5 = require("child_process");
20156
20386
  var DEFAULT_TIMEOUT_MS3 = 2e4;
20387
+ function definedOnly(env) {
20388
+ return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
20389
+ }
20157
20390
  function parseRpcLine(line) {
20158
20391
  try {
20159
20392
  const value = JSON.parse(line);
@@ -20166,10 +20399,9 @@ async function openAppServer(command, args, launch) {
20166
20399
  const runner = new RunnerProcess({
20167
20400
  spawn: import_child_process5.spawn,
20168
20401
  fetch: globalThis.fetch,
20169
- // The cwd is no workspace: discovery asks for the catalog before one is
20170
- // chosen, so it pays no direnv round trip before a spawn with a deadline.
20171
- // The account monitor passes the workspace's environment itself, since
20172
- // that can choose whose account is read.
20402
+ // The cwd is no workspace, so it pays no direnv round trip before a spawn
20403
+ // with a deadline. Discovery and the account monitor pass the workspace's
20404
+ // environment themselves, since that can choose whose account is read.
20173
20405
  workspaceEnv: async () => ({}),
20174
20406
  ...launch.deps
20175
20407
  });
@@ -20222,7 +20454,11 @@ async function openAppServer(command, args, launch) {
20222
20454
  }
20223
20455
  var APP_SERVER_CLIENT_INFO = { name: "ordewell", title: "Ordewell", version: "0.1.0" };
20224
20456
  async function discoverAppServerModels(command, args, options = {}) {
20225
- const server4 = await openAppServer(command, args, { cwd: process.cwd(), deps: options.deps });
20457
+ const env = options.env;
20458
+ const server4 = await openAppServer(command, args, {
20459
+ cwd: process.cwd(),
20460
+ deps: { ...env ? { workspaceEnv: async () => definedOnly(env) } : {}, ...options.deps }
20461
+ });
20226
20462
  if (!server4) return null;
20227
20463
  const { rpc } = server4;
20228
20464
  const asked = (async () => {
@@ -20249,10 +20485,10 @@ async function discoverAppServerModels(command, args, options = {}) {
20249
20485
  var execAsync2 = (0, import_util2.promisify)(import_child_process6.exec);
20250
20486
  var defaultExec2 = async (command, options) => {
20251
20487
  const PATH = await augmentedPath();
20252
- const { stdout } = await execAsync2(command, { ...options, maxBuffer: 32 * 1024 * 1024, env: withPath(process.env, PATH) });
20488
+ const { stdout } = await execAsync2(command, { timeout: options?.timeout, maxBuffer: 32 * 1024 * 1024, env: withPath(options?.env ?? process.env, PATH) });
20253
20489
  return { stdout: String(stdout) };
20254
20490
  };
20255
- var defaultAppServerClient = (command, args, options) => discoverAppServerModels(command, args, { timeout: options?.timeout });
20491
+ var defaultAppServerClient = (command, args, options) => discoverAppServerModels(command, args, { timeout: options?.timeout, env: options?.env });
20256
20492
  function effortVariant(effort) {
20257
20493
  return { id: effort, label: effort.charAt(0).toUpperCase() + effort.slice(1) };
20258
20494
  }
@@ -20286,7 +20522,7 @@ function geminiDiscoveryCacheKey(apiKey) {
20286
20522
  return {
20287
20523
  key: `gemini-models:${hash}`,
20288
20524
  dir,
20289
- file: path26.join(dir, `gemini_models_${hash}.json`)
20525
+ file: path28.join(dir, `gemini_models_${hash}.json`)
20290
20526
  };
20291
20527
  }
20292
20528
  function readGeminiCache(apiKey) {
@@ -20373,15 +20609,22 @@ function applyVariants(models, manifest) {
20373
20609
  if (!variants || variants.length === 0) return models;
20374
20610
  return models.map((m) => m.variants.length > 0 ? m : { ...m, variants });
20375
20611
  }
20376
- function resolveApiAuth(authMethods) {
20612
+ function listDir(dir) {
20613
+ try {
20614
+ return fsSync5.readdirSync(dir);
20615
+ } catch {
20616
+ return [];
20617
+ }
20618
+ }
20619
+ function resolveApiAuth(authMethods, env) {
20377
20620
  for (const method of authMethods) {
20378
20621
  try {
20379
20622
  let token;
20380
20623
  if (method.type === "env") {
20381
- token = process.env[method.varName];
20624
+ token = env[method.varName];
20382
20625
  } else {
20383
- const filePath = method.path.replace(/^~/, osMod.homedir());
20384
- if (!fsSync5.existsSync(filePath)) continue;
20626
+ const filePath = expandRunnerHome(typeof method.path === "function" ? method.path(env) : method.path, env);
20627
+ if (!filePath || !fsSync5.existsSync(filePath)) continue;
20385
20628
  const data = JSON.parse(fsSync5.readFileSync(filePath, "utf8"));
20386
20629
  token = method.jsonPath.split(".").reduce((obj, key) => {
20387
20630
  if (obj && typeof obj === "object") return obj[key];
@@ -20397,8 +20640,8 @@ function resolveApiAuth(authMethods) {
20397
20640
  }
20398
20641
  return null;
20399
20642
  }
20400
- async function discoverFromApi(manifest, config, fetchImpl) {
20401
- const auth = resolveApiAuth(config.auth);
20643
+ async function discoverFromApi(manifest, config, env, fetchImpl) {
20644
+ const auth = resolveApiAuth(config.auth, env);
20402
20645
  if (!auth) return null;
20403
20646
  try {
20404
20647
  const headers = { ...config.headers, [auth.header]: auth.value };
@@ -20414,7 +20657,7 @@ async function discoverFromApi(manifest, config, fetchImpl) {
20414
20657
  function wasKilled(err) {
20415
20658
  return !!err && typeof err === "object" && err.killed === true;
20416
20659
  }
20417
- async function discoverFromCommand(manifest, execImpl) {
20660
+ async function discoverFromCommand(manifest, execImpl, env) {
20418
20661
  const discovery = manifest.modelDiscovery;
20419
20662
  const commands = discovery.discoveryCommands ?? [
20420
20663
  { command: discovery.command, args: discovery.args || [], parser: discovery.parser }
@@ -20423,7 +20666,7 @@ async function discoverFromCommand(manifest, execImpl) {
20423
20666
  let timedOut2 = false;
20424
20667
  for (const cmd of commands) {
20425
20668
  try {
20426
- const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
20669
+ const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3, env });
20427
20670
  const models = parseModelOutput(stdout, cmd.parser, manifest);
20428
20671
  if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
20429
20672
  } catch (err) {
@@ -20489,21 +20732,26 @@ var ModelDiscovery = class {
20489
20732
  * same background service.
20490
20733
  */
20491
20734
  inflight = /* @__PURE__ */ new Map();
20735
+ /** Bumped by every `clear()`: a discovery begun before one answers its own lookups but fills no cache. */
20736
+ generation = 0;
20492
20737
  registry;
20493
20738
  execImpl;
20494
20739
  fetchImpl;
20495
20740
  appServerClient;
20496
20741
  readFileImpl;
20497
20742
  now;
20498
- constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
20743
+ env;
20744
+ constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now, env = async () => process.env) {
20499
20745
  this.registry = registry;
20500
20746
  this.execImpl = execImpl;
20501
20747
  this.fetchImpl = fetchImpl;
20502
20748
  this.appServerClient = appServerClient;
20503
20749
  this.now = now;
20750
+ this.env = env;
20504
20751
  this.readFileImpl = readFileImpl ?? ((filePath) => {
20505
20752
  try {
20506
- return fsSync5.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
20753
+ const file = expandRunnerHome(filePath);
20754
+ return file ? fsSync5.readFileSync(file, "utf8") : null;
20507
20755
  } catch {
20508
20756
  return null;
20509
20757
  }
@@ -20515,16 +20763,17 @@ var ModelDiscovery = class {
20515
20763
  * null when both fail so `discover()` falls through to the remaining
20516
20764
  * methods.
20517
20765
  */
20518
- async discoverFromAppServer(manifest) {
20766
+ async discoverFromAppServer(manifest, env) {
20519
20767
  const config = manifest.modelDiscovery.appServer;
20520
20768
  if (!config) return null;
20521
- const raw = await this.appServerClient(config.command, config.args);
20769
+ const raw = await this.appServerClient(config.command, config.args, { env });
20522
20770
  if (raw) {
20523
20771
  const models = parseAppServerModels(raw);
20524
20772
  if (models.length > 0) return models;
20525
20773
  }
20526
- if (config.cacheFile) {
20527
- const content = this.readFileImpl(config.cacheFile);
20774
+ const cacheFile = config.cacheFile && expandRunnerHome(typeof config.cacheFile === "function" ? config.cacheFile(env) : config.cacheFile, env);
20775
+ if (cacheFile) {
20776
+ const content = this.readFileImpl(cacheFile);
20528
20777
  if (content) {
20529
20778
  const models = parseCodexModelsCache(content);
20530
20779
  if (models.length > 0) return models;
@@ -20562,37 +20811,60 @@ var ModelDiscovery = class {
20562
20811
  if (!manifest) {
20563
20812
  return [];
20564
20813
  }
20565
- let models;
20814
+ const generation2 = this.generation;
20815
+ const env = await this.env().catch(() => process.env);
20816
+ const found = await this.discoverFromSources(manifest, env);
20817
+ const models = this.withSettingsModels(found.models, manifest, env);
20818
+ if (generation2 !== this.generation) return models;
20819
+ if (found.cacheable && models.length > 0) this.cache.set(runner, models);
20820
+ else this.misses.set(runner, { models, at: this.now() });
20821
+ return models;
20822
+ }
20823
+ async discoverFromSources(manifest, env) {
20566
20824
  let cacheable = true;
20567
20825
  const discovery = manifest.modelDiscovery;
20568
20826
  if (discovery.appServer) {
20569
- const appServerModels = await this.discoverFromAppServer(manifest);
20570
- if (appServerModels && appServerModels.length > 0) {
20571
- this.cache.set(runner, appServerModels);
20572
- return appServerModels;
20573
- }
20827
+ const appServerModels = await this.discoverFromAppServer(manifest, env);
20828
+ if (appServerModels && appServerModels.length > 0) return { models: appServerModels, cacheable: true };
20574
20829
  cacheable = false;
20575
20830
  }
20576
20831
  if (discovery.apiDiscovery) {
20577
- const apiModels = await discoverFromApi(manifest, discovery.apiDiscovery, this.fetchImpl);
20832
+ const apiModels = await discoverFromApi(manifest, discovery.apiDiscovery, env, this.fetchImpl);
20578
20833
  if (apiModels && apiModels.length > 0) {
20579
- models = applyVariants(mergeCanonicalAliases(apiModels, manifest), manifest);
20580
- if (cacheable && models.length > 0) this.cache.set(runner, models);
20581
- return models;
20834
+ return { models: applyVariants(mergeCanonicalAliases(apiModels, manifest), manifest), cacheable };
20582
20835
  }
20583
20836
  }
20584
20837
  if (discovery.method === "command" && (discovery.command || discovery.discoveryCommands?.length)) {
20585
- const result = await discoverFromCommand(manifest, this.execImpl);
20586
- models = result.models;
20587
- cacheable = !result.fromFallback;
20588
- } else {
20589
- models = buildFallbackModels(manifest);
20838
+ const result = await discoverFromCommand(manifest, this.execImpl, env);
20839
+ return { models: result.models, cacheable: !result.fromFallback };
20590
20840
  }
20591
- if (cacheable && models.length > 0) this.cache.set(runner, models);
20592
- else this.misses.set(runner, { models, at: this.now() });
20593
- return models;
20841
+ return { models: buildFallbackModels(manifest), cacheable };
20842
+ }
20843
+ /**
20844
+ * Append the models registered in the runner's own settings. Read on every
20845
+ * discovery rather than cached apart, so a refresh picks up an edit;
20846
+ * whatever the sources already offer keeps its entry and metadata.
20847
+ */
20848
+ withSettingsModels(models, manifest, env) {
20849
+ const read2 = manifest.modelDiscovery.settingsModels;
20850
+ if (!read2) return models;
20851
+ const files = {
20852
+ read: (filePath) => {
20853
+ const file = expandRunnerHome(filePath, env);
20854
+ return file ? this.readFileImpl(file) : null;
20855
+ },
20856
+ list: (dir) => {
20857
+ const expanded = expandRunnerHome(dir, env);
20858
+ return expanded ? listDir(expanded) : [];
20859
+ },
20860
+ env
20861
+ };
20862
+ const seen = new Set(models.map((m) => m.modelId));
20863
+ const added = read2(files).filter((m) => !seen.has(m.modelId));
20864
+ return added.length > 0 ? [...models, ...applyVariants(added, manifest)] : models;
20594
20865
  }
20595
20866
  clear() {
20867
+ this.generation++;
20596
20868
  this.cache.clear();
20597
20869
  this.misses.clear();
20598
20870
  this.inflight.clear();
@@ -20758,7 +21030,7 @@ function parseMaxParallel(value) {
20758
21030
 
20759
21031
  // src/services/SettingsService.ts
20760
21032
  var fs12 = __toESM(require("fs"));
20761
- var path33 = __toESM(require("path"));
21033
+ var path35 = __toESM(require("path"));
20762
21034
 
20763
21035
  // src/utils/fileLock.ts
20764
21036
  var fs7 = __toESM(require("fs"));
@@ -20890,6 +21162,10 @@ var RECEIPT_LABELS = {
20890
21162
  function receiptLabel(task2) {
20891
21163
  return isTaskRunning(task2) && task2.completionReceived ? RECEIPT_LABELS[task2.completionReceived] : null;
20892
21164
  }
21165
+ function attachingLabel(task2) {
21166
+ if (!isTaskRunning(task2) || !task2.attaching) return null;
21167
+ return task2.attaching === 2 ? "Connecting Ordewell's tools, retry" : "Connecting Ordewell's tools";
21168
+ }
20893
21169
  function approvalLabel(count) {
20894
21170
  if (!count) return null;
20895
21171
  return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
@@ -22359,7 +22635,7 @@ function createWakePolicy(options) {
22359
22635
  }
22360
22636
 
22361
22637
  // src/services/runnerExit.ts
22362
- var USAGE_LIMIT_RE = /\b(?:usage|session|weekly|daily|monthly) limit\b|\brate limit (?:exceeded|reached)\b|\brate[- ]limited\b|\blimit (?:will )?reset\b|\bquota (?:exceeded|reached)\b|\btoo many requests\b/i;
22638
+ var USAGE_LIMIT_RE = /\b(?:usage|session|weekly|daily|monthly) limit\b|\byou['’]ve hit your (?:[\w'’-]+ ){0,3}?limit\b|\brate limit (?:exceeded|reached)\b|\brate[- ]limited\b|\blimit (?:will )?reset\b|\bquota (?:exceeded|reached)\b|\btoo many requests\b/i;
22363
22639
  var USAGE_LIMIT_TAIL = 4096;
22364
22640
  function classifyRunnerStop(said2) {
22365
22641
  if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
@@ -22720,8 +22996,8 @@ function compactionCut(transcript) {
22720
22996
  }
22721
22997
 
22722
22998
  // src/services/skillInvocation.ts
22723
- var os10 = __toESM(require("os"));
22724
- function abbreviateHome(file, home = os10.homedir(), platform = process.platform) {
22999
+ var os9 = __toESM(require("os"));
23000
+ function abbreviateHome(file, home = os9.homedir(), platform = process.platform) {
22725
23001
  const win = platform === "win32";
22726
23002
  const normal = (p) => win ? p.replace(/\\/g, "/") : p;
22727
23003
  const shown = normal(file);
@@ -22736,13 +23012,13 @@ function modelInvocablePlannerSkills(skills) {
22736
23012
  function modelInvocableTaskSkills(skills) {
22737
23013
  return skills.filter((skill) => skill.appliesTo === "task" && skill.modelInvocable);
22738
23014
  }
22739
- function snapshotSkill(skill, invokedBy, home = os10.homedir()) {
23015
+ function snapshotSkill(skill, invokedBy, home = os9.homedir()) {
22740
23016
  return { invokedBy, name: skill.name, source: skill.source, path: abbreviateHome(skill.path, home), content: skill.content };
22741
23017
  }
22742
23018
  function attachSkill(skill, home) {
22743
23019
  return { ...snapshotSkill(skill, "user", home), content: "", attaches: { description: skill.description } };
22744
23020
  }
22745
- function resolveSkillInvocation(text6, skillsService, home = os10.homedir()) {
23021
+ function resolveSkillInvocation(text6, skillsService, home = os9.homedir()) {
22746
23022
  const skills = skillTokens(text6).flatMap(({ name }) => {
22747
23023
  const skill = skillsService.findSkill(name);
22748
23024
  if (!skill?.userInvocable) return [];
@@ -22806,33 +23082,33 @@ ${skillBlock(entry.skill)}`, timestamp: entry.timestamp });
22806
23082
  }
22807
23083
 
22808
23084
  // src/services/taskSkills.ts
22809
- var path32 = __toESM(require("path"));
23085
+ var path34 = __toESM(require("path"));
22810
23086
 
22811
23087
  // src/services/SkillsService.ts
22812
23088
  var import_crypto12 = require("crypto");
22813
23089
  var fs11 = __toESM(require("fs"));
22814
- var path31 = __toESM(require("path"));
23090
+ var path33 = __toESM(require("path"));
22815
23091
 
22816
23092
  // src/services/builtinSkills.ts
22817
- var path27 = __toESM(require("path"));
23093
+ var path29 = __toESM(require("path"));
22818
23094
  var import_url = require("url");
22819
23095
  function builtinSkillsDir() {
22820
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path27.dirname((0, import_url.fileURLToPath)(
23096
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path29.dirname((0, import_url.fileURLToPath)(
22821
23097
  // @ts-expect-error - import.meta is only valid under an ESM module setting
22822
23098
  __filename
22823
23099
  ));
22824
- return path27.join(dir, "..", "skills");
23100
+ return path29.join(dir, "..", "skills");
22825
23101
  }
22826
23102
 
22827
23103
  // src/services/repoGroup.ts
22828
23104
  var fs10 = __toESM(require("fs"));
22829
- var path30 = __toESM(require("path"));
23105
+ var path32 = __toESM(require("path"));
22830
23106
 
22831
23107
  // src/services/worktreeBootstrap.ts
22832
23108
  var import_child_process8 = require("child_process");
22833
23109
  var import_util4 = require("util");
22834
23110
  var fs9 = __toESM(require("fs"));
22835
- var path29 = __toESM(require("path"));
23111
+ var path31 = __toESM(require("path"));
22836
23112
 
22837
23113
  // src/services/gitExec.ts
22838
23114
  var import_child_process7 = require("child_process");
@@ -22914,7 +23190,7 @@ function subcommandOf2(args) {
22914
23190
 
22915
23191
  // src/services/worktreeLink.ts
22916
23192
  var fs8 = __toESM(require("fs"));
22917
- var path28 = __toESM(require("path"));
23193
+ var path30 = __toESM(require("path"));
22918
23194
  function linkPath(source, target, platform, hardLink = fs8.linkSync) {
22919
23195
  const isDir = fs8.statSync(source).isDirectory();
22920
23196
  if (platform !== "win32") {
@@ -22939,8 +23215,8 @@ function mirrorDir(source, target, opts) {
22939
23215
  const mirror = (from, to, prefix, top) => {
22940
23216
  fs8.mkdirSync(to, { recursive: true });
22941
23217
  for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
22942
- const name = path28.join(prefix, entry.name);
22943
- const at = { from: path28.join(from, entry.name), to: path28.join(to, entry.name), name };
23218
+ const name = path30.join(prefix, entry.name);
23219
+ const at = { from: path30.join(from, entry.name), to: path30.join(to, entry.name), name };
22944
23220
  if (entry.isSymbolicLink()) links.push(at);
22945
23221
  else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
22946
23222
  else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
@@ -22960,16 +23236,16 @@ function mirrorDir(source, target, opts) {
22960
23236
  }
22961
23237
  function relink(from, to, opts) {
22962
23238
  const text6 = fs8.readlinkSync(from);
22963
- const pointee = path28.isAbsolute(text6) ? rebased(text6, opts) : text6;
23239
+ const pointee = path30.isAbsolute(text6) ? rebased(text6, opts) : text6;
22964
23240
  if (opts.platform !== "win32") {
22965
- if (!fs8.existsSync(path28.resolve(path28.dirname(to), pointee)) && fs8.existsSync(path28.resolve(path28.dirname(from), pointee))) {
22966
- fs8.symlinkSync(rebased(path28.resolve(path28.dirname(from), pointee), opts), to);
23241
+ if (!fs8.existsSync(path30.resolve(path30.dirname(to), pointee)) && fs8.existsSync(path30.resolve(path30.dirname(from), pointee))) {
23242
+ fs8.symlinkSync(rebased(path30.resolve(path30.dirname(from), pointee), opts), to);
22967
23243
  return "symlink";
22968
23244
  }
22969
23245
  fs8.symlinkSync(pointee, to);
22970
23246
  return "symlink";
22971
23247
  }
22972
- const resolved = fs8.existsSync(path28.resolve(path28.dirname(to), pointee)) ? path28.resolve(path28.dirname(to), pointee) : rebased(path28.resolve(path28.dirname(from), pointee), opts);
23248
+ const resolved = fs8.existsSync(path30.resolve(path30.dirname(to), pointee)) ? path30.resolve(path30.dirname(to), pointee) : rebased(path30.resolve(path30.dirname(from), pointee), opts);
22973
23249
  try {
22974
23250
  fs8.statSync(resolved);
22975
23251
  } catch {
@@ -22978,9 +23254,9 @@ function relink(from, to, opts) {
22978
23254
  return linkPath(resolved, to, opts.platform, opts.hardLink);
22979
23255
  }
22980
23256
  function rebased(pointee, opts) {
22981
- const rel = path28.relative(opts.from, pointee);
22982
- const inside2 = rel !== "" && !rel.startsWith("..") && !path28.isAbsolute(rel);
22983
- return inside2 && !rel.split(/[\\/]/).includes("node_modules") ? path28.join(opts.to, rel) : pointee;
23257
+ const rel = path30.relative(opts.from, pointee);
23258
+ const inside2 = rel !== "" && !rel.startsWith("..") && !path30.isAbsolute(rel);
23259
+ return inside2 && !rel.split(/[\\/]/).includes("node_modules") ? path30.join(opts.to, rel) : pointee;
22984
23260
  }
22985
23261
 
22986
23262
  // src/services/worktreeBootstrap.ts
@@ -22997,7 +23273,7 @@ function lexists(target) {
22997
23273
  return false;
22998
23274
  }
22999
23275
  }
23000
- function listDir(dir) {
23276
+ function listDir2(dir) {
23001
23277
  try {
23002
23278
  return fs9.readdirSync(dir).sort();
23003
23279
  } catch {
@@ -23017,18 +23293,18 @@ function matchLinks(root, patterns) {
23017
23293
  }
23018
23294
  function segmentMatches(root, base, segment) {
23019
23295
  const under = (name) => base ? `${base}/${name}` : name;
23020
- if (!/[*?]/.test(segment)) return lexists(path29.join(root, base, segment)) ? [under(segment)] : [];
23296
+ if (!/[*?]/.test(segment)) return lexists(path31.join(root, base, segment)) ? [under(segment)] : [];
23021
23297
  const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
23022
- return listDir(path29.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
23298
+ return listDir2(path31.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
23023
23299
  }
23024
23300
  function installDirs(root) {
23025
23301
  const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
23026
- return ["node_modules", ...packages].filter((dir) => lexists(path29.join(root, dir)));
23302
+ return ["node_modules", ...packages].filter((dir) => lexists(path31.join(root, dir)));
23027
23303
  }
23028
23304
  function workspaceGlobs(root) {
23029
23305
  let manifest;
23030
23306
  try {
23031
- manifest = JSON.parse(fs9.readFileSync(path29.join(root, "package.json"), "utf8"));
23307
+ manifest = JSON.parse(fs9.readFileSync(path31.join(root, "package.json"), "utf8"));
23032
23308
  } catch {
23033
23309
  return [];
23034
23310
  }
@@ -23043,19 +23319,19 @@ async function bootstrap(repo, cwd, opts) {
23043
23319
  const linked = [];
23044
23320
  const copied = [];
23045
23321
  const link = (name) => {
23046
- const target = path29.join(cwd, name);
23322
+ const target = path31.join(cwd, name);
23047
23323
  if (lexists(target)) return;
23048
- fs9.mkdirSync(path29.dirname(target), { recursive: true });
23049
- if (linkPath(path29.join(repo.root, name), target, opts.platform) === "copy") copied.push(name);
23324
+ fs9.mkdirSync(path31.dirname(target), { recursive: true });
23325
+ if (linkPath(path31.join(repo.root, name), target, opts.platform) === "copy") copied.push(name);
23050
23326
  linked.push(name);
23051
23327
  };
23052
23328
  const mirror = (name) => {
23053
- const target = path29.join(cwd, name);
23054
- if (lexists(target) || !fs9.existsSync(path29.dirname(target))) return;
23055
- const source = path29.join(repo.root, name);
23329
+ const target = path31.join(cwd, name);
23330
+ if (lexists(target) || !fs9.existsSync(path31.dirname(target))) return;
23331
+ const source = path31.join(repo.root, name);
23056
23332
  if (!fs9.lstatSync(source).isDirectory()) return link(name);
23057
23333
  const mirrored = mirrorDir(source, target, { platform: opts.platform, from: repo.root, to: cwd });
23058
- copied.push(...mirrored.map((entry) => path29.join(name, entry)));
23334
+ copied.push(...mirrored.map((entry) => path31.join(name, entry)));
23059
23335
  linked.push(name);
23060
23336
  };
23061
23337
  if (!setup) {
@@ -23090,12 +23366,12 @@ async function runSetup(command, repo, cwd, resolvePath) {
23090
23366
 
23091
23367
  // src/services/repoGroup.ts
23092
23368
  function hasGitEntry(dir) {
23093
- return fs10.existsSync(path30.join(dir, ".git"));
23369
+ return fs10.existsSync(path32.join(dir, ".git"));
23094
23370
  }
23095
23371
  function groupPathOf(listed3) {
23096
23372
  const slashed = listed3.trim().replace(/\\/g, "/");
23097
- if (path30.posix.isAbsolute(slashed) || path30.win32.isAbsolute(slashed)) return null;
23098
- const rel = path30.posix.normalize(slashed).replace(/\/+$/, "");
23373
+ if (path32.posix.isAbsolute(slashed) || path32.win32.isAbsolute(slashed)) return null;
23374
+ const rel = path32.posix.normalize(slashed).replace(/\/+$/, "");
23099
23375
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
23100
23376
  }
23101
23377
  function childDirs(dir) {
@@ -23106,14 +23382,14 @@ function childDirs(dir) {
23106
23382
  }
23107
23383
  }
23108
23384
  function groupPaths(workspaceRoot, listed3) {
23109
- if (listed3.length === 0) return childDirs(workspaceRoot).filter((name) => hasGitEntry(path30.join(workspaceRoot, name)));
23110
- const paths = listed3.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path30.join(workspaceRoot, rel)));
23385
+ if (listed3.length === 0) return childDirs(workspaceRoot).filter((name) => hasGitEntry(path32.join(workspaceRoot, name)));
23386
+ const paths = listed3.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path32.join(workspaceRoot, rel)));
23111
23387
  return [...new Set(paths)].sort();
23112
23388
  }
23113
23389
  function workspaceRepoGroup(workspaceRoot, listed3) {
23114
- for (let dir = path30.resolve(workspaceRoot); ; dir = path30.dirname(dir)) {
23390
+ for (let dir = path32.resolve(workspaceRoot); ; dir = path32.dirname(dir)) {
23115
23391
  if (hasGitEntry(dir)) return [SELF_REPO];
23116
- if (path30.dirname(dir) === dir) break;
23392
+ if (path32.dirname(dir) === dir) break;
23117
23393
  }
23118
23394
  return groupPaths(workspaceRoot, listed3);
23119
23395
  }
@@ -23194,8 +23470,8 @@ function copyDirSync(src, dest) {
23194
23470
  fs11.mkdirSync(dest, { recursive: true });
23195
23471
  const entries = fs11.readdirSync(src, { withFileTypes: true });
23196
23472
  for (const entry of entries) {
23197
- const srcPath = path31.join(src, entry.name);
23198
- const destPath = path31.join(dest, entry.name);
23473
+ const srcPath = path33.join(src, entry.name);
23474
+ const destPath = path33.join(dest, entry.name);
23199
23475
  if (entry.isDirectory()) copyDirSync(srcPath, destPath);
23200
23476
  else fs11.copyFileSync(srcPath, destPath);
23201
23477
  }
@@ -23219,7 +23495,7 @@ function readDir(dir, source) {
23219
23495
  if (!fs11.existsSync(dir)) return { skills, invalid };
23220
23496
  for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
23221
23497
  if (!entry.isDirectory()) continue;
23222
- const skillFile = path31.join(dir, entry.name, "SKILL.md");
23498
+ const skillFile = path33.join(dir, entry.name, "SKILL.md");
23223
23499
  if (!fs11.existsSync(skillFile)) continue;
23224
23500
  if (!isSkillName(entry.name)) {
23225
23501
  invalid.push({ name: entry.name, path: skillFile, source, reason: INVALID_SKILL_NAME_REASON });
@@ -23238,15 +23514,15 @@ function skillFileIn(dir, name) {
23238
23514
  return void 0;
23239
23515
  }
23240
23516
  if (!entries.some((entry) => entry.isDirectory() && entry.name === name)) return void 0;
23241
- const skillFile = path31.join(dir, name, "SKILL.md");
23517
+ const skillFile = path33.join(dir, name, "SKILL.md");
23242
23518
  return fs11.existsSync(skillFile) ? skillFile : void 0;
23243
23519
  }
23244
23520
  function skillsDirOf(root) {
23245
- return path31.join(root, STATE_DIR, "skills");
23521
+ return path33.join(root, STATE_DIR, "skills");
23246
23522
  }
23247
23523
  function workspaceSkillRoots(workspaceRoot, repos, checkout = workspaceRoot) {
23248
23524
  if (repos.length === 0 || repos.includes(SELF_REPO)) return [checkout];
23249
- return [workspaceRoot, ...repos.map((repo) => path31.join(checkout, repo))];
23525
+ return [workspaceRoot, ...repos.map((repo) => path33.join(checkout, repo))];
23250
23526
  }
23251
23527
  var SkillsService = class _SkillsService {
23252
23528
  workspaceRoots;
@@ -23269,11 +23545,11 @@ var SkillsService = class _SkillsService {
23269
23545
  return this.workspaceRoots.map(skillsDirOf);
23270
23546
  }
23271
23547
  globalDir() {
23272
- return path31.join(globalDataDir(), "skills");
23548
+ return path33.join(globalDataDir(), "skills");
23273
23549
  }
23274
23550
  readSeedManifest() {
23275
23551
  try {
23276
- const parsed = JSON.parse(fs11.readFileSync(path31.join(this.globalDir(), SEED_MANIFEST), "utf8"));
23552
+ const parsed = JSON.parse(fs11.readFileSync(path33.join(this.globalDir(), SEED_MANIFEST), "utf8"));
23277
23553
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23278
23554
  return Object.fromEntries(
23279
23555
  Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
@@ -23289,7 +23565,7 @@ var SkillsService = class _SkillsService {
23289
23565
  manifest[name] = hash;
23290
23566
  try {
23291
23567
  fs11.mkdirSync(this.globalDir(), { recursive: true });
23292
- fs11.writeFileSync(path31.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
23568
+ fs11.writeFileSync(path33.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
23293
23569
  } catch (err) {
23294
23570
  console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
23295
23571
  }
@@ -23300,11 +23576,11 @@ var SkillsService = class _SkillsService {
23300
23576
  return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
23301
23577
  }
23302
23578
  seed(name) {
23303
- const dest = path31.join(this.globalDir(), name);
23579
+ const dest = path33.join(this.globalDir(), name);
23304
23580
  const exists = fs11.existsSync(dest);
23305
23581
  let srcDir;
23306
23582
  try {
23307
- srcDir = path31.join(builtinSkillsDir(), name);
23583
+ srcDir = path33.join(builtinSkillsDir(), name);
23308
23584
  } catch (err) {
23309
23585
  if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
23310
23586
  return false;
@@ -23313,9 +23589,9 @@ var SkillsService = class _SkillsService {
23313
23589
  if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
23314
23590
  return false;
23315
23591
  }
23316
- const srcHash = fileHash(path31.join(srcDir, "SKILL.md"));
23592
+ const srcHash = fileHash(path33.join(srcDir, "SKILL.md"));
23317
23593
  if (exists) {
23318
- const currentHash = fileHash(path31.join(dest, "SKILL.md"));
23594
+ const currentHash = fileHash(path33.join(dest, "SKILL.md"));
23319
23595
  if (currentHash !== void 0 && currentHash === srcHash) {
23320
23596
  this.recordSeed(name, currentHash);
23321
23597
  return false;
@@ -23359,7 +23635,7 @@ var SkillsService = class _SkillsService {
23359
23635
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
23360
23636
  let raw;
23361
23637
  try {
23362
- raw = readSkillText(path31.join(dir, "SKILL.md"));
23638
+ raw = readSkillText(path33.join(dir, "SKILL.md"));
23363
23639
  } catch {
23364
23640
  return false;
23365
23641
  }
@@ -23367,7 +23643,7 @@ var SkillsService = class _SkillsService {
23367
23643
  }
23368
23644
  pruneRetired() {
23369
23645
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
23370
- const dest = path31.join(this.globalDir(), name);
23646
+ const dest = path33.join(this.globalDir(), name);
23371
23647
  if (!fs11.existsSync(dest)) continue;
23372
23648
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
23373
23649
  try {
@@ -23505,12 +23781,12 @@ var runGit = execFileWithTimeout(1e4);
23505
23781
  async function committedInHead(files) {
23506
23782
  const byDir = /* @__PURE__ */ new Map();
23507
23783
  for (const file of new Set(files)) {
23508
- const dir = path32.dirname(path32.dirname(file));
23784
+ const dir = path34.dirname(path34.dirname(file));
23509
23785
  byDir.set(dir, [...byDir.get(dir) ?? [], file]);
23510
23786
  }
23511
23787
  const result = /* @__PURE__ */ new Map();
23512
23788
  await Promise.all([...byDir].map(async ([dir, inDir]) => {
23513
- const relative17 = (file) => path32.relative(dir, file).split(path32.sep).join("/");
23789
+ const relative17 = (file) => path34.relative(dir, file).split(path34.sep).join("/");
23514
23790
  try {
23515
23791
  const { stdout } = await runGit("git", ["ls-tree", "--name-only", "HEAD", "--", ...inDir.map(relative17)], { cwd: dir, env: cleanEnv() });
23516
23792
  const listed3 = new Set(stdout.split("\n").map((line) => line.trim()).filter(Boolean));
@@ -23530,9 +23806,9 @@ function plannedSkillLookup(skillsAt, workspaceRoot, layout) {
23530
23806
  searchedDirs: () => lookup2.searchedDirs(),
23531
23807
  listSkills: () => lookup2.listSkills(),
23532
23808
  uncommitted: async (skills) => {
23533
- const checked = skills.filter((skill) => skill.source === "workspace" && !(readInPlace && path32.dirname(path32.dirname(skill.path)) === readInPlace));
23809
+ const checked = skills.filter((skill) => skill.source === "workspace" && !(readInPlace && path34.dirname(path34.dirname(skill.path)) === readInPlace));
23534
23810
  const committed = await committedInHead(checked.map((skill) => skill.path));
23535
- return new Map(checked.filter((skill) => committed.get(skill.path) === false).map((skill) => [skill.path, path32.relative(workspaceRoot, path32.dirname(skill.path)).split(path32.sep).join("/")]));
23811
+ return new Map(checked.filter((skill) => committed.get(skill.path) === false).map((skill) => [skill.path, path34.relative(workspaceRoot, path34.dirname(skill.path)).split(path34.sep).join("/")]));
23536
23812
  }
23537
23813
  };
23538
23814
  }
@@ -23545,13 +23821,13 @@ function checkPlanSkills(tasks, lookup2) {
23545
23821
  function checkTaskSkillsEdit(task2, skills, lookup2) {
23546
23822
  return check([{ taskId: task2.id, owner: `Task "${task2.title}"`, ...attached(skills) }], lookup2);
23547
23823
  }
23548
- function subtaskEntries(op, spec, path47 = []) {
23824
+ function subtaskEntries(op, spec, path50 = []) {
23549
23825
  const subtasks = spec?.subtasks;
23550
23826
  if (!Array.isArray(subtasks)) return [];
23551
23827
  return subtasks.flatMap((sub, i) => {
23552
23828
  if (typeof sub !== "object" || sub === null) return [];
23553
23829
  const child = sub;
23554
- const at = [...path47, typeof child.title === "string" && child.title.trim() ? `"${child.title}"` : `#${i + 1}`];
23830
+ const at = [...path50, typeof child.title === "string" && child.title.trim() ? `"${child.title}"` : `#${i + 1}`];
23555
23831
  return [{ op, owner: `subtask ${at.join(" > ")}`, ...attached(child.skills) }, ...subtaskEntries(op, child, at)];
23556
23832
  });
23557
23833
  }
@@ -27421,7 +27697,7 @@ var RUN_SETTING_KEYS = Object.keys(RUN_SETTING_CHECKS);
27421
27697
  function getSettingsPath() {
27422
27698
  const override = process.env.ORDEWELL_SETTINGS_PATH;
27423
27699
  if (override && override.trim()) return override.trim();
27424
- return path33.join(globalDataDir(), "settings.json");
27700
+ return path35.join(globalDataDir(), "settings.json");
27425
27701
  }
27426
27702
  function readPlannerModels(raw) {
27427
27703
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -27501,8 +27777,8 @@ var SettingsService = class {
27501
27777
  * a watch on the old one pointing at nothing.
27502
27778
  */
27503
27779
  watch(listener) {
27504
- const dir = path33.dirname(this.filePath);
27505
- const name = path33.basename(this.filePath);
27780
+ const dir = path35.dirname(this.filePath);
27781
+ const name = path35.basename(this.filePath);
27506
27782
  let seen = this.fileStamp();
27507
27783
  let timer;
27508
27784
  let dead = false;
@@ -27511,7 +27787,7 @@ var SettingsService = class {
27511
27787
  fs12.mkdirSync(dir, { recursive: true });
27512
27788
  watcher = fs12.watch(dir, (_event, changed) => {
27513
27789
  if (dead) return;
27514
- if (changed && changed !== name && changed !== path33.basename(dir)) return;
27790
+ if (changed && changed !== name && changed !== path35.basename(dir)) return;
27515
27791
  clearTimeout(timer);
27516
27792
  timer = setTimeout(() => {
27517
27793
  const stamp2 = this.fileStamp();
@@ -27744,7 +28020,7 @@ var SettingsService = class {
27744
28020
  if (this.unreadable) {
27745
28021
  throw new Error(`Cannot save settings: ${this.filePath} could not be read, so it will not be overwritten. Fix or remove it and try again.`);
27746
28022
  }
27747
- const dir = path33.dirname(this.filePath);
28023
+ const dir = path35.dirname(this.filePath);
27748
28024
  if (!fs12.existsSync(dir)) {
27749
28025
  fs12.mkdirSync(dir, { recursive: true });
27750
28026
  }
@@ -28188,7 +28464,7 @@ var BaseConfig = class {
28188
28464
  const { model, effort, override = "" } = configured;
28189
28465
  const named3 = isPlannerBackend(configured.setting ?? "") || isPlannerBackend(process.env.AI_PROVIDER ?? "");
28190
28466
  const pickedElsewhere = configured.pickedFor !== void 0 && configured.pickedFor !== provider;
28191
- const kept = configured.pickedFor === provider || !pickedElsewhere && (isApiProvider(provider) || named3 && !isForeignModel(provider, model, this.agentCatalog(provider)));
28467
+ const kept = configured.pickedFor === provider || !pickedElsewhere && (isApiProvider(provider) || named3 && !isForeignModel(provider, model, () => this.agentCatalog(provider)));
28192
28468
  if (kept) return { model: override || model, effort, source: override ? "request" : "configured" };
28193
28469
  const own = pickedElsewhere || named3 ? rememberedOrDefault(provider) : detectedAgentChoice(provider, configured);
28194
28470
  return !override || override === own.model ? own : { model: override, effort: "", source: "request" };
@@ -28197,7 +28473,8 @@ var BaseConfig = class {
28197
28473
  * The model ids a coding agent was last discovered serving, empty while
28198
28474
  * discovery is cold. A host that holds a runner catalog supplies it: it is
28199
28475
  * the only evidence that tells OpenCode's `provider/model` ids from an
28200
- * OpenRouter slug.
28476
+ * OpenRouter slug. Asked only when a named agent's configured model is
28477
+ * neither its alias nor its remembered pick.
28201
28478
  */
28202
28479
  agentCatalog(_provider) {
28203
28480
  return [];
@@ -28222,8 +28499,10 @@ function isVendorModelFor(agent, model) {
28222
28499
  return !discoveryOf(agent)?.providerQualifiedIds && SLUG_SHAPE.test(model);
28223
28500
  }
28224
28501
  function isForeignModel(agent, model, catalog) {
28225
- if (!model || isStableAlias(agent, model) || catalog.includes(model) || rememberedChoice(agent)?.model === model) return false;
28226
- return isVendorModelFor(agent, model) || catalog.length > 0 && SLUG_SHAPE.test(model);
28502
+ if (!model || isStableAlias(agent, model) || rememberedChoice(agent)?.model === model) return false;
28503
+ const listed3 = catalog();
28504
+ if (listed3.includes(model)) return false;
28505
+ return isVendorModelFor(agent, model) || listed3.length > 0 && SLUG_SHAPE.test(model);
28227
28506
  }
28228
28507
  function rememberedChoice(provider) {
28229
28508
  const entry = new SettingsService().getPlannerModel(provider);
@@ -28343,11 +28622,11 @@ function mergeOutcome(result, branch, group, repaired = []) {
28343
28622
  // src/services/taskDiffRead.ts
28344
28623
  var import_crypto13 = require("crypto");
28345
28624
  var fs13 = __toESM(require("fs"));
28346
- var path34 = __toESM(require("path"));
28625
+ var path36 = __toESM(require("path"));
28347
28626
  var import_os3 = require("os");
28348
28627
  var TASK_DIFF_UNAVAILABLE = "Task diff is not available for this run: no task-specific changes were recorded (shared runs cannot attribute workspace changes to one task).";
28349
28628
  function allowedPath(file) {
28350
- return file.length > 0 && !path34.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && !file.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".ordewell");
28629
+ return file.length > 0 && !path36.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && !file.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".ordewell");
28351
28630
  }
28352
28631
  function stats(raw) {
28353
28632
  return raw.split("\0").flatMap((row) => {
@@ -28356,8 +28635,8 @@ function stats(raw) {
28356
28635
  });
28357
28636
  }
28358
28637
  function inside(root, target) {
28359
- const rel = path34.relative(root, target);
28360
- return rel === "" || !rel.startsWith(`..${path34.sep}`) && rel !== ".." && !path34.isAbsolute(rel);
28638
+ const rel = path36.relative(root, target);
28639
+ return rel === "" || !rel.startsWith(`..${path36.sep}`) && rel !== ".." && !path36.isAbsolute(rel);
28361
28640
  }
28362
28641
  function unquoted(quoted4) {
28363
28642
  const bytes = [];
@@ -28405,10 +28684,10 @@ function patchFiles(patch) {
28405
28684
  const sections = patch.toString("utf8").split(/(?<![^\n])(?=diff --git )/).filter((section) => section.startsWith("diff --git "));
28406
28685
  const files = /* @__PURE__ */ new Map();
28407
28686
  for (const section of sections) {
28408
- const path47 = sectionPath(section);
28687
+ const path50 = sectionPath(section);
28409
28688
  const counts = sectionStats(section);
28410
- const earlier = files.get(path47);
28411
- files.set(path47, earlier ? { path: path47, section: earlier.section + section, added: summed(earlier.added, counts.added), removed: summed(earlier.removed, counts.removed) } : { path: path47, section, ...counts });
28689
+ const earlier = files.get(path50);
28690
+ files.set(path50, earlier ? { path: path50, section: earlier.section + section, added: summed(earlier.added, counts.added), removed: summed(earlier.removed, counts.removed) } : { path: path50, section, ...counts });
28412
28691
  }
28413
28692
  return [...files.values()];
28414
28693
  }
@@ -28478,11 +28757,11 @@ async function readTaskDiff(invoker, run, taskId, options) {
28478
28757
  files.push({ ...file, repo: repo.path, patch: () => git(reader, cwd, [...diffArgs, "--", `:(top,literal)${prefix}${file.path}`]) });
28479
28758
  }
28480
28759
  if (live3) {
28481
- const workspace = path34.join(entry.worktree, prefix);
28760
+ const workspace = path36.join(entry.worktree, prefix);
28482
28761
  const others = await git(reader, workspace, [...common, "ls-files", "--others", "--exclude-standard", "-z", "--", "."]);
28483
28762
  for (const file of others.split("\0").filter((f) => allowed(f))) {
28484
- const full = path34.join(workspace, file);
28485
- if (!inside(fs13.realpathSync(workspace), fs13.realpathSync(path34.dirname(full)))) continue;
28763
+ const full = path36.join(workspace, file);
28764
+ if (!inside(fs13.realpathSync(workspace), fs13.realpathSync(path36.dirname(full)))) continue;
28486
28765
  const untracked = async (numstat) => {
28487
28766
  const result = await tryGit(reader, workspace, [...common, "diff", "--no-index", "--no-renames", "--no-ext-diff", "--no-textconv", ...numstat ? ["--numstat", "-z"] : [], "--", import_os3.devNull, file]);
28488
28767
  if (!result.ok && result.code !== 1) throw new Error(result.stderr.trim() || "Could not read an untracked file diff.");
@@ -28512,11 +28791,11 @@ ${await file.patch()}`);
28512
28791
  var import_crypto14 = require("crypto");
28513
28792
  var import_child_process9 = require("child_process");
28514
28793
  var fs15 = __toESM(require("fs"));
28515
- var path36 = __toESM(require("path"));
28794
+ var path38 = __toESM(require("path"));
28516
28795
 
28517
28796
  // src/utils/prdStore.ts
28518
28797
  var fs14 = __toESM(require("fs"));
28519
- var path35 = __toESM(require("path"));
28798
+ var path37 = __toESM(require("path"));
28520
28799
  var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
28521
28800
  function extractPrdBlock(text6) {
28522
28801
  const match = PRD_BLOCK_RE.exec(text6);
@@ -28530,9 +28809,9 @@ function sanitizeSlug(raw) {
28530
28809
  return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
28531
28810
  }
28532
28811
  function savePrdMarkdown(workspace, slug, markdown) {
28533
- const dir = path35.join(workspace, ".scratch", sanitizeSlug(slug));
28812
+ const dir = path37.join(workspace, ".scratch", sanitizeSlug(slug));
28534
28813
  fs14.mkdirSync(dir, { recursive: true });
28535
- const file = path35.join(dir, "PRD.md");
28814
+ const file = path37.join(dir, "PRD.md");
28536
28815
  fs14.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
28537
28816
  return file;
28538
28817
  }
@@ -28564,12 +28843,12 @@ var NESTED_REPO_DEPTH = 2;
28564
28843
  var GITLINK_MODE = "160000";
28565
28844
  var READ_ONLY = ["--no-optional-locks", "-c", "core.fsmonitor=false"];
28566
28845
  function isInside(parent, child) {
28567
- const rel = path36.relative(parent, child);
28568
- return rel !== "" && !rel.startsWith("..") && !path36.isAbsolute(rel);
28846
+ const rel = path38.relative(parent, child);
28847
+ return rel !== "" && !rel.startsWith("..") && !path38.isAbsolute(rel);
28569
28848
  }
28570
28849
  function resolvesAsNamed(base, p) {
28571
28850
  try {
28572
- return fs15.realpathSync(p) === path36.join(fs15.realpathSync(base), path36.relative(base, p));
28851
+ return fs15.realpathSync(p) === path38.join(fs15.realpathSync(base), path38.relative(base, p));
28573
28852
  } catch {
28574
28853
  return false;
28575
28854
  }
@@ -28624,7 +28903,7 @@ function sameDir(a, b) {
28624
28903
  try {
28625
28904
  return fs15.realpathSync.native(p);
28626
28905
  } catch {
28627
- return path36.resolve(p);
28906
+ return path38.resolve(p);
28628
28907
  }
28629
28908
  };
28630
28909
  return real(a) === real(b);
@@ -28646,7 +28925,7 @@ function clearImmutableFlags(platform, dir) {
28646
28925
  (0, import_child_process9.spawnSync)("/usr/bin/chflags", ["-R", "nouchg", dir], { stdio: "ignore" });
28647
28926
  }
28648
28927
  function leftoverLinks(dir) {
28649
- const named3 = listDir(dir).filter((name) => LINKED_ARTIFACTS.has(name) || isEnvFile(name) || isLink(path36.join(dir, name)));
28928
+ const named3 = listDir2(dir).filter((name) => LINKED_ARTIFACTS.has(name) || isEnvFile(name) || isLink(path38.join(dir, name)));
28650
28929
  return [.../* @__PURE__ */ new Set([...named3, ...installDirs(dir)])];
28651
28930
  }
28652
28931
  function isLink(target) {
@@ -28719,7 +28998,7 @@ var GitWorktreeIsolation = class {
28719
28998
  const head = await this.headOf(root);
28720
28999
  const repo = { path: repoPath, head };
28721
29000
  if (scan.toplevel !== void 0) {
28722
- const top = path36.relative(workspaceRoot, scan.toplevel).split(path36.sep).join("/");
29001
+ const top = path38.relative(workspaceRoot, scan.toplevel).split(path38.sep).join("/");
28723
29002
  if (top !== "") repo.top = top;
28724
29003
  }
28725
29004
  if (enabled && (head.kind === "branch" || head.kind === "detached")) {
@@ -28776,9 +29055,9 @@ var GitWorktreeIsolation = class {
28776
29055
  async nestedRepos(workspaceRoot, toplevel) {
28777
29056
  const found = [];
28778
29057
  const walk = (rel, depth) => {
28779
- for (const name of childDirs(path36.join(workspaceRoot, rel))) {
29058
+ for (const name of childDirs(path38.join(workspaceRoot, rel))) {
28780
29059
  const child = rel ? `${rel}/${name}` : name;
28781
- if (hasGitEntry(path36.join(workspaceRoot, child))) found.push(child);
29060
+ if (hasGitEntry(path38.join(workspaceRoot, child))) found.push(child);
28782
29061
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
28783
29062
  }
28784
29063
  };
@@ -28796,7 +29075,7 @@ var GitWorktreeIsolation = class {
28796
29075
  const [meta, file] = entry.split(" ");
28797
29076
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
28798
29077
  }
28799
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path36.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
29078
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path38.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
28800
29079
  for (const entry of declared.stdout.split("\0")) {
28801
29080
  const value = entry.split("\n")[1];
28802
29081
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -28854,7 +29133,7 @@ var GitWorktreeIsolation = class {
28854
29133
  async acceptsWorktree(run, repo) {
28855
29134
  const dir = this.integrationDir(run, repo);
28856
29135
  ensureStateDirIgnored(run.workspaceRoot);
28857
- fs15.mkdirSync(path36.dirname(dir), { recursive: true });
29136
+ fs15.mkdirSync(path38.dirname(dir), { recursive: true });
28858
29137
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
28859
29138
  await this.removeWorktreeDir(run, repo, dir, []);
28860
29139
  return added.ok;
@@ -28869,12 +29148,12 @@ var GitWorktreeIsolation = class {
28869
29148
  if (isolated.includes(SELF_REPO)) return [];
28870
29149
  const shared2 = [];
28871
29150
  const walk = (rel) => {
28872
- for (const name of listDir(path36.join(workspaceRoot, rel))) {
29151
+ for (const name of listDir2(path38.join(workspaceRoot, rel))) {
28873
29152
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
28874
29153
  const child = rel ? `${rel}/${name}` : name;
28875
29154
  if (isolated.includes(child)) continue;
28876
29155
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
28877
- else if (resolves(path36.join(workspaceRoot, child))) shared2.push(child);
29156
+ else if (resolves(path38.join(workspaceRoot, child))) shared2.push(child);
28878
29157
  }
28879
29158
  };
28880
29159
  walk("");
@@ -28914,9 +29193,9 @@ var GitWorktreeIsolation = class {
28914
29193
  try {
28915
29194
  ensureStateDirIgnored(run.workspaceRoot);
28916
29195
  for (const repo of run.repos) {
28917
- const worktree = path36.join(dir, repo.path);
29196
+ const worktree = path38.join(dir, repo.path);
28918
29197
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
28919
- fs15.mkdirSync(path36.dirname(worktree), { recursive: true });
29198
+ fs15.mkdirSync(path38.dirname(worktree), { recursive: true });
28920
29199
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
28921
29200
  record.repos[repo.path] = { worktree, linked: [], baseRef: tip };
28922
29201
  }
@@ -28941,13 +29220,13 @@ var GitWorktreeIsolation = class {
28941
29220
  setupFailed.push({ repo: repo.path, detail: err.detail });
28942
29221
  }
28943
29222
  entry.linked = boot.linked;
28944
- copied.push(...boot.copied.map((name) => path36.posix.join(repo.path, name)));
29223
+ copied.push(...boot.copied.map((name) => path38.posix.join(repo.path, name)));
28945
29224
  }
28946
29225
  for (const rel of run.shared) {
28947
- const source = path36.join(run.workspaceRoot, rel);
28948
- const target = path36.join(cwd, rel);
29226
+ const source = path38.join(run.workspaceRoot, rel);
29227
+ const target = path38.join(cwd, rel);
28949
29228
  if (!resolves(source) || lexists(target)) continue;
28950
- fs15.mkdirSync(path36.dirname(target), { recursive: true });
29229
+ fs15.mkdirSync(path38.dirname(target), { recursive: true });
28951
29230
  if (linkPath(source, target, this.platform) === "copy") copied.push(rel);
28952
29231
  const entry = self ? record.repos[SELF_REPO] : void 0;
28953
29232
  if (entry) entry.linked.push(rel);
@@ -28979,7 +29258,7 @@ var GitWorktreeIsolation = class {
28979
29258
  */
28980
29259
  async clearLeftovers(run, task2, names) {
28981
29260
  const removal = noRemoval();
28982
- const holder = Object.values(run.tasks).find((r) => r.taskId !== task2.id && (r.branch === names.branch || path36.resolve(r.workspace) === path36.resolve(names.dir) || isInside(r.workspace, names.dir)));
29261
+ const holder = Object.values(run.tasks).find((r) => r.taskId !== task2.id && (r.branch === names.branch || path38.resolve(r.workspace) === path38.resolve(names.dir) || isInside(r.workspace, names.dir)));
28983
29262
  if (holder) throw new Error(`Its worktree path or branch is held by the record of task ${holder.order} (${holder.title}); Ordewell will not remove it`);
28984
29263
  const found = [];
28985
29264
  for (const repo of run.repos) {
@@ -28989,14 +29268,14 @@ var GitWorktreeIsolation = class {
28989
29268
  worktree: /^worktree (.+)$/m.exec(block)?.[1],
28990
29269
  branch: /^branch refs\/heads\/(.+)$/m.exec(block)?.[1]
28991
29270
  }));
28992
- const at = /* @__PURE__ */ new Set([path36.join(names.dir, repo.path)]);
29271
+ const at = /* @__PURE__ */ new Set([path38.join(names.dir, repo.path)]);
28993
29272
  for (const entry of registered) {
28994
- if (entry.worktree && entry.branch === names.branch && isInside(this.runRoot(run), path36.resolve(entry.worktree))) at.add(entry.worktree);
29273
+ if (entry.worktree && entry.branch === names.branch && isInside(this.runRoot(run), path38.resolve(entry.worktree))) at.add(entry.worktree);
28995
29274
  }
28996
- const base = { branch: names.branch, name: path36.basename(names.dir), title: task2.title };
29275
+ const base = { branch: names.branch, name: path38.basename(names.dir), title: task2.title };
28997
29276
  for (const worktree of at) {
28998
29277
  if (!fs15.existsSync(worktree)) continue;
28999
- const linked = leftoverLinks(path36.join(worktree, await this.prefixOf(repo)));
29278
+ const linked = leftoverLinks(path38.join(worktree, await this.prefixOf(repo)));
29000
29279
  found.push({ repo, worktree, target: { ...base, worktree, linked } });
29001
29280
  }
29002
29281
  found.push({ repo, worktree: names.dir, target: { ...base, linked: [] } });
@@ -29089,7 +29368,7 @@ var GitWorktreeIsolation = class {
29089
29368
  */
29090
29369
  async inWorkspacePlace(repo, worktree) {
29091
29370
  const prefix = await this.prefixOf(repo);
29092
- return prefix ? path36.join(worktree, prefix) : worktree;
29371
+ return prefix ? path38.join(worktree, prefix) : worktree;
29093
29372
  }
29094
29373
  async prefixOf(repo) {
29095
29374
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -29619,15 +29898,15 @@ var GitWorktreeIsolation = class {
29619
29898
  return landingMessage(kind, from, this.deps.plannedWith?.() ?? false);
29620
29899
  }
29621
29900
  integrationDir(run, repo) {
29622
- return path36.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
29901
+ return path38.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
29623
29902
  }
29624
29903
  async ensureIntegrationWorktree(run, repo) {
29625
29904
  return this.admin(run.workspaceRoot, async () => {
29626
29905
  const dir = this.integrationDir(run, repo);
29627
- if (fs15.existsSync(path36.join(dir, ".git"))) return dir;
29906
+ if (fs15.existsSync(path38.join(dir, ".git"))) return dir;
29628
29907
  await this.tryGit(repo.root, ["worktree", "prune"]);
29629
29908
  ensureStateDirIgnored(run.workspaceRoot);
29630
- fs15.mkdirSync(path36.dirname(dir), { recursive: true });
29909
+ fs15.mkdirSync(path38.dirname(dir), { recursive: true });
29631
29910
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
29632
29911
  return dir;
29633
29912
  });
@@ -29650,7 +29929,7 @@ var GitWorktreeIsolation = class {
29650
29929
  const removal = noRemoval();
29651
29930
  for (const repo of opts.preserve === false || record.readOnly ? [] : run.repos) {
29652
29931
  const entry = record.repos[repo.path];
29653
- const target = { worktree: entry?.worktree, linked: entry?.linked ?? [], branch: record.branch, name: path36.basename(record.workspace), title: record.title, commit: record.commit };
29932
+ const target = { worktree: entry?.worktree, linked: entry?.linked ?? [], branch: record.branch, name: path38.basename(record.workspace), title: record.title, commit: record.commit };
29654
29933
  try {
29655
29934
  const kept = await this.preserve(run, repo, target, opts.intent);
29656
29935
  if (kept) removal.preserved.push({ task: task2, ...kept });
@@ -29731,7 +30010,7 @@ var GitWorktreeIsolation = class {
29731
30010
  return;
29732
30011
  }
29733
30012
  for (const entry of entries) {
29734
- const target = path36.join(dir, entry.name);
30013
+ const target = path38.join(dir, entry.name);
29735
30014
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
29736
30015
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
29737
30016
  }
@@ -29753,20 +30032,20 @@ var GitWorktreeIsolation = class {
29753
30032
  return false;
29754
30033
  }
29755
30034
  };
29756
- const owned = new Set(Object.values(run.tasks).map((r) => path36.resolve(r.workspace)));
30035
+ const owned = new Set(Object.values(run.tasks).map((r) => path38.resolve(r.workspace)));
29757
30036
  const root = this.runRoot(run);
29758
30037
  if (fs15.existsSync(root)) {
29759
30038
  for (const entry of fs15.readdirSync(root)) {
29760
- const dir = path36.join(root, entry);
29761
- if (entry === INTEGRATION_DIR || owned.has(path36.resolve(dir))) continue;
30039
+ const dir = path38.join(root, entry);
30040
+ if (entry === INTEGRATION_DIR || owned.has(path38.resolve(dir))) continue;
29762
30041
  let kept = true;
29763
30042
  for (const repo of run.repos) {
29764
- const worktree = path36.join(dir, repo.path);
29765
- const linked = leftoverLinks(path36.join(worktree, await this.prefixOf(repo)));
30043
+ const worktree = path38.join(dir, repo.path);
30044
+ const linked = leftoverLinks(path38.join(worktree, await this.prefixOf(repo)));
29766
30045
  if (!await keep2(repo, { worktree, linked, branch: null, name: entry, title: entry }, worktree)) kept = false;
29767
30046
  }
29768
30047
  if (!kept) continue;
29769
- for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path36.join(dir, repo.path), []);
30048
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path38.join(dir, repo.path), []);
29770
30049
  this.removeTaskWorkspace(run, dir);
29771
30050
  }
29772
30051
  }
@@ -29784,13 +30063,13 @@ var GitWorktreeIsolation = class {
29784
30063
  }
29785
30064
  async removeWorktreeDir(run, repo, dir, linked) {
29786
30065
  for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
29787
- const target = path36.join(dir, name);
29788
- if (!resolvesAsNamed(this.runRoot(run), path36.dirname(target))) continue;
30066
+ const target = path38.join(dir, name);
30067
+ if (!resolvesAsNamed(this.runRoot(run), path38.dirname(target))) continue;
29789
30068
  this.unlinkIfLink(target);
29790
30069
  this.unlinkLinksUnder(target);
29791
30070
  }
29792
30071
  for (const name of resolvesAsNamed(this.runRoot(run), dir) ? fs15.readdirSync(dir) : []) {
29793
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path36.join(dir, name));
30072
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path38.join(dir, name));
29794
30073
  }
29795
30074
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
29796
30075
  if (fs15.existsSync(dir) && isInside(this.runRoot(run), dir)) {
@@ -29798,7 +30077,7 @@ var GitWorktreeIsolation = class {
29798
30077
  fs15.rmSync(dir, { recursive: true, force: true });
29799
30078
  }
29800
30079
  await this.tryGit(repo.root, ["worktree", "prune"]);
29801
- for (let parent = path36.dirname(dir); isInside(this.runRoot(run), parent); parent = path36.dirname(parent)) this.removeIfEmpty(parent);
30080
+ for (let parent = path38.dirname(dir); isInside(this.runRoot(run), parent); parent = path38.dirname(parent)) this.removeIfEmpty(parent);
29802
30081
  }
29803
30082
  unlinkIfLink(target) {
29804
30083
  try {
@@ -29813,14 +30092,14 @@ var GitWorktreeIsolation = class {
29813
30092
  }
29814
30093
  }
29815
30094
  runRoot(run) {
29816
- return path36.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
30095
+ return path38.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
29817
30096
  }
29818
30097
  namesFor(run, task2) {
29819
30098
  const slug = sanitizeSlug(task2.title).slice(0, 40).replace(/-+$/, "") || "task";
29820
30099
  let name = `${task2.order}-${slug}`;
29821
30100
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task2.id && r.branch === `ordewell/${run.id}/${name}`);
29822
30101
  if (clash) name = `${name}-${sanitizeSlug(task2.id) || "x"}`;
29823
- return { branch: `ordewell/${run.id}/${name}`, dir: path36.join(this.runRoot(run), name) };
30102
+ return { branch: `ordewell/${run.id}/${name}`, dir: path38.join(this.runRoot(run), name) };
29824
30103
  }
29825
30104
  async unmergedPaths(cwd) {
29826
30105
  const listed3 = await this.tryGit(cwd, ["diff", "--name-only", "-z", "--diff-filter=U"]);
@@ -29834,7 +30113,7 @@ var GitWorktreeIsolation = class {
29834
30113
  async rebaseInProgress(cwd) {
29835
30114
  for (const dir of ["rebase-merge", "rebase-apply"]) {
29836
30115
  const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
29837
- if (found.ok && fs15.existsSync(path36.resolve(cwd, found.stdout.trim()))) return true;
30116
+ if (found.ok && fs15.existsSync(path38.resolve(cwd, found.stdout.trim()))) return true;
29838
30117
  }
29839
30118
  return false;
29840
30119
  }
@@ -29908,6 +30187,29 @@ function describeIsolationPreview(preview) {
29908
30187
  }
29909
30188
  }
29910
30189
 
30190
+ // src/services/repositoryRunnerConfig.ts
30191
+ var RUNNER_CONFIG_ROOTS = [".mcp.json", ".claude", ".claude-plugin", "opencode.json", "opencode.jsonc", ".opencode", ".codex"];
30192
+ function repositoryRunnerConfigPaths(paths) {
30193
+ return [...new Set(paths.filter(
30194
+ (path50) => (
30195
+ // Project MCP servers start commands or send data to a repository-chosen URL.
30196
+ path50 === ".mcp.json" || // Settings can run hooks and helpers, load plugins, or redirect traffic.
30197
+ path50 === ".claude/settings.json" || // Local settings have the same execution capabilities as project settings.
30198
+ path50 === ".claude/settings.local.json" || // Hook files provide commands invoked by the runner.
30199
+ path50.startsWith(".claude/hooks/") || // Plugin files can provide code and hooks loaded by the runner.
30200
+ path50.startsWith(".claude/plugins/") || // Plugin metadata can load plugins and their executable components.
30201
+ path50.startsWith(".claude-plugin/") || // Plugin and marketplace manifests can select code for the runner to load.
30202
+ /^\.claude\/(?:[^/]+\/)*(?:plugin|marketplace)\.json$/.test(path50) || // OpenCode config declares MCP servers and plugins; its plugin and tool
30203
+ // directories are code it loads, and an agent definition sets the prompt,
30204
+ // model and permissions of the built-in agent its id names, which a task
30205
+ // launches with. The rest of `.opencode/` is dependencies OpenCode
30206
+ // installs itself, which a later launch must not be held for.
30207
+ /^(?:\.opencode\/)?opencode\.jsonc?$/.test(path50) || /^\.opencode\/(?:plugins?|tools?|agents?)\//.test(path50) || // Codex project config carries MCP servers, hooks and `notify` commands.
30208
+ path50.startsWith(".codex/")
30209
+ )
30210
+ ))].sort();
30211
+ }
30212
+
29911
30213
  // src/services/mergeWording.ts
29912
30214
  function isRepoGroup(handoff) {
29913
30215
  return handoff.repos.some((r) => r.path !== ".");
@@ -29942,7 +30244,7 @@ function handoffBaseLine(handoff, length) {
29942
30244
  return handoff.repos.map((repo) => `${group ? `${repo.path}: ` : ""}${repoBaseLine(repo, length)}`).join(", ");
29943
30245
  }
29944
30246
  function repoResultLines(handoff) {
29945
- return handoff.repos.map(({ path: path47, landed: landed2 }) => `${path47}: ${landed2.length === 0 ? "nothing to merge" : `${landed2.length} task${landed2.length === 1 ? "" : "s"} landed`}`);
30247
+ return handoff.repos.map(({ path: path50, landed: landed2 }) => `${path50}: ${landed2.length === 0 ? "nothing to merge" : `${landed2.length} task${landed2.length === 1 ? "" : "s"} landed`}`);
29946
30248
  }
29947
30249
 
29948
30250
  // src/interfaces/ILogger.ts
@@ -33001,6 +33303,7 @@ var IsolationRunController = class {
33001
33303
  /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
33002
33304
  resolvers = {};
33003
33305
  opening = null;
33306
+ opened = 0;
33004
33307
  /** Bumped by an interrupt, so an activation in flight across it does not open the run it ended. */
33005
33308
  activations = 0;
33006
33309
  /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
@@ -33032,6 +33335,10 @@ var IsolationRunController = class {
33032
33335
  get isOpen() {
33033
33336
  return this.mode !== null;
33034
33337
  }
33338
+ /** How many runs have opened: what tells one run's launches from the next one's. */
33339
+ get epoch() {
33340
+ return this.opened;
33341
+ }
33035
33342
  /** The open run has decided whether it isolates. */
33036
33343
  get decided() {
33037
33344
  return this.mode === "isolated" || this.mode === "shared";
@@ -33129,6 +33436,7 @@ var IsolationRunController = class {
33129
33436
  open() {
33130
33437
  if (this.mode) return;
33131
33438
  this.mode = "undecided";
33439
+ this.opened += 1;
33132
33440
  }
33133
33441
  /**
33134
33442
  * Decide once per run, as its first change task starts, whether it executes
@@ -33769,15 +34077,21 @@ var Landing = class {
33769
34077
  * has landed. Its branch is on the integration branch by then, so it merges
33770
34078
  * clean — and one the resolver did not really bring along conflicts again
33771
34079
  * instead of being taken at its word. Null when there is nothing to land.
34080
+ * The task is named before its landing settles, so the caller can hold
34081
+ * its worktree for as long as the merge is in it; one the caller says is
34082
+ * `leaving` the plan is not landed at all.
33772
34083
  */
33773
- async landResolved(resolverId) {
34084
+ landResolved(resolverId, leaving = () => false) {
33774
34085
  const conflictedId = this.runs.takeResolver(resolverId);
33775
- if (!conflictedId) return null;
34086
+ if (!conflictedId || leaving(conflictedId)) return null;
33776
34087
  const task2 = this.tasks.get(conflictedId);
33777
34088
  if (!task2 || this.runs.current?.tasks[conflictedId]?.status !== "conflict") return null;
34089
+ return { task: task2, outcome: this.landConflicted(task2) };
34090
+ }
34091
+ async landConflicted(task2) {
33778
34092
  const landing2 = await this.runs.integrate(task2);
33779
- if (landing2 !== "merged") return { task: task2, outcome: this.unmerged(task2, landing2) };
33780
- return { task: task2, outcome: { kind: "landed", messages: [{ level: "info", text: `Task "${task2.title}" landed through its conflict resolution.` }] } };
34093
+ if (landing2 !== "merged") return this.unmerged(task2, landing2);
34094
+ return { kind: "landed", messages: [{ level: "info", text: `Task "${task2.title}" landed through its conflict resolution.` }] };
33781
34095
  }
33782
34096
  /** 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. */
33783
34097
  nextRepair(taskId) {
@@ -34122,7 +34436,7 @@ function heldFrom(state) {
34122
34436
  }
34123
34437
 
34124
34438
  // src/services/Attempts.ts
34125
- var path39 = __toESM(require("path"));
34439
+ var path42 = __toESM(require("path"));
34126
34440
 
34127
34441
  // src/services/VerdictEngine.ts
34128
34442
  var IDLE_TIMEOUT_MS = 10 * 6e4;
@@ -34230,6 +34544,18 @@ var VerdictEngine = class {
34230
34544
  this.resumeIdle(taskId);
34231
34545
  this.toolCheckpoints.get(taskId)?.({ kind: "rejected", reason });
34232
34546
  }
34547
+ /**
34548
+ * An attempt's runner is being started, with no session to watch yet. Its
34549
+ * silence counts from here, so one whose start never resolves — stuck
34550
+ * launching, or waiting for its tools to attach — is flagged quiet as well.
34551
+ * {@link watch} starts the count again once the runner is up.
34552
+ */
34553
+ starting(taskId) {
34554
+ const gen = this.bumpGeneration(taskId);
34555
+ this.idlePaused.delete(taskId);
34556
+ this.openApprovals.delete(taskId);
34557
+ this.touchIdle(taskId, gen);
34558
+ }
34233
34559
  /**
34234
34560
  * Attach to a spawned session. The runner's `task_complete` call on the
34235
34561
  * attempt's token is the only completion evidence (ADR-0022, V2); a session
@@ -34240,6 +34566,9 @@ var VerdictEngine = class {
34240
34566
  */
34241
34567
  watch(task2, session) {
34242
34568
  const gen = this.bumpGeneration(task2.id);
34569
+ this.idlePaused.delete(task2.id);
34570
+ this.openApprovals.delete(task2.id);
34571
+ this.touchIdle(task2.id, gen);
34243
34572
  session.onOutput(() => {
34244
34573
  if (this.generations.get(task2.id) === gen) this.touchIdle(task2.id, gen);
34245
34574
  });
@@ -34495,10 +34824,132 @@ function reportedVerdict(report) {
34495
34824
  return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt, reported };
34496
34825
  }
34497
34826
 
34827
+ // src/services/taskStartConfig.ts
34828
+ var path39 = __toESM(require("path"));
34829
+ var import_crypto15 = require("crypto");
34830
+ var import_fs13 = require("fs");
34831
+ var MAX_FILES = 2e3;
34832
+ var MAX_DEPTH = 8;
34833
+ function liveConfig(dir) {
34834
+ const found = /* @__PURE__ */ new Map();
34835
+ const visit = (rel, depth) => {
34836
+ if (found.size >= MAX_FILES || depth > MAX_DEPTH) return;
34837
+ const full = path39.join(dir, rel);
34838
+ try {
34839
+ const info = (0, import_fs13.statSync)(full);
34840
+ if (info.isFile()) {
34841
+ found.set(rel, (0, import_crypto15.createHash)("sha1").update((0, import_fs13.readFileSync)(full)).digest("hex"));
34842
+ } else if (info.isDirectory() && path39.basename(rel) !== "node_modules") {
34843
+ for (const name of (0, import_fs13.readdirSync)(full)) visit(path39.posix.join(rel, name), depth + 1);
34844
+ }
34845
+ } catch {
34846
+ }
34847
+ };
34848
+ for (const root of RUNNER_CONFIG_ROOTS) visit(root, 0);
34849
+ const selected = new Set(repositoryRunnerConfigPaths([...found.keys()]));
34850
+ return new Map([...found].filter(([name]) => selected.has(name)));
34851
+ }
34852
+ function linkedRoots(dir) {
34853
+ const linked = /* @__PURE__ */ new Set();
34854
+ for (const root of RUNNER_CONFIG_ROOTS) {
34855
+ try {
34856
+ if ((0, import_fs13.lstatSync)(path39.join(dir, root)).isSymbolicLink()) linked.add(root);
34857
+ } catch {
34858
+ }
34859
+ }
34860
+ return linked;
34861
+ }
34862
+ function readDirs(root) {
34863
+ const dirs = [{ dir: root, prefix: "" }];
34864
+ for (let dir = root, prefix = ""; !(0, import_fs13.existsSync)(path39.join(dir, ".git")); ) {
34865
+ const parent = path39.dirname(dir);
34866
+ if (parent === dir) return [dirs[0]];
34867
+ dir = parent;
34868
+ prefix += "../";
34869
+ dirs.push({ dir, prefix });
34870
+ }
34871
+ return dirs;
34872
+ }
34873
+ var TaskStartConfig = class {
34874
+ live = /* @__PURE__ */ new Map();
34875
+ /**
34876
+ * `epoch` names the run the check belongs to; a new run takes a new
34877
+ * baseline. `root` is the workspace root, the checkout a worktree run lands
34878
+ * in; `cwd` is where the runner starts, and `worktree` whether that is a
34879
+ * worktree prepared for it.
34880
+ */
34881
+ async check(run, taskId, where) {
34882
+ const found = where.worktree ? await this.gitDifferences(run, taskId) : [];
34883
+ const id2 = String(where.epoch);
34884
+ const checkout = readDirs(where.root).flatMap(({ dir, prefix }) => this.liveDifferences(`${id2}:checkout:${dir}`, dir).map((name) => `${prefix}${name}`));
34885
+ if (!where.worktree) found.push(...checkout);
34886
+ else {
34887
+ const cwd = where.cwd ?? run?.tasks[taskId]?.workspace;
34888
+ if (cwd) found.push(...this.worktreeDifferences(id2, cwd, where.root));
34889
+ }
34890
+ return [...new Set(found)].sort();
34891
+ }
34892
+ /** The baselines go with the plan they belong to. */
34893
+ clear() {
34894
+ this.live.clear();
34895
+ }
34896
+ /**
34897
+ * What a worktree's runner reads that differs from where the run started.
34898
+ * The `.claude` and `.opencode` directories it links from the checkout are
34899
+ * the checkout's own, so they are compared with the checkout's first state,
34900
+ * which an earlier task's write there does not move. Anything else is the
34901
+ * worktree's, and is compared with its own first state: a setup command's
34902
+ * files are trusted, and a task's later write is not.
34903
+ */
34904
+ worktreeDifferences(id2, dir, root) {
34905
+ const now = liveConfig(dir);
34906
+ const linked = linkedRoots(dir);
34907
+ const fromCheckout = (name) => linked.has(name.split("/")[0]);
34908
+ const key = `${id2}:worktree:${dir}`;
34909
+ let local = this.live.get(key);
34910
+ if (!local) {
34911
+ local = new Map([...now].filter(([name]) => !fromCheckout(name)));
34912
+ this.live.set(key, local);
34913
+ }
34914
+ const checkout = this.live.get(`${id2}:checkout:${root}`);
34915
+ return [...now].filter(([name, hash]) => fromCheckout(name) ? checkout?.get(name) !== hash : local.get(name) !== hash).map(([name]) => name);
34916
+ }
34917
+ liveDifferences(key, dir) {
34918
+ const now = liveConfig(dir);
34919
+ const before = this.live.get(key);
34920
+ if (!before) {
34921
+ this.live.set(key, now);
34922
+ return [];
34923
+ }
34924
+ return [...now].filter(([name, hash]) => before.get(name) !== hash).map(([name]) => name);
34925
+ }
34926
+ async gitDifferences(run, taskId) {
34927
+ if (!run) return [];
34928
+ const invoker = { exec: defaultExecFile2, resolvePath: augmentedPath };
34929
+ const found = [];
34930
+ for (const repo of run.repos) {
34931
+ const cwd = run.tasks[taskId]?.repos[repo.path]?.worktree;
34932
+ if (!cwd || !repo.baseRef || !(0, import_fs13.existsSync)(cwd)) continue;
34933
+ const prefix = repo.path === "." ? (await tryGit(invoker, repo.root, ["rev-parse", "--show-prefix"])).stdout.trim() : "";
34934
+ const roots = [...RUNNER_CONFIG_ROOTS, ...prefix ? RUNNER_CONFIG_ROOTS.map((r) => `${prefix}${r}`) : []];
34935
+ const diff = await tryGit(invoker, cwd, ["diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-only", "--diff-filter=ACMRTU", "-z", repo.baseRef, "--"]);
34936
+ const untracked = await tryGit(invoker, cwd, ["ls-files", "--others", "-z", "--", ...roots]);
34937
+ if (!diff.ok || !untracked.ok) continue;
34938
+ for (const name of [...diff.stdout.split("\0"), ...untracked.stdout.split("\0")]) {
34939
+ const here = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : void 0;
34940
+ if (repositoryRunnerConfigPaths([name]).length || here !== void 0 && repositoryRunnerConfigPaths([here]).length) {
34941
+ found.push(repo.path === "." ? name : path39.posix.join(repo.path, name));
34942
+ }
34943
+ }
34944
+ }
34945
+ return found;
34946
+ }
34947
+ };
34948
+
34498
34949
  // src/services/ScriptRunner.ts
34499
34950
  var import_child_process11 = require("child_process");
34500
34951
  var fs16 = __toESM(require("fs"));
34501
- var path37 = __toESM(require("path"));
34952
+ var path40 = __toESM(require("path"));
34502
34953
  var import_events2 = require("events");
34503
34954
  var import_string_decoder2 = require("string_decoder");
34504
34955
 
@@ -34639,7 +35090,7 @@ var ScriptSession = class extends AbstractRunnerSession {
34639
35090
  async launchOf(step) {
34640
35091
  if ("argv" in step) {
34641
35092
  const [named3, ...args] = step.argv;
34642
- const program = this.setup.platform === "win32" && path37.win32.basename(named3) !== named3 && !path37.win32.isAbsolute(named3) ? path37.win32.resolve(this.setup.cwd, named3) : named3;
35093
+ const program = this.setup.platform === "win32" && path40.win32.basename(named3) !== named3 && !path40.win32.isAbsolute(named3) ? path40.win32.resolve(this.setup.cwd, named3) : named3;
34643
35094
  const plan = await planDirectLaunch(program, args, this.setup.launch);
34644
35095
  return { file: plan.file, args: plan.args, shell: false, ...plan.verbatim ? { verbatim: true } : {} };
34645
35096
  }
@@ -34872,10 +35323,10 @@ function passed(status) {
34872
35323
  }
34873
35324
 
34874
35325
  // src/services/PatchCapture.ts
34875
- var import_crypto15 = require("crypto");
35326
+ var import_crypto16 = require("crypto");
34876
35327
  var fs17 = __toESM(require("fs"));
34877
- var os11 = __toESM(require("os"));
34878
- var path38 = __toESM(require("path"));
35328
+ var os10 = __toESM(require("os"));
35329
+ var path41 = __toESM(require("path"));
34879
35330
 
34880
35331
  // src/services/readTask.ts
34881
35332
  function readTask2(args, readerId, plan, log) {
@@ -35142,7 +35593,7 @@ function linkInPatch(patch) {
35142
35593
  function readOwn(scratch, name, max) {
35143
35594
  let fd;
35144
35595
  try {
35145
- fd = fs17.openSync(path38.join(scratch, name), fs17.constants.O_RDONLY | (fs17.constants.O_NOFOLLOW ?? 0));
35596
+ fd = fs17.openSync(path41.join(scratch, name), fs17.constants.O_RDONLY | (fs17.constants.O_NOFOLLOW ?? 0));
35146
35597
  } catch (err) {
35147
35598
  throw new Refusal(`the capture left no ${name.split(".").pop()} Ordewell could read (${err.code ?? String(err)})`);
35148
35599
  }
@@ -35179,8 +35630,8 @@ async function captureHeld(runs, opts, signal) {
35179
35630
  if (!runs.touched) return { kind: "empty", reason: "no command of this task has run sealed, so its worktree holds only what Ordewell checked out.", flags: [] };
35180
35631
  const [worktree] = runs.worktrees;
35181
35632
  const scratch = runs.scratch;
35182
- const tag = `ordewell-patch-${(0, import_crypto15.randomBytes)(8).toString("hex")}`;
35183
- const stem = path38.join(scratch, tag);
35633
+ const tag = `ordewell-patch-${(0, import_crypto16.randomBytes)(8).toString("hex")}`;
35634
+ const stem = path41.join(scratch, tag);
35184
35635
  const own = (suffix, max = LISTING_BYTES) => readOwn(scratch, `${tag}.${suffix}`, max);
35185
35636
  try {
35186
35637
  const stage = await runs.run(stageScript(stem, opts.linked ?? []), worktree);
@@ -35218,7 +35669,7 @@ async function captureHeld(runs, opts, signal) {
35218
35669
  return {
35219
35670
  kind: "saved",
35220
35671
  patch,
35221
- stats: { base, ...countedIn(patch), bytes: patch.length, sha256: (0, import_crypto15.createHash)("sha256").update(patch).digest("hex"), flags: [...flags, ...flag("normalized", normalized2)] }
35672
+ stats: { base, ...countedIn(patch), bytes: patch.length, sha256: (0, import_crypto16.createHash)("sha256").update(patch).digest("hex"), flags: [...flags, ...flag("normalized", normalized2)] }
35222
35673
  };
35223
35674
  } catch (err) {
35224
35675
  if (err instanceof Refusal) return { kind: "refused", reason: `${err.message}.` };
@@ -35272,8 +35723,8 @@ async function indexEntries(invoker, worktree) {
35272
35723
  return entries;
35273
35724
  }
35274
35725
  async function trialApply(invoker, worktree, patchFile, scratch) {
35275
- const index = path38.resolve(worktree, (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--git-path", "index"])).trim());
35276
- const copy = path38.join(scratch, "index");
35726
+ const index = path41.resolve(worktree, (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--git-path", "index"])).trim());
35727
+ const copy = path41.join(scratch, "index");
35277
35728
  fs17.copyFileSync(index, copy);
35278
35729
  const trial = { ...invoker, exec: (file, args, opts) => invoker.exec(file, args, { ...opts, env: { ...opts.env, GIT_INDEX_FILE: copy } }) };
35279
35730
  const tried = await tryGit(trial, worktree, [...APPLY_GIT, "apply", "--cached", "--3way", "--whitespace=nowarn", patchFile]);
@@ -35293,7 +35744,7 @@ function gitRefused(lead, stderr) {
35293
35744
  function onDisk3(worktree, names) {
35294
35745
  return names.map((name) => {
35295
35746
  try {
35296
- const stat = fs17.lstatSync(path38.join(worktree, name), { bigint: true, throwIfNoEntry: false });
35747
+ const stat = fs17.lstatSync(path41.join(worktree, name), { bigint: true, throwIfNoEntry: false });
35297
35748
  return stat ? `${stat.mode} ${stat.ino} ${stat.size} ${stat.mtimeNs} ${stat.ctimeNs}` : "none";
35298
35749
  } catch (err) {
35299
35750
  return err.code ?? "unreadable";
@@ -35333,7 +35784,7 @@ function leavesWorktree(worktree, names, linked, held2, left) {
35333
35784
  if (i < parts.length && held2.get(at)?.some((entry) => entry.mode === SUBMODULE)) {
35334
35785
  return `the patch writes ${name}, under ${at}, a submodule, whose files are another repository's.`;
35335
35786
  }
35336
- const full = path38.join(worktree, ...parts.slice(0, i));
35787
+ const full = path41.join(worktree, ...parts.slice(0, i));
35337
35788
  const stat = fs17.lstatSync(full, { throwIfNoEntry: false });
35338
35789
  if (!stat) break;
35339
35790
  const last = i === parts.length;
@@ -35354,14 +35805,14 @@ async function applyPatch(invoker, worktree, patch, saved, opts = {}) {
35354
35805
  if (patch.length !== saved.bytes) {
35355
35806
  return refused4(`the patch is ${byteSize(patch.length)} where its artifact says ${byteSize(saved.bytes)}, so it was cut short or changed after it was saved.`);
35356
35807
  }
35357
- if ((0, import_crypto15.createHash)("sha256").update(patch).digest("hex") !== saved.sha256) {
35808
+ if ((0, import_crypto16.createHash)("sha256").update(patch).digest("hex") !== saved.sha256) {
35358
35809
  return refused4("the patch is not the file that was saved: its hash is not the one its artifact records, so it was changed after it was saved.");
35359
35810
  }
35360
35811
  const link = linkInPatch(patch);
35361
35812
  if (link !== void 0) {
35362
35813
  return refused4(`the patch holds a link or a submodule (${JSON.stringify(link.slice(0, 80))}), which a saved patch never does, so it is not one that was saved.`);
35363
35814
  }
35364
- const dir = fs17.mkdtempSync(path38.join(os11.tmpdir(), "ordewell-apply-"));
35815
+ const dir = fs17.mkdtempSync(path41.join(os10.tmpdir(), "ordewell-apply-"));
35365
35816
  try {
35366
35817
  const toplevel = (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--show-toplevel"])).trim();
35367
35818
  if (fs17.realpathSync(toplevel) !== fs17.realpathSync(worktree)) return refused4(`${worktree} is not the root of a worktree of its own.`);
@@ -35371,7 +35822,7 @@ async function applyPatch(invoker, worktree, patch, saved, opts = {}) {
35371
35822
  }
35372
35823
  const before = await unmergedPaths(invoker, worktree);
35373
35824
  if (before.length > 0) return refused4(`your worktree still has unresolved conflicts (${listed(before.slice(0, 10))}${before.length > 10 ? `, +${before.length - 10} more` : ""}). Resolve them and git add them, or undo them, then apply the patch.`);
35374
- const file = path38.join(dir, "saved.patch");
35825
+ const file = path41.join(dir, "saved.patch");
35375
35826
  fs17.writeFileSync(file, patch, { flag: "wx", mode: 384 });
35376
35827
  const held2 = await indexEntries(invoker, worktree);
35377
35828
  const named3 = await pathsNamed(invoker, worktree, file);
@@ -35521,6 +35972,11 @@ var TaskWebReader = class {
35521
35972
  };
35522
35973
 
35523
35974
  // src/services/Attempts.ts
35975
+ var TaskStartConfigError = class extends Error {
35976
+ constructor(paths) {
35977
+ super(`repository configuration ${paths.map((p) => JSON.stringify(p)).join(", ")} differs from where this run started; remove it to continue, or start a new run once your branch holds it`);
35978
+ }
35979
+ };
35524
35980
  var DEFAULT_GIT = { exec: defaultExecFile2, resolvePath: () => augmentedPath() };
35525
35981
  var Attempts = class {
35526
35982
  constructor(deps) {
@@ -35552,6 +36008,7 @@ var Attempts = class {
35552
36008
  live = /* @__PURE__ */ new Map();
35553
36009
  /** Bumped by every plan load: an ended attempt's wind-down belongs to the scope before it. */
35554
36010
  planScope = 0;
36011
+ startConfig = new TaskStartConfig();
35555
36012
  /** Each task's runners still winding down once stopped: a continue waits for them (ADR-0018, L1). */
35556
36013
  windDowns = /* @__PURE__ */ new Map();
35557
36014
  /**
@@ -35607,6 +36064,10 @@ var Attempts = class {
35607
36064
  receipt(taskId) {
35608
36065
  return this.live.get(taskId)?.receipt ?? null;
35609
36066
  }
36067
+ /** Which spawn of the live attempt's runner waits for Ordewell's tools to connect; null when none does. */
36068
+ attaching(taskId) {
36069
+ return this.live.get(taskId)?.attaching ?? null;
36070
+ }
35610
36071
  /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
35611
36072
  nextKind(taskId) {
35612
36073
  const task2 = this.deps.store.get(taskId);
@@ -35647,7 +36108,9 @@ var Attempts = class {
35647
36108
  shell: null,
35648
36109
  web: null,
35649
36110
  patch: null,
35650
- receipt: null
36111
+ receipt: null,
36112
+ attaching: null,
36113
+ spawning: new AbortController()
35651
36114
  };
35652
36115
  this.spawnCounts.set(task2.id, attempt.attempt);
35653
36116
  this.live.set(task2.id, attempt);
@@ -35662,7 +36125,8 @@ var Attempts = class {
35662
36125
  /**
35663
36126
  * The one way an attempt ends. Dropping the record is itself what
35664
36127
  * invalidates a spawn still in flight ({@link run} kills the session it
35665
- * receives for an attempt that is no longer current).
36128
+ * receives for an attempt that is no longer current), and its cancel takes
36129
+ * back a runner still starting meanwhile.
35666
36130
  *
35667
36131
  * A user interruption also bumps the verifier's generation *before* stopping
35668
36132
  * the runner: a runner may fire onExit synchronously from
@@ -35678,6 +36142,7 @@ var Attempts = class {
35678
36142
  const session = attempt?.session ?? null;
35679
36143
  const nativeSessionId = session?.nativeSessionId() ?? null;
35680
36144
  if (attempt) {
36145
+ attempt.spawning.abort();
35681
36146
  store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
35682
36147
  this.deps.output.detach(taskId);
35683
36148
  attempt.shell?.end();
@@ -35713,6 +36178,7 @@ var Attempts = class {
35713
36178
  const ended = this.endAll("load");
35714
36179
  this.planScope += 1;
35715
36180
  this.spawnCounts.clear();
36181
+ this.startConfig.clear();
35716
36182
  return ended;
35717
36183
  }
35718
36184
  /** A task leaving the plan: no later attempt of it is numbered from its runs. */
@@ -35845,21 +36311,31 @@ var Attempts = class {
35845
36311
  if (unavailable2) throw new Error(unavailable2);
35846
36312
  }
35847
36313
  if (kind.kind === "continuation") await this.windDowns.get(task2.id);
36314
+ if (!attempt.script && kind.kind === "repair") {
36315
+ const paths = await this.startConfig.check(runs.current, task2.id, { epoch: runs.epoch, root: this.deps.workspaceRoot(), cwd: null, worktree: true });
36316
+ if (paths.length) throw new TaskStartConfigError(paths);
36317
+ }
35848
36318
  const { cwd, worktree, previousWork, workspace } = await attemptCwd(kind, task2, runs);
35849
36319
  attempt.cwd = cwd;
35850
36320
  attempt.worktree = worktree;
36321
+ if (!attempt.script && kind.kind !== "repair" && !landsNothing(kind)) {
36322
+ const paths = await this.startConfig.check(runs.current, task2.id, { epoch: runs.epoch, root: this.deps.workspaceRoot(), cwd, worktree });
36323
+ if (paths.length) throw new TaskStartConfigError(paths);
36324
+ }
35851
36325
  if (checksTree(kind)) attempt.snapshot = await runs.snapshotWorkspace();
35852
36326
  if (this.live.get(task2.id) !== attempt) {
35853
36327
  if (attempt.worktree && !this.live.has(task2.id) && !this.host.completing(task2.id)) await runs.release(task2.id, { keep: kind.kind === "repair" });
35854
36328
  this.abandonSpawn(task2, attempt);
35855
36329
  return false;
35856
36330
  }
36331
+ this.verifier.starting(task2.id);
35857
36332
  const session = attempt.script ? await this.spawnScript(task2, attempt, cwd) : await this.spawnRunner(task2, attempt, cwd, worktree ? workspace : void 0, previousWork);
35858
36333
  if (this.live.get(task2.id) !== attempt) {
35859
36334
  this.abandonSpawn(task2, attempt, session);
35860
36335
  return false;
35861
36336
  }
35862
36337
  attempt.phase = "running";
36338
+ attempt.attaching = null;
35863
36339
  attempt.session = session;
35864
36340
  store.setTaskRunnerSessionId(task2.id, void 0);
35865
36341
  store.clearCarriedFrom(task2.id);
@@ -35928,6 +36404,9 @@ var Attempts = class {
35928
36404
  return { kind: "usage-limited", taskId: task2.id, attempt, said: { message }, output: "" };
35929
36405
  }
35930
36406
  const failed = (failure2) => ({ kind: "spawn-failed", task: task2, attempt, failure: failure2 });
36407
+ if (err instanceof TaskStartConfigError) {
36408
+ return failed({ leaves: "held", hold: err.message, preserveWorktree: true, settles: false, notice: { level: "warn", text: `Task "${task2.title}" is on hold: ${err.message}` } });
36409
+ }
35931
36410
  if (attempt.kind.kind === "continuation" && !attachTimeout) {
35932
36411
  const refused4 = err instanceof RunnerStartupExit;
35933
36412
  const reason = unresumedMessage(task2, refused4 ? `${attempt.runner} could not find its saved session` : `could not start: ${message}`);
@@ -36005,7 +36484,13 @@ var Attempts = class {
36005
36484
  ...web ? { webReads: web } : {},
36006
36485
  applyPatch: (args) => this.applyPatch(task2, attempt, args),
36007
36486
  workspaceRoot: this.deps.workspaceRoot(),
36008
- onNotice: (message) => this.host.tell("warn", `Task "${task2.title}": ${message}`)
36487
+ onNotice: (message) => this.host.tell("warn", `Task "${task2.title}": ${message}`),
36488
+ cancelled: attempt.spawning.signal,
36489
+ onAttaching: (spawn) => {
36490
+ if (this.live.get(task2.id) !== attempt) return;
36491
+ attempt.attaching = spawn;
36492
+ this.host.emit("onTaskChanged");
36493
+ }
36009
36494
  });
36010
36495
  }
36011
36496
  /**
@@ -36058,9 +36543,9 @@ var Attempts = class {
36058
36543
  const repos = Object.values(this.deps.runs.openRecord(taskId)?.repos ?? {});
36059
36544
  if (repos.length !== 1 || cwd === null) return [];
36060
36545
  const [{ worktree, linked }] = repos;
36061
- const prefix = path39.relative(worktree, cwd).split(path39.sep).join("/");
36062
- if (prefix.startsWith("..") || path39.isAbsolute(prefix)) return [];
36063
- return linked.map((name) => (prefix ? `${prefix}/${name}` : name).split(path39.sep).join("/"));
36546
+ const prefix = path42.relative(worktree, cwd).split(path42.sep).join("/");
36547
+ if (prefix.startsWith("..") || path42.isAbsolute(prefix)) return [];
36548
+ return linked.map((name) => (prefix ? `${prefix}/${name}` : name).split(path42.sep).join("/"));
36064
36549
  }
36065
36550
  /**
36066
36551
  * A script task's start (ADR-0035): its body as the plan holds it now, run
@@ -36098,7 +36583,7 @@ var Attempts = class {
36098
36583
  this.deps.notifications.warn(message);
36099
36584
  };
36100
36585
  if (resolved.blockedEnvrc) {
36101
- warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path39.dirname(resolved.blockedEnvrc)} to use them.`);
36586
+ warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path42.dirname(resolved.blockedEnvrc)} to use them.`);
36102
36587
  }
36103
36588
  if (resolved.trackedEnvFile) {
36104
36589
  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.`);
@@ -36124,7 +36609,7 @@ var Attempts = class {
36124
36609
  uncommittedSkillFolder(name) {
36125
36610
  const root = this.deps.workspaceRoot();
36126
36611
  const skill = this.deps.skillsAt(this.skillRoots(root)).findSkill(name);
36127
- return skill?.source === "workspace" ? path39.relative(root, path39.dirname(skill.path)).split(path39.sep).join("/") : void 0;
36612
+ return skill?.source === "workspace" ? path42.relative(root, path42.dirname(skill.path)).split(path42.sep).join("/") : void 0;
36128
36613
  }
36129
36614
  /**
36130
36615
  * The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
@@ -36554,29 +37039,29 @@ function ancestors(task2, store) {
36554
37039
 
36555
37040
  // src/services/sessionLease.ts
36556
37041
  var fs19 = __toESM(require("fs"));
36557
- var path41 = __toESM(require("path"));
36558
- var import_crypto17 = require("crypto");
37042
+ var path44 = __toESM(require("path"));
37043
+ var import_crypto18 = require("crypto");
36559
37044
 
36560
37045
  // src/utils/sessionStore.ts
36561
37046
  var fs18 = __toESM(require("fs"));
36562
- var path40 = __toESM(require("path"));
37047
+ var path43 = __toESM(require("path"));
36563
37048
 
36564
37049
  // src/utils/sessionId.ts
36565
- var import_crypto16 = require("crypto");
37050
+ var import_crypto17 = require("crypto");
36566
37051
  function mintSessionId() {
36567
- return `session-${(0, import_crypto16.randomBytes)(16).toString("hex")}`;
37052
+ return `session-${(0, import_crypto17.randomBytes)(16).toString("hex")}`;
36568
37053
  }
36569
37054
 
36570
37055
  // src/utils/sessionStore.ts
36571
37056
  var SESSIONS_SUBDIR = "sessions";
36572
37057
  function getSessionsDir(baseDir) {
36573
- return path40.join(getStateDir(baseDir), SESSIONS_SUBDIR);
37058
+ return path43.join(getStateDir(baseDir), SESSIONS_SUBDIR);
36574
37059
  }
36575
37060
  function idSegment(id2) {
36576
37061
  return /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(id2) ? id2 : `~${Buffer.from(id2, "utf-8").toString("hex")}`;
36577
37062
  }
36578
37063
  function sessionDataDir(sessionId, baseDir) {
36579
- return path40.join(getSessionsDir(baseDir), idSegment(sessionId));
37064
+ return path43.join(getSessionsDir(baseDir), idSegment(sessionId));
36580
37065
  }
36581
37066
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
36582
37067
  function isSessionMeta(value) {
@@ -36603,8 +37088,8 @@ function legacySessionFilename(session) {
36603
37088
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
36604
37089
  }
36605
37090
  function dropLegacyFile(sessionsDir, meta) {
36606
- const legacy = path40.join(sessionsDir, legacySessionFilename(meta));
36607
- if (legacy === path40.join(sessionsDir, sessionFilename(meta)) || !fs18.existsSync(legacy)) return;
37091
+ const legacy = path43.join(sessionsDir, legacySessionFilename(meta));
37092
+ if (legacy === path43.join(sessionsDir, sessionFilename(meta)) || !fs18.existsSync(legacy)) return;
36608
37093
  try {
36609
37094
  const held2 = JSON.parse(fs18.readFileSync(legacy, "utf-8")).meta?.id;
36610
37095
  if (held2 === meta.id) fs18.unlinkSync(legacy);
@@ -36631,7 +37116,7 @@ function saveSession(plan, goal, baseDir, id2) {
36631
37116
  };
36632
37117
  dropRetiredPlanFields(session.plan);
36633
37118
  const filename = sessionFilename(meta);
36634
- const filePath = path40.join(sessionsDir, filename);
37119
+ const filePath = path43.join(sessionsDir, filename);
36635
37120
  writeFileAtomic(filePath, JSON.stringify(session, null, 2));
36636
37121
  dropLegacyFile(sessionsDir, meta);
36637
37122
  return meta;
@@ -36643,7 +37128,7 @@ function listSessions(baseDir, logger = defaultLogger) {
36643
37128
  return files.map((f) => {
36644
37129
  let parsed;
36645
37130
  try {
36646
- parsed = JSON.parse(fs18.readFileSync(path40.join(sessionsDir, f), "utf-8"));
37131
+ parsed = JSON.parse(fs18.readFileSync(path43.join(sessionsDir, f), "utf-8"));
36647
37132
  } catch (err) {
36648
37133
  if (err instanceof SyntaxError) return moveAside(sessionsDir, f, "is not valid JSON", logger);
36649
37134
  logger.warn("sessionStore", `failed to read session file ${f}; skipping`, err);
@@ -36664,7 +37149,7 @@ function listSessions(baseDir, logger = defaultLogger) {
36664
37149
  function moveAside(sessionsDir, file, reason, logger) {
36665
37150
  const aside = `${file}.${Date.now()}.corrupt`;
36666
37151
  try {
36667
- fs18.renameSync(path40.join(sessionsDir, file), path40.join(sessionsDir, aside));
37152
+ fs18.renameSync(path43.join(sessionsDir, file), path43.join(sessionsDir, aside));
36668
37153
  logger.warn("sessionStore", `session file ${file} ${reason}; moved aside to ${aside} rather than overwritten`);
36669
37154
  } catch {
36670
37155
  logger.warn("sessionStore", `failed to read session file ${file}; skipping`);
@@ -36675,7 +37160,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
36675
37160
  const files = fs18.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
36676
37161
  for (const f of files) {
36677
37162
  try {
36678
- const raw = fs18.readFileSync(path40.join(sessionsDir, f), "utf-8");
37163
+ const raw = fs18.readFileSync(path43.join(sessionsDir, f), "utf-8");
36679
37164
  const session = JSON.parse(raw);
36680
37165
  if (session.meta.id === sessionId) return f;
36681
37166
  } catch (err) {
@@ -36690,7 +37175,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
36690
37175
  try {
36691
37176
  const file = findSessionFile(sessionsDir, sessionId, logger);
36692
37177
  if (file) {
36693
- const raw = fs18.readFileSync(path40.join(sessionsDir, file), "utf-8");
37178
+ const raw = fs18.readFileSync(path43.join(sessionsDir, file), "utf-8");
36694
37179
  const parsed = JSON.parse(raw);
36695
37180
  if (hasScalarRunner(parsed)) {
36696
37181
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -36732,7 +37217,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
36732
37217
  try {
36733
37218
  const file = findSessionFile(sessionsDir, sessionId, logger);
36734
37219
  if (file) {
36735
- fs18.unlinkSync(path40.join(sessionsDir, file));
37220
+ fs18.unlinkSync(path43.join(sessionsDir, file));
36736
37221
  return true;
36737
37222
  }
36738
37223
  } catch (err) {
@@ -36785,7 +37270,7 @@ var UNLEASED = {
36785
37270
  };
36786
37271
  var hostsHere = /* @__PURE__ */ new Set();
36787
37272
  function leaseFile(sessionId, workspace) {
36788
- return path41.join(sessionDataDir(sessionId, workspace), LEASE_FILE);
37273
+ return path44.join(sessionDataDir(sessionId, workspace), LEASE_FILE);
36789
37274
  }
36790
37275
  function isRecord9(value) {
36791
37276
  if (typeof value !== "object" || value === null) return false;
@@ -36821,7 +37306,7 @@ var LeaseHost = class {
36821
37306
  held = /* @__PURE__ */ new Map();
36822
37307
  heartbeat = null;
36823
37308
  constructor(options = {}) {
36824
- this.id = options.id ?? (0, import_crypto17.randomUUID)();
37309
+ this.id = options.id ?? (0, import_crypto18.randomUUID)();
36825
37310
  this.pid = options.pid ?? process.pid;
36826
37311
  this.kindOf = options.kind ?? (() => void 0);
36827
37312
  this.alive = options.alive ?? processAlive;
@@ -36874,7 +37359,7 @@ var LeaseHost = class {
36874
37359
  */
36875
37360
  acquire(file, workspace, holder) {
36876
37361
  ensureStateDirIgnored(workspace);
36877
- ensurePrivateDir(path41.dirname(file));
37362
+ ensurePrivateDir(path44.dirname(file));
36878
37363
  const outcome = withFileLock(file, () => {
36879
37364
  const current = readRecord(file);
36880
37365
  const taken = current !== null && current.hostId !== this.id ? current : null;
@@ -36929,7 +37414,7 @@ var LeaseHost = class {
36929
37414
  withFileLock(file, () => {
36930
37415
  if (readRecord(file)?.hostId === this.id) fs19.rmSync(file, { force: true });
36931
37416
  }, "release a session lease");
36932
- fs19.rmdirSync(path41.dirname(file));
37417
+ fs19.rmdirSync(path44.dirname(file));
36933
37418
  } catch {
36934
37419
  }
36935
37420
  }
@@ -37122,10 +37607,14 @@ var TaskOrchestrator = class _TaskOrchestrator {
37122
37607
  /** Conflict repairs owed with every slot taken and no scheduler to claim them: a freed slot starts them. */
37123
37608
  owedRepairs = /* @__PURE__ */ new Set();
37124
37609
  /**
37125
- * Tasks Mark complete is landing after it ended their attempt: in progress
37126
- * with no attempt for as long as that takes, and not orphans for it.
37610
+ * Tasks Mark complete is landing after it ended their attempt, or that
37611
+ * their conflict resolver is landing, and that landing: in progress with
37612
+ * no attempt for as long as it takes, and not orphans for it. Both can be
37613
+ * landing one task at once, so each holds it for itself (see {@link holdWhileLanding}).
37127
37614
  */
37128
- completing = /* @__PURE__ */ new Set();
37615
+ completing = /* @__PURE__ */ new Map();
37616
+ /** Tasks {@link releaseTask} is tearing down once their landings have settled: none may begin another. */
37617
+ releasing = /* @__PURE__ */ new Set();
37129
37618
  /** Tasks paused on a usage limit, the run's hold on them, and each runner's wait for its reset (ADR-0018, U1). */
37130
37619
  limitPauses;
37131
37620
  /**
@@ -37261,6 +37750,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
37261
37750
  getCompletionReceipt(taskId) {
37262
37751
  return this.attempts.receipt(taskId);
37263
37752
  }
37753
+ /** Which spawn of a live attempt's runner waits for Ordewell's tools to connect (ADR-0025); null when none does. */
37754
+ getToolsAttaching(taskId) {
37755
+ return this.attempts.attaching(taskId);
37756
+ }
37264
37757
  /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
37265
37758
  getLiveOutput(taskId, opts) {
37266
37759
  return this.attempts.liveTail(taskId, opts);
@@ -37425,12 +37918,13 @@ var TaskOrchestrator = class _TaskOrchestrator {
37425
37918
  /**
37426
37919
  * Something can still start that nobody has to ask for again: a start
37427
37920
  * admitted and not yet claimed, a force start queued behind its
37428
- * dependencies, a task that resumes on its own at its usage limit's
37429
- * reset, or a start a dirty checkout parked for Stash and continue to
37430
- * replay. A Stop is owed to it, whether or not anything runs.
37921
+ * dependencies, a conflict repair owed a slot or the end of a start hold,
37922
+ * a task that resumes on its own at its usage limit's reset, or a start a
37923
+ * dirty checkout parked for Stash and continue to replay. A Stop is owed to
37924
+ * it, whether or not anything runs.
37431
37925
  */
37432
37926
  get startsPending() {
37433
- return this.admissions > 0 || this.queuedStarts.size > 0 || this.runs.blocked || this.store.allTasks.some(waitsForReset);
37927
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.owedRepairs.size > 0 || this.runs.blocked || this.store.allTasks.some(waitsForReset);
37434
37928
  }
37435
37929
  /**
37436
37930
  * The plan's activity as one word for every surface: a start on its way,
@@ -37866,7 +38360,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
37866
38360
  this.store.markPending(task2.id);
37867
38361
  this.hold(task2.id, failure2.hold, false);
37868
38362
  }
37869
- await this.runs.release(task2.id, { keep: failure2.leaves === "failed" && failure2.keepWorktree });
38363
+ if (failure2.leaves !== "held" || !failure2.preserveWorktree) {
38364
+ await this.runs.release(task2.id, { keep: failure2.leaves === "failed" && failure2.keepWorktree });
38365
+ }
37870
38366
  const { level, text: text6, toastOnly } = failure2.notice;
37871
38367
  if (toastOnly) this.notifications[level](text6);
37872
38368
  else this.tell(level, text6);
@@ -37931,6 +38427,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
37931
38427
  * Work that did not land waits on the user — it never fails the task or
37932
38428
  * blocks its dependents — unless its conflict is owed a repair, which takes
37933
38429
  * the slot the ending attempt freed and never one more than the run allows.
38430
+ * A planner turn's start hold holds it as it holds any new spawn (ADR-0029).
37934
38431
  * The repair follows on from the start whose work it repairs: a Stop since
37935
38432
  * that was admitted withdraws it.
37936
38433
  */
@@ -37938,20 +38435,22 @@ var TaskOrchestrator = class _TaskOrchestrator {
37938
38435
  this.store.markAwaitingUser(task2.id, "conflict");
37939
38436
  this.say(outcome.messages);
37940
38437
  if (outcome.kind !== "repair-needed") return;
37941
- if (this.attempts.size < this.config.maxParallelSessions) {
38438
+ const waitsFor = this.startHolds > 0 ? "the planner's turn ends" : this.slotsFull() ? "a slot is free" : null;
38439
+ if (!waitsFor) {
37942
38440
  const claimed = this.claim(task2.id, admission);
37943
38441
  if ("refused" in claimed) this.tell("warn", `The conflict of task "${task2.title}" is not repaired: ${claimed.refused}`);
37944
38442
  else this.unawaited(claimed.spawned, `Could not start the repair of task "${task2.title}"`);
37945
38443
  } else {
37946
38444
  this.store.markPending(task2.id);
37947
38445
  if (!this.isScheduling) this.owedRepairs.add(task2.id);
37948
- this.tell("info", `Task "${task2.title}" is repaired once a slot is free.`);
38446
+ this.tell("info", `Task "${task2.title}" is repaired once ${waitsFor}.`);
37949
38447
  }
37950
38448
  }
37951
- /** Start the repairs {@link settleUnlanded} left owed, as slots allow. */
38449
+ /** Start the repairs {@link settleUnlanded} left owed, as slots and any start hold allow. */
37952
38450
  startOwedRepairs(admission) {
38451
+ if (this.startHolds > 0) return;
37953
38452
  for (const id2 of [...this.owedRepairs]) {
37954
- if (this.attempts.size >= this.config.maxParallelSessions) return;
38453
+ if (this.slotsFull()) return;
37955
38454
  this.owedRepairs.delete(id2);
37956
38455
  const task2 = this.store.get(id2);
37957
38456
  if (!task2 || task2.status !== "pending" || this.attempts.has(id2) || !this.landing.nextRepair(id2)) continue;
@@ -37961,13 +38460,31 @@ var TaskOrchestrator = class _TaskOrchestrator {
37961
38460
  }
37962
38461
  }
37963
38462
  async landResolved(resolverId, admission) {
37964
- const resolved = await this.landing.landResolved(resolverId);
38463
+ const resolved = this.landing.landResolved(resolverId, (id2) => this.releasing.has(id2));
37965
38464
  if (!resolved) return;
37966
- const { task: task2, outcome } = resolved;
38465
+ const { task: task2 } = resolved;
38466
+ const outcome = await this.holdWhileLanding(task2.id, resolved.outcome);
38467
+ if (this.store.get(task2.id)?.status === "completed") return;
37967
38468
  await this.applyLanding(task2, outcome, admission, () => {
37968
38469
  if (task2.verdict) this.logAndArchive(task2, task2.verdict);
37969
38470
  });
37970
38471
  }
38472
+ /**
38473
+ * Hold a task for as long as `landing` is merging its worktree. Its end lets
38474
+ * go of its own hold only: another landing of the task begun meanwhile
38475
+ * still holds it.
38476
+ */
38477
+ async holdWhileLanding(taskId, landing2) {
38478
+ const holds2 = this.completing.get(taskId) ?? /* @__PURE__ */ new Set();
38479
+ holds2.add(landing2);
38480
+ this.completing.set(taskId, holds2);
38481
+ try {
38482
+ return await landing2;
38483
+ } finally {
38484
+ holds2.delete(landing2);
38485
+ if (holds2.size === 0 && this.completing.get(taskId) === holds2) this.completing.delete(taskId);
38486
+ }
38487
+ }
37971
38488
  getReadyTasks() {
37972
38489
  return this.readiness().ready;
37973
38490
  }
@@ -38075,17 +38592,27 @@ var TaskOrchestrator = class _TaskOrchestrator {
38075
38592
  * as {@link cancelTask} does, but its worktree goes: no task is left to land
38076
38593
  * it into. A spawn still in flight just loses its attempt,
38077
38594
  * which is what makes {@link Attempts} kill the session it is about to
38078
- * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
38079
- * count kept for a task that no longer exists would be inherited by nothing.
38595
+ * receive. Every landing of it in flight — Mark complete's, its resolver's —
38596
+ * settles first, so its worktree is not torn down under a merge, and none
38597
+ * begins while it goes; a repair a landing claims is cancelled as any
38598
+ * running attempt is. The id's cross-attempt bookkeeping
38599
+ * goes too — a hold or retry count kept for a task that no longer exists
38600
+ * would be inherited by nothing.
38080
38601
  */
38081
38602
  async releaseTask(taskId) {
38082
38603
  this.limitPauses.cancel(taskId);
38083
- const phase = this.attempts.phaseOf(taskId);
38084
- if (phase === "running" || phase === "integrating") await this.cancelAttempt(taskId, { keep: false });
38085
- else {
38086
- this.attempts.end(taskId, "release");
38087
- if (this.store.get(taskId)?.status === "in_progress") this.store.markPending(taskId);
38088
- await this.runs.release(taskId, { keep: false });
38604
+ for (let holds2 = this.completing.get(taskId); holds2; holds2 = this.completing.get(taskId)) await Promise.allSettled(holds2);
38605
+ this.releasing.add(taskId);
38606
+ try {
38607
+ const phase = this.attempts.phaseOf(taskId);
38608
+ if (phase === "running" || phase === "integrating") await this.cancelAttempt(taskId, { keep: false });
38609
+ else {
38610
+ this.attempts.end(taskId, "release");
38611
+ if (this.store.get(taskId)?.status === "in_progress") this.store.markPending(taskId);
38612
+ await this.runs.release(taskId, { keep: false });
38613
+ }
38614
+ } finally {
38615
+ this.releasing.delete(taskId);
38089
38616
  }
38090
38617
  this.onHold.delete(taskId);
38091
38618
  this.retryCounts.delete(taskId);
@@ -38094,17 +38621,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
38094
38621
  async markTaskComplete(taskId) {
38095
38622
  const task2 = this.store.get(taskId);
38096
38623
  if (!task2 || task2.status === "completed") return;
38097
- if (this.completing.has(taskId)) return;
38624
+ if (this.completing.has(taskId) || this.releasing.has(taskId)) return;
38098
38625
  const admission = this.admittedNow();
38099
38626
  const ended = this.attempts.end(taskId, "complete");
38100
38627
  const verdict = this.attempts.markComplete(task2);
38101
- this.completing.add(taskId);
38102
- let landing2;
38103
- try {
38104
- landing2 = await (ended?.integration ?? this.landing.landVouched(task2));
38105
- } finally {
38106
- this.completing.delete(taskId);
38107
- }
38628
+ const landed2 = ended?.integration ?? this.landing.landVouched(task2);
38629
+ const landing2 = await this.holdWhileLanding(taskId, landed2);
38108
38630
  this.store.setTaskVerdict(taskId, verdict);
38109
38631
  const handoff = task2.type === "user" ? userTaskHandoff(task2.userSteps) : void 0;
38110
38632
  this.attempts.handOver(taskId, summarizeOutput(verdict.reason, ""), handoff);
@@ -38393,7 +38915,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
38393
38915
  if (!await this.openForAdmitted(taskId, origin, resume, admission)) return;
38394
38916
  if (this.attempts.has(taskId)) return;
38395
38917
  this.refuseOpsDuringMerge(taskId, forced);
38396
- if (forced.queued && this.holdsQueued(taskId, forced)) {
38918
+ if (forced.queued && (this.holdsQueued(taskId, forced) || this.slotsFull())) {
38397
38919
  this.queuedStarts.set(taskId, { pastGate: forced.pastGate });
38398
38920
  return;
38399
38921
  }
@@ -38452,8 +38974,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
38452
38974
  this.queuedStarts.delete(id2);
38453
38975
  continue;
38454
38976
  }
38977
+ if (this.slotsFull()) continue;
38455
38978
  const waits = this.startWaits(task2, task2.research ? void 0 : { queued: true, pastGate: queued.pastGate });
38456
- if (task2.research && (waits.length > 0 || this.attempts.size >= this.config.maxParallelSessions)) continue;
38979
+ if (task2.research && waits.length > 0) continue;
38457
38980
  if (waits.some((w) => QUEUED_WAITS.has(w.kind))) continue;
38458
38981
  if (this.leftToScheduler(task2) && queued.pastGate.length === 0) {
38459
38982
  this.queuedStarts.delete(id2);
@@ -38482,6 +39005,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
38482
39005
  const task2 = this.store.get(taskId);
38483
39006
  return !!task2 && this.startWaits(task2, forced).some((w) => QUEUED_WAITS.has(w.kind) || w.kind === "dependencies" || w.kind === "merge-gate");
38484
39007
  }
39008
+ slotsFull() {
39009
+ return this.attempts.size >= this.config.maxParallelSessions;
39010
+ }
38485
39011
  sayQueuedAtGate(task2, queued, unmerged) {
38486
39012
  const titles = unmerged.map((id2) => `"${this.store.get(id2)?.title ?? id2}"`).join(", ");
38487
39013
  const notice = `Task "${task2.title}" waits for Merge all: the work of ${titles} is not merged into your branch yet. Merge all to start it, or force start it again to start it without that work.`;
@@ -38582,7 +39108,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
38582
39108
  */
38583
39109
  async settleRun(after) {
38584
39110
  if (!this.store.isFinished()) this.transition({ type: "reopen" });
38585
- if (this.attempts.size > 0) return;
39111
+ if (this.attempts.size > 0 || this.owedRepairs.size > 0) return;
38586
39112
  if (this.runState.kind === "held") return;
38587
39113
  if (this.runState.kind !== "scheduling") {
38588
39114
  if (after === "tick") {
@@ -38876,21 +39402,45 @@ var Planner = class {
38876
39402
  };
38877
39403
 
38878
39404
  // src/services/ModelResolver.ts
38879
- var ModelResolver = class {
39405
+ var ModelResolver = class _ModelResolver {
38880
39406
  constructor(registry, config, deps = {}) {
38881
39407
  this.registry = registry;
38882
39408
  this.config = config;
38883
- this.discovery = new ModelDiscovery(registry, deps.execImpl, deps.fetchImpl);
39409
+ this.discovery = new ModelDiscovery(registry, deps.execImpl, deps.fetchImpl, void 0, void 0, void 0, deps.env);
38884
39410
  this.fetchImpl = deps.fetchImpl;
39411
+ this.execImpl = deps.execImpl;
39412
+ this.shared = { vendor: { options: null, load: null, errors: {}, generation: 0 }, root: this, views: /* @__PURE__ */ new Map() };
38885
39413
  }
38886
39414
  registry;
38887
39415
  config;
38888
39416
  discovery;
38889
39417
  fetchImpl;
38890
- cachedPickerOptions = null;
38891
- /** A catalog load under way: a second ask, or a warm-up, shares it rather than fetching again. */
38892
- pickerLoad = null;
38893
- lastDiscoveryErrors = {};
39418
+ execImpl;
39419
+ shared;
39420
+ get vendor() {
39421
+ return this.shared.vendor;
39422
+ }
39423
+ /**
39424
+ * This resolver for runners launched with another environment, a
39425
+ * workspace's own: their catalogs are discovered and cached under `env`, as
39426
+ * `CLAUDE_CONFIG_DIR` there can name another account, while the vendor
39427
+ * catalogs stay this resolver's, fetched once. One view per `key`, made on
39428
+ * its first ask: a later ask with the same key gets that view, whatever
39429
+ * `env` it passes.
39430
+ */
39431
+ withRunnerEnv(key, env) {
39432
+ let view = this.shared.views.get(key);
39433
+ if (!view) {
39434
+ view = new _ModelResolver(this.registry, this.config, { fetchImpl: this.fetchImpl, execImpl: this.execImpl, env });
39435
+ view.shared = this.shared;
39436
+ this.shared.views.set(key, view);
39437
+ }
39438
+ return view;
39439
+ }
39440
+ /** Every runner-environment catalog this resolver's family holds: its root's and each view's. */
39441
+ runnerCatalogs() {
39442
+ return [this.shared.root, ...this.shared.views.values()].map((resolver) => resolver.discovery);
39443
+ }
38894
39444
  static builtinOptions() {
38895
39445
  return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
38896
39446
  }
@@ -38906,16 +39456,16 @@ var ModelResolver = class {
38906
39456
  return Object.fromEntries(discovered);
38907
39457
  }
38908
39458
  async pickerOptions() {
38909
- if (this.cachedPickerOptions) return this.cachedPickerOptions;
38910
- if (!this.pickerLoad) {
39459
+ if (this.vendor.options) return this.vendor.options;
39460
+ if (!this.vendor.load) {
38911
39461
  const load = this.loadPickerOptions();
38912
- this.pickerLoad = load;
39462
+ this.vendor.load = load;
38913
39463
  void load.finally(() => {
38914
- if (this.pickerLoad === load) this.pickerLoad = null;
39464
+ if (this.vendor.load === load) this.vendor.load = null;
38915
39465
  }).catch(() => {
38916
39466
  });
38917
39467
  }
38918
- return this.pickerLoad;
39468
+ return this.vendor.load;
38919
39469
  }
38920
39470
  /**
38921
39471
  * Start loading the vendor catalogs without waiting for them, unless they
@@ -38923,20 +39473,25 @@ var ModelResolver = class {
38923
39473
  * known yet reads it from them on a later call, never waiting on the fetch.
38924
39474
  */
38925
39475
  warmPickerOptions() {
38926
- if (this.cachedPickerOptions || this.pickerLoad) return;
39476
+ if (this.vendor.options || this.vendor.load) return;
38927
39477
  this.pickerOptions().catch(() => {
38928
39478
  });
38929
39479
  }
38930
39480
  async loadPickerOptions() {
39481
+ const vendor = this.vendor;
39482
+ const generation2 = vendor.generation;
38931
39483
  const { apiKeys, baseUrls } = collectProviderCredentials(this.config);
38932
39484
  const { models, errors } = await fetchAllProviderModels({
38933
39485
  apiKeys,
38934
39486
  baseUrls,
38935
39487
  fetchImpl: this.fetchImpl
38936
39488
  });
38937
- this.lastDiscoveryErrors = errors;
38938
- this.cachedPickerOptions = toOrchestratorOptions(models, ORCHESTRATOR_SHORTCUTS);
38939
- return this.cachedPickerOptions;
39489
+ const options = toOrchestratorOptions(models, ORCHESTRATOR_SHORTCUTS);
39490
+ if (vendor.generation === generation2) {
39491
+ vendor.errors = errors;
39492
+ vendor.options = options;
39493
+ }
39494
+ return options;
38940
39495
  }
38941
39496
  /**
38942
39497
  * Per-provider failures from the most recent `pickerOptions()` fetch, keyed
@@ -38945,7 +39500,7 @@ var ModelResolver = class {
38945
39500
  * key/endpoint is set but whose catalog could not be reached.
38946
39501
  */
38947
39502
  getDiscoveryErrors() {
38948
- return this.lastDiscoveryErrors;
39503
+ return this.vendor.errors;
38949
39504
  }
38950
39505
  async refresh() {
38951
39506
  const options = await this.pickerOptions();
@@ -38956,8 +39511,25 @@ var ModelResolver = class {
38956
39511
  this.config.setProviderModelLists(lists);
38957
39512
  return lists;
38958
39513
  }
39514
+ /** Forget every runner catalog, each workspace's included; the vendor catalogs stay. */
38959
39515
  refreshRunnerModels() {
38960
- this.discovery.clear();
39516
+ for (const discovery of this.runnerCatalogs()) discovery.clear();
39517
+ }
39518
+ /**
39519
+ * The models a pick for `provider` is judged against: a harness planner's
39520
+ * own runner catalog, or the vendor picker catalog (ADR-0009). Discovered
39521
+ * if not held. Never throws: a catalog that cannot be had is empty.
39522
+ */
39523
+ async plannerCatalog(provider) {
39524
+ const runner = runnerForProvider(provider);
39525
+ if (runner) return candidatesOf((await this.modelsForRunners([runner]))[runner] ?? []);
39526
+ return (await this.pickerOptions().catch(() => [])).map((o) => ({ id: o.id }));
39527
+ }
39528
+ /** {@link plannerCatalog} from what is already held, so a cold catalog is empty; never discovers. */
39529
+ cachedPlannerCatalog(provider) {
39530
+ const runner = runnerForProvider(provider);
39531
+ if (runner) return candidatesOf(this.getCachedRunnerModels(runner));
39532
+ return this.getCachedPickerOptions().map((o) => ({ id: o.id }));
38961
39533
  }
38962
39534
  /**
38963
39535
  * Synchronous reads of whatever the last `modelsForRunners`/`pickerOptions`
@@ -38968,7 +39540,7 @@ var ModelResolver = class {
38968
39540
  return this.discovery.getCached(runner) ?? [];
38969
39541
  }
38970
39542
  getCachedPickerOptions() {
38971
- return this.cachedPickerOptions ?? [];
39543
+ return this.vendor.options ?? [];
38972
39544
  }
38973
39545
  /**
38974
39546
  * A model's context window from whatever catalog is already cached (#49),
@@ -38981,7 +39553,7 @@ var ModelResolver = class {
38981
39553
  */
38982
39554
  contextWindowFor(source, model) {
38983
39555
  if (!model) return void 0;
38984
- const window2 = isApiProvider(source) ? this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.contextWindow : this.discovery.getCached(source)?.find((m) => m.modelId === model)?.contextWindow;
39556
+ const window2 = isApiProvider(source) ? this.vendor.options?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.contextWindow : this.discovery.getCached(source)?.find((m) => m.modelId === model)?.contextWindow;
38985
39557
  return window2 && window2 > 0 ? window2 : void 0;
38986
39558
  }
38987
39559
  /**
@@ -38991,14 +39563,16 @@ var ModelResolver = class {
38991
39563
  */
38992
39564
  maxOutputTokensFor(source, model) {
38993
39565
  if (!model || !isApiProvider(source)) return void 0;
38994
- const cap = this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.maxOutputTokens;
39566
+ const cap = this.vendor.options?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.maxOutputTokens;
38995
39567
  return cap && cap > 0 ? cap : void 0;
38996
39568
  }
39569
+ /** Forget every catalog: the vendor ones, and each runner environment's. */
38997
39570
  invalidate() {
38998
- this.discovery.clear();
38999
- this.cachedPickerOptions = null;
39000
- this.pickerLoad = null;
39001
- this.lastDiscoveryErrors = {};
39571
+ this.refreshRunnerModels();
39572
+ this.vendor.generation++;
39573
+ this.vendor.options = null;
39574
+ this.vendor.load = null;
39575
+ this.vendor.errors = {};
39002
39576
  if (!this.fetchImpl) {
39003
39577
  for (const provider of PROVIDER_PRIORITY) {
39004
39578
  const baseUrl = this.config.getProviderBaseUrl(provider);
@@ -39008,6 +39582,9 @@ var ModelResolver = class {
39008
39582
  }
39009
39583
  }
39010
39584
  };
39585
+ function candidatesOf(models) {
39586
+ return models.map((m) => ({ id: m.modelId, variants: m.variants }));
39587
+ }
39011
39588
 
39012
39589
  // src/services/RunnerInstallation.ts
39013
39590
  var import_child_process12 = require("child_process");
@@ -39598,8 +40175,8 @@ function codexWindow(slot, raw) {
39598
40175
 
39599
40176
  // src/services/harness/claudeAccountLimits.ts
39600
40177
  var import_child_process13 = require("child_process");
39601
- var import_crypto18 = require("crypto");
39602
- var import_fs13 = require("fs");
40178
+ var import_crypto19 = require("crypto");
40179
+ var import_fs14 = require("fs");
39603
40180
  var import_os5 = require("os");
39604
40181
  var import_path8 = require("path");
39605
40182
  var DEFAULT_TIMEOUT_MS5 = 2e4;
@@ -39633,10 +40210,10 @@ function claudeAccountLimitCollector(options) {
39633
40210
  };
39634
40211
  }
39635
40212
  async function readClaudeAccountLimits(options, signal) {
39636
- const cwd = (0, import_fs13.mkdtempSync)((0, import_path8.join)((0, import_os5.tmpdir)(), "ordewell-claude-limits-"));
40213
+ const cwd = (0, import_fs14.mkdtempSync)((0, import_path8.join)((0, import_os5.tmpdir)(), "ordewell-claude-limits-"));
39637
40214
  const removeCwd = () => {
39638
40215
  try {
39639
- (0, import_fs13.rmSync)(cwd, { recursive: true, force: true });
40216
+ (0, import_fs14.rmSync)(cwd, { recursive: true, force: true });
39640
40217
  } catch {
39641
40218
  }
39642
40219
  };
@@ -39678,7 +40255,7 @@ async function readClaudeAccountLimits(options, signal) {
39678
40255
  }
39679
40256
  function claudeAccountOwner(account) {
39680
40257
  const email = isRecord11(account) ? text5(account.email) : void 0;
39681
- if (!isRecord11(account) || !email) return `unnamed\0${(0, import_crypto18.randomUUID)()}`;
40258
+ if (!isRecord11(account) || !email) return `unnamed\0${(0, import_crypto19.randomUUID)()}`;
39682
40259
  return [text5(account.apiProvider) ?? "", email, text5(account.organization) ?? ""].join("\0");
39683
40260
  }
39684
40261
  function claudeLimitReading(usage, owner, accountPlan) {
@@ -39720,8 +40297,8 @@ function isoInstant(value) {
39720
40297
  var import_os7 = require("os");
39721
40298
 
39722
40299
  // src/services/harness/openCodeCredentials.ts
39723
- var import_crypto19 = require("crypto");
39724
- var import_fs14 = require("fs");
40300
+ var import_crypto20 = require("crypto");
40301
+ var import_fs15 = require("fs");
39725
40302
  var import_os6 = require("os");
39726
40303
  var import_path9 = require("path");
39727
40304
  var import_worker_threads = require("worker_threads");
@@ -39778,19 +40355,19 @@ function walStore(path) {
39778
40355
  parentPort.postMessage(select());
39779
40356
  `;
39780
40357
  var isRecord12 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39781
- var digest = (...parts) => (0, import_crypto19.createHash)("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
40358
+ var digest = (...parts) => (0, import_crypto20.createHash)("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
39782
40359
  function openCodeStorePaths(env, home) {
39783
40360
  const data = (0, import_path9.join)(env.XDG_DATA_HOME || (0, import_path9.join)(home, ".local", "share"), "opencode");
39784
40361
  const file = env.OPENCODE_DB || "opencode.db";
39785
40362
  return { db: file === ":memory:" ? null : (0, import_path9.resolve)(data, file), auth: (0, import_path9.join)(data, "auth.json") };
39786
40363
  }
39787
40364
  function openCodeGoLoginReader(deps = {}) {
39788
- const homedir12 = deps.homedir ?? import_os6.homedir;
40365
+ const homedir10 = deps.homedir ?? import_os6.homedir;
39789
40366
  let last;
39790
40367
  return {
39791
40368
  async read(env, signal) {
39792
40369
  if (signal.aborted) return { kind: "unreadable" };
39793
- const paths = openCodeStorePaths(env, homedir12());
40370
+ const paths = openCodeStorePaths(env, homedir10());
39794
40371
  const [db, wal, auth] = await Promise.all([
39795
40372
  paths.db ? stamp(paths.db) : null,
39796
40373
  paths.db ? stamp(`${paths.db}-wal`) : null,
@@ -39844,8 +40421,8 @@ function v2Login(db, row) {
39844
40421
  login: { route: "console", owner: digest("console", db, row.id, account, orgID ?? ""), access: value.access, ...orgID ? { orgID } : {}, expires: value.expires }
39845
40422
  };
39846
40423
  }
39847
- async function legacyLogin(path47, env) {
39848
- const content = env.OPENCODE_AUTH_CONTENT ?? await readSmall(path47);
40424
+ async function legacyLogin(path50, env) {
40425
+ const content = env.OPENCODE_AUTH_CONTENT ?? await readSmall(path50);
39849
40426
  if (content === void 0) return env[ENV_KEY] ? { kind: "unsupported" } : { kind: "none" };
39850
40427
  if (content === null) return { kind: "unreadable" };
39851
40428
  const auth = parseJson(content);
@@ -39854,15 +40431,15 @@ async function legacyLogin(path47, env) {
39854
40431
  if (entry === void 0) return env[ENV_KEY] ? { kind: "unsupported" } : { kind: "none" };
39855
40432
  if (!isRecord12(entry) || entry.type !== "api") return { kind: "unsupported" };
39856
40433
  if (typeof entry.key !== "string" || entry.key.length === 0) return { kind: "unreadable" };
39857
- return { kind: "login", login: { route: "key", owner: digest("v1", path47, entry.key), key: entry.key } };
40434
+ return { kind: "login", login: { route: "key", owner: digest("v1", path50, entry.key), key: entry.key } };
39858
40435
  }
39859
- function queryStore(path47, signal, deps) {
40436
+ function queryStore(path50, signal, deps) {
39860
40437
  return new Promise((settle) => {
39861
40438
  let worker;
39862
40439
  try {
39863
40440
  worker = (deps.spawnWorker ?? ((source, options) => new import_worker_threads.Worker(source, options)))(WORKER_SOURCE, {
39864
40441
  eval: true,
39865
- workerData: { path: path47, integrations: GO_INTEGRATIONS, busyMs: BUSY_MS, maxValueBytes: MAX_VALUE_BYTES },
40442
+ workerData: { path: path50, integrations: GO_INTEGRATIONS, busyMs: BUSY_MS, maxValueBytes: MAX_VALUE_BYTES },
39866
40443
  execArgv: deps.workerExecArgv ?? [],
39867
40444
  resourceLimits: { maxOldGenerationSizeMb: 64 }
39868
40445
  });
@@ -39889,19 +40466,19 @@ function queryStore(path47, signal, deps) {
39889
40466
  worker.once("exit", aborted);
39890
40467
  });
39891
40468
  }
39892
- async function stamp(path47) {
40469
+ async function stamp(path50) {
39893
40470
  try {
39894
- const stat = await import_fs14.promises.stat(path47);
40471
+ const stat = await import_fs15.promises.stat(path50);
39895
40472
  return [stat.ino, stat.size, stat.mtimeMs];
39896
40473
  } catch {
39897
40474
  return null;
39898
40475
  }
39899
40476
  }
39900
- async function readSmall(path47) {
40477
+ async function readSmall(path50) {
39901
40478
  try {
39902
- const stat = await import_fs14.promises.stat(path47);
40479
+ const stat = await import_fs15.promises.stat(path50);
39903
40480
  if (!stat.isFile() || stat.size > MAX_AUTH_FILE_BYTES) return null;
39904
- return await import_fs14.promises.readFile(path47, "utf8");
40481
+ return await import_fs15.promises.readFile(path50, "utf8");
39905
40482
  } catch (err) {
39906
40483
  return err.code === "ENOENT" ? void 0 : null;
39907
40484
  }
@@ -41030,20 +41607,20 @@ function createAiService(config, deps) {
41030
41607
  }
41031
41608
 
41032
41609
  // src/services/createSession.ts
41033
- var path44 = __toESM(require("path"));
41610
+ var path47 = __toESM(require("path"));
41034
41611
 
41035
41612
  // src/services/handoffReferences.ts
41036
41613
  var fs21 = __toESM(require("fs"));
41037
- var path42 = __toESM(require("path"));
41614
+ var path45 = __toESM(require("path"));
41038
41615
  var import_child_process15 = require("child_process");
41039
41616
  function resolveHandoffReferences(handoff, workspace) {
41040
41617
  const checked = /* @__PURE__ */ new Map();
41041
41618
  const contained = (candidate) => {
41042
41619
  try {
41043
41620
  const root = fs21.realpathSync(workspace);
41044
- const resolved = fs21.realpathSync(path42.resolve(root, candidate));
41045
- const relative17 = path42.relative(root, resolved);
41046
- return relative17 !== ".." && !relative17.startsWith(`..${path42.sep}`) && !path42.isAbsolute(relative17) ? resolved : void 0;
41621
+ const resolved = fs21.realpathSync(path45.resolve(root, candidate));
41622
+ const relative17 = path45.relative(root, resolved);
41623
+ return relative17 !== ".." && !relative17.startsWith(`..${path45.sep}`) && !path45.isAbsolute(relative17) ? resolved : void 0;
41047
41624
  } catch {
41048
41625
  return void 0;
41049
41626
  }
@@ -41064,7 +41641,7 @@ function resolveHandoffReferences(handoff, workspace) {
41064
41641
  }
41065
41642
  } else if (/^[0-9a-fA-F]{4,64}$/.test(ref.sha)) {
41066
41643
  const repo = contained(ref.repo ?? ".");
41067
- if (repo && fs21.existsSync(path42.join(repo, ".git"))) {
41644
+ if (repo && fs21.existsSync(path45.join(repo, ".git"))) {
41068
41645
  const result2 = (0, import_child_process15.spawnSync)("git", ["--no-optional-locks", "--no-replace-objects", "cat-file", "-t", ref.sha], {
41069
41646
  cwd: repo,
41070
41647
  encoding: "utf8",
@@ -42022,7 +42599,7 @@ function serializeTask(t) {
42022
42599
  ...t.loop && t.loopRound !== void 0 ? { loopRound: t.loopRound } : {}
42023
42600
  };
42024
42601
  }
42025
- function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate2 = [], checkpoint = "", waitsForApproval2 = false, holdReason = null, completionReceived = null) {
42602
+ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate2 = [], checkpoint = "", waitsForApproval2 = false, holdReason = null, completionReceived = null, attaching = null) {
42026
42603
  return {
42027
42604
  id: t.id,
42028
42605
  type: t.type,
@@ -42030,6 +42607,7 @@ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [],
42030
42607
  verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [], ...t.verdict.reported ? { reported: t.verdict.reported } : {} } : null,
42031
42608
  idleSince,
42032
42609
  ...completionReceived ? { completionReceived } : {},
42610
+ ...attaching ? { attaching } : {},
42033
42611
  ...t.attempted ? { attempted: true } : {},
42034
42612
  ...t.attemptUsage ? { attemptUsage: structuredClone(t.attemptUsage) } : {},
42035
42613
  ...isolation ? { isolation } : {},
@@ -42059,7 +42637,7 @@ function surfacePendingTurns(pending) {
42059
42637
  function surfaceTask({ attemptSkills, subtasks, runnerSessionId: _runnerSessionId, ...task2 }) {
42060
42638
  return {
42061
42639
  ...task2,
42062
- ...attemptSkills ? { attemptSkills: attemptSkills.map(({ name, source, path: path47 }) => ({ name, source, path: path47 })) } : {},
42640
+ ...attemptSkills ? { attemptSkills: attemptSkills.map(({ name, source, path: path50 }) => ({ name, source, path: path50 })) } : {},
42063
42641
  subtasks: (subtasks ?? []).map(surfaceTask)
42064
42642
  };
42065
42643
  }
@@ -42479,8 +43057,8 @@ var RunSupervision = class {
42479
43057
 
42480
43058
  // src/services/runView.ts
42481
43059
  var EMPTY_RUN = { tasks: [], runMerge: null, handoff: null, mergeResult: null, activity: IDLE_ACTIVITY };
42482
- function split({ verdict, idleSince, completionReceived, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2, holdReason, handoff, attempted, ...plan }) {
42483
- return { plan, status: { verdict, idleSince, completionReceived, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2, holdReason, handoff, attempted } };
43060
+ function split({ verdict, idleSince, completionReceived, attaching, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2, holdReason, handoff, attempted, ...plan }) {
43061
+ return { plan, status: { verdict, idleSince, completionReceived, attaching, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2, holdReason, handoff, attempted } };
42484
43062
  }
42485
43063
  function refresh(previous, tasks) {
42486
43064
  const held2 = new Map([...walkPlan(previous)].map(({ task: task2 }) => [task2.id, task2]));
@@ -42716,7 +43294,8 @@ var SessionEventRelay = class {
42716
43294
  this.checkpointQuestion(t.id),
42717
43295
  this.waitsForApproval(t.id),
42718
43296
  this.orchestrator.holdReason(t.id),
42719
- this.orchestrator.getCompletionReceipt(t.id)
43297
+ this.orchestrator.getCompletionReceipt(t.id),
43298
+ this.orchestrator.getToolsAttaching(t.id)
42720
43299
  )),
42721
43300
  ...runMerge ? { runMerge } : {},
42722
43301
  activity: this.orchestrator.activity
@@ -42838,10 +43417,10 @@ var SessionEventRelay = class {
42838
43417
 
42839
43418
  // src/utils/taskLogStore.ts
42840
43419
  var fs22 = __toESM(require("fs"));
42841
- var path43 = __toESM(require("path"));
43420
+ var path46 = __toESM(require("path"));
42842
43421
  var EXTENSION = ".jsonl";
42843
43422
  function taskDir({ baseDir, sessionId }, taskId) {
42844
- return path43.join(sessionDataDir(sessionId, baseDir), "tasks", idSegment(taskId));
43423
+ return path46.join(sessionDataDir(sessionId, baseDir), "tasks", idSegment(taskId));
42845
43424
  }
42846
43425
  function listTaskLogAttempts(location, taskId) {
42847
43426
  let names;
@@ -42859,7 +43438,7 @@ function openTaskLog(location, taskId, attempt) {
42859
43438
  const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
42860
43439
  let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
42861
43440
  for (; ; ) {
42862
- const file = path43.join(dir, `${number}${EXTENSION}`);
43441
+ const file = path46.join(dir, `${number}${EXTENSION}`);
42863
43442
  try {
42864
43443
  fs22.writeFileSync(file, "", { flag: "wx" });
42865
43444
  } catch (err) {
@@ -42883,15 +43462,15 @@ function prepareTaskOutputFile(location, taskId, attempt) {
42883
43462
  ensureStateDirIgnored(location.baseDir);
42884
43463
  const dir = taskDir(location, taskId);
42885
43464
  fs22.mkdirSync(dir, { recursive: true });
42886
- return path43.join(dir, `${attempt}.output.txt`);
43465
+ return path46.join(dir, `${attempt}.output.txt`);
42887
43466
  }
42888
43467
  function taskPatchFile(location, taskId, attempt) {
42889
- return path43.join(taskDir(location, taskId), `${attempt}.patch`);
43468
+ return path46.join(taskDir(location, taskId), `${attempt}.patch`);
42890
43469
  }
42891
43470
  function saveTaskPatch(location, taskId, attempt, patch) {
42892
43471
  ensureStateDirIgnored(location.baseDir);
42893
43472
  const file = taskPatchFile(location, taskId, attempt);
42894
- fs22.mkdirSync(path43.dirname(file), { recursive: true });
43473
+ fs22.mkdirSync(path46.dirname(file), { recursive: true });
42895
43474
  const partial = `${file}.${process.pid}-${Date.now()}.partial`;
42896
43475
  fs22.writeFileSync(partial, patch, { flag: "wx" });
42897
43476
  fs22.renameSync(partial, file);
@@ -42912,7 +43491,7 @@ function readTaskLog(location, taskId, attempt) {
42912
43491
  if (!Number.isInteger(attempt) || attempt < 1) return [];
42913
43492
  let raw;
42914
43493
  try {
42915
- raw = fs22.readFileSync(path43.join(taskDir(location, taskId), `${attempt}${EXTENSION}`), "utf-8");
43494
+ raw = fs22.readFileSync(path46.join(taskDir(location, taskId), `${attempt}${EXTENSION}`), "utf-8");
42916
43495
  } catch {
42917
43496
  return [];
42918
43497
  }
@@ -44974,7 +45553,7 @@ function createSession(deps) {
44974
45553
  ...deps.openTaskLog ? {} : {
44975
45554
  keepPatch: (taskId, attempt, patch) => {
44976
45555
  const location = session.taskLogLocation;
44977
- return path44.relative(location.baseDir, saveTaskPatch(location, taskId, attempt, patch)).split(path44.sep).join("/");
45556
+ return path47.relative(location.baseDir, saveTaskPatch(location, taskId, attempt, patch)).split(path47.sep).join("/");
44978
45557
  }
44979
45558
  },
44980
45559
  recordHandoff: (sessionId, taskId, handoff) => taskLogs.note(sessionId, taskId, { type: "handoff", handoff }),
@@ -47842,6 +48421,7 @@ function taskRowView(task2, context = {}) {
47842
48421
  resumes: resumeLabel(task2, context.now),
47843
48422
  approvals: approvalLabel(task2.awaitingApproval),
47844
48423
  receipt: receiptLabel(task2),
48424
+ attaching: attachingLabel(task2),
47845
48425
  waitsForApproval: task2.waitsForApproval && NOT_STARTED.has(task2.status) ? WAITS_FOR_APPROVAL_LABEL : null,
47846
48426
  holdReason: task2.holdReason?.trim() || null,
47847
48427
  research: task2.type === "ai" && task2.research === true,
@@ -48155,6 +48735,7 @@ var StructuredSession = class extends AbstractRunnerSession {
48155
48735
  */
48156
48736
  async startAttached(opts) {
48157
48737
  const { server: server4, scope, shell, savePatch, webReads } = this.launch.tools;
48738
+ this.launch.onAttaching?.(1);
48158
48739
  const spawned = await spawnAttached({
48159
48740
  runner: this.launch.runner,
48160
48741
  create: () => this.launch.createAdapter(this.launch.runner, this.launch.deps),
@@ -48191,7 +48772,10 @@ var StructuredSession = class extends AbstractRunnerSession {
48191
48772
  },
48192
48773
  cancelled: this.ended.signal,
48193
48774
  startupExit: () => this.startupExit,
48194
- onRespawn: (failure2) => this.launch.onNotice?.(`${this.launch.runner} started without Ordewell's tools (${failure2}); respawning once.`),
48775
+ onRespawn: (failure2) => {
48776
+ this.launch.onNotice?.(`${this.launch.runner} started without Ordewell's tools (${failure2}); respawning once.`);
48777
+ this.launch.onAttaching?.(2);
48778
+ },
48195
48779
  failed: (first, second) => `Could not start ${this.launch.runner} with Ordewell's tools, so the task was not sent to it. First spawn: ${first}. Respawn: ${second}.`
48196
48780
  });
48197
48781
  if (!spawned) return;
@@ -48774,16 +49358,22 @@ var StructuredRunner = class extends AbstractRunner {
48774
49358
  checkHandoff: opts.checkHandoff ?? ((completion) => checkHandoff(completion, opts.taskId, [])),
48775
49359
  ...opts.readTask ? { readTask: opts.readTask } : {},
48776
49360
  ...opts.applyPatch ? { applyPatch: opts.applyPatch } : {},
48777
- onNotice: opts.onNotice
49361
+ onNotice: opts.onNotice,
49362
+ onAttaching: opts.onAttaching
48778
49363
  });
48779
49364
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
48780
49365
  this.registerSession(id2, session);
49366
+ const cancel = () => void session.stop();
49367
+ opts.cancelled?.addEventListener("abort", cancel, { once: true });
48781
49368
  try {
49369
+ if (opts.cancelled?.aborted) cancel();
48782
49370
  await session.start(opts.prompt);
48783
49371
  } catch (err) {
48784
49372
  session.kill();
48785
49373
  this.sessions.delete(id2);
48786
49374
  throw err;
49375
+ } finally {
49376
+ opts.cancelled?.removeEventListener("abort", cancel);
48787
49377
  }
48788
49378
  return session;
48789
49379
  }
@@ -48857,7 +49447,7 @@ var RunnerRegistry = class {
48857
49447
 
48858
49448
  // src/plugins/removedPluginNotice.ts
48859
49449
  var fs23 = __toESM(require("fs"));
48860
- var path45 = __toESM(require("path"));
49450
+ var path48 = __toESM(require("path"));
48861
49451
  function isManifestFile(file) {
48862
49452
  try {
48863
49453
  return fs23.statSync(file).isFile();
@@ -48866,10 +49456,10 @@ function isManifestFile(file) {
48866
49456
  }
48867
49457
  }
48868
49458
  function removedPluginNotice(dataDir = globalDataDir()) {
48869
- const directory = path45.join(dataDir, "plugins");
49459
+ const directory = path48.join(dataDir, "plugins");
48870
49460
  try {
48871
- const hasManifest = isManifestFile(path45.join(directory, "manifest.json")) || fs23.readdirSync(directory, { withFileTypes: true }).some(
48872
- (entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(path45.join(directory, entry.name, "manifest.json"))
49461
+ const hasManifest = isManifestFile(path48.join(directory, "manifest.json")) || fs23.readdirSync(directory, { withFileTypes: true }).some(
49462
+ (entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(path48.join(directory, entry.name, "manifest.json"))
48873
49463
  );
48874
49464
  if (hasManifest) return "Plugin runners were removed; manifests in ~/.ordewell/plugins/ are ignored. See the CHANGELOG.";
48875
49465
  } catch {
@@ -48880,15 +49470,15 @@ function removedPluginNotice(dataDir = globalDataDir()) {
48880
49470
 
48881
49471
  // src/utils/stateStore.ts
48882
49472
  var fs24 = __toESM(require("fs"));
48883
- var path46 = __toESM(require("path"));
49473
+ var path49 = __toESM(require("path"));
48884
49474
  var STATE_FILE2 = "state.json";
48885
49475
  function getStatePath(baseDir) {
48886
- return path46.join(getStateDir(baseDir), STATE_FILE2);
49476
+ return path49.join(getStateDir(baseDir), STATE_FILE2);
48887
49477
  }
48888
49478
  var SESSION_ID_KEY = "sessionId";
48889
49479
  function saveState(plan, baseDir, sessionId) {
48890
49480
  const statePath = getStatePath(baseDir ?? process.cwd());
48891
- ensureDir(path46.dirname(statePath));
49481
+ ensureDir(path49.dirname(statePath));
48892
49482
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
48893
49483
  const file = sessionId ? { ...plan, [SESSION_ID_KEY]: sessionId } : plan;
48894
49484
  dropRetiredPlanFields(file);
@@ -49175,6 +49765,7 @@ var PlannerModelMemory = class {
49175
49765
  assignedRunnerOf,
49176
49766
  assignmentIssue,
49177
49767
  assignmentIssueText,
49768
+ attachingLabel,
49178
49769
  augmentedPath,
49179
49770
  automergeSummary,
49180
49771
  autonomyLevelLabel,
@@ -49436,6 +50027,7 @@ var PlannerModelMemory = class {
49436
50027
  repoBaseLine,
49437
50028
  repoResultLines,
49438
50029
  reposWithWork,
50030
+ repositoryRunnerConfigPaths,
49439
50031
  researchShellWarning,
49440
50032
  researchToolsPath,
49441
50033
  resetInstalledPlanner,