@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.mjs CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  assignmentConsequence,
48
48
  assignmentIssue,
49
49
  assignmentIssueText,
50
+ attachingLabel,
50
51
  automergeSummary,
51
52
  autorunSummary,
52
53
  awaitingLabel,
@@ -231,7 +232,7 @@ import {
231
232
  withPatch,
232
233
  withoutSealedMark,
233
234
  worstSeverity
234
- } from "./chunk-4QXOWEGC.mjs";
235
+ } from "./chunk-7WNLN63J.mjs";
235
236
  import {
236
237
  DEFAULT_LOOP_ROUNDS,
237
238
  DEFAULT_SCRIPT_TIMEOUT_MS,
@@ -918,9 +919,9 @@ function literalCdTarget(seg) {
918
919
  return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
919
920
  }
920
921
  function followCd(segments) {
921
- if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
922
+ if (segments.some((s) => s.joinedBy === "other" || movesShell(s))) return;
922
923
  const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
923
- if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
924
+ if (segments.some((s, i) => s.binary.toLowerCase() === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
924
925
  let chain = [];
925
926
  segments.forEach((seg, i) => {
926
927
  seg.cwd = chain;
@@ -929,7 +930,7 @@ function followCd(segments) {
929
930
  });
930
931
  }
931
932
  function markShellState(segments, bareSegment) {
932
- if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
933
+ if (!bareSegment && !segments.some(movesShell)) return;
933
934
  for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
934
935
  }
935
936
  var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
@@ -948,6 +949,7 @@ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
948
949
  "exec",
949
950
  "pushd",
950
951
  "popd",
952
+ "chdir",
951
953
  "alias",
952
954
  "unalias",
953
955
  "builtin",
@@ -956,6 +958,9 @@ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
956
958
  "hash",
957
959
  "trap"
958
960
  ]);
961
+ function movesShell(seg) {
962
+ return SHELL_STATE_COMMANDS.has(seg.binary.toLowerCase());
963
+ }
959
964
 
960
965
  // src/services/commandRunners.ts
961
966
  var HELP_FLAGS = ["--help", "--version"];
@@ -1811,8 +1816,9 @@ var FLAG_POLICY = {
1811
1816
  sort: {
1812
1817
  cluster: true,
1813
1818
  // Absent on purpose: `-o`/`--output` writes a file with no redirect for the
1814
- // redirect check to see, and `--compress-program` names a program that sort
1815
- // runs over its own temporary files — a permitted binary as an interpreter.
1819
+ // redirect check to see, `-T`/`--temporary-directory` names where sort writes
1820
+ // its run files, and `--compress-program` names a program that sort runs
1821
+ // over its own temporary files — a permitted binary as an interpreter.
1816
1822
  booleans: [
1817
1823
  "-b",
1818
1824
  "-c",
@@ -1851,11 +1857,9 @@ var FLAG_POLICY = {
1851
1857
  "-k",
1852
1858
  "-t",
1853
1859
  "-S",
1854
- "-T",
1855
1860
  "--key",
1856
1861
  "--field-separator",
1857
1862
  "--buffer-size",
1858
- "--temporary-directory",
1859
1863
  "--random-source",
1860
1864
  "--batch-size",
1861
1865
  "--parallel",
@@ -2482,23 +2486,28 @@ function scanFlags(seg) {
2482
2486
  const booleans = /* @__PURE__ */ new Set([...spec.booleans ?? [], ...UNIVERSAL_FLAGS]);
2483
2487
  const values = new Set(spec.values ?? []);
2484
2488
  const operands = [];
2489
+ const operandAt = [];
2490
+ const operand = (at) => {
2491
+ operands.push(seg.args[at]);
2492
+ operandAt.push(at);
2493
+ };
2485
2494
  let i = 0;
2486
2495
  while (i < seg.args.length) {
2487
2496
  const token = seg.args[i];
2488
2497
  if (token === "--" && !spec.expressionArgs) {
2489
- operands.push(...seg.args.slice(i + 1));
2498
+ for (let j = i + 1; j < seg.args.length; j++) operand(j);
2490
2499
  break;
2491
2500
  }
2492
2501
  if (!/^-./.test(token)) {
2493
- operands.push(token);
2502
+ operand(i);
2494
2503
  i++;
2495
2504
  continue;
2496
2505
  }
2497
2506
  const shape = matchFlag(spec, booleans, values, token);
2498
- if (shape === void 0) return { unknown: flagLabel(token), operands };
2507
+ if (shape === void 0) return { unknown: flagLabel(token), operands, operandAt };
2499
2508
  i += shape === "value" && takesNextToken(seg.args[i + 1]) ? 2 : 1;
2500
2509
  }
2501
- return { operands };
2510
+ return { operands, operandAt };
2502
2511
  }
2503
2512
  function subcommandOf(seg) {
2504
2513
  const scan = scanFlags(seg);
@@ -2651,6 +2660,8 @@ function sedRefusalFor(binary, scripts) {
2651
2660
  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}`;
2652
2661
  case "write":
2653
2662
  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.`;
2663
+ case "read":
2664
+ return `The "${binary}" script reads a file it names itself (the "r"/"R" command), which path confinement cannot see. Read that file directly instead.`;
2654
2665
  default:
2655
2666
  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.`;
2656
2667
  }
@@ -2771,6 +2782,7 @@ function sedScriptHazard(script) {
2771
2782
  if (cmd === void 0) return "unreadable";
2772
2783
  if (cmd === "e") return "exec";
2773
2784
  if (cmd === "w" || cmd === "W") return "write";
2785
+ if (cmd === "r" || cmd === "R") return "read";
2774
2786
  if (cmd === "{" || cmd === "}") continue;
2775
2787
  if (cmd === "#") {
2776
2788
  while (i < n && script[i] !== "\n") i++;
@@ -2792,7 +2804,7 @@ function sedScriptHazard(script) {
2792
2804
  if (!commandEnds()) return "unreadable";
2793
2805
  continue;
2794
2806
  }
2795
- if ("aicrR".includes(cmd)) {
2807
+ if ("aic".includes(cmd)) {
2796
2808
  while (i < n && script[i] !== "\n") i += script[i] === "\\" ? 2 : 1;
2797
2809
  continue;
2798
2810
  }
@@ -2892,6 +2904,12 @@ function awkRefusalFor(binary, programs) {
2892
2904
  return `The "${binary}" program pipes to or from a command ("|"), which runs it through a shell this classifier cannot inspect. ${INSPECT_OR_TASK}`;
2893
2905
  case "write":
2894
2906
  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.`;
2907
+ case "read":
2908
+ 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.`;
2909
+ case "environment":
2910
+ return `The "${binary}" program reads the environment ("ENVIRON"), which holds credentials. Run printenv for the one variable you need, which asks first.`;
2911
+ case "symbol-table":
2912
+ return `The "${binary}" program uses "SYMTAB", which gawk reads as every global by name, "ENVIRON" and "ARGV" included. Name the variable you need directly.`;
2895
2913
  case "extension":
2896
2914
  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}`;
2897
2915
  default:
@@ -2933,6 +2951,7 @@ function awkProgramHazard(program) {
2933
2951
  let slash = "regex";
2934
2952
  let depth = 0;
2935
2953
  let printDepth;
2954
+ let getlineDepth;
2936
2955
  let continues = false;
2937
2956
  const readRegex = () => {
2938
2957
  i++;
@@ -2999,7 +3018,10 @@ function awkProgramHazard(program) {
2999
3018
  continue;
3000
3019
  }
3001
3020
  if (c === "\n" || c === ";") {
3002
- if (c === ";" || !continues) printDepth = void 0;
3021
+ if (c === ";" || !continues) {
3022
+ printDepth = void 0;
3023
+ getlineDepth = void 0;
3024
+ }
3003
3025
  slash = "regex";
3004
3026
  i++;
3005
3027
  continue;
@@ -3050,6 +3072,12 @@ function awkProgramHazard(program) {
3050
3072
  slash = "unclear";
3051
3073
  continue;
3052
3074
  }
3075
+ if (c === "<") {
3076
+ if (getlineDepth !== void 0 && depth >= getlineDepth) return "read";
3077
+ i++;
3078
+ slash = "unclear";
3079
+ continue;
3080
+ }
3053
3081
  if (c === "(" || c === "[") {
3054
3082
  depth++;
3055
3083
  i++;
@@ -3058,12 +3086,16 @@ function awkProgramHazard(program) {
3058
3086
  }
3059
3087
  if (c === ")" || c === "]") {
3060
3088
  depth--;
3089
+ if (getlineDepth !== void 0 && depth < getlineDepth) getlineDepth = void 0;
3061
3090
  i++;
3062
3091
  slash = "divide";
3063
3092
  continue;
3064
3093
  }
3065
3094
  if (c === "{" || c === "}") {
3066
- if (c === "}") printDepth = void 0;
3095
+ if (c === "}") {
3096
+ printDepth = void 0;
3097
+ getlineDepth = void 0;
3098
+ }
3067
3099
  i++;
3068
3100
  slash = "regex";
3069
3101
  continue;
@@ -3084,6 +3116,10 @@ function awkProgramHazard(program) {
3084
3116
  while (i < n && /[A-Za-z0-9_.]/.test(program[i])) i++;
3085
3117
  const word = program.slice(start, i);
3086
3118
  if (word === "system") return "exec";
3119
+ if (word === "ENVIRON") return "environment";
3120
+ if (word === "ARGV") return "read";
3121
+ if (word === "SYMTAB") return "symbol-table";
3122
+ if (word === "getline") getlineDepth = depth;
3087
3123
  if (word === "print" || word === "printf") printDepth = depth;
3088
3124
  slash = AWK_KEYWORDS.includes(word) ? "unclear" : "divide";
3089
3125
  continue;
@@ -3970,6 +4006,36 @@ var GIT_READONLY_SUBCOMMANDS = [
3970
4006
  "whatchanged",
3971
4007
  "cat-file"
3972
4008
  ];
4009
+ var onlyAction = (...reads) => (after) => after[0] === void 0 || reads.includes(after[0]);
4010
+ var GIT_PROMPTABLE = {
4011
+ "ls-remote": () => true,
4012
+ "for-each-ref": () => true,
4013
+ "show-ref": () => true,
4014
+ "name-rev": () => true,
4015
+ "merge-base": () => true,
4016
+ "count-objects": () => true,
4017
+ "cherry": () => true,
4018
+ "range-diff": () => true,
4019
+ "diff-tree": () => true,
4020
+ "diff-files": () => true,
4021
+ "diff-index": () => true,
4022
+ "show-branch": () => true,
4023
+ "annotate": () => true,
4024
+ "check-ignore": () => true,
4025
+ "check-attr": () => true,
4026
+ "check-ref-format": () => true,
4027
+ "check-mailmap": () => true,
4028
+ "var": () => true,
4029
+ "version": () => true,
4030
+ "worktree": onlyAction("list"),
4031
+ "notes": onlyAction("list", "show"),
4032
+ "bisect": onlyAction("log"),
4033
+ "sparse-checkout": onlyAction("list"),
4034
+ "reflog": onlyAction("show", "exists"),
4035
+ "symbolic-ref": (after) => after.filter((a) => !a.startsWith("-")).length <= 1,
4036
+ // `-w` writes the object; git's shared flag set keeps it for `git diff -w`.
4037
+ "hash-object": (after) => !after.some((a) => /^-[^-]*w/.test(a))
4038
+ };
3973
4039
  var REFUSED_COMMANDS = [
3974
4040
  "rm",
3975
4041
  "rmdir",
@@ -4061,7 +4127,28 @@ var REFUSED_SUBCOMMANDS = {
4061
4127
  "clone",
4062
4128
  "fetch",
4063
4129
  "pull",
4064
- "submodule"
4130
+ "submodule",
4131
+ "add",
4132
+ "rm",
4133
+ "mv",
4134
+ "format-patch",
4135
+ "replace",
4136
+ "mergetool",
4137
+ "repack",
4138
+ "update-index",
4139
+ "read-tree",
4140
+ "checkout-index",
4141
+ "merge-file",
4142
+ "bundle",
4143
+ "commit-tree",
4144
+ "write-tree",
4145
+ "mktree",
4146
+ "mktag",
4147
+ "unpack-objects",
4148
+ "fast-import",
4149
+ "pack-refs",
4150
+ "prune-packed",
4151
+ "maintenance"
4065
4152
  ],
4066
4153
  npm: ["publish", "unpublish", "deprecate", "owner", "access", "token", "login", "logout"],
4067
4154
  yarn: ["publish", "npm"],
@@ -4271,7 +4358,7 @@ function pathRefs(command, opts = {}) {
4271
4358
  return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
4272
4359
  const patterns = patternArgs(seg);
4273
4360
  const cwd = seg.cwd ?? [];
4274
- return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path46) => ({ path: path46, cwd }));
4361
+ return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path49) => ({ path: path49, cwd }));
4275
4362
  });
4276
4363
  }
4277
4364
  function excludedBy(command, allow, opts = {}) {
@@ -4378,7 +4465,9 @@ function patternArgs(seg) {
4378
4465
  var SCOPE_LEAD_ARGS = 2;
4379
4466
  function scopeFor(seg) {
4380
4467
  const name = seg.binaryPath ?? seg.binary;
4381
- if (!MULTIPLEXERS.includes(seg.binary.toLowerCase())) return name;
4468
+ const program = programName(seg.binary);
4469
+ if (!MULTIPLEXERS.includes(program)) return name;
4470
+ if (program === "git") return gitScope(seg, name);
4382
4471
  const lead = [];
4383
4472
  for (const arg of seg.args) {
4384
4473
  if (arg.startsWith("-")) break;
@@ -4387,6 +4476,14 @@ function scopeFor(seg) {
4387
4476
  }
4388
4477
  return [name, ...lead].join(" ");
4389
4478
  }
4479
+ function gitScope(seg, name) {
4480
+ const scan = scanFlags({ ...seg, binary: "git" });
4481
+ if (!scan || scan.unknown || scan.operands[0] === "ls-remote") return [name, ...seg.args].map(scopeWord).join(" ");
4482
+ return [name, ...scan.operands.slice(0, SCOPE_LEAD_ARGS)].join(" ");
4483
+ }
4484
+ function scopeWord(word) {
4485
+ return word === "" || /[\s"'\\]/.test(word) ? JSON.stringify(word) : word;
4486
+ }
4390
4487
  function climbsByGlob(seg) {
4391
4488
  if (!seg.globbed) return false;
4392
4489
  const patterns = patternArgs(seg);
@@ -4408,6 +4505,27 @@ function swallowsRefName(arg) {
4408
4505
  const values = FLAG_POLICY.git.values ?? [];
4409
4506
  return [...arg.slice(1)].some((c) => c !== "n" && values.includes(`-${c}`));
4410
4507
  }
4508
+ function gitArgsAfterSubcommand(seg) {
4509
+ const at = scanFlags(seg)?.operandAt[0];
4510
+ return at === void 0 ? [] : seg.args.slice(at + 1);
4511
+ }
4512
+ var REMOTE_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
4513
+ function namesConfiguredRemote(seg, after) {
4514
+ const destination2 = scanFlags({ ...seg, args: after })?.operands[0];
4515
+ return destination2 === void 0 || REMOTE_NAME.test(destination2);
4516
+ }
4517
+ function programName(binary) {
4518
+ return binary.toLowerCase().replace(/\.exe$/, "");
4519
+ }
4520
+ function isRemoteListing(seg) {
4521
+ const git2 = { ...seg, binary: programName(seg.binary) };
4522
+ return git2.binary === "git" && subcommandOf(git2) === "ls-remote";
4523
+ }
4524
+ function remoteListingTexts(seg) {
4525
+ if (!isRemoteListing(seg)) return [];
4526
+ const git2 = { ...seg, binary: "git" };
4527
+ return scanFlags({ ...git2, args: gitArgsAfterSubcommand(git2) })?.operands ?? [];
4528
+ }
4411
4529
  function refusalFor(seg, trustedResolves) {
4412
4530
  const web = webCommand(seg, trustedResolves);
4413
4531
  if (web?.reason) return web.reason;
@@ -4445,8 +4563,24 @@ function refusalFor(seg, trustedResolves) {
4445
4563
  }
4446
4564
  if (seg.binary === "git") {
4447
4565
  const sub = subcommandOf(seg);
4566
+ const after = gitArgsAfterSubcommand(seg);
4567
+ if (sub !== void 0 && !GIT_READONLY_SUBCOMMANDS.includes(sub)) {
4568
+ const reads = GIT_PROMPTABLE[sub];
4569
+ if (!reads) {
4570
+ 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}`;
4571
+ }
4572
+ if (!reads(after)) {
4573
+ return `"git ${[sub, ...after].join(" ")}" is not a read-only form of "git ${sub}". You are a read-only planner: ${EFFECT_ROUTE}`;
4574
+ }
4575
+ }
4576
+ if (sub === "ls-remote" && !namesConfiguredRemote(seg, after)) {
4577
+ 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.`;
4578
+ }
4579
+ if (sub === "ls-remote" && (seg.expandable || seg.globbed || after.some((a) => a.includes("~")))) {
4580
+ 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.`;
4581
+ }
4448
4582
  if (sub === "branch" || sub === "tag") {
4449
- const flag2 = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a)) ?? seg.args.slice(seg.args.indexOf(sub) + 1).find(swallowsRefName);
4583
+ const flag2 = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a)) ?? after.find(swallowsRefName);
4450
4584
  if (flag2) {
4451
4585
  return `"git ${sub} ${flag2}" mutates refs. You are a read-only planner: ${EFFECT_ROUTE}`;
4452
4586
  }
@@ -4585,10 +4719,10 @@ function classifyCommand(command, opts = {}) {
4585
4719
  return { tier: "refuse", scope: "", reason: `curl and wget are refused in Full and allowlist mode. ${web.effect ? EFFECT_REFUSAL_HINT : READ_REFUSAL_HINT}` };
4586
4720
  }
4587
4721
  const allow = opts.allow;
4588
- if (web && allow && web.urls.some((url) => scopeExcluded(allow, `${seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`))) {
4722
+ if (web && allow && web.urls.some((url) => scopeExcluded(allow, `${programName(seg.binary)} ${url.origin}/*`))) {
4589
4723
  return { tier: "refuse", scope: "", reason: `The web origin is excluded by the planner allowlist. ${READ_REFUSAL_HINT}` };
4590
4724
  }
4591
- const reason = refusalFor({ ...seg, binary: seg.binary.toLowerCase() }, opts.trustedWebResolves);
4725
+ const reason = refusalFor({ ...seg, binary: programName(seg.binary) }, opts.trustedWebResolves);
4592
4726
  if (reason) return { tier: "refuse", scope: "", reason: withWrapperNote(reason, wrappers) };
4593
4727
  if (runner && !XARGS_TARGETS.includes(seg.binary)) {
4594
4728
  return {
@@ -4598,23 +4732,24 @@ function classifyCommand(command, opts = {}) {
4598
4732
  };
4599
4733
  }
4600
4734
  }
4601
- if (unwrapped.some(({ seg }) => seg.binary.toLowerCase().replace(/\.exe$/, "") === "wget") && wgetVersionCommands(command, opts.dialect).includes(void 0)) {
4735
+ if (unwrapped.some(({ seg }) => programName(seg.binary) === "wget") && wgetVersionCommands(command, opts.dialect).includes(void 0)) {
4602
4736
  return { tier: "refuse", scope: "", reason: `The wget executable cannot be checked safely in this working directory. ${READ_REFUSAL_HINT}` };
4603
4737
  }
4604
4738
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || u.wrappers.some((w) => w !== w.toLowerCase()) || !isAuto(u.seg, opts));
4605
4739
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
4740
+ const changesDirectory = unwrapped.some(({ seg }) => seg.binary.toLowerCase() === "cd");
4606
4741
  const lineScoped = nonAuto.some((u) => {
4607
4742
  const name = u.seg.binary.toLowerCase();
4608
- return name === "cd" || name !== "command" && SHELL_STATE_COMMANDS.has(name) || climbsByGlob(u.seg);
4743
+ return name === "cd" || name !== "command" && SHELL_STATE_COMMANDS.has(name) || climbsByGlob(u.seg) || isRemoteListing(u.seg) && (changesDirectory || u.wrappers.length > 0 || nonAuto.length > 1);
4609
4744
  });
4610
4745
  const webUrls = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.urls ?? []);
4611
- const webOutboundTexts = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.outboundTexts ?? []);
4746
+ const outboundTexts = nonAuto.flatMap((u) => webCommand(u.seg, opts.trustedWebResolves)?.outboundTexts ?? remoteListingTexts(u.seg));
4612
4747
  if (lineScoped && webUrls.length === 0) {
4613
- return { tier: "ask", scope: trimmed };
4748
+ return { tier: "ask", scope: trimmed, ...outboundTexts.length ? { outboundTexts } : {} };
4614
4749
  }
4615
4750
  const scopes = [...new Set(nonAuto.flatMap((u) => {
4616
4751
  const web = webCommand(u.seg, opts.trustedWebResolves);
4617
- return web ? web.urls.map((url) => `${u.seg.binary.toLowerCase().replace(/\.exe$/, "")} ${url.origin}/*`) : [lineScoped ? trimmed : u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)];
4752
+ return web ? web.urls.map((url) => `${programName(u.seg.binary)} ${url.origin}/*`) : [lineScoped ? trimmed : u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)];
4618
4753
  }))].sort();
4619
4754
  const scope = scopes.join(" + ");
4620
4755
  const perInvocation = nonAuto.some((u) => webCommand(u.seg, opts.trustedWebResolves)?.perInvocation);
@@ -4622,7 +4757,8 @@ function classifyCommand(command, opts = {}) {
4622
4757
  return {
4623
4758
  tier: "ask",
4624
4759
  scope,
4625
- ...webUrls.length ? { webUrls, webOutboundTexts } : {},
4760
+ ...webUrls.length ? { webUrls } : {},
4761
+ ...outboundTexts.length ? { outboundTexts } : {},
4626
4762
  ...scopes.length > 1 && webUrls.length ? { scopes } : {},
4627
4763
  ...perInvocation ? { perInvocation } : {},
4628
4764
  ...allowedBy ? { allowedBy } : {}
@@ -6176,7 +6312,7 @@ ${TASK_CONTINUE}`);
6176
6312
  command = disableWebCommandStartup(command, this.researchShell.dialect);
6177
6313
  const { approval, root } = site;
6178
6314
  const allow = approval.allowlist?.();
6179
- const { tier, scope, reason, allowedBy, perInvocation, scopes: commandScopes, webUrls, webOutboundTexts } = classifyCommand(command, {
6315
+ const { tier, scope, reason, allowedBy, perInvocation, scopes: commandScopes, webUrls, outboundTexts } = classifyCommand(command, {
6180
6316
  ...this.policyOptions(),
6181
6317
  ...approval.commandPolicy?.(),
6182
6318
  ...allow ? { allow } : {}
@@ -6201,8 +6337,9 @@ ${TASK_CONTINUE}`);
6201
6337
  if (site.task?.sealed) return this.runSealed(site, command, request, "after", signal);
6202
6338
  if (tier === "refuse") return this.sealedInstead(site, command, request, refusal(`Command refused: ${reason}${site.task ? `
6203
6339
  ${TASK_UNSEALED} ${TASK_CONTINUE}` : ""}`), signal);
6204
- if (webUrls?.length && carriesSecret([...webOutboundTexts ?? [], ...webUrls.map((url) => url.href)], [...this.outboundSecrets(), ...sensitiveEnvValues(process.env)])) {
6205
- return refusal("Command refused: a web command contains what looks like a secret. Fetch the page with your web fetch tool, without credentials.");
6340
+ const outbound = [...outboundTexts ?? [], ...(webUrls ?? []).map((url) => url.href)];
6341
+ if (outbound.length && carriesSecret(outbound, [...this.outboundSecrets(), ...sensitiveEnvValues(process.env)])) {
6342
+ 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.");
6206
6343
  }
6207
6344
  const vettedAddresses = /* @__PURE__ */ new Map();
6208
6345
  for (const url of webUrls ?? []) {
@@ -6305,7 +6442,7 @@ ${outcome.output}${outcome.success ? "" : `
6305
6442
  }
6306
6443
  let shell;
6307
6444
  try {
6308
- const sealed = site.task?.gitBefore ? { ...site.sealed, gitEntries: site.task.gitBefore.entries.map(({ path: path46, directory }) => ({ path: path46, directory })) } : site.sealed;
6445
+ const sealed = site.task?.gitBefore ? { ...site.sealed, gitEntries: site.task.gitBefore.entries.map(({ path: path49, directory }) => ({ path: path49, directory })) } : site.sealed;
6309
6446
  shell = await site.sandbox.launchSealed(this.researchShell, sealed);
6310
6447
  } catch (err) {
6311
6448
  return { refused: refusal(`Command not run: its sealed sandbox could not be prepared (${err instanceof Error ? err.message : String(err)}).`) };
@@ -7984,6 +8121,7 @@ function enabledRunners(cfg) {
7984
8121
  }
7985
8122
 
7986
8123
  // src/plugins/builtin/claudeModels.ts
8124
+ import * as path12 from "path";
7987
8125
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
7988
8126
  var ANTHROPIC_VARIANT_LABELS = {
7989
8127
  adaptive: "Adaptive",
@@ -8077,6 +8215,53 @@ function parseClaudeHelp(stdout) {
8077
8215
  }
8078
8216
  return models;
8079
8217
  }
8218
+ function claudeConfigDir(env) {
8219
+ return env.CLAUDE_CONFIG_DIR?.trim() || "~/.claude";
8220
+ }
8221
+ function claudeManagedSettingsDir(platform = process.platform) {
8222
+ if (platform === "darwin") return "/Library/Application Support/ClaudeCode";
8223
+ if (platform === "win32") return "C:\\Program Files\\ClaudeCode";
8224
+ return "/etc/claude-code";
8225
+ }
8226
+ function readClaudeSettingsModels(files, platform = process.platform) {
8227
+ const managed = claudeManagedSettingsDir(platform);
8228
+ const join45 = platform === "win32" ? path12.win32.join : path12.posix.join;
8229
+ const dropIns = files.list(join45(managed, "managed-settings.d")).filter((name) => name.endsWith(".json") && !name.startsWith(".")).sort().map((name) => join45(managed, "managed-settings.d", name));
8230
+ const ranked = [...dropIns.reverse(), join45(managed, "managed-settings.json"), join45(claudeConfigDir(files.env), "settings.json")];
8231
+ for (const file of ranked) {
8232
+ const content = files.read(file);
8233
+ const lineup = content === null ? null : parseClaudeSettingsModels(content);
8234
+ if (lineup) return lineup;
8235
+ }
8236
+ return [];
8237
+ }
8238
+ function parseClaudeSettingsModels(content) {
8239
+ let parsed;
8240
+ try {
8241
+ parsed = JSON.parse(content);
8242
+ } catch {
8243
+ return null;
8244
+ }
8245
+ const picker = parsed && typeof parsed === "object" ? parsed.modelPicker : void 0;
8246
+ const options = picker && typeof picker === "object" ? picker.options : void 0;
8247
+ if (!Array.isArray(options)) return null;
8248
+ const seen = /* @__PURE__ */ new Set();
8249
+ const models = [];
8250
+ for (const row of options) {
8251
+ if (!row || typeof row !== "object") continue;
8252
+ const { model: raw, label } = row;
8253
+ const model = typeof raw === "string" ? raw.trim() : "";
8254
+ if (!model || seen.has(model)) continue;
8255
+ seen.add(model);
8256
+ models.push({
8257
+ modelId: model,
8258
+ modelLabel: typeof label === "string" && label.trim() ? label.trim() : model,
8259
+ runnerProvider: model.includes("/") ? model.split("/")[0] : void 0,
8260
+ variants: []
8261
+ });
8262
+ }
8263
+ return models;
8264
+ }
8080
8265
 
8081
8266
  // src/plugins/builtin/claude-code.manifest.ts
8082
8267
  var TURN_NOT_LOGGED_IN = /not logged in\s*·\s*please run \/login/i;
@@ -8115,18 +8300,24 @@ var CLAUDE_CODE_MANIFEST = {
8115
8300
  // 3. canonicalAliases — stable --model contracts (opus/sonnet/haiku/fable)
8116
8301
  // merged into any successful result to fill gaps, and used as the full
8117
8302
  // last-resort list when both API and CLI are unavailable.
8303
+ //
8304
+ // Whichever source answers, the models the user added to Claude's own
8305
+ // picker (`modelPicker.options` in its managed or user settings, usually
8306
+ // gateway IDs) are appended (settingsModels): the CLI accepts them as
8307
+ // `--model` values though no catalog above lists them.
8118
8308
  apiDiscovery: {
8119
8309
  url: "https://api.anthropic.com/v1/models?limit=100",
8120
8310
  headers: { "anthropic-version": "2023-06-01" },
8121
8311
  auth: [
8122
8312
  { type: "env", varName: "ANTHROPIC_API_KEY", header: "X-Api-Key" },
8123
- { type: "file", path: "~/.claude/.credentials.json", jsonPath: "claudeAiOauth.accessToken", header: "Authorization", prefix: "Bearer " }
8313
+ { type: "file", path: (env) => `${claudeConfigDir(env)}/.credentials.json`, jsonPath: "claudeAiOauth.accessToken", header: "Authorization", prefix: "Bearer " }
8124
8314
  ],
8125
8315
  parser: parseAnthropicModels
8126
8316
  },
8127
8317
  discoveryCommands: [
8128
8318
  { command: "claude", args: ["--help"], parser: parseClaudeHelp }
8129
8319
  ],
8320
+ settingsModels: (files) => readClaudeSettingsModels(files),
8130
8321
  canonicalAliases: [
8131
8322
  { modelId: "opus", modelLabel: "Opus" },
8132
8323
  { modelId: "sonnet", modelLabel: "Sonnet" },
@@ -8184,7 +8375,7 @@ import { join as join11, resolve as resolve4 } from "path";
8184
8375
 
8185
8376
  // src/services/harness/repoConfig.ts
8186
8377
  import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
8187
- import * as path12 from "path";
8378
+ import * as path13 from "path";
8188
8379
 
8189
8380
  // src/services/mcp/OrdewellMcpServer.ts
8190
8381
  import { createServer } from "http";
@@ -8897,12 +9088,12 @@ function checkpointReply(answer2) {
8897
9088
  return { text: `The checkpoint was withdrawn: ${answer2.why}`, isError: true };
8898
9089
  }
8899
9090
  }
8900
- function fieldPath(path46) {
8901
- return path46.reduce((text4, part) => typeof part === "number" ? `${text4}[${part}]` : text4 ? `${text4}.${String(part)}` : String(part), "");
9091
+ function fieldPath(path49) {
9092
+ return path49.reduce((text4, part) => typeof part === "number" ? `${text4}[${part}]` : text4 ? `${text4}.${String(part)}` : String(part), "");
8902
9093
  }
8903
- function valueAt(value, path46) {
9094
+ function valueAt(value, path49) {
8904
9095
  let current = value;
8905
- for (const part of path46) {
9096
+ for (const part of path49) {
8906
9097
  if (typeof part === "number") {
8907
9098
  if (!Array.isArray(current)) return void 0;
8908
9099
  current = current[part];
@@ -8924,7 +9115,7 @@ function boundedRefusal(issue, args) {
8924
9115
  var LEAKED_PARAMETER = /<\/parameter>|<parameter\s+name\s*=\s*"([^"]*)">/g;
8925
9116
  function leakedMarkupIn(value) {
8926
9117
  const found = [];
8927
- const walk = (node, path46) => {
9118
+ const walk = (node, path49) => {
8928
9119
  if (typeof node === "string") {
8929
9120
  const names = [];
8930
9121
  let closing = false;
@@ -8932,25 +9123,25 @@ function leakedMarkupIn(value) {
8932
9123
  if (match[1] !== void 0) names.push(match[1]);
8933
9124
  else closing = true;
8934
9125
  }
8935
- if (names.length > 0 || closing) found.push({ path: path46, names: [...new Set(names)], closing });
9126
+ if (names.length > 0 || closing) found.push({ path: path49, names: [...new Set(names)], closing });
8936
9127
  return;
8937
9128
  }
8938
9129
  if (Array.isArray(node)) {
8939
- node.forEach((item, i) => walk(item, `${path46}[${i}]`));
9130
+ node.forEach((item, i) => walk(item, `${path49}[${i}]`));
8940
9131
  return;
8941
9132
  }
8942
9133
  if (node !== null && typeof node === "object") {
8943
- for (const [key, item] of Object.entries(node)) walk(item, path46 ? `${path46}.${key}` : key);
9134
+ for (const [key, item] of Object.entries(node)) walk(item, path49 ? `${path49}.${key}` : key);
8944
9135
  }
8945
9136
  };
8946
9137
  walk(value, "");
8947
9138
  return found;
8948
9139
  }
8949
9140
  function markupRefusal(leaks) {
8950
- return leaks.map(({ path: path46, names, closing }) => {
9141
+ return leaks.map(({ path: path49, names, closing }) => {
8951
9142
  const tags = [...names.map((n) => `<parameter name="${n}">`), ...closing ? ["</parameter>"] : []];
8952
- const guide = names.length > 0 ? ` The parameters ${names.map((n) => JSON.stringify(n)).join(", ")} are inside it; pass them as separate arguments, not inside ${path46}.` : " Pass each argument separately, not inside one string.";
8953
- return `${path46} contains leaked tool-call markup: ${tags.join(", ")}.${guide}`;
9143
+ const guide = names.length > 0 ? ` The parameters ${names.map((n) => JSON.stringify(n)).join(", ")} are inside it; pass them as separate arguments, not inside ${path49}.` : " Pass each argument separately, not inside one string.";
9144
+ return `${path49} contains leaked tool-call markup: ${tags.join(", ")}.${guide}`;
8954
9145
  }).join("\n");
8955
9146
  }
8956
9147
  function invalidArguments(name, error, args) {
@@ -9415,9 +9606,9 @@ function mcpClientConfig(credential) {
9415
9606
  }
9416
9607
  function ownerOnlyConfigFile(fileName, contents) {
9417
9608
  const dir = mkdtempSync2(join9(tmpdir2(), "ordewell-mcp-"));
9418
- const path46 = join9(dir, fileName);
9419
- writeFileSync(path46, contents, { mode: 384 });
9420
- return { path: path46, remove: () => rmSync2(dir, { recursive: true, force: true }) };
9609
+ const path49 = join9(dir, fileName);
9610
+ writeFileSync(path49, contents, { mode: 384 });
9611
+ return { path: path49, remove: () => rmSync2(dir, { recursive: true, force: true }) };
9421
9612
  }
9422
9613
 
9423
9614
  // src/services/harness/repoConfig.ts
@@ -9440,19 +9631,19 @@ function onDisk(target) {
9440
9631
  try {
9441
9632
  return realpathSync2.native(target);
9442
9633
  } catch {
9443
- return path12.resolve(target);
9634
+ return path13.resolve(target);
9444
9635
  }
9445
9636
  }
9446
9637
  function insideOnDisk(root, target) {
9447
- const rel = path12.relative(onDisk(root), onDisk(target));
9448
- return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
9638
+ const rel = path13.relative(onDisk(root), onDisk(target));
9639
+ return rel === "" || !rel.startsWith("..") && !path13.isAbsolute(rel);
9449
9640
  }
9450
9641
  function repoLevels(workspace) {
9451
9642
  const levels = [];
9452
- for (let dir = path12.resolve(workspace); ; dir = path12.dirname(dir)) {
9643
+ for (let dir = path13.resolve(workspace); ; dir = path13.dirname(dir)) {
9453
9644
  levels.unshift(dir);
9454
- if (existsSync3(path12.join(dir, ".git"))) return levels;
9455
- if (path12.dirname(dir) === dir) return [path12.resolve(workspace)];
9645
+ if (existsSync3(path13.join(dir, ".git"))) return levels;
9646
+ if (path13.dirname(dir) === dir) return [path13.resolve(workspace)];
9456
9647
  }
9457
9648
  }
9458
9649
  function repoRoot(workspace) {
@@ -9546,33 +9737,33 @@ var TomlReader = class {
9546
9737
  if (this.i >= this.src.length) return root;
9547
9738
  if (this.src.startsWith("[[", this.i)) {
9548
9739
  this.i += 2;
9549
- const path46 = this.key();
9740
+ const path49 = this.key();
9550
9741
  this.expect("]]");
9551
- const parent = this.tableAt(root, path46.slice(0, -1));
9552
- const last = path46[path46.length - 1];
9742
+ const parent = this.tableAt(root, path49.slice(0, -1));
9743
+ const last = path49[path49.length - 1];
9553
9744
  const list = Object.hasOwn(parent, last) ? parent[last] : parent[last] = [];
9554
- if (!Array.isArray(list)) throw this.fail(`${path46.join(".")} is not an array of tables`);
9745
+ if (!Array.isArray(list)) throw this.fail(`${path49.join(".")} is not an array of tables`);
9555
9746
  table = newTable();
9556
9747
  list.push(table);
9557
9748
  } else if (this.src[this.i] === "[") {
9558
9749
  this.i += 1;
9559
- const path46 = this.key();
9750
+ const path49 = this.key();
9560
9751
  this.expect("]");
9561
- table = this.tableAt(root, path46);
9752
+ table = this.tableAt(root, path49);
9562
9753
  } else {
9563
- const path46 = this.key();
9754
+ const path49 = this.key();
9564
9755
  this.skipBlank(false);
9565
9756
  this.expect("=");
9566
9757
  this.skipBlank(false);
9567
- const owner = this.tableAt(table, path46.slice(0, -1));
9568
- owner[path46[path46.length - 1]] = this.value();
9758
+ const owner = this.tableAt(table, path49.slice(0, -1));
9759
+ owner[path49[path49.length - 1]] = this.value();
9569
9760
  }
9570
9761
  this.endOfLine();
9571
9762
  }
9572
9763
  }
9573
- tableAt(from, path46) {
9764
+ tableAt(from, path49) {
9574
9765
  let table = from;
9575
- for (const part of path46) {
9766
+ for (const part of path49) {
9576
9767
  const next = Object.hasOwn(table, part) ? table[part] : void 0;
9577
9768
  if (next === void 0) table = table[part] = newTable();
9578
9769
  else if (isRecord(next)) table = next;
@@ -9641,10 +9832,10 @@ var TomlReader = class {
9641
9832
  this.i++;
9642
9833
  return table;
9643
9834
  }
9644
- const path46 = this.key();
9835
+ const path49 = this.key();
9645
9836
  this.expect("=");
9646
9837
  this.skipBlank(false);
9647
- this.tableAt(table, path46.slice(0, -1))[path46[path46.length - 1]] = this.value();
9838
+ this.tableAt(table, path49.slice(0, -1))[path49[path49.length - 1]] = this.value();
9648
9839
  this.skipBlank(true);
9649
9840
  if (this.src[this.i] === ",") this.i++;
9650
9841
  else if (this.src[this.i] !== "}") throw this.fail("expected , or } in an inline table");
@@ -9789,7 +9980,8 @@ var CODEX_MANIFEST = {
9789
9980
  appServer: {
9790
9981
  command: "codex",
9791
9982
  args: ["app-server"],
9792
- cacheFile: "~/.codex/models_cache.json"
9983
+ // CODEX_HOME moves all of Codex's own files from `~/.codex`.
9984
+ cacheFile: (env) => `${env.CODEX_HOME?.trim() || "~/.codex"}/models_cache.json`
9793
9985
  },
9794
9986
  canonicalAliases: [
9795
9987
  { modelId: "gpt-5.6-sol", modelLabel: "GPT-5.6-Sol" },
@@ -10018,8 +10210,7 @@ var LineBuffer = class {
10018
10210
  // src/services/harness/ClaudeCodeAdapter.ts
10019
10211
  import { randomUUID as randomUUID3 } from "crypto";
10020
10212
  import { realpathSync as realpathSync7 } from "fs";
10021
- import * as os9 from "os";
10022
- import * as path20 from "path";
10213
+ import * as path22 from "path";
10023
10214
 
10024
10215
  // src/plugins/resolveArgs.ts
10025
10216
  function claudeThinkingArgs(effort) {
@@ -10057,14 +10248,29 @@ function resolveTaskRunnerFlags(manifest, ctx) {
10057
10248
  return flags;
10058
10249
  }
10059
10250
 
10251
+ // src/utils/runnerHome.ts
10252
+ import * as os5 from "os";
10253
+ function runnerHomeDir(env, platform = process.platform) {
10254
+ const name = platform === "win32" ? "USERPROFILE" : "HOME";
10255
+ const key = platform === "win32" ? Object.keys(env).filter((k) => k.toUpperCase() === name).pop() : name;
10256
+ const home = key === void 0 ? void 0 : env[key];
10257
+ return home ?? os5.homedir();
10258
+ }
10259
+ function expandRunnerHome(filePath, env = process.env) {
10260
+ if (!filePath.startsWith("~")) return filePath;
10261
+ const home = runnerHomeDir(env);
10262
+ return home ? home + filePath.slice(1) : null;
10263
+ }
10264
+
10060
10265
  // src/services/harness/runnerProcess.ts
10266
+ import { Socket } from "net";
10061
10267
  import { StringDecoder } from "string_decoder";
10062
10268
 
10063
10269
  // src/utils/shellPath.ts
10064
10270
  import { exec } from "child_process";
10065
10271
  import { promisify } from "util";
10066
- import * as os5 from "os";
10067
- import * as path13 from "path";
10272
+ import * as os6 from "os";
10273
+ import * as path14 from "path";
10068
10274
  var execAsync = promisify(exec);
10069
10275
  var LOGIN_SHELL_TIMEOUT_MS = 5e3;
10070
10276
  var PATH_MARKER = "__ORDEWELL_PATH__";
@@ -10073,42 +10279,42 @@ var defaultExec = async (command, options) => {
10073
10279
  return { stdout: String(stdout) };
10074
10280
  };
10075
10281
  function posixBinDirs() {
10076
- const home = os5.homedir();
10282
+ const home = os6.homedir();
10077
10283
  return [
10078
- path13.join(home, ".local", "bin"),
10079
- path13.join(home, ".opencode", "bin"),
10080
- path13.join(home, ".claude", "bin"),
10081
- path13.join(home, ".bun", "bin"),
10082
- path13.join(home, ".volta", "bin"),
10083
- path13.join(home, ".npm-global", "bin"),
10084
- path13.join(home, "bin"),
10284
+ path14.join(home, ".local", "bin"),
10285
+ path14.join(home, ".opencode", "bin"),
10286
+ path14.join(home, ".claude", "bin"),
10287
+ path14.join(home, ".bun", "bin"),
10288
+ path14.join(home, ".volta", "bin"),
10289
+ path14.join(home, ".npm-global", "bin"),
10290
+ path14.join(home, "bin"),
10085
10291
  "/usr/local/bin",
10086
10292
  "/opt/homebrew/bin"
10087
10293
  ];
10088
10294
  }
10089
10295
  function windowsBinDirs(env, home) {
10090
- const join43 = (...parts) => path13.win32.join(...parts);
10091
- const under = (base, ...parts) => base ? [join43(base, ...parts)] : [];
10296
+ const join45 = (...parts) => path14.win32.join(...parts);
10297
+ const under = (base, ...parts) => base ? [join45(base, ...parts)] : [];
10092
10298
  const appData = env.APPDATA;
10093
10299
  const localAppData = env.LOCALAPPDATA;
10094
10300
  const programData = env.ProgramData;
10095
10301
  return [
10096
10302
  // Native per-user installers, PowerShell-driven and otherwise.
10097
- join43(home, ".local", "bin"),
10098
- join43(home, ".opencode", "bin"),
10099
- join43(home, ".claude", "bin"),
10100
- join43(home, ".codex", "bin"),
10303
+ join45(home, ".local", "bin"),
10304
+ join45(home, ".opencode", "bin"),
10305
+ join45(home, ".claude", "bin"),
10306
+ join45(home, ".codex", "bin"),
10101
10307
  ...under(localAppData, "Programs", "claude"),
10102
10308
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
10103
10309
  // literal spelling is kept as a fallback for a host that does not export
10104
10310
  // APPDATA (a service account, a stripped CI container).
10105
10311
  ...under(appData, "npm"),
10106
- join43(home, "AppData", "Roaming", "npm"),
10312
+ join45(home, "AppData", "Roaming", "npm"),
10107
10313
  ...under(localAppData, "pnpm"),
10108
10314
  ...under(localAppData, "Yarn", "bin"),
10109
- join43(home, ".bun", "bin"),
10315
+ join45(home, ".bun", "bin"),
10110
10316
  // Windows package managers.
10111
- join43(home, "scoop", "shims"),
10317
+ join45(home, "scoop", "shims"),
10112
10318
  ...under(env.SCOOP, "shims"),
10113
10319
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
10114
10320
  ...under(programData, "chocolatey", "bin"),
@@ -10118,13 +10324,13 @@ function windowsBinDirs(env, home) {
10118
10324
  ...under(localAppData, "Microsoft", "WindowsApps"),
10119
10325
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
10120
10326
  ...under(localAppData, "Volta", "bin"),
10121
- join43(home, ".volta", "bin")
10327
+ join45(home, ".volta", "bin")
10122
10328
  ];
10123
10329
  }
10124
10330
  function wellKnownBinDirs(deps = {}) {
10125
10331
  const platform = deps.platform ?? process.platform;
10126
10332
  if (platform !== "win32") return posixBinDirs();
10127
- return [...new Set(windowsBinDirs(deps.env ?? process.env, deps.home ?? os5.homedir()))];
10333
+ return [...new Set(windowsBinDirs(deps.env ?? process.env, deps.home ?? os6.homedir()))];
10128
10334
  }
10129
10335
  async function loginShellPath(execImpl) {
10130
10336
  if (process.platform === "win32") return null;
@@ -10147,14 +10353,14 @@ function augmentedPath(execImpl = defaultExec) {
10147
10353
  const segments = [];
10148
10354
  const push = (p) => {
10149
10355
  if (!p) return;
10150
- for (const seg of p.split(path13.delimiter)) {
10356
+ for (const seg of p.split(path14.delimiter)) {
10151
10357
  if (seg && !segments.includes(seg)) segments.push(seg);
10152
10358
  }
10153
10359
  };
10154
10360
  push(process.env.PATH);
10155
10361
  push(await loginShellPath(execImpl));
10156
10362
  for (const dir of wellKnownBinDirs()) push(dir);
10157
- return segments.join(path13.delimiter);
10363
+ return segments.join(path14.delimiter);
10158
10364
  })();
10159
10365
  }
10160
10366
  return cachedPath;
@@ -10176,7 +10382,7 @@ function withPath(base, resolvedPath, overrides = {}) {
10176
10382
 
10177
10383
  // src/utils/launch.ts
10178
10384
  import * as fsSync4 from "fs";
10179
- import * as path14 from "path";
10385
+ import * as path15 from "path";
10180
10386
  var DIRECT_EXTENSIONS = [".exe", ".com"];
10181
10387
  var BATCH_EXTENSIONS = [".cmd", ".bat"];
10182
10388
  var POWERSHELL_EXTENSIONS = [".ps1"];
@@ -10184,7 +10390,7 @@ var CMD_EXE_MAX_COMMAND_LINE = 8191;
10184
10390
  var WINDOWS_MAX_COMMAND_LINE = 32767;
10185
10391
  function defaultPowerShell() {
10186
10392
  const root = process.env.SystemRoot || process.env.windir;
10187
- return root ? path14.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
10393
+ return root ? path15.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
10188
10394
  }
10189
10395
  var POWERSHELL_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"];
10190
10396
  var CommandLineTooLongError = class extends Error {
@@ -10250,7 +10456,7 @@ Searched: ${searchedPath || "(empty PATH)"}`);
10250
10456
  };
10251
10457
  function isResolvableOnPosixPath(command, PATH, exists) {
10252
10458
  if (command.includes("/")) return exists(command);
10253
- return PATH.split(path14.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path14.join(dir, command)));
10459
+ return PATH.split(path15.delimiter).map((d) => d.trim()).filter(Boolean).some((dir) => exists(path15.join(dir, command)));
10254
10460
  }
10255
10461
  function isExecutableResolved(command, plan, PATH, deps = {}) {
10256
10462
  if ((deps.platform ?? process.platform) === "win32") return plan.file !== command;
@@ -10290,9 +10496,9 @@ function cmdArgs(line) {
10290
10496
  return ["/d", "/s", "/c", `"${line}"`];
10291
10497
  }
10292
10498
  function candidateExtensions(command, pathExt) {
10293
- const declared = pathExt.split(path14.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
10499
+ const declared = pathExt.split(path15.win32.delimiter).map((e) => e.trim().toLowerCase()).filter(Boolean);
10294
10500
  const known = /* @__PURE__ */ new Set([...DIRECT_EXTENSIONS, ...BATCH_EXTENSIONS, ...POWERSHELL_EXTENSIONS, ...declared]);
10295
- const ext = path14.win32.extname(command).toLowerCase();
10501
+ const ext = path15.win32.extname(command).toLowerCase();
10296
10502
  if (ext && known.has(ext)) {
10297
10503
  const route = BATCH_EXTENSIONS.includes(ext) ? "cmd" : POWERSHELL_EXTENSIONS.includes(ext) ? "powershell" : "direct";
10298
10504
  return { direct: [], cmd: [], powershell: [], [route]: [""] };
@@ -10313,13 +10519,13 @@ async function findWindowsExecutables(command, deps) {
10313
10519
  const exists = deps.exists ?? defaultExists3;
10314
10520
  const pathExt = (deps.pathExt ?? (() => process.env.PATHEXT ?? ""))();
10315
10521
  const byRoute = candidateExtensions(command, pathExt);
10316
- const dirs = path14.win32.basename(command) !== command ? [path14.win32.dirname(path14.win32.resolve(command))] : (await (deps.resolvePath ?? augmentedPath)() || "").split(path14.win32.delimiter).map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
10317
- const base = path14.win32.basename(command);
10522
+ 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);
10523
+ const base = path15.win32.basename(command);
10318
10524
  const found = [];
10319
10525
  for (const route of ROUTES) {
10320
10526
  outer: for (const ext of byRoute[route]) {
10321
10527
  for (const dir of dirs) {
10322
- const candidate = path14.win32.join(dir, `${base}${ext}`);
10528
+ const candidate = path15.win32.join(dir, `${base}${ext}`);
10323
10529
  if (exists(candidate)) {
10324
10530
  found.push({ file: candidate, route });
10325
10531
  break outer;
@@ -10367,7 +10573,7 @@ async function planDirectLaunch(command, args, deps = {}) {
10367
10573
 
10368
10574
  // src/utils/workspace.ts
10369
10575
  import * as fs3 from "fs";
10370
- import * as path15 from "path";
10576
+ import * as path16 from "path";
10371
10577
  var WorkspaceNotFoundError = class extends Error {
10372
10578
  constructor(workspace) {
10373
10579
  super(`Workspace "${workspace}" does not exist.`);
@@ -10423,7 +10629,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
10423
10629
  throw new WorkspaceNotAProjectError(workspace);
10424
10630
  }
10425
10631
  const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
10426
- (marker2) => exists(path15.join(resolved, marker2))
10632
+ (marker2) => exists(path16.join(resolved, marker2))
10427
10633
  );
10428
10634
  if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
10429
10635
  }
@@ -10431,12 +10637,12 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
10431
10637
  // src/services/workspaceEnv.ts
10432
10638
  import { execFile as execFile3 } from "child_process";
10433
10639
  import * as fs5 from "fs";
10434
- import * as os6 from "os";
10435
- import * as path17 from "path";
10640
+ import * as os7 from "os";
10641
+ import * as path18 from "path";
10436
10642
 
10437
10643
  // src/utils/fsHelpers.ts
10438
10644
  import * as fs4 from "fs";
10439
- import * as path16 from "path";
10645
+ import * as path17 from "path";
10440
10646
  import { randomUUID as randomUUID2 } from "crypto";
10441
10647
  var STATE_DIR = ".ordewell";
10442
10648
  function ensureDir(dir) {
@@ -10445,9 +10651,9 @@ function ensureDir(dir) {
10445
10651
  }
10446
10652
  }
10447
10653
  function writeFileAtomic(filePath, content) {
10448
- const dir = path16.dirname(filePath);
10654
+ const dir = path17.dirname(filePath);
10449
10655
  ensureDir(dir);
10450
- const tempPath = path16.join(dir, `.${path16.basename(filePath)}.${randomUUID2()}.tmp`);
10656
+ const tempPath = path17.join(dir, `.${path17.basename(filePath)}.${randomUUID2()}.tmp`);
10451
10657
  fs4.writeFileSync(tempPath, content, { flag: "wx" });
10452
10658
  try {
10453
10659
  fs4.renameSync(tempPath, filePath);
@@ -10460,7 +10666,7 @@ function writeFileAtomic(filePath, content) {
10460
10666
  }
10461
10667
  }
10462
10668
  function getStateDir(baseDir) {
10463
- return path16.join(baseDir ?? process.cwd(), STATE_DIR);
10669
+ return path17.join(baseDir ?? process.cwd(), STATE_DIR);
10464
10670
  }
10465
10671
  var IGNORE_FILE = ".gitignore";
10466
10672
  var STATE_DIR_IGNORE = "*\n!.gitignore\n!skills/\n!skills/**\n";
@@ -10468,7 +10674,7 @@ var LEGACY_IGNORES = /* @__PURE__ */ new Set(["*", "*\n"]);
10468
10674
  function ensureStateDirIgnored(baseDir) {
10469
10675
  const dir = getStateDir(baseDir);
10470
10676
  ensureDir(dir);
10471
- const file = path16.join(dir, IGNORE_FILE);
10677
+ const file = path17.join(dir, IGNORE_FILE);
10472
10678
  try {
10473
10679
  fs4.writeFileSync(file, STATE_DIR_IGNORE, { flag: "wx" });
10474
10680
  return;
@@ -11003,7 +11209,7 @@ function defaultDeps() {
11003
11209
  }
11004
11210
  },
11005
11211
  baseEnv: process.env,
11006
- home: os6.homedir()
11212
+ home: os7.homedir()
11007
11213
  };
11008
11214
  }
11009
11215
  var REFUSED2 = new Set(SETTINGS_ENV_REFUSED);
@@ -11015,7 +11221,7 @@ async function resolveWorkspaceEnv(cwd, overrides = {}) {
11015
11221
  const direnv = await fromDirenv(checkoutDirOf(cwd, deps), deps);
11016
11222
  result.blockedEnvrc = direnv.blocked;
11017
11223
  Object.assign(collected, direnv.env);
11018
- const file = findUp(cwd, path17.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
11224
+ const file = findUp(cwd, path18.join(STATE_DIR, WORKSPACE_ENV_FILE), deps);
11019
11225
  if (file) {
11020
11226
  const content = deps.readFile(file);
11021
11227
  if (content !== null) {
@@ -11032,7 +11238,7 @@ async function resolveWorkspaceEnv(cwd, overrides = {}) {
11032
11238
  }
11033
11239
  var evaluating = /* @__PURE__ */ new Map();
11034
11240
  async function workspaceEnvOf(cwd) {
11035
- const key = path17.resolve(cwd);
11241
+ const key = path18.resolve(cwd);
11036
11242
  let running = evaluating.get(key);
11037
11243
  if (!running) {
11038
11244
  running = resolveWorkspaceEnv(cwd).then((result) => result.env).finally(() => evaluating.delete(key));
@@ -11041,11 +11247,11 @@ async function workspaceEnvOf(cwd) {
11041
11247
  return { ...await running };
11042
11248
  }
11043
11249
  function checkoutDirOf(cwd, deps) {
11044
- const parts = path17.resolve(cwd).split(path17.sep);
11250
+ const parts = path18.resolve(cwd).split(path18.sep);
11045
11251
  const at = parts.findIndex((part, i) => part === STATE_DIR && parts[i + 1] === "worktrees");
11046
11252
  if (at < 0 || parts.length < at + 4) return cwd;
11047
- const root = parts.slice(0, at).join(path17.sep) || path17.sep;
11048
- const mapped = path17.join(root, ...parts.slice(at + 4));
11253
+ const root = parts.slice(0, at).join(path18.sep) || path18.sep;
11254
+ const mapped = path18.join(root, ...parts.slice(at + 4));
11049
11255
  return deps.isDirectory(mapped) ? mapped : root;
11050
11256
  }
11051
11257
  async function fromDirenv(cwd, deps) {
@@ -11075,18 +11281,18 @@ async function fromDirenv(cwd, deps) {
11075
11281
  return { env, blocked: null };
11076
11282
  }
11077
11283
  function findUp(start, relative17, deps) {
11078
- let dir = path17.resolve(start);
11284
+ let dir = path18.resolve(start);
11079
11285
  for (; ; ) {
11080
- const candidate = path17.join(dir, relative17);
11286
+ const candidate = path18.join(dir, relative17);
11081
11287
  if (deps.readFile(candidate) !== null) return candidate;
11082
- const parent = path17.dirname(dir);
11288
+ const parent = path18.dirname(dir);
11083
11289
  if (parent === dir) return null;
11084
11290
  dir = parent;
11085
11291
  }
11086
11292
  }
11087
11293
  async function isTracked(file, deps) {
11088
11294
  try {
11089
- const out = await deps.run("git", ["ls-files", "--error-unmatch", path17.basename(file)], { cwd: path17.dirname(file), env: deps.baseEnv });
11295
+ const out = await deps.run("git", ["ls-files", "--error-unmatch", path18.basename(file)], { cwd: path18.dirname(file), env: deps.baseEnv });
11090
11296
  return out.code === 0;
11091
11297
  } catch {
11092
11298
  return false;
@@ -11096,7 +11302,7 @@ function parseEnvFile(content, context, home) {
11096
11302
  const env = {};
11097
11303
  const lookup2 = (name) => env[name] ?? context[name] ?? "";
11098
11304
  const expand = (value) => {
11099
- const tilde = value.startsWith("~/") ? path17.join(home, value.slice(2)) : value;
11305
+ const tilde = value.startsWith("~/") ? path18.join(home, value.slice(2)) : value;
11100
11306
  return tilde.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => lookup2(braced ?? bare));
11101
11307
  };
11102
11308
  for (const raw of content.split(/\r?\n/)) {
@@ -11366,7 +11572,7 @@ function readOnlySessionRules(site) {
11366
11572
  real = realpathSync4.native(root);
11367
11573
  } catch {
11368
11574
  }
11369
- for (const path46 of /* @__PURE__ */ new Set([root, real])) rules.push({ action: "external_directory", resource: `${path46.replace(/[\\/]$/, "")}/*`, effect: "allow" });
11575
+ for (const path49 of /* @__PURE__ */ new Set([root, real])) rules.push({ action: "external_directory", resource: `${path49.replace(/[\\/]$/, "")}/*`, effect: "allow" });
11370
11576
  }
11371
11577
  return rules;
11372
11578
  }
@@ -11492,6 +11698,14 @@ function runnerEnv(resolvedPath, workspace = {}, launch = {}, drop) {
11492
11698
  // src/services/harness/runnerProcess.ts
11493
11699
  var STDERR_TAIL_CHARS = 4e3;
11494
11700
  var CLOSE_GRACE_MS = 2e3;
11701
+ function releasePipes(child) {
11702
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
11703
+ if (stream instanceof Socket) stream.unref();
11704
+ }
11705
+ for (const stream of [child.stdout, child.stderr]) {
11706
+ if (stream instanceof Socket) stream.resume();
11707
+ }
11708
+ }
11495
11709
  var RunnerProcess = class {
11496
11710
  constructor(deps) {
11497
11711
  this.deps = deps;
@@ -11564,6 +11778,7 @@ var RunnerProcess = class {
11564
11778
  const grace = setTimeout(() => {
11565
11779
  for (const flush of [...this.stdoutEnds]) flush();
11566
11780
  this.end({ code, signal });
11781
+ releasePipes(child);
11567
11782
  }, CLOSE_GRACE_MS);
11568
11783
  grace.unref?.();
11569
11784
  void this.ended.then(() => clearTimeout(grace));
@@ -11662,11 +11877,11 @@ ${tail}` : ""}`;
11662
11877
 
11663
11878
  // src/services/sandbox/nativeShellLauncher.ts
11664
11879
  import { chmodSync, mkdtempSync as mkdtempSync3, realpathSync as realpathSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
11665
- import * as os7 from "os";
11666
- import * as path18 from "path";
11880
+ import * as os8 from "os";
11881
+ import * as path19 from "path";
11667
11882
  function nativeShellInvocation(platform, env = process.env) {
11668
11883
  const shell = platform === "darwin" ? "/bin/zsh" : env.SHELL;
11669
- return { shell: shell && path18.posix.isAbsolute(shell) ? shell : "/bin/sh", args: ["-c"] };
11884
+ return { shell: shell && path19.posix.isAbsolute(shell) ? shell : "/bin/sh", args: ["-c"] };
11670
11885
  }
11671
11886
  var live = /* @__PURE__ */ new Set();
11672
11887
  var exitHook = false;
@@ -11698,10 +11913,10 @@ function launcherScript(launch) {
11698
11913
  }
11699
11914
  async function createNativeShellLauncher(opts) {
11700
11915
  const invocation = nativeShellInvocation(opts.platform ?? process.platform, opts.env);
11701
- const root = realpathSync5(mkdtempSync3(path18.join(os7.tmpdir(), "ordewell-native-shell-")));
11916
+ const root = realpathSync5(mkdtempSync3(path19.join(os8.tmpdir(), "ordewell-native-shell-")));
11702
11917
  let shortTmp = null;
11703
11918
  try {
11704
- shortTmp = realpathSync5(mkdtempSync3(path18.join("/tmp", "ow-")));
11919
+ shortTmp = realpathSync5(mkdtempSync3(path19.join("/tmp", "ow-")));
11705
11920
  const tmpdir7 = shortTmp;
11706
11921
  const launch = await opts.sandbox.launchNativeShell({
11707
11922
  workspace: opts.workspace,
@@ -11710,7 +11925,7 @@ async function createNativeShellLauncher(opts) {
11710
11925
  shellArgs: invocation.args,
11711
11926
  ...opts.env ? { env: opts.env } : {}
11712
11927
  });
11713
- const prefix = path18.join(root, "run");
11928
+ const prefix = path19.join(root, "run");
11714
11929
  writeFileSync3(prefix, launcherScript(launch), { mode: 448 });
11715
11930
  chmodSync(prefix, 448);
11716
11931
  keep(root);
@@ -12079,6 +12294,20 @@ ${lines.map((line) => `${line}
12079
12294
  }
12080
12295
 
12081
12296
  // src/services/harness/claudeOrdewell.ts
12297
+ import * as path20 from "path";
12298
+ function assertNoClaudeTaskCollision(cwd) {
12299
+ const text4 = readText(path20.join(cwd, ".mcp.json"));
12300
+ if (text4 === void 0) return;
12301
+ let parsed;
12302
+ try {
12303
+ parsed = JSON.parse(text4);
12304
+ } catch {
12305
+ return;
12306
+ }
12307
+ if (isRecord(parsed) && isRecord(parsed.mcpServers) && Object.hasOwn(parsed.mcpServers, ORDEWELL_MCP_SERVER_NAME)) {
12308
+ throw new Error(".mcp.json declares an MCP server named ordewell; rename or remove the ordewell server before starting this task.");
12309
+ }
12310
+ }
12082
12311
  var CLAUDE_ORDEWELL = {
12083
12312
  toolName: MCP_CLIENT_TOOL_NAMES.toolName,
12084
12313
  toolNames: MCP_CLIENT_TOOL_NAMES.toolNames,
@@ -12226,9 +12455,8 @@ var ReplyRuns = class {
12226
12455
  };
12227
12456
 
12228
12457
  // src/services/harness/claudeRepoConfig.ts
12229
- import * as os8 from "os";
12230
- import * as path19 from "path";
12231
- var MEMORY_FILES = ["CLAUDE.md", path19.join(".claude", "CLAUDE.md"), "CLAUDE.local.md"];
12458
+ import * as path21 from "path";
12459
+ var MEMORY_FILES = ["CLAUDE.md", path21.join(".claude", "CLAUDE.md"), "CLAUDE.local.md"];
12232
12460
  var MAX_IMPORT_DEPTH = 5;
12233
12461
  function importsOf(text4) {
12234
12462
  const prose = text4.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
@@ -12239,11 +12467,11 @@ function withImports(workspace, file, depth, seen, into) {
12239
12467
  seen.add(file);
12240
12468
  const text4 = readText(file);
12241
12469
  if (text4 === void 0) return;
12242
- into.push({ path: path19.relative(workspace, file), text: text4 });
12470
+ into.push({ path: path21.relative(workspace, file), text: text4 });
12243
12471
  if (depth >= MAX_IMPORT_DEPTH) return;
12244
12472
  for (const target of importsOf(text4)) {
12245
12473
  if (target.startsWith("~")) continue;
12246
- const abs = path19.resolve(path19.dirname(file), target);
12474
+ const abs = path21.resolve(path21.dirname(file), target);
12247
12475
  if (insideOnDisk(workspace, abs)) withImports(workspace, abs, depth + 1, seen, into);
12248
12476
  }
12249
12477
  }
@@ -12264,12 +12492,12 @@ var CLAUDE_REPO_CONFIG = {
12264
12492
  instructions(workspace) {
12265
12493
  const files = [];
12266
12494
  const seen = /* @__PURE__ */ new Set();
12267
- for (const name of MEMORY_FILES) withImports(workspace, path19.join(workspace, name), 0, seen, files);
12495
+ for (const name of MEMORY_FILES) withImports(workspace, path21.join(workspace, name), 0, seen, files);
12268
12496
  return files;
12269
12497
  },
12270
12498
  projectServers(workspace) {
12271
12499
  const source = ".mcp.json";
12272
- const text4 = readText(path19.join(workspace, source));
12500
+ const text4 = readText(path21.join(workspace, source));
12273
12501
  if (text4 === void 0) return NO_SERVERS;
12274
12502
  let parsed;
12275
12503
  try {
@@ -12287,13 +12515,14 @@ var CLAUDE_REPO_CONFIG = {
12287
12515
  return found;
12288
12516
  }
12289
12517
  };
12290
- function claudeLocalServers(configDir2, workspace) {
12291
- const file = configDir2 ? path19.join(configDir2, ".claude.json") : path19.join(os8.homedir(), ".claude.json");
12292
- const text4 = readText(file);
12518
+ function claudeLocalServers(env, workspace) {
12519
+ const configDir2 = env.CLAUDE_CONFIG_DIR?.trim();
12520
+ const file = expandRunnerHome(configDir2 ? path21.join(configDir2, ".claude.json") : "~/.claude.json", env);
12521
+ const text4 = file === null ? void 0 : readText(file);
12293
12522
  if (text4 === void 0) return {};
12294
12523
  try {
12295
12524
  const parsed = JSON.parse(text4);
12296
- const project = isRecord(parsed) && isRecord(parsed.projects) ? parsed.projects[path19.resolve(workspace)] : void 0;
12525
+ const project = isRecord(parsed) && isRecord(parsed.projects) ? parsed.projects[path21.resolve(workspace)] : void 0;
12297
12526
  return isRecord(project) && isRecord(project.mcpServers) ? project.mcpServers : {};
12298
12527
  } catch {
12299
12528
  return {};
@@ -12548,13 +12777,13 @@ var NO_AUTO_MEMORY = { CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
12548
12777
  function inSavedOutput(abs, configDir2, sessionId) {
12549
12778
  let projects;
12550
12779
  try {
12551
- projects = realpathSync7.native(path20.join(configDir2, "projects"));
12780
+ projects = realpathSync7.native(path22.join(configDir2, "projects"));
12552
12781
  } catch {
12553
12782
  return false;
12554
12783
  }
12555
- const rel = path20.relative(projects, abs);
12556
- if (rel.startsWith("..") || path20.isAbsolute(rel)) return false;
12557
- const [project, session, folder] = rel.split(path20.sep);
12784
+ const rel = path22.relative(projects, abs);
12785
+ if (rel.startsWith("..") || path22.isAbsolute(rel)) return false;
12786
+ const [project, session, folder] = rel.split(path22.sep);
12558
12787
  return Boolean(project) && session === sessionId && folder === "tool-results";
12559
12788
  }
12560
12789
  var PATH_TOOLS = {
@@ -12792,7 +13021,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12792
13021
  "--append-system-prompt",
12793
13022
  opts.systemPrompt
12794
13023
  ];
12795
- if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner", this.plannerServers(opts, workspace)));
13024
+ if (opts.mcp) args.push(...this.ordewellServerArgs(opts.mcp, "planner", this.plannerServers(opts, { ...process.env, ...workspace })));
12796
13025
  if (opts.model) args.push("--model", opts.model);
12797
13026
  if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
12798
13027
  if (opts.resumeSessionId) {
@@ -12800,7 +13029,8 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12800
13029
  this.countFrom(opts.resumeSessionId, opts.resumeCost);
12801
13030
  this.pendingResume = opts.resumeSessionId;
12802
13031
  }
12803
- this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path20.join(os9.homedir(), ".claude");
13032
+ const env = { ...process.env, ...workspace };
13033
+ this.configDir = expandRunnerHome(claudeConfigDir(env), env);
12804
13034
  return {
12805
13035
  command: "claude",
12806
13036
  args,
@@ -12844,6 +13074,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12844
13074
  */
12845
13075
  taskSpawnSpec(opts) {
12846
13076
  const readOnly = readOnlyTask(opts);
13077
+ if (!readOnly) assertNoClaudeTaskCollision(opts.cwd);
12847
13078
  this.readOnlySite = readOnly ? opts : null;
12848
13079
  this.requestedMode = readOnly ? PLANNER_PERMISSION_MODE : opts.flags.permissionMode;
12849
13080
  this.followedModel = opts.model;
@@ -12870,13 +13101,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
12870
13101
  /**
12871
13102
  * What user-only settings stop the CLI loading that is not the
12872
13103
  * repository's to choose (ADR-0028): the user's own local-scope servers,
12873
- * under the `CLAUDE_CONFIG_DIR` the planner runs with, and the project
12874
- * servers approved for it.
13104
+ * from the account in the environment the planner runs with, and the
13105
+ * project servers approved for it.
12875
13106
  */
12876
- plannerServers(opts, workspace) {
12877
- const configDir2 = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR;
13107
+ plannerServers(opts, env) {
12878
13108
  return {
12879
- ...claudeLocalServers(configDir2, opts.cwd),
13109
+ ...claudeLocalServers(env, opts.cwd),
12880
13110
  ...Object.fromEntries((opts.projectServers ?? []).map((server4) => [server4.name, server4.definition]))
12881
13111
  };
12882
13112
  }
@@ -13640,8 +13870,8 @@ function runProbe(deps, cwd, env, flags) {
13640
13870
  }
13641
13871
 
13642
13872
  // src/services/harness/codexRepoConfig.ts
13643
- import * as path21 from "path";
13644
- var CONFIG_FILE = path21.join(".codex", "config.toml");
13873
+ import * as path23 from "path";
13874
+ var CONFIG_FILE = path23.join(".codex", "config.toml");
13645
13875
  function stringsOf(value) {
13646
13876
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
13647
13877
  }
@@ -13667,9 +13897,9 @@ var CODEX_REPO_CONFIG = {
13667
13897
  const files = [];
13668
13898
  for (const dir of repoLevels(workspace)) {
13669
13899
  for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
13670
- const text4 = readText(path21.join(dir, name));
13900
+ const text4 = readText(path23.join(dir, name));
13671
13901
  if (text4 === void 0) continue;
13672
- files.push({ path: path21.relative(workspace, path21.join(dir, name)) || name, text: text4 });
13902
+ files.push({ path: path23.relative(workspace, path23.join(dir, name)) || name, text: text4 });
13673
13903
  break;
13674
13904
  }
13675
13905
  }
@@ -13680,10 +13910,10 @@ var CODEX_REPO_CONFIG = {
13680
13910
  const byName = /* @__PURE__ */ new Map();
13681
13911
  const unreadable = [];
13682
13912
  for (const dir of repoLevels(workspace)) {
13683
- const file = path21.join(dir, CONFIG_FILE);
13913
+ const file = path23.join(dir, CONFIG_FILE);
13684
13914
  const text4 = readText(file);
13685
13915
  if (text4 === void 0) continue;
13686
- const source = path21.relative(workspace, file);
13916
+ const source = path23.relative(workspace, file);
13687
13917
  let parsed;
13688
13918
  try {
13689
13919
  parsed = parseToml(text4);
@@ -13703,9 +13933,9 @@ var CODEX_REPO_CONFIG = {
13703
13933
  };
13704
13934
  function untrustedProjects(workspace) {
13705
13935
  const projects = {};
13706
- for (let dir = path21.resolve(workspace); ; dir = path21.dirname(dir)) {
13936
+ for (let dir = path23.resolve(workspace); ; dir = path23.dirname(dir)) {
13707
13937
  projects[dir] = { trust_level: "untrusted" };
13708
- if (path21.dirname(dir) === dir) return projects;
13938
+ if (path23.dirname(dir) === dir) return projects;
13709
13939
  }
13710
13940
  }
13711
13941
 
@@ -14826,7 +15056,7 @@ ${this.exitMessage()}`);
14826
15056
  case "webSearch":
14827
15057
  return;
14828
15058
  case "fileChange": {
14829
- const paths = (item.changes ?? []).map((change) => change.path).filter((path46) => !!path46);
15059
+ const paths = (item.changes ?? []).map((change) => change.path).filter((path49) => !!path49);
14830
15060
  this.fileChangePaths.set(item.id, paths);
14831
15061
  if (this.startOpts?.kind === "task") {
14832
15062
  for (const change of item.changes ?? []) this.announceFileChange(item.id, change, emit, subagentId);
@@ -14929,20 +15159,20 @@ ${this.exitMessage()}`);
14929
15159
  announceFileChange(itemId, change, emit, subagentId) {
14930
15160
  const rows = this.fileChangeRows.get(itemId) ?? /* @__PURE__ */ new Map();
14931
15161
  this.fileChangeRows.set(itemId, rows);
14932
- const path46 = this.shownChangePath(change);
14933
- const known = rows.get(path46);
15162
+ const path49 = this.shownChangePath(change);
15163
+ const known = rows.get(path49);
14934
15164
  if (known) return known;
14935
- const row = rows.size === 0 ? itemId : `${itemId}:${path46}`;
14936
- rows.set(path46, row);
14937
- emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path46 }, ...subagentId ? { subagentId } : {} });
15165
+ const row = rows.size === 0 ? itemId : `${itemId}:${path49}`;
15166
+ rows.set(path49, row);
15167
+ emit({ type: "tool_call", id: row, name: changeName(change), args: { path: path49 }, ...subagentId ? { subagentId } : {} });
14938
15168
  return row;
14939
15169
  }
14940
15170
  /** Relative to the task's worktree, where every path but a stray one lies; a move names both ends. */
14941
15171
  shownChangePath(change) {
14942
- const shown = (path46) => {
15172
+ const shown = (path49) => {
14943
15173
  const cwd = this.startOpts?.cwd;
14944
- const rel = cwd ? relative8(cwd, path46) : path46;
14945
- return rel && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute6(rel) ? rel : path46;
15174
+ const rel = cwd ? relative8(cwd, path49) : path49;
15175
+ return rel && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute6(rel) ? rel : path49;
14946
15176
  };
14947
15177
  const from = shown(change.path ?? "");
14948
15178
  return change.kind?.move_path ? `${from} \u2192 ${shown(change.kind.move_path)}` : from;
@@ -14993,7 +15223,7 @@ import { realpathSync as realpathSync8 } from "fs";
14993
15223
 
14994
15224
  // src/services/harness/projectServers.ts
14995
15225
  import { createHash } from "crypto";
14996
- import * as path22 from "path";
15226
+ import * as path24 from "path";
14997
15227
  function canonical2(value) {
14998
15228
  if (Array.isArray(value)) return value.map(canonical2);
14999
15229
  if (!isRecord(value)) return value;
@@ -15004,7 +15234,7 @@ function projectServerScope(server4) {
15004
15234
  return `mcp-server:${server4.name}@${hash}`;
15005
15235
  }
15006
15236
  var scopeName = (scope) => scope.slice(0, scope.lastIndexOf("@"));
15007
- function fileProjectServerApprovals(file = path22.join(globalDataDir(), "project-mcp-servers.json")) {
15237
+ function fileProjectServerApprovals(file = path24.join(globalDataDir(), "project-mcp-servers.json")) {
15008
15238
  const load = () => {
15009
15239
  const text4 = readText(file);
15010
15240
  if (text4 === void 0) return {};
@@ -16167,8 +16397,8 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
16167
16397
  this.v2 = new OpenCodeV2({
16168
16398
  version,
16169
16399
  nativeShell: this.nativeShell?.prefix ?? null,
16170
- request: (method, path46, body, signal, timeoutMs) => this.request(method, path46, body, signal, timeoutMs),
16171
- json: (method, path46, body, signal, timeoutMs) => this.json(method, path46, body, signal, timeoutMs),
16400
+ request: (method, path49, body, signal, timeoutMs) => this.request(method, path49, body, signal, timeoutMs),
16401
+ json: (method, path49, body, signal, timeoutMs) => this.json(method, path49, body, signal, timeoutMs),
16172
16402
  openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
16173
16403
  processEnded: this.processEnded,
16174
16404
  isExited: () => this.exited,
@@ -16805,15 +17035,15 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
16805
17035
  onActivity
16806
17036
  );
16807
17037
  }
16808
- async json(method, path46, body, signal, timeoutMs) {
16809
- const response = await this.request(method, path46, body, signal, timeoutMs);
16810
- if (!response.ok) throw new OpenCodeHttpError(`${method} ${path46} failed: ${response.status} ${response.statusText}`);
17038
+ async json(method, path49, body, signal, timeoutMs) {
17039
+ const response = await this.request(method, path49, body, signal, timeoutMs);
17040
+ if (!response.ok) throw new OpenCodeHttpError(`${method} ${path49} failed: ${response.status} ${response.statusText}`);
16811
17041
  return await response.json().catch(() => null);
16812
17042
  }
16813
17043
  /** `timeoutMs` null is for a turn's own POST, which the server answers when the turn is done. */
16814
- request(method, path46, body, signal, timeoutMs = REQUEST_TIMEOUT_MS) {
17044
+ request(method, path49, body, signal, timeoutMs = REQUEST_TIMEOUT_MS) {
16815
17045
  const deadline = withDeadline(signal, timeoutMs);
16816
- return this.deps.fetch(`${this.baseUrl}${path46}`, {
17046
+ return this.deps.fetch(`${this.baseUrl}${path49}`, {
16817
17047
  method,
16818
17048
  headers: this.headers({ "content-type": "application/json" }),
16819
17049
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
@@ -16874,8 +17104,8 @@ var OpenCodeAdapter = class extends ProcessLifecycle {
16874
17104
  };
16875
17105
 
16876
17106
  // src/services/harness/openCodeRepoConfig.ts
16877
- import * as path23 from "path";
16878
- var CONFIG_FILES = ["opencode.json", "opencode.jsonc", path23.join(".opencode", "opencode.json"), path23.join(".opencode", "opencode.jsonc")];
17107
+ import * as path25 from "path";
17108
+ var CONFIG_FILES = ["opencode.json", "opencode.jsonc", path25.join(".opencode", "opencode.json"), path25.join(".opencode", "opencode.jsonc")];
16879
17109
  var INSTRUCTION_NAMES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"];
16880
17110
  function server3(name, entry, source) {
16881
17111
  if (!isRecord(entry)) return "not an object";
@@ -16894,8 +17124,8 @@ var OPENCODE_REPO_CONFIG = {
16894
17124
  const levels = repoLevels(workspace);
16895
17125
  for (const name of INSTRUCTION_NAMES) {
16896
17126
  const files = levels.flatMap((dir) => {
16897
- const text4 = readText(path23.join(dir, name));
16898
- return text4 === void 0 ? [] : [{ path: path23.relative(workspace, path23.join(dir, name)) || name, text: text4 }];
17127
+ const text4 = readText(path25.join(dir, name));
17128
+ return text4 === void 0 ? [] : [{ path: path25.relative(workspace, path25.join(dir, name)) || name, text: text4 }];
16899
17129
  });
16900
17130
  if (files.length > 0) return files;
16901
17131
  }
@@ -16907,10 +17137,10 @@ var OPENCODE_REPO_CONFIG = {
16907
17137
  const unreadable = [];
16908
17138
  for (const dir of repoLevels(workspace)) {
16909
17139
  for (const name of CONFIG_FILES) {
16910
- const file = path23.join(dir, name);
17140
+ const file = path25.join(dir, name);
16911
17141
  const text4 = readText(file);
16912
17142
  if (text4 === void 0) continue;
16913
- const source = path23.relative(workspace, file);
17143
+ const source = path25.relative(workspace, file);
16914
17144
  let parsed;
16915
17145
  try {
16916
17146
  parsed = parseJsonc(text4);
@@ -17099,7 +17329,7 @@ function knownModelId(value, optionIds, shortcuts) {
17099
17329
 
17100
17330
  // src/services/ModelCatalog.ts
17101
17331
  import * as fs6 from "fs";
17102
- import * as path24 from "path";
17332
+ import * as path26 from "path";
17103
17333
  var CACHE_KEY = "model-catalog";
17104
17334
  var CACHE_TTL_MS = 60 * 60 * 1e3;
17105
17335
  function storageKey(baseUrl) {
@@ -17152,7 +17382,7 @@ var ModelCatalog = class {
17152
17382
  const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
17153
17383
  const key = storageKey(url);
17154
17384
  const cacheDir = globalCacheDir();
17155
- const cacheFile = path24.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17385
+ const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17156
17386
  if (fs6.existsSync(cacheFile)) fs6.rmSync(cacheFile);
17157
17387
  } catch {
17158
17388
  }
@@ -17160,7 +17390,7 @@ var ModelCatalog = class {
17160
17390
  static readCache(key) {
17161
17391
  try {
17162
17392
  const cacheDir = globalCacheDir();
17163
- const cacheFile = path24.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17393
+ const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17164
17394
  if (!fs6.existsSync(cacheFile)) return null;
17165
17395
  const raw = fs6.readFileSync(cacheFile, "utf8");
17166
17396
  const data = JSON.parse(raw);
@@ -17174,7 +17404,7 @@ var ModelCatalog = class {
17174
17404
  try {
17175
17405
  const cacheDir = globalCacheDir();
17176
17406
  fs6.mkdirSync(cacheDir, { recursive: true });
17177
- const cacheFile = path24.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17407
+ const cacheFile = path26.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
17178
17408
  fs6.writeFileSync(
17179
17409
  cacheFile,
17180
17410
  JSON.stringify({ models, cachedAt: Date.now() }, null, 2)
@@ -17185,15 +17415,17 @@ var ModelCatalog = class {
17185
17415
  };
17186
17416
 
17187
17417
  // src/services/ModelDiscovery.ts
17188
- import * as path25 from "path";
17418
+ import * as path27 from "path";
17189
17419
  import { exec as exec2 } from "child_process";
17190
17420
  import { promisify as promisify2 } from "util";
17191
17421
  import * as fsSync5 from "fs";
17192
- import * as osMod from "os";
17193
17422
 
17194
17423
  // src/services/harness/appServerModels.ts
17195
17424
  import { spawn as nodeSpawn } from "child_process";
17196
17425
  var DEFAULT_TIMEOUT_MS3 = 2e4;
17426
+ function definedOnly(env) {
17427
+ return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
17428
+ }
17197
17429
  function parseRpcLine(line) {
17198
17430
  try {
17199
17431
  const value = JSON.parse(line);
@@ -17206,10 +17438,9 @@ async function openAppServer(command, args, launch) {
17206
17438
  const runner = new RunnerProcess({
17207
17439
  spawn: nodeSpawn,
17208
17440
  fetch: globalThis.fetch,
17209
- // The cwd is no workspace: discovery asks for the catalog before one is
17210
- // chosen, so it pays no direnv round trip before a spawn with a deadline.
17211
- // The account monitor passes the workspace's environment itself, since
17212
- // that can choose whose account is read.
17441
+ // The cwd is no workspace, so it pays no direnv round trip before a spawn
17442
+ // with a deadline. Discovery and the account monitor pass the workspace's
17443
+ // environment themselves, since that can choose whose account is read.
17213
17444
  workspaceEnv: async () => ({}),
17214
17445
  ...launch.deps
17215
17446
  });
@@ -17262,7 +17493,11 @@ async function openAppServer(command, args, launch) {
17262
17493
  }
17263
17494
  var APP_SERVER_CLIENT_INFO = { name: "ordewell", title: "Ordewell", version: "0.1.0" };
17264
17495
  async function discoverAppServerModels(command, args, options = {}) {
17265
- const server4 = await openAppServer(command, args, { cwd: process.cwd(), deps: options.deps });
17496
+ const env = options.env;
17497
+ const server4 = await openAppServer(command, args, {
17498
+ cwd: process.cwd(),
17499
+ deps: { ...env ? { workspaceEnv: async () => definedOnly(env) } : {}, ...options.deps }
17500
+ });
17266
17501
  if (!server4) return null;
17267
17502
  const { rpc } = server4;
17268
17503
  const asked = (async () => {
@@ -17289,10 +17524,10 @@ async function discoverAppServerModels(command, args, options = {}) {
17289
17524
  var execAsync2 = promisify2(exec2);
17290
17525
  var defaultExec2 = async (command, options) => {
17291
17526
  const PATH = await augmentedPath();
17292
- const { stdout } = await execAsync2(command, { ...options, maxBuffer: 32 * 1024 * 1024, env: withPath(process.env, PATH) });
17527
+ const { stdout } = await execAsync2(command, { timeout: options?.timeout, maxBuffer: 32 * 1024 * 1024, env: withPath(options?.env ?? process.env, PATH) });
17293
17528
  return { stdout: String(stdout) };
17294
17529
  };
17295
- var defaultAppServerClient = (command, args, options) => discoverAppServerModels(command, args, { timeout: options?.timeout });
17530
+ var defaultAppServerClient = (command, args, options) => discoverAppServerModels(command, args, { timeout: options?.timeout, env: options?.env });
17296
17531
  function effortVariant(effort) {
17297
17532
  return { id: effort, label: effort.charAt(0).toUpperCase() + effort.slice(1) };
17298
17533
  }
@@ -17326,7 +17561,7 @@ function geminiDiscoveryCacheKey(apiKey) {
17326
17561
  return {
17327
17562
  key: `gemini-models:${hash}`,
17328
17563
  dir,
17329
- file: path25.join(dir, `gemini_models_${hash}.json`)
17564
+ file: path27.join(dir, `gemini_models_${hash}.json`)
17330
17565
  };
17331
17566
  }
17332
17567
  function readGeminiCache(apiKey) {
@@ -17413,15 +17648,22 @@ function applyVariants(models, manifest) {
17413
17648
  if (!variants || variants.length === 0) return models;
17414
17649
  return models.map((m) => m.variants.length > 0 ? m : { ...m, variants });
17415
17650
  }
17416
- function resolveApiAuth(authMethods) {
17651
+ function listDir(dir) {
17652
+ try {
17653
+ return fsSync5.readdirSync(dir);
17654
+ } catch {
17655
+ return [];
17656
+ }
17657
+ }
17658
+ function resolveApiAuth(authMethods, env) {
17417
17659
  for (const method of authMethods) {
17418
17660
  try {
17419
17661
  let token;
17420
17662
  if (method.type === "env") {
17421
- token = process.env[method.varName];
17663
+ token = env[method.varName];
17422
17664
  } else {
17423
- const filePath = method.path.replace(/^~/, osMod.homedir());
17424
- if (!fsSync5.existsSync(filePath)) continue;
17665
+ const filePath = expandRunnerHome(typeof method.path === "function" ? method.path(env) : method.path, env);
17666
+ if (!filePath || !fsSync5.existsSync(filePath)) continue;
17425
17667
  const data = JSON.parse(fsSync5.readFileSync(filePath, "utf8"));
17426
17668
  token = method.jsonPath.split(".").reduce((obj, key) => {
17427
17669
  if (obj && typeof obj === "object") return obj[key];
@@ -17437,8 +17679,8 @@ function resolveApiAuth(authMethods) {
17437
17679
  }
17438
17680
  return null;
17439
17681
  }
17440
- async function discoverFromApi(manifest, config, fetchImpl) {
17441
- const auth = resolveApiAuth(config.auth);
17682
+ async function discoverFromApi(manifest, config, env, fetchImpl) {
17683
+ const auth = resolveApiAuth(config.auth, env);
17442
17684
  if (!auth) return null;
17443
17685
  try {
17444
17686
  const headers = { ...config.headers, [auth.header]: auth.value };
@@ -17454,7 +17696,7 @@ async function discoverFromApi(manifest, config, fetchImpl) {
17454
17696
  function wasKilled(err) {
17455
17697
  return !!err && typeof err === "object" && err.killed === true;
17456
17698
  }
17457
- async function discoverFromCommand(manifest, execImpl) {
17699
+ async function discoverFromCommand(manifest, execImpl, env) {
17458
17700
  const discovery = manifest.modelDiscovery;
17459
17701
  const commands = discovery.discoveryCommands ?? [
17460
17702
  { command: discovery.command, args: discovery.args || [], parser: discovery.parser }
@@ -17463,7 +17705,7 @@ async function discoverFromCommand(manifest, execImpl) {
17463
17705
  let timedOut2 = false;
17464
17706
  for (const cmd of commands) {
17465
17707
  try {
17466
- const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
17708
+ const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3, env });
17467
17709
  const models = parseModelOutput(stdout, cmd.parser, manifest);
17468
17710
  if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
17469
17711
  } catch (err) {
@@ -17529,21 +17771,26 @@ var ModelDiscovery = class {
17529
17771
  * same background service.
17530
17772
  */
17531
17773
  inflight = /* @__PURE__ */ new Map();
17774
+ /** Bumped by every `clear()`: a discovery begun before one answers its own lookups but fills no cache. */
17775
+ generation = 0;
17532
17776
  registry;
17533
17777
  execImpl;
17534
17778
  fetchImpl;
17535
17779
  appServerClient;
17536
17780
  readFileImpl;
17537
17781
  now;
17538
- constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
17782
+ env;
17783
+ constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now, env = async () => process.env) {
17539
17784
  this.registry = registry;
17540
17785
  this.execImpl = execImpl;
17541
17786
  this.fetchImpl = fetchImpl;
17542
17787
  this.appServerClient = appServerClient;
17543
17788
  this.now = now;
17789
+ this.env = env;
17544
17790
  this.readFileImpl = readFileImpl ?? ((filePath) => {
17545
17791
  try {
17546
- return fsSync5.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
17792
+ const file = expandRunnerHome(filePath);
17793
+ return file ? fsSync5.readFileSync(file, "utf8") : null;
17547
17794
  } catch {
17548
17795
  return null;
17549
17796
  }
@@ -17555,16 +17802,17 @@ var ModelDiscovery = class {
17555
17802
  * null when both fail so `discover()` falls through to the remaining
17556
17803
  * methods.
17557
17804
  */
17558
- async discoverFromAppServer(manifest) {
17805
+ async discoverFromAppServer(manifest, env) {
17559
17806
  const config = manifest.modelDiscovery.appServer;
17560
17807
  if (!config) return null;
17561
- const raw = await this.appServerClient(config.command, config.args);
17808
+ const raw = await this.appServerClient(config.command, config.args, { env });
17562
17809
  if (raw) {
17563
17810
  const models = parseAppServerModels(raw);
17564
17811
  if (models.length > 0) return models;
17565
17812
  }
17566
- if (config.cacheFile) {
17567
- const content = this.readFileImpl(config.cacheFile);
17813
+ const cacheFile = config.cacheFile && expandRunnerHome(typeof config.cacheFile === "function" ? config.cacheFile(env) : config.cacheFile, env);
17814
+ if (cacheFile) {
17815
+ const content = this.readFileImpl(cacheFile);
17568
17816
  if (content) {
17569
17817
  const models = parseCodexModelsCache(content);
17570
17818
  if (models.length > 0) return models;
@@ -17602,37 +17850,60 @@ var ModelDiscovery = class {
17602
17850
  if (!manifest) {
17603
17851
  return [];
17604
17852
  }
17605
- let models;
17853
+ const generation2 = this.generation;
17854
+ const env = await this.env().catch(() => process.env);
17855
+ const found = await this.discoverFromSources(manifest, env);
17856
+ const models = this.withSettingsModels(found.models, manifest, env);
17857
+ if (generation2 !== this.generation) return models;
17858
+ if (found.cacheable && models.length > 0) this.cache.set(runner, models);
17859
+ else this.misses.set(runner, { models, at: this.now() });
17860
+ return models;
17861
+ }
17862
+ async discoverFromSources(manifest, env) {
17606
17863
  let cacheable = true;
17607
17864
  const discovery = manifest.modelDiscovery;
17608
17865
  if (discovery.appServer) {
17609
- const appServerModels = await this.discoverFromAppServer(manifest);
17610
- if (appServerModels && appServerModels.length > 0) {
17611
- this.cache.set(runner, appServerModels);
17612
- return appServerModels;
17613
- }
17866
+ const appServerModels = await this.discoverFromAppServer(manifest, env);
17867
+ if (appServerModels && appServerModels.length > 0) return { models: appServerModels, cacheable: true };
17614
17868
  cacheable = false;
17615
17869
  }
17616
17870
  if (discovery.apiDiscovery) {
17617
- const apiModels = await discoverFromApi(manifest, discovery.apiDiscovery, this.fetchImpl);
17871
+ const apiModels = await discoverFromApi(manifest, discovery.apiDiscovery, env, this.fetchImpl);
17618
17872
  if (apiModels && apiModels.length > 0) {
17619
- models = applyVariants(mergeCanonicalAliases(apiModels, manifest), manifest);
17620
- if (cacheable && models.length > 0) this.cache.set(runner, models);
17621
- return models;
17873
+ return { models: applyVariants(mergeCanonicalAliases(apiModels, manifest), manifest), cacheable };
17622
17874
  }
17623
17875
  }
17624
17876
  if (discovery.method === "command" && (discovery.command || discovery.discoveryCommands?.length)) {
17625
- const result = await discoverFromCommand(manifest, this.execImpl);
17626
- models = result.models;
17627
- cacheable = !result.fromFallback;
17628
- } else {
17629
- models = buildFallbackModels(manifest);
17877
+ const result = await discoverFromCommand(manifest, this.execImpl, env);
17878
+ return { models: result.models, cacheable: !result.fromFallback };
17630
17879
  }
17631
- if (cacheable && models.length > 0) this.cache.set(runner, models);
17632
- else this.misses.set(runner, { models, at: this.now() });
17633
- return models;
17880
+ return { models: buildFallbackModels(manifest), cacheable };
17881
+ }
17882
+ /**
17883
+ * Append the models registered in the runner's own settings. Read on every
17884
+ * discovery rather than cached apart, so a refresh picks up an edit;
17885
+ * whatever the sources already offer keeps its entry and metadata.
17886
+ */
17887
+ withSettingsModels(models, manifest, env) {
17888
+ const read2 = manifest.modelDiscovery.settingsModels;
17889
+ if (!read2) return models;
17890
+ const files = {
17891
+ read: (filePath) => {
17892
+ const file = expandRunnerHome(filePath, env);
17893
+ return file ? this.readFileImpl(file) : null;
17894
+ },
17895
+ list: (dir) => {
17896
+ const expanded = expandRunnerHome(dir, env);
17897
+ return expanded ? listDir(expanded) : [];
17898
+ },
17899
+ env
17900
+ };
17901
+ const seen = new Set(models.map((m) => m.modelId));
17902
+ const added = read2(files).filter((m) => !seen.has(m.modelId));
17903
+ return added.length > 0 ? [...models, ...applyVariants(added, manifest)] : models;
17634
17904
  }
17635
17905
  clear() {
17906
+ this.generation++;
17636
17907
  this.cache.clear();
17637
17908
  this.misses.clear();
17638
17909
  this.inflight.clear();
@@ -17798,7 +18069,7 @@ function parseMaxParallel(value) {
17798
18069
 
17799
18070
  // src/services/SettingsService.ts
17800
18071
  import * as fs12 from "fs";
17801
- import * as path32 from "path";
18072
+ import * as path34 from "path";
17802
18073
 
17803
18074
  // src/utils/fileLock.ts
17804
18075
  import * as fs7 from "fs";
@@ -18088,7 +18359,7 @@ function stable(value) {
18088
18359
  }
18089
18360
 
18090
18361
  // src/services/runnerExit.ts
18091
- 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;
18362
+ 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;
18092
18363
  var USAGE_LIMIT_TAIL = 4096;
18093
18364
  function classifyRunnerStop(said2) {
18094
18365
  if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
@@ -18316,8 +18587,8 @@ var TurnStream = class {
18316
18587
  };
18317
18588
 
18318
18589
  // src/services/skillInvocation.ts
18319
- import * as os10 from "os";
18320
- function abbreviateHome(file, home = os10.homedir(), platform = process.platform) {
18590
+ import * as os9 from "os";
18591
+ function abbreviateHome(file, home = os9.homedir(), platform = process.platform) {
18321
18592
  const win = platform === "win32";
18322
18593
  const normal = (p) => win ? p.replace(/\\/g, "/") : p;
18323
18594
  const shown = normal(file);
@@ -18332,13 +18603,13 @@ function modelInvocablePlannerSkills(skills) {
18332
18603
  function modelInvocableTaskSkills(skills) {
18333
18604
  return skills.filter((skill) => skill.appliesTo === "task" && skill.modelInvocable);
18334
18605
  }
18335
- function snapshotSkill(skill, invokedBy, home = os10.homedir()) {
18606
+ function snapshotSkill(skill, invokedBy, home = os9.homedir()) {
18336
18607
  return { invokedBy, name: skill.name, source: skill.source, path: abbreviateHome(skill.path, home), content: skill.content };
18337
18608
  }
18338
18609
  function attachSkill(skill, home) {
18339
18610
  return { ...snapshotSkill(skill, "user", home), content: "", attaches: { description: skill.description } };
18340
18611
  }
18341
- function resolveSkillInvocation(text4, skillsService, home = os10.homedir()) {
18612
+ function resolveSkillInvocation(text4, skillsService, home = os9.homedir()) {
18342
18613
  const skills = skillTokens(text4).flatMap(({ name }) => {
18343
18614
  const skill = skillsService.findSkill(name);
18344
18615
  if (!skill?.userInvocable) return [];
@@ -18402,33 +18673,33 @@ ${skillBlock(entry.skill)}`, timestamp: entry.timestamp });
18402
18673
  }
18403
18674
 
18404
18675
  // src/services/taskSkills.ts
18405
- import * as path31 from "path";
18676
+ import * as path33 from "path";
18406
18677
 
18407
18678
  // src/services/SkillsService.ts
18408
18679
  import { createHash as createHash2 } from "crypto";
18409
18680
  import * as fs11 from "fs";
18410
- import * as path30 from "path";
18681
+ import * as path32 from "path";
18411
18682
 
18412
18683
  // src/services/builtinSkills.ts
18413
- import * as path26 from "path";
18684
+ import * as path28 from "path";
18414
18685
  import { fileURLToPath } from "url";
18415
18686
  function builtinSkillsDir() {
18416
- const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path26.dirname(fileURLToPath(
18687
+ const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path28.dirname(fileURLToPath(
18417
18688
  // @ts-expect-error - import.meta is only valid under an ESM module setting
18418
18689
  import.meta.url
18419
18690
  ));
18420
- return path26.join(dir, "..", "skills");
18691
+ return path28.join(dir, "..", "skills");
18421
18692
  }
18422
18693
 
18423
18694
  // src/services/repoGroup.ts
18424
18695
  import * as fs10 from "fs";
18425
- import * as path29 from "path";
18696
+ import * as path31 from "path";
18426
18697
 
18427
18698
  // src/services/worktreeBootstrap.ts
18428
18699
  import { exec as exec3 } from "child_process";
18429
18700
  import { promisify as promisify4 } from "util";
18430
18701
  import * as fs9 from "fs";
18431
- import * as path28 from "path";
18702
+ import * as path30 from "path";
18432
18703
 
18433
18704
  // src/services/gitExec.ts
18434
18705
  import { execFile as execFile4 } from "child_process";
@@ -18510,7 +18781,7 @@ function subcommandOf2(args) {
18510
18781
 
18511
18782
  // src/services/worktreeLink.ts
18512
18783
  import * as fs8 from "fs";
18513
- import * as path27 from "path";
18784
+ import * as path29 from "path";
18514
18785
  function linkPath(source, target, platform, hardLink = fs8.linkSync) {
18515
18786
  const isDir = fs8.statSync(source).isDirectory();
18516
18787
  if (platform !== "win32") {
@@ -18535,8 +18806,8 @@ function mirrorDir(source, target, opts) {
18535
18806
  const mirror = (from, to, prefix, top) => {
18536
18807
  fs8.mkdirSync(to, { recursive: true });
18537
18808
  for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
18538
- const name = path27.join(prefix, entry.name);
18539
- const at = { from: path27.join(from, entry.name), to: path27.join(to, entry.name), name };
18809
+ const name = path29.join(prefix, entry.name);
18810
+ const at = { from: path29.join(from, entry.name), to: path29.join(to, entry.name), name };
18540
18811
  if (entry.isSymbolicLink()) links.push(at);
18541
18812
  else if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin")) mirror(at.from, at.to, name, false);
18542
18813
  else if (linkPath(at.from, at.to, opts.platform, opts.hardLink) === "copy") copied.push(name);
@@ -18556,16 +18827,16 @@ function mirrorDir(source, target, opts) {
18556
18827
  }
18557
18828
  function relink(from, to, opts) {
18558
18829
  const text4 = fs8.readlinkSync(from);
18559
- const pointee = path27.isAbsolute(text4) ? rebased(text4, opts) : text4;
18830
+ const pointee = path29.isAbsolute(text4) ? rebased(text4, opts) : text4;
18560
18831
  if (opts.platform !== "win32") {
18561
- if (!fs8.existsSync(path27.resolve(path27.dirname(to), pointee)) && fs8.existsSync(path27.resolve(path27.dirname(from), pointee))) {
18562
- fs8.symlinkSync(rebased(path27.resolve(path27.dirname(from), pointee), opts), to);
18832
+ if (!fs8.existsSync(path29.resolve(path29.dirname(to), pointee)) && fs8.existsSync(path29.resolve(path29.dirname(from), pointee))) {
18833
+ fs8.symlinkSync(rebased(path29.resolve(path29.dirname(from), pointee), opts), to);
18563
18834
  return "symlink";
18564
18835
  }
18565
18836
  fs8.symlinkSync(pointee, to);
18566
18837
  return "symlink";
18567
18838
  }
18568
- const resolved = fs8.existsSync(path27.resolve(path27.dirname(to), pointee)) ? path27.resolve(path27.dirname(to), pointee) : rebased(path27.resolve(path27.dirname(from), pointee), opts);
18839
+ const resolved = fs8.existsSync(path29.resolve(path29.dirname(to), pointee)) ? path29.resolve(path29.dirname(to), pointee) : rebased(path29.resolve(path29.dirname(from), pointee), opts);
18569
18840
  try {
18570
18841
  fs8.statSync(resolved);
18571
18842
  } catch {
@@ -18574,9 +18845,9 @@ function relink(from, to, opts) {
18574
18845
  return linkPath(resolved, to, opts.platform, opts.hardLink);
18575
18846
  }
18576
18847
  function rebased(pointee, opts) {
18577
- const rel = path27.relative(opts.from, pointee);
18578
- const inside2 = rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
18579
- return inside2 && !rel.split(/[\\/]/).includes("node_modules") ? path27.join(opts.to, rel) : pointee;
18848
+ const rel = path29.relative(opts.from, pointee);
18849
+ const inside2 = rel !== "" && !rel.startsWith("..") && !path29.isAbsolute(rel);
18850
+ return inside2 && !rel.split(/[\\/]/).includes("node_modules") ? path29.join(opts.to, rel) : pointee;
18580
18851
  }
18581
18852
 
18582
18853
  // src/services/worktreeBootstrap.ts
@@ -18593,7 +18864,7 @@ function lexists(target) {
18593
18864
  return false;
18594
18865
  }
18595
18866
  }
18596
- function listDir(dir) {
18867
+ function listDir2(dir) {
18597
18868
  try {
18598
18869
  return fs9.readdirSync(dir).sort();
18599
18870
  } catch {
@@ -18613,18 +18884,18 @@ function matchLinks(root, patterns) {
18613
18884
  }
18614
18885
  function segmentMatches(root, base, segment) {
18615
18886
  const under = (name) => base ? `${base}/${name}` : name;
18616
- if (!/[*?]/.test(segment)) return lexists(path28.join(root, base, segment)) ? [under(segment)] : [];
18887
+ if (!/[*?]/.test(segment)) return lexists(path30.join(root, base, segment)) ? [under(segment)] : [];
18617
18888
  const pattern = new RegExp(`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
18618
- return listDir(path28.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
18889
+ return listDir2(path30.join(root, base)).filter((name) => !NEVER_SCANNED.has(name) && pattern.test(name)).map(under);
18619
18890
  }
18620
18891
  function installDirs(root) {
18621
18892
  const packages = matchLinks(root, workspaceGlobs(root)).map((pkg) => `${pkg}/node_modules`);
18622
- return ["node_modules", ...packages].filter((dir) => lexists(path28.join(root, dir)));
18893
+ return ["node_modules", ...packages].filter((dir) => lexists(path30.join(root, dir)));
18623
18894
  }
18624
18895
  function workspaceGlobs(root) {
18625
18896
  let manifest;
18626
18897
  try {
18627
- manifest = JSON.parse(fs9.readFileSync(path28.join(root, "package.json"), "utf8"));
18898
+ manifest = JSON.parse(fs9.readFileSync(path30.join(root, "package.json"), "utf8"));
18628
18899
  } catch {
18629
18900
  return [];
18630
18901
  }
@@ -18639,19 +18910,19 @@ async function bootstrap(repo, cwd, opts) {
18639
18910
  const linked = [];
18640
18911
  const copied = [];
18641
18912
  const link = (name) => {
18642
- const target = path28.join(cwd, name);
18913
+ const target = path30.join(cwd, name);
18643
18914
  if (lexists(target)) return;
18644
- fs9.mkdirSync(path28.dirname(target), { recursive: true });
18645
- if (linkPath(path28.join(repo.root, name), target, opts.platform) === "copy") copied.push(name);
18915
+ fs9.mkdirSync(path30.dirname(target), { recursive: true });
18916
+ if (linkPath(path30.join(repo.root, name), target, opts.platform) === "copy") copied.push(name);
18646
18917
  linked.push(name);
18647
18918
  };
18648
18919
  const mirror = (name) => {
18649
- const target = path28.join(cwd, name);
18650
- if (lexists(target) || !fs9.existsSync(path28.dirname(target))) return;
18651
- const source = path28.join(repo.root, name);
18920
+ const target = path30.join(cwd, name);
18921
+ if (lexists(target) || !fs9.existsSync(path30.dirname(target))) return;
18922
+ const source = path30.join(repo.root, name);
18652
18923
  if (!fs9.lstatSync(source).isDirectory()) return link(name);
18653
18924
  const mirrored = mirrorDir(source, target, { platform: opts.platform, from: repo.root, to: cwd });
18654
- copied.push(...mirrored.map((entry) => path28.join(name, entry)));
18925
+ copied.push(...mirrored.map((entry) => path30.join(name, entry)));
18655
18926
  linked.push(name);
18656
18927
  };
18657
18928
  if (!setup) {
@@ -18686,12 +18957,12 @@ async function runSetup(command, repo, cwd, resolvePath) {
18686
18957
 
18687
18958
  // src/services/repoGroup.ts
18688
18959
  function hasGitEntry(dir) {
18689
- return fs10.existsSync(path29.join(dir, ".git"));
18960
+ return fs10.existsSync(path31.join(dir, ".git"));
18690
18961
  }
18691
18962
  function groupPathOf(listed3) {
18692
18963
  const slashed = listed3.trim().replace(/\\/g, "/");
18693
- if (path29.posix.isAbsolute(slashed) || path29.win32.isAbsolute(slashed)) return null;
18694
- const rel = path29.posix.normalize(slashed).replace(/\/+$/, "");
18964
+ if (path31.posix.isAbsolute(slashed) || path31.win32.isAbsolute(slashed)) return null;
18965
+ const rel = path31.posix.normalize(slashed).replace(/\/+$/, "");
18695
18966
  return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") ? null : rel;
18696
18967
  }
18697
18968
  function childDirs(dir) {
@@ -18702,14 +18973,14 @@ function childDirs(dir) {
18702
18973
  }
18703
18974
  }
18704
18975
  function groupPaths(workspaceRoot, listed3) {
18705
- if (listed3.length === 0) return childDirs(workspaceRoot).filter((name) => hasGitEntry(path29.join(workspaceRoot, name)));
18706
- const paths = listed3.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path29.join(workspaceRoot, rel)));
18976
+ if (listed3.length === 0) return childDirs(workspaceRoot).filter((name) => hasGitEntry(path31.join(workspaceRoot, name)));
18977
+ const paths = listed3.map(groupPathOf).filter((rel) => rel !== null && hasGitEntry(path31.join(workspaceRoot, rel)));
18707
18978
  return [...new Set(paths)].sort();
18708
18979
  }
18709
18980
  function workspaceRepoGroup(workspaceRoot, listed3) {
18710
- for (let dir = path29.resolve(workspaceRoot); ; dir = path29.dirname(dir)) {
18981
+ for (let dir = path31.resolve(workspaceRoot); ; dir = path31.dirname(dir)) {
18711
18982
  if (hasGitEntry(dir)) return [SELF_REPO];
18712
- if (path29.dirname(dir) === dir) break;
18983
+ if (path31.dirname(dir) === dir) break;
18713
18984
  }
18714
18985
  return groupPaths(workspaceRoot, listed3);
18715
18986
  }
@@ -18790,8 +19061,8 @@ function copyDirSync(src, dest) {
18790
19061
  fs11.mkdirSync(dest, { recursive: true });
18791
19062
  const entries = fs11.readdirSync(src, { withFileTypes: true });
18792
19063
  for (const entry of entries) {
18793
- const srcPath = path30.join(src, entry.name);
18794
- const destPath = path30.join(dest, entry.name);
19064
+ const srcPath = path32.join(src, entry.name);
19065
+ const destPath = path32.join(dest, entry.name);
18795
19066
  if (entry.isDirectory()) copyDirSync(srcPath, destPath);
18796
19067
  else fs11.copyFileSync(srcPath, destPath);
18797
19068
  }
@@ -18815,7 +19086,7 @@ function readDir(dir, source) {
18815
19086
  if (!fs11.existsSync(dir)) return { skills, invalid };
18816
19087
  for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
18817
19088
  if (!entry.isDirectory()) continue;
18818
- const skillFile = path30.join(dir, entry.name, "SKILL.md");
19089
+ const skillFile = path32.join(dir, entry.name, "SKILL.md");
18819
19090
  if (!fs11.existsSync(skillFile)) continue;
18820
19091
  if (!isSkillName(entry.name)) {
18821
19092
  invalid.push({ name: entry.name, path: skillFile, source, reason: INVALID_SKILL_NAME_REASON });
@@ -18834,15 +19105,15 @@ function skillFileIn(dir, name) {
18834
19105
  return void 0;
18835
19106
  }
18836
19107
  if (!entries.some((entry) => entry.isDirectory() && entry.name === name)) return void 0;
18837
- const skillFile = path30.join(dir, name, "SKILL.md");
19108
+ const skillFile = path32.join(dir, name, "SKILL.md");
18838
19109
  return fs11.existsSync(skillFile) ? skillFile : void 0;
18839
19110
  }
18840
19111
  function skillsDirOf(root) {
18841
- return path30.join(root, STATE_DIR, "skills");
19112
+ return path32.join(root, STATE_DIR, "skills");
18842
19113
  }
18843
19114
  function workspaceSkillRoots(workspaceRoot, repos, checkout = workspaceRoot) {
18844
19115
  if (repos.length === 0 || repos.includes(SELF_REPO)) return [checkout];
18845
- return [workspaceRoot, ...repos.map((repo) => path30.join(checkout, repo))];
19116
+ return [workspaceRoot, ...repos.map((repo) => path32.join(checkout, repo))];
18846
19117
  }
18847
19118
  var SkillsService = class _SkillsService {
18848
19119
  workspaceRoots;
@@ -18865,11 +19136,11 @@ var SkillsService = class _SkillsService {
18865
19136
  return this.workspaceRoots.map(skillsDirOf);
18866
19137
  }
18867
19138
  globalDir() {
18868
- return path30.join(globalDataDir(), "skills");
19139
+ return path32.join(globalDataDir(), "skills");
18869
19140
  }
18870
19141
  readSeedManifest() {
18871
19142
  try {
18872
- const parsed = JSON.parse(fs11.readFileSync(path30.join(this.globalDir(), SEED_MANIFEST), "utf8"));
19143
+ const parsed = JSON.parse(fs11.readFileSync(path32.join(this.globalDir(), SEED_MANIFEST), "utf8"));
18873
19144
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
18874
19145
  return Object.fromEntries(
18875
19146
  Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
@@ -18885,7 +19156,7 @@ var SkillsService = class _SkillsService {
18885
19156
  manifest[name] = hash;
18886
19157
  try {
18887
19158
  fs11.mkdirSync(this.globalDir(), { recursive: true });
18888
- fs11.writeFileSync(path30.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
19159
+ fs11.writeFileSync(path32.join(this.globalDir(), SEED_MANIFEST), JSON.stringify(manifest, null, 2));
18889
19160
  } catch (err) {
18890
19161
  console.warn(`Could not record seeded skill ${name}: ${String(err)}`);
18891
19162
  }
@@ -18896,11 +19167,11 @@ var SkillsService = class _SkillsService {
18896
19167
  return this.readSeedManifest()[name] === currentHash || (PRIOR_BUILTIN_HASHES[name] ?? []).includes(currentHash);
18897
19168
  }
18898
19169
  seed(name) {
18899
- const dest = path30.join(this.globalDir(), name);
19170
+ const dest = path32.join(this.globalDir(), name);
18900
19171
  const exists = fs11.existsSync(dest);
18901
19172
  let srcDir;
18902
19173
  try {
18903
- srcDir = path30.join(builtinSkillsDir(), name);
19174
+ srcDir = path32.join(builtinSkillsDir(), name);
18904
19175
  } catch (err) {
18905
19176
  if (!exists) console.warn(`Could not locate built-in skills dir: ${String(err)}`);
18906
19177
  return false;
@@ -18909,9 +19180,9 @@ var SkillsService = class _SkillsService {
18909
19180
  if (!exists) console.warn(`Built-in skill not found in package: ${name}`);
18910
19181
  return false;
18911
19182
  }
18912
- const srcHash = fileHash(path30.join(srcDir, "SKILL.md"));
19183
+ const srcHash = fileHash(path32.join(srcDir, "SKILL.md"));
18913
19184
  if (exists) {
18914
- const currentHash = fileHash(path30.join(dest, "SKILL.md"));
19185
+ const currentHash = fileHash(path32.join(dest, "SKILL.md"));
18915
19186
  if (currentHash !== void 0 && currentHash === srcHash) {
18916
19187
  this.recordSeed(name, currentHash);
18917
19188
  return false;
@@ -18955,7 +19226,7 @@ var SkillsService = class _SkillsService {
18955
19226
  if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
18956
19227
  let raw;
18957
19228
  try {
18958
- raw = readSkillText(path30.join(dir, "SKILL.md"));
19229
+ raw = readSkillText(path32.join(dir, "SKILL.md"));
18959
19230
  } catch {
18960
19231
  return false;
18961
19232
  }
@@ -18963,7 +19234,7 @@ var SkillsService = class _SkillsService {
18963
19234
  }
18964
19235
  pruneRetired() {
18965
19236
  for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
18966
- const dest = path30.join(this.globalDir(), name);
19237
+ const dest = path32.join(this.globalDir(), name);
18967
19238
  if (!fs11.existsSync(dest)) continue;
18968
19239
  if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
18969
19240
  try {
@@ -19101,12 +19372,12 @@ var runGit = execFileWithTimeout(1e4);
19101
19372
  async function committedInHead(files) {
19102
19373
  const byDir = /* @__PURE__ */ new Map();
19103
19374
  for (const file of new Set(files)) {
19104
- const dir = path31.dirname(path31.dirname(file));
19375
+ const dir = path33.dirname(path33.dirname(file));
19105
19376
  byDir.set(dir, [...byDir.get(dir) ?? [], file]);
19106
19377
  }
19107
19378
  const result = /* @__PURE__ */ new Map();
19108
19379
  await Promise.all([...byDir].map(async ([dir, inDir]) => {
19109
- const relative17 = (file) => path31.relative(dir, file).split(path31.sep).join("/");
19380
+ const relative17 = (file) => path33.relative(dir, file).split(path33.sep).join("/");
19110
19381
  try {
19111
19382
  const { stdout } = await runGit("git", ["ls-tree", "--name-only", "HEAD", "--", ...inDir.map(relative17)], { cwd: dir, env: cleanEnv() });
19112
19383
  const listed3 = new Set(stdout.split("\n").map((line) => line.trim()).filter(Boolean));
@@ -19126,9 +19397,9 @@ function plannedSkillLookup(skillsAt, workspaceRoot, layout) {
19126
19397
  searchedDirs: () => lookup2.searchedDirs(),
19127
19398
  listSkills: () => lookup2.listSkills(),
19128
19399
  uncommitted: async (skills) => {
19129
- const checked = skills.filter((skill) => skill.source === "workspace" && !(readInPlace && path31.dirname(path31.dirname(skill.path)) === readInPlace));
19400
+ const checked = skills.filter((skill) => skill.source === "workspace" && !(readInPlace && path33.dirname(path33.dirname(skill.path)) === readInPlace));
19130
19401
  const committed = await committedInHead(checked.map((skill) => skill.path));
19131
- return new Map(checked.filter((skill) => committed.get(skill.path) === false).map((skill) => [skill.path, path31.relative(workspaceRoot, path31.dirname(skill.path)).split(path31.sep).join("/")]));
19402
+ return new Map(checked.filter((skill) => committed.get(skill.path) === false).map((skill) => [skill.path, path33.relative(workspaceRoot, path33.dirname(skill.path)).split(path33.sep).join("/")]));
19132
19403
  }
19133
19404
  };
19134
19405
  }
@@ -19141,13 +19412,13 @@ function checkPlanSkills(tasks, lookup2) {
19141
19412
  function checkTaskSkillsEdit(task2, skills, lookup2) {
19142
19413
  return check([{ taskId: task2.id, owner: `Task "${task2.title}"`, ...attached(skills) }], lookup2);
19143
19414
  }
19144
- function subtaskEntries(op, spec, path46 = []) {
19415
+ function subtaskEntries(op, spec, path49 = []) {
19145
19416
  const subtasks = spec?.subtasks;
19146
19417
  if (!Array.isArray(subtasks)) return [];
19147
19418
  return subtasks.flatMap((sub, i) => {
19148
19419
  if (typeof sub !== "object" || sub === null) return [];
19149
19420
  const child = sub;
19150
- const at = [...path46, typeof child.title === "string" && child.title.trim() ? `"${child.title}"` : `#${i + 1}`];
19421
+ const at = [...path49, typeof child.title === "string" && child.title.trim() ? `"${child.title}"` : `#${i + 1}`];
19151
19422
  return [{ op, owner: `subtask ${at.join(" > ")}`, ...attached(child.skills) }, ...subtaskEntries(op, child, at)];
19152
19423
  });
19153
19424
  }
@@ -22989,7 +23260,7 @@ var RUN_SETTING_KEYS = Object.keys(RUN_SETTING_CHECKS);
22989
23260
  function getSettingsPath() {
22990
23261
  const override = process.env.ORDEWELL_SETTINGS_PATH;
22991
23262
  if (override && override.trim()) return override.trim();
22992
- return path32.join(globalDataDir(), "settings.json");
23263
+ return path34.join(globalDataDir(), "settings.json");
22993
23264
  }
22994
23265
  function readPlannerModels(raw) {
22995
23266
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -23069,8 +23340,8 @@ var SettingsService = class {
23069
23340
  * a watch on the old one pointing at nothing.
23070
23341
  */
23071
23342
  watch(listener) {
23072
- const dir = path32.dirname(this.filePath);
23073
- const name = path32.basename(this.filePath);
23343
+ const dir = path34.dirname(this.filePath);
23344
+ const name = path34.basename(this.filePath);
23074
23345
  let seen = this.fileStamp();
23075
23346
  let timer;
23076
23347
  let dead = false;
@@ -23079,7 +23350,7 @@ var SettingsService = class {
23079
23350
  fs12.mkdirSync(dir, { recursive: true });
23080
23351
  watcher = fs12.watch(dir, (_event, changed) => {
23081
23352
  if (dead) return;
23082
- if (changed && changed !== name && changed !== path32.basename(dir)) return;
23353
+ if (changed && changed !== name && changed !== path34.basename(dir)) return;
23083
23354
  clearTimeout(timer);
23084
23355
  timer = setTimeout(() => {
23085
23356
  const stamp2 = this.fileStamp();
@@ -23312,7 +23583,7 @@ var SettingsService = class {
23312
23583
  if (this.unreadable) {
23313
23584
  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.`);
23314
23585
  }
23315
- const dir = path32.dirname(this.filePath);
23586
+ const dir = path34.dirname(this.filePath);
23316
23587
  if (!fs12.existsSync(dir)) {
23317
23588
  fs12.mkdirSync(dir, { recursive: true });
23318
23589
  }
@@ -23756,7 +24027,7 @@ var BaseConfig = class {
23756
24027
  const { model, effort, override = "" } = configured;
23757
24028
  const named3 = isPlannerBackend(configured.setting ?? "") || isPlannerBackend(process.env.AI_PROVIDER ?? "");
23758
24029
  const pickedElsewhere = configured.pickedFor !== void 0 && configured.pickedFor !== provider;
23759
- const kept = configured.pickedFor === provider || !pickedElsewhere && (isApiProvider(provider) || named3 && !isForeignModel(provider, model, this.agentCatalog(provider)));
24030
+ const kept = configured.pickedFor === provider || !pickedElsewhere && (isApiProvider(provider) || named3 && !isForeignModel(provider, model, () => this.agentCatalog(provider)));
23760
24031
  if (kept) return { model: override || model, effort, source: override ? "request" : "configured" };
23761
24032
  const own = pickedElsewhere || named3 ? rememberedOrDefault(provider) : detectedAgentChoice(provider, configured);
23762
24033
  return !override || override === own.model ? own : { model: override, effort: "", source: "request" };
@@ -23765,7 +24036,8 @@ var BaseConfig = class {
23765
24036
  * The model ids a coding agent was last discovered serving, empty while
23766
24037
  * discovery is cold. A host that holds a runner catalog supplies it: it is
23767
24038
  * the only evidence that tells OpenCode's `provider/model` ids from an
23768
- * OpenRouter slug.
24039
+ * OpenRouter slug. Asked only when a named agent's configured model is
24040
+ * neither its alias nor its remembered pick.
23769
24041
  */
23770
24042
  agentCatalog(_provider) {
23771
24043
  return [];
@@ -23790,8 +24062,10 @@ function isVendorModelFor(agent, model) {
23790
24062
  return !discoveryOf(agent)?.providerQualifiedIds && SLUG_SHAPE.test(model);
23791
24063
  }
23792
24064
  function isForeignModel(agent, model, catalog) {
23793
- if (!model || isStableAlias(agent, model) || catalog.includes(model) || rememberedChoice(agent)?.model === model) return false;
23794
- return isVendorModelFor(agent, model) || catalog.length > 0 && SLUG_SHAPE.test(model);
24065
+ if (!model || isStableAlias(agent, model) || rememberedChoice(agent)?.model === model) return false;
24066
+ const listed3 = catalog();
24067
+ if (listed3.includes(model)) return false;
24068
+ return isVendorModelFor(agent, model) || listed3.length > 0 && SLUG_SHAPE.test(model);
23795
24069
  }
23796
24070
  function rememberedChoice(provider) {
23797
24071
  const entry = new SettingsService().getPlannerModel(provider);
@@ -23826,11 +24100,11 @@ var EnvConfig = class extends BaseConfig {
23826
24100
  // src/services/taskDiffRead.ts
23827
24101
  import { createHash as createHash3 } from "crypto";
23828
24102
  import * as fs13 from "fs";
23829
- import * as path33 from "path";
24103
+ import * as path35 from "path";
23830
24104
  import { devNull } from "os";
23831
24105
  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).";
23832
24106
  function allowedPath(file) {
23833
- return file.length > 0 && !path33.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && !file.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".ordewell");
24107
+ return file.length > 0 && !path35.isAbsolute(file) && !/^[A-Za-z]:/.test(file) && !file.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".ordewell");
23834
24108
  }
23835
24109
  function stats(raw) {
23836
24110
  return raw.split("\0").flatMap((row) => {
@@ -23839,8 +24113,8 @@ function stats(raw) {
23839
24113
  });
23840
24114
  }
23841
24115
  function inside(root, target) {
23842
- const rel = path33.relative(root, target);
23843
- return rel === "" || !rel.startsWith(`..${path33.sep}`) && rel !== ".." && !path33.isAbsolute(rel);
24116
+ const rel = path35.relative(root, target);
24117
+ return rel === "" || !rel.startsWith(`..${path35.sep}`) && rel !== ".." && !path35.isAbsolute(rel);
23844
24118
  }
23845
24119
  function unquoted(quoted4) {
23846
24120
  const bytes = [];
@@ -23888,10 +24162,10 @@ function patchFiles(patch) {
23888
24162
  const sections = patch.toString("utf8").split(/(?<![^\n])(?=diff --git )/).filter((section) => section.startsWith("diff --git "));
23889
24163
  const files = /* @__PURE__ */ new Map();
23890
24164
  for (const section of sections) {
23891
- const path46 = sectionPath(section);
24165
+ const path49 = sectionPath(section);
23892
24166
  const counts = sectionStats(section);
23893
- const earlier = files.get(path46);
23894
- files.set(path46, earlier ? { path: path46, section: earlier.section + section, added: summed(earlier.added, counts.added), removed: summed(earlier.removed, counts.removed) } : { path: path46, section, ...counts });
24167
+ const earlier = files.get(path49);
24168
+ files.set(path49, earlier ? { path: path49, section: earlier.section + section, added: summed(earlier.added, counts.added), removed: summed(earlier.removed, counts.removed) } : { path: path49, section, ...counts });
23895
24169
  }
23896
24170
  return [...files.values()];
23897
24171
  }
@@ -23961,11 +24235,11 @@ async function readTaskDiff(invoker, run, taskId, options) {
23961
24235
  files.push({ ...file, repo: repo.path, patch: () => git(reader, cwd, [...diffArgs, "--", `:(top,literal)${prefix}${file.path}`]) });
23962
24236
  }
23963
24237
  if (live2) {
23964
- const workspace = path33.join(entry.worktree, prefix);
24238
+ const workspace = path35.join(entry.worktree, prefix);
23965
24239
  const others = await git(reader, workspace, [...common, "ls-files", "--others", "--exclude-standard", "-z", "--", "."]);
23966
24240
  for (const file of others.split("\0").filter((f) => allowed(f))) {
23967
- const full = path33.join(workspace, file);
23968
- if (!inside(fs13.realpathSync(workspace), fs13.realpathSync(path33.dirname(full)))) continue;
24241
+ const full = path35.join(workspace, file);
24242
+ if (!inside(fs13.realpathSync(workspace), fs13.realpathSync(path35.dirname(full)))) continue;
23969
24243
  const untracked = async (numstat) => {
23970
24244
  const result = await tryGit(reader, workspace, [...common, "diff", "--no-index", "--no-renames", "--no-ext-diff", "--no-textconv", ...numstat ? ["--numstat", "-z"] : [], "--", devNull, file]);
23971
24245
  if (!result.ok && result.code !== 1) throw new Error(result.stderr.trim() || "Could not read an untracked file diff.");
@@ -23995,11 +24269,11 @@ ${await file.patch()}`);
23995
24269
  import { randomBytes as randomBytes6 } from "crypto";
23996
24270
  import { spawnSync } from "child_process";
23997
24271
  import * as fs15 from "fs";
23998
- import * as path35 from "path";
24272
+ import * as path37 from "path";
23999
24273
 
24000
24274
  // src/utils/prdStore.ts
24001
24275
  import * as fs14 from "fs";
24002
- import * as path34 from "path";
24276
+ import * as path36 from "path";
24003
24277
  var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
24004
24278
  function extractPrdBlock(text4) {
24005
24279
  const match = PRD_BLOCK_RE.exec(text4);
@@ -24013,9 +24287,9 @@ function sanitizeSlug(raw) {
24013
24287
  return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
24014
24288
  }
24015
24289
  function savePrdMarkdown(workspace, slug, markdown) {
24016
- const dir = path34.join(workspace, ".scratch", sanitizeSlug(slug));
24290
+ const dir = path36.join(workspace, ".scratch", sanitizeSlug(slug));
24017
24291
  fs14.mkdirSync(dir, { recursive: true });
24018
- const file = path34.join(dir, "PRD.md");
24292
+ const file = path36.join(dir, "PRD.md");
24019
24293
  fs14.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
24020
24294
  return file;
24021
24295
  }
@@ -24047,12 +24321,12 @@ var NESTED_REPO_DEPTH = 2;
24047
24321
  var GITLINK_MODE = "160000";
24048
24322
  var READ_ONLY = ["--no-optional-locks", "-c", "core.fsmonitor=false"];
24049
24323
  function isInside(parent, child) {
24050
- const rel = path35.relative(parent, child);
24051
- return rel !== "" && !rel.startsWith("..") && !path35.isAbsolute(rel);
24324
+ const rel = path37.relative(parent, child);
24325
+ return rel !== "" && !rel.startsWith("..") && !path37.isAbsolute(rel);
24052
24326
  }
24053
24327
  function resolvesAsNamed(base, p) {
24054
24328
  try {
24055
- return fs15.realpathSync(p) === path35.join(fs15.realpathSync(base), path35.relative(base, p));
24329
+ return fs15.realpathSync(p) === path37.join(fs15.realpathSync(base), path37.relative(base, p));
24056
24330
  } catch {
24057
24331
  return false;
24058
24332
  }
@@ -24107,7 +24381,7 @@ function sameDir(a, b) {
24107
24381
  try {
24108
24382
  return fs15.realpathSync.native(p);
24109
24383
  } catch {
24110
- return path35.resolve(p);
24384
+ return path37.resolve(p);
24111
24385
  }
24112
24386
  };
24113
24387
  return real(a) === real(b);
@@ -24129,7 +24403,7 @@ function clearImmutableFlags(platform, dir) {
24129
24403
  spawnSync("/usr/bin/chflags", ["-R", "nouchg", dir], { stdio: "ignore" });
24130
24404
  }
24131
24405
  function leftoverLinks(dir) {
24132
- const named3 = listDir(dir).filter((name) => LINKED_ARTIFACTS.has(name) || isEnvFile(name) || isLink(path35.join(dir, name)));
24406
+ const named3 = listDir2(dir).filter((name) => LINKED_ARTIFACTS.has(name) || isEnvFile(name) || isLink(path37.join(dir, name)));
24133
24407
  return [.../* @__PURE__ */ new Set([...named3, ...installDirs(dir)])];
24134
24408
  }
24135
24409
  function isLink(target) {
@@ -24202,7 +24476,7 @@ var GitWorktreeIsolation = class {
24202
24476
  const head = await this.headOf(root);
24203
24477
  const repo = { path: repoPath, head };
24204
24478
  if (scan.toplevel !== void 0) {
24205
- const top = path35.relative(workspaceRoot, scan.toplevel).split(path35.sep).join("/");
24479
+ const top = path37.relative(workspaceRoot, scan.toplevel).split(path37.sep).join("/");
24206
24480
  if (top !== "") repo.top = top;
24207
24481
  }
24208
24482
  if (enabled && (head.kind === "branch" || head.kind === "detached")) {
@@ -24259,9 +24533,9 @@ var GitWorktreeIsolation = class {
24259
24533
  async nestedRepos(workspaceRoot, toplevel) {
24260
24534
  const found = [];
24261
24535
  const walk = (rel, depth) => {
24262
- for (const name of childDirs(path35.join(workspaceRoot, rel))) {
24536
+ for (const name of childDirs(path37.join(workspaceRoot, rel))) {
24263
24537
  const child = rel ? `${rel}/${name}` : name;
24264
- if (hasGitEntry(path35.join(workspaceRoot, child))) found.push(child);
24538
+ if (hasGitEntry(path37.join(workspaceRoot, child))) found.push(child);
24265
24539
  else if (depth < NESTED_REPO_DEPTH) walk(child, depth + 1);
24266
24540
  }
24267
24541
  };
@@ -24279,7 +24553,7 @@ var GitWorktreeIsolation = class {
24279
24553
  const [meta, file] = entry.split(" ");
24280
24554
  if (file !== void 0 && meta.startsWith(`${GITLINK_MODE} `)) paths.add(file);
24281
24555
  }
24282
- const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path35.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
24556
+ const declared = await this.tryGit(workspaceRoot, ["config", "-z", "-f", path37.join(toplevel, ".gitmodules"), "--get-regexp", "^submodule\\..*\\.path$"]);
24283
24557
  for (const entry of declared.stdout.split("\0")) {
24284
24558
  const value = entry.split("\n")[1];
24285
24559
  if (value) paths.add(value.replace(/\/+$/, ""));
@@ -24337,7 +24611,7 @@ var GitWorktreeIsolation = class {
24337
24611
  async acceptsWorktree(run, repo) {
24338
24612
  const dir = this.integrationDir(run, repo);
24339
24613
  ensureStateDirIgnored(run.workspaceRoot);
24340
- fs15.mkdirSync(path35.dirname(dir), { recursive: true });
24614
+ fs15.mkdirSync(path37.dirname(dir), { recursive: true });
24341
24615
  const added = await this.tryGit(repo.root, ["worktree", "add", "-q", "--no-checkout", dir, repo.integrationBranch]);
24342
24616
  await this.removeWorktreeDir(run, repo, dir, []);
24343
24617
  return added.ok;
@@ -24352,12 +24626,12 @@ var GitWorktreeIsolation = class {
24352
24626
  if (isolated.includes(SELF_REPO)) return [];
24353
24627
  const shared2 = [];
24354
24628
  const walk = (rel) => {
24355
- for (const name of listDir(path35.join(workspaceRoot, rel))) {
24629
+ for (const name of listDir2(path37.join(workspaceRoot, rel))) {
24356
24630
  if (!rel && (name === STATE_DIR || name === ".git")) continue;
24357
24631
  const child = rel ? `${rel}/${name}` : name;
24358
24632
  if (isolated.includes(child)) continue;
24359
24633
  if (isolated.some((p) => p.startsWith(`${child}/`))) walk(child);
24360
- else if (resolves(path35.join(workspaceRoot, child))) shared2.push(child);
24634
+ else if (resolves(path37.join(workspaceRoot, child))) shared2.push(child);
24361
24635
  }
24362
24636
  };
24363
24637
  walk("");
@@ -24397,9 +24671,9 @@ var GitWorktreeIsolation = class {
24397
24671
  try {
24398
24672
  ensureStateDirIgnored(run.workspaceRoot);
24399
24673
  for (const repo of run.repos) {
24400
- const worktree = path35.join(dir, repo.path);
24674
+ const worktree = path37.join(dir, repo.path);
24401
24675
  const tip = (await this.git(repo.root, ["rev-parse", repo.integrationBranch])).trim();
24402
- fs15.mkdirSync(path35.dirname(worktree), { recursive: true });
24676
+ fs15.mkdirSync(path37.dirname(worktree), { recursive: true });
24403
24677
  await this.git(repo.root, ["worktree", "add", "-q", "-B", branch, worktree, tip]);
24404
24678
  record.repos[repo.path] = { worktree, linked: [], baseRef: tip };
24405
24679
  }
@@ -24424,13 +24698,13 @@ var GitWorktreeIsolation = class {
24424
24698
  setupFailed.push({ repo: repo.path, detail: err.detail });
24425
24699
  }
24426
24700
  entry.linked = boot.linked;
24427
- copied.push(...boot.copied.map((name) => path35.posix.join(repo.path, name)));
24701
+ copied.push(...boot.copied.map((name) => path37.posix.join(repo.path, name)));
24428
24702
  }
24429
24703
  for (const rel of run.shared) {
24430
- const source = path35.join(run.workspaceRoot, rel);
24431
- const target = path35.join(cwd, rel);
24704
+ const source = path37.join(run.workspaceRoot, rel);
24705
+ const target = path37.join(cwd, rel);
24432
24706
  if (!resolves(source) || lexists(target)) continue;
24433
- fs15.mkdirSync(path35.dirname(target), { recursive: true });
24707
+ fs15.mkdirSync(path37.dirname(target), { recursive: true });
24434
24708
  if (linkPath(source, target, this.platform) === "copy") copied.push(rel);
24435
24709
  const entry = self ? record.repos[SELF_REPO] : void 0;
24436
24710
  if (entry) entry.linked.push(rel);
@@ -24462,7 +24736,7 @@ var GitWorktreeIsolation = class {
24462
24736
  */
24463
24737
  async clearLeftovers(run, task2, names) {
24464
24738
  const removal = noRemoval();
24465
- const holder = Object.values(run.tasks).find((r) => r.taskId !== task2.id && (r.branch === names.branch || path35.resolve(r.workspace) === path35.resolve(names.dir) || isInside(r.workspace, names.dir)));
24739
+ const holder = Object.values(run.tasks).find((r) => r.taskId !== task2.id && (r.branch === names.branch || path37.resolve(r.workspace) === path37.resolve(names.dir) || isInside(r.workspace, names.dir)));
24466
24740
  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`);
24467
24741
  const found = [];
24468
24742
  for (const repo of run.repos) {
@@ -24472,14 +24746,14 @@ var GitWorktreeIsolation = class {
24472
24746
  worktree: /^worktree (.+)$/m.exec(block)?.[1],
24473
24747
  branch: /^branch refs\/heads\/(.+)$/m.exec(block)?.[1]
24474
24748
  }));
24475
- const at = /* @__PURE__ */ new Set([path35.join(names.dir, repo.path)]);
24749
+ const at = /* @__PURE__ */ new Set([path37.join(names.dir, repo.path)]);
24476
24750
  for (const entry of registered) {
24477
- if (entry.worktree && entry.branch === names.branch && isInside(this.runRoot(run), path35.resolve(entry.worktree))) at.add(entry.worktree);
24751
+ if (entry.worktree && entry.branch === names.branch && isInside(this.runRoot(run), path37.resolve(entry.worktree))) at.add(entry.worktree);
24478
24752
  }
24479
- const base = { branch: names.branch, name: path35.basename(names.dir), title: task2.title };
24753
+ const base = { branch: names.branch, name: path37.basename(names.dir), title: task2.title };
24480
24754
  for (const worktree of at) {
24481
24755
  if (!fs15.existsSync(worktree)) continue;
24482
- const linked = leftoverLinks(path35.join(worktree, await this.prefixOf(repo)));
24756
+ const linked = leftoverLinks(path37.join(worktree, await this.prefixOf(repo)));
24483
24757
  found.push({ repo, worktree, target: { ...base, worktree, linked } });
24484
24758
  }
24485
24759
  found.push({ repo, worktree: names.dir, target: { ...base, linked: [] } });
@@ -24572,7 +24846,7 @@ var GitWorktreeIsolation = class {
24572
24846
  */
24573
24847
  async inWorkspacePlace(repo, worktree) {
24574
24848
  const prefix = await this.prefixOf(repo);
24575
- return prefix ? path35.join(worktree, prefix) : worktree;
24849
+ return prefix ? path37.join(worktree, prefix) : worktree;
24576
24850
  }
24577
24851
  async prefixOf(repo) {
24578
24852
  return (await this.git(repo.root, ["rev-parse", "--show-prefix"])).trim();
@@ -25102,15 +25376,15 @@ var GitWorktreeIsolation = class {
25102
25376
  return landingMessage(kind, from, this.deps.plannedWith?.() ?? false);
25103
25377
  }
25104
25378
  integrationDir(run, repo) {
25105
- return path35.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
25379
+ return path37.join(this.runRoot(run), INTEGRATION_DIR, repo.path);
25106
25380
  }
25107
25381
  async ensureIntegrationWorktree(run, repo) {
25108
25382
  return this.admin(run.workspaceRoot, async () => {
25109
25383
  const dir = this.integrationDir(run, repo);
25110
- if (fs15.existsSync(path35.join(dir, ".git"))) return dir;
25384
+ if (fs15.existsSync(path37.join(dir, ".git"))) return dir;
25111
25385
  await this.tryGit(repo.root, ["worktree", "prune"]);
25112
25386
  ensureStateDirIgnored(run.workspaceRoot);
25113
- fs15.mkdirSync(path35.dirname(dir), { recursive: true });
25387
+ fs15.mkdirSync(path37.dirname(dir), { recursive: true });
25114
25388
  await this.git(repo.root, ["worktree", "add", "-q", dir, repo.integrationBranch]);
25115
25389
  return dir;
25116
25390
  });
@@ -25133,7 +25407,7 @@ var GitWorktreeIsolation = class {
25133
25407
  const removal = noRemoval();
25134
25408
  for (const repo of opts.preserve === false || record.readOnly ? [] : run.repos) {
25135
25409
  const entry = record.repos[repo.path];
25136
- const target = { worktree: entry?.worktree, linked: entry?.linked ?? [], branch: record.branch, name: path35.basename(record.workspace), title: record.title, commit: record.commit };
25410
+ const target = { worktree: entry?.worktree, linked: entry?.linked ?? [], branch: record.branch, name: path37.basename(record.workspace), title: record.title, commit: record.commit };
25137
25411
  try {
25138
25412
  const kept = await this.preserve(run, repo, target, opts.intent);
25139
25413
  if (kept) removal.preserved.push({ task: task2, ...kept });
@@ -25214,7 +25488,7 @@ var GitWorktreeIsolation = class {
25214
25488
  return;
25215
25489
  }
25216
25490
  for (const entry of entries) {
25217
- const target = path35.join(dir, entry.name);
25491
+ const target = path37.join(dir, entry.name);
25218
25492
  if (entry.isSymbolicLink()) this.unlinkIfLink(target);
25219
25493
  else if (entry.isDirectory()) this.unlinkLinksUnder(target);
25220
25494
  }
@@ -25236,20 +25510,20 @@ var GitWorktreeIsolation = class {
25236
25510
  return false;
25237
25511
  }
25238
25512
  };
25239
- const owned = new Set(Object.values(run.tasks).map((r) => path35.resolve(r.workspace)));
25513
+ const owned = new Set(Object.values(run.tasks).map((r) => path37.resolve(r.workspace)));
25240
25514
  const root = this.runRoot(run);
25241
25515
  if (fs15.existsSync(root)) {
25242
25516
  for (const entry of fs15.readdirSync(root)) {
25243
- const dir = path35.join(root, entry);
25244
- if (entry === INTEGRATION_DIR || owned.has(path35.resolve(dir))) continue;
25517
+ const dir = path37.join(root, entry);
25518
+ if (entry === INTEGRATION_DIR || owned.has(path37.resolve(dir))) continue;
25245
25519
  let kept = true;
25246
25520
  for (const repo of run.repos) {
25247
- const worktree = path35.join(dir, repo.path);
25248
- const linked = leftoverLinks(path35.join(worktree, await this.prefixOf(repo)));
25521
+ const worktree = path37.join(dir, repo.path);
25522
+ const linked = leftoverLinks(path37.join(worktree, await this.prefixOf(repo)));
25249
25523
  if (!await keep2(repo, { worktree, linked, branch: null, name: entry, title: entry }, worktree)) kept = false;
25250
25524
  }
25251
25525
  if (!kept) continue;
25252
- for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path35.join(dir, repo.path), []);
25526
+ for (const repo of run.repos) await this.removeWorktreeDir(run, repo, path37.join(dir, repo.path), []);
25253
25527
  this.removeTaskWorkspace(run, dir);
25254
25528
  }
25255
25529
  }
@@ -25267,13 +25541,13 @@ var GitWorktreeIsolation = class {
25267
25541
  }
25268
25542
  async removeWorktreeDir(run, repo, dir, linked) {
25269
25543
  for (const name of /* @__PURE__ */ new Set([...linked, ...installDirs(dir)])) {
25270
- const target = path35.join(dir, name);
25271
- if (!resolvesAsNamed(this.runRoot(run), path35.dirname(target))) continue;
25544
+ const target = path37.join(dir, name);
25545
+ if (!resolvesAsNamed(this.runRoot(run), path37.dirname(target))) continue;
25272
25546
  this.unlinkIfLink(target);
25273
25547
  this.unlinkLinksUnder(target);
25274
25548
  }
25275
25549
  for (const name of resolvesAsNamed(this.runRoot(run), dir) ? fs15.readdirSync(dir) : []) {
25276
- if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path35.join(dir, name));
25550
+ if (LINKED_ARTIFACTS.has(name) || isEnvFile(name)) this.unlinkIfLink(path37.join(dir, name));
25277
25551
  }
25278
25552
  await this.tryGit(repo.root, ["worktree", "remove", "--force", dir]);
25279
25553
  if (fs15.existsSync(dir) && isInside(this.runRoot(run), dir)) {
@@ -25281,7 +25555,7 @@ var GitWorktreeIsolation = class {
25281
25555
  fs15.rmSync(dir, { recursive: true, force: true });
25282
25556
  }
25283
25557
  await this.tryGit(repo.root, ["worktree", "prune"]);
25284
- for (let parent = path35.dirname(dir); isInside(this.runRoot(run), parent); parent = path35.dirname(parent)) this.removeIfEmpty(parent);
25558
+ for (let parent = path37.dirname(dir); isInside(this.runRoot(run), parent); parent = path37.dirname(parent)) this.removeIfEmpty(parent);
25285
25559
  }
25286
25560
  unlinkIfLink(target) {
25287
25561
  try {
@@ -25296,14 +25570,14 @@ var GitWorktreeIsolation = class {
25296
25570
  }
25297
25571
  }
25298
25572
  runRoot(run) {
25299
- return path35.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
25573
+ return path37.join(run.workspaceRoot, STATE_DIR, "worktrees", run.id);
25300
25574
  }
25301
25575
  namesFor(run, task2) {
25302
25576
  const slug = sanitizeSlug(task2.title).slice(0, 40).replace(/-+$/, "") || "task";
25303
25577
  let name = `${task2.order}-${slug}`;
25304
25578
  const clash = Object.values(run.tasks).some((r) => r.taskId !== task2.id && r.branch === `ordewell/${run.id}/${name}`);
25305
25579
  if (clash) name = `${name}-${sanitizeSlug(task2.id) || "x"}`;
25306
- return { branch: `ordewell/${run.id}/${name}`, dir: path35.join(this.runRoot(run), name) };
25580
+ return { branch: `ordewell/${run.id}/${name}`, dir: path37.join(this.runRoot(run), name) };
25307
25581
  }
25308
25582
  async unmergedPaths(cwd) {
25309
25583
  const listed3 = await this.tryGit(cwd, ["diff", "--name-only", "-z", "--diff-filter=U"]);
@@ -25317,7 +25591,7 @@ var GitWorktreeIsolation = class {
25317
25591
  async rebaseInProgress(cwd) {
25318
25592
  for (const dir of ["rebase-merge", "rebase-apply"]) {
25319
25593
  const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
25320
- if (found.ok && fs15.existsSync(path35.resolve(cwd, found.stdout.trim()))) return true;
25594
+ if (found.ok && fs15.existsSync(path37.resolve(cwd, found.stdout.trim()))) return true;
25321
25595
  }
25322
25596
  return false;
25323
25597
  }
@@ -25344,6 +25618,29 @@ function createWorktreeIsolation(deps) {
25344
25618
  return new GitWorktreeIsolation(deps);
25345
25619
  }
25346
25620
 
25621
+ // src/services/repositoryRunnerConfig.ts
25622
+ var RUNNER_CONFIG_ROOTS = [".mcp.json", ".claude", ".claude-plugin", "opencode.json", "opencode.jsonc", ".opencode", ".codex"];
25623
+ function repositoryRunnerConfigPaths(paths) {
25624
+ return [...new Set(paths.filter(
25625
+ (path49) => (
25626
+ // Project MCP servers start commands or send data to a repository-chosen URL.
25627
+ path49 === ".mcp.json" || // Settings can run hooks and helpers, load plugins, or redirect traffic.
25628
+ path49 === ".claude/settings.json" || // Local settings have the same execution capabilities as project settings.
25629
+ path49 === ".claude/settings.local.json" || // Hook files provide commands invoked by the runner.
25630
+ path49.startsWith(".claude/hooks/") || // Plugin files can provide code and hooks loaded by the runner.
25631
+ path49.startsWith(".claude/plugins/") || // Plugin metadata can load plugins and their executable components.
25632
+ path49.startsWith(".claude-plugin/") || // Plugin and marketplace manifests can select code for the runner to load.
25633
+ /^\.claude\/(?:[^/]+\/)*(?:plugin|marketplace)\.json$/.test(path49) || // OpenCode config declares MCP servers and plugins; its plugin and tool
25634
+ // directories are code it loads, and an agent definition sets the prompt,
25635
+ // model and permissions of the built-in agent its id names, which a task
25636
+ // launches with. The rest of `.opencode/` is dependencies OpenCode
25637
+ // installs itself, which a later launch must not be held for.
25638
+ /^(?:\.opencode\/)?opencode\.jsonc?$/.test(path49) || /^\.opencode\/(?:plugins?|tools?|agents?)\//.test(path49) || // Codex project config carries MCP servers, hooks and `notify` commands.
25639
+ path49.startsWith(".codex/")
25640
+ )
25641
+ ))].sort();
25642
+ }
25643
+
25347
25644
  // src/interfaces/ILogger.ts
25348
25645
  var ConsoleLogger = class {
25349
25646
  warn(scope, message, err) {
@@ -28379,6 +28676,7 @@ var IsolationRunController = class {
28379
28676
  /** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
28380
28677
  resolvers = {};
28381
28678
  opening = null;
28679
+ opened = 0;
28382
28680
  /** Bumped by an interrupt, so an activation in flight across it does not open the run it ended. */
28383
28681
  activations = 0;
28384
28682
  /** The start a dirty tree turned away, handed back once the user chooses how to go on. */
@@ -28410,6 +28708,10 @@ var IsolationRunController = class {
28410
28708
  get isOpen() {
28411
28709
  return this.mode !== null;
28412
28710
  }
28711
+ /** How many runs have opened: what tells one run's launches from the next one's. */
28712
+ get epoch() {
28713
+ return this.opened;
28714
+ }
28413
28715
  /** The open run has decided whether it isolates. */
28414
28716
  get decided() {
28415
28717
  return this.mode === "isolated" || this.mode === "shared";
@@ -28507,6 +28809,7 @@ var IsolationRunController = class {
28507
28809
  open() {
28508
28810
  if (this.mode) return;
28509
28811
  this.mode = "undecided";
28812
+ this.opened += 1;
28510
28813
  }
28511
28814
  /**
28512
28815
  * Decide once per run, as its first change task starts, whether it executes
@@ -29147,15 +29450,21 @@ var Landing = class {
29147
29450
  * has landed. Its branch is on the integration branch by then, so it merges
29148
29451
  * clean — and one the resolver did not really bring along conflicts again
29149
29452
  * instead of being taken at its word. Null when there is nothing to land.
29453
+ * The task is named before its landing settles, so the caller can hold
29454
+ * its worktree for as long as the merge is in it; one the caller says is
29455
+ * `leaving` the plan is not landed at all.
29150
29456
  */
29151
- async landResolved(resolverId) {
29457
+ landResolved(resolverId, leaving = () => false) {
29152
29458
  const conflictedId = this.runs.takeResolver(resolverId);
29153
- if (!conflictedId) return null;
29459
+ if (!conflictedId || leaving(conflictedId)) return null;
29154
29460
  const task2 = this.tasks.get(conflictedId);
29155
29461
  if (!task2 || this.runs.current?.tasks[conflictedId]?.status !== "conflict") return null;
29462
+ return { task: task2, outcome: this.landConflicted(task2) };
29463
+ }
29464
+ async landConflicted(task2) {
29156
29465
  const landing2 = await this.runs.integrate(task2);
29157
- if (landing2 !== "merged") return { task: task2, outcome: this.unmerged(task2, landing2) };
29158
- return { task: task2, outcome: { kind: "landed", messages: [{ level: "info", text: `Task "${task2.title}" landed through its conflict resolution.` }] } };
29466
+ if (landing2 !== "merged") return this.unmerged(task2, landing2);
29467
+ return { kind: "landed", messages: [{ level: "info", text: `Task "${task2.title}" landed through its conflict resolution.` }] };
29159
29468
  }
29160
29469
  /** 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. */
29161
29470
  nextRepair(taskId) {
@@ -29500,7 +29809,7 @@ function heldFrom(state) {
29500
29809
  }
29501
29810
 
29502
29811
  // src/services/Attempts.ts
29503
- import * as path38 from "path";
29812
+ import * as path41 from "path";
29504
29813
 
29505
29814
  // src/services/VerdictEngine.ts
29506
29815
  var IDLE_TIMEOUT_MS = 10 * 6e4;
@@ -29608,6 +29917,18 @@ var VerdictEngine = class {
29608
29917
  this.resumeIdle(taskId);
29609
29918
  this.toolCheckpoints.get(taskId)?.({ kind: "rejected", reason });
29610
29919
  }
29920
+ /**
29921
+ * An attempt's runner is being started, with no session to watch yet. Its
29922
+ * silence counts from here, so one whose start never resolves — stuck
29923
+ * launching, or waiting for its tools to attach — is flagged quiet as well.
29924
+ * {@link watch} starts the count again once the runner is up.
29925
+ */
29926
+ starting(taskId) {
29927
+ const gen = this.bumpGeneration(taskId);
29928
+ this.idlePaused.delete(taskId);
29929
+ this.openApprovals.delete(taskId);
29930
+ this.touchIdle(taskId, gen);
29931
+ }
29611
29932
  /**
29612
29933
  * Attach to a spawned session. The runner's `task_complete` call on the
29613
29934
  * attempt's token is the only completion evidence (ADR-0022, V2); a session
@@ -29618,6 +29939,9 @@ var VerdictEngine = class {
29618
29939
  */
29619
29940
  watch(task2, session) {
29620
29941
  const gen = this.bumpGeneration(task2.id);
29942
+ this.idlePaused.delete(task2.id);
29943
+ this.openApprovals.delete(task2.id);
29944
+ this.touchIdle(task2.id, gen);
29621
29945
  session.onOutput(() => {
29622
29946
  if (this.generations.get(task2.id) === gen) this.touchIdle(task2.id, gen);
29623
29947
  });
@@ -29873,10 +30197,132 @@ function reportedVerdict(report) {
29873
30197
  return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt, reported };
29874
30198
  }
29875
30199
 
30200
+ // src/services/taskStartConfig.ts
30201
+ import * as path38 from "path";
30202
+ import { createHash as createHash4 } from "crypto";
30203
+ import { existsSync as existsSync15, lstatSync as lstatSync4, readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync12 } from "fs";
30204
+ var MAX_FILES = 2e3;
30205
+ var MAX_DEPTH = 8;
30206
+ function liveConfig(dir) {
30207
+ const found = /* @__PURE__ */ new Map();
30208
+ const visit = (rel, depth) => {
30209
+ if (found.size >= MAX_FILES || depth > MAX_DEPTH) return;
30210
+ const full = path38.join(dir, rel);
30211
+ try {
30212
+ const info = statSync12(full);
30213
+ if (info.isFile()) {
30214
+ found.set(rel, createHash4("sha1").update(readFileSync13(full)).digest("hex"));
30215
+ } else if (info.isDirectory() && path38.basename(rel) !== "node_modules") {
30216
+ for (const name of readdirSync8(full)) visit(path38.posix.join(rel, name), depth + 1);
30217
+ }
30218
+ } catch {
30219
+ }
30220
+ };
30221
+ for (const root of RUNNER_CONFIG_ROOTS) visit(root, 0);
30222
+ const selected = new Set(repositoryRunnerConfigPaths([...found.keys()]));
30223
+ return new Map([...found].filter(([name]) => selected.has(name)));
30224
+ }
30225
+ function linkedRoots(dir) {
30226
+ const linked = /* @__PURE__ */ new Set();
30227
+ for (const root of RUNNER_CONFIG_ROOTS) {
30228
+ try {
30229
+ if (lstatSync4(path38.join(dir, root)).isSymbolicLink()) linked.add(root);
30230
+ } catch {
30231
+ }
30232
+ }
30233
+ return linked;
30234
+ }
30235
+ function readDirs(root) {
30236
+ const dirs = [{ dir: root, prefix: "" }];
30237
+ for (let dir = root, prefix = ""; !existsSync15(path38.join(dir, ".git")); ) {
30238
+ const parent = path38.dirname(dir);
30239
+ if (parent === dir) return [dirs[0]];
30240
+ dir = parent;
30241
+ prefix += "../";
30242
+ dirs.push({ dir, prefix });
30243
+ }
30244
+ return dirs;
30245
+ }
30246
+ var TaskStartConfig = class {
30247
+ live = /* @__PURE__ */ new Map();
30248
+ /**
30249
+ * `epoch` names the run the check belongs to; a new run takes a new
30250
+ * baseline. `root` is the workspace root, the checkout a worktree run lands
30251
+ * in; `cwd` is where the runner starts, and `worktree` whether that is a
30252
+ * worktree prepared for it.
30253
+ */
30254
+ async check(run, taskId, where) {
30255
+ const found = where.worktree ? await this.gitDifferences(run, taskId) : [];
30256
+ const id2 = String(where.epoch);
30257
+ const checkout = readDirs(where.root).flatMap(({ dir, prefix }) => this.liveDifferences(`${id2}:checkout:${dir}`, dir).map((name) => `${prefix}${name}`));
30258
+ if (!where.worktree) found.push(...checkout);
30259
+ else {
30260
+ const cwd = where.cwd ?? run?.tasks[taskId]?.workspace;
30261
+ if (cwd) found.push(...this.worktreeDifferences(id2, cwd, where.root));
30262
+ }
30263
+ return [...new Set(found)].sort();
30264
+ }
30265
+ /** The baselines go with the plan they belong to. */
30266
+ clear() {
30267
+ this.live.clear();
30268
+ }
30269
+ /**
30270
+ * What a worktree's runner reads that differs from where the run started.
30271
+ * The `.claude` and `.opencode` directories it links from the checkout are
30272
+ * the checkout's own, so they are compared with the checkout's first state,
30273
+ * which an earlier task's write there does not move. Anything else is the
30274
+ * worktree's, and is compared with its own first state: a setup command's
30275
+ * files are trusted, and a task's later write is not.
30276
+ */
30277
+ worktreeDifferences(id2, dir, root) {
30278
+ const now = liveConfig(dir);
30279
+ const linked = linkedRoots(dir);
30280
+ const fromCheckout = (name) => linked.has(name.split("/")[0]);
30281
+ const key = `${id2}:worktree:${dir}`;
30282
+ let local = this.live.get(key);
30283
+ if (!local) {
30284
+ local = new Map([...now].filter(([name]) => !fromCheckout(name)));
30285
+ this.live.set(key, local);
30286
+ }
30287
+ const checkout = this.live.get(`${id2}:checkout:${root}`);
30288
+ return [...now].filter(([name, hash]) => fromCheckout(name) ? checkout?.get(name) !== hash : local.get(name) !== hash).map(([name]) => name);
30289
+ }
30290
+ liveDifferences(key, dir) {
30291
+ const now = liveConfig(dir);
30292
+ const before = this.live.get(key);
30293
+ if (!before) {
30294
+ this.live.set(key, now);
30295
+ return [];
30296
+ }
30297
+ return [...now].filter(([name, hash]) => before.get(name) !== hash).map(([name]) => name);
30298
+ }
30299
+ async gitDifferences(run, taskId) {
30300
+ if (!run) return [];
30301
+ const invoker = { exec: defaultExecFile2, resolvePath: augmentedPath };
30302
+ const found = [];
30303
+ for (const repo of run.repos) {
30304
+ const cwd = run.tasks[taskId]?.repos[repo.path]?.worktree;
30305
+ if (!cwd || !repo.baseRef || !existsSync15(cwd)) continue;
30306
+ const prefix = repo.path === "." ? (await tryGit(invoker, repo.root, ["rev-parse", "--show-prefix"])).stdout.trim() : "";
30307
+ const roots = [...RUNNER_CONFIG_ROOTS, ...prefix ? RUNNER_CONFIG_ROOTS.map((r) => `${prefix}${r}`) : []];
30308
+ const diff = await tryGit(invoker, cwd, ["diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-only", "--diff-filter=ACMRTU", "-z", repo.baseRef, "--"]);
30309
+ const untracked = await tryGit(invoker, cwd, ["ls-files", "--others", "-z", "--", ...roots]);
30310
+ if (!diff.ok || !untracked.ok) continue;
30311
+ for (const name of [...diff.stdout.split("\0"), ...untracked.stdout.split("\0")]) {
30312
+ const here = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : void 0;
30313
+ if (repositoryRunnerConfigPaths([name]).length || here !== void 0 && repositoryRunnerConfigPaths([here]).length) {
30314
+ found.push(repo.path === "." ? name : path38.posix.join(repo.path, name));
30315
+ }
30316
+ }
30317
+ }
30318
+ return found;
30319
+ }
30320
+ };
30321
+
29876
30322
  // src/services/ScriptRunner.ts
29877
30323
  import { spawn as nodeSpawn2 } from "child_process";
29878
30324
  import * as fs16 from "fs";
29879
- import * as path36 from "path";
30325
+ import * as path39 from "path";
29880
30326
  import { EventEmitter as EventEmitter2 } from "events";
29881
30327
  import { StringDecoder as StringDecoder2 } from "string_decoder";
29882
30328
 
@@ -30017,7 +30463,7 @@ var ScriptSession = class extends AbstractRunnerSession {
30017
30463
  async launchOf(step) {
30018
30464
  if ("argv" in step) {
30019
30465
  const [named3, ...args] = step.argv;
30020
- const program = this.setup.platform === "win32" && path36.win32.basename(named3) !== named3 && !path36.win32.isAbsolute(named3) ? path36.win32.resolve(this.setup.cwd, named3) : named3;
30466
+ const program = this.setup.platform === "win32" && path39.win32.basename(named3) !== named3 && !path39.win32.isAbsolute(named3) ? path39.win32.resolve(this.setup.cwd, named3) : named3;
30021
30467
  const plan = await planDirectLaunch(program, args, this.setup.launch);
30022
30468
  return { file: plan.file, args: plan.args, shell: false, ...plan.verbatim ? { verbatim: true } : {} };
30023
30469
  }
@@ -30250,10 +30696,10 @@ function passed(status) {
30250
30696
  }
30251
30697
 
30252
30698
  // src/services/PatchCapture.ts
30253
- import { createHash as createHash4, randomBytes as randomBytes7 } from "crypto";
30699
+ import { createHash as createHash5, randomBytes as randomBytes7 } from "crypto";
30254
30700
  import * as fs17 from "fs";
30255
- import * as os11 from "os";
30256
- import * as path37 from "path";
30701
+ import * as os10 from "os";
30702
+ import * as path40 from "path";
30257
30703
 
30258
30704
  // src/services/readTask.ts
30259
30705
  function readTask2(args, readerId, plan, log) {
@@ -30520,7 +30966,7 @@ function linkInPatch(patch) {
30520
30966
  function readOwn(scratch, name, max) {
30521
30967
  let fd;
30522
30968
  try {
30523
- fd = fs17.openSync(path37.join(scratch, name), fs17.constants.O_RDONLY | (fs17.constants.O_NOFOLLOW ?? 0));
30969
+ fd = fs17.openSync(path40.join(scratch, name), fs17.constants.O_RDONLY | (fs17.constants.O_NOFOLLOW ?? 0));
30524
30970
  } catch (err) {
30525
30971
  throw new Refusal(`the capture left no ${name.split(".").pop()} Ordewell could read (${err.code ?? String(err)})`);
30526
30972
  }
@@ -30558,7 +31004,7 @@ async function captureHeld(runs, opts, signal) {
30558
31004
  const [worktree] = runs.worktrees;
30559
31005
  const scratch = runs.scratch;
30560
31006
  const tag = `ordewell-patch-${randomBytes7(8).toString("hex")}`;
30561
- const stem = path37.join(scratch, tag);
31007
+ const stem = path40.join(scratch, tag);
30562
31008
  const own = (suffix, max = LISTING_BYTES) => readOwn(scratch, `${tag}.${suffix}`, max);
30563
31009
  try {
30564
31010
  const stage = await runs.run(stageScript(stem, opts.linked ?? []), worktree);
@@ -30596,7 +31042,7 @@ async function captureHeld(runs, opts, signal) {
30596
31042
  return {
30597
31043
  kind: "saved",
30598
31044
  patch,
30599
- stats: { base, ...countedIn(patch), bytes: patch.length, sha256: createHash4("sha256").update(patch).digest("hex"), flags: [...flags, ...flag("normalized", normalized2)] }
31045
+ stats: { base, ...countedIn(patch), bytes: patch.length, sha256: createHash5("sha256").update(patch).digest("hex"), flags: [...flags, ...flag("normalized", normalized2)] }
30600
31046
  };
30601
31047
  } catch (err) {
30602
31048
  if (err instanceof Refusal) return { kind: "refused", reason: `${err.message}.` };
@@ -30650,8 +31096,8 @@ async function indexEntries(invoker, worktree) {
30650
31096
  return entries;
30651
31097
  }
30652
31098
  async function trialApply(invoker, worktree, patchFile, scratch) {
30653
- const index = path37.resolve(worktree, (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--git-path", "index"])).trim());
30654
- const copy = path37.join(scratch, "index");
31099
+ const index = path40.resolve(worktree, (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--git-path", "index"])).trim());
31100
+ const copy = path40.join(scratch, "index");
30655
31101
  fs17.copyFileSync(index, copy);
30656
31102
  const trial = { ...invoker, exec: (file, args, opts) => invoker.exec(file, args, { ...opts, env: { ...opts.env, GIT_INDEX_FILE: copy } }) };
30657
31103
  const tried = await tryGit(trial, worktree, [...APPLY_GIT, "apply", "--cached", "--3way", "--whitespace=nowarn", patchFile]);
@@ -30671,7 +31117,7 @@ function gitRefused(lead, stderr) {
30671
31117
  function onDisk3(worktree, names) {
30672
31118
  return names.map((name) => {
30673
31119
  try {
30674
- const stat = fs17.lstatSync(path37.join(worktree, name), { bigint: true, throwIfNoEntry: false });
31120
+ const stat = fs17.lstatSync(path40.join(worktree, name), { bigint: true, throwIfNoEntry: false });
30675
31121
  return stat ? `${stat.mode} ${stat.ino} ${stat.size} ${stat.mtimeNs} ${stat.ctimeNs}` : "none";
30676
31122
  } catch (err) {
30677
31123
  return err.code ?? "unreadable";
@@ -30711,7 +31157,7 @@ function leavesWorktree(worktree, names, linked, held2, left) {
30711
31157
  if (i < parts.length && held2.get(at)?.some((entry) => entry.mode === SUBMODULE)) {
30712
31158
  return `the patch writes ${name}, under ${at}, a submodule, whose files are another repository's.`;
30713
31159
  }
30714
- const full = path37.join(worktree, ...parts.slice(0, i));
31160
+ const full = path40.join(worktree, ...parts.slice(0, i));
30715
31161
  const stat = fs17.lstatSync(full, { throwIfNoEntry: false });
30716
31162
  if (!stat) break;
30717
31163
  const last = i === parts.length;
@@ -30732,14 +31178,14 @@ async function applyPatch(invoker, worktree, patch, saved, opts = {}) {
30732
31178
  if (patch.length !== saved.bytes) {
30733
31179
  return refused3(`the patch is ${byteSize(patch.length)} where its artifact says ${byteSize(saved.bytes)}, so it was cut short or changed after it was saved.`);
30734
31180
  }
30735
- if (createHash4("sha256").update(patch).digest("hex") !== saved.sha256) {
31181
+ if (createHash5("sha256").update(patch).digest("hex") !== saved.sha256) {
30736
31182
  return refused3("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.");
30737
31183
  }
30738
31184
  const link = linkInPatch(patch);
30739
31185
  if (link !== void 0) {
30740
31186
  return refused3(`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.`);
30741
31187
  }
30742
- const dir = fs17.mkdtempSync(path37.join(os11.tmpdir(), "ordewell-apply-"));
31188
+ const dir = fs17.mkdtempSync(path40.join(os10.tmpdir(), "ordewell-apply-"));
30743
31189
  try {
30744
31190
  const toplevel = (await git(invoker, worktree, [...APPLY_GIT, "rev-parse", "--show-toplevel"])).trim();
30745
31191
  if (fs17.realpathSync(toplevel) !== fs17.realpathSync(worktree)) return refused3(`${worktree} is not the root of a worktree of its own.`);
@@ -30749,7 +31195,7 @@ async function applyPatch(invoker, worktree, patch, saved, opts = {}) {
30749
31195
  }
30750
31196
  const before = await unmergedPaths(invoker, worktree);
30751
31197
  if (before.length > 0) return refused3(`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.`);
30752
- const file = path37.join(dir, "saved.patch");
31198
+ const file = path40.join(dir, "saved.patch");
30753
31199
  fs17.writeFileSync(file, patch, { flag: "wx", mode: 384 });
30754
31200
  const held2 = await indexEntries(invoker, worktree);
30755
31201
  const named3 = await pathsNamed(invoker, worktree, file);
@@ -30899,6 +31345,11 @@ var TaskWebReader = class {
30899
31345
  };
30900
31346
 
30901
31347
  // src/services/Attempts.ts
31348
+ var TaskStartConfigError = class extends Error {
31349
+ constructor(paths) {
31350
+ 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`);
31351
+ }
31352
+ };
30902
31353
  var DEFAULT_GIT = { exec: defaultExecFile2, resolvePath: () => augmentedPath() };
30903
31354
  var Attempts = class {
30904
31355
  constructor(deps) {
@@ -30930,6 +31381,7 @@ var Attempts = class {
30930
31381
  live = /* @__PURE__ */ new Map();
30931
31382
  /** Bumped by every plan load: an ended attempt's wind-down belongs to the scope before it. */
30932
31383
  planScope = 0;
31384
+ startConfig = new TaskStartConfig();
30933
31385
  /** Each task's runners still winding down once stopped: a continue waits for them (ADR-0018, L1). */
30934
31386
  windDowns = /* @__PURE__ */ new Map();
30935
31387
  /**
@@ -30985,6 +31437,10 @@ var Attempts = class {
30985
31437
  receipt(taskId) {
30986
31438
  return this.live.get(taskId)?.receipt ?? null;
30987
31439
  }
31440
+ /** Which spawn of the live attempt's runner waits for Ordewell's tools to connect; null when none does. */
31441
+ attaching(taskId) {
31442
+ return this.live.get(taskId)?.attaching ?? null;
31443
+ }
30988
31444
  /** The kind a task's next attempt starts as, short of a repair or a continue: what is weighed before one exists. */
30989
31445
  nextKind(taskId) {
30990
31446
  const task2 = this.deps.store.get(taskId);
@@ -31025,7 +31481,9 @@ var Attempts = class {
31025
31481
  shell: null,
31026
31482
  web: null,
31027
31483
  patch: null,
31028
- receipt: null
31484
+ receipt: null,
31485
+ attaching: null,
31486
+ spawning: new AbortController()
31029
31487
  };
31030
31488
  this.spawnCounts.set(task2.id, attempt.attempt);
31031
31489
  this.live.set(task2.id, attempt);
@@ -31040,7 +31498,8 @@ var Attempts = class {
31040
31498
  /**
31041
31499
  * The one way an attempt ends. Dropping the record is itself what
31042
31500
  * invalidates a spawn still in flight ({@link run} kills the session it
31043
- * receives for an attempt that is no longer current).
31501
+ * receives for an attempt that is no longer current), and its cancel takes
31502
+ * back a runner still starting meanwhile.
31044
31503
  *
31045
31504
  * A user interruption also bumps the verifier's generation *before* stopping
31046
31505
  * the runner: a runner may fire onExit synchronously from
@@ -31056,6 +31515,7 @@ var Attempts = class {
31056
31515
  const session = attempt?.session ?? null;
31057
31516
  const nativeSessionId = session?.nativeSessionId() ?? null;
31058
31517
  if (attempt) {
31518
+ attempt.spawning.abort();
31059
31519
  store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
31060
31520
  this.deps.output.detach(taskId);
31061
31521
  attempt.shell?.end();
@@ -31091,6 +31551,7 @@ var Attempts = class {
31091
31551
  const ended = this.endAll("load");
31092
31552
  this.planScope += 1;
31093
31553
  this.spawnCounts.clear();
31554
+ this.startConfig.clear();
31094
31555
  return ended;
31095
31556
  }
31096
31557
  /** A task leaving the plan: no later attempt of it is numbered from its runs. */
@@ -31223,21 +31684,31 @@ var Attempts = class {
31223
31684
  if (unavailable2) throw new Error(unavailable2);
31224
31685
  }
31225
31686
  if (kind.kind === "continuation") await this.windDowns.get(task2.id);
31687
+ if (!attempt.script && kind.kind === "repair") {
31688
+ const paths = await this.startConfig.check(runs.current, task2.id, { epoch: runs.epoch, root: this.deps.workspaceRoot(), cwd: null, worktree: true });
31689
+ if (paths.length) throw new TaskStartConfigError(paths);
31690
+ }
31226
31691
  const { cwd, worktree, previousWork, workspace } = await attemptCwd(kind, task2, runs);
31227
31692
  attempt.cwd = cwd;
31228
31693
  attempt.worktree = worktree;
31694
+ if (!attempt.script && kind.kind !== "repair" && !landsNothing(kind)) {
31695
+ const paths = await this.startConfig.check(runs.current, task2.id, { epoch: runs.epoch, root: this.deps.workspaceRoot(), cwd, worktree });
31696
+ if (paths.length) throw new TaskStartConfigError(paths);
31697
+ }
31229
31698
  if (checksTree(kind)) attempt.snapshot = await runs.snapshotWorkspace();
31230
31699
  if (this.live.get(task2.id) !== attempt) {
31231
31700
  if (attempt.worktree && !this.live.has(task2.id) && !this.host.completing(task2.id)) await runs.release(task2.id, { keep: kind.kind === "repair" });
31232
31701
  this.abandonSpawn(task2, attempt);
31233
31702
  return false;
31234
31703
  }
31704
+ this.verifier.starting(task2.id);
31235
31705
  const session = attempt.script ? await this.spawnScript(task2, attempt, cwd) : await this.spawnRunner(task2, attempt, cwd, worktree ? workspace : void 0, previousWork);
31236
31706
  if (this.live.get(task2.id) !== attempt) {
31237
31707
  this.abandonSpawn(task2, attempt, session);
31238
31708
  return false;
31239
31709
  }
31240
31710
  attempt.phase = "running";
31711
+ attempt.attaching = null;
31241
31712
  attempt.session = session;
31242
31713
  store.setTaskRunnerSessionId(task2.id, void 0);
31243
31714
  store.clearCarriedFrom(task2.id);
@@ -31306,6 +31777,9 @@ var Attempts = class {
31306
31777
  return { kind: "usage-limited", taskId: task2.id, attempt, said: { message }, output: "" };
31307
31778
  }
31308
31779
  const failed = (failure2) => ({ kind: "spawn-failed", task: task2, attempt, failure: failure2 });
31780
+ if (err instanceof TaskStartConfigError) {
31781
+ return failed({ leaves: "held", hold: err.message, preserveWorktree: true, settles: false, notice: { level: "warn", text: `Task "${task2.title}" is on hold: ${err.message}` } });
31782
+ }
31309
31783
  if (attempt.kind.kind === "continuation" && !attachTimeout) {
31310
31784
  const refused3 = err instanceof RunnerStartupExit;
31311
31785
  const reason = unresumedMessage(task2, refused3 ? `${attempt.runner} could not find its saved session` : `could not start: ${message}`);
@@ -31383,7 +31857,13 @@ var Attempts = class {
31383
31857
  ...web ? { webReads: web } : {},
31384
31858
  applyPatch: (args) => this.applyPatch(task2, attempt, args),
31385
31859
  workspaceRoot: this.deps.workspaceRoot(),
31386
- onNotice: (message) => this.host.tell("warn", `Task "${task2.title}": ${message}`)
31860
+ onNotice: (message) => this.host.tell("warn", `Task "${task2.title}": ${message}`),
31861
+ cancelled: attempt.spawning.signal,
31862
+ onAttaching: (spawn) => {
31863
+ if (this.live.get(task2.id) !== attempt) return;
31864
+ attempt.attaching = spawn;
31865
+ this.host.emit("onTaskChanged");
31866
+ }
31387
31867
  });
31388
31868
  }
31389
31869
  /**
@@ -31436,9 +31916,9 @@ var Attempts = class {
31436
31916
  const repos = Object.values(this.deps.runs.openRecord(taskId)?.repos ?? {});
31437
31917
  if (repos.length !== 1 || cwd === null) return [];
31438
31918
  const [{ worktree, linked }] = repos;
31439
- const prefix = path38.relative(worktree, cwd).split(path38.sep).join("/");
31440
- if (prefix.startsWith("..") || path38.isAbsolute(prefix)) return [];
31441
- return linked.map((name) => (prefix ? `${prefix}/${name}` : name).split(path38.sep).join("/"));
31919
+ const prefix = path41.relative(worktree, cwd).split(path41.sep).join("/");
31920
+ if (prefix.startsWith("..") || path41.isAbsolute(prefix)) return [];
31921
+ return linked.map((name) => (prefix ? `${prefix}/${name}` : name).split(path41.sep).join("/"));
31442
31922
  }
31443
31923
  /**
31444
31924
  * A script task's start (ADR-0035): its body as the plan holds it now, run
@@ -31476,7 +31956,7 @@ var Attempts = class {
31476
31956
  this.deps.notifications.warn(message);
31477
31957
  };
31478
31958
  if (resolved.blockedEnvrc) {
31479
- warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path38.dirname(resolved.blockedEnvrc)} to use them.`);
31959
+ warn(`blocked:${resolved.blockedEnvrc}`, `direnv has blocked ${resolved.blockedEnvrc}, so tasks start without its variables. Run \`direnv allow\` in ${path41.dirname(resolved.blockedEnvrc)} to use them.`);
31480
31960
  }
31481
31961
  if (resolved.trackedEnvFile) {
31482
31962
  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.`);
@@ -31502,7 +31982,7 @@ var Attempts = class {
31502
31982
  uncommittedSkillFolder(name) {
31503
31983
  const root = this.deps.workspaceRoot();
31504
31984
  const skill = this.deps.skillsAt(this.skillRoots(root)).findSkill(name);
31505
- return skill?.source === "workspace" ? path38.relative(root, path38.dirname(skill.path)).split(path38.sep).join("/") : void 0;
31985
+ return skill?.source === "workspace" ? path41.relative(root, path41.dirname(skill.path)).split(path41.sep).join("/") : void 0;
31506
31986
  }
31507
31987
  /**
31508
31988
  * The roots whose `.ordewell/skills/` an attempt at `cwd` reads: in a repo
@@ -31871,12 +32351,12 @@ function ancestors(task2, store) {
31871
32351
 
31872
32352
  // src/services/sessionLease.ts
31873
32353
  import * as fs19 from "fs";
31874
- import * as path40 from "path";
32354
+ import * as path43 from "path";
31875
32355
  import { randomUUID as randomUUID6 } from "crypto";
31876
32356
 
31877
32357
  // src/utils/sessionStore.ts
31878
32358
  import * as fs18 from "fs";
31879
- import * as path39 from "path";
32359
+ import * as path42 from "path";
31880
32360
 
31881
32361
  // src/utils/sessionId.ts
31882
32362
  import { randomBytes as randomBytes8 } from "crypto";
@@ -31887,13 +32367,13 @@ function mintSessionId() {
31887
32367
  // src/utils/sessionStore.ts
31888
32368
  var SESSIONS_SUBDIR = "sessions";
31889
32369
  function getSessionsDir(baseDir) {
31890
- return path39.join(getStateDir(baseDir), SESSIONS_SUBDIR);
32370
+ return path42.join(getStateDir(baseDir), SESSIONS_SUBDIR);
31891
32371
  }
31892
32372
  function idSegment(id2) {
31893
32373
  return /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(id2) ? id2 : `~${Buffer.from(id2, "utf-8").toString("hex")}`;
31894
32374
  }
31895
32375
  function sessionDataDir(sessionId, baseDir) {
31896
- return path39.join(getSessionsDir(baseDir), idSegment(sessionId));
32376
+ return path42.join(getSessionsDir(baseDir), idSegment(sessionId));
31897
32377
  }
31898
32378
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
31899
32379
  function isSessionMeta(value) {
@@ -31920,8 +32400,8 @@ function legacySessionFilename(session) {
31920
32400
  return `${filenameStem(session)}_${session.id.slice(0, 8)}.json`;
31921
32401
  }
31922
32402
  function dropLegacyFile(sessionsDir, meta) {
31923
- const legacy = path39.join(sessionsDir, legacySessionFilename(meta));
31924
- if (legacy === path39.join(sessionsDir, sessionFilename(meta)) || !fs18.existsSync(legacy)) return;
32403
+ const legacy = path42.join(sessionsDir, legacySessionFilename(meta));
32404
+ if (legacy === path42.join(sessionsDir, sessionFilename(meta)) || !fs18.existsSync(legacy)) return;
31925
32405
  try {
31926
32406
  const held2 = JSON.parse(fs18.readFileSync(legacy, "utf-8")).meta?.id;
31927
32407
  if (held2 === meta.id) fs18.unlinkSync(legacy);
@@ -31948,7 +32428,7 @@ function saveSession(plan, goal, baseDir, id2) {
31948
32428
  };
31949
32429
  dropRetiredPlanFields(session.plan);
31950
32430
  const filename = sessionFilename(meta);
31951
- const filePath = path39.join(sessionsDir, filename);
32431
+ const filePath = path42.join(sessionsDir, filename);
31952
32432
  writeFileAtomic(filePath, JSON.stringify(session, null, 2));
31953
32433
  dropLegacyFile(sessionsDir, meta);
31954
32434
  return meta;
@@ -31960,7 +32440,7 @@ function listSessions(baseDir, logger = defaultLogger) {
31960
32440
  return files.map((f) => {
31961
32441
  let parsed;
31962
32442
  try {
31963
- parsed = JSON.parse(fs18.readFileSync(path39.join(sessionsDir, f), "utf-8"));
32443
+ parsed = JSON.parse(fs18.readFileSync(path42.join(sessionsDir, f), "utf-8"));
31964
32444
  } catch (err) {
31965
32445
  if (err instanceof SyntaxError) return moveAside(sessionsDir, f, "is not valid JSON", logger);
31966
32446
  logger.warn("sessionStore", `failed to read session file ${f}; skipping`, err);
@@ -31981,7 +32461,7 @@ function listSessions(baseDir, logger = defaultLogger) {
31981
32461
  function moveAside(sessionsDir, file, reason, logger) {
31982
32462
  const aside = `${file}.${Date.now()}.corrupt`;
31983
32463
  try {
31984
- fs18.renameSync(path39.join(sessionsDir, file), path39.join(sessionsDir, aside));
32464
+ fs18.renameSync(path42.join(sessionsDir, file), path42.join(sessionsDir, aside));
31985
32465
  logger.warn("sessionStore", `session file ${file} ${reason}; moved aside to ${aside} rather than overwritten`);
31986
32466
  } catch {
31987
32467
  logger.warn("sessionStore", `failed to read session file ${file}; skipping`);
@@ -31992,7 +32472,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
31992
32472
  const files = fs18.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
31993
32473
  for (const f of files) {
31994
32474
  try {
31995
- const raw = fs18.readFileSync(path39.join(sessionsDir, f), "utf-8");
32475
+ const raw = fs18.readFileSync(path42.join(sessionsDir, f), "utf-8");
31996
32476
  const session = JSON.parse(raw);
31997
32477
  if (session.meta.id === sessionId) return f;
31998
32478
  } catch (err) {
@@ -32007,7 +32487,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
32007
32487
  try {
32008
32488
  const file = findSessionFile(sessionsDir, sessionId, logger);
32009
32489
  if (file) {
32010
- const raw = fs18.readFileSync(path39.join(sessionsDir, file), "utf-8");
32490
+ const raw = fs18.readFileSync(path42.join(sessionsDir, file), "utf-8");
32011
32491
  const parsed = JSON.parse(raw);
32012
32492
  if (hasScalarRunner(parsed)) {
32013
32493
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -32049,7 +32529,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
32049
32529
  try {
32050
32530
  const file = findSessionFile(sessionsDir, sessionId, logger);
32051
32531
  if (file) {
32052
- fs18.unlinkSync(path39.join(sessionsDir, file));
32532
+ fs18.unlinkSync(path42.join(sessionsDir, file));
32053
32533
  return true;
32054
32534
  }
32055
32535
  } catch (err) {
@@ -32102,7 +32582,7 @@ var UNLEASED = {
32102
32582
  };
32103
32583
  var hostsHere = /* @__PURE__ */ new Set();
32104
32584
  function leaseFile(sessionId, workspace) {
32105
- return path40.join(sessionDataDir(sessionId, workspace), LEASE_FILE);
32585
+ return path43.join(sessionDataDir(sessionId, workspace), LEASE_FILE);
32106
32586
  }
32107
32587
  function isRecord6(value) {
32108
32588
  if (typeof value !== "object" || value === null) return false;
@@ -32191,7 +32671,7 @@ var LeaseHost = class {
32191
32671
  */
32192
32672
  acquire(file, workspace, holder) {
32193
32673
  ensureStateDirIgnored(workspace);
32194
- ensurePrivateDir(path40.dirname(file));
32674
+ ensurePrivateDir(path43.dirname(file));
32195
32675
  const outcome = withFileLock(file, () => {
32196
32676
  const current = readRecord(file);
32197
32677
  const taken = current !== null && current.hostId !== this.id ? current : null;
@@ -32246,7 +32726,7 @@ var LeaseHost = class {
32246
32726
  withFileLock(file, () => {
32247
32727
  if (readRecord(file)?.hostId === this.id) fs19.rmSync(file, { force: true });
32248
32728
  }, "release a session lease");
32249
- fs19.rmdirSync(path40.dirname(file));
32729
+ fs19.rmdirSync(path43.dirname(file));
32250
32730
  } catch {
32251
32731
  }
32252
32732
  }
@@ -32439,10 +32919,14 @@ var TaskOrchestrator = class _TaskOrchestrator {
32439
32919
  /** Conflict repairs owed with every slot taken and no scheduler to claim them: a freed slot starts them. */
32440
32920
  owedRepairs = /* @__PURE__ */ new Set();
32441
32921
  /**
32442
- * Tasks Mark complete is landing after it ended their attempt: in progress
32443
- * with no attempt for as long as that takes, and not orphans for it.
32922
+ * Tasks Mark complete is landing after it ended their attempt, or that
32923
+ * their conflict resolver is landing, and that landing: in progress with
32924
+ * no attempt for as long as it takes, and not orphans for it. Both can be
32925
+ * landing one task at once, so each holds it for itself (see {@link holdWhileLanding}).
32444
32926
  */
32445
- completing = /* @__PURE__ */ new Set();
32927
+ completing = /* @__PURE__ */ new Map();
32928
+ /** Tasks {@link releaseTask} is tearing down once their landings have settled: none may begin another. */
32929
+ releasing = /* @__PURE__ */ new Set();
32446
32930
  /** Tasks paused on a usage limit, the run's hold on them, and each runner's wait for its reset (ADR-0018, U1). */
32447
32931
  limitPauses;
32448
32932
  /**
@@ -32578,6 +33062,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
32578
33062
  getCompletionReceipt(taskId) {
32579
33063
  return this.attempts.receipt(taskId);
32580
33064
  }
33065
+ /** Which spawn of a live attempt's runner waits for Ordewell's tools to connect (ADR-0025); null when none does. */
33066
+ getToolsAttaching(taskId) {
33067
+ return this.attempts.attaching(taskId);
33068
+ }
32581
33069
  /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
32582
33070
  getLiveOutput(taskId, opts) {
32583
33071
  return this.attempts.liveTail(taskId, opts);
@@ -32742,12 +33230,13 @@ var TaskOrchestrator = class _TaskOrchestrator {
32742
33230
  /**
32743
33231
  * Something can still start that nobody has to ask for again: a start
32744
33232
  * admitted and not yet claimed, a force start queued behind its
32745
- * dependencies, a task that resumes on its own at its usage limit's
32746
- * reset, or a start a dirty checkout parked for Stash and continue to
32747
- * replay. A Stop is owed to it, whether or not anything runs.
33233
+ * dependencies, a conflict repair owed a slot or the end of a start hold,
33234
+ * a task that resumes on its own at its usage limit's reset, or a start a
33235
+ * dirty checkout parked for Stash and continue to replay. A Stop is owed to
33236
+ * it, whether or not anything runs.
32748
33237
  */
32749
33238
  get startsPending() {
32750
- return this.admissions > 0 || this.queuedStarts.size > 0 || this.runs.blocked || this.store.allTasks.some(waitsForReset);
33239
+ return this.admissions > 0 || this.queuedStarts.size > 0 || this.owedRepairs.size > 0 || this.runs.blocked || this.store.allTasks.some(waitsForReset);
32751
33240
  }
32752
33241
  /**
32753
33242
  * The plan's activity as one word for every surface: a start on its way,
@@ -33183,7 +33672,9 @@ var TaskOrchestrator = class _TaskOrchestrator {
33183
33672
  this.store.markPending(task2.id);
33184
33673
  this.hold(task2.id, failure2.hold, false);
33185
33674
  }
33186
- await this.runs.release(task2.id, { keep: failure2.leaves === "failed" && failure2.keepWorktree });
33675
+ if (failure2.leaves !== "held" || !failure2.preserveWorktree) {
33676
+ await this.runs.release(task2.id, { keep: failure2.leaves === "failed" && failure2.keepWorktree });
33677
+ }
33187
33678
  const { level, text: text4, toastOnly } = failure2.notice;
33188
33679
  if (toastOnly) this.notifications[level](text4);
33189
33680
  else this.tell(level, text4);
@@ -33248,6 +33739,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
33248
33739
  * Work that did not land waits on the user — it never fails the task or
33249
33740
  * blocks its dependents — unless its conflict is owed a repair, which takes
33250
33741
  * the slot the ending attempt freed and never one more than the run allows.
33742
+ * A planner turn's start hold holds it as it holds any new spawn (ADR-0029).
33251
33743
  * The repair follows on from the start whose work it repairs: a Stop since
33252
33744
  * that was admitted withdraws it.
33253
33745
  */
@@ -33255,20 +33747,22 @@ var TaskOrchestrator = class _TaskOrchestrator {
33255
33747
  this.store.markAwaitingUser(task2.id, "conflict");
33256
33748
  this.say(outcome.messages);
33257
33749
  if (outcome.kind !== "repair-needed") return;
33258
- if (this.attempts.size < this.config.maxParallelSessions) {
33750
+ const waitsFor = this.startHolds > 0 ? "the planner's turn ends" : this.slotsFull() ? "a slot is free" : null;
33751
+ if (!waitsFor) {
33259
33752
  const claimed = this.claim(task2.id, admission);
33260
33753
  if ("refused" in claimed) this.tell("warn", `The conflict of task "${task2.title}" is not repaired: ${claimed.refused}`);
33261
33754
  else this.unawaited(claimed.spawned, `Could not start the repair of task "${task2.title}"`);
33262
33755
  } else {
33263
33756
  this.store.markPending(task2.id);
33264
33757
  if (!this.isScheduling) this.owedRepairs.add(task2.id);
33265
- this.tell("info", `Task "${task2.title}" is repaired once a slot is free.`);
33758
+ this.tell("info", `Task "${task2.title}" is repaired once ${waitsFor}.`);
33266
33759
  }
33267
33760
  }
33268
- /** Start the repairs {@link settleUnlanded} left owed, as slots allow. */
33761
+ /** Start the repairs {@link settleUnlanded} left owed, as slots and any start hold allow. */
33269
33762
  startOwedRepairs(admission) {
33763
+ if (this.startHolds > 0) return;
33270
33764
  for (const id2 of [...this.owedRepairs]) {
33271
- if (this.attempts.size >= this.config.maxParallelSessions) return;
33765
+ if (this.slotsFull()) return;
33272
33766
  this.owedRepairs.delete(id2);
33273
33767
  const task2 = this.store.get(id2);
33274
33768
  if (!task2 || task2.status !== "pending" || this.attempts.has(id2) || !this.landing.nextRepair(id2)) continue;
@@ -33278,13 +33772,31 @@ var TaskOrchestrator = class _TaskOrchestrator {
33278
33772
  }
33279
33773
  }
33280
33774
  async landResolved(resolverId, admission) {
33281
- const resolved = await this.landing.landResolved(resolverId);
33775
+ const resolved = this.landing.landResolved(resolverId, (id2) => this.releasing.has(id2));
33282
33776
  if (!resolved) return;
33283
- const { task: task2, outcome } = resolved;
33777
+ const { task: task2 } = resolved;
33778
+ const outcome = await this.holdWhileLanding(task2.id, resolved.outcome);
33779
+ if (this.store.get(task2.id)?.status === "completed") return;
33284
33780
  await this.applyLanding(task2, outcome, admission, () => {
33285
33781
  if (task2.verdict) this.logAndArchive(task2, task2.verdict);
33286
33782
  });
33287
33783
  }
33784
+ /**
33785
+ * Hold a task for as long as `landing` is merging its worktree. Its end lets
33786
+ * go of its own hold only: another landing of the task begun meanwhile
33787
+ * still holds it.
33788
+ */
33789
+ async holdWhileLanding(taskId, landing2) {
33790
+ const holds2 = this.completing.get(taskId) ?? /* @__PURE__ */ new Set();
33791
+ holds2.add(landing2);
33792
+ this.completing.set(taskId, holds2);
33793
+ try {
33794
+ return await landing2;
33795
+ } finally {
33796
+ holds2.delete(landing2);
33797
+ if (holds2.size === 0 && this.completing.get(taskId) === holds2) this.completing.delete(taskId);
33798
+ }
33799
+ }
33288
33800
  getReadyTasks() {
33289
33801
  return this.readiness().ready;
33290
33802
  }
@@ -33392,17 +33904,27 @@ var TaskOrchestrator = class _TaskOrchestrator {
33392
33904
  * as {@link cancelTask} does, but its worktree goes: no task is left to land
33393
33905
  * it into. A spawn still in flight just loses its attempt,
33394
33906
  * which is what makes {@link Attempts} kill the session it is about to
33395
- * receive. The id's cross-attempt bookkeeping goes too — a hold or retry
33396
- * count kept for a task that no longer exists would be inherited by nothing.
33907
+ * receive. Every landing of it in flight — Mark complete's, its resolver's —
33908
+ * settles first, so its worktree is not torn down under a merge, and none
33909
+ * begins while it goes; a repair a landing claims is cancelled as any
33910
+ * running attempt is. The id's cross-attempt bookkeeping
33911
+ * goes too — a hold or retry count kept for a task that no longer exists
33912
+ * would be inherited by nothing.
33397
33913
  */
33398
33914
  async releaseTask(taskId) {
33399
33915
  this.limitPauses.cancel(taskId);
33400
- const phase = this.attempts.phaseOf(taskId);
33401
- if (phase === "running" || phase === "integrating") await this.cancelAttempt(taskId, { keep: false });
33402
- else {
33403
- this.attempts.end(taskId, "release");
33404
- if (this.store.get(taskId)?.status === "in_progress") this.store.markPending(taskId);
33405
- await this.runs.release(taskId, { keep: false });
33916
+ for (let holds2 = this.completing.get(taskId); holds2; holds2 = this.completing.get(taskId)) await Promise.allSettled(holds2);
33917
+ this.releasing.add(taskId);
33918
+ try {
33919
+ const phase = this.attempts.phaseOf(taskId);
33920
+ if (phase === "running" || phase === "integrating") await this.cancelAttempt(taskId, { keep: false });
33921
+ else {
33922
+ this.attempts.end(taskId, "release");
33923
+ if (this.store.get(taskId)?.status === "in_progress") this.store.markPending(taskId);
33924
+ await this.runs.release(taskId, { keep: false });
33925
+ }
33926
+ } finally {
33927
+ this.releasing.delete(taskId);
33406
33928
  }
33407
33929
  this.onHold.delete(taskId);
33408
33930
  this.retryCounts.delete(taskId);
@@ -33411,17 +33933,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
33411
33933
  async markTaskComplete(taskId) {
33412
33934
  const task2 = this.store.get(taskId);
33413
33935
  if (!task2 || task2.status === "completed") return;
33414
- if (this.completing.has(taskId)) return;
33936
+ if (this.completing.has(taskId) || this.releasing.has(taskId)) return;
33415
33937
  const admission = this.admittedNow();
33416
33938
  const ended = this.attempts.end(taskId, "complete");
33417
33939
  const verdict = this.attempts.markComplete(task2);
33418
- this.completing.add(taskId);
33419
- let landing2;
33420
- try {
33421
- landing2 = await (ended?.integration ?? this.landing.landVouched(task2));
33422
- } finally {
33423
- this.completing.delete(taskId);
33424
- }
33940
+ const landed2 = ended?.integration ?? this.landing.landVouched(task2);
33941
+ const landing2 = await this.holdWhileLanding(taskId, landed2);
33425
33942
  this.store.setTaskVerdict(taskId, verdict);
33426
33943
  const handoff = task2.type === "user" ? userTaskHandoff(task2.userSteps) : void 0;
33427
33944
  this.attempts.handOver(taskId, summarizeOutput(verdict.reason, ""), handoff);
@@ -33710,7 +34227,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
33710
34227
  if (!await this.openForAdmitted(taskId, origin, resume, admission)) return;
33711
34228
  if (this.attempts.has(taskId)) return;
33712
34229
  this.refuseOpsDuringMerge(taskId, forced);
33713
- if (forced.queued && this.holdsQueued(taskId, forced)) {
34230
+ if (forced.queued && (this.holdsQueued(taskId, forced) || this.slotsFull())) {
33714
34231
  this.queuedStarts.set(taskId, { pastGate: forced.pastGate });
33715
34232
  return;
33716
34233
  }
@@ -33769,8 +34286,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
33769
34286
  this.queuedStarts.delete(id2);
33770
34287
  continue;
33771
34288
  }
34289
+ if (this.slotsFull()) continue;
33772
34290
  const waits = this.startWaits(task2, task2.research ? void 0 : { queued: true, pastGate: queued.pastGate });
33773
- if (task2.research && (waits.length > 0 || this.attempts.size >= this.config.maxParallelSessions)) continue;
34291
+ if (task2.research && waits.length > 0) continue;
33774
34292
  if (waits.some((w) => QUEUED_WAITS.has(w.kind))) continue;
33775
34293
  if (this.leftToScheduler(task2) && queued.pastGate.length === 0) {
33776
34294
  this.queuedStarts.delete(id2);
@@ -33799,6 +34317,9 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
33799
34317
  const task2 = this.store.get(taskId);
33800
34318
  return !!task2 && this.startWaits(task2, forced).some((w) => QUEUED_WAITS.has(w.kind) || w.kind === "dependencies" || w.kind === "merge-gate");
33801
34319
  }
34320
+ slotsFull() {
34321
+ return this.attempts.size >= this.config.maxParallelSessions;
34322
+ }
33802
34323
  sayQueuedAtGate(task2, queued, unmerged) {
33803
34324
  const titles = unmerged.map((id2) => `"${this.store.get(id2)?.title ?? id2}"`).join(", ");
33804
34325
  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.`;
@@ -33899,7 +34420,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
33899
34420
  */
33900
34421
  async settleRun(after) {
33901
34422
  if (!this.store.isFinished()) this.transition({ type: "reopen" });
33902
- if (this.attempts.size > 0) return;
34423
+ if (this.attempts.size > 0 || this.owedRepairs.size > 0) return;
33903
34424
  if (this.runState.kind === "held") return;
33904
34425
  if (this.runState.kind !== "scheduling") {
33905
34426
  if (after === "tick") {
@@ -34193,21 +34714,45 @@ var Planner = class {
34193
34714
  };
34194
34715
 
34195
34716
  // src/services/ModelResolver.ts
34196
- var ModelResolver = class {
34717
+ var ModelResolver = class _ModelResolver {
34197
34718
  constructor(registry, config, deps = {}) {
34198
34719
  this.registry = registry;
34199
34720
  this.config = config;
34200
- this.discovery = new ModelDiscovery(registry, deps.execImpl, deps.fetchImpl);
34721
+ this.discovery = new ModelDiscovery(registry, deps.execImpl, deps.fetchImpl, void 0, void 0, void 0, deps.env);
34201
34722
  this.fetchImpl = deps.fetchImpl;
34723
+ this.execImpl = deps.execImpl;
34724
+ this.shared = { vendor: { options: null, load: null, errors: {}, generation: 0 }, root: this, views: /* @__PURE__ */ new Map() };
34202
34725
  }
34203
34726
  registry;
34204
34727
  config;
34205
34728
  discovery;
34206
34729
  fetchImpl;
34207
- cachedPickerOptions = null;
34208
- /** A catalog load under way: a second ask, or a warm-up, shares it rather than fetching again. */
34209
- pickerLoad = null;
34210
- lastDiscoveryErrors = {};
34730
+ execImpl;
34731
+ shared;
34732
+ get vendor() {
34733
+ return this.shared.vendor;
34734
+ }
34735
+ /**
34736
+ * This resolver for runners launched with another environment, a
34737
+ * workspace's own: their catalogs are discovered and cached under `env`, as
34738
+ * `CLAUDE_CONFIG_DIR` there can name another account, while the vendor
34739
+ * catalogs stay this resolver's, fetched once. One view per `key`, made on
34740
+ * its first ask: a later ask with the same key gets that view, whatever
34741
+ * `env` it passes.
34742
+ */
34743
+ withRunnerEnv(key, env) {
34744
+ let view = this.shared.views.get(key);
34745
+ if (!view) {
34746
+ view = new _ModelResolver(this.registry, this.config, { fetchImpl: this.fetchImpl, execImpl: this.execImpl, env });
34747
+ view.shared = this.shared;
34748
+ this.shared.views.set(key, view);
34749
+ }
34750
+ return view;
34751
+ }
34752
+ /** Every runner-environment catalog this resolver's family holds: its root's and each view's. */
34753
+ runnerCatalogs() {
34754
+ return [this.shared.root, ...this.shared.views.values()].map((resolver) => resolver.discovery);
34755
+ }
34211
34756
  static builtinOptions() {
34212
34757
  return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
34213
34758
  }
@@ -34223,16 +34768,16 @@ var ModelResolver = class {
34223
34768
  return Object.fromEntries(discovered);
34224
34769
  }
34225
34770
  async pickerOptions() {
34226
- if (this.cachedPickerOptions) return this.cachedPickerOptions;
34227
- if (!this.pickerLoad) {
34771
+ if (this.vendor.options) return this.vendor.options;
34772
+ if (!this.vendor.load) {
34228
34773
  const load = this.loadPickerOptions();
34229
- this.pickerLoad = load;
34774
+ this.vendor.load = load;
34230
34775
  void load.finally(() => {
34231
- if (this.pickerLoad === load) this.pickerLoad = null;
34776
+ if (this.vendor.load === load) this.vendor.load = null;
34232
34777
  }).catch(() => {
34233
34778
  });
34234
34779
  }
34235
- return this.pickerLoad;
34780
+ return this.vendor.load;
34236
34781
  }
34237
34782
  /**
34238
34783
  * Start loading the vendor catalogs without waiting for them, unless they
@@ -34240,20 +34785,25 @@ var ModelResolver = class {
34240
34785
  * known yet reads it from them on a later call, never waiting on the fetch.
34241
34786
  */
34242
34787
  warmPickerOptions() {
34243
- if (this.cachedPickerOptions || this.pickerLoad) return;
34788
+ if (this.vendor.options || this.vendor.load) return;
34244
34789
  this.pickerOptions().catch(() => {
34245
34790
  });
34246
34791
  }
34247
34792
  async loadPickerOptions() {
34793
+ const vendor = this.vendor;
34794
+ const generation2 = vendor.generation;
34248
34795
  const { apiKeys, baseUrls } = collectProviderCredentials(this.config);
34249
34796
  const { models, errors } = await fetchAllProviderModels({
34250
34797
  apiKeys,
34251
34798
  baseUrls,
34252
34799
  fetchImpl: this.fetchImpl
34253
34800
  });
34254
- this.lastDiscoveryErrors = errors;
34255
- this.cachedPickerOptions = toOrchestratorOptions(models, ORCHESTRATOR_SHORTCUTS);
34256
- return this.cachedPickerOptions;
34801
+ const options = toOrchestratorOptions(models, ORCHESTRATOR_SHORTCUTS);
34802
+ if (vendor.generation === generation2) {
34803
+ vendor.errors = errors;
34804
+ vendor.options = options;
34805
+ }
34806
+ return options;
34257
34807
  }
34258
34808
  /**
34259
34809
  * Per-provider failures from the most recent `pickerOptions()` fetch, keyed
@@ -34262,7 +34812,7 @@ var ModelResolver = class {
34262
34812
  * key/endpoint is set but whose catalog could not be reached.
34263
34813
  */
34264
34814
  getDiscoveryErrors() {
34265
- return this.lastDiscoveryErrors;
34815
+ return this.vendor.errors;
34266
34816
  }
34267
34817
  async refresh() {
34268
34818
  const options = await this.pickerOptions();
@@ -34273,8 +34823,25 @@ var ModelResolver = class {
34273
34823
  this.config.setProviderModelLists(lists);
34274
34824
  return lists;
34275
34825
  }
34826
+ /** Forget every runner catalog, each workspace's included; the vendor catalogs stay. */
34276
34827
  refreshRunnerModels() {
34277
- this.discovery.clear();
34828
+ for (const discovery of this.runnerCatalogs()) discovery.clear();
34829
+ }
34830
+ /**
34831
+ * The models a pick for `provider` is judged against: a harness planner's
34832
+ * own runner catalog, or the vendor picker catalog (ADR-0009). Discovered
34833
+ * if not held. Never throws: a catalog that cannot be had is empty.
34834
+ */
34835
+ async plannerCatalog(provider) {
34836
+ const runner = runnerForProvider(provider);
34837
+ if (runner) return candidatesOf((await this.modelsForRunners([runner]))[runner] ?? []);
34838
+ return (await this.pickerOptions().catch(() => [])).map((o) => ({ id: o.id }));
34839
+ }
34840
+ /** {@link plannerCatalog} from what is already held, so a cold catalog is empty; never discovers. */
34841
+ cachedPlannerCatalog(provider) {
34842
+ const runner = runnerForProvider(provider);
34843
+ if (runner) return candidatesOf(this.getCachedRunnerModels(runner));
34844
+ return this.getCachedPickerOptions().map((o) => ({ id: o.id }));
34278
34845
  }
34279
34846
  /**
34280
34847
  * Synchronous reads of whatever the last `modelsForRunners`/`pickerOptions`
@@ -34285,7 +34852,7 @@ var ModelResolver = class {
34285
34852
  return this.discovery.getCached(runner) ?? [];
34286
34853
  }
34287
34854
  getCachedPickerOptions() {
34288
- return this.cachedPickerOptions ?? [];
34855
+ return this.vendor.options ?? [];
34289
34856
  }
34290
34857
  /**
34291
34858
  * A model's context window from whatever catalog is already cached (#49),
@@ -34298,7 +34865,7 @@ var ModelResolver = class {
34298
34865
  */
34299
34866
  contextWindowFor(source, model) {
34300
34867
  if (!model) return void 0;
34301
- 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;
34868
+ 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;
34302
34869
  return window2 && window2 > 0 ? window2 : void 0;
34303
34870
  }
34304
34871
  /**
@@ -34308,14 +34875,16 @@ var ModelResolver = class {
34308
34875
  */
34309
34876
  maxOutputTokensFor(source, model) {
34310
34877
  if (!model || !isApiProvider(source)) return void 0;
34311
- const cap = this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.maxOutputTokens;
34878
+ const cap = this.vendor.options?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.maxOutputTokens;
34312
34879
  return cap && cap > 0 ? cap : void 0;
34313
34880
  }
34881
+ /** Forget every catalog: the vendor ones, and each runner environment's. */
34314
34882
  invalidate() {
34315
- this.discovery.clear();
34316
- this.cachedPickerOptions = null;
34317
- this.pickerLoad = null;
34318
- this.lastDiscoveryErrors = {};
34883
+ this.refreshRunnerModels();
34884
+ this.vendor.generation++;
34885
+ this.vendor.options = null;
34886
+ this.vendor.load = null;
34887
+ this.vendor.errors = {};
34319
34888
  if (!this.fetchImpl) {
34320
34889
  for (const provider of PROVIDER_PRIORITY) {
34321
34890
  const baseUrl = this.config.getProviderBaseUrl(provider);
@@ -34325,6 +34894,9 @@ var ModelResolver = class {
34325
34894
  }
34326
34895
  }
34327
34896
  };
34897
+ function candidatesOf(models) {
34898
+ return models.map((m) => ({ id: m.modelId, variants: m.variants }));
34899
+ }
34328
34900
 
34329
34901
  // src/services/RunnerInstallation.ts
34330
34902
  import { exec as exec4 } from "child_process";
@@ -34918,7 +35490,7 @@ import { spawn as nodeSpawn3 } from "child_process";
34918
35490
  import { randomUUID as randomUUID7 } from "crypto";
34919
35491
  import { mkdtempSync as mkdtempSync5, rmSync as rmSync12 } from "fs";
34920
35492
  import { tmpdir as tmpdir6 } from "os";
34921
- import { join as join37 } from "path";
35493
+ import { join as join39 } from "path";
34922
35494
  var DEFAULT_TIMEOUT_MS5 = 2e4;
34923
35495
  var INITIALIZE_TIMEOUT_MS2 = 1e4;
34924
35496
  var USAGE_TIMEOUT_MS = 1e4;
@@ -34950,7 +35522,7 @@ function claudeAccountLimitCollector(options) {
34950
35522
  };
34951
35523
  }
34952
35524
  async function readClaudeAccountLimits(options, signal) {
34953
- const cwd = mkdtempSync5(join37(tmpdir6(), "ordewell-claude-limits-"));
35525
+ const cwd = mkdtempSync5(join39(tmpdir6(), "ordewell-claude-limits-"));
34954
35526
  const removeCwd = () => {
34955
35527
  try {
34956
35528
  rmSync12(cwd, { recursive: true, force: true });
@@ -35034,13 +35606,13 @@ function isoInstant(value) {
35034
35606
  }
35035
35607
 
35036
35608
  // src/services/harness/openCodeGoLimits.ts
35037
- import { homedir as homedir11 } from "os";
35609
+ import { homedir as homedir9 } from "os";
35038
35610
 
35039
35611
  // src/services/harness/openCodeCredentials.ts
35040
- import { createHash as createHash5 } from "crypto";
35612
+ import { createHash as createHash6 } from "crypto";
35041
35613
  import { promises as fs20 } from "fs";
35042
35614
  import { homedir as osHomedir } from "os";
35043
- import { join as join38, resolve as resolve13 } from "path";
35615
+ import { join as join40, resolve as resolve13 } from "path";
35044
35616
  import { Worker } from "worker_threads";
35045
35617
  var DEFAULT_TIMEOUT_MS6 = 5e3;
35046
35618
  var BUSY_MS = 250;
@@ -35095,19 +35667,19 @@ function walStore(path) {
35095
35667
  parentPort.postMessage(select());
35096
35668
  `;
35097
35669
  var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
35098
- var digest = (...parts) => createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
35670
+ var digest = (...parts) => createHash6("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
35099
35671
  function openCodeStorePaths(env, home) {
35100
- const data = join38(env.XDG_DATA_HOME || join38(home, ".local", "share"), "opencode");
35672
+ const data = join40(env.XDG_DATA_HOME || join40(home, ".local", "share"), "opencode");
35101
35673
  const file = env.OPENCODE_DB || "opencode.db";
35102
- return { db: file === ":memory:" ? null : resolve13(data, file), auth: join38(data, "auth.json") };
35674
+ return { db: file === ":memory:" ? null : resolve13(data, file), auth: join40(data, "auth.json") };
35103
35675
  }
35104
35676
  function openCodeGoLoginReader(deps = {}) {
35105
- const homedir12 = deps.homedir ?? osHomedir;
35677
+ const homedir10 = deps.homedir ?? osHomedir;
35106
35678
  let last;
35107
35679
  return {
35108
35680
  async read(env, signal) {
35109
35681
  if (signal.aborted) return { kind: "unreadable" };
35110
- const paths = openCodeStorePaths(env, homedir12());
35682
+ const paths = openCodeStorePaths(env, homedir10());
35111
35683
  const [db, wal, auth] = await Promise.all([
35112
35684
  paths.db ? stamp(paths.db) : null,
35113
35685
  paths.db ? stamp(`${paths.db}-wal`) : null,
@@ -35161,8 +35733,8 @@ function v2Login(db, row) {
35161
35733
  login: { route: "console", owner: digest("console", db, row.id, account, orgID ?? ""), access: value.access, ...orgID ? { orgID } : {}, expires: value.expires }
35162
35734
  };
35163
35735
  }
35164
- async function legacyLogin(path46, env) {
35165
- const content = env.OPENCODE_AUTH_CONTENT ?? await readSmall(path46);
35736
+ async function legacyLogin(path49, env) {
35737
+ const content = env.OPENCODE_AUTH_CONTENT ?? await readSmall(path49);
35166
35738
  if (content === void 0) return env[ENV_KEY] ? { kind: "unsupported" } : { kind: "none" };
35167
35739
  if (content === null) return { kind: "unreadable" };
35168
35740
  const auth = parseJson(content);
@@ -35171,15 +35743,15 @@ async function legacyLogin(path46, env) {
35171
35743
  if (entry === void 0) return env[ENV_KEY] ? { kind: "unsupported" } : { kind: "none" };
35172
35744
  if (!isRecord9(entry) || entry.type !== "api") return { kind: "unsupported" };
35173
35745
  if (typeof entry.key !== "string" || entry.key.length === 0) return { kind: "unreadable" };
35174
- return { kind: "login", login: { route: "key", owner: digest("v1", path46, entry.key), key: entry.key } };
35746
+ return { kind: "login", login: { route: "key", owner: digest("v1", path49, entry.key), key: entry.key } };
35175
35747
  }
35176
- function queryStore(path46, signal, deps) {
35748
+ function queryStore(path49, signal, deps) {
35177
35749
  return new Promise((settle) => {
35178
35750
  let worker;
35179
35751
  try {
35180
35752
  worker = (deps.spawnWorker ?? ((source, options) => new Worker(source, options)))(WORKER_SOURCE, {
35181
35753
  eval: true,
35182
- workerData: { path: path46, integrations: GO_INTEGRATIONS, busyMs: BUSY_MS, maxValueBytes: MAX_VALUE_BYTES },
35754
+ workerData: { path: path49, integrations: GO_INTEGRATIONS, busyMs: BUSY_MS, maxValueBytes: MAX_VALUE_BYTES },
35183
35755
  execArgv: deps.workerExecArgv ?? [],
35184
35756
  resourceLimits: { maxOldGenerationSizeMb: 64 }
35185
35757
  });
@@ -35206,19 +35778,19 @@ function queryStore(path46, signal, deps) {
35206
35778
  worker.once("exit", aborted);
35207
35779
  });
35208
35780
  }
35209
- async function stamp(path46) {
35781
+ async function stamp(path49) {
35210
35782
  try {
35211
- const stat = await fs20.stat(path46);
35783
+ const stat = await fs20.stat(path49);
35212
35784
  return [stat.ino, stat.size, stat.mtimeMs];
35213
35785
  } catch {
35214
35786
  return null;
35215
35787
  }
35216
35788
  }
35217
- async function readSmall(path46) {
35789
+ async function readSmall(path49) {
35218
35790
  try {
35219
- const stat = await fs20.stat(path46);
35791
+ const stat = await fs20.stat(path49);
35220
35792
  if (!stat.isFile() || stat.size > MAX_AUTH_FILE_BYTES) return null;
35221
- return await fs20.readFile(path46, "utf8");
35793
+ return await fs20.readFile(path49, "utf8");
35222
35794
  } catch (err) {
35223
35795
  return err.code === "ENOENT" ? void 0 : null;
35224
35796
  }
@@ -35251,7 +35823,7 @@ function openCodeGoAccountLimitCollector(options) {
35251
35823
  label: "OpenCode Go",
35252
35824
  provider: "opencode-go",
35253
35825
  scope: () => {
35254
- const paths = openCodeStorePaths(env(), (deps.reader?.homedir ?? homedir11)());
35826
+ const paths = openCodeStorePaths(env(), (deps.reader?.homedir ?? homedir9)());
35255
35827
  return ["opencode", paths.db ?? ":memory:", paths.auth].join("\0");
35256
35828
  },
35257
35829
  acquire: async (signal) => {
@@ -36347,20 +36919,20 @@ function createAiService(config, deps) {
36347
36919
  }
36348
36920
 
36349
36921
  // src/services/createSession.ts
36350
- import * as path43 from "path";
36922
+ import * as path46 from "path";
36351
36923
 
36352
36924
  // src/services/handoffReferences.ts
36353
36925
  import * as fs21 from "fs";
36354
- import * as path41 from "path";
36926
+ import * as path44 from "path";
36355
36927
  import { spawnSync as spawnSync2 } from "child_process";
36356
36928
  function resolveHandoffReferences(handoff, workspace) {
36357
36929
  const checked = /* @__PURE__ */ new Map();
36358
36930
  const contained = (candidate) => {
36359
36931
  try {
36360
36932
  const root = fs21.realpathSync(workspace);
36361
- const resolved = fs21.realpathSync(path41.resolve(root, candidate));
36362
- const relative17 = path41.relative(root, resolved);
36363
- return relative17 !== ".." && !relative17.startsWith(`..${path41.sep}`) && !path41.isAbsolute(relative17) ? resolved : void 0;
36933
+ const resolved = fs21.realpathSync(path44.resolve(root, candidate));
36934
+ const relative17 = path44.relative(root, resolved);
36935
+ return relative17 !== ".." && !relative17.startsWith(`..${path44.sep}`) && !path44.isAbsolute(relative17) ? resolved : void 0;
36364
36936
  } catch {
36365
36937
  return void 0;
36366
36938
  }
@@ -36381,7 +36953,7 @@ function resolveHandoffReferences(handoff, workspace) {
36381
36953
  }
36382
36954
  } else if (/^[0-9a-fA-F]{4,64}$/.test(ref.sha)) {
36383
36955
  const repo = contained(ref.repo ?? ".");
36384
- if (repo && fs21.existsSync(path41.join(repo, ".git"))) {
36956
+ if (repo && fs21.existsSync(path44.join(repo, ".git"))) {
36385
36957
  const result2 = spawnSync2("git", ["--no-optional-locks", "--no-replace-objects", "cat-file", "-t", ref.sha], {
36386
36958
  cwd: repo,
36387
36959
  encoding: "utf8",
@@ -37733,7 +38305,8 @@ var SessionEventRelay = class {
37733
38305
  this.checkpointQuestion(t.id),
37734
38306
  this.waitsForApproval(t.id),
37735
38307
  this.orchestrator.holdReason(t.id),
37736
- this.orchestrator.getCompletionReceipt(t.id)
38308
+ this.orchestrator.getCompletionReceipt(t.id),
38309
+ this.orchestrator.getToolsAttaching(t.id)
37737
38310
  )),
37738
38311
  ...runMerge ? { runMerge } : {},
37739
38312
  activity: this.orchestrator.activity
@@ -37855,10 +38428,10 @@ var SessionEventRelay = class {
37855
38428
 
37856
38429
  // src/utils/taskLogStore.ts
37857
38430
  import * as fs22 from "fs";
37858
- import * as path42 from "path";
38431
+ import * as path45 from "path";
37859
38432
  var EXTENSION = ".jsonl";
37860
38433
  function taskDir({ baseDir, sessionId }, taskId) {
37861
- return path42.join(sessionDataDir(sessionId, baseDir), "tasks", idSegment(taskId));
38434
+ return path45.join(sessionDataDir(sessionId, baseDir), "tasks", idSegment(taskId));
37862
38435
  }
37863
38436
  function listTaskLogAttempts(location, taskId) {
37864
38437
  let names;
@@ -37876,7 +38449,7 @@ function openTaskLog(location, taskId, attempt) {
37876
38449
  const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
37877
38450
  let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
37878
38451
  for (; ; ) {
37879
- const file = path42.join(dir, `${number}${EXTENSION}`);
38452
+ const file = path45.join(dir, `${number}${EXTENSION}`);
37880
38453
  try {
37881
38454
  fs22.writeFileSync(file, "", { flag: "wx" });
37882
38455
  } catch (err) {
@@ -37900,15 +38473,15 @@ function prepareTaskOutputFile(location, taskId, attempt) {
37900
38473
  ensureStateDirIgnored(location.baseDir);
37901
38474
  const dir = taskDir(location, taskId);
37902
38475
  fs22.mkdirSync(dir, { recursive: true });
37903
- return path42.join(dir, `${attempt}.output.txt`);
38476
+ return path45.join(dir, `${attempt}.output.txt`);
37904
38477
  }
37905
38478
  function taskPatchFile(location, taskId, attempt) {
37906
- return path42.join(taskDir(location, taskId), `${attempt}.patch`);
38479
+ return path45.join(taskDir(location, taskId), `${attempt}.patch`);
37907
38480
  }
37908
38481
  function saveTaskPatch(location, taskId, attempt, patch) {
37909
38482
  ensureStateDirIgnored(location.baseDir);
37910
38483
  const file = taskPatchFile(location, taskId, attempt);
37911
- fs22.mkdirSync(path42.dirname(file), { recursive: true });
38484
+ fs22.mkdirSync(path45.dirname(file), { recursive: true });
37912
38485
  const partial = `${file}.${process.pid}-${Date.now()}.partial`;
37913
38486
  fs22.writeFileSync(partial, patch, { flag: "wx" });
37914
38487
  fs22.renameSync(partial, file);
@@ -37929,7 +38502,7 @@ function readTaskLog(location, taskId, attempt) {
37929
38502
  if (!Number.isInteger(attempt) || attempt < 1) return [];
37930
38503
  let raw;
37931
38504
  try {
37932
- raw = fs22.readFileSync(path42.join(taskDir(location, taskId), `${attempt}${EXTENSION}`), "utf-8");
38505
+ raw = fs22.readFileSync(path45.join(taskDir(location, taskId), `${attempt}${EXTENSION}`), "utf-8");
37933
38506
  } catch {
37934
38507
  return [];
37935
38508
  }
@@ -39958,7 +40531,7 @@ function createSession(deps) {
39958
40531
  ...deps.openTaskLog ? {} : {
39959
40532
  keepPatch: (taskId, attempt, patch) => {
39960
40533
  const location = session.taskLogLocation;
39961
- return path43.relative(location.baseDir, saveTaskPatch(location, taskId, attempt, patch)).split(path43.sep).join("/");
40534
+ return path46.relative(location.baseDir, saveTaskPatch(location, taskId, attempt, patch)).split(path46.sep).join("/");
39962
40535
  }
39963
40536
  },
39964
40537
  recordHandoff: (sessionId, taskId, handoff) => taskLogs.note(sessionId, taskId, { type: "handoff", handoff }),
@@ -42087,6 +42660,7 @@ var StructuredSession = class extends AbstractRunnerSession {
42087
42660
  */
42088
42661
  async startAttached(opts) {
42089
42662
  const { server: server4, scope, shell, savePatch, webReads } = this.launch.tools;
42663
+ this.launch.onAttaching?.(1);
42090
42664
  const spawned = await spawnAttached({
42091
42665
  runner: this.launch.runner,
42092
42666
  create: () => this.launch.createAdapter(this.launch.runner, this.launch.deps),
@@ -42123,7 +42697,10 @@ var StructuredSession = class extends AbstractRunnerSession {
42123
42697
  },
42124
42698
  cancelled: this.ended.signal,
42125
42699
  startupExit: () => this.startupExit,
42126
- onRespawn: (failure2) => this.launch.onNotice?.(`${this.launch.runner} started without Ordewell's tools (${failure2}); respawning once.`),
42700
+ onRespawn: (failure2) => {
42701
+ this.launch.onNotice?.(`${this.launch.runner} started without Ordewell's tools (${failure2}); respawning once.`);
42702
+ this.launch.onAttaching?.(2);
42703
+ },
42127
42704
  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}.`
42128
42705
  });
42129
42706
  if (!spawned) return;
@@ -42706,16 +43283,22 @@ var StructuredRunner = class extends AbstractRunner {
42706
43283
  checkHandoff: opts.checkHandoff ?? ((completion) => checkHandoff(completion, opts.taskId, [])),
42707
43284
  ...opts.readTask ? { readTask: opts.readTask } : {},
42708
43285
  ...opts.applyPatch ? { applyPatch: opts.applyPatch } : {},
42709
- onNotice: opts.onNotice
43286
+ onNotice: opts.onNotice,
43287
+ onAttaching: opts.onAttaching
42710
43288
  });
42711
43289
  console.error(`[structured] Starting ${opts.runner} [${opts.mode ?? "default"}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}`);
42712
43290
  this.registerSession(id2, session);
43291
+ const cancel = () => void session.stop();
43292
+ opts.cancelled?.addEventListener("abort", cancel, { once: true });
42713
43293
  try {
43294
+ if (opts.cancelled?.aborted) cancel();
42714
43295
  await session.start(opts.prompt);
42715
43296
  } catch (err) {
42716
43297
  session.kill();
42717
43298
  this.sessions.delete(id2);
42718
43299
  throw err;
43300
+ } finally {
43301
+ opts.cancelled?.removeEventListener("abort", cancel);
42719
43302
  }
42720
43303
  return session;
42721
43304
  }
@@ -42789,7 +43372,7 @@ var RunnerRegistry = class {
42789
43372
 
42790
43373
  // src/plugins/removedPluginNotice.ts
42791
43374
  import * as fs23 from "fs";
42792
- import * as path44 from "path";
43375
+ import * as path47 from "path";
42793
43376
  function isManifestFile(file) {
42794
43377
  try {
42795
43378
  return fs23.statSync(file).isFile();
@@ -42798,10 +43381,10 @@ function isManifestFile(file) {
42798
43381
  }
42799
43382
  }
42800
43383
  function removedPluginNotice(dataDir = globalDataDir()) {
42801
- const directory = path44.join(dataDir, "plugins");
43384
+ const directory = path47.join(dataDir, "plugins");
42802
43385
  try {
42803
- const hasManifest = isManifestFile(path44.join(directory, "manifest.json")) || fs23.readdirSync(directory, { withFileTypes: true }).some(
42804
- (entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(path44.join(directory, entry.name, "manifest.json"))
43386
+ const hasManifest = isManifestFile(path47.join(directory, "manifest.json")) || fs23.readdirSync(directory, { withFileTypes: true }).some(
43387
+ (entry) => (entry.isDirectory() || entry.isSymbolicLink()) && isManifestFile(path47.join(directory, entry.name, "manifest.json"))
42805
43388
  );
42806
43389
  if (hasManifest) return "Plugin runners were removed; manifests in ~/.ordewell/plugins/ are ignored. See the CHANGELOG.";
42807
43390
  } catch {
@@ -42812,15 +43395,15 @@ function removedPluginNotice(dataDir = globalDataDir()) {
42812
43395
 
42813
43396
  // src/utils/stateStore.ts
42814
43397
  import * as fs24 from "fs";
42815
- import * as path45 from "path";
43398
+ import * as path48 from "path";
42816
43399
  var STATE_FILE2 = "state.json";
42817
43400
  function getStatePath(baseDir) {
42818
- return path45.join(getStateDir(baseDir), STATE_FILE2);
43401
+ return path48.join(getStateDir(baseDir), STATE_FILE2);
42819
43402
  }
42820
43403
  var SESSION_ID_KEY = "sessionId";
42821
43404
  function saveState(plan, baseDir, sessionId) {
42822
43405
  const statePath = getStatePath(baseDir ?? process.cwd());
42823
- ensureDir(path45.dirname(statePath));
43406
+ ensureDir(path48.dirname(statePath));
42824
43407
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
42825
43408
  const file = sessionId ? { ...plan, [SESSION_ID_KEY]: sessionId } : plan;
42826
43409
  dropRetiredPlanFields(file);
@@ -43106,6 +43689,7 @@ export {
43106
43689
  assignedRunnerOf,
43107
43690
  assignmentIssue,
43108
43691
  assignmentIssueText,
43692
+ attachingLabel,
43109
43693
  augmentedPath,
43110
43694
  automergeSummary,
43111
43695
  autonomyLevelLabel,
@@ -43367,6 +43951,7 @@ export {
43367
43951
  repoBaseLine,
43368
43952
  repoResultLines,
43369
43953
  reposWithWork,
43954
+ repositoryRunnerConfigPaths,
43370
43955
  researchShellWarning,
43371
43956
  researchToolsPath,
43372
43957
  resetInstalledPlanner,