@rallycry/conveyor-agent 10.13.50 → 10.13.51

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.
@@ -26,21 +26,30 @@ import {
26
26
  import {
27
27
  MAX_BETWEEN_TURN_BUFFER,
28
28
  MAX_DIAGNOSTIC_OUTPUT,
29
+ buildExitErrors,
29
30
  buildPromptBytes,
31
+ buildSpawnArgs,
32
+ cleanTerminalOutput,
30
33
  inheritedEnv,
31
34
  killPtyWithEscalation,
35
+ needsRawReadyGate,
32
36
  parseUserQuestions,
33
37
  renderPromptContentText,
38
+ resolveClaudeBinary,
34
39
  resolvePlanDialogTiming,
35
40
  resolvePtySpawn,
41
+ resolveRawTuiProbeTiming,
36
42
  resolveSubmitNudgeTiming,
37
43
  resolveSubmitRedeliveryMaxAttempts,
38
44
  resolveSubmitSettleMs,
45
+ sawTerminalSetup,
46
+ sentinelEchoed,
39
47
  sessionTempBase,
40
48
  sleep,
49
+ spawnOptionsFingerprint,
41
50
  transcriptSize,
42
51
  turnOptionsFrom
43
- } from "./chunk-XR6H326I.js";
52
+ } from "./chunk-KZQBYSMN.js";
44
53
 
45
54
  // src/setup/bootstrap.ts
46
55
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -1728,7 +1737,11 @@ import { z as z6 } from "zod";
1728
1737
  import { z as z7 } from "zod";
1729
1738
  var EXTERNAL_AGENT_MESSAGE_SOURCE = "external_agent";
1730
1739
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
1740
+ var DEFAULT_OPUS_MODEL = "claude-opus-5";
1741
+ var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
1731
1742
  var FABLE_MODEL = "claude-fable-5";
1743
+ var PREVIOUS_SONNET_MODEL = "claude-sonnet-4-6";
1744
+ var PREVIOUS_OPUS_MODEL = "claude-opus-4-8";
1732
1745
  var PTY_STREAM_PORT_BASE = 7420;
1733
1746
  var PTY_STREAM_PORT_ATTEMPTS = 8;
1734
1747
  function encodePtyStreamFrame(frame) {
@@ -1800,6 +1813,14 @@ var checkpointPathSchema = z.string().transform(normalizeCheckpointPath).pipe(
1800
1813
  var secretNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
1801
1814
  var checkpointKeySchema = z.string().regex(/^[0-9a-f]{64}$/);
1802
1815
  var checkpointDigestRefSchema = z.string().regex(/^[^\s@]+@sha256:[0-9a-f]{64}$/);
1816
+ var ACTIONS_PREBAKE_REGISTRY_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::(?:[1-9][0-9]{0,4}))?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
1817
+ var actionsPrebakeRegistrySchema = z.string().trim().min(1).regex(
1818
+ ACTIONS_PREBAKE_REGISTRY_PATTERN,
1819
+ "Actions prebake registry must be a lowercase host[:port] with an optional path prefix"
1820
+ ).refine((value) => {
1821
+ const port = /:([0-9]+)(?:\/|$)/.exec(value)?.[1];
1822
+ return !port || Number(port) <= 65535;
1823
+ }, "Actions prebake registry port must be between 1 and 65535");
1803
1824
  function uniqueSortedArray(item, minimum = 0) {
1804
1825
  return z.array(item).min(minimum).superRefine((values, ctx) => {
1805
1826
  if (new Set(values).size !== values.length) {
@@ -3174,6 +3195,30 @@ var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
3174
3195
  var TASK_CHAT_HISTORY_LIMIT = 20;
3175
3196
  var PM_CHAT_HISTORY_LIMIT = 40;
3176
3197
  var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
3198
+ function formatModelId(provider, model) {
3199
+ return `${provider}/${model}`;
3200
+ }
3201
+ function anthropicEntry(model, label, inputPerMillion, outputPerMillion, experimental = false) {
3202
+ return {
3203
+ provider: "anthropic",
3204
+ model,
3205
+ id: formatModelId("anthropic", model),
3206
+ label,
3207
+ format: "anthropic-messages",
3208
+ inputPrice: inputPerMillion / 1e6,
3209
+ outputPrice: outputPerMillion / 1e6,
3210
+ supportsTools: true,
3211
+ ...experimental ? { experimental: true } : {}
3212
+ };
3213
+ }
3214
+ var ANTHROPIC_CATALOG = [
3215
+ anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5 Latest", 15, 75),
3216
+ anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 4.8", 15, 75),
3217
+ anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest", 3, 15),
3218
+ anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 4.6", 3, 15),
3219
+ anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 0.8, 4),
3220
+ anthropicEntry(FABLE_MODEL, "Fable 5 (experimental)", 10, 50, true)
3221
+ ];
3177
3222
  var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
3178
3223
  When you write prose a person will read \u2014 chat messages, plan updates, PR titles and bodies, PR review guides (the \`publish_review_guide\` overview and section explanations), review comments \u2014 follow these rules (based on ASD-STE100 Simplified Technical English):
3179
3224
  - Use active voice. Say who does what ("The API rejects the request", not "the request is rejected").
@@ -3760,6 +3805,8 @@ function defineTool(name, description, schema, handler, options) {
3760
3805
  import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
3761
3806
  import { z as z8 } from "zod";
3762
3807
  var ClaudeCodeHarness = class {
3808
+ /** The SDK stream is itself the structured-event source. */
3809
+ emitsStructuredEvents = true;
3763
3810
  async *executeQuery(opts) {
3764
3811
  const sdkEvents = query({
3765
3812
  prompt: opts.prompt,
@@ -4611,76 +4658,6 @@ async function writeHookSettings(dir, opts = {}) {
4611
4658
  return { settingsPath, helperPath };
4612
4659
  }
4613
4660
 
4614
- // src/harness/pty/spawn-args.ts
4615
- function resolveClaudeBinary() {
4616
- return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
4617
- }
4618
- function buildSpawnArgs(input) {
4619
- const args = [];
4620
- if (input.resume) {
4621
- args.push("--resume", input.resume);
4622
- } else if (input.sessionId) {
4623
- args.push("--session-id", input.sessionId);
4624
- }
4625
- args.push("--model", input.model);
4626
- if (input.permissionMode === "bypassPermissions") {
4627
- args.push("--dangerously-skip-permissions");
4628
- } else {
4629
- args.push("--permission-mode", "plan");
4630
- }
4631
- args.push("--settings", input.settingsPath);
4632
- if (input.appendSystemPrompt) {
4633
- args.push("--append-system-prompt", input.appendSystemPrompt);
4634
- }
4635
- if (input.mcpConfigPath) {
4636
- args.push("--mcp-config", input.mcpConfigPath);
4637
- if (input.strictMcpConfig) {
4638
- args.push("--strict-mcp-config");
4639
- }
4640
- }
4641
- return args;
4642
- }
4643
- function spawnOptionsFingerprint(input) {
4644
- return JSON.stringify([
4645
- input.model,
4646
- input.permissionMode,
4647
- input.appendSystemPrompt ?? "",
4648
- input.cwd
4649
- ]);
4650
- }
4651
- var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
4652
- function cleanTerminalOutput(raw, maxChars = 1200) {
4653
- const noAnsi = raw.replace(ANSI_CSI, "");
4654
- let out = "";
4655
- for (const ch of noAnsi) {
4656
- const code = ch.charCodeAt(0);
4657
- if (ch === "\r" || ch === "\n") out += "\n";
4658
- else if (ch === " ") out += ch;
4659
- else if (code < 32 || code === 127) continue;
4660
- else out += ch;
4661
- }
4662
- const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
4663
- const text = lines.join("\n").trim();
4664
- return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
4665
- }
4666
- function isMissingBinaryFailure(tail) {
4667
- return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
4668
- }
4669
- function buildExitErrors(exitCode, rawOutput, binary) {
4670
- const errors = [`claude exited (code ${exitCode}) without a result`];
4671
- const tail = cleanTerminalOutput(rawOutput);
4672
- if (isMissingBinaryFailure(tail)) {
4673
- errors.push(
4674
- `The \`${binary}\` CLI could not be started \u2014 it is not installed or not on PATH. Install the Claude Code CLI in this environment (npm i -g @anthropic-ai/claude-code) or set CONVEYOR_CLAUDE_BIN to its absolute path.`
4675
- );
4676
- }
4677
- if (tail) {
4678
- errors.push(`Last terminal output before exit:
4679
- ${tail}`);
4680
- }
4681
- return errors;
4682
- }
4683
-
4684
4661
  // src/execution/redactor.ts
4685
4662
  var REDACTED = "<redacted>";
4686
4663
  var BEARER_RE = /\b(Bearer\s+)[A-Za-z0-9_\-.]{20,}/g;
@@ -4986,10 +4963,10 @@ async function startToolServers(mcpServers, tempDir) {
4986
4963
  servers.push(server);
4987
4964
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
4988
4965
  }
4989
- if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
4966
+ if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null, entries: config };
4990
4967
  const mcpConfigPath = join2(tempDir, "mcp-config.json");
4991
4968
  await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
4992
- return { servers, mcpConfigPath };
4969
+ return { servers, mcpConfigPath, entries: config };
4993
4970
  }
