@rallycry/conveyor-agent 10.13.50 → 10.13.52

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-UAGIXPYD.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,
@@ -3799,8 +3846,237 @@ var ClaudeCodeHarness = class {
3799
3846
 
3800
3847
  // src/harness/pty/session.ts
3801
3848
  import { randomUUID } from "crypto";
3802
- import { mkdtemp, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3803
- import { join as join4, dirname } from "path";
3849
+ import { mkdtemp, mkdir as mkdir3, rm as rm2, writeFile as writeFile5 } from "fs/promises";
3850
+ import { join as join5, dirname } from "path";
3851
+
3852
+ // src/harness/opencode/plugin.ts
3853
+ import { writeFile } from "fs/promises";
3854
+ import { join } from "path";
3855
+ var OPENCODE_EVENTS_FILE_ENV = "CONVEYOR_OPENCODE_EVENTS_FILE";
3856
+ var CONVEYOR_PLUGIN_SOURCE = `import { appendFileSync } from "node:fs";
3857
+
3858
+ export const ConveyorEventsPlugin = async () => ({
3859
+ event: async ({ event }) => {
3860
+ const sink = process.env.${OPENCODE_EVENTS_FILE_ENV};
3861
+ if (!sink || !event || event.type === "message.part.delta") return;
3862
+ try {
3863
+ appendFileSync(sink, JSON.stringify(event) + "\\n");
3864
+ } catch {
3865
+ // Never let a sink hiccup break the user's opencode session.
3866
+ }
3867
+ },
3868
+ });
3869
+ `;
3870
+ async function stageOpenCodePlugin(tempDir) {
3871
+ const pluginPath = join(tempDir, "conveyor-events-plugin.mjs");
3872
+ const eventsSinkPath = join(tempDir, "opencode-events.ndjson");
3873
+ await writeFile(pluginPath, CONVEYOR_PLUGIN_SOURCE, "utf8");
3874
+ await writeFile(eventsSinkPath, "", "utf8");
3875
+ return { pluginPath, eventsSinkPath };
3876
+ }
3877
+
3878
+ // src/harness/opencode/events.ts
3879
+ function parseOpenCodeLine(line) {
3880
+ const trimmed = line.trim();
3881
+ if (!trimmed.startsWith("{")) return null;
3882
+ try {
3883
+ return JSON.parse(trimmed);
3884
+ } catch {
3885
+ return null;
3886
+ }
3887
+ }
3888
+ function numberAt(source, ...keys) {
3889
+ let cursor = source;
3890
+ for (const key of keys) {
3891
+ if (typeof cursor !== "object" || cursor === null) return 0;
3892
+ cursor = cursor[key];
3893
+ }
3894
+ return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
3895
+ }
3896
+ function accumulateUsage(event, into) {
3897
+ const part = event.part;
3898
+ if (!part) return;
3899
+ into.inputTokens += numberAt(part.tokens, "input");
3900
+ into.outputTokens += numberAt(part.tokens, "output");
3901
+ const cost = part.cost;
3902
+ if (typeof cost === "number" && Number.isFinite(cost)) into.totalCostUsd += cost;
3903
+ }
3904
+ function mapOpenCodeEvent(event) {
3905
+ const part = event.part;
3906
+ if (!part) return null;
3907
+ if (part.type === "text") return mapTextPart(part);
3908
+ if (part.type === "tool") return mapToolPart(part);
3909
+ return null;
3910
+ }
3911
+ function mapTextPart(part) {
3912
+ const text = typeof part.text === "string" ? part.text : "";
3913
+ if (text.trim() === "") return null;
3914
+ return {
3915
+ type: "assistant",
3916
+ message: { role: "assistant", content: [{ type: "text", text }] }
3917
+ };
3918
+ }
3919
+ function mapToolPart(part) {
3920
+ const status = part.state?.status;
3921
+ if (status !== "completed" && status !== "error") return null;
3922
+ return {
3923
+ type: "assistant",
3924
+ message: {
3925
+ role: "assistant",
3926
+ content: [
3927
+ {
3928
+ type: "tool_use",
3929
+ name: typeof part.tool === "string" ? part.tool : "unknown",
3930
+ input: part.state?.input ?? {},
3931
+ ...typeof part.id === "string" ? { id: part.id } : {}
3932
+ }
3933
+ ]
3934
+ }
3935
+ };
3936
+ }
3937
+ function errorMessageOf(event) {
3938
+ if (event.type !== "error") return null;
3939
+ const error = event.error;
3940
+ const message = typeof error?.data?.message === "string" ? error.data.message : null;
3941
+ const name = typeof error?.name === "string" ? error.name : "error";
3942
+ const status = typeof error?.data?.statusCode === "number" ? ` (HTTP ${error.data.statusCode})` : "";
3943
+ return message ? `${name}${status}: ${message}` : `${name}${status}`;
3944
+ }
3945
+ function sessionIdOf(event) {
3946
+ return typeof event.sessionID === "string" && event.sessionID.length > 0 ? event.sessionID : null;
3947
+ }
3948
+ function busSessionIdOf(event) {
3949
+ const direct = event.properties?.sessionID;
3950
+ if (typeof direct === "string" && direct.length > 0) return direct;
3951
+ const viaInfo = event.properties?.info?.sessionID;
3952
+ return typeof viaInfo === "string" && viaInfo.length > 0 ? viaInfo : null;
3953
+ }
3954
+ function isIdleEvent(event) {
3955
+ if (event.type === "session.idle") return true;
3956
+ return event.type === "session.status" && event.properties?.status?.type === "idle";
3957
+ }
3958
+ function isBusyEvent(event) {
3959
+ return event.type === "session.status" && event.properties?.status?.type === "busy";
3960
+ }
3961
+ function busInfoOf(event) {
3962
+ if (event.type !== "message.updated") return null;
3963
+ return event.properties?.info ?? null;
3964
+ }
3965
+ function busPartOf(event) {
3966
+ if (event.type !== "message.part.updated") return null;
3967
+ return event.properties?.part ?? null;
3968
+ }
3969
+ function buildResultEvent(exitCode, usage, assistantText, stderrTail, reportedError) {
3970
+ if (exitCode === 0) {
3971
+ return {
3972
+ type: "result",
3973
+ subtype: "success",
3974
+ // The runner substitutes "Task completed." for an empty summary, so a run
3975
+ // that only used tools still reads sensibly in chat.
3976
+ result: assistantText,
3977
+ total_cost_usd: usage.totalCostUsd
3978
+ };
3979
+ }
3980
+ const errors = reportedError ? [reportedError] : [`opencode run exited with code ${exitCode}`];
3981
+ if (stderrTail.trim()) errors.push(`stderr:
3982
+ ${stderrTail.trim()}`);
3983
+ return { type: "result", subtype: "error", errors };
3984
+ }
3985
+
3986
+ // src/harness/opencode/event-source.ts
3987
+ var MAX_TRACKED_MESSAGES = 200;
3988
+ var OpenCodeEventSource = class {
3989
+ constructor(emit, onSessionId) {
3990
+ this.emit = emit;
3991
+ this.onSessionId = onSessionId;
3992
+ }
3993
+ emit;
3994
+ onSessionId;
3995
+ roles = /* @__PURE__ */ new Map();
3996
+ usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
3997
+ assistantText = "";
3998
+ /** Busy has been seen and idle has not — a turn is in flight on the bus. */
3999
+ active = false;
4000
+ latchedSessionId = null;
4001
+ /** The opencode-assigned session id, once any event has carried it. */
4002
+ get sessionId() {
4003
+ return this.latchedSessionId;
4004
+ }
4005
+ /**
4006
+ * Ingest one parsed sink record. Self-arming: `busy` opens a turn (resetting
4007
+ * the accumulators) whether it came from our pasted prompt or a human typing
4008
+ * into the parked TUI — the passive case is exactly why turn state cannot
4009
+ * only reset from beginTurn.
4010
+ */
4011
+ handleRecord(raw) {
4012
+ if (typeof raw !== "object" || raw === null) return;
4013
+ const event = raw;
4014
+ this.latchSessionId(event);
4015
+ const info = busInfoOf(event);
4016
+ if (info?.id && info.role) this.trackRole(info.id, info.role);
4017
+ if (isBusyEvent(event)) {
4018
+ if (!this.active) this.beginBusTurn();
4019
+ return;
4020
+ }
4021
+ if (isIdleEvent(event)) {
4022
+ this.finishBusTurn();
4023
+ return;
4024
+ }
4025
+ if (event.type === "session.error") {
4026
+ this.finishBusTurn(describeSessionError(event));
4027
+ return;
4028
+ }
4029
+ this.handlePart(event);
4030
+ }
4031
+ handlePart(event) {
4032
+ const part = busPartOf(event);
4033
+ if (!part) return;
4034
+ if (!this.active) this.beginBusTurn();
4035
+ if (typeof part.messageID !== "string" || this.roles.get(part.messageID) !== "assistant") {
4036
+ return;
4037
+ }
4038
+ accumulateUsage({ part }, this.usage);
4039
+ const mapped = mapOpenCodeEvent({ part });
4040
+ if (!mapped) return;
4041
+ this.emit(mapped);
4042
+ if (mapped.type === "assistant") {
4043
+ const block = mapped.message.content[0];
4044
+ if (block?.type === "text" && block.text) this.assistantText += block.text;
4045
+ }
4046
+ }
4047
+ beginBusTurn() {
4048
+ this.active = true;
4049
+ this.usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
4050
+ this.assistantText = "";
4051
+ }
4052
+ /** Idle (or an error) closes the turn exactly once. */
4053
+ finishBusTurn(error) {
4054
+ if (!this.active) return;
4055
+ this.active = false;
4056
+ this.emit(
4057
+ error ? buildResultEvent(1, this.usage, "", "", error) : buildResultEvent(0, this.usage, this.assistantText.trim(), "")
4058
+ );
4059
+ }
4060
+ latchSessionId(event) {
4061
+ if (this.latchedSessionId) return;
4062
+ const id = busSessionIdOf(event);
4063
+ if (!id) return;
4064
+ this.latchedSessionId = id;
4065
+ this.onSessionId?.(id);
4066
+ }
4067
+ trackRole(id, role) {
4068
+ this.roles.set(id, role);
4069
+ if (this.roles.size > MAX_TRACKED_MESSAGES) {
4070
+ const oldest = this.roles.keys().next().value;
4071
+ if (oldest !== void 0) this.roles.delete(oldest);
4072
+ }
4073
+ }
4074
+ };
4075
+ function describeSessionError(event) {
4076
+ const error = event.properties?.error;
4077
+ if (error && typeof error.message === "string") return error.message;
4078
+ return `opencode reported a session error: ${JSON.stringify(event.properties ?? {}).slice(0, 300)}`;
4079
+ }
3804
4080
 
3805
4081
  // src/harness/pty/event-queue.ts
3806
4082
  var AsyncEventQueue = class {
@@ -4081,14 +4357,16 @@ function mapTranscriptRecord(raw) {
4081
4357
  // src/harness/pty/jsonl-tailer.ts
4082
4358
  var POLL_INTERVAL_MS = 25;
4083
4359
  var JsonlTailer = class {
4084
- constructor(path2, onEvent, onRawRecord) {
4360
+ constructor(path2, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
4085
4361
  this.path = path2;
4086
4362
  this.onEvent = onEvent;
4087
4363
  this.onRawRecord = onRawRecord;
4364
+ this.mapRecord = mapRecord;
4088
4365
  }
4089
4366
  path;
4090
4367
  onEvent;
4091
4368
  onRawRecord;
4369
+ mapRecord;
4092
4370
  offset = 0;
4093
4371
  buffer = "";
4094
4372
  timer = null;
@@ -4161,7 +4439,7 @@ var JsonlTailer = class {
4161
4439
  return;
4162
4440
  }
4163
4441
  this.onRawRecord?.(parsed);
4164
- const event = mapTranscriptRecord(parsed);
4442
+ const event = this.mapRecord(parsed);
4165
4443
  if (event) this.onEvent(event);
4166
4444
  }
4167
4445
  };
@@ -4354,17 +4632,17 @@ function mapChatRecords(raw) {
4354
4632
  }
4355
4633
 
4356
4634
  // src/harness/pty/settings.ts
4357
- import { mkdir, writeFile, chmod } from "fs/promises";
4635
+ import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
4358
4636
  import { homedir } from "os";
4359
- import { join } from "path";
4637
+ import { join as join2 } from "path";
4360
4638
  function claudeConfigHome() {
4361
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
4639
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
4362
4640
  }
4363
4641
  function projectSlug(cwd) {
4364
4642
  return cwd.replace(/\//g, "-");
4365
4643
  }
4366
4644
  function sessionTranscriptPath(cwd, sessionId) {
4367
- return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
4645
+ return join2(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
4368
4646
  }
4369
4647
  var ALLOW_RULES = [
4370
4648
  "Bash",
@@ -4567,10 +4845,10 @@ function buildPreToolUseHooks(helperPath, gateAllTools) {
4567
4845
  ];
4568
4846
  }
4569
4847
  async function writeHookSettings(dir, opts = {}) {
4570
- const helperPath = join(dir, "hook-helper.cjs");
4571
- const settingsPath = join(dir, "settings.json");
4848
+ const helperPath = join2(dir, "hook-helper.cjs");
4849
+ const settingsPath = join2(dir, "settings.json");
4572
4850
  await mkdir(dir, { recursive: true });
4573
- await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
4851
+ await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
4574
4852
  await chmod(helperPath, 493);
4575
4853
  const settings = {
4576
4854
  // Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
@@ -4607,80 +4885,10 @@ async function writeHookSettings(dir, opts = {}) {
4607
4885
  ]
4608
4886
  }
4609
4887
  };
4610
- await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4888
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4611
4889
  return { settingsPath, helperPath };
4612
4890
  }
4613
4891
 
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
4892
  // src/execution/redactor.ts
4685
4893
  var REDACTED = "<redacted>";
4686
4894
  var BEARER_RE = /\b(Bearer\s+)[A-Za-z0-9_\-.]{20,}/g;
@@ -4795,8 +5003,8 @@ var PtyOutputCoalescer = class {
4795
5003
  // src/harness/pty/tool-server.ts
4796
5004
  import { createServer as createServer2 } from "http";
4797
5005
  import { z as z9 } from "zod";
4798
- import { writeFile as writeFile2 } from "fs/promises";
4799
- import { join as join2 } from "path";
5006
+ import { writeFile as writeFile3 } from "fs/promises";
5007
+ import { join as join3 } from "path";
4800
5008
  import { randomBytes } from "crypto";
4801
5009
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4802
5010
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -4986,20 +5194,20 @@ async function startToolServers(mcpServers, tempDir) {
4986
5194
  servers.push(server);
4987
5195
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
4988
5196
  }
4989
- if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
4990
- const mcpConfigPath = join2(tempDir, "mcp-config.json");
4991
- await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
4992
- return { servers, mcpConfigPath };
5197
+ if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null, entries: config };
5198
+ const mcpConfigPath = join3(tempDir, "mcp-config.json");
5199
+ await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
5200
+ return { servers, mcpConfigPath, entries: config };
4993
5201
  }
4994
5202
 
4995
5203
  // src/harness/pty/credentials.ts
4996
- import { chmod as chmod2, mkdir as mkdir2, readFile, rm, writeFile as writeFile3 } from "fs/promises";
5204
+ import { chmod as chmod2, mkdir as mkdir2, readFile, rm, writeFile as writeFile4 } from "fs/promises";
4997
5205
  import { homedir as homedir2 } from "os";
4998
- import { join as join3 } from "path";
5206
+ import { join as join4 } from "path";
4999
5207
  var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
5000
5208
  var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
5001
5209
  function claudeCredentialsPath() {
5002
- return join3(claudeConfigHome(), ".credentials.json");
5210
+ return join4(claudeConfigHome(), ".credentials.json");
5003
5211
  }
5004
5212
  function isConveyorCloudEnv(env = process.env) {
5005
5213
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
@@ -5090,7 +5298,7 @@ async function writeWithReadBackRetry(io2, contents, delaysMs = READ_BACK_DELAYS
5090
5298
  }
5091
5299
  function fsWriteIo(path2, mode) {
5092
5300
  return {
5093
- write: (contents) => writeFile3(path2, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
5301
+ write: (contents) => writeFile4(path2, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
5094
5302
  read: () => readRaw(path2)
5095
5303
  };
5096
5304
  }
@@ -5162,7 +5370,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
5162
5370
  }
5163
5371
  function claudeJsonPath() {
5164
5372
  const configDir = process.env.CLAUDE_CONFIG_DIR;
5165
- return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
5373
+ return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
5166
5374
  }
5167
5375
  function asRecord(value) {
5168
5376
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -5255,7 +5463,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
5255
5463
  return changed ? JSON.stringify(config) : null;
5256
5464
  }
5257
5465
  function conveyorOauthMarkerPath() {
5258
- return join3(claudeConfigHome(), "conveyor-oauth-account.json");
5466
+ return join4(claudeConfigHome(), "conveyor-oauth-account.json");
5259
5467
  }
5260
5468
  function parseOauthIdentity(raw) {
5261
5469
  if (!raw || raw.trim() === "") return null;
@@ -5339,7 +5547,8 @@ var ClaudeTuiAdapter = class {
5339
5547
  resume: true,
5340
5548
  structuredEvents: true,
5341
5549
  prefill: true,
5342
- passiveTurns: true
5550
+ passiveTurns: true,
5551
+ rawPromptGate: false
5343
5552
  };
5344
5553
  resolveBinary(env = process.env) {
5345
5554
  return env.CONVEYOR_CLAUDE_BIN ?? "claude";
@@ -5415,6 +5624,12 @@ var PtySession = class {
5415
5624
  lastTurnCleanResult = false;
5416
5625
  socket = null;
5417
5626
  tailer = null;
5627
+ // opencode structured-events state: the sink-record interpreter, and the
5628
+ // opencode-assigned session id it latched (reported via a synthetic init and
5629
+ // used for `--session` resume — opencode manages its own lineage, unlike
5630
+ // Claude's deterministic transcript UUIDs).
5631
+ opencodeSource = null;
5632
+ reportedOpenCodeId = null;
5418
5633
  pty = null;
5419
5634
  tempDir = "";
5420
5635
  sawResult = false;
@@ -5425,6 +5640,20 @@ var PtySession = class {
5425
5640
  recentOutput = "";
5426
5641
  coalescer = null;
5427
5642
  _toreDown = false;
5643
+ // Raw-TUI input detection (see awaitRawTuiInputLive). `probeWindow`, when
5644
+ // non-null, accumulates raw output so a probe can look for its own sentinel
5645
+ // coming back.
5646
+ spawnedAt = 0;
5647
+ // Set once the child writes a DEC private mode — it owns the tty from then on,
5648
+ // so the kernel no longer echoes our keystrokes and a sentinel coming back is
5649
+ // attributable to the app's own repaint.
5650
+ sawTerminalSetup = false;
5651
+ probeWindow = null;
5652
+ // Whether this process has already taken a prompt write. Only the FIRST one
5653
+ // waits on readiness: once the TUI is up, later writes (a multi-message
5654
+ // prompt, a follow-up turn) must go straight through — output is flowing
5655
+ // while the model works, so a quiet check would block until the turn ended.
5656
+ wroteToProcess = false;
5428
5657
  exitListeners = [];
5429
5658
  idleListeners = [];
5430
5659
  abortHandler = null;
@@ -5516,9 +5745,18 @@ var PtySession = class {
5516
5745
  const tail = cleanTerminalOutput(this.recentOutput, MAX_DIAGNOSTIC_OUTPUT);
5517
5746
  return tail ? redact(tail).output : "";
5518
5747
  }
5519
- /** The deterministic session UUID this process is bound to. */
5748
+ /**
5749
+ * The session identity this process is bound to. For Claude, the
5750
+ * deterministic lineage UUID; for opencode, the `ses_…` id the engine
5751
+ * assigned (latched from the plugin events), which wins once known so a
5752
+ * respawn's explicit resume target matches.
5753
+ */
5520
5754
  get sessionUuid() {
5521
- return this.resume ?? this.options.sessionId;
5755
+ return this.reportedOpenCodeId ?? this.resume ?? this.options.sessionId;
5756
+ }
5757
+ /** The opencode-assigned session id, or null (always null for Claude). */
5758
+ get reportedSessionId() {
5759
+ return this.reportedOpenCodeId;
5522
5760
  }
5523
5761
  /** Fingerprint of the spawn-time options a reused process cannot change. */
5524
5762
  get spawnFingerprint() {
@@ -5533,9 +5771,19 @@ var PtySession = class {
5533
5771
  * Whether this parked process can serve a turn wanting `resume`/`fingerprint`.
5534
5772
  * Requires: live (not torn down / not exited), idle (no turn draining), a
5535
5773
  * matching resume target, and matching spawn options.
5774
+ *
5775
+ * The resume match is adapter-shaped. Claude's resume tokens are transcript
5776
+ * files, so an undefined `resume` means "start a fresh conversation" and
5777
+ * must NOT reuse a parked process bound to an old one. opencode manages its
5778
+ * own lineage — the executor can never learn its `ses_…` ids from disk, so
5779
+ * every follow-up arrives with `resume` undefined; there, the live parked
5780
+ * process IS the conversation, and reuse is keyed on the id the plugin
5781
+ * events latched. Gating on `reportedOpenCodeId` (which Claude sessions
5782
+ * never set) keeps the Claude semantics byte-identical.
5536
5783
  */
5537
5784
  canReuse(resume, fingerprint) {
5538
- return !this._toreDown && !this.exited && this.activeQueue === null && resume !== void 0 && resume === this.sessionUuid && fingerprint === this.spawnFingerprint;
5785
+ const resumeMatches = resume === void 0 ? this.reportedOpenCodeId !== null : resume === this.sessionUuid;
5786
+ return !this._toreDown && !this.exited && this.activeQueue === null && resumeMatches && fingerprint === this.spawnFingerprint;
5539
5787
  }
5540
5788
  get hookSocketPath() {
5541
5789
  return this.socket ? this.socket.socketPath : null;
@@ -5663,11 +5911,14 @@ var PtySession = class {
5663
5911
  return;
5664
5912
  }
5665
5913
  try {
5666
- if (this.adapter.capabilities.structuredEvents) {
5914
+ if (this.adapter.capabilities.structuredEvents && this.adapter.id === "opencode") {
5915
+ const extras = await this.startOpenCodeEventSources();
5916
+ await this.spawn(void 0, void 0, extras);
5917
+ } else if (this.adapter.capabilities.structuredEvents) {
5667
5918
  const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
5668
5919
  await this.spawn(settingsPath, socketPath);
5669
5920
  } else {
5670
- this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
5921
+ this.tempDir = await mkdtemp(join5(sessionTempBase(), "conveyor-pty-"));
5671
5922
  await this.spawn();
5672
5923
  this.pushEvent({
5673
5924
  type: "system",
@@ -5696,6 +5947,61 @@ var PtySession = class {
5696
5947
  throw err;
5697
5948
  }
5698
5949
  }
5950
+ /**
5951
+ * Allocate the opencode structured-event sources: the in-process tool
5952
+ * servers (shared with the Claude path — the same loopback StreamableHTTP
5953
+ * servers, consumed via config `mcp` entries instead of `--mcp-config`),
5954
+ * the instructions file carrying the system prompt, the staged Conveyor
5955
+ * events plugin, and a tailer on its sink. Returns the spawn extras the
5956
+ * adapter bakes into OPENCODE_CONFIG_CONTENT.
5957
+ *
5958
+ * The tailer routes every parsed record to the OpenCodeEventSource, which
5959
+ * owns per-turn state (roles, usage, busy/idle) and feeds the mapped events
5960
+ * through the SAME handleTranscriptEvent path Claude records take — so
5961
+ * nudge-disarm, passive buffering, and result-ends-turn run unchanged. The
5962
+ * synthetic `system init` reports the opencode-assigned session id the
5963
+ * moment it is known, which is how the runner learns the resume target.
5964
+ */
5965
+ async startOpenCodeEventSources() {
5966
+ this.tempDir = await mkdtemp(join5(sessionTempBase(), "conveyor-pty-"));
5967
+ const { servers, entries } = await startToolServers(
5968
+ this.options.mcpServers ?? {},
5969
+ this.tempDir
5970
+ );
5971
+ this.toolServers = servers;
5972
+ const { pluginPath, eventsSinkPath } = await stageOpenCodePlugin(this.tempDir);
5973
+ let instructionsPath;
5974
+ const systemPrompt = this.options.appendSystemPrompt;
5975
+ if (systemPrompt && systemPrompt.trim() !== "") {
5976
+ instructionsPath = join5(this.tempDir, "conveyor-instructions.md");
5977
+ await writeFile5(instructionsPath, systemPrompt, "utf8");
5978
+ }
5979
+ this.opencodeSource = new OpenCodeEventSource(
5980
+ (event) => this.handleTranscriptEvent(event),
5981
+ (id) => {
5982
+ this.reportedOpenCodeId = id;
5983
+ this.pushEvent({
5984
+ type: "system",
5985
+ subtype: "init",
5986
+ session_id: id,
5987
+ model: this.options.model
5988
+ });
5989
+ }
5990
+ );
5991
+ this.tailer = new JsonlTailer(
5992
+ eventsSinkPath,
5993
+ () => void 0,
5994
+ (raw) => this.opencodeSource?.handleRecord(raw),
5995
+ () => null
5996
+ );
5997
+ this.tailer.start(0);
5998
+ return {
5999
+ mcpEntries: entries,
6000
+ eventsSinkPath,
6001
+ pluginPath,
6002
+ ...instructionsPath ? { instructionsPath } : {}
6003
+ };
6004
+ }
5699
6005
  /**
5700
6006
  * Allocate the Claude-style structured-event sources: the PostToolUse hook
5701
6007
  * socket, the per-run settings file, the in-process tool servers, and the
@@ -5704,8 +6010,8 @@ var PtySession = class {
5704
6010
  * paths spawn() must wire into the child's argv/env.
5705
6011
  */
5706
6012
  async startStructuredEventSources(sessionId) {
5707
- this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
5708
- const socketPath = join4(this.tempDir, "hook.sock");
6013
+ this.tempDir = await mkdtemp(join5(sessionTempBase(), "conveyor-pty-"));
6014
+ const socketPath = join5(this.tempDir, "hook.sock");
5709
6015
  this.socket = new HookSocketServer(
5710
6016
  socketPath,
5711
6017
  (progress) => this.handleProgress(progress),
@@ -5910,19 +6216,21 @@ var PtySession = class {
5910
6216
  this.toolServers = servers;
5911
6217
  this.mcpConfigPath = mcpConfigPath;
5912
6218
  }
5913
- async spawn(settingsPath, socketPath) {
6219
+ async spawn(settingsPath, socketPath, opencodeExtras) {
6220
+ const resume = this.reportedOpenCodeId ?? this.resume;
5914
6221
  const spec = this.adapter.buildSpawn({
5915
6222
  options: this.options,
5916
- ...this.resume ? { resume: this.resume } : {},
6223
+ ...resume ? { resume } : {},
5917
6224
  ...settingsPath ? { settingsPath } : {},
5918
6225
  ...socketPath ? { hookSocketPath: socketPath } : {},
5919
6226
  // Only this config: ignore the user's ~/.claude.json / project .mcp.json
5920
6227
  // so the agent's tool set is deterministic (and a stale personal conveyor
5921
6228
  // server doesn't load).
5922
- ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
6229
+ ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {},
6230
+ ...opencodeExtras ?? {}
5923
6231
  });
5924
- const spawn = await resolvePtySpawn();
5925
- const pty = spawn(spec.file, spec.args, {
6232
+ const spawn2 = await resolvePtySpawn();
6233
+ const pty = spawn2(spec.file, spec.args, {
5926
6234
  name: "xterm-color",
5927
6235
  cols: this.cols,
5928
6236
  rows: this.rows,
@@ -5940,15 +6248,101 @@ var PtySession = class {
5940
6248
  }
5941
6249
  pty.onData((data) => {
5942
6250
  this.coalescer?.write(data);
6251
+ if (!this.sawTerminalSetup && sawTerminalSetup(data)) this.sawTerminalSetup = true;
6252
+ if (this.probeWindow !== null) this.probeWindow += data;
5943
6253
  this.recentOutput = (this.recentOutput + data).slice(-MAX_DIAGNOSTIC_OUTPUT);
5944
6254
  });
5945
6255
  pty.onExit((event) => {
5946
6256
  void this.finalizeOnExit(event.exitCode);
5947
6257
  });
6258
+ this.spawnedAt = Date.now();
6259
+ this.sawTerminalSetup = false;
6260
+ this.probeWindow = null;
6261
+ this.wroteToProcess = false;
5948
6262
  this.pty = pty;
5949
6263
  }
6264
+ /** Open a fresh probe-observation window over raw output. */
6265
+ openProbeWindow() {
6266
+ this.probeWindow = "";
6267
+ }
6268
+ /**
6269
+ * Write `bytes`, then wait up to `ackMs` for the TUI to render `sentinel` back.
6270
+ * Positive proof that a live input box consumed the keystrokes.
6271
+ */
6272
+ async writeAndAwaitEcho(bytes, sentinel, timing) {
6273
+ this.openProbeWindow();
6274
+ this.writeStdin(bytes);
6275
+ const deadline = Date.now() + timing.ackMs;
6276
+ while (Date.now() < deadline) {
6277
+ if (this._toreDown) return false;
6278
+ if (sentinelEchoed(this.probeWindow ?? "", sentinel)) return true;
6279
+ await sleep(timing.pollMs);
6280
+ }
6281
+ return false;
6282
+ }
6283
+ /**
6284
+ * Wait until a raw-relay TUI's input box is DEMONSTRABLY accepting keystrokes,
6285
+ * by typing a short sentinel and watching for the TUI to render it back, then
6286
+ * erasing it. Readiness is detected, never inferred from elapsed time.
6287
+ *
6288
+ * A structured-events TUI does not need this: `armSubmitNudge` re-presses
6289
+ * Enter until a transcript record proves the turn started. A raw adapter has
6290
+ * no such evidence, so the nudge is deliberately never armed for it — its
6291
+ * single paste + Enter is the only shot, and one fired during startup is
6292
+ * discarded with no trace, leaving the card parked on an empty input box.
6293
+ *
6294
+ * See resolveRawTuiProbeTiming for the measurements behind this, including the
6295
+ * signals that were tried and rejected (DECSET 2004, output-quiet windows, and
6296
+ * "any output after a keystroke" — all of which report ready too early).
6297
+ */
6298
+ async awaitRawTuiInputLive() {
6299
+ if (!needsRawReadyGate(this.adapter.capabilities)) return;
6300
+ const timing = resolveRawTuiProbeTiming();
6301
+ const { sentinel } = timing;
6302
+ const start = this.spawnedAt || Date.now();
6303
+ const setupDeadline = start + timing.firstOutputMaxMs;
6304
+ while (!this._toreDown && Date.now() < setupDeadline) {
6305
+ if (this.sawTerminalSetup) break;
6306
+ await sleep(timing.pollMs);
6307
+ }
6308
+ if (this._toreDown) return;
6309
+ const deadline = start + timing.maxMs;
6310
+ let live = false;
6311
+ let probes = 0;
6312
+ while (!this._toreDown && Date.now() < deadline) {
6313
+ probes++;
6314
+ if (await this.writeAndAwaitEcho(sentinel, sentinel, timing)) {
6315
+ live = true;
6316
+ break;
6317
+ }
6318
+ await sleep(timing.retryMs);
6319
+ }
6320
+ if (this._toreDown) return;
6321
+ if (!live) {
6322
+ process.stderr.write(
6323
+ `[conveyor-agent] raw TUI never echoed the input probe after ${probes} attempt(s) in ${Date.now() - start}ms \u2014 pasting anyway
6324
+ `
6325
+ );
6326
+ this.probeWindow = null;
6327
+ return;
6328
+ }
6329
+ for (let round = 0; round < probes && !this._toreDown; round++) {
6330
+ const gone = !await this.writeAndAwaitEcho(
6331
+ "\x7F".repeat(sentinel.length),
6332
+ sentinel,
6333
+ timing
6334
+ );
6335
+ if (gone) break;
6336
+ }
6337
+ this.probeWindow = null;
6338
+ }
5950
6339
  async deliverPrompt(text) {
5951
6340
  if (text === "" && !this.adapter.capabilities.prefill) return;
6341
+ if (!this.wroteToProcess) {
6342
+ await this.awaitRawTuiInputLive();
6343
+ if (this._toreDown) return;
6344
+ this.wroteToProcess = true;
6345
+ }
5952
6346
  this.writeStdin(this.adapter.encodePromptBytes(text));
5953
6347
  if (this.turn.promptDelivery === "prefill") return;
5954
6348
  this.deliveredTexts.push(text);
@@ -6206,7 +6600,7 @@ var PtySession = class {
6206
6600
  // src/harness/pty/config-home-health.ts
6207
6601
  import { lstat, mkdir as mkdir4, symlink, unlink as unlink2 } from "fs/promises";
6208
6602
  import { homedir as homedir3 } from "os";
6209
- import { join as join5 } from "path";
6603
+ import { join as join6 } from "path";
6210
6604
  var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
6211
6605
  var MOUNT_DISCONNECT_MESSAGES = [
6212
6606
  "socket is not connected",
@@ -6222,10 +6616,10 @@ function isMountDisconnectError(err) {
6222
6616
  return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
6223
6617
  }
6224
6618
  function podLocalConfigHome() {
6225
- return join5(homedir3(), ".claude-local");
6619
+ return join6(homedir3(), ".claude-local");
6226
6620
  }
6227
6621
  function sharedConfigHomePath() {
6228
- return join5(homedir3(), ".claude");
6622
+ return join6(homedir3(), ".claude");
6229
6623
  }
6230
6624
  function isConfigHomeFallbackActive() {
6231
6625
  return claudeConfigHome() === podLocalConfigHome();
@@ -6256,7 +6650,7 @@ async function repointSharedConfigHomeSymlink(fallback, log) {
6256
6650
  }
6257
6651
  async function isConfigHomeMountDead(cwd) {
6258
6652
  try {
6259
- await mkdir4(join5(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
6653
+ await mkdir4(join6(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
6260
6654
  return false;
6261
6655
  } catch (err) {
6262
6656
  return isMountDisconnectError(err);
@@ -6265,7 +6659,7 @@ async function isConfigHomeMountDead(cwd) {
6265
6659
  async function ensureUsableClaudeConfigHome(cwd, log) {
6266
6660
  const configHome = claudeConfigHome();
6267
6661
  try {
6268
- await mkdir4(join5(configHome, "projects", projectSlug(cwd)), { recursive: true });
6662
+ await mkdir4(join6(configHome, "projects", projectSlug(cwd)), { recursive: true });
6269
6663
  return { configHome, fellBack: false };
6270
6664
  } catch (err) {
6271
6665
  if (!isMountDisconnectError(err)) throw err;
@@ -6279,7 +6673,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
6279
6673
  }
6280
6674
  );
6281
6675
  process.env.CLAUDE_CONFIG_DIR = fallback;
6282
- await mkdir4(join5(fallback, "projects", projectSlug(cwd)), { recursive: true });
6676
+ await mkdir4(join6(fallback, "projects", projectSlug(cwd)), { recursive: true });
6283
6677
  await repointSharedConfigHomeSymlink(fallback, log);
6284
6678
  return { configHome: fallback, fellBack: true };
6285
6679
  }
@@ -6300,6 +6694,11 @@ var PtyHarness = class _PtyHarness {
6300
6694
  bridge;
6301
6695
  adapter;
6302
6696
  static log = createServiceLogger("pty-harness");
6697
+ /** Delegated to the adapter: Claude tails a transcript + hook socket, while a
6698
+ * raw-relay TUI (opencode) has no trusted event source at all. */
6699
+ get emitsStructuredEvents() {
6700
+ return this.adapter.capabilities.structuredEvents;
6701
+ }
6303
6702
  /** Fingerprint of the spawn-time options a reused process cannot change. */
6304
6703
  fingerprintOf(options) {
6305
6704
  return this.adapter.spawnFingerprint({
@@ -6355,7 +6754,7 @@ var PtyHarness = class _PtyHarness {
6355
6754
  }
6356
6755
  }
6357
6756
  async *executeQuery(opts) {
6358
- const want = opts.resume ?? opts.options.resume;
6757
+ const want = opts.resume ?? opts.options.resume ?? this.parked?.reportedSessionId ?? void 0;
6359
6758
  const fingerprint = this.fingerprintOf(opts.options);
6360
6759
  let session;
6361
6760
  if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(opts.options)) {
@@ -6610,21 +7009,499 @@ var PtyHarness = class _PtyHarness {
6610
7009
  }
6611
7010
  };
6612
7011
 
7012
+ // src/harness/opencode/index.ts
7013
+ import { spawn } from "child_process";
7014
+
7015
+ // src/harness/pty/adapters/types.ts
7016
+ import { accessSync, constants, statSync } from "fs";
7017
+ import { join as join7 } from "path";
7018
+ var TuiUnavailableError = class extends Error {
7019
+ constructor(tui, message) {
7020
+ super(message);
7021
+ this.tui = tui;
7022
+ this.name = "TuiUnavailableError";
7023
+ }
7024
+ tui;
7025
+ };
7026
+ function isExecutable(path2) {
7027
+ try {
7028
+ if (!statSync(path2).isFile()) return false;
7029
+ accessSync(path2, constants.X_OK);
7030
+ return true;
7031
+ } catch {
7032
+ return false;
7033
+ }
7034
+ }
7035
+ function findOnPath(binary, env = process.env) {
7036
+ if (binary.includes("/")) {
7037
+ return isExecutable(binary) ? binary : null;
7038
+ }
7039
+ for (const dir of (env.PATH ?? "").split(":")) {
7040
+ if (!dir) continue;
7041
+ const candidate = join7(dir, binary);
7042
+ if (isExecutable(candidate)) return candidate;
7043
+ }
7044
+ return null;
7045
+ }
7046
+
7047
+ // src/harness/pty/adapters/opencode-auth.ts
7048
+ import { promises as fs } from "fs";
7049
+ import { dirname as dirname2, join as join8 } from "path";
7050
+ import { homedir as homedir4 } from "os";
7051
+ var logger = createServiceLogger("opencode-auth");
7052
+ var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
7053
+ var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
7054
+ function opencodeAuthPath(env) {
7055
+ const dataHome = env.XDG_DATA_HOME ?? join8(env.HOME ?? homedir4(), ".local", "share");
7056
+ return join8(dataHome, "opencode", "auth.json");
7057
+ }
7058
+ function opencodeConfigPath(env) {
7059
+ const configHome = env.XDG_CONFIG_HOME ?? join8(env.HOME ?? homedir4(), ".config");
7060
+ return join8(configHome, "opencode", "opencode.json");
7061
+ }
7062
+ function parseOauthSeed(b64) {
7063
+ if (!b64) return null;
7064
+ try {
7065
+ const parsed = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
7066
+ if (typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
7067
+ return { access: parsed.access, refresh: parsed.refresh, expires: parsed.expires };
7068
+ }
7069
+ return null;
7070
+ } catch {
7071
+ return null;
7072
+ }
7073
+ }
7074
+ function shouldSeed(existingEntry, seed) {
7075
+ if (!existingEntry || typeof existingEntry !== "object") return true;
7076
+ const entry = existingEntry;
7077
+ if (entry.type !== "oauth") return true;
7078
+ if (typeof entry.access !== "string" || typeof entry.refresh !== "string") return true;
7079
+ if (typeof entry.expires !== "number") return true;
7080
+ return entry.expires < seed.expires;
7081
+ }
7082
+ async function readJsonFile(path2) {
7083
+ try {
7084
+ return JSON.parse(await fs.readFile(path2, "utf8"));
7085
+ } catch {
7086
+ return {};
7087
+ }
7088
+ }
7089
+ async function writeJsonFile(path2, value) {
7090
+ await fs.mkdir(dirname2(path2), { recursive: true });
7091
+ await fs.writeFile(path2, `${JSON.stringify(value, null, 2)}
7092
+ `, { mode: 384 });
7093
+ }
7094
+ async function ensureAuthEntry(env, seed) {
7095
+ const path2 = opencodeAuthPath(env);
7096
+ const store = await readJsonFile(path2);
7097
+ if (!shouldSeed(store.openai, seed)) {
7098
+ logger.info("opencode oauth store is fresher than the seed; leaving it alone");
7099
+ return;
7100
+ }
7101
+ store.openai = {
7102
+ type: "oauth",
7103
+ access: seed.access,
7104
+ refresh: seed.refresh,
7105
+ expires: seed.expires
7106
+ };
7107
+ await writeJsonFile(path2, store);
7108
+ logger.info("seeded opencode oauth store entry");
7109
+ }
7110
+ async function ensurePluginConfig(env) {
7111
+ const path2 = opencodeConfigPath(env);
7112
+ const config = await readJsonFile(path2);
7113
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
7114
+ const isOurs = (p) => typeof p === "string" && (p === PLUGIN_PACKAGE || p.startsWith(`${PLUGIN_PACKAGE}@`));
7115
+ const hasExactPin = plugins.includes(OPENCODE_CODEX_PLUGIN);
7116
+ const hasStalePin = plugins.some((p) => isOurs(p) && p !== OPENCODE_CODEX_PLUGIN);
7117
+ if (hasExactPin && !hasStalePin) return;
7118
+ const kept = plugins.filter((p) => !isOurs(p));
7119
+ config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
7120
+ await writeJsonFile(path2, config);
7121
+ logger.info("ensured opencode codex-auth plugin in config");
7122
+ }
7123
+ async function seedOpenCodeOauth(env) {
7124
+ const seed = parseOauthSeed(env.CONVEYOR_OPENCODE_OAUTH);
7125
+ if (!seed) return;
7126
+ try {
7127
+ await ensureAuthEntry(env, seed);
7128
+ await ensurePluginConfig(env);
7129
+ } catch (err) {
7130
+ logger.warn(
7131
+ `failed to seed opencode oauth store: ${err instanceof Error ? err.message : String(err)}`
7132
+ );
7133
+ }
7134
+ }
7135
+
7136
+ // src/harness/opencode/credentials.ts
7137
+ var PROVIDER_KEY_ENV = {
7138
+ openai: "OPENAI_API_KEY",
7139
+ anthropic: "ANTHROPIC_API_KEY"
7140
+ };
7141
+ var DEFAULT_OPENCODE_PROVIDER = "openai";
7142
+ function buildOpenCodeCredentialEnv(source) {
7143
+ const set = {};
7144
+ const oauthActive = Boolean(source.CONVEYOR_OPENCODE_OAUTH);
7145
+ const key = source.CONVEYOR_AGENT_KEY;
7146
+ const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
7147
+ const keyEnvVar = PROVIDER_KEY_ENV[provider];
7148
+ if (key && keyEnvVar && !oauthActive) set[keyEnvVar] = key;
7149
+ return { set, unset: ["CONVEYOR_AGENT_KEY", "CONVEYOR_OPENCODE_OAUTH"] };
7150
+ }
7151
+ async function prepareOpenCodeCredentials(source = process.env) {
7152
+ await seedOpenCodeOauth(source);
7153
+ }
7154
+ function buildOpenCodeChildEnv(source, configContent) {
7155
+ const env = {};
7156
+ for (const [key, value] of Object.entries(source)) {
7157
+ if (typeof value === "string") env[key] = value;
7158
+ }
7159
+ const { set, unset } = buildOpenCodeCredentialEnv(source);
7160
+ for (const key of unset) delete env[key];
7161
+ Object.assign(env, set);
7162
+ if (configContent) env.OPENCODE_CONFIG_CONTENT = configContent;
7163
+ return env;
7164
+ }
7165
+ function resolveOpenCodeModel(source, optionsModel) {
7166
+ const model = source.CONVEYOR_AGENT_MODEL ?? optionsModel;
7167
+ if (!model) return void 0;
7168
+ if (model.includes("/")) return model;
7169
+ const provider = source.CONVEYOR_AGENT_PROVIDER ?? DEFAULT_OPENCODE_PROVIDER;
7170
+ return `${provider}/${model}`;
7171
+ }
7172
+
7173
+ // src/harness/opencode/spawn.ts
7174
+ function resolveOpenCodeBinary(env = process.env) {
7175
+ const override = env.CONVEYOR_OPENCODE_BIN;
7176
+ const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
7177
+ if (!found) {
7178
+ throw new TuiUnavailableError(
7179
+ "opencode",
7180
+ "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."
7181
+ );
7182
+ }
7183
+ return found;
7184
+ }
7185
+ var PROMPT_MAX_CHARS = 96e3;
7186
+ var MUTATING_TOOLS = ["edit", "bash", "external_directory"];
7187
+ function buildRunArgs(input) {
7188
+ const args = ["run", "--dir", input.cwd, "--format", "json", "--auto"];
7189
+ if (input.model) {
7190
+ args.push("--model", input.model);
7191
+ }
7192
+ if (input.resumeSessionId) args.push("--session", input.resumeSessionId);
7193
+ args.push(truncatePrompt(input.prompt));
7194
+ return args;
7195
+ }
7196
+ function truncatePrompt(prompt) {
7197
+ if (prompt.length <= PROMPT_MAX_CHARS) return prompt;
7198
+ return `${prompt.slice(0, PROMPT_MAX_CHARS)}
7199
+
7200
+ [prompt truncated at ${PROMPT_MAX_CHARS} characters]`;
7201
+ }
7202
+ function buildOpenCodeConfigContent(input) {
7203
+ const { entries, systemPromptPath, readOnly, pluginPath } = input;
7204
+ const mcp = {};
7205
+ for (const [name, entry] of Object.entries(entries)) {
7206
+ if (entry.type === "http") {
7207
+ mcp[name] = {
7208
+ type: "remote",
7209
+ url: entry.url,
7210
+ enabled: true,
7211
+ ...Object.keys(entry.headers).length > 0 ? { headers: entry.headers } : {}
7212
+ };
7213
+ } else {
7214
+ mcp[name] = {
7215
+ type: "local",
7216
+ command: [entry.command, ...entry.args ?? []],
7217
+ enabled: true,
7218
+ ...entry.env ? { environment: entry.env } : {}
7219
+ };
7220
+ }
7221
+ }
7222
+ const config = {};
7223
+ if (Object.keys(mcp).length > 0) config.mcp = mcp;
7224
+ if (systemPromptPath) config.instructions = [systemPromptPath];
7225
+ if (pluginPath) config.plugin = [`file://${pluginPath}`];
7226
+ config.permission = {
7227
+ "*": "allow",
7228
+ ...readOnly ? Object.fromEntries(MUTATING_TOOLS.map((tool2) => [tool2, "deny"])) : {}
7229
+ };
7230
+ if (Object.keys(config).length === 0) return null;
7231
+ return JSON.stringify(config);
7232
+ }
7233
+
7234
+ // src/harness/opencode/index.ts
7235
+ import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile6 } from "fs/promises";
7236
+ import { join as join9 } from "path";
7237
+ var MAX_STDERR_TAIL = 4e3;
7238
+ var OpenCodeHeadlessHarness = class {
7239
+ /** NDJSON from `--format json` is a trusted structured source. */
7240
+ emitsStructuredEvents = true;
7241
+ /** opencode's session id from the last run, for `--session` on the next turn. */
7242
+ lastSessionId = null;
7243
+ /** MCP tool servers started for the in-flight run. */
7244
+ toolServers = [];
7245
+ tempDir = "";
7246
+ /** The same in-process MCP handle the PTY path uses — tools are identical. */
7247
+ createMcpServer(config) {
7248
+ return new PtyMcpServer(config.name, config.tools);
7249
+ }
7250
+ /** opencode's own session id, exposed so the runner can persist lineage. */
7251
+ get sessionId() {
7252
+ return this.lastSessionId;
7253
+ }
7254
+ async *executeQuery(opts) {
7255
+ const prompt = await collectPrompt(opts.prompt);
7256
+ const binary = resolveOpenCodeBinary(process.env);
7257
+ await prepareOpenCodeCredentials(process.env);
7258
+ this.tempDir = await mkdtemp2(join9(sessionTempBase(), "opencode-headless-"));
7259
+ const { servers, entries } = await startToolServers(
7260
+ opts.options.mcpServers ?? {},
7261
+ this.tempDir
7262
+ );
7263
+ this.toolServers = servers;
7264
+ const args = buildRunArgs({
7265
+ prompt,
7266
+ cwd: opts.options.cwd,
7267
+ model: resolveOpenCodeModel(process.env, opts.options.model),
7268
+ // `resume` is Claude-transcript-derived, so it is always absent here (there
7269
+ // is no Claude session file for an opencode run). Falling back to the id
7270
+ // opencode itself reported is what makes a multi-turn card keep its
7271
+ // conversation instead of starting fresh on every message.
7272
+ resumeSessionId: opts.resume ?? this.lastSessionId
7273
+ });
7274
+ const readOnly = opts.options.allowDangerouslySkipPermissions === false;
7275
+ const systemPromptPath = await this.writeSystemPrompt(opts.options.appendSystemPrompt);
7276
+ const configContent = buildOpenCodeConfigContent({
7277
+ entries,
7278
+ systemPromptPath,
7279
+ readOnly
7280
+ });
7281
+ const queue = new AsyncEventQueue();
7282
+ const usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
7283
+ let assistantText = "";
7284
+ let stderrTail = "";
7285
+ let reportedError = null;
7286
+ const child = spawn(binary, args, {
7287
+ cwd: opts.options.cwd,
7288
+ // stdin IGNORED — an open pipe makes `opencode run` block forever.
7289
+ stdio: ["ignore", "pipe", "pipe"],
7290
+ env: buildOpenCodeChildEnv(process.env, configContent)
7291
+ });
7292
+ const abort = opts.options.abortController;
7293
+ const onAbort = () => {
7294
+ child.kill("SIGTERM");
7295
+ };
7296
+ abort?.signal.addEventListener("abort", onAbort, { once: true });
7297
+ let carry = "";
7298
+ const ingest = (line) => {
7299
+ const { text, error } = this.ingestLine(line, usage, queue);
7300
+ if (text) assistantText += text;
7301
+ if (error) reportedError = error;
7302
+ };
7303
+ child.stdout.setEncoding("utf8");
7304
+ child.stdout.on("data", (chunk) => {
7305
+ carry += chunk;
7306
+ const lines = carry.split("\n");
7307
+ carry = lines.pop() ?? "";
7308
+ for (const line of lines) ingest(line);
7309
+ });
7310
+ child.stderr.setEncoding("utf8");
7311
+ child.stderr.on("data", (chunk) => {
7312
+ stderrTail = (stderrTail + chunk).slice(-MAX_STDERR_TAIL);
7313
+ });
7314
+ const exited = new Promise((resolve) => {
7315
+ child.once("error", () => resolve(-1));
7316
+ child.once("close", (code) => resolve(code ?? -1));
7317
+ });
7318
+ const finish = (async () => {
7319
+ const code = await exited;
7320
+ ingest(carry);
7321
+ queue.push(buildResultEvent(code, usage, assistantText.trim(), stderrTail, reportedError));
7322
+ queue.close();
7323
+ })();
7324
+ try {
7325
+ for await (const event of queue.drain()) yield event;
7326
+ await finish;
7327
+ } finally {
7328
+ abort?.signal.removeEventListener("abort", onAbort);
7329
+ if (!child.killed) child.kill("SIGTERM");
7330
+ await this.cleanup();
7331
+ }
7332
+ }
7333
+ /**
7334
+ * Persist the Conveyor system prompt (task context, plan, mode instructions)
7335
+ * so it can be referenced by the config's `instructions`. opencode has no
7336
+ * `--append-system-prompt`; without this the card runs with no context at all.
7337
+ */
7338
+ async writeSystemPrompt(text) {
7339
+ if (!text || text.trim() === "") return null;
7340
+ const path2 = join9(this.tempDir, "conveyor-instructions.md");
7341
+ await writeFile6(path2, text, "utf8");
7342
+ return path2;
7343
+ }
7344
+ /**
7345
+ * Parse one NDJSON line and push whatever it maps to. Returns assistant text so
7346
+ * the caller can accumulate the turn's summary; the session id is latched here
7347
+ * because every event carries it and any one of them will do.
7348
+ */
7349
+ ingestLine(line, usage, queue) {
7350
+ const none = { text: "", error: null };
7351
+ const parsed = parseOpenCodeLine(line);
7352
+ if (!parsed) return none;
7353
+ const sid = sessionIdOf(parsed);
7354
+ if (sid) this.lastSessionId = sid;
7355
+ accumulateUsage(parsed, usage);
7356
+ const error = errorMessageOf(parsed);
7357
+ if (error) return { text: "", error };
7358
+ const mapped = mapOpenCodeEvent(parsed);
7359
+ if (!mapped) return none;
7360
+ queue.push(mapped);
7361
+ if (mapped.type !== "assistant") return none;
7362
+ const block = mapped.message.content[0];
7363
+ return { text: block?.type === "text" && block.text ? block.text : "", error: null };
7364
+ }
7365
+ async dispose() {
7366
+ await this.cleanup();
7367
+ }
7368
+ async cleanup() {
7369
+ for (const server of this.toolServers) await server.close().catch(() => void 0);
7370
+ this.toolServers = [];
7371
+ if (this.tempDir) {
7372
+ await rm3(this.tempDir, { recursive: true, force: true }).catch(() => void 0);
7373
+ this.tempDir = "";
7374
+ }
7375
+ }
7376
+ };
7377
+ async function collectPrompt(prompt) {
7378
+ if (typeof prompt === "string") return prompt;
7379
+ const parts = [];
7380
+ for await (const message of prompt) {
7381
+ const content = message?.message?.content;
7382
+ if (typeof content === "string") parts.push(content);
7383
+ else if (Array.isArray(content)) {
7384
+ for (const block of content) {
7385
+ const b = block;
7386
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
7387
+ }
7388
+ }
7389
+ }
7390
+ return parts.join("\n\n");
7391
+ }
7392
+
6613
7393
  // src/harness/index.ts
6614
- function createHarness(kind = "sdk", ptyBridge) {
6615
- return kind === "pty" ? new PtyHarness(ptyBridge) : new ClaudeCodeHarness();
7394
+ function supportsImageBlocks(kind) {
7395
+ return kind === "sdk";
7396
+ }
7397
+ function createHarness(kind = "sdk", ptyBridge, adapter) {
7398
+ if (kind === "opencode") return new OpenCodeHeadlessHarness();
7399
+ if (kind !== "pty") return new ClaudeCodeHarness();
7400
+ return adapter ? new PtyHarness(ptyBridge, adapter) : new PtyHarness(ptyBridge);
7401
+ }
7402
+
7403
+ // src/harness/pty/adapters/opencode.ts
7404
+ var OpenCodeTuiAdapter = class {
7405
+ constructor(env = process.env) {
7406
+ this.env = env;
7407
+ }
7408
+ env;
7409
+ id = "opencode";
7410
+ capabilities = {
7411
+ // `--session <ses_…>` continues the id the plugin events latched.
7412
+ resume: true,
7413
+ structuredEvents: true,
7414
+ // The input box tolerates paste-without-submit, but manual prefill has no
7415
+ // card users yet — keep the initial-query behavior unchanged for now.
7416
+ prefill: false,
7417
+ // Plugin events arriving while parked ARE the passive signal.
7418
+ passiveTurns: true,
7419
+ // opencode silently discards early stdin while the TUI paints; the pasted
7420
+ // text itself is lost, so the readiness probe must gate the first write
7421
+ // even though structured events exist now.
7422
+ rawPromptGate: true
7423
+ };
7424
+ resolveBinary(env = this.env) {
7425
+ const override = env.CONVEYOR_OPENCODE_BIN;
7426
+ const found = override ? findOnPath(override, env) : findOnPath("opencode", env);
7427
+ if (!found) {
7428
+ throw new TuiUnavailableError(
7429
+ "opencode",
7430
+ "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."
7431
+ );
7432
+ }
7433
+ return found;
7434
+ }
7435
+ buildSpawn(input) {
7436
+ const env = { ...inheritedEnv() };
7437
+ const credentials = buildOpenCodeCredentialEnv(this.env);
7438
+ for (const key of credentials.unset) delete env[key];
7439
+ Object.assign(env, credentials.set);
7440
+ const configContent = buildOpenCodeConfigContent({
7441
+ entries: input.mcpEntries ?? {},
7442
+ systemPromptPath: input.instructionsPath ?? null,
7443
+ readOnly: input.options.permissionMode === "plan",
7444
+ pluginPath: input.pluginPath ?? null
7445
+ });
7446
+ if (configContent) env.OPENCODE_CONFIG_CONTENT = configContent;
7447
+ if (input.eventsSinkPath) env[OPENCODE_EVENTS_FILE_ENV] = input.eventsSinkPath;
7448
+ const model = resolveOpenCodeModel(this.env, input.options.model);
7449
+ const args = [];
7450
+ if (model) args.push("--model", model);
7451
+ if (input.resume) args.push("--session", input.resume);
7452
+ return { file: this.resolveBinary(), args, env };
7453
+ }
7454
+ async prepareEnvironment() {
7455
+ await prepareOpenCodeCredentials(this.env);
7456
+ }
7457
+ spawnFingerprint(input) {
7458
+ return JSON.stringify([
7459
+ "opencode",
7460
+ input.model,
7461
+ input.cwd,
7462
+ input.permissionMode,
7463
+ input.appendSystemPrompt ?? ""
7464
+ ]);
7465
+ }
7466
+ encodePromptBytes(text) {
7467
+ return buildPromptBytes(text);
7468
+ }
7469
+ buildExitErrors(exitCode, rawOutput) {
7470
+ const errors = [`opencode exited (code ${exitCode}) without a result`];
7471
+ const tail = cleanTerminalOutput(rawOutput);
7472
+ if (tail) errors.push(`Last terminal output before exit:
7473
+ ${tail}`);
7474
+ return errors;
7475
+ }
7476
+ };
7477
+
7478
+ // src/harness/pty/adapters/index.ts
7479
+ function resolveTuiKindFromEnv(env) {
7480
+ const raw = env.CONVEYOR_TUI ?? "claude-code";
7481
+ if (TUI_KINDS.includes(raw)) return raw;
7482
+ throw new Error(`Unknown TUI "${raw}" in CONVEYOR_TUI (expected: ${TUI_KINDS.join(", ")})`);
7483
+ }
7484
+ function resolveTuiAdapter(kind = "claude-code") {
7485
+ switch (kind) {
7486
+ case "claude-code":
7487
+ return new ClaudeTuiAdapter();
7488
+ case "opencode":
7489
+ return new OpenCodeTuiAdapter();
7490
+ default:
7491
+ throw new Error(`Unknown TUI kind: ${kind}`);
7492
+ }
6616
7493
  }
6617
7494
 
6618
7495
  // src/harness/pty/stream-server.ts
6619
7496
  import net from "net";
6620
- var logger = createServiceLogger("PtyStreamServer");
7497
+ var logger2 = createServiceLogger("PtyStreamServer");
6621
7498
  var RING_MAX_CHARS = 256 * 1024;
6622
7499
  var PtyStreamServer = class {
6623
7500
  constructor(options) {
6624
7501
  this.options = options;
6625
7502
  this.server = net.createServer((socket) => this.handleConnection(socket));
6626
7503
  this.server.on("error", (err) => {
6627
- logger.warn(`PTY stream server error: ${err.message}`);
7504
+ logger2.warn(`PTY stream server error: ${err.message}`);
6628
7505
  });
6629
7506
  }
6630
7507
  options;
@@ -6665,7 +7542,7 @@ var PtyStreamServer = class {
6665
7542
  return port;
6666
7543
  }
6667
7544
  }
6668
- logger.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
7545
+ logger2.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
6669
7546
  return null;
6670
7547
  }
6671
7548
  tryListen(port) {
@@ -6786,7 +7663,7 @@ var PtyStreamServer = class {
6786
7663
  };
6787
7664
 
6788
7665
  // src/harness/pty/direct-stream.ts
6789
- var logger2 = createServiceLogger("PtyDirectStream");
7666
+ var logger3 = createServiceLogger("PtyDirectStream");
6790
7667
  var RELAY_COALESCE_MS = 2e3;
6791
7668
  var RELAY_MAX_BUFFER_CHARS = 48 * 1024;
6792
7669
  var DirectStreamController = class {
@@ -6835,7 +7712,7 @@ var DirectStreamController = class {
6835
7712
  });
6836
7713
  void created.listen().then((port) => this.onListening(created, port)).catch((err) => {
6837
7714
  this.starting = false;
6838
- logger2.warn(
7715
+ logger3.warn(
6839
7716
  `PTY stream server failed to start: ${err instanceof Error ? err.message : String(err)}`
6840
7717
  );
6841
7718
  });
@@ -6848,7 +7725,7 @@ var DirectStreamController = class {
6848
7725
  }
6849
7726
  this.server = created;
6850
7727
  this.reporter.reportPtyStream(port);
6851
- logger2.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
7728
+ logger3.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
6852
7729
  }
6853
7730
  /** Push the min box across both transports to the pty. */
6854
7731
  applyDims() {
@@ -10154,7 +11031,7 @@ function buildMutationTools(connection, config) {
10154
11031
  }
10155
11032
 
10156
11033
  // src/tools/attachment-tools.ts
10157
- import { basename, extname, isAbsolute, join as join6 } from "path";
11034
+ import { basename, extname, isAbsolute, join as join10 } from "path";
10158
11035
  var MIME_BY_EXT = {
10159
11036
  ".png": "image/png",
10160
11037
  ".jpg": "image/jpeg",
@@ -10205,7 +11082,7 @@ ${snippet}`;
10205
11082
  function buildUploadAttachmentTool(connection, config) {
10206
11083
  return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
10207
11084
  try {
10208
- const filePath = isAbsolute(path2) ? path2 : join6(config.workspaceDir, path2);
11085
+ const filePath = isAbsolute(path2) ? path2 : join10(config.workspaceDir, path2);
10209
11086
  const mimeType = inferMimeType(filePath);
10210
11087
  const info = await statWorkspacePath(filePath);
10211
11088
  if (!info.isFile) {
@@ -10717,7 +11594,7 @@ import { z as z16 } from "zod";
10717
11594
 
10718
11595
  // src/execution/context-path-verifier.ts
10719
11596
  import { readFile as readFile2 } from "fs/promises";
10720
- import { isAbsolute as isAbsolute2, join as join7, normalize } from "path";
11597
+ import { isAbsolute as isAbsolute2, join as join11, normalize } from "path";
10721
11598
  var PROBLEM_TEXT = {
10722
11599
  not_found: "does not exist in the repo",
10723
11600
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -10762,7 +11639,7 @@ async function verifyContextPaths(links, workspaceDir) {
10762
11639
  problems.push({ type: link.type, path: link.path, reason: shape });
10763
11640
  continue;
10764
11641
  }
10765
- const absolutePath = join7(workspaceDir, toRelativePath(link.path));
11642
+ const absolutePath = join11(workspaceDir, toRelativePath(link.path));
10766
11643
  const stat = await statWorkspacePath(absolutePath);
10767
11644
  const wantsDirectory = expectsDirectory(link.type);
10768
11645
  if (!stat.exists) {
@@ -11435,38 +12312,6 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
11435
12312
  });
11436
12313
  }
11437
12314
 
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
12315
  // src/execution/playwright-mcp.ts
11471
12316
  var PLAYWRIGHT_MCP_BINARIES = ["playwright-mcp", "mcp-server-playwright"];
11472
12317
  var PLAYWRIGHT_MCP_ARGS = ["--browser", "chromium", "--headless", "--no-sandbox", "--isolated"];
@@ -11479,7 +12324,7 @@ function resolvePlaywrightMcpServer(env = process.env) {
11479
12324
  }
11480
12325
 
11481
12326
  // src/execution/event-handlers.ts
11482
- var logger3 = createServiceLogger("event-handlers");
12327
+ var logger4 = createServiceLogger("event-handlers");
11483
12328
  function safeVoid(promise, context) {
11484
12329
  if (promise && typeof promise.catch === "function") {
11485
12330
  promise.catch((err) => {
@@ -11634,7 +12479,7 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
11634
12479
  }
11635
12480
  function handleRateLimitEvent(event, host) {
11636
12481
  const { rate_limit_info } = event;
11637
- logger3.info("Rate limit event received", { rate_limit_info });
12482
+ logger4.info("Rate limit event received", { rate_limit_info });
11638
12483
  const status = rate_limit_info.status;
11639
12484
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
11640
12485
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
@@ -12357,7 +13202,7 @@ function buildCanUseTool(host) {
12357
13202
  }
12358
13203
 
12359
13204
  // src/execution/query-executor.ts
12360
- var logger4 = createServiceLogger("QueryExecutor");
13205
+ var logger5 = createServiceLogger("QueryExecutor");
12361
13206
  var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
12362
13207
  var RETRY_DELAYS_MS2 = [6e4, 12e4, 18e4, 3e5];
12363
13208
  function buildHooks(host) {
@@ -12449,7 +13294,7 @@ function repairTornSessionFile(path2) {
12449
13294
  }
12450
13295
  if (keepEnd === content.length) return false;
12451
13296
  truncateSync(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
12452
- logger4.warn("Repaired torn transcript before resume", {
13297
+ logger5.warn("Repaired torn transcript before resume", {
12453
13298
  path: path2,
12454
13299
  trimmedBytes: content.length - keepEnd
12455
13300
  });
@@ -12500,10 +13345,11 @@ function buildQueryOptions(host, context) {
12500
13345
  permissionMode: needsCanUseTool ? "plan" : "bypassPermissions",
12501
13346
  allowDangerouslySkipPermissions: !needsCanUseTool,
12502
13347
  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 } : {},
13348
+ // A spawned CLI never sees `systemPrompt` (an SDK-only option) — deliver the
13349
+ // same text via `appendSystemPrompt`, which each spawning harness routes its
13350
+ // own way (`claude --append-system-prompt`; opencode an `instructions` file).
13351
+ // Without this an opencode card would run with no task context at all.
13352
+ ...host.harnessKind !== "sdk" && systemPromptText ? { appendSystemPrompt: systemPromptText } : {},
12507
13353
  // Auto mode pre-exit (residual sessions only — auto now boots post-exit):
12508
13354
  // after the ExitPlanMode hook allows the call, the CLI's plan dialog still
12509
13355
  // renders — press Enter so the autonomous agent continues building in the
@@ -12529,7 +13375,7 @@ function buildQueryOptions(host, context) {
12529
13375
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
12530
13376
  enableFileCheckpointing: settings.enableFileCheckpointing,
12531
13377
  stderr: (data) => {
12532
- logger4.warn("Claude Code stderr", { data: data.trimEnd() });
13378
+ logger5.warn("Claude Code stderr", { data: data.trimEnd() });
12533
13379
  }
12534
13380
  };
12535
13381
  }
@@ -12580,16 +13426,16 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
12580
13426
 
12581
13427
  The team says:
12582
13428
  ${followUpText}` : followUpText;
12583
- const isPty = host.harnessKind === "pty";
13429
+ const skipImages = !supportsImageBlocks(host.harnessKind);
12584
13430
  if (isPmMode) {
12585
- const prompt = buildMultimodalPrompt(textPrompt, context, isPty);
13431
+ const prompt = buildMultimodalPrompt(textPrompt, context, skipImages);
12586
13432
  if (followUpImages.length > 0 && Array.isArray(prompt)) {
12587
13433
  prompt.push(...followUpImages);
12588
13434
  }
12589
13435
  return prompt;
12590
13436
  }
12591
13437
  if (followUpImages.length > 0) {
12592
- if (isPty) {
13438
+ if (skipImages) {
12593
13439
  const refs = followUpImages.map(
12594
13440
  () => `[Image attachment \u2014 use list_task_files / get_attachment to view]`
12595
13441
  );
@@ -12639,7 +13485,7 @@ function abortWatchedTurn(host, stderrLine, chatMessage) {
12639
13485
  host.connection.sendEvent({ type: "error", message: chatMessage });
12640
13486
  host.abortController?.abort();
12641
13487
  }
12642
- async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead) {
13488
+ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMountDead, disableWedgeAbort) {
12643
13489
  if (state.abortedAsWedged) {
12644
13490
  process.stderr.write(
12645
13491
  "[conveyor-agent] Wedged turn did not settle after abort \u2014 abandoning the turn stream\n"
@@ -12657,6 +13503,10 @@ async function handleTurnSilence(host, state, waitMs, silenceTimeoutMs, probeMou
12657
13503
  return "keep_waiting";
12658
13504
  }
12659
13505
  if (state.silentMs < silenceTimeoutMs) return "keep_waiting";
13506
+ if (disableWedgeAbort) {
13507
+ state.silentMs = 0;
13508
+ return "keep_waiting";
13509
+ }
12660
13510
  if (isHeavyGateActive()) {
12661
13511
  state.silentMs = 0;
12662
13512
  return "keep_waiting";
@@ -12693,7 +13543,8 @@ async function* watchForParkedTui(inner, host, opts) {
12693
13543
  state,
12694
13544
  waitMs,
12695
13545
  silenceTimeoutMs,
12696
- opts?.probeMountDead
13546
+ opts?.probeMountDead,
13547
+ opts?.disableWedgeAbort
12697
13548
  );
12698
13549
  if (outcome === "abandon") return;
12699
13550
  continue;
@@ -12740,7 +13591,7 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
12740
13591
  await runFollowUpQuery(host, context, options, resume, followUpContent);
12741
13592
  return;
12742
13593
  }
12743
- if (isDiscoveryLike && (resume || host.harnessKind !== "pty")) {
13594
+ if (isDiscoveryLike && (resume || host.harnessKind === "sdk")) {
12744
13595
  return;
12745
13596
  }
12746
13597
  await runInitialQuery(host, context, options, resume, promptDelivery);
@@ -12795,9 +13646,12 @@ async function runPassiveTurn(host, context) {
12795
13646
  }
12796
13647
  async function trackAndRun(host, context, options, agentQuery) {
12797
13648
  if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
12798
- agentQuery = watchForParkedTui(agentQuery, host, {
12799
- probeMountDead: () => isConfigHomeMountDead(options.cwd)
12800
- });
13649
+ const rawRelay = host.harness.emitsStructuredEvents === false;
13650
+ agentQuery = watchForParkedTui(
13651
+ agentQuery,
13652
+ host,
13653
+ rawRelay ? { disableWedgeAbort: true } : { probeMountDead: () => isConfigHomeMountDead(options.cwd) }
13654
+ );
12801
13655
  }
12802
13656
  host.activeQuery = agentQuery;
12803
13657
  try {
@@ -12822,9 +13676,10 @@ function selectInitialPromptInput(promptDelivery, initialPrompt, context, baseAp
12822
13676
  };
12823
13677
  }
12824
13678
  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"),
13679
+ // Only the SDK takes image blocks: a PTY would paste them into the TUI as
13680
+ // text and headless opencode passes argv — the prompt body already links
13681
+ // each image (get_attachment / downloadUrl).
13682
+ prompt: buildMultimodalPrompt(initialPrompt, context, !supportsImageBlocks(harnessKind)),
12828
13683
  appendSystemPrompt: baseAppendSystemPrompt
12829
13684
  };
12830
13685
  }
@@ -12865,7 +13720,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
12865
13720
  const retryPrompt = buildMultimodalPrompt(
12866
13721
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12867
13722
  context,
12868
- lastErrorWasImage || host.harnessKind === "pty"
13723
+ lastErrorWasImage || !supportsImageBlocks(host.harnessKind)
12869
13724
  );
12870
13725
  return host.harness.executeQuery({
12871
13726
  prompt: host.createInputStream(retryPrompt),
@@ -12894,7 +13749,7 @@ async function handleAuthError(context, host, options) {
12894
13749
  const freshPrompt = buildMultimodalPrompt(
12895
13750
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12896
13751
  context,
12897
- host.harnessKind === "pty"
13752
+ !supportsImageBlocks(host.harnessKind)
12898
13753
  );
12899
13754
  const freshQuery = host.harness.executeQuery({
12900
13755
  prompt: host.createInputStream(freshPrompt),
@@ -12909,7 +13764,7 @@ async function handleStaleSession(context, host, options) {
12909
13764
  const freshPrompt = buildMultimodalPrompt(
12910
13765
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12911
13766
  context,
12912
- host.harnessKind === "pty"
13767
+ !supportsImageBlocks(host.harnessKind)
12913
13768
  );
12914
13769
  const freshQuery = host.harness.executeQuery({
12915
13770
  prompt: host.createInputStream(freshPrompt),
@@ -13003,7 +13858,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
13003
13858
  const freshPrompt = buildMultimodalPrompt(
13004
13859
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13005
13860
  context,
13006
- host.harnessKind === "pty"
13861
+ !supportsImageBlocks(host.harnessKind)
13007
13862
  );
13008
13863
  const freshQuery = host.harness.executeQuery({
13009
13864
  prompt: host.createInputStream(freshPrompt),
@@ -13076,9 +13931,14 @@ async function runWithRetry(initialQuery, context, host, options) {
13076
13931
  }
13077
13932
 
13078
13933
  // src/runner/query-bridge.ts
13079
- var logger5 = createServiceLogger("QueryBridge");
13934
+ var logger6 = createServiceLogger("QueryBridge");
13080
13935
  function resolveHarnessKind() {
13081
- return process.env.CONVEYOR_FORCE_SDK_CARDS === "1" ? "sdk" : "pty";
13936
+ if (process.env.CONVEYOR_FORCE_SDK_CARDS === "1") return "sdk";
13937
+ return "pty";
13938
+ }
13939
+ function resolveCardTui(mode) {
13940
+ if (mode === "code-review") return "claude-code";
13941
+ return resolveTuiKindFromEnv(process.env);
13082
13942
  }
13083
13943
  function buildPtyBridge(connection, onBackgroundTaskDone) {
13084
13944
  return {
@@ -13109,7 +13969,11 @@ var QueryBridge = class {
13109
13969
  );
13110
13970
  bridge = this.directStream.bridge;
13111
13971
  }
13112
- this.harness = createHarness(harnessKind, bridge);
13972
+ this.harness = createHarness(
13973
+ harnessKind,
13974
+ bridge,
13975
+ harnessKind === "pty" ? resolveTuiAdapter(resolveCardTui(runnerConfig.mode)) : void 0
13976
+ );
13113
13977
  }
13114
13978
  connection;
13115
13979
  mode;
@@ -13249,9 +14113,9 @@ var QueryBridge = class {
13249
14113
  const msg = err instanceof Error ? err.message : String(err);
13250
14114
  const isAbort = this._stopped || /abort/i.test(msg);
13251
14115
  if (isAbort) {
13252
- logger5.info("Query stopped by user", { error: msg });
14116
+ logger6.info("Query stopped by user", { error: msg });
13253
14117
  } else {
13254
- logger5.error("Query execution failed", { error: msg });
14118
+ logger6.error("Query execution failed", { error: msg });
13255
14119
  this.connection.sendEvent({ type: "error", message: msg });
13256
14120
  }
13257
14121
  } finally {
@@ -13277,9 +14141,9 @@ var QueryBridge = class {
13277
14141
  const msg = err instanceof Error ? err.message : String(err);
13278
14142
  const isAbort = this._stopped || /abort/i.test(msg);
13279
14143
  if (isAbort) {
13280
- logger5.info("Passive turn stopped", { error: msg });
14144
+ logger6.info("Passive turn stopped", { error: msg });
13281
14145
  } else {
13282
- logger5.error("Passive turn failed", { error: msg });
14146
+ logger6.error("Passive turn failed", { error: msg });
13283
14147
  this.connection.sendEvent({ type: "error", message: msg });
13284
14148
  }
13285
14149
  } finally {
@@ -13466,11 +14330,11 @@ function parseResetInstant(rowText, now, defaultTz = localTimeZone()) {
13466
14330
 
13467
14331
  // src/usage/parse-usage.ts
13468
14332
  var ESC = "\\u001b";
13469
- var ANSI_CSI2 = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
14333
+ var ANSI_CSI = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
13470
14334
  var ANSI_OSC = new RegExp(`${ESC}\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\)`, "g");
13471
14335
  var BAR_GLYPHS = /[─-▟]/g;
13472
14336
  function normalizeUsageText(stdout) {
13473
- return stdout.replace(ANSI_CSI2, "").replace(ANSI_OSC, "").replace(BAR_GLYPHS, " ").replace(/\r/g, "");
14337
+ return stdout.replace(ANSI_CSI, "").replace(ANSI_OSC, "").replace(BAR_GLYPHS, " ").replace(/\r/g, "");
13474
14338
  }
13475
14339
  function parseUsageGauges(stdout, now = Date.now(), defaultTz) {
13476
14340
  const text = normalizeUsageText(stdout);
@@ -13571,10 +14435,10 @@ var UsageProbeRun = class {
13571
14435
  }
13572
14436
  };
13573
14437
  async function runUsageProbe(deps = {}) {
13574
- let spawn = deps.spawn;
13575
- if (!spawn) {
14438
+ let spawn2 = deps.spawn;
14439
+ if (!spawn2) {
13576
14440
  try {
13577
- spawn = await resolvePtySpawn();
14441
+ spawn2 = await resolvePtySpawn();
13578
14442
  } catch {
13579
14443
  return "";
13580
14444
  }
@@ -13590,7 +14454,7 @@ async function runUsageProbe(deps = {}) {
13590
14454
  return new Promise((resolve) => {
13591
14455
  let child;
13592
14456
  try {
13593
- child = spawn(binary, [], {
14457
+ child = spawn2(binary, [], {
13594
14458
  name: "xterm-256color",
13595
14459
  cols: 120,
13596
14460
  rows: 45,
@@ -13606,7 +14470,7 @@ async function runUsageProbe(deps = {}) {
13606
14470
  }
13607
14471
 
13608
14472
  // src/execution/usage-sampler.ts
13609
- var logger6 = createServiceLogger("usage-sampler");
14473
+ var logger7 = createServiceLogger("usage-sampler");
13610
14474
  function isAttributable(identity, sessionToken) {
13611
14475
  if (!identity) return { ok: true };
13612
14476
  if (identity.hasRefreshToken) {
@@ -13622,7 +14486,7 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
13622
14486
  try {
13623
14487
  const attributable = isAttributable(await readIdentity(), token);
13624
14488
  if (!attributable.ok) {
13625
- logger6.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
14489
+ logger7.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
13626
14490
  reason: attributable.reason
13627
14491
  });
13628
14492
  return [];
@@ -13649,14 +14513,14 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
13649
14513
  });
13650
14514
  }
13651
14515
  if (samples.length === 0) {
13652
- logger6.info("usage sample produced no gauges", {
14516
+ logger7.info("usage sample produced no gauges", {
13653
14517
  stdoutLength: stdout.length,
13654
14518
  stdoutHead: stdout.slice(0, 200).replaceAll("\n", " ")
13655
14519
  });
13656
14520
  }
13657
14521
  return samples;
13658
14522
  } catch (error) {
13659
- logger6.info("usage sample failed", {
14523
+ logger7.info("usage sample failed", {
13660
14524
  error: error instanceof Error ? error.message : String(error)
13661
14525
  });
13662
14526
  return [];
@@ -15232,12 +16096,12 @@ var SessionRunner = class _SessionRunner {
15232
16096
  };
15233
16097
 
15234
16098
  // src/setup/config.ts
15235
- import { join as join9 } from "path";
16099
+ import { join as join12 } from "path";
15236
16100
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
15237
16101
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
15238
16102
  async function loadForwardPorts(workspaceDir) {
15239
16103
  try {
15240
- const raw = await readWorkspaceFile(join9(workspaceDir, DEVCONTAINER_PATH));
16104
+ const raw = await readWorkspaceFile(join12(workspaceDir, DEVCONTAINER_PATH));
15241
16105
  const parsed = JSON.parse(raw);
15242
16106
  const ports = (parsed.forwardPorts ?? []).filter(
15243
16107
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -15292,16 +16156,15 @@ export {
15292
16156
  applyBootstrapToEnv,
15293
16157
  AgentConnection,
15294
16158
  DEFAULT_SONNET_MODEL,
15295
- TUI_KINDS,
15296
16159
  isPermissionDeniedError,
15297
16160
  DEFAULT_LIFECYCLE_CONFIG,
15298
16161
  Lifecycle,
15299
- cleanTerminalOutput,
15300
16162
  buildSynthesizedCredentials,
15301
16163
  claudeJsonPath,
15302
- ClaudeTuiAdapter,
15303
16164
  createServiceLogger,
15304
16165
  PtyHarness,
16166
+ resolveTuiKindFromEnv,
16167
+ resolveTuiAdapter,
15305
16168
  workspacePathExists,
15306
16169
  GIT_TIMEOUT_MS,
15307
16170
  hasUncommittedChanges,
@@ -15312,8 +16175,6 @@ export {
15312
16175
  flushPendingChanges,
15313
16176
  pushToOrigin,
15314
16177
  buildProjectTools,
15315
- TuiUnavailableError,
15316
- findOnPath,
15317
16178
  resolvePlaywrightMcpServer,
15318
16179
  resolveSessionStart,
15319
16180
  parseUsageGauges,
@@ -15328,4 +16189,4 @@ export {
15328
16189
  loadConveyorConfig,
15329
16190
  unshallowRepo
15330
16191
  };
15331
- //# sourceMappingURL=chunk-36VMMHYD.js.map
16192
+ //# sourceMappingURL=chunk-ZTLGCN5G.js.map