@ordewell/core 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ __export(index_exports, {
67
67
  FsPluginStore: () => FsPluginStore,
68
68
  GIT_READONLY_SUBCOMMANDS: () => GIT_READONLY_SUBCOMMANDS,
69
69
  GREP_DEFAULT_HEAD_LIMIT: () => GREP_DEFAULT_HEAD_LIMIT,
70
+ GREP_PCRE_PROBE_ARGS: () => GREP_PCRE_PROBE_ARGS,
70
71
  GeminiService: () => GeminiService,
71
72
  HeadlessRunner: () => HeadlessRunner,
72
73
  HeadlessSession: () => HeadlessSession,
@@ -206,6 +207,7 @@ __export(index_exports, {
206
207
  dependencyCandidates: () => dependencyCandidates,
207
208
  dependentsOf: () => dependentsOf,
208
209
  describeMergeResult: () => describeMergeResult,
210
+ digestTaskLog: () => digestTaskLog,
209
211
  discoverGeminiModels: () => discoverGeminiModels,
210
212
  drainNext: () => drainNext,
211
213
  effectiveAllowlist: () => effectiveAllowlist,
@@ -238,6 +240,7 @@ __export(index_exports, {
238
240
  getStateDir: () => getStateDir,
239
241
  globalDataDir: () => globalDataDir,
240
242
  grantScopeFor: () => grantScopeFor,
243
+ grepRegexFromProbe: () => grepRegexFromProbe,
241
244
  hasHiddenDetail: () => hasHiddenDetail,
242
245
  hasTmux: () => hasTmux,
243
246
  holdPrompt: () => holdPrompt,
@@ -766,6 +769,8 @@ function plannerContextFill(usage) {
766
769
  var HEAD_LINES = 60;
767
770
  var TAIL_LINES = 40;
768
771
  var MAX_LINE_CHARS = 2e3;
772
+ var DIGEST_ARGS_CHARS = 120;
773
+ var DIGEST_MESSAGE_CHARS = 1e3;
769
774
  function capLine(line) {
770
775
  return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
771
776
  }
@@ -848,6 +853,33 @@ function coalesceTaskLog(events) {
848
853
  }
849
854
  return out;
850
855
  }
856
+ function digestTaskLog(events, maxCalls) {
857
+ const outcomes = /* @__PURE__ */ new Map();
858
+ let lastText;
859
+ let lastError;
860
+ for (const event of events) {
861
+ if (event.type === "tool_result") outcomes.set(event.id, event.success);
862
+ else if (event.type === "text") lastText = event.text;
863
+ else if (event.type === "error") lastError = event.message;
864
+ }
865
+ const lines = [];
866
+ for (const event of events) {
867
+ if (event.type !== "tool_call") continue;
868
+ const outcome = outcomes.get(event.id);
869
+ const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
870
+ lines.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
871
+ }
872
+ const omitted = lines.length - maxCalls;
873
+ const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines.slice(omitted)] : lines;
874
+ const gap = () => out.length > 0 ? [""] : [];
875
+ const said2 = lastText?.trim();
876
+ if (said2) {
877
+ const end = said2.length > DIGEST_MESSAGE_CHARS ? `\u2026${said2.slice(-DIGEST_MESSAGE_CHARS)}` : said2;
878
+ out.push(...gap(), "Its last message:", ...end.split("\n").map((l) => ` ${l}`));
879
+ }
880
+ if (lastError) out.push(...gap(), `It stopped with an error: ${lastError}`);
881
+ return out.join("\n");
882
+ }
851
883
 
852
884
  // src/interfaces/IFileSystem.ts
853
885
  var GREP_DEFAULT_HEAD_LIMIT = 100;
@@ -3085,6 +3117,10 @@ function lex(command, nested, dialect) {
3085
3117
  let unsafeRedirect;
3086
3118
  let processSubstitution = false;
3087
3119
  let unbalanced = false;
3120
+ let grouped = false;
3121
+ let joinedBy;
3122
+ let followable = true;
3123
+ let bareSegment = false;
3088
3124
  let tokens = [];
3089
3125
  let expandable = false;
3090
3126
  let current = "";
@@ -3114,13 +3150,25 @@ function lex(command, nested, dialect) {
3114
3150
  current = "";
3115
3151
  started = false;
3116
3152
  };
3117
- const endSegment = (nextPiped) => {
3153
+ const endSegment = (nextPiped, next = "other", last = false) => {
3118
3154
  endToken();
3119
- if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs });
3155
+ if (!last && next === "other") followable = false;
3156
+ if (tokens.length > 0) {
3157
+ const seg = toSegment(tokens, piped, dialect);
3158
+ if (!seg.binary) {
3159
+ followable = false;
3160
+ bareSegment = true;
3161
+ }
3162
+ segments.push({ ...seg, expandable, inputs, joinedBy });
3163
+ } else if (!last || segments.length > 0 || joinedBy !== void 0) {
3164
+ followable = false;
3165
+ bareSegment = true;
3166
+ }
3120
3167
  tokens = [];
3121
3168
  inputs = [];
3122
3169
  expandable = false;
3123
3170
  piped = nextPiped;
3171
+ joinedBy = next;
3124
3172
  };
3125
3173
  let i = 0;
3126
3174
  while (i < command.length) {
@@ -3242,17 +3290,18 @@ function lex(command, nested, dialect) {
3242
3290
  }
3243
3291
  if (c === "(" || c === ")") {
3244
3292
  endToken();
3293
+ grouped = true;
3245
3294
  i++;
3246
3295
  continue;
3247
3296
  }
3248
3297
  if (c === "|") {
3249
3298
  const double = command[i + 1] === "|";
3250
- endSegment(!double);
3299
+ endSegment(!double, double ? "other" : "pipe");
3251
3300
  i += double || command[i + 1] === "&" ? 2 : 1;
3252
3301
  continue;
3253
3302
  }
3254
3303
  if (c === "&" || c === ";" || c === "\n") {
3255
- endSegment(false);
3304
+ endSegment(false, c === "&" && command[i + 1] === "&" ? "and" : "other");
3256
3305
  i += command[i + 1] === c ? 2 : 1;
3257
3306
  continue;
3258
3307
  }
@@ -3269,8 +3318,8 @@ function lex(command, nested, dialect) {
3269
3318
  i++;
3270
3319
  }
3271
3320
  if (quote !== "") unbalanced = true;
3272
- endSegment(false);
3273
- return { segments, unsafeRedirect, processSubstitution, unbalanced };
3321
+ endSegment(false, "other", true);
3322
+ return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped, followable, bareSegment };
3274
3323
  }
3275
3324
  function binaryName(token, dialect) {
3276
3325
  const base = token.split(/[/\\]/).pop() ?? "";
@@ -3410,10 +3459,12 @@ function unwrap(seg, dialect) {
3410
3459
  }
3411
3460
  return { seg: { ...current, assignments }, wrappers, runner };
3412
3461
  }
3413
- function lexAll(command, dialect) {
3462
+ function lexAll(command, dialect, cdpathSet = false) {
3414
3463
  const queue = [];
3415
3464
  const top = lex(command, queue, dialect);
3416
3465
  const all = { ...top, segments: top.segments.filter((s) => s.binary) };
3466
+ markShellState(all.segments, top.bareSegment);
3467
+ if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
3417
3468
  for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
3418
3469
  const inner = lex(queue.shift(), queue, dialect);
3419
3470
  all.segments.push(...inner.segments.filter((s) => s.binary));
@@ -3423,15 +3474,61 @@ function lexAll(command, dialect) {
3423
3474
  }
3424
3475
  return all;
3425
3476
  }
3477
+ function literalCdTarget(seg) {
3478
+ if (seg.binary !== "cd" || seg.assignments.length > 0 || seg.expandable || seg.inputs.length > 0) return void 0;
3479
+ if (seg.args.length !== 1) return void 0;
3480
+ const [target] = seg.args;
3481
+ return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
3482
+ }
3483
+ function followCd(segments) {
3484
+ if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
3485
+ const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
3486
+ if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
3487
+ let chain = [];
3488
+ segments.forEach((seg, i) => {
3489
+ seg.cwd = chain;
3490
+ const target = inPipeline(i) ? void 0 : literalCdTarget(seg);
3491
+ if (target !== void 0) chain = [...chain, target];
3492
+ });
3493
+ }
3494
+ function markShellState(segments, bareSegment) {
3495
+ if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
3496
+ for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
3497
+ }
3498
+ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
3499
+ "export",
3500
+ "set",
3501
+ "shopt",
3502
+ "declare",
3503
+ "typeset",
3504
+ "readonly",
3505
+ "local",
3506
+ "unset",
3507
+ "let",
3508
+ "source",
3509
+ ".",
3510
+ "eval",
3511
+ "exec",
3512
+ "pushd",
3513
+ "popd",
3514
+ "alias",
3515
+ "unalias",
3516
+ "builtin",
3517
+ "command",
3518
+ "enable",
3519
+ "hash",
3520
+ "trap"
3521
+ ]);
3426
3522
  function looksLikePath(arg) {
3427
3523
  if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
3428
3524
  return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
3429
3525
  }
3430
- function pathLikeArgs(command, opts = {}) {
3526
+ function pathRefs(command, opts = {}) {
3431
3527
  const dialect = dialectFor(opts.dialect);
3432
- return lexAll(command, dialect).segments.flatMap((seg) => {
3528
+ return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
3433
3529
  const patterns = patternArgs(seg);
3434
- return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn);
3530
+ const cwd = seg.cwd ?? [];
3531
+ return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path27) => ({ path: path27, cwd }));
3435
3532
  });
3436
3533
  }
3437
3534
  function pathIn(a) {
@@ -3595,7 +3692,8 @@ function scopeFor(seg) {
3595
3692
  }
3596
3693
  return [seg.binary, ...lead].join(" ");
3597
3694
  }
3598
- function isAuto(seg) {
3695
+ function isAuto(seg, opts) {
3696
+ if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
3599
3697
  if (!AUTO_COMMANDS.includes(seg.binary)) return false;
3600
3698
  if (seg.expandable) return false;
3601
3699
  if (seg.binary !== "git") return true;
@@ -3690,7 +3788,7 @@ function classifyCommand(command, opts = {}) {
3690
3788
  const trimmed = command.trim();
3691
3789
  if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
3692
3790
  const dialect = dialectFor(opts.dialect);
3693
- const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect);
3791
+ const { segments, unsafeRedirect, processSubstitution, unbalanced } = lexAll(trimmed, dialect, opts.cdpathSet);
3694
3792
  if (unbalanced) {
3695
3793
  return {
3696
3794
  tier: "refuse",
@@ -3727,7 +3825,7 @@ function classifyCommand(command, opts = {}) {
3727
3825
  };
3728
3826
  }
3729
3827
  }
3730
- const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg));
3828
+ const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
3731
3829
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
3732
3830
  const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
3733
3831
  return { tier: "ask", scope };
@@ -3797,6 +3895,9 @@ function researchShellWarning(shell) {
3797
3895
  if (shell.dialect !== "cmd") return null;
3798
3896
  return "No POSIX shell was found on this machine, so research commands run through cmd.exe, where most read-only tools (ls, cat, wc, head, grep, find) do not exist. Installing Git for Windows provides one and restores the full research toolset.";
3799
3897
  }
3898
+ function researchPolicyOptions(shell) {
3899
+ return { dialect: shell.dialect, cdpathSet: !!process.env.CDPATH };
3900
+ }
3800
3901
 
3801
3902
  // src/services/pathScope.ts
3802
3903
  var os = __toESM(require("os"));
@@ -3807,11 +3908,15 @@ function expandHome(target) {
3807
3908
  if (target === "~" || target.startsWith("~/")) return path2.join(home, target.slice(1));
3808
3909
  return path2.join(path2.dirname(home), target.slice(1));
3809
3910
  }
3810
- function resolveWithin(root, target) {
3811
- const abs = path2.resolve(root, expandHome(target));
3911
+ function resolveWithin(root, target, from = root) {
3912
+ const abs = path2.resolve(from, expandHome(target));
3812
3913
  const rel = path2.relative(root, abs);
3813
3914
  return { abs, inside: rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel) };
3814
3915
  }
3916
+ function resolveRef(root, ref) {
3917
+ const from = ref.cwd.reduce((dir, target) => resolveWithin(root, target, dir).abs, root);
3918
+ return resolveWithin(root, ref.path, from);
3919
+ }
3815
3920
  function grantScopeFor(abs, kind) {
3816
3921
  if (kind === "directory") return path2.join(abs, "*");
3817
3922
  const parent = path2.dirname(abs);
@@ -4017,14 +4122,17 @@ ${refs.output.trim()}`);
4017
4122
  outsidePaths(command) {
4018
4123
  const root = this.getWorkspaceRoot();
4019
4124
  const found = /* @__PURE__ */ new Map();
4020
- for (const p of pathLikeArgs(command, { dialect: this.researchShell.dialect })) {
4021
- const { abs, inside } = resolveWithin(root, p);
4125
+ for (const ref of pathRefs(command, this.policyOptions())) {
4126
+ const { abs, inside } = resolveRef(root, ref);
4022
4127
  if (inside || isInertDevice(abs)) continue;
4023
4128
  const scope = grantScopeFor(abs, "file");
4024
4129
  if (!found.has(scope)) found.set(scope, abs);
4025
4130
  }
4026
4131
  return [...found].map(([scope, abs]) => ({ abs, scope }));
4027
4132
  }
4133
+ policyOptions() {
4134
+ return researchPolicyOptions(this.researchShell);
4135
+ }
4028
4136
  /**
4029
4137
  * Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
4030
4138
  * anything else asks once and is remembered, and writes are refused outright
@@ -4036,7 +4144,7 @@ ${refs.output.trim()}`);
4036
4144
  * directory the next command names.
4037
4145
  */
4038
4146
  async bash(command, signal) {
4039
- const { tier, scope, reason } = classifyCommand(command, { dialect: this.researchShell.dialect });
4147
+ const { tier, scope, reason } = classifyCommand(command, this.policyOptions());
4040
4148
  if (tier === "refuse") {
4041
4149
  return { success: false, output: `Command refused: ${reason}`, truncated: false };
4042
4150
  }
@@ -5212,10 +5320,78 @@ function buildGrepArgs(pattern, opts, root) {
5212
5320
  function buildGlobArgs(pattern, root) {
5213
5321
  return ["--files", ...SORT_BY_RECENCY, "--glob", pattern, ...exclusionArgs(), "--", root];
5214
5322
  }
5215
- function buildFallbackGrepArgs(pattern, opts, root) {
5323
+ var GREP_PCRE_PROBE_ARGS = ["-P", "-e", "x", "/dev/null"];
5324
+ function grepRegexFromProbe(exitCode) {
5325
+ return exitCode === 0 || exitCode === 1 ? "pcre" : "ere";
5326
+ }
5327
+ function toPosixClasses(pattern) {
5328
+ const CLASS_ESCAPES = { s: "[:space:]", w: "[:alnum:]_", d: "[:digit:]", t: " " };
5329
+ let out = "";
5330
+ let i = 0;
5331
+ while (i < pattern.length) {
5332
+ const c = pattern[i];
5333
+ if (c === "\\") {
5334
+ out += pattern.slice(i, i + 2);
5335
+ i += 2;
5336
+ continue;
5337
+ }
5338
+ if (c !== "[") {
5339
+ out += c;
5340
+ i++;
5341
+ continue;
5342
+ }
5343
+ const start = i;
5344
+ i++;
5345
+ let negate = false;
5346
+ if (pattern[i] === "^") {
5347
+ negate = true;
5348
+ i++;
5349
+ }
5350
+ let body = "";
5351
+ let closeBracket = false;
5352
+ let openBracket = false;
5353
+ let dash = false;
5354
+ let closed = false;
5355
+ while (i < pattern.length) {
5356
+ const ch = pattern[i];
5357
+ if (ch === "]") {
5358
+ closed = true;
5359
+ i++;
5360
+ break;
5361
+ }
5362
+ if (ch === "[" && pattern[i + 1] === ":") {
5363
+ const end = pattern.indexOf(":]", i + 2);
5364
+ if (end !== -1) {
5365
+ body += pattern.slice(i, end + 2);
5366
+ i = end + 2;
5367
+ continue;
5368
+ }
5369
+ }
5370
+ if (ch === "\\" && i + 1 < pattern.length) {
5371
+ const e = pattern[i + 1];
5372
+ i += 2;
5373
+ if (e in CLASS_ESCAPES) body += CLASS_ESCAPES[e];
5374
+ else if (e === "]") closeBracket = true;
5375
+ else if (e === "[") openBracket = true;
5376
+ else if (e === "-") dash = true;
5377
+ else body += e;
5378
+ continue;
5379
+ }
5380
+ body += ch;
5381
+ i++;
5382
+ }
5383
+ if (!closed) {
5384
+ out += pattern.slice(start);
5385
+ break;
5386
+ }
5387
+ out += `[${negate ? "^" : ""}${closeBracket ? "]" : ""}${body}${openBracket ? "[" : ""}${dash ? "-" : ""}]`;
5388
+ }
5389
+ return out;
5390
+ }
5391
+ function buildFallbackGrepArgs(pattern, opts, root, regex = "pcre") {
5216
5392
  const args = ["-r", "-n"];
5217
5393
  if (opts.literal) args.push("-F");
5218
- else args.push("-P");
5394
+ else args.push(regex === "pcre" ? "-P" : "-E");
5219
5395
  if (opts.caseInsensitive) args.push("-i");
5220
5396
  if (opts.outputMode === "files") args.push("-l");
5221
5397
  if (opts.outputMode === "count") args.push("-c");
@@ -5225,7 +5401,7 @@ function buildFallbackGrepArgs(pattern, opts, root) {
5225
5401
  }
5226
5402
  if (opts.include) args.push(`--include=${opts.include}`);
5227
5403
  for (const dir of SEARCH_EXCLUSIONS) args.push(`--exclude-dir=${dir}`);
5228
- args.push("-e", pattern, root);
5404
+ args.push("-e", !opts.literal && regex === "ere" ? toPosixClasses(pattern) : pattern, root);
5229
5405
  return args;
5230
5406
  }
5231
5407
  function globSegmentToRegex(segment) {
@@ -9264,7 +9440,7 @@ function parseTaskOpsObject(json, text2) {
9264
9440
  }
9265
9441
  return ops;
9266
9442
  }
9267
- function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
9443
+ function resolveRef2(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
9268
9444
  if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
9269
9445
  const s = String(ref).trim();
9270
9446
  const ownerIdx = handleOwner.get(s);
@@ -9477,7 +9653,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9477
9653
  const bad = [];
9478
9654
  const liveFlat = flattenTasks(tasks);
9479
9655
  for (const d of Array.isArray(deps) ? deps : []) {
9480
- const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
9656
+ const r = resolveRef2(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
9481
9657
  if ("error" in r) {
9482
9658
  bad.push(`${String(d)} (${r.error})`);
9483
9659
  continue;
@@ -9494,7 +9670,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9494
9670
  const label = `op ${i + 1} (${op.op})`;
9495
9671
  switch (op.op) {
9496
9672
  case "update": {
9497
- const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9673
+ const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9498
9674
  if ("error" in ref) {
9499
9675
  errors.push(`${label}: ${ref.error}`);
9500
9676
  break;
@@ -9565,7 +9741,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9565
9741
  break;
9566
9742
  }
9567
9743
  case "remove": {
9568
- const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9744
+ const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9569
9745
  if ("error" in ref) {
9570
9746
  errors.push(`${label}: ${ref.error}`);
9571
9747
  break;
@@ -9588,7 +9764,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9588
9764
  break;
9589
9765
  }
9590
9766
  case "reorder": {
9591
- const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
9767
+ const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
9592
9768
  const badRef = refs.find((r) => "error" in r);
9593
9769
  if (badRef) {
9594
9770
  errors.push(`${label}: ${badRef.error}`);
@@ -9610,7 +9786,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9610
9786
  break;
9611
9787
  }
9612
9788
  case "merge": {
9613
- const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
9789
+ const refs = (op.taskIds ?? []).map((r) => resolveRef2(r, currentTasks, tasks, handleOwner, handleId, i));
9614
9790
  const badRef = refs.find((r) => "error" in r);
9615
9791
  if (badRef) {
9616
9792
  errors.push(`${label}: ${badRef.error}`);
@@ -9680,7 +9856,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9680
9856
  break;
9681
9857
  }
9682
9858
  case "split": {
9683
- const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
9859
+ const ref = resolveRef2(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
9684
9860
  if ("error" in ref) {
9685
9861
  errors.push(`${label}: ${ref.error}`);
9686
9862
  break;
@@ -9759,7 +9935,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
9759
9935
  break;
9760
9936
  }
9761
9937
  case "rearm": {
9762
- const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9938
+ const ref = resolveRef2(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
9763
9939
  if ("error" in ref) {
9764
9940
  errors.push(`${label}: ${ref.error}`);
9765
9941
  break;
@@ -10402,15 +10578,15 @@ function nonPromptingFs(fs16) {
10402
10578
  findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs16.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
10403
10579
  listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
10404
10580
  bash: async (command, signal) => {
10405
- const dialect = resolveResearchShell().dialect;
10406
- const { tier, reason } = classifyCommand(command, { dialect });
10581
+ const policy = researchPolicyOptions(resolveResearchShell());
10582
+ const { tier, reason } = classifyCommand(command, policy);
10407
10583
  if (tier !== "auto") {
10408
10584
  return refused2(
10409
10585
  tier === "refuse" ? `Command refused: ${reason}` : `"${command}" needs user approval, and research agents run in the background where nothing can be approved. Use the read-only research tools, and note in your digest that this command would answer the question.`
10410
10586
  );
10411
10587
  }
10412
- const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
10413
- if (escaping) return outsideRefusal(escaping);
10588
+ const escaping = pathRefs(command, policy).map((ref) => resolveRef(fs16.getWorkspaceRoot(), ref)).find((r) => !r.inside);
10589
+ if (escaping) return outsideRefusal(escaping.abs);
10414
10590
  return fs16.bash(command, signal);
10415
10591
  }
10416
10592
  };
@@ -11724,6 +11900,12 @@ var VerdictEngine = class {
11724
11900
  idleSince = /* @__PURE__ */ new Map();
11725
11901
  /** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
11726
11902
  idlePaused = /* @__PURE__ */ new Set();
11903
+ /**
11904
+ * Open tool approvals per structured task. They arrive mid-turn and leave the
11905
+ * status alone (ADR-0018, W1), but a task waiting on one is waiting on the
11906
+ * user, so it is not idle either.
11907
+ */
11908
+ openApprovals = /* @__PURE__ */ new Map();
11727
11909
  onVerdict(listener) {
11728
11910
  this.listeners.push(listener);
11729
11911
  }
@@ -11752,7 +11934,7 @@ var VerdictEngine = class {
11752
11934
  }
11753
11935
  /** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
11754
11936
  touchIdle(taskId, gen) {
11755
- if (this.idlePaused.has(taskId)) return;
11937
+ if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
11756
11938
  const existing = this.idleTimers.get(taskId);
11757
11939
  if (existing) clearTimeout(existing);
11758
11940
  if (this.idleSince.get(taskId)) {
@@ -11834,7 +12016,10 @@ ${line}
11834
12016
  });
11835
12017
  if (isStructuredSession(session)) {
11836
12018
  session.onEvent((event) => {
11837
- if (event.type === "turn_start" && this.generations.get(task.id) === gen) this.resumeIdle(task.id);
12019
+ if (this.generations.get(task.id) !== gen) return;
12020
+ if (event.type === "turn_start") this.resumeIdle(task.id);
12021
+ else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
12022
+ else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
11838
12023
  });
11839
12024
  }
11840
12025
  session.onExit((exitCode) => {
@@ -11845,6 +12030,18 @@ ${line}
11845
12030
  for (const l of this.listeners) l(task.id, verdict);
11846
12031
  });
11847
12032
  }
12033
+ approvalOpened(taskId, approvalId) {
12034
+ const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
12035
+ open.add(approvalId);
12036
+ this.openApprovals.set(taskId, open);
12037
+ this.clearIdle(taskId);
12038
+ }
12039
+ approvalClosed(taskId, approvalId, gen) {
12040
+ const open = this.openApprovals.get(taskId);
12041
+ if (!open?.delete(approvalId) || open.size) return;
12042
+ this.openApprovals.delete(taskId);
12043
+ this.touchIdle(taskId, gen);
12044
+ }
11848
12045
  /** Scan only the new text plus the unmatched carry, so a long run stays linear. */
11849
12046
  scanCheckpoints(taskId, session, text2) {
11850
12047
  const scan = (this.checkpointCarry.get(taskId) ?? "") + text2;
@@ -11866,6 +12063,7 @@ ${line}
11866
12063
  this.checkpointCarry.delete(taskId);
11867
12064
  this.pausedSessions.delete(taskId);
11868
12065
  this.idlePaused.delete(taskId);
12066
+ this.openApprovals.delete(taskId);
11869
12067
  this.clearIdle(taskId);
11870
12068
  }
11871
12069
  /** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
@@ -11903,6 +12101,7 @@ ${line}
11903
12101
  this.idleTimers.clear();
11904
12102
  this.idleSince.clear();
11905
12103
  this.idlePaused.clear();
12104
+ this.openApprovals.clear();
11906
12105
  this.generations.clear();
11907
12106
  }
11908
12107
  decide(task, exitCode) {
@@ -13424,11 +13623,17 @@ function buildShellInvocation(command, args) {
13424
13623
  const inner = [command, ...args].map(posixShellQuote).join(" ");
13425
13624
  return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
13426
13625
  }
13626
+ var BSD_SCRIPT_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "freebsd", "openbsd", "netbsd"]);
13627
+ var BSD_SCRIPT_FEED = 'exec 5<&0; p=$(mktemp) || exit 1; { cat <&5 & echo $! >"$p"; wait; } 2>/dev/null | { script -q -e -F /dev/null /bin/sh -c "$0"; s=$?; kill "$(cat "$p")" 2>/dev/null; rm -f "$p"; exit $s; }';
13427
13628
  function wrapWithPty(command, args, opts = {}) {
13428
13629
  const inner = [command, ...args].map(posixShellQuote).join(" ");
13429
13630
  const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
13430
13631
  const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
13431
- return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
13632
+ const line = `${watcher}${setup}${inner}`;
13633
+ if (BSD_SCRIPT_PLATFORMS.has(opts.platform ?? process.platform)) {
13634
+ return { command: "/bin/sh", args: ["-c", BSD_SCRIPT_FEED, line] };
13635
+ }
13636
+ return { command: "script", args: ["-q", "-e", "-f", "-c", line, "/dev/null"] };
13432
13637
  }
13433
13638
 
13434
13639
  // src/services/blockingPrompts.ts
@@ -13623,24 +13828,37 @@ var Block = /* @__PURE__ */ ((Block2) => {
13623
13828
  Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
13624
13829
  Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
13625
13830
  Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
13831
+ Block2[Block2["IfHeadlessModel"] = 11] = "IfHeadlessModel";
13626
13832
  return Block2;
13627
13833
  })(Block || {});
13628
13834
  function isInteractive(ctx) {
13629
13835
  return ctx.interactive ?? !ctx.headless;
13630
13836
  }
13837
+ function majorVersion(version) {
13838
+ const match = version?.match(/(\d+)\.\d+/);
13839
+ return match ? Number(match[1]) : void 0;
13840
+ }
13841
+ function versionedInvocation(manifest, ctx) {
13842
+ const major = majorVersion(ctx.runnerVersion);
13843
+ if (major === void 0) return void 0;
13844
+ return (manifest.runner.versioned ?? []).filter((v) => major >= v.minMajor).sort((a, b) => b.minMajor - a.minMajor)[0];
13845
+ }
13631
13846
  function resolveArgs(manifest, ctx) {
13632
13847
  const args = [];
13633
13848
  const env = {};
13634
- if (manifest.runner.env) {
13635
- for (const [key, value] of Object.entries(manifest.runner.env)) {
13849
+ const versioned = versionedInvocation(manifest, ctx);
13850
+ const argsTemplate = versioned?.argsTemplate ?? manifest.runner.argsTemplate;
13851
+ const envTemplate = versioned ? versioned.env : manifest.runner.env;
13852
+ if (envTemplate) {
13853
+ for (const [key, value] of Object.entries(envTemplate)) {
13636
13854
  const resolved = substituteSimple(value, ctx);
13637
13855
  if (resolved.length > 0) env[key] = resolved;
13638
13856
  }
13639
13857
  }
13640
13858
  let block = 0 /* None */;
13641
13859
  let include = true;
13642
- for (let i = 0; i < manifest.runner.argsTemplate.length; i++) {
13643
- const raw = manifest.runner.argsTemplate[i];
13860
+ for (let i = 0; i < argsTemplate.length; i++) {
13861
+ const raw = argsTemplate[i];
13644
13862
  const blockOpen = parseBlockOpen(raw);
13645
13863
  if (blockOpen !== null) {
13646
13864
  if (block !== 0 /* None */) throw new ResolveError(`Nested conditional block at position ${i}: "${raw}"`);
@@ -13699,6 +13917,8 @@ function parseBlockOpen(raw) {
13699
13917
  return 9 /* IfInteractiveVariant */;
13700
13918
  case "{{if projectTrust}}":
13701
13919
  return 10 /* IfProjectTrust */;
13920
+ case "{{if headlessModel}}":
13921
+ return 11 /* IfHeadlessModel */;
13702
13922
  default:
13703
13923
  return null;
13704
13924
  }
@@ -13738,6 +13958,9 @@ function shouldIncludeBlock(block, ctx) {
13738
13958
  // there is no cwd to name.
13739
13959
  case 10 /* IfProjectTrust */:
13740
13960
  return isInteractive(ctx) && !!ctx.cwd;
13961
+ // A model flag that only the non-interactive subcommand takes.
13962
+ case 11 /* IfHeadlessModel */:
13963
+ return !!ctx.model && !isInteractive(ctx);
13741
13964
  default:
13742
13965
  return true;
13743
13966
  }
@@ -13783,6 +14006,10 @@ function resolveToken(token, manifest, ctx) {
13783
14006
  if (token === "{{model}}") return ctx.model || "";
13784
14007
  if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
13785
14008
  if (token === "{{mode}}") return ctx.mode || "build";
14009
+ if (token === "{{modelWithVariant}}") {
14010
+ if (!ctx.model) return "";
14011
+ return ctx.thinkingEffort ? `${ctx.model}#${ctx.thinkingEffort}` : ctx.model;
14012
+ }
13786
14013
  if (token === "{{projectTrust}}") {
13787
14014
  if (!ctx.cwd) return "";
13788
14015
  return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
@@ -13857,13 +14084,21 @@ function resolveOpencodeVariantConfig(ctx) {
13857
14084
  }
13858
14085
  return JSON.stringify(config);
13859
14086
  }
14087
+ function resolveOpencodeSessionConfig(ctx) {
14088
+ const mode = ctx.mode || "build";
14089
+ const pinned = resolveOpencodeVariantConfig(ctx);
14090
+ const config = pinned ? JSON.parse(pinned) : {};
14091
+ config.default_agent = mode;
14092
+ if (ctx.model && !pinned) config.agent = { [mode]: { model: ctx.model } };
14093
+ return JSON.stringify(config);
14094
+ }
13860
14095
  function substituteSimple(value, ctx) {
13861
14096
  const conditioned = value.replace(/\{\{if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, name, body) => {
13862
14097
  const block = parseBlockOpen(`{{if ${name}}}`);
13863
14098
  if (block === null) return match;
13864
14099
  return shouldIncludeBlock(block, ctx) ? body : "";
13865
14100
  });
13866
- return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
14101
+ return conditioned.replace(/\{\{opencodeVariantConfig\}\}/g, () => resolveOpencodeVariantConfig(ctx)).replace(/\{\{opencodeSessionConfig\}\}/g, () => resolveOpencodeSessionConfig(ctx)).replace(/\{\{prompt\}\}/g, ctx.prompt).replace(/\{\{model\}\}/g, ctx.model || "").replace(/\{\{thinkingEffort\}\}/g, ctx.thinkingEffort || "").replace(/\{\{mode\}\}/g, ctx.mode || "build");
13867
14102
  }
13868
14103
 
13869
14104
  // src/utils/launch.ts
@@ -15566,9 +15801,502 @@ ${item.text}` : item.text });
15566
15801
 
15567
15802
  // src/services/harness/OpenCodeAdapter.ts
15568
15803
  var import_crypto2 = require("crypto");
15804
+
15805
+ // src/services/harness/OpenCodeV2.ts
15806
+ var STREAM_CONNECT_TIMEOUT_MS = 5e3;
15807
+ var ACTIVE_POLL_INTERVAL_MS = 1e3;
15808
+ var IDLE_POLLS_BEFORE_START = 3;
15809
+ var PLANNER_RULES = [
15810
+ { action: "question", resource: "*", effect: "deny" },
15811
+ { action: "edit", resource: "*", effect: "deny" }
15812
+ ];
15813
+ var TASK_RULES = [{ action: "question", resource: "*", effect: "deny" }];
15814
+ var AUTO_APPROVALS = "auto";
15815
+ function newTurnState() {
15816
+ return { seen: /* @__PURE__ */ new Set(), toolNames: /* @__PURE__ */ new Map(), stepModels: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), briefs: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map(), streamedMessages: /* @__PURE__ */ new Set() };
15817
+ }
15818
+ function splitModelId(id) {
15819
+ const slash = id.indexOf("/");
15820
+ if (slash <= 0 || slash === id.length - 1) return null;
15821
+ return { providerID: id.slice(0, slash), id: id.slice(slash + 1) };
15822
+ }
15823
+ function delay(ms, signal) {
15824
+ return new Promise((resolve7) => {
15825
+ const timer = setTimeout(resolve7, ms);
15826
+ timer.unref?.();
15827
+ signal?.addEventListener("abort", () => {
15828
+ clearTimeout(timer);
15829
+ resolve7();
15830
+ }, { once: true });
15831
+ });
15832
+ }
15833
+ function contentText(content) {
15834
+ return (content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
15835
+ }
15836
+ function subagentDigest(output) {
15837
+ const inner = output.match(/<subagent[^>]*>\n?([\s\S]*?)\n?<\/subagent>/);
15838
+ return inner ? inner[1] : output;
15839
+ }
15840
+ function permissionReply(decision) {
15841
+ if (decision.decision === "allow") return { decision: "once" };
15842
+ if (decision.decision === "allowForTask") return { decision: "always" };
15843
+ const note = decision.note?.trim();
15844
+ return note ? { decision: "reject", message: note } : { decision: "reject" };
15845
+ }
15846
+ var OpenCodeV2 = class {
15847
+ constructor(host, opts, role) {
15848
+ this.host = host;
15849
+ this.opts = opts;
15850
+ this.role = role;
15851
+ this.agent = opts.kind === "task" ? opts.flags.permissionMode : "plan";
15852
+ const split = opts.model ? splitModelId(opts.model) : null;
15853
+ const variant = opts.kind === "task" ? opts.flags.effort : opts.effort;
15854
+ if (split) this.model = { ...split, ...variant ? { variant } : {} };
15855
+ }
15856
+ host;
15857
+ opts;
15858
+ role;
15859
+ sessionId = null;
15860
+ model = null;
15861
+ agent;
15862
+ turn = null;
15863
+ turnHasText = false;
15864
+ interruptRequested = false;
15865
+ /** A task's requests waiting for an answer, by request id, with the session that asked. */
15866
+ openPermissions = /* @__PURE__ */ new Map();
15867
+ nativeSessionId() {
15868
+ return this.sessionId;
15869
+ }
15870
+ async start() {
15871
+ const resume = this.opts.resumeSessionId;
15872
+ if (resume) {
15873
+ const existing = await this.host.json("GET", `/api/session/${resume}`).catch(() => null);
15874
+ if (existing?.data?.id) {
15875
+ this.sessionId = existing.data.id;
15876
+ if (this.model) await this.host.request("POST", `/api/session/${this.sessionId}/model`, { model: this.model }).catch(() => void 0);
15877
+ await this.host.request("POST", `/api/session/${this.sessionId}/agent`, { agent: this.agent }).catch(() => void 0);
15878
+ await this.applyInstructions();
15879
+ return;
15880
+ }
15881
+ if (this.opts.kind === "task") throw new Error(`OpenCode could not resume session ${resume}: the server does not know it.`);
15882
+ }
15883
+ const created = await this.host.json("POST", "/api/session", {
15884
+ agent: this.agent,
15885
+ ...this.model ? { model: this.model } : {},
15886
+ permissions: this.role === "planner" ? PLANNER_RULES : TASK_RULES
15887
+ });
15888
+ if (!created?.data?.id) throw new Error(`The OpenCode ${this.role} server did not return a session id.`);
15889
+ this.sessionId = created.data.id;
15890
+ await this.applyInstructions();
15891
+ }
15892
+ /** The planner's system prompt rides on the session as an instruction entry — a prompt has no system field. */
15893
+ async applyInstructions() {
15894
+ if (this.opts.kind !== "planner" || !this.opts.systemPrompt) return;
15895
+ const response = await this.host.request("PUT", `/api/experimental/session/${this.sessionId}/instructions/entries/ordewell-planner`, { value: this.opts.systemPrompt });
15896
+ if (!response.ok) throw new Error(`OpenCode did not take the planner instructions: ${response.status} ${response.statusText}`);
15897
+ }
15898
+ async send(message, onEvent, signal, onActivity) {
15899
+ if (!this.sessionId) throw new Error(`OpenCode ${this.role} session is not started`);
15900
+ if (this.host.isExited()) {
15901
+ onEvent({ type: "error", message: this.host.exitMessage() });
15902
+ return;
15903
+ }
15904
+ const state = newTurnState();
15905
+ this.turnHasText = false;
15906
+ this.interruptRequested = false;
15907
+ let markDone = () => {
15908
+ };
15909
+ const ended = new Promise((resolve7) => {
15910
+ markDone = resolve7;
15911
+ });
15912
+ const turn = { live: false, done: false, outcome: null, failure: null, ended, finish: () => {
15913
+ turn.done = true;
15914
+ markDone();
15915
+ } };
15916
+ this.turn = turn;
15917
+ const closeStream = await this.openStream(state, turn, onEvent, onActivity);
15918
+ const poll = new AbortController();
15919
+ try {
15920
+ try {
15921
+ await this.host.json("POST", `/api/session/${this.sessionId}/prompt`, { text: message }, signal);
15922
+ } catch (err) {
15923
+ if (signal?.aborted) return;
15924
+ onEvent({ type: "error", message: `OpenCode did not take the message: ${err instanceof Error ? err.message : String(err)}` });
15925
+ return;
15926
+ }
15927
+ void this.pollActive(turn, poll.signal);
15928
+ const aborted = new Promise((resolve7) => {
15929
+ if (signal?.aborted) resolve7();
15930
+ signal?.addEventListener("abort", () => resolve7(), { once: true });
15931
+ });
15932
+ await Promise.race([turn.ended, aborted, this.host.processEnded]);
15933
+ if (signal?.aborted) return;
15934
+ if (!turn.done) {
15935
+ onEvent({ type: "error", message: this.host.exitMessage() });
15936
+ return;
15937
+ }
15938
+ await this.readBack(state, onEvent);
15939
+ for (const id of [...this.openPermissions.keys()]) {
15940
+ this.openPermissions.delete(id);
15941
+ onEvent({ type: "permission_cancelled", id });
15942
+ }
15943
+ if (turn.outcome === "interrupted" || this.interruptRequested) onEvent({ type: "turn_end", interrupted: true });
15944
+ else if (turn.outcome === "failed") onEvent({ type: "error", message: turn.failure ?? "OpenCode reported that the turn failed." });
15945
+ else onEvent({ type: "turn_end" });
15946
+ } finally {
15947
+ this.turn = null;
15948
+ poll.abort();
15949
+ await closeStream();
15950
+ }
15951
+ }
15952
+ /**
15953
+ * The stream can drop the end frame while still delivering the rest, so the
15954
+ * turn also asks which sessions are running. A session that is not, after
15955
+ * this turn's work was seen, is over; one never seen working after a few
15956
+ * polls had already finished before the stream connected.
15957
+ */
15958
+ async pollActive(turn, signal) {
15959
+ let idlePolls = 0;
15960
+ while (!turn.done && !signal.aborted) {
15961
+ await delay(ACTIVE_POLL_INTERVAL_MS, signal);
15962
+ if (turn.done || signal.aborted) return;
15963
+ const active = await this.host.json("GET", "/api/session/active").catch(() => null);
15964
+ if (!active?.data) continue;
15965
+ if (this.sessionId && this.sessionId in active.data) {
15966
+ turn.live = true;
15967
+ idlePolls = 0;
15968
+ continue;
15969
+ }
15970
+ idlePolls++;
15971
+ if (turn.live || idlePolls >= IDLE_POLLS_BEFORE_START) {
15972
+ const messages = await this.messages();
15973
+ const idle = [...messages ?? []].reverse().find((m) => m.type === "idle");
15974
+ if (idle?.outcome === "failed" || idle?.outcome === "interrupted" || idle?.outcome === "succeeded") turn.outcome ??= idle.outcome;
15975
+ turn.outcome ??= "succeeded";
15976
+ if (!turn.done) turn.finish();
15977
+ }
15978
+ }
15979
+ }
15980
+ async messages() {
15981
+ const response = await this.host.json("GET", `/api/session/${this.sessionId}/message`).catch(() => null);
15982
+ return Array.isArray(response?.data) ? response.data : null;
15983
+ }
15984
+ /**
15985
+ * Whatever of the turn's own replies the stream missed. A message whose text
15986
+ * the stream delivered adds nothing here; one that lost its text frames still
15987
+ * reaches the plain-text channel, where the done marker is looked for.
15988
+ */
15989
+ async readBack(state, onEvent) {
15990
+ const messages = await this.messages();
15991
+ if (!messages) return;
15992
+ const latestUser = messages.findIndex((m) => m.type === "user");
15993
+ const own = latestUser < 0 ? messages : messages.slice(0, latestUser);
15994
+ for (const message of [...own].reverse()) {
15995
+ if (message.type !== "assistant" || !message.id || state.streamedMessages.has(message.id)) continue;
15996
+ (message.content ?? []).forEach((part, index) => {
15997
+ if (part.type === "text" && part.text) this.emitText(`${message.id}:readback:${index}`, part.text, state, onEvent);
15998
+ });
15999
+ }
16000
+ }
16001
+ /**
16002
+ * Abort the running turn, keeping the server and its session. OpenCode
16003
+ * acknowledges with `session.execution.interrupted`, which also ends the turn.
16004
+ */
16005
+ async interrupt(timeoutMs) {
16006
+ if (!this.sessionId || this.host.isExited()) return false;
16007
+ const turn = this.turn;
16008
+ this.interruptRequested = true;
16009
+ const response = await this.host.json("POST", `/api/session/${this.sessionId}/interrupt`).catch(() => null);
16010
+ if (!response) return false;
16011
+ if (!turn || turn.done) return true;
16012
+ let timer;
16013
+ const timedOut = new Promise((resolve7) => {
16014
+ timer = setTimeout(() => resolve7(false), timeoutMs);
16015
+ timer.unref?.();
16016
+ });
16017
+ const acknowledged = await Promise.race([turn.ended.then(() => true), this.host.processEnded.then(() => false), timedOut]);
16018
+ clearTimeout(timer);
16019
+ return acknowledged;
16020
+ }
16021
+ answerPermission(id, decision) {
16022
+ const sessionId = this.openPermissions.get(id);
16023
+ if (sessionId === void 0) return false;
16024
+ this.openPermissions.delete(id);
16025
+ void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
16026
+ });
16027
+ return true;
16028
+ }
16029
+ async replyPermission(id, sessionId, reply) {
16030
+ await this.host.request("POST", `/api/session/${sessionId}/permission/${id}/reply`, reply);
16031
+ }
16032
+ async openStream(state, turn, onEvent, onActivity) {
16033
+ const streamAbort = new AbortController();
16034
+ let connected = () => {
16035
+ };
16036
+ const ready = new Promise((resolve7) => {
16037
+ connected = resolve7;
16038
+ });
16039
+ const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, state, turn, onEvent), connected, onActivity);
16040
+ await Promise.race([ready, new Promise((r) => {
16041
+ const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
16042
+ t.unref?.();
16043
+ })]);
16044
+ return async () => {
16045
+ streamAbort.abort();
16046
+ await live.catch(() => {
16047
+ });
16048
+ };
16049
+ }
16050
+ async streamEvents(signal, onFrame, onConnected, onActivity) {
16051
+ const response = await this.host.openEvents(signal).catch(() => null);
16052
+ const body = response?.body;
16053
+ if (!body) {
16054
+ onConnected();
16055
+ return;
16056
+ }
16057
+ onConnected();
16058
+ const reader = body.getReader();
16059
+ const decoder = new TextDecoder();
16060
+ let buffer = "";
16061
+ for (; ; ) {
16062
+ const { done, value } = await reader.read().catch(() => ({ done: true, value: void 0 }));
16063
+ if (done) return;
16064
+ onActivity?.();
16065
+ buffer += decoder.decode(value, { stream: true });
16066
+ let newline;
16067
+ while ((newline = buffer.indexOf("\n")) >= 0) {
16068
+ const line = buffer.slice(0, newline).trim();
16069
+ buffer = buffer.slice(newline + 1);
16070
+ if (!line.startsWith("data:")) continue;
16071
+ try {
16072
+ onFrame(JSON.parse(line.slice(5).trim()));
16073
+ } catch {
16074
+ }
16075
+ }
16076
+ }
16077
+ }
16078
+ /**
16079
+ * One frame. Only this session and its children are followed: the stream is
16080
+ * the whole server's, and another client's session is none of this turn's business.
16081
+ */
16082
+ onFrame(frame, state, turn, onEvent) {
16083
+ const data = frame.data;
16084
+ if (!data || !frame.type) return;
16085
+ if (frame.type === "session.created") {
16086
+ if (data.parentID && data.parentID === this.sessionId && data.sessionID && !state.children.has(data.sessionID)) state.children.set(data.sessionID, null);
16087
+ return;
16088
+ }
16089
+ const session = data.sessionID;
16090
+ if (!session) return;
16091
+ const isChild = session !== this.sessionId;
16092
+ if (isChild && !state.children.has(session)) return;
16093
+ if (!isChild) {
16094
+ if (frame.type === "session.execution.started") {
16095
+ turn.live = true;
16096
+ return;
16097
+ }
16098
+ if (frame.type === "session.execution.succeeded" || frame.type === "session.execution.failed" || frame.type === "session.execution.interrupted") {
16099
+ if (!turn.live || turn.done) return;
16100
+ turn.outcome = frame.type === "session.execution.succeeded" ? "succeeded" : frame.type === "session.execution.failed" ? "failed" : "interrupted";
16101
+ if (turn.outcome === "failed") turn.failure = data.error?.message ?? data.error?.type ?? null;
16102
+ turn.finish();
16103
+ return;
16104
+ }
16105
+ }
16106
+ if (frame.type === "permission.asked") {
16107
+ if (this.role === "task") this.askPermission(data, session, state, onEvent);
16108
+ else this.denyPermission(data, session, state, onEvent);
16109
+ return;
16110
+ }
16111
+ if (frame.type === "permission.replied") {
16112
+ const id = data.requestID;
16113
+ if (id && this.openPermissions.delete(id)) onEvent({ type: "permission_cancelled", id });
16114
+ return;
16115
+ }
16116
+ let subagentId;
16117
+ if (isChild) {
16118
+ const owner = state.children.get(session);
16119
+ if (!owner) {
16120
+ state.heldFrames.set(session, [...state.heldFrames.get(session) ?? [], frame]);
16121
+ return;
16122
+ }
16123
+ subagentId = owner;
16124
+ }
16125
+ this.onContentFrame(frame, data, state, turn, onEvent, subagentId);
16126
+ }
16127
+ onContentFrame(frame, data, state, turn, onEvent, subagentId) {
16128
+ const message = data.assistantMessageID ?? "";
16129
+ switch (frame.type) {
16130
+ case "session.step.started":
16131
+ if (message && data.model?.providerID && data.model.id) state.stepModels.set(message, `${data.model.providerID}/${data.model.id}`);
16132
+ return;
16133
+ case "session.reasoning.delta":
16134
+ if (data.delta) onEvent({ type: "thinking_delta", text: data.delta, subagentId });
16135
+ return;
16136
+ case "session.reasoning.ended": {
16137
+ const key = `reasoning:${message}:${data.ordinal ?? 0}`;
16138
+ if (!data.text || state.seen.has(key)) return;
16139
+ state.seen.add(key);
16140
+ onEvent({ type: "thinking", text: data.text, subagentId });
16141
+ return;
16142
+ }
16143
+ case "session.text.delta":
16144
+ if (!subagentId && data.delta) this.onTextDelta(`${message}:${data.ordinal ?? 0}`, data.delta, state, onEvent);
16145
+ return;
16146
+ case "session.text.ended":
16147
+ if (subagentId || !data.text) return;
16148
+ if (message) state.streamedMessages.add(message);
16149
+ this.emitText(`${message}:${data.ordinal ?? 0}`, data.text, state, onEvent);
16150
+ return;
16151
+ case "session.tool.input.started":
16152
+ if (data.id && data.name) state.toolNames.set(data.id, data.name);
16153
+ return;
16154
+ case "session.tool.called": {
16155
+ const id = data.id;
16156
+ if (!id || state.seen.has(`call:${id}`)) return;
16157
+ state.seen.add(`call:${id}`);
16158
+ const input = data.input ?? {};
16159
+ const name = state.toolNames.get(id) ?? "tool";
16160
+ const brief = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
16161
+ state.briefs.set(id, brief);
16162
+ onEvent({ type: "tool_call", id, name, args: input, subagentId });
16163
+ return;
16164
+ }
16165
+ case "session.tool.progress":
16166
+ if (!subagentId) this.adoptChild(data, state, onEvent, turn);
16167
+ return;
16168
+ case "session.tool.success":
16169
+ case "session.tool.failed": {
16170
+ const id = data.id;
16171
+ if (!id || state.seen.has(`result:${id}`)) return;
16172
+ state.seen.add(`result:${id}`);
16173
+ const success = frame.type === "session.tool.success";
16174
+ const output = success ? contentText(data.content) : data.error?.message ?? data.error?.type ?? "";
16175
+ onEvent({ type: "tool_result", id, name: state.toolNames.get(id) ?? "tool", output, success, subagentId });
16176
+ if (!subagentId) this.finishChild(id, success, output, state, onEvent);
16177
+ return;
16178
+ }
16179
+ case "session.step.ended":
16180
+ case "session.step.failed":
16181
+ this.countUsage(data, state, onEvent, subagentId);
16182
+ return;
16183
+ default:
16184
+ }
16185
+ }
16186
+ adoptChild(data, state, onEvent, turn) {
16187
+ const child = data.metadata?.sessionID;
16188
+ const callId = data.id;
16189
+ if (!child || !callId || state.children.get(child)) return;
16190
+ const model = state.stepModels.get(data.assistantMessageID ?? "");
16191
+ onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
16192
+ state.children.set(child, callId);
16193
+ const held = state.heldFrames.get(child) ?? [];
16194
+ state.heldFrames.delete(child);
16195
+ for (const frame of held) this.onFrame(frame, state, turn, onEvent);
16196
+ }
16197
+ finishChild(callId, success, output, state, onEvent) {
16198
+ if (![...state.children.values()].includes(callId)) return;
16199
+ onEvent({ type: "subagent_finished", subagentId: callId, outcome: success ? "done" : "failed", digest: subagentDigest(output) });
16200
+ }
16201
+ /** One model call's usage, once, when its step ends. Zeros mean the call failed before the provider answered. */
16202
+ countUsage(data, state, onEvent, subagentId) {
16203
+ const message = data.assistantMessageID;
16204
+ const tokens = data.tokens;
16205
+ if (!message || !tokens || state.seen.has(`usage:${message}`)) return;
16206
+ state.seen.add(`usage:${message}`);
16207
+ const prompt = partedPromptUsage({ uncached: tokens.input, cacheRead: tokens.cache?.read, cacheWrite: tokens.cache?.write });
16208
+ const outputTokens = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
16209
+ if ((prompt.inputTokens ?? 0) + outputTokens === 0) return;
16210
+ const record = { source: "opencode", ...prompt, outputTokens };
16211
+ const model = state.stepModels.get(message);
16212
+ if (model) record.model = model;
16213
+ if (typeof data.cost === "number" && data.cost > 0) record.reportedCost = { amount: data.cost, currency: "USD" };
16214
+ if (subagentId) record.subagentId = subagentId;
16215
+ onEvent({ type: "usage", record });
16216
+ }
16217
+ /** One complete reply text, once. A message can carry text on both sides of a tool call, so each run after the first opens a paragraph. */
16218
+ emitText(id, text2, state, onEvent) {
16219
+ if (state.seen.has(`text:${id}`)) return;
16220
+ state.seen.add(`text:${id}`);
16221
+ if (!text2.trim()) return;
16222
+ const lead = state.textRuns.get(id)?.lead ?? (this.turnHasText ? "\n\n" : "");
16223
+ onEvent({ type: "assistant_text", text: `${lead}${text2}` });
16224
+ this.turnHasText = true;
16225
+ }
16226
+ /**
16227
+ * Stream one piece of a reply text. The paragraph break goes out with the
16228
+ * first visible delta, so the deltas add up to exactly the text the completed
16229
+ * run then re-sends; a run that is only whitespace so far is held back.
16230
+ */
16231
+ onTextDelta(id, delta, state, onEvent) {
16232
+ if (state.seen.has(`text:${id}`)) return;
16233
+ const run = state.textRuns.get(id) ?? { held: "", lead: null };
16234
+ state.textRuns.set(id, run);
16235
+ if (run.lead !== null) {
16236
+ onEvent({ type: "assistant_text_delta", text: delta });
16237
+ return;
16238
+ }
16239
+ run.held += delta;
16240
+ if (!run.held.trim()) return;
16241
+ run.lead = this.turnHasText ? "\n\n" : "";
16242
+ this.turnHasText = true;
16243
+ onEvent({ type: "assistant_text_delta", text: `${run.lead}${run.held}` });
16244
+ }
16245
+ permissionRequest(ask) {
16246
+ const id = ask.id;
16247
+ if (!id) return null;
16248
+ const scope = (ask.resources ?? []).join(", ");
16249
+ return {
16250
+ type: "permission_request",
16251
+ id,
16252
+ name: ask.action ?? "permission",
16253
+ detail: JSON.stringify(scope ? { scope } : {}),
16254
+ input: ask.resources ? { resources: ask.resources } : {},
16255
+ // The patterns `always` would grant: the runner's own offer, and the only grounds for "Allow for this task".
16256
+ ...ask.save?.length ? { suggestions: ask.save } : {},
16257
+ ...ask.source?.id ? { toolUseId: ask.source.id } : {}
16258
+ };
16259
+ }
16260
+ /**
16261
+ * Deny one planner request. OpenCode blocks the turn until it is answered, so
16262
+ * this must answer: `reject` rather than a silent drop is the "absent answer
16263
+ * is a denial" invariant ADR-0008 states. The refusal is announced so the
16264
+ * timeline shows the planner reaching for something it may not have.
16265
+ */
16266
+ denyPermission(ask, session, state, onEvent) {
16267
+ const request = this.permissionRequest(ask);
16268
+ if (!request || state.seen.has(`perm:${request.id}`)) return;
16269
+ state.seen.add(`perm:${request.id}`);
16270
+ onEvent(request);
16271
+ void this.replyPermission(request.id, session, { decision: "reject" }).catch(() => {
16272
+ });
16273
+ }
16274
+ /**
16275
+ * A task's request. Under a mode whose manifest sets `approvals: auto` it is
16276
+ * answered at once with what `opencode run --auto` answers, so the same plan
16277
+ * behaves the same on both transports (ADR-0001), and announced already
16278
+ * decided so the log still shows it. Any other mode leaves it open for an
16279
+ * approval card.
16280
+ */
16281
+ askPermission(ask, session, state, onEvent) {
16282
+ const request = this.permissionRequest(ask);
16283
+ if (!request || state.seen.has(`perm:${request.id}`)) return;
16284
+ state.seen.add(`perm:${request.id}`);
16285
+ if (this.opts.kind === "task" && this.opts.flags.modeSettings.approvals === AUTO_APPROVALS) {
16286
+ onEvent({ ...request, decided: { decision: "allow" } });
16287
+ void this.replyPermission(request.id, session, { decision: "once" }).catch(() => {
16288
+ });
16289
+ return;
16290
+ }
16291
+ this.openPermissions.set(request.id, session);
16292
+ onEvent(request);
16293
+ }
16294
+ };
16295
+
16296
+ // src/services/harness/OpenCodeAdapter.ts
15569
16297
  var SERVER_READY_TIMEOUT_MS = 3e4;
15570
16298
  var STDERR_TAIL_CHARS2 = 4e3;
15571
- var STREAM_CONNECT_TIMEOUT_MS = 5e3;
16299
+ var STREAM_CONNECT_TIMEOUT_MS2 = 5e3;
15572
16300
  var RECOVERY_POLL_INTERVAL_MS = 2e3;
15573
16301
  var RECOVERY_TIMEOUT_MS = 9e5;
15574
16302
  var STATUS_POLL_INTERVAL_MS = 1e3;
@@ -15581,8 +16309,8 @@ var DISABLED_TOOLS = {
15581
16309
  todowrite: false
15582
16310
  };
15583
16311
  var TASK_DISABLED_TOOLS = { question: false };
15584
- var AUTO_APPROVALS = "auto";
15585
- function splitModelId(id) {
16312
+ var AUTO_APPROVALS2 = "auto";
16313
+ function splitModelId2(id) {
15586
16314
  const slash = id.indexOf("/");
15587
16315
  if (slash <= 0 || slash === id.length - 1) return null;
15588
16316
  return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
@@ -15638,7 +16366,7 @@ function unwrapFileToolOutput(output) {
15638
16366
  const inner = output.match(/<content>\n?([\s\S]*?)\n?<\/content>/);
15639
16367
  return inner ? inner[1] : output;
15640
16368
  }
15641
- function delay(ms, signal) {
16369
+ function delay2(ms, signal) {
15642
16370
  return new Promise((resolve7) => {
15643
16371
  const timer = setTimeout(resolve7, ms);
15644
16372
  timer.unref?.();
@@ -15648,10 +16376,10 @@ function delay(ms, signal) {
15648
16376
  }, { once: true });
15649
16377
  });
15650
16378
  }
15651
- function newTurnState() {
16379
+ function newTurnState2() {
15652
16380
  return { seen: /* @__PURE__ */ new Set(), assistantMessages: /* @__PURE__ */ new Set(), partTypes: /* @__PURE__ */ new Map(), textRuns: /* @__PURE__ */ new Map(), children: /* @__PURE__ */ new Map(), heldFrames: /* @__PURE__ */ new Map() };
15653
16381
  }
15654
- function permissionReply(decision) {
16382
+ function permissionReply2(decision) {
15655
16383
  if (decision.decision === "allow") return { reply: "once" };
15656
16384
  if (decision.decision === "allowForTask") return { reply: "always" };
15657
16385
  const note = decision.note?.trim();
@@ -15688,6 +16416,8 @@ var OpenCodeAdapter = class {
15688
16416
  interruptRequested = false;
15689
16417
  /** A task's requests waiting for an answer, by request id, with the session that asked. */
15690
16418
  openPermissions = /* @__PURE__ */ new Map();
16419
+ /** Set once the server turns out to speak the 2.x API, which then owns the session. */
16420
+ v2 = null;
15691
16421
  async start(opts) {
15692
16422
  if (opts.kind === "task") this.task = opts;
15693
16423
  else this.planner = opts;
@@ -15702,14 +16432,14 @@ var OpenCodeAdapter = class {
15702
16432
  throw new ExecutableNotFoundError("opencode", PATH);
15703
16433
  }
15704
16434
  const workspaceEnv = await (this.deps.workspaceEnv ?? workspaceEnvOf)(opts.cwd);
15705
- const password = opts.kind === "task" ? (0, import_crypto2.randomBytes)(24).toString("base64url") : null;
16435
+ const password = (0, import_crypto2.randomBytes)(24).toString("base64url");
15706
16436
  this.process = this.deps.spawn(launch.file, launch.args, {
15707
- env: runnerEnv(PATH, password ? { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password } : workspaceEnv),
16437
+ env: runnerEnv(PATH, { ...workspaceEnv, OPENCODE_SERVER_USERNAME: SERVER_USERNAME, OPENCODE_SERVER_PASSWORD: password }),
15708
16438
  stdio: ["pipe", "pipe", "pipe"],
15709
16439
  cwd: opts.cwd,
15710
16440
  windowsVerbatimArguments: launch.verbatim
15711
16441
  });
15712
- if (password) this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
16442
+ this.authorization = `Basic ${Buffer.from(`${SERVER_USERNAME}:${password}`).toString("base64")}`;
15713
16443
  this.process.stdin?.on("error", () => {
15714
16444
  });
15715
16445
  const banner = new Promise((resolve7) => {
@@ -15746,6 +16476,24 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS2);
15746
16476
 
15747
16477
  ${this.stderrTail.trim()}` : ""}`);
15748
16478
  }
16479
+ if (await this.speaksV2()) {
16480
+ this.v2 = new OpenCodeV2({
16481
+ request: (method, path27, body, signal) => this.request(method, path27, body, signal),
16482
+ json: (method, path27, body, signal) => this.json(method, path27, body, signal),
16483
+ openEvents: (signal) => this.deps.fetch(`${this.baseUrl}/api/event`, { signal, headers: this.headers() }),
16484
+ processEnded: this.processEnded,
16485
+ isExited: () => this.exited,
16486
+ exitMessage: () => this.exitMessage()
16487
+ }, opts, this.role());
16488
+ try {
16489
+ await this.v2.start();
16490
+ } catch (err) {
16491
+ this.dispose();
16492
+ throw err;
16493
+ }
16494
+ this.sessionId = this.v2.nativeSessionId();
16495
+ return;
16496
+ }
15749
16497
  if (opts.resumeSessionId) {
15750
16498
  const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
15751
16499
  if (existing?.id) {
@@ -15769,12 +16517,17 @@ ${this.stderrTail.trim()}` : ""}`);
15769
16517
  onEvent({ type: "error", message: this.exitMessage() });
15770
16518
  return;
15771
16519
  }
16520
+ if (this.v2) {
16521
+ await this.v2.send(message, onEvent, signal, onActivity);
16522
+ if (signal?.aborted) this.dispose();
16523
+ return;
16524
+ }
15772
16525
  if (this.task) return this.sendTask(this.task, message, onEvent, signal, onActivity);
15773
- const turn = newTurnState();
16526
+ const turn = newTurnState2();
15774
16527
  this.turnHasText = false;
15775
16528
  const closeStream = await this.openStream(turn, onEvent, onActivity);
15776
16529
  try {
15777
- const model = this.planner?.model ? splitModelId(this.planner.model) : null;
16530
+ const model = this.planner?.model ? splitModelId2(this.planner.model) : null;
15778
16531
  const body = {
15779
16532
  parts: [{ type: "text", text: message }],
15780
16533
  // The read-only guarantee: OpenCode's own plan agent has no write tools.
@@ -15815,7 +16568,7 @@ ${this.stderrTail.trim()}` : ""}`);
15815
16568
  * {@link recoverReply}, never applies.
15816
16569
  */
15817
16570
  async sendTask(task, message, onEvent, signal, onActivity) {
15818
- const state = newTurnState();
16571
+ const state = newTurnState2();
15819
16572
  this.turnHasText = false;
15820
16573
  this.interruptRequested = false;
15821
16574
  let markDone = () => {
@@ -15831,7 +16584,7 @@ ${this.stderrTail.trim()}` : ""}`);
15831
16584
  const closeStream = await this.openStream(state, onEvent, onActivity);
15832
16585
  const poll = new AbortController();
15833
16586
  try {
15834
- const model = task.model ? splitModelId(task.model) : null;
16587
+ const model = task.model ? splitModelId2(task.model) : null;
15835
16588
  const body = {
15836
16589
  parts: [{ type: "text", text: message }],
15837
16590
  // The manifest's meaning of the task's mode (ADR-0001): for OpenCode a mode is an agent.
@@ -15911,7 +16664,7 @@ ${this.stderrTail.trim()}` : ""}`);
15911
16664
  */
15912
16665
  async pollStatus(turn, signal) {
15913
16666
  while (!turn.done && !signal.aborted) {
15914
- await delay(STATUS_POLL_INTERVAL_MS, signal);
16667
+ await delay2(STATUS_POLL_INTERVAL_MS, signal);
15915
16668
  if (turn.done || signal.aborted) return;
15916
16669
  const busy = await this.sessionBusy();
15917
16670
  if (busy === true) turn.live = true;
@@ -15958,6 +16711,7 @@ ${this.stderrTail.trim()}` : ""}`);
15958
16711
  */
15959
16712
  async interrupt(timeoutMs) {
15960
16713
  if (!this.process || !this.sessionId || this.exited) return false;
16714
+ if (this.v2) return this.v2.interrupt(timeoutMs);
15961
16715
  const turn = this.taskTurn;
15962
16716
  this.interruptRequested = true;
15963
16717
  const posted = await this.json("POST", `/session/${this.sessionId}/abort`).then(() => true, () => false);
@@ -15978,13 +16732,23 @@ ${this.stderrTail.trim()}` : ""}`);
15978
16732
  void this.processEnded.then(() => listener(this.exitCode));
15979
16733
  }
15980
16734
  answerPermission(id, decision) {
16735
+ if (this.v2) return this.process ? this.v2.answerPermission(id, decision) : false;
15981
16736
  const sessionId = this.openPermissions.get(id);
15982
16737
  if (sessionId === void 0 || !this.process) return false;
15983
16738
  this.openPermissions.delete(id);
15984
- void this.replyPermission(id, sessionId, permissionReply(decision)).catch(() => {
16739
+ void this.replyPermission(id, sessionId, permissionReply2(decision)).catch(() => {
15985
16740
  });
15986
16741
  return true;
15987
16742
  }
16743
+ /**
16744
+ * OpenCode 2.x replaced the 1.x HTTP surface rather than extending it, and
16745
+ * only 2.x answers `/api/info` with its version. A 1.x server — or one that
16746
+ * answers with anything else — keeps the 1.x protocol this class speaks.
16747
+ */
16748
+ async speaksV2() {
16749
+ const info = await this.json("GET", "/api/info").catch(() => null);
16750
+ return typeof info === "object" && info !== null && typeof info.version === "string";
16751
+ }
15988
16752
  role() {
15989
16753
  return this.task ? "task" : "planner";
15990
16754
  }
@@ -16035,7 +16799,7 @@ ${this.stderrTail.trim()}` : ""}`);
16035
16799
  if (pending?.info?.time?.completed || pending?.info?.error) return pending;
16036
16800
  if (pending) onActivity?.();
16037
16801
  if (Date.now() >= deadline) return null;
16038
- await delay(RECOVERY_POLL_INTERVAL_MS, signal);
16802
+ await delay2(RECOVERY_POLL_INTERVAL_MS, signal);
16039
16803
  }
16040
16804
  }
16041
16805
  newestAssistantId(messages) {
@@ -16248,7 +17012,7 @@ ${this.stderrTail.trim()}` : ""}`);
16248
17012
  ...ask.always?.length ? { suggestions: ask.always } : {},
16249
17013
  ...ask.tool?.callID ? { toolUseId: ask.tool.callID } : {}
16250
17014
  };
16251
- if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS) {
17015
+ if (this.task?.flags.modeSettings.approvals === AUTO_APPROVALS2) {
16252
17016
  onEvent({ ...request, decided: { decision: "allow" } });
16253
17017
  void this.replyPermission(id, sessionId, { reply: "once" }).catch(() => {
16254
17018
  });
@@ -16282,7 +17046,7 @@ ${this.stderrTail.trim()}` : ""}`);
16282
17046
  });
16283
17047
  const live = this.streamEvents(streamAbort.signal, (frame) => this.onFrame(frame, turn, onEvent), connected, onActivity);
16284
17048
  await Promise.race([streamReady, new Promise((r) => {
16285
- const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS);
17049
+ const t = setTimeout(r, STREAM_CONNECT_TIMEOUT_MS2);
16286
17050
  t.unref?.();
16287
17051
  })]);
16288
17052
  return async () => {
@@ -16487,6 +17251,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
16487
17251
  observers = [];
16488
17252
  tddEnabled;
16489
17253
  readRunnerTransport;
17254
+ previousAttemptFromLog;
16490
17255
  /**
16491
17256
  * The setting as the latest run copied it when it opened. Every spawn of
16492
17257
  * that run uses this, so flipping the setting mid-run changes the next run
@@ -16506,6 +17271,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
16506
17271
  this.workspaceEnv = deps.workspaceEnv;
16507
17272
  this.tddEnabled = deps.tddEnabled;
16508
17273
  this.readRunnerTransport = deps.runnerTransport;
17274
+ this.previousAttemptFromLog = deps.previousAttemptFromLog;
16509
17275
  this.store.onMutate = () => this.emit("onTaskChanged");
16510
17276
  this.verifier.onVerdict((taskId, verdict) => this.onVerdict(taskId, verdict));
16511
17277
  this.verifier.onCheckpoint((taskId, summary) => {
@@ -16561,7 +17327,8 @@ var TaskOrchestrator = class _TaskOrchestrator {
16561
17327
  workspaceRoot,
16562
17328
  workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
16563
17329
  tddEnabled: options.tddEnabled ?? (() => false),
16564
- runnerTransport: options.runnerTransport ?? (() => "terminal")
17330
+ runnerTransport: options.runnerTransport ?? (() => "terminal"),
17331
+ previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null)
16565
17332
  });
16566
17333
  return orchestrator;
16567
17334
  }
@@ -16569,6 +17336,18 @@ var TaskOrchestrator = class _TaskOrchestrator {
16569
17336
  getIdleSince(taskId) {
16570
17337
  return this.verifier.getIdleSince(taskId);
16571
17338
  }
17339
+ /**
17340
+ * What an ops retry is told about the attempt before. The saved log is read
17341
+ * whether or not the session was reloaded, so both behave alike; the terminal
17342
+ * buffer is the fallback for a runner that has no log. A reload clears the
17343
+ * attempt count along with the buffer, so a retry made since is what says
17344
+ * there was an attempt to report on.
17345
+ */
17346
+ opsPreviousAttempt(taskId) {
17347
+ const saved = this.previousAttemptFromLog(taskId) ?? this.output.liveTail(taskId, { maxLines: OPS_RETRY_TAIL_LINES })?.text;
17348
+ if (saved !== void 0) return saved;
17349
+ return (this.retryCounts.get(taskId) ?? 0) > 0 ? "Its output is not available: this session was reloaded since it ran." : void 0;
17350
+ }
16572
17351
  /** Recent clean output of a task's latest attempt, running or ended; null if it never ran. */
16573
17352
  getLiveOutput(taskId, opts) {
16574
17353
  return this.output.liveTail(taskId, opts);
@@ -17426,7 +18205,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
17426
18205
  tddEnabled: !attempt.repair && this.tddEnabled(),
17427
18206
  // An ops task's effects outlive a failed attempt and are never rolled
17428
18207
  // back, so the next one is told what the last one did (ADR-0020).
17429
- previousAttempt: attempt.ops ? this.output.liveTail(task.id, { maxLines: OPS_RETRY_TAIL_LINES })?.text : void 0
18208
+ previousAttempt: attempt.ops ? this.opsPreviousAttempt(task.id) : void 0
17430
18209
  });
17431
18210
  this.lingering.close(task.id);
17432
18211
  const env = await this.envForTask(cwd);
@@ -21107,6 +21886,13 @@ var PlanEditError = class extends Error {
21107
21886
  this.name = "PlanEditError";
21108
21887
  }
21109
21888
  };
21889
+ var OPS_RETRY_DIGEST_CALLS = 20;
21890
+ function lastAttemptDigest(location, taskId) {
21891
+ const attempt = listTaskLogAttempts(location, taskId).pop();
21892
+ if (attempt === void 0) return null;
21893
+ const events = readTaskLog(location, taskId, attempt);
21894
+ return events.length > 0 ? digestTaskLog(events, OPS_RETRY_DIGEST_CALLS) : null;
21895
+ }
21110
21896
  function sessionRuntimeSettings(settings) {
21111
21897
  return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist, runnerTransport: settings.runnerTransport };
21112
21898
  }
@@ -21191,7 +21977,8 @@ function createSession(deps) {
21191
21977
  registry: deps.registry,
21192
21978
  workspaceRoot: deps.workspaceRoot,
21193
21979
  tddEnabled: () => deps.settings().tddEnabled,
21194
- runnerTransport: () => deps.settings().runnerTransport ?? "terminal"
21980
+ runnerTransport: () => deps.settings().runnerTransport ?? "terminal",
21981
+ previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId)
21195
21982
  });
21196
21983
  const usage = new PlannerUsageLedger();
21197
21984
  const events = new SessionEventRelay({
@@ -22739,12 +23526,32 @@ function buildRunnerInvocation(opts) {
22739
23526
  mode: opts.mode ?? "build",
22740
23527
  headless: opts.headless,
22741
23528
  interactive: opts.interactive,
22742
- cwd: opts.cwd
23529
+ cwd: opts.cwd,
23530
+ runnerVersion: opts.runnerVersion
22743
23531
  });
22744
23532
  }
22745
23533
 
22746
23534
  // src/services/HeadlessRunner.ts
23535
+ var import_child_process9 = require("child_process");
23536
+
23537
+ // src/services/runnerVersion.ts
22747
23538
  var import_child_process8 = require("child_process");
23539
+ var VERSION_TIMEOUT_MS = 5e3;
23540
+ var cache = /* @__PURE__ */ new Map();
23541
+ var probeRunnerVersion = (command, PATH) => {
23542
+ const key = `${command}\0${PATH}`;
23543
+ let pending = cache.get(key);
23544
+ if (!pending) {
23545
+ pending = new Promise((resolve7) => {
23546
+ (0, import_child_process8.execFile)(command, ["--version"], { env: { ...process.env, PATH }, timeout: VERSION_TIMEOUT_MS }, (err, stdout) => {
23547
+ const line = String(stdout ?? "").trim();
23548
+ resolve7(err || !line ? void 0 : line);
23549
+ });
23550
+ });
23551
+ cache.set(key, pending);
23552
+ }
23553
+ return pending;
23554
+ };
22748
23555
 
22749
23556
  // src/services/AbstractRunner.ts
22750
23557
  var import_events = require("events");
@@ -22799,7 +23606,7 @@ var AbstractRunner = class {
22799
23606
  function defaultHasScriptCmd() {
22800
23607
  if (process.platform === "win32") return false;
22801
23608
  try {
22802
- (0, import_child_process8.execSync)("which script", { stdio: "ignore" });
23609
+ (0, import_child_process9.execSync)("which script", { stdio: "ignore" });
22803
23610
  return true;
22804
23611
  } catch {
22805
23612
  return false;
@@ -22866,6 +23673,10 @@ Process error: ${err.message}
22866
23673
  this.process.stdin.write(text2);
22867
23674
  }
22868
23675
  }
23676
+ /** Closes the runner's stdin: what it has read so far is all it will get. */
23677
+ endInput() {
23678
+ this.process?.stdin?.end();
23679
+ }
22869
23680
  /** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
22870
23681
  writeControl(text2) {
22871
23682
  if (this.controlStream && !this.controlStream.destroyed) {
@@ -22878,6 +23689,7 @@ var HeadlessRunner = class extends AbstractRunner {
22878
23689
  hasScriptCmd;
22879
23690
  resolvePath;
22880
23691
  launchDeps;
23692
+ runnerVersion;
22881
23693
  spawnCount = 0;
22882
23694
  /**
22883
23695
  * Session shape: a piped subprocess is not a terminal, so runners get their
@@ -22889,10 +23701,11 @@ var HeadlessRunner = class extends AbstractRunner {
22889
23701
  defaultInteractive = false;
22890
23702
  constructor(deps = {}) {
22891
23703
  super();
22892
- this.spawnImpl = deps.spawnImpl ?? import_child_process8.spawn;
23704
+ this.spawnImpl = deps.spawnImpl ?? import_child_process9.spawn;
22893
23705
  this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
22894
23706
  this.resolvePath = deps.resolvePath ?? augmentedPath;
22895
23707
  this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
23708
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
22896
23709
  }
22897
23710
  /**
22898
23711
  * Unique per spawn, not per task: a retry reuses its task id and ids often
@@ -22910,6 +23723,9 @@ var HeadlessRunner = class extends AbstractRunner {
22910
23723
  /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
22911
23724
  async prepareLaunch(opts, ptyOptions) {
22912
23725
  const interactive = this.defaultInteractive;
23726
+ const resolvedPath = await this.resolvePath();
23727
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
23728
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, resolvedPath) : void 0;
22913
23729
  const invocation = buildRunnerInvocation({
22914
23730
  runner: opts.runner,
22915
23731
  prompt: opts.prompt,
@@ -22920,18 +23736,18 @@ var HeadlessRunner = class extends AbstractRunner {
22920
23736
  headless: opts.headless ?? true,
22921
23737
  interactive,
22922
23738
  cwd: opts.cwd,
23739
+ runnerVersion,
22923
23740
  registry: opts.registry
22924
23741
  });
22925
- const manifest = opts.registry?.get(opts.runner)?.manifest;
22926
23742
  const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
22927
- const resolvedPath = await this.resolvePath();
22928
- const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, ptyOptions) : { command: invocation.command, args: invocation.args };
23743
+ const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, { platform: this.launchDeps.platform, ...ptyOptions }) : { command: invocation.command, args: invocation.args };
22929
23744
  return {
22930
23745
  launch: await planDirectLaunch(command, args, this.launchDeps),
22931
23746
  resolvedPath,
22932
23747
  env: { ...opts.env, ...invocation.env },
22933
23748
  pty,
22934
- submitPromptKey: invocation.submitPromptKey
23749
+ submitPromptKey: invocation.submitPromptKey,
23750
+ closeStdin: !pty && !interactive && invocation.promptInArgs
22935
23751
  };
22936
23752
  }
22937
23753
  async spawn(opts) {
@@ -22942,20 +23758,21 @@ var HeadlessRunner = class extends AbstractRunner {
22942
23758
  console.error(`[headless] Starting ${opts.runner} [${modeStr}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}${prepared.pty ? " (PTY)" : ""}`);
22943
23759
  session.start(prepared.launch, opts.cwd, prepared.resolvedPath, prepared.env);
22944
23760
  if (prepared.submitPromptKey) session.write("\r");
23761
+ if (prepared.closeStdin) session.endInput();
22945
23762
  this.registerSession(id, session);
22946
23763
  return session;
22947
23764
  }
22948
23765
  };
22949
23766
 
22950
23767
  // src/services/TmuxRunner.ts
22951
- var import_child_process10 = require("child_process");
23768
+ var import_child_process11 = require("child_process");
22952
23769
  var import_util5 = require("util");
22953
23770
  var import_os = require("os");
22954
23771
  var import_path2 = require("path");
22955
23772
  var import_fs3 = require("fs");
22956
23773
 
22957
23774
  // src/utils/tmux.ts
22958
- var import_child_process9 = require("child_process");
23775
+ var import_child_process10 = require("child_process");
22959
23776
  function tmuxSessionName(port) {
22960
23777
  return `ordewell-${port}`;
22961
23778
  }
@@ -22968,7 +23785,7 @@ function tmuxWindowName(taskId, planSessionId) {
22968
23785
  return `t-${scope}${slug(taskId).slice(0, 16)}`;
22969
23786
  }
22970
23787
  var defaultProbe = () => {
22971
- (0, import_child_process9.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
23788
+ (0, import_child_process10.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
22972
23789
  };
22973
23790
  function hasTmux(probe = defaultProbe) {
22974
23791
  try {
@@ -22980,7 +23797,7 @@ function hasTmux(probe = defaultProbe) {
22980
23797
  }
22981
23798
  var defaultHasBin = (bin) => {
22982
23799
  try {
22983
- (0, import_child_process9.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
23800
+ (0, import_child_process10.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
22984
23801
  return true;
22985
23802
  } catch {
22986
23803
  return false;
@@ -23005,7 +23822,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
23005
23822
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
23006
23823
  var LIVENESS_INTERVAL_MS = 5e3;
23007
23824
  var LIVENESS_MISSES = 2;
23008
- var execFileAsync2 = (0, import_util5.promisify)(import_child_process10.execFile);
23825
+ var execFileAsync2 = (0, import_util5.promisify)(import_child_process11.execFile);
23009
23826
  var defaultExecFile3 = async (command, args) => {
23010
23827
  const { stdout, stderr } = await execFileAsync2(command, args);
23011
23828
  return { stdout: String(stdout), stderr: String(stderr) };
@@ -23155,6 +23972,7 @@ var TmuxRunner = class extends AbstractRunner {
23155
23972
  pollIntervalMs;
23156
23973
  logDir;
23157
23974
  clipboardCommand;
23975
+ runnerVersion;
23158
23976
  /** Retries get a freshly named window so a failed attempt's output stays inspectable. */
23159
23977
  attempts = /* @__PURE__ */ new Map();
23160
23978
  ready = null;
@@ -23167,6 +23985,7 @@ var TmuxRunner = class extends AbstractRunner {
23167
23985
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
23168
23986
  this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
23169
23987
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
23988
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
23170
23989
  }
23171
23990
  /**
23172
23991
  * Reaps a session orphaned by a crashed prior daemon on the same port, then
@@ -23250,6 +24069,8 @@ var TmuxRunner = class extends AbstractRunner {
23250
24069
  }
23251
24070
  }
23252
24071
  async spawn(opts) {
24072
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
24073
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, await this.resolvePath()) : void 0;
23253
24074
  const invocation = buildRunnerInvocation({
23254
24075
  runner: opts.runner,
23255
24076
  prompt: opts.prompt,
@@ -23265,6 +24086,7 @@ var TmuxRunner = class extends AbstractRunner {
23265
24086
  headless: opts.headless ?? true,
23266
24087
  interactive: true,
23267
24088
  cwd: opts.cwd,
24089
+ runnerVersion,
23268
24090
  registry: opts.registry
23269
24091
  });
23270
24092
  await this.ensureSession();
@@ -23294,7 +24116,7 @@ var TmuxRunner = class extends AbstractRunner {
23294
24116
 
23295
24117
  // src/services/StructuredRunner.ts
23296
24118
  var import_events2 = require("events");
23297
- var import_child_process11 = require("child_process");
24119
+ var import_child_process12 = require("child_process");
23298
24120
  var DEFAULT_INTERRUPT_GRACE_MS = 5e3;
23299
24121
  var TOOL_ARG_MAX_CHARS = 120;
23300
24122
  var TURN_OPENING_EVENTS = /* @__PURE__ */ new Set(["assistant_text", "assistant_text_delta", "thinking", "thinking_delta", "tool_call"]);
@@ -23668,7 +24490,7 @@ var StructuredRunner = class extends AbstractRunner {
23668
24490
  const session = new StructuredSession(id, opts.taskId, {
23669
24491
  runner: opts.runner,
23670
24492
  deps: {
23671
- spawn: import_child_process11.spawn,
24493
+ spawn: import_child_process12.spawn,
23672
24494
  fetch: globalThis.fetch,
23673
24495
  ...this.processDeps,
23674
24496
  // Already resolved by the caller (ADR-0016); resolving it again here
@@ -23763,7 +24585,7 @@ function tokensMatch(presented, expected) {
23763
24585
  var fs13 = __toESM(require("fs"));
23764
24586
  var path24 = __toESM(require("path"));
23765
24587
  var os6 = __toESM(require("os"));
23766
- var import_child_process12 = require("child_process");
24588
+ var import_child_process13 = require("child_process");
23767
24589
  var import_crypto6 = require("crypto");
23768
24590
 
23769
24591
  // src/plugins/FsPluginStore.ts
@@ -24181,6 +25003,37 @@ var OPENCODE_MANIFEST = {
24181
25003
  env: {
24182
25004
  OPENCODE_CONFIG_CONTENT: "{{if interactiveVariant}}{{opencodeVariantConfig}}{{/if}}"
24183
25005
  },
25006
+ // OpenCode 2.x changed both shapes. Its TUI takes no `--model`/`--agent`,
25007
+ // so they ride on OPENCODE_CONFIG_CONTENT instead, and its `run` has no
25008
+ // `--variant` — the variant is a `#suffix` on the model id. Both run
25009
+ // `--standalone`: without it the TUI and `run` attach to the user's
25010
+ // background service, which neither reads this process's config nor
25011
+ // starts in the task's directory and environment.
25012
+ versioned: [{
25013
+ minMajor: 2,
25014
+ argsTemplate: [
25015
+ "{{if headlessSession}}",
25016
+ "run",
25017
+ "--agent",
25018
+ "{{mode}}",
25019
+ "{{/if}}",
25020
+ "{{if headlessModel}}",
25021
+ "--model",
25022
+ "{{modelWithVariant}}",
25023
+ "{{/if}}",
25024
+ "--standalone",
25025
+ "{{if buildMode}}",
25026
+ "--auto",
25027
+ "{{/if}}",
25028
+ "{{if interactive}}",
25029
+ "--prompt",
25030
+ "{{/if}}",
25031
+ "{{prompt}}"
25032
+ ],
25033
+ env: {
25034
+ OPENCODE_CONFIG_CONTENT: "{{if interactive}}{{opencodeSessionConfig}}{{/if}}"
25035
+ }
25036
+ }],
24184
25037
  promptInArgs: true,
24185
25038
  requiresTty: true,
24186
25039
  // `--prompt` only pre-fills the TUI's composer; it does not submit it.
@@ -24299,7 +25152,7 @@ function isReservedRunnerName(name) {
24299
25152
  return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
24300
25153
  }
24301
25154
  function gitClone(url, destDir) {
24302
- (0, import_child_process12.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
25155
+ (0, import_child_process13.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
24303
25156
  stdio: "pipe",
24304
25157
  timeout: 3e4,
24305
25158
  // A repository that demands credentials should fail, not hang on a prompt.
@@ -24780,6 +25633,7 @@ var PlannerModelMemory = class {
24780
25633
  FsPluginStore,
24781
25634
  GIT_READONLY_SUBCOMMANDS,
24782
25635
  GREP_DEFAULT_HEAD_LIMIT,
25636
+ GREP_PCRE_PROBE_ARGS,
24783
25637
  GeminiService,
24784
25638
  HeadlessRunner,
24785
25639
  HeadlessSession,
@@ -24919,6 +25773,7 @@ var PlannerModelMemory = class {
24919
25773
  dependencyCandidates,
24920
25774
  dependentsOf,
24921
25775
  describeMergeResult,
25776
+ digestTaskLog,
24922
25777
  discoverGeminiModels,
24923
25778
  drainNext,
24924
25779
  effectiveAllowlist,
@@ -24951,6 +25806,7 @@ var PlannerModelMemory = class {
24951
25806
  getStateDir,
24952
25807
  globalDataDir,
24953
25808
  grantScopeFor,
25809
+ grepRegexFromProbe,
24954
25810
  hasHiddenDetail,
24955
25811
  hasTmux,
24956
25812
  holdPrompt,