4994
4971
 
4995
4972
  // src/harness/pty/credentials.ts
@@ -5425,6 +5402,20 @@ var PtySession = class {
5425
5402
  recentOutput = "";
5426
5403
  coalescer = null;
5427
5404
  _toreDown = false;
5405
+ // Raw-TUI input detection (see awaitRawTuiInputLive). `probeWindow`, when
5406
+ // non-null, accumulates raw output so a probe can look for its own sentinel
5407
+ // coming back.
5408
+ spawnedAt = 0;
5409
+ // Set once the child writes a DEC private mode — it owns the tty from then on,
5410
+ // so the kernel no longer echoes our keystrokes and a sentinel coming back is
5411
+ // attributable to the app's own repaint.
5412
+ sawTerminalSetup = false;
5413
+ probeWindow = null;
5414
+ // Whether this process has already taken a prompt write. Only the FIRST one
5415
+ // waits on readiness: once the TUI is up, later writes (a multi-message
5416
+ // prompt, a follow-up turn) must go straight through — output is flowing
5417
+ // while the model works, so a quiet check would block until the turn ended.
5418
+ wroteToProcess = false;
5428
5419
  exitListeners = [];
5429
5420
  idleListeners = [];
5430
5421
  abortHandler = null;
@@ -5921,8 +5912,8 @@ var PtySession = class {
5921
5912
  // server doesn't load).
5922
5913
  ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
5923
5914
  });
