@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.mjs CHANGED
@@ -2345,7 +2345,10 @@ var WRAPPER_FAMILY = {
2345
2345
  // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
2346
2346
  // name arrives as its first argument.
2347
2347
  busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
2348
- command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
2348
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] },
2349
+ // Runs the named shell builtin, so `builtin eval …` is an `eval`. Classified
2350
+ // on its own name it only asked, and a grant for `builtin echo` covered it.
2351
+ builtin: {}
2349
2352
  };
2350
2353
  var XARGS_SPEC = {
2351
2354
  valueFlags: [
@@ -2477,6 +2480,8 @@ function lex(command, nested, dialect) {
2477
2480
  let unbalanced = false;
2478
2481
  let grouped = false;
2479
2482
  let joinedBy;
2483
+ let followable = true;
2484
+ let bareSegment = false;
2480
2485
  let tokens = [];
2481
2486
  let expandable = false;
2482
2487
  let current = "";
@@ -2506,9 +2511,20 @@ function lex(command, nested, dialect) {
2506
2511
  current = "";
2507
2512
  started = false;
2508
2513
  };
2509
- const endSegment = (nextPiped, next = "other") => {
2514
+ const endSegment = (nextPiped, next = "other", last = false) => {
2510
2515
  endToken();
2511
- if (tokens.length > 0) segments.push({ ...toSegment(tokens, piped, dialect), expandable, inputs, joinedBy });
2516
+ if (!last && next === "other") followable = false;
2517
+ if (tokens.length > 0) {
2518
+ const seg = toSegment(tokens, piped, dialect);
2519
+ if (!seg.binary) {
2520
+ followable = false;
2521
+ bareSegment = true;
2522
+ }
2523
+ segments.push({ ...seg, expandable, inputs, joinedBy });
2524
+ } else if (!last || segments.length > 0 || joinedBy !== void 0) {
2525
+ followable = false;
2526
+ bareSegment = true;
2527
+ }
2512
2528
  tokens = [];
2513
2529
  inputs = [];
2514
2530
  expandable = false;
@@ -2663,8 +2679,8 @@ function lex(command, nested, dialect) {
2663
2679
  i++;
2664
2680
  }
2665
2681
  if (quote !== "") unbalanced = true;
2666
- endSegment(false);
2667
- return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped };
2682
+ endSegment(false, "other", true);
2683
+ return { segments, unsafeRedirect, processSubstitution, unbalanced, grouped, followable, bareSegment };
2668
2684
  }
2669
2685
  function binaryName(token, dialect) {
2670
2686
  const base = token.split(/[/\\]/).pop() ?? "";
@@ -2808,7 +2824,8 @@ function lexAll(command, dialect, cdpathSet = false) {
2808
2824
  const queue = [];
2809
2825
  const top = lex(command, queue, dialect);
2810
2826
  const all = { ...top, segments: top.segments.filter((s) => s.binary) };
2811
- if (!top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
2827
+ markShellState(all.segments, top.bareSegment);
2828
+ if (top.followable && !top.grouped && queue.length === 0 && !cdpathSet) followCd(all.segments);
2812
2829
  for (let depth = 0; depth < 32 && queue.length > 0; depth++) {
2813
2830
  const inner = lex(queue.shift(), queue, dialect);
2814
2831
  all.segments.push(...inner.segments.filter((s) => s.binary));
@@ -2825,22 +2842,51 @@ function literalCdTarget(seg) {
2825
2842
  return target === "" || target.startsWith("-") || target.startsWith("~") || /[*?[]/.test(target) ? void 0 : target;
2826
2843
  }
2827
2844
  function followCd(segments) {
2828
- if (segments.some((s) => s.joinedBy === "other")) return;
2845
+ if (segments.some((s) => s.joinedBy === "other" || SHELL_STATE_COMMANDS.has(s.binary))) return;
2846
+ const inPipeline = (i) => segments[i].joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
2847
+ if (segments.some((s, i) => s.binary === "cd" && !inPipeline(i) && literalCdTarget(s) === void 0)) return;
2829
2848
  let chain = [];
2830
2849
  segments.forEach((seg, i) => {
2831
2850
  seg.cwd = chain;
2832
- const inPipeline = seg.joinedBy === "pipe" || segments[i + 1]?.joinedBy === "pipe";
2833
- const target = inPipeline ? void 0 : literalCdTarget(seg);
2851
+ const target = inPipeline(i) ? void 0 : literalCdTarget(seg);
2834
2852
  if (target !== void 0) chain = [...chain, target];
2835
2853
  });
2836
2854
  }
2855
+ function markShellState(segments, bareSegment) {
2856
+ if (!bareSegment && !segments.some((s) => SHELL_STATE_COMMANDS.has(s.binary))) return;
2857
+ for (const seg of segments) if (seg.binary === "cd") seg.cdSteered = true;
2858
+ }
2859
+ var SHELL_STATE_COMMANDS = /* @__PURE__ */ new Set([
2860
+ "export",
2861
+ "set",
2862
+ "shopt",
2863
+ "declare",
2864
+ "typeset",
2865
+ "readonly",
2866
+ "local",
2867
+ "unset",
2868
+ "let",
2869
+ "source",
2870
+ ".",
2871
+ "eval",
2872
+ "exec",
2873
+ "pushd",
2874
+ "popd",
2875
+ "alias",
2876
+ "unalias",
2877
+ "builtin",
2878
+ "command",
2879
+ "enable",
2880
+ "hash",
2881
+ "trap"
2882
+ ]);
2837
2883
  function looksLikePath(arg) {
2838
2884
  if (arg.startsWith("--") && arg.includes("=")) return looksLikePath(arg.slice(arg.indexOf("=") + 1));
2839
2885
  return arg.startsWith("/") || arg.startsWith("~") || arg.startsWith("../") || arg === ".." || arg.startsWith("./") || /^[A-Za-z]:/.test(arg) || arg.startsWith("\\") || arg.startsWith("..\\") || arg.startsWith(".\\") || /(^|[\\/])\.\.([\\/]|$)/.test(arg);
2840
2886
  }
2841
2887
  function pathRefs(command, opts = {}) {
2842
2888
  const dialect = dialectFor(opts.dialect);
2843
- return lexAll(command, dialect, opts.cdpathSet).segments.flatMap((seg) => {
2889
+ return lexAll(command.trim(), dialect, opts.cdpathSet).segments.flatMap((seg) => {
2844
2890
  const patterns = patternArgs(seg);
2845
2891
  const cwd = seg.cwd ?? [];
2846
2892
  return [...seg.args.filter((_, i) => !patterns.has(i)), ...seg.inputs].flatMap(pathIn).map((path26) => ({ path: path26, cwd }));
@@ -3008,7 +3054,7 @@ function scopeFor(seg) {
3008
3054
  return [seg.binary, ...lead].join(" ");
3009
3055
  }
3010
3056
  function isAuto(seg, opts) {
3011
- if (seg.binary === "cd") return !opts.cdpathSet && literalCdTarget(seg) !== void 0;
3057
+ if (seg.binary === "cd") return !opts.cdpathSet && !seg.cdSteered && literalCdTarget(seg) !== void 0;
3012
3058
  if (!AUTO_COMMANDS.includes(seg.binary)) return false;
3013
3059
  if (seg.expandable) return false;
3014
3060
  if (seg.binary !== "git") return true;
@@ -3032,6 +3078,9 @@ function refusalFor(seg) {
3032
3078
  if (seg.binary === "source" || seg.binary === ".") {
3033
3079
  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.`;
3034
3080
  }
3081
+ if (seg.binary === "enable" && seg.args.some((a) => /^-[a-zA-Z]*f/.test(a))) {
3082
+ 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.`;
3083
+ }
3035
3084
  if (SHELL_KEYWORDS.includes(seg.binary)) {
3036
3085
  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.`;
3037
3086
  }
@@ -3142,6 +3191,9 @@ function classifyCommand(command, opts = {}) {
3142
3191
  }
3143
3192
  const nonAuto = unwrapped.filter((u) => u.runner !== void 0 || !isAuto(u.seg, opts));
3144
3193
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
3194
+ if (nonAuto.some((u) => u.seg.binary === "cd" || u.seg.binary !== "command" && SHELL_STATE_COMMANDS.has(u.seg.binary))) {
3195
+ return { tier: "ask", scope: trimmed };
3196
+ }
3145
3197
  const scope = [...new Set(nonAuto.map((u) => u.runner ? `${u.runner} ${scopeFor(u.seg)}` : scopeFor(u.seg)))].sort().join(" + ");
3146
3198
  return { tier: "ask", scope };
3147
3199
  }
@@ -3210,6 +3262,9 @@ function researchShellWarning(shell) {
3210
3262
  if (shell.dialect !== "cmd") return null;
3211
3263
  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.";
3212
3264
  }
3265
+ function researchPolicyOptions(shell) {
3266
+ return { dialect: shell.dialect, cdpathSet: !!process.env.CDPATH };
3267
+ }
3213
3268
 
3214
3269
  // src/services/pathScope.ts
3215
3270
  import * as os from "os";
@@ -3443,7 +3498,7 @@ ${refs.output.trim()}`);
3443
3498
  return [...found].map(([scope, abs]) => ({ abs, scope }));
3444
3499
  }
3445
3500
  policyOptions() {
3446
- return { dialect: this.researchShell.dialect, cdpathSet: !!process.env.CDPATH };
3501
+ return researchPolicyOptions(this.researchShell);
3447
3502
  }
3448
3503
  /**
3449
3504
  * Three tiers (see `commandPolicy.ts`): read-only inspection runs silently,
@@ -7726,16 +7781,15 @@ function nonPromptingFs(fs16) {
7726
7781
  findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs16.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
7727
7782
  listDir: (p, depth) => withinWorkspace(p) ? fs16.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
7728
7783
  bash: async (command, signal) => {
7729
- const dialect = resolveResearchShell().dialect;
7730
- const policy = { dialect, cdpathSet: !!process.env.CDPATH };
7784
+ const policy = researchPolicyOptions(resolveResearchShell());
7731
7785
  const { tier, reason } = classifyCommand(command, policy);
7732
7786
  if (tier !== "auto") {
7733
7787
  return refused2(
7734
7788
  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.`
7735
7789
  );
7736
7790
  }
7737
- const escaping = pathRefs(command, policy).find((ref) => !resolveRef(fs16.getWorkspaceRoot(), ref).inside);
7738
- if (escaping) return outsideRefusal(escaping.path);
7791
+ const escaping = pathRefs(command, policy).map((ref) => resolveRef(fs16.getWorkspaceRoot(), ref)).find((r) => !r.inside);
7792
+ if (escaping) return outsideRefusal(escaping.abs);
7739
7793
  return fs16.bash(command, signal);
7740
7794
  }
7741
7795
  };
@@ -10774,11 +10828,17 @@ function buildShellInvocation(command, args) {
10774
10828
  const inner = [command, ...args].map(posixShellQuote).join(" ");
10775
10829
  return { shellPath: "/bin/bash", shellArgs: ["-lc", inner] };
10776
10830
  }
10831
+ var BSD_SCRIPT_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "freebsd", "openbsd", "netbsd"]);
10832
+ 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; }';
10777
10833
  function wrapWithPty(command, args, opts = {}) {
10778
10834
  const inner = [command, ...args].map(posixShellQuote).join(" ");
10779
10835
  const watcher = opts.controlChannel ? 'exec 4<&0; ( while read -r C R <&3; do stty cols "$C" rows "$R" <&4; done ) & ' : "";
10780
10836
  const setup = opts.size ? `stty cols ${opts.size.cols} rows ${opts.size.rows}; ` : "";
10781
- return { command: "script", args: ["-q", "-e", "-f", "-c", `${watcher}${setup}${inner}`, "/dev/null"] };
10837
+ const line = `${watcher}${setup}${inner}`;
10838
+ if (BSD_SCRIPT_PLATFORMS.has(opts.platform ?? process.platform)) {
10839
+ return { command: "/bin/sh", args: ["-c", BSD_SCRIPT_FEED, line] };
10840
+ }
10841
+ return { command: "script", args: ["-q", "-e", "-f", "-c", line, "/dev/null"] };
10782
10842
  }
10783
10843
 
10784
10844
  // src/services/blockingPrompts.ts
@@ -10973,24 +11033,37 @@ var Block = /* @__PURE__ */ ((Block2) => {
10973
11033
  Block2[Block2["IfHeadlessSession"] = 8] = "IfHeadlessSession";
10974
11034
  Block2[Block2["IfInteractiveVariant"] = 9] = "IfInteractiveVariant";
10975
11035
  Block2[Block2["IfProjectTrust"] = 10] = "IfProjectTrust";
11036
+ Block2[Block2["IfHeadlessModel"] = 11] = "IfHeadlessModel";
10976
11037
  return Block2;
10977
11038
  })(Block || {});
10978
11039
  function isInteractive(ctx) {
10979
11040
  return ctx.interactive ?? !ctx.headless;
10980
11041
  }
11042
+ function majorVersion(version) {
11043
+ const match = version?.match(/(\d+)\.\d+/);
11044
+ return match ? Number(match[1]) : void 0;
11045
+ }
11046
+ function versionedInvocation(manifest, ctx) {
11047
+ const major = majorVersion(ctx.runnerVersion);
11048
+ if (major === void 0) return void 0;
11049
+ return (manifest.runner.versioned ?? []).filter((v) => major >= v.minMajor).sort((a, b) => b.minMajor - a.minMajor)[0];
11050
+ }
10981
11051
  function resolveArgs(manifest, ctx) {
10982
11052
  const args = [];
10983
11053
  const env = {};
10984
- if (manifest.runner.env) {
10985
- for (const [key, value] of Object.entries(manifest.runner.env)) {
11054
+ const versioned = versionedInvocation(manifest, ctx);
11055
+ const argsTemplate = versioned?.argsTemplate ?? manifest.runner.argsTemplate;
11056
+ const envTemplate = versioned ? versioned.env : manifest.runner.env;
11057
+ if (envTemplate) {
11058
+ for (const [key, value] of Object.entries(envTemplate)) {
10986
11059
  const resolved = substituteSimple(value, ctx);
10987
11060
  if (resolved.length > 0) env[key] = resolved;
10988
11061
  }
10989
11062
  }
10990
11063
  let block = 0 /* None */;
10991
11064
  let include = true;
10992
- for (let i = 0; i < manifest.runner.argsTemplate.length; i++) {
10993
- const raw = manifest.runner.argsTemplate[i];
11065
+ for (let i = 0; i < argsTemplate.length; i++) {
11066
+ const raw = argsTemplate[i];
10994
11067
  const blockOpen = parseBlockOpen(raw);
10995
11068
  if (blockOpen !== null) {
10996
11069
  if (block !== 0 /* None */) throw new ResolveError(`Nested conditional block at position ${i}: "${raw}"`);
@@ -11049,6 +11122,8 @@ function parseBlockOpen(raw) {
11049
11122
  return 9 /* IfInteractiveVariant */;
11050
11123
  case "{{if projectTrust}}":
11051
11124
  return 10 /* IfProjectTrust */;
11125
+ case "{{if headlessModel}}":
11126
+ return 11 /* IfHeadlessModel */;
11052
11127
  default:
11053
11128
  return null;
11054
11129
  }
@@ -11088,6 +11163,9 @@ function shouldIncludeBlock(block, ctx) {
11088
11163
  // there is no cwd to name.
11089
11164
  case 10 /* IfProjectTrust */:
11090
11165
  return isInteractive(ctx) && !!ctx.cwd;
11166
+ // A model flag that only the non-interactive subcommand takes.
11167
+ case 11 /* IfHeadlessModel */:
11168
+ return !!ctx.model && !isInteractive(ctx);
11091
11169
  default:
11092
11170
  return true;
11093
11171
  }
@@ -11133,6 +11211,10 @@ function resolveToken(token, manifest, ctx) {
11133
11211
  if (token === "{{model}}") return ctx.model || "";
11134
11212
  if (token === "{{thinkingEffort}}") return ctx.thinkingEffort || "";
11135
11213
  if (token === "{{mode}}") return ctx.mode || "build";
11214
+ if (token === "{{modelWithVariant}}") {
11215
+ if (!ctx.model) return "";
11216
+ return ctx.thinkingEffort ? `${ctx.model}#${ctx.thinkingEffort}` : ctx.model;
11217
+ }
11136
11218
  if (token === "{{projectTrust}}") {
11137
11219
  if (!ctx.cwd) return "";
11138
11220
  return `projects.${JSON.stringify(ctx.cwd)}.trust_level="trusted"`;
@@ -11207,13 +11289,21 @@ function resolveOpencodeVariantConfig(ctx) {
11207
11289
  }
11208
11290
  return JSON.stringify(config);
11209
11291
  }
11292
+ function resolveOpencodeSessionConfig(ctx) {
11293
+ const mode = ctx.mode || "build";
11294
+ const pinned = resolveOpencodeVariantConfig(ctx);
11295
+ const config = pinned ? JSON.parse(pinned) : {};
11296
+ config.default_agent = mode;
11297
+ if (ctx.model && !pinned) config.agent = { [mode]: { model: ctx.model } };
11298
+ return JSON.stringify(config);
11299
+ }
11210
11300
  function substituteSimple(value, ctx) {
11211
11301
  const conditioned = value.replace(/\{\{if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, name, body) => {
11212
11302
  const block = parseBlockOpen(`{{if ${name}}}`);
11213
11303
  if (block === null) return match;
11214
11304
  return shouldIncludeBlock(block, ctx) ? body : "";
11215
11305
  });
11216
- 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");
11306
+ 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");
11217
11307
  }
11218
11308
 
11219
11309
  // src/utils/launch.ts
@@ -20138,13 +20228,33 @@ function buildRunnerInvocation(opts) {
20138
20228
  mode: opts.mode ?? "build",
20139
20229
  headless: opts.headless,
20140
20230
  interactive: opts.interactive,
20141
- cwd: opts.cwd
20231
+ cwd: opts.cwd,
20232
+ runnerVersion: opts.runnerVersion
20142
20233
  });
20143
20234
  }
20144
20235
 
20145
20236
  // src/services/HeadlessRunner.ts
20146
20237
  import { spawn as nodeSpawn2, execSync } from "child_process";
20147
20238
 
20239
+ // src/services/runnerVersion.ts
20240
+ import { execFile as execFile4 } from "child_process";
20241
+ var VERSION_TIMEOUT_MS = 5e3;
20242
+ var cache = /* @__PURE__ */ new Map();
20243
+ var probeRunnerVersion = (command, PATH) => {
20244
+ const key = `${command}\0${PATH}`;
20245
+ let pending = cache.get(key);
20246
+ if (!pending) {
20247
+ pending = new Promise((resolve7) => {
20248
+ execFile4(command, ["--version"], { env: { ...process.env, PATH }, timeout: VERSION_TIMEOUT_MS }, (err, stdout) => {
20249
+ const line = String(stdout ?? "").trim();
20250
+ resolve7(err || !line ? void 0 : line);
20251
+ });
20252
+ });
20253
+ cache.set(key, pending);
20254
+ }
20255
+ return pending;
20256
+ };
20257
+
20148
20258
  // src/services/AbstractRunner.ts
20149
20259
  import { EventEmitter } from "events";
20150
20260
  var AbstractTerminalSession = class {
@@ -20265,6 +20375,10 @@ Process error: ${err.message}
20265
20375
  this.process.stdin.write(text);
20266
20376
  }
20267
20377
  }
20378
+ /** Closes the runner's stdin: what it has read so far is all it will get. */
20379
+ endInput() {
20380
+ this.process?.stdin?.end();
20381
+ }
20268
20382
  /** PTY resize requests from a surface that owns the terminal rendering the wrapper. */
20269
20383
  writeControl(text) {
20270
20384
  if (this.controlStream && !this.controlStream.destroyed) {
@@ -20277,6 +20391,7 @@ var HeadlessRunner = class extends AbstractRunner {
20277
20391
  hasScriptCmd;
20278
20392
  resolvePath;
20279
20393
  launchDeps;
20394
+ runnerVersion;
20280
20395
  spawnCount = 0;
20281
20396
  /**
20282
20397
  * Session shape: a piped subprocess is not a terminal, so runners get their
@@ -20292,6 +20407,7 @@ var HeadlessRunner = class extends AbstractRunner {
20292
20407
  this.hasScriptCmd = deps.hasScriptCmd ?? defaultHasScriptCmd;
20293
20408
  this.resolvePath = deps.resolvePath ?? augmentedPath;
20294
20409
  this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
20410
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
20295
20411
  }
20296
20412
  /**
20297
20413
  * Unique per spawn, not per task: a retry reuses its task id and ids often
@@ -20309,6 +20425,9 @@ var HeadlessRunner = class extends AbstractRunner {
20309
20425
  /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
20310
20426
  async prepareLaunch(opts, ptyOptions) {
20311
20427
  const interactive = this.defaultInteractive;
20428
+ const resolvedPath = await this.resolvePath();
20429
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
20430
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, resolvedPath) : void 0;
20312
20431
  const invocation = buildRunnerInvocation({
20313
20432
  runner: opts.runner,
20314
20433
  prompt: opts.prompt,
@@ -20319,18 +20438,18 @@ var HeadlessRunner = class extends AbstractRunner {
20319
20438
  headless: opts.headless ?? true,
20320
20439
  interactive,
20321
20440
  cwd: opts.cwd,
20441
+ runnerVersion,
20322
20442
  registry: opts.registry
20323
20443
  });
20324
- const manifest = opts.registry?.get(opts.runner)?.manifest;
20325
20444
  const pty = (manifest?.runner.requiresTty === true || interactive) && this.hasScriptCmd();
20326
- const resolvedPath = await this.resolvePath();
20327
- const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, ptyOptions) : { command: invocation.command, args: invocation.args };
20445
+ const { command, args } = pty ? wrapWithPty(invocation.command, invocation.args, { platform: this.launchDeps.platform, ...ptyOptions }) : { command: invocation.command, args: invocation.args };
20328
20446
  return {
20329
20447
  launch: await planDirectLaunch(command, args, this.launchDeps),
20330
20448
  resolvedPath,
20331
20449
  env: { ...opts.env, ...invocation.env },
20332
20450
  pty,
20333
- submitPromptKey: invocation.submitPromptKey
20451
+ submitPromptKey: invocation.submitPromptKey,
20452
+ closeStdin: !pty && !interactive && invocation.promptInArgs
20334
20453
  };
20335
20454
  }
20336
20455
  async spawn(opts) {
@@ -20341,13 +20460,14 @@ var HeadlessRunner = class extends AbstractRunner {
20341
20460
  console.error(`[headless] Starting ${opts.runner} [${modeStr}] (${opts.modelId || "default"}) for task ${opts.taskId.slice(0, 8)}${prepared.pty ? " (PTY)" : ""}`);
20342
20461
  session.start(prepared.launch, opts.cwd, prepared.resolvedPath, prepared.env);
20343
20462
  if (prepared.submitPromptKey) session.write("\r");
20463
+ if (prepared.closeStdin) session.endInput();
20344
20464
  this.registerSession(id, session);
20345
20465
  return session;
20346
20466
  }
20347
20467
  };
20348
20468
 
20349
20469
  // src/services/TmuxRunner.ts
20350
- import { execFile as execFile4 } from "child_process";
20470
+ import { execFile as execFile5 } from "child_process";
20351
20471
  import { promisify as promisify5 } from "util";
20352
20472
  import { tmpdir } from "os";
20353
20473
  import { join as join19 } from "path";
@@ -20404,7 +20524,7 @@ function clipboardCopyCommand(hasBin = defaultHasBin, platform = process.platfor
20404
20524
  var EXIT_RE = /<<<ORDEWELL_TMUX_EXIT:(\d+)>>>/;
20405
20525
  var LIVENESS_INTERVAL_MS = 5e3;
20406
20526
  var LIVENESS_MISSES = 2;
20407
- var execFileAsync2 = promisify5(execFile4);
20527
+ var execFileAsync2 = promisify5(execFile5);
20408
20528
  var defaultExecFile3 = async (command, args) => {
20409
20529
  const { stdout, stderr } = await execFileAsync2(command, args);
20410
20530
  return { stdout: String(stdout), stderr: String(stderr) };
@@ -20554,6 +20674,7 @@ var TmuxRunner = class extends AbstractRunner {
20554
20674
  pollIntervalMs;
20555
20675
  logDir;
20556
20676
  clipboardCommand;
20677
+ runnerVersion;
20557
20678
  /** Retries get a freshly named window so a failed attempt's output stays inspectable. */
20558
20679
  attempts = /* @__PURE__ */ new Map();
20559
20680
  ready = null;
@@ -20566,6 +20687,7 @@ var TmuxRunner = class extends AbstractRunner {
20566
20687
  this.pollIntervalMs = deps.pollIntervalMs ?? 500;
20567
20688
  this.logDir = deps.logDir ?? tmpdir();
20568
20689
  this.clipboardCommand = deps.clipboardCommand ?? (() => clipboardCopyCommand());
20690
+ this.runnerVersion = deps.runnerVersion ?? probeRunnerVersion;
20569
20691
  }
20570
20692
  /**
20571
20693
  * Reaps a session orphaned by a crashed prior daemon on the same port, then
@@ -20649,6 +20771,8 @@ var TmuxRunner = class extends AbstractRunner {
20649
20771
  }
20650
20772
  }
20651
20773
  async spawn(opts) {
20774
+ const manifest = opts.registry?.get(opts.runner)?.manifest;
20775
+ const runnerVersion = manifest?.runner.versioned?.length ? await this.runnerVersion(manifest.runner.command, await this.resolvePath()) : void 0;
20652
20776
  const invocation = buildRunnerInvocation({
20653
20777
  runner: opts.runner,
20654
20778
  prompt: opts.prompt,
@@ -20664,6 +20788,7 @@ var TmuxRunner = class extends AbstractRunner {
20664
20788
  headless: opts.headless ?? true,
20665
20789
  interactive: true,
20666
20790
  cwd: opts.cwd,
20791
+ runnerVersion,
20667
20792
  registry: opts.registry
20668
20793
  });
20669
20794
  await this.ensureSession();
@@ -21580,6 +21705,37 @@ var OPENCODE_MANIFEST = {
21580
21705
  env: {
21581
21706
  OPENCODE_CONFIG_CONTENT: "{{if interactiveVariant}}{{opencodeVariantConfig}}{{/if}}"
21582
21707
  },
21708
+ // OpenCode 2.x changed both shapes. Its TUI takes no `--model`/`--agent`,
21709
+ // so they ride on OPENCODE_CONFIG_CONTENT instead, and its `run` has no
21710
+ // `--variant` — the variant is a `#suffix` on the model id. Both run
21711
+ // `--standalone`: without it the TUI and `run` attach to the user's
21712
+ // background service, which neither reads this process's config nor
21713
+ // starts in the task's directory and environment.
21714
+ versioned: [{
21715
+ minMajor: 2,
21716
+ argsTemplate: [
21717
+ "{{if headlessSession}}",
21718
+ "run",
21719
+ "--agent",
21720
+ "{{mode}}",
21721
+ "{{/if}}",
21722
+ "{{if headlessModel}}",
21723
+ "--model",
21724
+ "{{modelWithVariant}}",
21725
+ "{{/if}}",
21726
+ "--standalone",
21727
+ "{{if buildMode}}",
21728
+ "--auto",
21729
+ "{{/if}}",
21730
+ "{{if interactive}}",
21731
+ "--prompt",
21732
+ "{{/if}}",
21733
+ "{{prompt}}"
21734
+ ],
21735
+ env: {
21736
+ OPENCODE_CONFIG_CONTENT: "{{if interactive}}{{opencodeSessionConfig}}{{/if}}"
21737
+ }
21738
+ }],
21583
21739
  promptInArgs: true,
21584
21740
  requiresTty: true,
21585
21741
  // `--prompt` only pre-fills the TUI's composer; it does not submit it.