@ordewell/core 0.6.2 → 0.6.4

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
@@ -2987,7 +2987,10 @@ var WRAPPER_FAMILY = {
2987
2987
  // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
2988
2988
  // name arrives as its first argument.
2989
2989
  busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
2990
- command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
2990
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] },
2991
+ // Runs the named shell builtin, so `builtin eval …` is an `eval`. Classified
2992
+ // on its own name it only asked, and a grant for `builtin echo` covered it.
2993
+ builtin: {}
2991
2994
  };
2992
2995
  var XARGS_SPEC = {
2993
2996
  valueFlags: [
@@ -3119,6 +3122,8 @@ function lex(command, nested, dialect) {
3119
3122
  let unbalanced = false;
3120
3123
  let grouped = false;
3121
3124
  let joinedBy;
3125
+ let followable = true;
3126
+ let bareSegment = false;
3122
3127
  let tokens = [];
3123
3128
  let expandable = false;
3124
3129
  let current = "";
@@ -3148,9 +3153,20 @@ function lex(command, nested, dialect) {
3148
3153
  current = "";
3149
3154
  started = false;
3150
3155
  };
3151
- const endSegment = (nextPiped, next = "other") => {
3156
+ const endSegment = (nextPiped, next = "other", last = false) => {
3152
3157
  endToken();
3153
- if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs, joinedBy });
3158
+ if (!last && next === "other") followable = false;
3159
+ if (tokens.length > 0) {
3160
+ const seg = toSegment(tokens, piped, dialect);
3161
+ if (!seg.binary) {
3162
+ followable = false;
3163
+ bareSegment = true;
3164
+ }
3165
+ segments.push({ ...seg, expandable, inputs, joinedBy });
3166
+ } else if (!last || segments.length > 0 || joinedBy !== void 0) {
3167
+ followable = false;
3168
+ bareSegment = true;
3169
+ }
3154
3170
  tokens = [];
3155
3171
  inputs = [];
3156
3172
  expandable = false;
@@ -3305,8 +3321,8 @@ function lex(command, nested, dialect) {
3305
3321
  i++;
3306
3322
  }
3307
3323
  if (quote !== "") unbalanced = true;
3308
- endSegment(false);
3309
- return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped };
3324
+ endSegment(false, "other", true);
3325
+ return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped, followable, bareSegment };
3310
3326
  }