5924
- const spawn = await resolvePtySpawn();
5925
- const pty = spawn(spec.file, spec.args, {
5915
+ const spawn2 = await resolvePtySpawn();
5916
+ const pty = spawn2(spec.file, spec.args, {
5926
5917
  name: "xterm-color",
5927
5918
  cols: this.cols,
5928
5919
  rows: this.rows,
@@ -5940,15 +5931,101 @@ var PtySession = class {
5940
5931
  }
5941
5932
  pty.onData((data) => {
5942
5933
  this.coalescer?.write(data);
5934
+ if (!this.sawTerminalSetup && sawTerminalSetup(data)) this.sawTerminalSetup = true;
5935
+ if (this.probeWindow !== null) this.probeWindow += data;
5943
5936
  this.recentOutput = (this.recentOutput + data).slice(-MAX_DIAGNOSTIC_OUTPUT);
5944
5937
  });
5945
5938
  pty.onExit((event) => {
5946
5939
  void this.finalizeOnExit(event.exitCode);
5947
5940
  });
5941
+ this.spawnedAt = Date.now();
5942
+ this.sawTerminalSetup = false;
5943
+ this.probeWindow = null;
5944
+ this.wroteToProcess = false;
5948
5945
  this.pty = pty;
5949
5946
  }
5947
+ /** Open a fresh probe-observation window over raw output. */
5948
+ openProbeWindow() {
5949
+ this.probeWindow = "";
5950
+ }
5951
+ /**
5952
+ * Write `bytes`, then wait up to `ackMs` for the TUI to render `sentinel` back.
5953
+ * Positive proof that a live input box consumed the keystrokes.
5954
+ */
5955
+ async writeAndAwaitEcho(bytes, sentinel, timing) {
5956
+ this.openProbeWindow();
5957
+ this.writeStdin(bytes);
5958
+ const deadline = Date.now() + timing.ackMs;
5959
+ while (Date.now() < deadline) {
5960
+ if (this._toreDown) return false;
5961
+ if (sentinelEchoed(this.probeWindow ?? "", sentinel)) return true;
5962
+ await sleep(timing.pollMs);
5963
+ }
5964
+ return false;
5965
+ }
5966
+ /**
5967
+ * Wait until a raw-relay TUI's input box is DEMONSTRABLY accepting keystrokes,
5968
+ * by typing a short sentinel and watching for the TUI to render it back, then
5969
+ * erasing it. Readiness is detected, never inferred from elapsed time.
5970
+ *
5971
+ * A structured-events TUI does not need this: `armSubmitNudge` re-presses
5972
+ * Enter until a transcript record proves the turn started. A raw adapter has
5973
+ * no such evidence, so the nudge is deliberately never armed for it — its
5974
+ * single paste + Enter is the only shot, and one fired during startup is
5975
+ * discarded with no trace, leaving the card parked on an empty input box.
5976
+ *
5977
+ * See resolveRawTuiProbeTiming for the measurements behind this, including the
5978
+ * signals that were tried and rejected (DECSET 2004, output-quiet windows, and
5979
+ * "any output after a keystroke" — all of which report ready too early).
5980
+ */
5981
+ async awaitRawTuiInputLive() {
5982
+ if (!needsRawReadyGate(this.adapter.capabilities)) return;
5983
+ const timing = resolveRawTuiProbeTiming();
5984
+ const { sentinel } = timing;
5985
+ const start = this.spawnedAt || Date.now();
5986
+ const setupDeadline = start + timing.firstOutputMaxMs;
5987
+ while (!this._toreDown && Date.now() < setupDeadline) {
5988
+ if (this.sawTerminalSetup) break;
5989
+ await sleep(timing.pollMs);
5990
+ }
5991
+ if (this._toreDown) return;
5992
+ const deadline = start + timing.maxMs;
5993
+ let live = false;
5994
+ let probes = 0;
5995
+ while (!this._toreDown && Date.now() < deadline) {
5996
+ probes++;
5997
+ if (await this.writeAndAwaitEcho(sentinel, sentinel, timing)) {
5998
+ live = true;
5999
+ break;
6000
+ }
6001
+ await sleep(timing.retryMs);
6002
+ }
6003
+ if (this._toreDown) return;
6004
+ if (!live) {
6005
+ process.stderr.write(
6006
+ `[conveyor-agent] raw TUI never echoed the input probe after ${probes} attempt(s) in ${Date.now() - start}ms \u2014 pasting anyway
6007
+ `
6008
+ );
6009
+ this.probeWindow = null;
6010
+ return;
6011
+ }
6012
+ for (let round = 0; round < probes && !this._toreDown; round++) {
6013
+ const gone = !await this.writeAndAwaitEcho(
6014
+ "\x7F".repeat(sentinel.length),
6015
+ sentinel,
6016
+ timing
6017
+ );
6018
+ if (gone) break;
6019
+ }
6020
+ this.probeWindow = null;
6021
+ }
5950
6022
  async deliverPrompt(text) {
5951
6023
  if (text === "" && !this.adapter.capabilities.prefill) return;
6024
+ if (!this.wroteToProcess) {
6025
+ await this.awaitRawTuiInputLive();
6026
+ if (this._toreDown) return;
6027
+ this.wroteToProcess = true;
6028
+ }
5952
6029
  this.writeStdin(this.adapter.encodePromptBytes(text));
5953
6030
  if (this.turn.promptDelivery === "prefill") return;
5954
6031
  this.deliveredTexts.push(text);
@@ -6300,6 +6377,11 @@ var PtyHarness = class _PtyHarness {
6300
6377
  bridge;
6301
6378
  adapter;
6302
6379
  static log = createServiceLogger("pty-harness");
6380
+ /** Delegated to the adapter: Claude tails a transcript + hook socket, while a
6381
+ * raw-relay TUI (opencode) has no trusted event source at all. */
6382
+ get emitsStructuredEvents() {
6383
+ return this.adapter.capabilities.structuredEvents;
6384
+ }
6303
6385
  /** Fingerprint of the spawn-time options a reused process cannot change. */
6304
6386
  fingerprintOf(options) {
6305
6387
  return this.adapter.spawnFingerprint({
@@ -6610,21 +6692,564 @@ var PtyHarness = class _PtyHarness {
6610
6692
  }
6611
6693
  };
6612
6694
 
6695
+ // src/harness/opencode/index.ts
6696
+ import { spawn } from "child_process";
6697
+
6698
+ // src/harness/pty/adapters/types.ts
6699
+ import { accessSync, constants, statSync } from "fs";
6700
+ import { join as join6 } from "path";
6701
+ var TuiUnavailableError = class extends Error {
6702
+ constructor(tui, message) {
6703
+ super(message);
6704
+ this.tui = tui;
6705
+ this.name = "TuiUnavailableError";
6706
+ }
6707
+ tui;
6708
+ };
6709
+ function isExecutable(path2) {
6710
+ try {
6711
+ if (!statSync(path2).isFile()) return false;
6712
+ accessSync(path2, constants.X_OK);
6713
+ return true;
6714
+ } catch {
6715
+ return false;
6716
+ }
6717
+ }
6718
+ function findOnPath(binary, env = process.env) {
6719
+ if (binary.includes("/")) {
6720
+ return isExecutable(binary) ? binary : null;
6721
+ }
6722
+ for (const dir of (env.PATH ?? "").split(":")) {
6723
+ if (!dir) continue;
6724
+ const candidate = join6(dir, binary);
6725
+ if (isExecutable(candidate)) return candidate;
6726
+ }
6727
+ return null;
6728
+ }
6729
+
6730
+ // src/harness/pty/adapters/opencode-auth.ts
6731
+ import { promises as fs } from "fs";
6732
+ import { dirname as dirname2, join as join7 } from "path";
6733
+ import { homedir as homedir4 } from "os";
6734
+ var logger = createServiceLogger("opencode-auth");
6735
+ var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
6736
+ var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
6737
+ function opencodeAuthPath(env) {
6738
+ const dataHome = env.XDG_DATA_HOME ?? join7(env.HOME ?? homedir4(), ".local", "share");
6739
+ return join7(dataHome, "opencode", "auth.json");
6740
+ }
6741
+ function opencodeConfigPath(env) {
6742
+ const configHome = env.XDG_CONFIG_HOME ?? join7(env.HOME ?? homedir4(), ".config");
6743
+ return join7(configHome, "opencode", "opencode.json");
6744
+ }
6745
+ function parseOauthSeed(b64) {
6746
+ if (!b64) return null;
6747
+ try {
6748
+ const parsed = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
6749
+ if (typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
6750
+ return { access: parsed.access, refresh: parsed.refresh, expires: parsed.expires };
6751
+ }
6752
+ return null;
6753
+ } catch {
6754
+ return null;
6755
+ }
6756
+ }
6757
+ function shouldSeed(existingEntry, seed) {
6758
+ if (!existingEntry || typeof existingEntry !== "object") return true;
6759
+ const entry = existingEntry;
6760
+ if (entry.type !== "oauth") return true;
6761
+ if (typeof entry.access !== "string" || typeof entry.refresh !== "string") return true;
6762
+ if (typeof entry.expires !== "number") return true;
6763
+ return entry.expires < seed.expires;
6764
+ }
6765
+ async function readJsonFile(path2) {
6766
+ try {
6767
+ return JSON.parse(await fs.readFile(path2, "utf8"));
6768
+ } catch {
6769
+ return {};
6770
+ }
6771
+ }
6772
+ async function writeJsonFile(path2, value) {
6773
+ await fs.mkdir(dirname2(path2), { recursive: true });
6774
+ await fs.writeFile(path2, `${JSON.stringify(value, null, 2)}
6775
+ `, { mode: 384 });
6776
+ }
6777
+ async function ensureAuthEntry(env, seed) {
6778
+ const path2 = opencodeAuthPath(env);
6779
+ const store = await readJsonFile(path2);
6780
+ if (!shouldSeed(store.openai, seed)) {
6781
+ logger.info("opencode oauth store is fresher than the seed; leaving it alone");
6782
+ return;
6783
+ }
6784
+ store.openai = {
6785
+ type: "oauth",
6786
+ access: seed.access,
6787
+ refresh: seed.refresh,
6788
+ expires: seed.expires
6789
+ };
6790
+ await writeJsonFile(path2, store);
6791
+ logger.info("seeded opencode oauth store entry");
6792
+ }
6793
+ async function ensurePluginConfig(env) {
6794
+ const path2 = opencodeConfigPath(env);
6795
+ const config = await readJsonFile(path2);
6796
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
6797
+ const isOurs = (p) => typeof p === "string" && (p === PLUGIN_PACKAGE || p.startsWith(`${PLUGIN_PACKAGE}@`));
6798
+ const hasExactPin = plugins.includes(OPENCODE_CODEX_PLUGIN);
6799
+ const hasStalePin = plugins.some((p) => isOurs(p) && p !== OPENCODE_CODEX_PLUGIN);
6800
+ if (hasExactPin && !hasStalePin) return;
6801
+ const kept = plugins.filter((p) => !isOurs(p));
6802
+ config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
6803
+ await writeJsonFile(path2, config);
6804
+ logger.info("ensured opencode codex-auth plugin in config");
6805
+ }
6806
+ async function seedOpenCodeOauth(env) {
6807
+ const seed = parseOauthSeed(env.CONVEYOR_OPENCODE_OAUTH);
6808
+ if (!seed) return;
6809
+ try {
6810
+ await ensureAuthEntry(env, seed);
6811
+ await ensurePluginConfig(env);
6812
+ } catch (err) {
6813
+ logger.warn(
6814
+ `failed to seed opencode oauth store: ${err instanceof Error ? err.message : String(err)}`
6815
+ );
6816
+ }
6817
+ }
6818
+
6819
+ // src/harness/opencode/credentials.ts
6820
+ var PROVIDER_KEY_ENV = {
6821
+ openai: "OPENAI_API_KEY",
6822
+ anthropic: "ANTHROPIC_API_KEY"
6823
+ };
6824
+ var DEFAULT_OPENCODE_PROVIDER = "openai";
6825
+ function buildOpenCodeCredentialEnv(source) {
6826
+ const set = {};
6827
+ const oauthActive = Boolean(source.CONVEYOR_OPENCODE_OAUTH);
6828
+ const key = source.CONVEYOR_AGENT_KEY;
6829
+ const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
6830
+ const keyEnvVar = PROVIDER_KEY_ENV[provider];
6831
+ if (key && keyEnvVar && !oauthActive) set[keyEnvVar] = key;
6832
+ return { set, unset: ["CONVEYOR_AGENT_KEY", "CONVEYOR_OPENCODE_OAUTH"] };
6833
+ }
6834
+ async function prepareOpenCodeCredentials(source = process.env) {
6835
+ await seedOpenCodeOauth(source);
6836
+ }
6837
+ function buildOpenCodeChildEnv(source, configContent) {
6838
+ const env = {};
6839
+ for (const [key, value] of Object.entries(source)) {
6840
+ if (typeof value === "string") env[key] = value;
6841
+ }
6842
+ const { set, unset } = buildOpenCodeCredentialEnv(source);
6843
+ for (const key of unset) delete env[key];
6844
+ Object.assign(env, set);
6845
+ if (configContent) env.OPENCODE_CONFIG_CONTENT = configContent;
6846
+ return env;
6847
+ }
6848
+ function resolveOpenCodeModel(source, optionsModel) {
6849
+ const model = source.CONVEYOR_AGENT_MODEL ?? optionsModel;
6850
+ if (!model) return void 0;
6851
+ if (model.includes("/")) return model;
6852
+ const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
6853
+ return `${provider}/${model}`;
6854
+ }
6855
+
6856
+ // src/harness/opencode/spawn.ts
6857
+ function resolveOpenCodeBinary(env = process.env) {
6858
+ const override = env.CONVEYOR_OPENCODE_BIN;
6859
+ const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
6860
+ if (!found) {
6861
+ throw new TuiUnavailableError(
6862
+ "opencode",
6863
+ "The opencode CLI is not available in this environment. It must be baked into the pod image (see Dockerfile.base) \u2014 re-run Build Image for this project, or set CONVEYOR_OPENCODE_BIN to its absolute path."
6864
+ );
6865
+ }
6866
+ return found;
6867
+ }
6868
+ var PROMPT_MAX_CHARS = 96e3;
6869
+ var MUTATING_TOOLS = ["edit", "bash", "external_directory"];
6870
+ function buildRunArgs(input) {
6871
+ const args = ["run", "--dir", input.cwd, "--format", "json", "--auto"];
6872
+ if (input.model) {
6873
+ args.push("--model", input.model);
6874
+ }
6875
+ if (input.resumeSessionId) args.push("--session", input.resumeSessionId);
6876
+ args.push(truncatePrompt(input.prompt));
6877
+ return args;
6878
+ }
6879
+ function truncatePrompt(prompt) {
6880
+ if (prompt.length <= PROMPT_MAX_CHARS) return prompt;
6881
+ return `${prompt.slice(0, PROMPT_MAX_CHARS)}
6882
+
6883
+ [prompt truncated at ${PROMPT_MAX_CHARS} characters]`;
6884
+ }
6885
+ function buildOpenCodeConfigContent(input) {
6886
+ const { entries, systemPromptPath, readOnly } = input;
6887
+ const mcp = {};
6888
+ for (const [name, entry] of Object.entries(entries)) {
6889
+ if (entry.type === "http") {
6890
+ mcp[name] = {
6891
+ type: "remote",
6892
+ url: entry.url,
6893
+ enabled: true,
6894
+ ...Object.keys(entry.headers).length > 0 ? { headers: entry.headers } : {}
6895
+ };
6896
+ } else {
6897
+ mcp[name] = {
6898
+ type: "local",
6899
+ command: [entry.command, ...entry.args ?? []],
6900
+ enabled: true,
6901
+ ...entry.env ? { environment: entry.env } : {}
6902
+ };
6903
+ }
6904
+ }
6905
+ const config = {};
6906
+ if (Object.keys(mcp).length > 0) config.mcp = mcp;
6907
+ if (systemPromptPath) config.instructions = [systemPromptPath];
6908
+ if (readOnly) {
6909
+ config.permission = {
6910
+ "*": "allow",
6911
+ ...Object.fromEntries(MUTATING_TOOLS.map((tool2) => [tool2, "deny"]))
6912
+ };
6913
+ }
6914
+ if (Object.keys(config).length === 0) return null;
6915
+ return JSON.stringify(config);
6916
+ }
6917
+
6918
+ // src/harness/opencode/events.ts
6919
+ function parseOpenCodeLine(line) {
6920
+ const trimmed = line.trim();
6921
+ if (!trimmed.startsWith("{")) return null;
6922
+ try {
6923
+ return JSON.parse(trimmed);
6924
+ } catch {
6925
+ return null;
6926
+ }
6927
+ }
6928
+ function numberAt(source, ...keys) {
6929
+ let cursor = source;
6930
+ for (const key of keys) {
6931
+ if (typeof cursor !== "object" || cursor === null) return 0;
6932
+ cursor = cursor[key];
6933
+ }
6934
+ return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
6935
+ }
6936
+ function accumulateUsage(event, into) {
6937
+ const part = event.part;
6938
+ if (!part) return;
6939
+ into.inputTokens += numberAt(part.tokens, "input");
6940
+ into.outputTokens += numberAt(part.tokens, "output");
6941
+ const cost = part.cost;
6942
+ if (typeof cost === "number" && Number.isFinite(cost)) into.totalCostUsd += cost;
6943
+ }
6944
+ function mapOpenCodeEvent(event) {
6945
+ const part = event.part;
6946
+ if (!part) return null;
6947
+ if (part.type === "text") return mapTextPart(part);
6948
+ if (part.type === "tool") return mapToolPart(part);
6949
+ return null;
6950
+ }
6951
+ function mapTextPart(part) {
6952
+ const text = typeof part.text === "string" ? part.text : "";
6953
+ if (text.trim() === "") return null;
6954
+ return {
6955
+ type: "assistant",
6956
+ message: { role: "assistant", content: [{ type: "text", text }] }
6957
+ };
6958
+ }
6959
+ function mapToolPart(part) {
6960
+ const status = part.state?.status;
6961
+ if (status !== "completed" && status !== "error") return null;
6962
+ return {
6963
+ type: "assistant",
6964
+ message: {
6965
+ role: "assistant",
6966
+ content: [
6967
+ {
6968
+ type: "tool_use",
6969
+ name: typeof part.tool === "string" ? part.tool : "unknown",
6970
+ input: part.state?.input ?? {},
6971
+ ...typeof part.id === "string" ? { id: part.id } : {}
6972
+ }
6973
+ ]
6974
+ }
6975
+ };
6976
+ }
6977
+ function errorMessageOf(event) {
6978
+ if (event.type !== "error") return null;
6979
+ const error = event.error;
6980
+ const message = typeof error?.data?.message === "string" ? error.data.message : null;
6981
+ const name = typeof error?.name === "string" ? error.name : "error";
6982
+ const status = typeof error?.data?.statusCode === "number" ? ` (HTTP ${error.data.statusCode})` : "";
6983
+ return message ? `${name}${status}: ${message}` : `${name}${status}`;
6984
+ }
6985
+ function sessionIdOf(event) {
6986
+ return typeof event.sessionID === "string" && event.sessionID.length > 0 ? event.sessionID : null;
6987
+ }
6988
+ function buildResultEvent(exitCode, usage, assistantText, stderrTail, reportedError) {
6989
+ if (exitCode === 0) {
6990
+ return {
6991
+ type: "result",
6992
+ subtype: "success",
6993
+ // The runner substitutes "Task completed." for an empty summary, so a run
6994
+ // that only used tools still reads sensibly in chat.
6995
+ result: assistantText,
6996
+ total_cost_usd: usage.totalCostUsd
6997
+ };
6998
+ }
6999
+ const errors = reportedError ? [reportedError] : [`opencode run exited with code ${exitCode}`];
7000
+ if (stderrTail.trim()) errors.push(`stderr:
7001
+ ${stderrTail.trim()}`);
7002
+ return { type: "result", subtype: "error", errors };
7003
+ }
7004
+
7005
+ // src/harness/opencode/index.ts
7006
+ import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
7007
+ import { join as join8 } from "path";
7008
+ var MAX_STDERR_TAIL = 4e3;
7009
+ var OpenCodeHeadlessHarness = class {
7010
+ /** NDJSON from `--format json` is a trusted structured source. */
7011
+ emitsStructuredEvents = true;
7012
+ /** opencode's session id from the last run, for `--session` on the next turn. */
7013
+ lastSessionId = null;
7014
+ /** MCP tool servers started for the in-flight run. */
7015
+ toolServers = [];
7016
+ tempDir = "";
7017
+ /** The same in-process MCP handle the PTY path uses — tools are identical. */
7018
+ createMcpServer(config) {
7019
+ return new PtyMcpServer(config.name, config.tools);
7020
+ }
7021
+ /** opencode's own session id, exposed so the runner can persist lineage. */
7022
+ get sessionId() {
7023
+ return this.lastSessionId;
7024
+ }
7025
+ async *executeQuery(opts) {
7026
+ const prompt = await collectPrompt(opts.prompt);
7027
+ const binary = resolveOpenCodeBinary(process.env);
7028
+ await prepareOpenCodeCredentials(process.env);
7029
+ this.tempDir = await mkdtemp2(join8(sessionTempBase(), "opencode-headless-"));
7030
+ const { servers, entries } = await startToolServers(
7031
+ opts.options.mcpServers ?? {},
7032
+ this.tempDir
7033
+ );
7034
+ this.toolServers = servers;
7035
+ const args = buildRunArgs({
7036
+ prompt,
7037
+ cwd: opts.options.cwd,
7038
+ model: resolveOpenCodeModel(process.env, opts.options.model),
7039
+ // `resume` is Claude-transcript-derived, so it is always absent here (there
7040
+ // is no Claude session file for an opencode run). Falling back to the id
7041
+ // opencode itself reported is what makes a multi-turn card keep its
7042
+ // conversation instead of starting fresh on every message.
7043
+ resumeSessionId: opts.resume ?? this.lastSessionId
7044
+ });
7045
+ const readOnly = opts.options.allowDangerouslySkipPermissions === false;
7046
+ const systemPromptPath = await this.writeSystemPrompt(opts.options.appendSystemPrompt);
7047
+ const configContent = buildOpenCodeConfigContent({
7048
+ entries,
7049
+ systemPromptPath,
7050
+ readOnly
7051
+ });
7052
+ const queue = new AsyncEventQueue();
7053
+ const usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
7054
+ let assistantText = "";
7055
+ let stderrTail = "";
7056
+ let reportedError = null;
7057
+ const child = spawn(binary, args, {
7058
+ cwd: opts.options.cwd,
7059
+ // stdin IGNORED — an open pipe makes `opencode run` block forever.
7060
+ stdio: ["ignore", "pipe", "pipe"],
7061
+ env: buildOpenCodeChildEnv(process.env, configContent)
7062
+ });
7063
+ const abort = opts.options.abortController;
7064
+ const onAbort = () => {
7065
+ child.kill("SIGTERM");
7066
+ };
7067
+ abort?.signal.addEventListener("abort", onAbort, { once: true });
7068
+ let carry = "";
7069
+ const ingest = (line) => {
7070
+ const { text, error } = this.ingestLine(line, usage, queue);
7071
+ if (text) assistantText += text;
7072
+ if (error) reportedError = error;
7073
+ };
7074
+ child.stdout.setEncoding("utf8");
7075
+ child.stdout.on("data", (chunk) => {
7076
+ carry += chunk;
7077
+ const lines = carry.split("\n");
7078
+ carry = lines.pop() ?? "";
7079
+ for (const line of lines) ingest(line);
7080
+ });
7081
+ child.stderr.setEncoding("utf8");
7082
+ child.stderr.on("data", (chunk) => {
7083
+ stderrTail = (stderrTail + chunk).slice(-MAX_STDERR_TAIL);
7084
+ });
7085
+ const exited = new Promise((resolve) => {
7086
+ child.once("error", () => resolve(-1));
7087
+ child.once("close", (code) => resolve(code ?? -1));
7088
+ });
7089
+ const finish = (async () => {
7090
+ const code = await exited;
7091
+ ingest(carry);
7092
+ queue.push(buildResultEvent(code, usage, assistantText.trim(), stderrTail, reportedError));
7093
+ queue.close();
7094
+ })();
7095
+ try {
7096
+ for await (const event of queue.drain()) yield event;
7097
+ await finish;
7098
+ } finally {
7099
+ abort?.signal.removeEventListener("abort", onAbort);
7100
+ if (!child.killed) child.kill("SIGTERM");
7101
+ await this.cleanup();
7102
+ }
7103
+ }
7104
+ /**
7105
+ * Persist the Conveyor system prompt (task context, plan, mode instructions)
7106
+ * so it can be referenced by the config's `instructions`. opencode has no
7107
+ * `--append-system-prompt`; without this the card runs with no context at all.
7108
+ */
7109
+ async writeSystemPrompt(text) {
7110
+ if (!text || text.trim() === "") return null;
7111
+ const path2 = join8(this.tempDir, "conveyor-instructions.md");
7112
+ await writeFile4(path2, text, "utf8");
7113
+ return path2;
7114
+ }
7115
+ /**
7116
+ * Parse one NDJSON line and push whatever it maps to. Returns assistant text so
7117
+ * the caller can accumulate the turn's summary; the session id is latched here
7118
+ * because every event carries it and any one of them will do.
7119
+ */
7120
+ ingestLine(line, usage, queue) {
7121
+ const none = { text: "", error: null };
7122
+ const parsed = parseOpenCodeLine(line);
7123
+ if (!parsed) return none;
7124
+ const sid = sessionIdOf(parsed);
7125
+ if (sid) this.lastSessionId = sid;
7126
+ accumulateUsage(parsed, usage);
7127
+ const error = errorMessageOf(parsed);
7128
+ if (error) return { text: "", error };
7129
+ const mapped = mapOpenCodeEvent(parsed);
7130
+ if (!mapped) return none;
7131
+ queue.push(mapped);
7132
+ if (mapped.type !== "assistant") return none;
7133
+ const block = mapped.message.content[0];
7134
+ return { text: block?.type === "text" && block.text ? block.text : "", error: null };
7135
+ }
7136
+ async dispose() {
7137
+ await this.cleanup();
7138
+ }
7139
+ async cleanup() {
7140
+ for (const server of this.toolServers) await server.close().catch(() => void 0);
7141
+ this.toolServers = [];
7142
+ if (this.tempDir) {
7143
+ await rm3(this.tempDir, { recursive: true, force: true }).catch(() => void 0);
7144
+ this.tempDir = "";
7145
+ }
7146
+ }
7147
+ };
7148
+ async function collectPrompt(prompt) {
7149
+ if (typeof prompt === "string") return prompt;
7150
+ const parts = [];
7151
+ for await (const message of prompt) {
7152
+ const content = message?.message?.content;
7153
+ if (typeof content === "string") parts.push(content);
7154
+ else if (Array.isArray(content)) {
7155
+ for (const block of content) {
7156
+ const b = block;
7157
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
7158
+ }
7159
+ }
7160
+ }
7161
+ return parts.join("\n\n");
7162
+ }
7163
+
6613
7164
  // src/harness/index.ts
6614
- function createHarness(kind = "sdk", ptyBridge) {
6615
- return kind === "pty" ? new PtyHarness(ptyBridge) : new ClaudeCodeHarness();
7165
+ function supportsImageBlocks(kind) {
7166
+ return kind === "sdk";
7167
+ }
7168
+ function createHarness(kind = "sdk", ptyBridge, adapter) {
7169
+ if (kind === "opencode") return new OpenCodeHeadlessHarness();
7170
+ if (kind !== "pty") return new ClaudeCodeHarness();
7171
+ return adapter ? new PtyHarness(ptyBridge, adapter) : new PtyHarness(ptyBridge);
7172
+ }
7173
+
7174
+ // src/harness/pty/adapters/opencode.ts
7175
+ var OpenCodeTuiAdapter = class {
7176
+ constructor(env = process.env) {
7177
+ this.env = env;
7178
+ }
7179
+ env;
7180
+ id = "opencode";
7181
+ capabilities = {
7182
+ resume: false,
7183
+ structuredEvents: false,
7184
+ prefill: false,
7185
+ passiveTurns: false
7186
+ };
7187
+ resolveBinary(env = this.env) {
7188
+ const override = env.CONVEYOR_OPENCODE_BIN;
7189
+ const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
7190
+ if (!found) {
7191
+ throw new TuiUnavailableError(
7192
+ "opencode",
7193
+ "The opencode CLI is not available in this environment. It must be baked into the pod image (see Dockerfile.base) \u2014 re-run Build Image for this project, or set CONVEYOR_OPENCODE_BIN to its absolute path."
7194
+ );
7195
+ }
7196
+ return found;
7197
+ }
7198
+ buildSpawn(input) {
7199
+ const env = { ...inheritedEnv() };
7200
+ const credentials = buildOpenCodeCredentialEnv(this.env);
7201
+ for (const key of credentials.unset) delete env[key];
7202
+ Object.assign(env, credentials.set);
7203
+ const model = resolveOpenCodeModel(this.env, input.options.model);
7204
+ const args = [];
7205
+ if (model) args.push("--model", model);
7206
+ return { file: this.resolveBinary(), args, env };
7207
+ }
7208
+ async prepareEnvironment() {
7209
+ await prepareOpenCodeCredentials(this.env);
7210
+ }
7211
+ spawnFingerprint(input) {
7212
+ return JSON.stringify(["opencode", input.model, input.cwd]);
7213
+ }
7214
+ encodePromptBytes(text) {
7215
+ return buildPromptBytes(text);
7216
+ }
7217
+ buildExitErrors(exitCode, rawOutput) {
7218
+ const errors = [`opencode exited (code ${exitCode}) without a result`];
7219
+ const tail = cleanTerminalOutput(rawOutput);
7220
+ if (tail) errors.push(`Last terminal output before exit:
7221
+ ${tail}`);
7222
+ return errors;
7223
+ }
7224
+ };
7225
+
7226
+ // src/harness/pty/adapters/index.ts
7227
+ function resolveTuiKindFromEnv(env) {
7228
+ const raw = env.CONVEYOR_TUI ?? "claude-code";
7229
+ if (TUI_KINDS.includes(raw)) return raw;
7230
+ throw new Error(`Unknown TUI "${raw}" in CONVEYOR_TUI (expected: ${TUI_KINDS.join(", ")})`);
7231
+ }
7232
+ function resolveTuiAdapter(kind = "claude-code") {
7233
+ switch (kind) {
7234
+ case "claude-code":
7235
+ return new ClaudeTuiAdapter();
7236
+ case "opencode":
7237
+ return new OpenCodeTuiAdapter();
7238
+ default:
7239
+ throw new Error(`Unknown TUI kind: ${kind}`);
7240
+ }
6616
7241
  }
6617
7242
 
6618
7243
  // src/harness/pty/stream-server.ts
6619
7244
  import net from "net";
6620
- var logger = createServiceLogger("PtyStreamServer");
7245
+ var logger2 = createServiceLogger("PtyStreamServer");
6621
7246
  var RING_MAX_CHARS = 256 * 1024;
6622
7247
  var PtyStreamServer = class {
6623
7248
  constructor(options) {
6624
7249
  this.options = options;
6625
7250
  this.server = net.createServer((socket) => this.handleConnection(socket));
6626
7251
  this.server.on("error", (err) => {
6627
- logger.warn(`PTY stream server error: ${err.message}`);
7252
+ logger2.warn(`PTY stream server error: ${err.message}`);
6628
7253
  });
6629
7254
  }
6630
7255
  options;
@@ -6665,7 +7290,7 @@ var PtyStreamServer = class {
6665
7290
  return port;
6666
7291
  }
6667
7292
  }
6668
- logger.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
7293
+ logger2.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
6669
7294
  return null;
6670
7295
  }
6671
7296
  tryListen(port) {
@@ -6786,7 +7411,7 @@ var PtyStreamServer = class {
6786
7411
  };
6787
7412
 
6788
7413
  // src/harness/pty/direct-stream.ts
6789
- var logger2 = createServiceLogger("PtyDirectStream");
7414
+ var logger3 = createServiceLogger("PtyDirectStream");
6790
7415
  var RELAY_COALESCE_MS = 2e3;
6791
7416
  var RELAY_MAX_BUFFER_CHARS = 48 * 1024;
6792
7417
  var DirectStreamController = class {
@@ -6835,7 +7460,7 @@ var DirectStreamController = class {
6835
7460
  });
6836
7461
  void created.listen().then((port) => this.onListening(created, port)).catch((err) => {
6837
7462
  this.starting = false;
6838
- logger2.warn(
7463
+ logger3.warn(
6839
7464
  `PTY stream server failed to start: ${err instanceof Error ? err.message : String(err)}`
6840
7465
  );
6841
7466
  });
@@ -6848,7 +7473,7 @@ var DirectStreamController = class {
6848
7473
  }
6849
7474
  this.server = created;
6850
7475
  this.reporter.reportPtyStream(port);
6851
- logger2.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
7476
+ logger3.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
6852
7477
  }
6853
7478
  /** Push the min box across both transports to the pty. */
6854
7479
  applyDims() {
@@ -10154,7 +10779,7 @@ function buildMutationTools(connection, config) {
10154
10779
  }
10155
10780
 
10156
10781
  // src/tools/attachment-tools.ts
10157
- import { basename, extname, isAbsolute, join as join6 } from "path";
10782
+ import { basename, extname, isAbsolute, join as join9 } from "path";
10158
10783
  var MIME_BY_EXT = {
10159
10784
  ".png": "image/png",
10160
10785
  ".jpg": "image/jpeg",
@@ -10205,7 +10830,7 @@ ${snippet}`;
10205
10830
  function buildUploadAttachmentTool(connection, config) {
10206
10831
  return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
10207
10832
  try {
10208
- const filePath = isAbsolute(path2) ? path2 : join6(config.workspaceDir, path2);
10833
+ const filePath = isAbsolute(path2) ? path2 : join9(config.workspaceDir, path2);
10209
10834
  const mimeType = inferMimeType(filePath);
10210
10835
  const info = await statWorkspacePath(filePath);
10211
10836
  if (!info.isFile) {
@@ -10717,7 +11342,7 @@ import { z as z16 } from "zod";
10717
11342
 
10718
11343
  // src/execution/context-path-verifier.ts
10719
11344
  import { readFile as readFile2 } from "fs/promises";
10720
- import { isAbsolute as isAbsolute2, join as join7, normalize } from "path";
11345
+ import { isAbsolute as isAbsolute2, join as join10, normalize } from "path";
10721
11346
  var PROBLEM_TEXT = {
10722
11347
  not_found: "does not exist in the repo",
10723
11348
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -10762,7 +11387,7 @@ async function verifyContextPaths(links, workspaceDir) {
10762
11387
  problems.push({ type: link.type, path: link.path, reason: shape });
10763
11388
  continue;
10764
11389
  }
10765
- const absolutePath = join7(workspaceDir, toRelativePath(link.path));
11390
+ const absolutePath = join10(workspaceDir, toRelativePath(link.path));
10766
11391
  const stat = await statWorkspacePath(absolutePath);
10767
11392
  const wantsDirectory = expectsDirectory(link.type);
10768
11393
  if (!stat.exists) {
@@ -11435,38 +12060,6 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
11435
12060
  });
11436
12061
  }
11437
12062
 
11438
- // src/harness/pty/adapters/types.ts
11439
- import { accessSync, constants, statSync } from "fs";
11440
- import { join as join8 } from "path";
11441
- var TuiUnavailableError = class extends Error {
11442
- constructor(tui, message) {
11443
- super(message);
11444
- this.tui = tui;
11445
- this.name = "TuiUnavailableError";
11446
- }
11447
- tui;
11448
- };
11449
- function isExecutable(path2) {
11450
- try {
11451
- if (!statSync(path2).isFile()) return false;
11452
- accessSync(path2, constants.X_OK);
11453
- return true;
11454
- } catch {
11455
- return false;
11456
- }
11457
- }
11458
- function findOnPath(binary, env = process.env) {
11459
- if (binary.includes("/")) {
11460
- return isExecutable(binary) ? binary : null;
11461
- }
11462
- for (const dir of (env.PATH ?? "").split(":")) {
11463
- if (!dir) continue;
11464
- const candidate = join8(dir, binary);
11465
- if (isExecutable(candidate)) return candidate;
11466
- }
11467
- return null;
11468
- }
11469
-
11470
12063
  // src/execution/playwright-mcp.ts
11471
12064
  var PLAYWRIGHT_MCP_BINARIES = ["playwright-mcp", "mcp-server-playwright"];
11472
12065
  var PLAYWRIGHT_MCP_ARGS = ["--browser", "chromium", "--headless", "--no-sandbox", "--isolated"];
@@ -11479,7 +12072,7 @@ function resolvePlaywrightMcpServer(env = process.env) {
11479
12072
  }
11480
12073
 
11481
12074
  // src/execution/event-handlers.ts
11482
- var logger3 = createServiceLogger("event-handlers");
12075
+ var logger4 = createServiceLogger("event-handlers");
11483
12076
  function safeVoid(promise, context) {
11484
12077
  if (promise && typeof promise.catch === "function") {
11485
12078
  promise.catch((err) => {
@@ -11634,7 +12227,7 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
11634
12227
  }
11635
12228
  function handleRateLimitEvent(event, host) {
11636
12229
  const { rate_limit_info } = event;
11637
- logger3.info("Rate limit event received", { rate_limit_info });
12230
+ logger4.info("Rate limit event received", { rate_limit_info });
11638
12231
  const status = rate_limit_info.status;
11639
12232
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
11640
12233
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
@@ -12357,7 +12950,7 @@ function buildCanUseTool(host) {
12357
12950
  }
12358
12951
 
12359
12952
  // src/execution/query-executor.ts
12360
- var logger4 = createServiceLogger("QueryExecutor");
12953
+ var logger5 = createServiceLogger("QueryExecutor");
12361
12954
  var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
12362
12955
  var RETRY_DELAYS_MS2 = [6e4, 12e4, 18e4, 3e5];
12363
12956
  function buildHooks(host) {
@@ -12449,7 +13042,7 @@ function repairTornSessionFile(path2) {
12449
13042
  }
12450
13043
  if (keepEnd === content.length) return false;
12451
13044
  truncateSync(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
12452
- logger4.warn("Repaired torn transcript before resume", {
13045
+ logger5.warn("Repaired torn transcript before resume", {
12453
13046
  path: path2,
12454
13047
  trimmedBytes: content.length - keepEnd
12455
13048
  });
@@ -12500,10 +13093,11 @@ function buildQueryOptions(host, context) {
12500
13093
  permissionMode: needsCanUseTool ? "plan" : "bypassPermissions",
12501
13094
  allowDangerouslySkipPermissions: !needsCanUseTool,
12502
13095
  canUseTool: buildCanUseTool(host),
12503
- // The spawned CLI never sees `systemPrompt` (an SDK-only option) — deliver
12504
- // the same text via `--append-system-prompt`. PTY-only: the SDK harness
12505
- // already receives it through systemPrompt.append.
12506
- ...host.harnessKind === "pty" && systemPromptText ? { appendSystemPrompt: systemPromptText } : {},
13096
+ // A spawned CLI never sees `systemPrompt` (an SDK-only option) — deliver the
13097
+ // same text via `appendSystemPrompt`, which each spawning harness routes its
13098
+ // own way (`claude --append-system-prompt`; opencode an `instructions` file).
13099
+ // Without this an opencode card would run with no task context at all.
13100
+ ...host.harnessKind !== "sdk" && systemPromptText ? { appendSystemPrompt: systemPromptText } : {},
12507
13101
  // Auto mode pre-exit (residual sessions only — auto now boots post-exit):
12508
13102
  // after the ExitPlanMode hook allows the call, the CLI's plan dialog still
12509
13103
  // renders — press Enter so the autonomous agent continues building in the
@@ -12529,7 +13123,7 @@ function buildQueryOptions(host, context) {
12529
13123
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
12530
13124
  enableFileCheckpointing: settings.enableFileCheckpointing,
12531
13125
  stderr: (data) => {
12532
- logger4.warn("Claude Code stderr", { data: data.trimEnd() });
13126
+ logger5.warn("Claude Code stderr", { data: data.trimEnd() });
12533
13127
  }
12534
13128
  };
12535
13129
  }
@@ -12580,16 +13174,16 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
12580
13174
 
12581
13175
  The team says:
12582
13176
  ${followUpText}` : followUpText;
12583
- const isPty = host.harnessKind === "pty";
13177
+ const skipImages = !supportsImageBlocks(host.harnessKind);
12584
13178
  if (isPmMode) {
12585
- const prompt = buildMultimodalPrompt(textPrompt, context, isPty);
13179
+ const prompt = buildMultimodalPrompt(textPrompt, context, skipImages);
12586
13180
  if (followUpImages.length > 0 && Array.isArray(prompt)) {
12587
13181
  prompt.push(...followUpImages);
12588
13182
  }
12589
13183
  return prompt;
12590
13184
  }
12591
13185
  if (followUpImages.length > 0) {
12592
- if (isPty) {
13186
+ if (skipImages) {
12593
13187
  const refs = followUpImages.map(
12594
13188
  () => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
12595
13189
  );
@@ -12639,7 +13233,7 @@ function abortWatchedTurn(host, stderrLine, chatMessage) {
12639
13233
  host.connection.sendEvent({ type: "error", message: chatMessage });
12640
13234
  host.abortController?.abort();
12641
13235
  }
12642
- async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead) {
13236
+ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead, disableWedgeAbort) {
12643
13237
  if (state.abortedAsWedged) {
12644
13238
  process.stderr.write(
12645
13239
  "[conveyor-agent] Wedged turn did not settle after abort \u2014 abandoning the turn stream\n"
@@ -12657,6 +13251,10 @@ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMou
12657
13251
  return "keep_waiting";
12658
13252
  }
12659
13253
  if (state.silentMs < silenceTimeoutMs) return "keep_waiting";
13254
+ if (disableWedgeAbort) {
13255
+ state.silentMs = 0;
13256
+ return "keep_waiting";
13257
+ }
12660
13258
  if (isHeavyGateActive()) {
12661
13259
  state.silentMs = 0;
12662
13260
  return "keep_waiting";
@@ -12693,7 +13291,8 @@ async function* watchForParkedTui(inner, host, opts) {
12693
13291
  state,
12694
13292
  waitMs,
12695
13293
  silenceTimeoutMs,
12696
- opts?.probeMountDead
13294
+ opts?.probeMountDead,
13295
+ opts?.disableWedgeAbort
12697
13296
  );
12698
13297
  if (outcome === "abandon") return;
12699
13298
  continue;
@@ -12740,7 +13339,7 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
12740
13339
  await runFollowUpQuery(host, context, options, resume, followUpContent);
12741
13340
  return;
12742
13341
  }
12743
- if (isDiscoveryLike && (resume || host.harnessKind !== "pty")) {
13342
+ if (isDiscoveryLike && (resume || host.harnessKind === "sdk")) {
12744
13343
  return;
12745
13344
  }
12746
13345
  await runInitialQuery(host, context, options, resume, promptDelivery);
@@ -12795,9 +13394,12 @@ async function runPassiveTurn(host, context) {
12795
13394
  }
12796
13395
  async function trackAndRun(host, context, options, agentQuery) {
12797
13396
  if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
12798
- agentQuery = watchForParkedTui(agentQuery, host, {
12799
- probeMountDead: () => isConfigHomeMountDead(options.cwd)
12800
- });
13397
+ const rawRelay = host.harness.emitsStructuredEvents === false;
13398
+ agentQuery = watchForParkedTui(
13399
+ agentQuery,
13400
+ host,
13401
+ rawRelay ? { disableWedgeAbort: true } : { probeMountDead: () => isConfigHomeMountDead(options.cwd) }
13402
+ );
12801
13403
  }
12802
13404
  host.activeQuery = agentQuery;
12803
13405
  try {
@@ -12822,9 +13424,10 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
12822
13424
  };
12823
13425
  }
12824
13426
  return {
12825
- // On the PTY harness image blocks would be pasted into the TUI as text —
12826
- // the prompt body already links each image (get_attachment / downloadUrl).
12827
- prompt: buildMultimodalPrompt(initialPrompt, context, harnessKind === "pty"),
13427
+ // Only the SDK takes image blocks: a PTY would paste them into the TUI as
13428
+ // text and headless opencode passes argv — the prompt body already links
13429
+ // each image (get_attachment / downloadUrl).
13430
+ prompt: buildMultimodalPrompt(initialPrompt, context, !supportsImageBlocks(harnessKind)),
12828
13431
  appendSystemPrompt: baseAppendSystemPrompt
12829
13432
  };
12830
13433
  }
@@ -12865,7 +13468,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
12865
13468
  const retryPrompt = buildMultimodalPrompt(
12866
13469
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12867
13470
  context,
12868
- lastErrorWasImage || host.harnessKind === "pty"
13471
+ lastErrorWasImage || !supportsImageBlocks(host.harnessKind)
12869
13472
  );
12870
13473
  return host.harness.executeQuery({
12871
13474
  prompt: host.createInputStream(retryPrompt),
@@ -12894,7 +13497,7 @@ async function handleAuthError(context, host, options) {
12894
13497
  const freshPrompt = buildMultimodalPrompt(
12895
13498
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12896
13499
  context,
12897
- host.harnessKind === "pty"
13500
+ !supportsImageBlocks(host.harnessKind)
12898
13501
  );
12899
13502
  const freshQuery = host.harness.executeQuery({
12900
13503
  prompt: host.createInputStream(freshPrompt),
@@ -12909,7 +13512,7 @@ async function handleStaleSession(context, host, options) {
12909
13512
  const freshPrompt = buildMultimodalPrompt(
12910
13513
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12911
13514
  context,
12912
- host.harnessKind === "pty"
13515
+ !supportsImageBlocks(host.harnessKind)
12913
13516
  );
12914
13517
  const freshQuery = host.harness.executeQuery({
12915
13518
  prompt: host.createInputStream(freshPrompt),
@@ -13003,7 +13606,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
13003
13606
  const freshPrompt = buildMultimodalPrompt(
13004
13607
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13005
13608
  context,
13006
- host.harnessKind === "pty"
13609
+ !supportsImageBlocks(host.harnessKind)
13007
13610
  );
13008
13611
  const freshQuery = host.harness.executeQuery({
13009
13612
  prompt: host.createInputStream(freshPrompt),
@@ -13076,9 +13679,14 @@ async function runWithRetry(initialQuery, context, host, options) {
13076
13679
  }
13077
13680
 
13078
13681
  // src/runner/query-bridge.ts
13079
- var logger5 = createServiceLogger("QueryBridge");
13080
- function resolveHarnessKind() {
13081
- return process.env.CONVEYOR_FORCE_SDK_CARDS === "1" ? "sdk" : "pty";
13682
+ var logger6 = createServiceLogger("QueryBridge");
13683
+ function resolveHarnessKind(mode) {
13684
+ if (process.env.CONVEYOR_FORCE_SDK_CARDS === "1") return "sdk";
13685
+ return resolveCardTui(mode) === "opencode" ? "opencode" : "pty";
13686
+ }
13687
+ function resolveCardTui(mode) {
13688
+ if (mode === "code-review") return "claude-code";
13689
+ return resolveTuiKindFromEnv(process.env);
13082
13690
  }
13083
13691
  function buildPtyBridge(connection, onBackgroundTaskDone) {
13084
13692
  return {
@@ -13099,7 +13707,7 @@ var QueryBridge = class {
13099
13707
  this.mode = mode;
13100
13708
  this.runnerConfig = runnerConfig;
13101
13709
  this.callbacks = callbacks;
13102
- const harnessKind = resolveHarnessKind();
13710
+ const harnessKind = resolveHarnessKind(runnerConfig.mode);
13103
13711
  this.harnessKind = harnessKind;
13104
13712
  let bridge;
13105
13713
  if (harnessKind === "pty") {
@@ -13109,7 +13717,11 @@ var QueryBridge = class {
13109
13717
  );
13110
13718
  bridge = this.directStream.bridge;
13111
13719
  }
13112
- this.harness = createHarness(harnessKind, bridge);
13720
+ this.harness = createHarness(
13721
+ harnessKind,
13722
+ bridge,
13723
+ harnessKind === "pty" ? resolveTuiAdapter(resolveCardTui(runnerConfig.mode)) : void 0
13724
+ );
13113
13725
  }
13114
13726
  connection;
13115
13727
  mode;
@@ -13249,9 +13861,9 @@ var QueryBridge = class {
13249
13861
  const msg = err instanceof Error ? err.message : String(err);
13250
13862
  const isAbort = this._stopped || /abort/i.test(msg);
13251
13863
  if (isAbort) {
13252
- logger5.info("Query stopped by user", { error: msg });
13864
+ logger6.info("Query stopped by user", { error: msg });
13253
13865
  } else {
13254
- logger5.error("Query execution failed", { error: msg });
13866
+ logger6.error("Query execution failed", { error: msg });
13255
13867
  this.connection.sendEvent({ type: "error", message: msg });
13256
13868
  }
13257
13869
  } finally {
@@ -13277,9 +13889,9 @@ var QueryBridge = class {
13277
13889
  const msg = err instanceof Error ? err.message : String(err);
13278
13890
  const isAbort = this._stopped || /abort/i.test(msg);
13279
13891
  if (isAbort) {
13280
- logger5.info("Passive turn stopped", { error: msg });
13892
+ logger6.info("Passive turn stopped", { error: msg });
13281
13893
  } else {
13282
- logger5.error("Passive turn failed", { error: msg });
13894
+ logger6.error("Passive turn failed", { error: msg });
13283
13895
  this.connection.sendEvent({ type: "error", message: msg });
13284
13896
  }
13285
13897
  } finally {
@@ -13466,11 +14078,11 @@ function parseResetInstant(rowText, now, defaultTz = localTimeZone()) {
13466
14078
 
13467
14079
  // src/usage/parse-usage.ts
13468
14080
  var ESC = "\\u001b";
13469
- var ANSI_CSI2 = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
14081
+ var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
13470
14082
  var ANSI_OSC = new RegExp(`${ESC}\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\)`, "g");
13471
14083
  var BAR_GLYPHS = /[─-▟]/g;
13472
14084
  function normalizeUsageText(stdout) {
13473
- return stdout.replace(ANSI_CSI2, "").replace(ANSI_OSC, "").replace(BAR_GLYPHS, " ").replace(/\r/g, "");
14085
+ return stdout.replace(ANSI_CSI, "").replace(ANSI_OSC, "").replace(BAR_GLYPHS, " ").replace(/\r/g, "");
13474
14086
  }
13475
14087
  function parseUsageGauges(stdout, now = Date.now(), defaultTz) {
13476
14088
  const text = normalizeUsageText(stdout);
@@ -13571,10 +14183,10 @@ var UsageProbeRun = class {
13571
14183
  }
13572
14184
  };
13573
14185
  async function runUsageProbe(deps = {}) {
13574
- let spawn = deps.spawn;
13575
- if (!spawn) {
14186
+ let spawn2 = deps.spawn;
14187
+ if (!spawn2) {
13576
14188
  try {
13577
- spawn = await resolvePtySpawn();
14189
+ spawn2 = await resolvePtySpawn();
13578
14190
  } catch {
13579
14191
  return "";
13580
14192
  }
@@ -13590,7 +14202,7 @@ async function runUsageProbe(deps = {}) {
13590
14202
  return new Promise((resolve) => {
13591
14203
  let child;
13592
14204
  try {
13593
- child = spawn(binary, [], {
14205
+ child = spawn2(binary, [], {
13594
14206
  name: "xterm-256color",
13595
14207
  cols: 120,
13596
14208
  rows: 45,
@@ -13606,7 +14218,7 @@ async function runUsageProbe(deps = {}) {
13606
14218
  }
13607
14219
 
13608
14220
  // src/execution/usage-sampler.ts
13609
- var logger6 = createServiceLogger("usage-sampler");
14221
+ var logger7 = createServiceLogger("usage-sampler");
13610
14222
  function isAttributable(identity, sessionToken) {
13611
14223
  if (!identity) return { ok: true };
13612
14224
  if (identity.hasRefreshToken) {
@@ -13622,7 +14234,7 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
13622
14234
  try {
13623
14235
  const attributable = isAttributable(await readIdentity(), token);
13624
14236
  if (!attributable.ok) {
13625
- logger6.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
14237
+ logger7.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
13626
14238
  reason: attributable.reason
13627
14239
  });
13628
14240
  return [];
@@ -13649,14 +14261,14 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
13649
14261
  });
13650
14262
  }
13651
14263
  if (samples.length === 0) {
13652
- logger6.info("usage sample produced no gauges", {
14264
+ logger7.info("usage sample produced no gauges", {
13653
14265
  stdoutLength: stdout.length,
13654
14266
  stdoutHead: stdout.slice(0, 200).replaceAll("\n", " ")
13655
14267
  });
13656
14268
  }
13657
14269
  return samples;
13658
14270
  } catch (error) {
13659
- logger6.info("usage sample failed", {
14271
+ logger7.info("usage sample failed", {
13660
14272
  error: error instanceof Error ? error.message : String(error)
13661
14273
  });
13662
14274
  return [];
@@ -15232,12 +15844,12 @@ var SessionRunner = class _SessionRunner {
15232
15844
  };
15233
15845
 
15234
15846
  // src/setup/config.ts
15235
- import { join as join9 } from "path";
15847
+ import { join as join11 } from "path";
15236
15848
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
15237
15849
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
15238
15850
  async function loadForwardPorts(workspaceDir) {
15239
15851
  try {
15240
- const raw = await readWorkspaceFile(join9(workspaceDir, DEVCONTAINER_PATH));
15852
+ const raw = await readWorkspaceFile(join11(workspaceDir, DEVCONTAINER_PATH));
15241
15853
  const parsed = JSON.parse(raw);
15242
15854
  const ports = (parsed.forwardPorts ?? []).filter(
15243
15855
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -15292,16 +15904,15 @@ export {
15292
15904
  applyBootstrapToEnv,
15293
15905
  AgentConnection,
15294
15906
  DEFAULT_SONNET_MODEL,
15295
- TUI_KINDS,
15296
15907
  isPermissionDeniedError,
15297
15908
  DEFAULT_LIFECYCLE_CONFIG,
15298
15909
  Lifecycle,
15299
- cleanTerminalOutput,
15300
15910
  buildSynthesizedCredentials,
15301
15911
  claudeJsonPath,
15302
- ClaudeTuiAdapter,
15303
15912
  createServiceLogger,
15304
15913
  PtyHarness,
15914
+ resolveTuiKindFromEnv,
15915
+ resolveTuiAdapter,
15305
15916
  workspacePathExists,
15306
15917
  GIT_TIMEOUT_MS,
15307
15918
  hasUncommittedChanges,
@@ -15312,8 +15923,6 @@ export {
15312
15923
  flushPendingChanges,
15313
15924
  pushToOrigin,
15314
15925
  buildProjectTools,
15315
- TuiUnavailableError,
15316
- findOnPath,
15317
15926
  resolvePlaywrightMcpServer,
15318
15927
  resolveSessionStart,
15319
15928
  parseUsageGauges,
@@ -15328,4 +15937,4 @@ export {
15328
15937
  loadConveyorConfig,
15329
15938
  unshallowRepo
15330
15939
  };
15331
- //# sourceMappingURL=chunk-36VMMHYD.js.map
15940
+ //# sourceMappingURL=chunk-EQO7QFGN.js.map