3311
3327
  function binaryName(token, dialect) {
3312
3328
  const base = token.split(/[/\\]/).pop() ?? "";
@@ -3450,7 +3466,8 @@ function lexAll(command, dialect, cdpathSet = false) {
3450
3466
  const queue = [];
3451
3467
  const top = lex(command, queue, dialect);
3452
3468
  const all = { ...top, segments: top.segments.filter((s) => s.binary) };
3453
- if (!top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
3469
+ markShellState(all.segments, top.bareSegment);
3470
+ if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
3454
3471
  for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
3455
3472
  const inner = lex(queue.shift(), queue, dialect);
3456
3473
  all.segments.push(...inner.segments.filter((s) => s.binary));
@@ -3467,22 +3484,51 @@ function literalCdTarget(seg) {
3467
3484
  return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
3468
3485
  }
3469
3486
  function followCd(segments) {
3470
- if (segments.some((s) => s.joinedBy === "other")) return;
3487
+ if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
3488
+ const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
3489
+ if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
3471
3490
  let chain = [];
3472
3491
  segments.forEach((seg, i) => {
3473
3492
  seg.cwd = chain;
3474
- const inPipeline = seg.joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
3475
- const target = inPipeline ? void 0 : literalCdTarget(seg);
3493
+ const target = inPipeline(i) ? void 0 : literalCdTarget(seg);
3476
3494
  if (target !== void 0) chain = [...chain, target];
3477
3495
  });
3478
3496
  }
3497
+ function markShellState(segments, bareSegment) {
3498
+ if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
3499
+ for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
3500
+ }
3501
+ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
3502
+ "export",
3503
+ "set",
3504
+ "shopt",
3505
+ "declare",
3506
+ "typeset",
3507
+ "readonly",
3508
+ "local",
3509
+ "unset",
3510
+ "let",
3511
+ "source",
3512
+ ".",
3513
+ "eval",
3514
+ "exec",
3515
+ "pushd",
3516
+ "popd",
3517
+ "alias",
3518
+ "unalias",
3519
+ "builtin",
3520
+ "command",
3521
+ "enable",
3522
+ "hash",
3523
+ "trap"
3524
+ ]);
3479
3525
  function looksLikePath(arg) {
3480
3526
  if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
3481
3527
  return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
3482
3528
  }
3483
3529
  function pathRefs(command, opts = {}) {
3484
3530
  const dialect = dialectFor(opts.dialect);
3485
- return lexAll(command, dialect, opts.cdpathSet).segments.flatMap((seg) => {
3531
+ return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
3486
3532
  const patterns = patternArgs(seg);
3487
3533
  const cwd = seg.cwd ?? [];
3488
3534
  return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path27) => ({ path: path27, cwd }));
@@ -3650,7 +3696,7 @@ function scopeFor(seg) {
3650
3696
  return [seg.binary, ...lead].join(" ");
3651
3697
  }
3652
3698
  function isAuto(seg, opts) {
3653
- if (seg.binary === "cd") return !opts.cdpathSet && literalCdTarget(seg) !== void 0;
3699
+ if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
3654
3700
  if (!AUTO_COMMANDS.includes(seg.binary)) return false;
3655
3701
  if (seg.expandable) return false;
3656
3702
  if (seg.binary !== "git") return true;
@@ -3674,6 +3720,9 @@ function refusalFor(seg) {
3674
3720
  if (seg.binary === "source" || seg.binary === ".") {
3675
3721
  return `"${seg.binary}" runs a file's contents as commands, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
3676
3722
  }
3723
+ if (seg.binary === "enable" && seg.args.some((a) => /^-[a-zA-Z]*f/.test(a))) {
3724
+ return `"enable -f" loads a shared library into the shell, which runs its code. Use the read-only research tools, or describe it as a task.`;
3725
+ }
3677
3726
  if (SHELL_KEYWORDS.includes(seg.binary)) {
3678
3727
  return `"${seg.binary}" is a shell keyword or compound-command opener. This classifier only inspects the command that runs first, and a keyword hides the real one from it. Run the inner command directly, or describe it as a task.`;
3679
3728
  }
@@ -3784,6 +3833,9 @@ function classifyCommand(command, opts = {}) {
3784
3833
  }
3785
3834
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
3786
3835
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
3836
+ if (nonAuto.some((u) => u.seg.binary === "cd" || u.seg.binary !== "command" && SHELL_STATE_COMMANDS.has(u.seg.binary))) {
3837
+ return { tier: "ask", scope: trimmed };
3838
+ }
3787
3839
  const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
3788
3840
  return { tier: "ask", scope };
3789
3841
  }
@@ -3852,6 +3904,9 @@ function researchShellWarning(shell) {
3852
3904
  if (shell.dialect !== "cmd") return null;
3853
3905
  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.";
3854
3906
  }
3907
+ function researchPolicyOptions(shell) {
3908
+ return { dialect: shell.dialect, cdpathSet: !!process.env.CDPATH };
3909
+ }
3855
3910
 
3856
3911
  // src/services/pathScope.ts
3857
3912
  var os = __toESM(require("os"));
@@ -4085,7 +4140,7 @@ ${refs.output.trim()}`);
4085
4140
  return [...found].map(([scope, abs]) => ({ abs, scope }));
4086
4141
  }
4087
4142
  policyOptions() {
4088
- return { dialect: this.researchShell.dialect, cdpathSet: !!process.env.CDPATH };
4143
+ return researchPolicyOptions(this.researchShell);
4089
4144
  }
4090
4145
  /**
4091
4146
  * Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
@@ -10532,16 +10587,15 @@ function nonPromptingFs(fs16) {
10532
10587
  findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs16.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
10533
10588
  listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
10534
10589
  bash: async (command, signal) => {
10535
- const dialect = resolveResearchShell().dialect;
10536
- const policy = { dialect, cdpathSet: !!process.env.CDPATH };
10590
+ const policy = researchPolicyOptions(resolveResearchShell());
10537
10591
  const { tier, reason } = classifyCommand(command, policy);
10538
10592
  if (tier !== "auto") {
10539
10593
  return refused2(
10540
10594
  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.`
10541
10595
  );
10542
10596
  }
10543
- const escaping = pathRefs(command, policy).find((ref) => !resolveRef(fs16.getWorkspaceRoot(), ref).inside);
10544
- if (escaping) return outsideRefusal(escaping.path);
10597
+ const escaping = pathRefs(command, policy).map((ref) => resolveRef(fs16.getWorkspaceRoot(), ref)).find((r) => !r.inside);
10598
+ if (escaping) return outsideRefusal(escaping.abs);
10545
10599
  return fs16.bash(command, signal);
10546
10600
  }
10547
10601
  };
@@ -13578,11 +13632,17 @@ function buildShellInvocation(command, args) {
13578
13632
  const inner = [command, ...args].map(posixShellQuote).join(" ");
13579
13633
  return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
13580
13634
  }
13635
+ var BSD_SCRIPT_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "freebsd", "openbsd", "netbsd"]);
13636
+ 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; }';
13581
13637
  function wrapWithPty(command, args, opts = {}) {
13582
13638
  const inner = [command, ...args].map(posixShellQuote).join(" ");
13583
13639
  const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
13584
13640
  const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
13585
- return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
13641
+ const line = `${watcher}${setup}${inner}`;
13642
+ if (BSD_SCRIPT_PLATFORMS.has(opts.platform ?? process.platform)) {
13643
+ return { command: "/bin/sh", args: ["-c", BSD_SCRIPT_FEED, line] };
13644
+ }
13645
+ return { command: "script", args: ["-q", "-e", "-f", "-c", line, "/dev/null"] };
13586
13646
  }
13587
13647
 
13588
13648
  // src/services/blockingPrompts.ts
@@ -13777,24 +13837,37 @@ var Block = /* @__PURE__ */ ((Block2) => {
13777
13837
  Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
13778
13838
  Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
13779
13839
  Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
13840
+ Block2[Block2["IfHeadlessModel"] = 11] = "IfHeadlessModel";
13780
13841
  return Block2;
13781
13842
  })(Block || {});
13782
13843
  function isInteractive(ctx) {
13783
13844
  return ctx.interactive ?? !ctx.headless;
13784
13845
  }
13846
+ function majorVersion(version) {
13847
+ const match = version?.match(/(\d+)\.\d+/);
13848
+ return match ? Number(match[1]) : void 0;
13849
+ }
13850
+ function versionedInvocation(manifest, ctx) {
13851
+ const major = majorVersion(ctx.runnerVersion);
13852
+ if (major === void 0) return void 0;
13853
+ return (manifest.runner.versioned ?? []).filter((v) => major >= v.minMajor).sort((a, b) => b.minMajor - a.minMajor)[0];
13854
+ }
13785
13855
  function resolveArgs(manifest, ctx) {
13786
13856
  const args = [];
13787
13857
  const env = {};
13788
- if (manifest.runner.env) {
13789
- for (const [key, value] of Object.entries(manifest.runner.env)) {
13858
+ const versioned = versionedInvocation(manifest, ctx);
13859
+ const argsTemplate = versioned?.argsTemplate ?? manifest.runner.argsTemplate;
13860
+ const envTemplate = versioned ? versioned.env : manifest.runner.env;
13861
+ if (envTemplate) {
13862
+ for (const [key, value] of Object.entries(envTemplate)) {
13790
13863
  const resolved = substituteSimple(value, ctx);
13791
13864
  if (resolved.length > 0) env[key] = resolved;
13792
13865
  }
13793
13866
  }
13794
13867
  let block = 0 /* None */;
13795
13868
  let include = true;
13796
- for (let i = 0; i < manifest.runner.argsTemplate.length; i++) {
13797
- const raw = manifest.runner.argsTemplate[i];
13869
+ for (let i = 0; i < argsTemplate.length; i++) {
13870
+ const raw = argsTemplate[i];
13798
13871
  const blockOpen = parseBlockOpen(raw);
13799
13872
  if (blockOpen !== null) {
13800
13873
  if (block !== 0 /* None */) throw new ResolveError(`Nested conditional block at position ${i}: "${raw}"`);
@@ -13853,6 +13926,8 @@ function parseBlockOpen(raw) {
13853
13926
  return 9 /* IfInteractiveVariant */;
13854
13927
  case "{{if projectTrust}}":
13855
13928
  return 10 /* IfProjectTrust */;
13929
+ case "{{if headlessModel}}":
13930
+ return 11 /* IfHeadlessModel */;
13856
13931
  default:
13857
13932
  return null;
13858
13933
  }
@@ -13892,6 +13967,9 @@ function shouldIncludeBlock(block, ctx) {
13892
13967
  // there is no cwd to name.
13893
13968
  case 10 /* IfProjectTrust */:
13894
13969
  return isInteractive(ctx) && !!ctx.cwd;
13970
+ // A model flag that only the non-interactive subcommand takes.
13971
+ case 11 /* IfHeadlessModel */:
13972
+ return !!ctx.model && !isInteractive(ctx);
13895
13973
  default:
13896
13974
  return true;
13897
13975
  }
@@ -13937,6 +14015,10 @@ function resolveToken(token, manifest, ctx) {
13937
14015
  if (token === "{{model}}") return ctx.model || "";
13938
14016
  if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
13939
14017
  if (token === "{{mode}}") return ctx.mode || "build";
14018
+ if (token === "{{modelWithVariant}}") {
14019
+ if (!ctx.model) return "";
14020
+ return ctx.thinkingEffort ? `${ctx.model}#${ctx.thinkingEffort}` : ctx.model;
14021
+ }
13940
14022
  if (token === "{{projectTrust}}") {
13941
14023
  if (!ctx.cwd) return "";
13942
14024
  return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
@@ -14011,13 +14093,21 @@ function resolveOpencodeVariantConfig(ctx) {
14011
14093
  }
14012
14094
  return JSON.stringify(config);
14013
14095
  }
14096
+ function resolveOpencodeSessionConfig(ctx) {
14097
+ const mode = ctx.mode || "build";
14098
+ const pinned = resolveOpencodeVariantConfig(ctx);
14099
+ const config = pinned ? JSON.parse(pinned) : {};
14100
+ config.default_agent = mode;
14101
+ if (ctx.model && !pinned) config.agent = { [mode]: { model: ctx.model } };
14102
+ return JSON.stringify(config);
14103
+ }
14014
14104
  function substituteSimple(value, ctx) {
14015
14105
  const conditioned = value.replace(/\{\{if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, name, body) => {
14016
14106
  const block = parseBlockOpen(`{{if ${name}}}`);
14017
14107
  if (block === null) return match;
14018
14108
  return shouldIncludeBlock(block, ctx) ? body : "";
14019
14109
  });
14020
- 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");
14110
+ 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");
14021
14111
  }
14022
14112
 
14023
14113
  // src/utils/launch.ts
@@ -23445,12 +23535,32 @@ function buildRunnerInvocation(opts) {
23445
23535
  mode: opts.mode ?? "build",
23446
23536
  headless: opts.headless,
23447
23537
  interactive: opts.interactive,
23448
- cwd: opts.cwd
23538
+ cwd: opts.cwd,
23539
+ runnerVersion: opts.runnerVersion
23449
23540
  });
23450
23541
  }
23451
23542
 
23452
23543
  // src/services/HeadlessRunner.ts
23544
+ var import_child_process9 = require("child_process");
23545
+
23546
+ // src/services/runnerVersion.ts
23453
23547
  var import_child_process8 = require("child_process");
23548
+ var VERSION_TIMEOUT_MS = 5e3;
23549
+ var cache = /* @__PURE__ */ new Map();
23550
+ var probeRunnerVersion = (command, PATH) => {
23551
+ const key = `${command}\0${PATH}`;
23552
+ let pending = cache.get(key);
23553
+ if (!pending) {
23554
+ pending = new Promise((resolve7) => {
23555
+ (0, import_child_process8.execFile)(command, ["--version"], { env: { ...process.env, PATH }, timeout: VERSION_TIMEOUT_MS }, (err, stdout) => {
23556
+ const line = String(stdout ?? "").trim();
23557
+ resolve7(err || !line ? void 0 : line);
23558
+ });
23559
+ });
23560
+ cache.set(key, pending);
23561
+ }
23562
+ return pending;
23563
+ };
23454
23564
 
23455
23565
  // src/services/AbstractRunner.ts
23456
23566
  var import_events = require("events");
@@ -23505,7 +23615,7 @@ var AbstractRunner = class {
23505
23615
  function defaultHasScriptCmd() {
23506
23616
  if (process.platform === "win32") return false;
23507
23617
  try {
23508
- (0, import_child_process8.execSync)("which script", { stdio: "ignore" });
23618
+ (0, import_child_process9.execSync)("which script", { stdio: "ignore" });
23509
23619
  return true;
23510
23620
  } catch {
23511
23621
  return false;
@@ -23572,6 +23682,10 @@ Process error: ${err.message}
23572
23682
  this.process.stdin.write(text2);
23573
23683
  }
23574
23684
  }
23685
+ /** Closes the runner's stdin: what it has read so far is all it will get. */
23686
+ endInput() {
23687
+ this.process?.stdin?.end();
23688
+ }
23575
23689
  /** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
23576
23690
  writeControl(text2) {
23577
23691
  if (this.controlStream && !this.controlStream.destroyed) {
@@ -23584,6 +23698,7 @@ var HeadlessRunner = class extends AbstractRunner {
23584
23698
  hasScriptCmd;
23585
23699
  resolvePath;
23586
23700
  launchDeps;
23701
+ runnerVersion;
23587
23702
  spawnCount = 0;
23588
23703
  /**
23589
23704
  * Session shape: a piped subprocess is not a terminal, so runners get their
@@ -23595,10 +23710,11 @@ var HeadlessRunner = class extends AbstractRunner {
23595
23710
  defaultInteractive = false;
23596
23711
  constructor(deps = {}) {
23597
23712
  super();
23598
- this.spawnImpl = deps.spawnImpl ?? import_child_process8.spawn;
23713
+ this.spawnImpl = deps.spawnImpl ?? import_child_process9.spawn;
23599
23714
  this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
23600
23715
  this.resolvePath = deps.resolvePath ?? augmentedPath;
23601
23716
  this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
23717
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
23602
23718
  }
23603
23719
  /**
23604
23720
  * Unique per spawn, not per task: a retry reuses its task id and ids often
@@ -23616,6 +23732,9 @@ var HeadlessRunner = class extends AbstractRunner {
23616
23732
  /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
23617
23733
  async prepareLaunch(opts, ptyOptions) {
23618
23734
  const interactive = this.defaultInteractive;
23735
+ const resolvedPath = await this.resolvePath();
23736
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
23737
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, resolvedPath) : void 0;
23619
23738
  const invocation = buildRunnerInvocation({
23620
23739
  runner: opts.runner,
23621
23740
  prompt: opts.prompt,
@@ -23626,18 +23745,18 @@ var HeadlessRunner = class extends AbstractRunner {
23626
23745
  headless: opts.headless ?? true,
23627
23746
  interactive,
23628
23747
  cwd: opts.cwd,
23748
+ runnerVersion,
23629
23749
  registry: opts.registry
23630
23750
  });
23631
- const manifest = opts.registry?.get(opts.runner)?.manifest;
23632
23751
  const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
23633
- const resolvedPath = await this.resolvePath();
23634
- const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, ptyOptions) : { command: invocation.command, args: invocation.args };
23752
+ const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, { platform: this.launchDeps.platform, ...ptyOptions }) : { command: invocation.command, args: invocation.args };
23635
23753
  return {
23636
23754
  launch: await planDirectLaunch(command, args, this.launchDeps),
23637
23755
  resolvedPath,
23638
23756
  env: { ...opts.env, ...invocation.env },
23639
23757
  pty,
23640
- submitPromptKey: invocation.submitPromptKey
23758
+ submitPromptKey: invocation.submitPromptKey,
23759
+ closeStdin: !pty && !interactive && invocation.promptInArgs
23641
23760
  };
23642
23761
  }
23643
23762
  async spawn(opts) {
@@ -23648,20 +23767,21 @@ var HeadlessRunner = class extends AbstractRunner {
23648
23767
  console.error(`[headless] Starting ${opts.runner} [${modeStr}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}${prepared.pty ? " (PTY)" : ""}`);
23649
23768
  session.start(prepared.launch, opts.cwd, prepared.resolvedPath, prepared.env);
23650
23769
  if (prepared.submitPromptKey) session.write("\r");
23770
+ if (prepared.closeStdin) session.endInput();
23651
23771
  this.registerSession(id, session);
23652
23772
  return session;
23653
23773
  }
23654
23774
  };
23655
23775
 
23656
23776
  // src/services/TmuxRunner.ts
23657
- var import_child_process10 = require("child_process");
23777
+ var import_child_process11 = require("child_process");
23658
23778
  var import_util5 = require("util");
23659
23779
  var import_os = require("os");
23660
23780
  var import_path2 = require("path");
23661
23781
  var import_fs3 = require("fs");
23662
23782
 
23663
23783
  // src/utils/tmux.ts
23664
- var import_child_process9 = require("child_process");
23784
+ var import_child_process10 = require("child_process");
23665
23785
  function tmuxSessionName(port) {
23666
23786
  return `ordewell-${port}`;
23667
23787
  }
@@ -23674,7 +23794,7 @@ function tmuxWindowName(taskId, planSessionId) {
23674
23794
  return `t-${scope}${slug(taskId).slice(0, 16)}`;
23675
23795
  }
23676
23796
  var defaultProbe = () => {
23677
- (0, import_child_process9.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
23797
+ (0, import_child_process10.execSync)(process.platform === "win32" ? "where tmux" : "which tmux", { stdio: "ignore" });
23678
23798
  };
23679
23799
  function hasTmux(probe = defaultProbe) {
23680
23800
  try {
@@ -23686,7 +23806,7 @@ function hasTmux(probe = defaultProbe) {
23686
23806
  }
23687
23807
  var defaultHasBin = (bin) => {
23688
23808
  try {
23689
- (0, import_child_process9.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
23809
+ (0, import_child_process10.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
23690
23810
  return true;
23691
23811
  } catch {
23692
23812
  return false;
@@ -23711,7 +23831,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
23711
23831
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
23712
23832
  var LIVENESS_INTERVAL_MS = 5e3;
23713
23833
  var LIVENESS_MISSES = 2;
23714
- var execFileAsync2 = (0, import_util5.promisify)(import_child_process10.execFile);
23834
+ var execFileAsync2 = (0, import_util5.promisify)(import_child_process11.execFile);
23715
23835
  var defaultExecFile3 = async (command, args) => {
23716
23836
  const { stdout, stderr } = await execFileAsync2(command, args);
23717
23837
  return { stdout: String(stdout), stderr: String(stderr) };
@@ -23861,6 +23981,7 @@ var TmuxRunner = class extends AbstractRunner {
23861
23981
  pollIntervalMs;
23862
23982
  logDir;
23863
23983
  clipboardCommand;
23984
+ runnerVersion;
23864
23985
  /** Retries get a freshly named window so a failed attempt's output stays inspectable. */
23865
23986
  attempts = /* @__PURE__ */ new Map();
23866
23987
  ready = null;
@@ -23873,6 +23994,7 @@ var TmuxRunner = class extends AbstractRunner {
23873
23994
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
23874
23995
  this.logDir = deps.logDir ?? (0, import_os.tmpdir)();
23875
23996
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
23997
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
23876
23998
  }
23877
23999
  /**
23878
24000
  * Reaps a session orphaned by a crashed prior daemon on the same port, then
@@ -23956,6 +24078,8 @@ var TmuxRunner = class extends AbstractRunner {
23956
24078
  }
23957
24079
  }
23958
24080
  async spawn(opts) {
24081
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
24082
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, await this.resolvePath()) : void 0;
23959
24083
  const invocation = buildRunnerInvocation({
23960
24084
  runner: opts.runner,
23961
24085
  prompt: opts.prompt,
@@ -23971,6 +24095,7 @@ var TmuxRunner = class extends AbstractRunner {
23971
24095
  headless: opts.headless ?? true,
23972
24096
  interactive: true,
23973
24097
  cwd: opts.cwd,
24098
+ runnerVersion,
23974
24099
  registry: opts.registry
23975
24100
  });
23976
24101
  await this.ensureSession();
@@ -24000,7 +24125,7 @@ var TmuxRunner = class extends AbstractRunner {
24000
24125
 
24001
24126
  // src/services/StructuredRunner.ts
24002
24127
  var import_events2 = require("events");
24003
- var import_child_process11 = require("child_process");
24128
+ var import_child_process12 = require("child_process");
24004
24129
  var DEFAULT_INTERRUPT_GRACE_MS = 5e3;
24005
24130
  var TOOL_ARG_MAX_CHARS = 120;
24006
24131
  var TURN_OPENING_EVENTS = /* @__PURE__ */ new Set(["assistant_text", "assistant_text_delta", "thinking", "thinking_delta", "tool_call"]);
@@ -24374,7 +24499,7 @@ var StructuredRunner = class extends AbstractRunner {
24374
24499
  const session = new StructuredSession(id, opts.taskId, {
24375
24500
  runner: opts.runner,
24376
24501
  deps: {
24377
- spawn: import_child_process11.spawn,
24502
+ spawn: import_child_process12.spawn,
24378
24503
  fetch: globalThis.fetch,
24379
24504
  ...this.processDeps,
24380
24505
  // Already resolved by the caller (ADR-0016); resolving it again here
@@ -24469,7 +24594,7 @@ function tokensMatch(presented, expected) {
24469
24594
  var fs13 = __toESM(require("fs"));
24470
24595
  var path24 = __toESM(require("path"));
24471
24596
  var os6 = __toESM(require("os"));
24472
- var import_child_process12 = require("child_process");
24597
+ var import_child_process13 = require("child_process");
24473
24598
  var import_crypto6 = require("crypto");
24474
24599
 
24475
24600
  // src/plugins/FsPluginStore.ts
@@ -24887,6 +25012,37 @@ var OPENCODE_MANIFEST = {
24887
25012
  env: {
24888
25013
  OPENCODE_CONFIG_CONTENT: "{{if interactiveVariant}}{{opencodeVariantConfig}}{{/if}}"
24889
25014
  },
25015
+ // OpenCode 2.x changed both shapes. Its TUI takes no `--model`/`--agent`,
25016
+ // so they ride on OPENCODE_CONFIG_CONTENT instead, and its `run` has no
25017
+ // `--variant` — the variant is a `#suffix` on the model id. Both run
25018
+ // `--standalone`: without it the TUI and `run` attach to the user's
25019
+ // background service, which neither reads this process's config nor
25020
+ // starts in the task's directory and environment.
25021
+ versioned: [{
25022
+ minMajor: 2,
25023
+ argsTemplate: [
25024
+ "{{if headlessSession}}",
25025
+ "run",
25026
+ "--agent",
25027
+ "{{mode}}",
25028
+ "{{/if}}",
25029
+ "{{if headlessModel}}",
25030
+ "--model",
25031
+ "{{modelWithVariant}}",
25032
+ "{{/if}}",
25033
+ "--standalone",
25034
+ "{{if buildMode}}",
25035
+ "--auto",
25036
+ "{{/if}}",
25037
+ "{{if interactive}}",
25038
+ "--prompt",
25039
+ "{{/if}}",
25040
+ "{{prompt}}"
25041
+ ],
25042
+ env: {
25043
+ OPENCODE_CONFIG_CONTENT: "{{if interactive}}{{opencodeSessionConfig}}{{/if}}"
25044
+ }
25045
+ }],
24890
25046
  promptInArgs: true,
24891
25047
  requiresTty: true,
24892
25048
  // `--prompt` only pre-fills the TUI's composer; it does not submit it.
@@ -25005,7 +25161,7 @@ function isReservedRunnerName(name) {
25005
25161
  return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
25006
25162
  }
25007
25163
  function gitClone(url, destDir) {
25008
- (0, import_child_process12.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
25164
+ (0, import_child_process13.execFileSync)("git", ["clone", "--depth", "1", "--", url, destDir], {
25009
25165
  stdio: "pipe",
25010
25166
  timeout: 3e4,
25011
25167
  // A repository that demands credentials should fail, not hang on a prompt.