@rallycry/conveyor-agent 10.13.51 → 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.
@@ -49,7 +49,7 @@ import {
49
49
  spawnOptionsFingerprint,
50
50
  transcriptSize,
51
51
  turnOptionsFrom
52
- } from "./chunk-KZQBYSMN.js";
52
+ } from "./chunk-UAGIXPYD.js";
53
53
 
54
54
  // src/setup/bootstrap.ts
55
55
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -3846,8 +3846,237 @@ var ClaudeCodeHarness = class {
3846
3846
 
3847
3847
  // src/harness/pty/session.ts
3848
3848
  import { randomUUID } from "crypto";
3849
- import { mkdtemp, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3850
- 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
+ }
3851
4080
 
3852
4081
  // src/harness/pty/event-queue.ts
3853
4082
  var AsyncEventQueue = class {
@@ -4128,14 +4357,16 @@ function mapTranscriptRecord(raw) {
4128
4357
  // src/harness/pty/jsonl-tailer.ts
4129
4358
  var POLL_INTERVAL_MS = 25;
4130
4359
  var JsonlTailer = class {
4131
- constructor(path2, onEvent, onRawRecord) {
4360
+ constructor(path2, onEvent, onRawRecord, mapRecord = mapTranscriptRecord) {
4132
4361
  this.path = path2;
4133
4362
  this.onEvent = onEvent;
4134
4363
  this.onRawRecord = onRawRecord;
4364
+ this.mapRecord = mapRecord;
4135
4365
  }
4136
4366
  path;
4137
4367
  onEvent;
4138
4368
  onRawRecord;
4369
+ mapRecord;
4139
4370
  offset = 0;
4140
4371
  buffer = "";
4141
4372
  timer = null;
@@ -4208,7 +4439,7 @@ var JsonlTailer = class {
4208
4439
  return;
4209
4440
  }
4210
4441
  this.onRawRecord?.(parsed);
4211
- const event = mapTranscriptRecord(parsed);
4442
+ const event = this.mapRecord(parsed);
4212
4443
  if (event) this.onEvent(event);
4213
4444
  }
4214
4445
  };
@@ -4401,17 +4632,17 @@ function mapChatRecords(raw) {
4401
4632
  }
4402
4633
 
4403
4634
  // src/harness/pty/settings.ts
4404
- import { mkdir, writeFile, chmod } from "fs/promises";
4635
+ import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
4405
4636
  import { homedir } from "os";
4406
- import { join } from "path";
4637
+ import { join as join2 } from "path";
4407
4638
  function claudeConfigHome() {
4408
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
4639
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
4409
4640
  }
4410
4641
  function projectSlug(cwd) {
4411
4642
  return cwd.replace(/\//g, "-");
4412
4643
  }
4413
4644
  function sessionTranscriptPath(cwd, sessionId) {
4414
- return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
4645
+ return join2(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
4415
4646
  }
4416
4647
  var ALLOW_RULES = [
4417
4648
  "Bash",
@@ -4614,10 +4845,10 @@ function buildPreToolUseHooks(helperPath, gateAllTools) {
4614
4845
  ];
4615
4846
  }
4616
4847
  async function writeHookSettings(dir, opts = {}) {
4617
- const helperPath = join(dir, "hook-helper.cjs");
4618
- const settingsPath = join(dir, "settings.json");
4848
+ const helperPath = join2(dir, "hook-helper.cjs");
4849
+ const settingsPath = join2(dir, "settings.json");
4619
4850
  await mkdir(dir, { recursive: true });
4620
- await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
4851
+ await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
4621
4852
  await chmod(helperPath, 493);
4622
4853
  const settings = {
4623
4854
  // Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
@@ -4654,7 +4885,7 @@ async function writeHookSettings(dir, opts = {}) {
4654
4885
  ]
4655
4886
  }
4656
4887
  };
4657
- await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4888
+ await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4658
4889
  return { settingsPath, helperPath };
4659
4890
  }
4660
4891
 
@@ -4772,8 +5003,8 @@ var PtyOutputCoalescer = class {
4772
5003
  // src/harness/pty/tool-server.ts
4773
5004
  import { createServer as createServer2 } from "http";
4774
5005
  import { z as z9 } from "zod";
4775
- import { writeFile as writeFile2 } from "fs/promises";
4776
- import { join as join2 } from "path";
5006
+ import { writeFile as writeFile3 } from "fs/promises";
5007
+ import { join as join3 } from "path";
4777
5008
  import { randomBytes } from "crypto";
4778
5009
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4779
5010
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -4964,19 +5195,19 @@ async function startToolServers(mcpServers, tempDir) {
4964
5195
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
4965
5196
  }
4966
5197
  if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null, entries: config };
4967
- const mcpConfigPath = join2(tempDir, "mcp-config.json");
4968
- await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
5198
+ const mcpConfigPath = join3(tempDir, "mcp-config.json");
5199
+ await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
4969
5200
  return { servers, mcpConfigPath, entries: config };
4970
5201
  }
4971
5202
 
4972
5203
  // src/harness/pty/credentials.ts
4973
- 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";
4974
5205
  import { homedir as homedir2 } from "os";
4975
- import { join as join3 } from "path";
5206
+ import { join as join4 } from "path";
4976
5207
  var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
4977
5208
  var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
4978
5209
  function claudeCredentialsPath() {
4979
- return join3(claudeConfigHome(), ".credentials.json");
5210
+ return join4(claudeConfigHome(), ".credentials.json");
4980
5211
  }
4981
5212
  function isConveyorCloudEnv(env = process.env) {
4982
5213
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
@@ -5067,7 +5298,7 @@ async function writeWithReadBackRetry(io2, contents, delaysMs = READ_BACK_DELAYS
5067
5298
  }
5068
5299
  function fsWriteIo(path2, mode) {
5069
5300
  return {
5070
- 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 }),
5071
5302
  read: () => readRaw(path2)
5072
5303
  };
5073
5304
  }
@@ -5139,7 +5370,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
5139
5370
  }
5140
5371
  function claudeJsonPath() {
5141
5372
  const configDir = process.env.CLAUDE_CONFIG_DIR;
5142
- return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
5373
+ return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
5143
5374
  }
5144
5375
  function asRecord(value) {
5145
5376
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -5232,7 +5463,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
5232
5463
  return changed ? JSON.stringify(config) : null;
5233
5464
  }
5234
5465
  function conveyorOauthMarkerPath() {
5235
- return join3(claudeConfigHome(), "conveyor-oauth-account.json");
5466
+ return join4(claudeConfigHome(), "conveyor-oauth-account.json");
5236
5467
  }
5237
5468
  function parseOauthIdentity(raw) {
5238
5469
  if (!raw || raw.trim() === "") return null;
@@ -5316,7 +5547,8 @@ var ClaudeTuiAdapter = class {
5316
5547
  resume: true,
5317
5548
  structuredEvents: true,
5318
5549
  prefill: true,
5319
- passiveTurns: true
5550
+ passiveTurns: true,
5551
+ rawPromptGate: false
5320
5552
  };
5321
5553
  resolveBinary(env = process.env) {
5322
5554
  return env.CONVEYOR_CLAUDE_BIN ?? "claude";
@@ -5392,6 +5624,12 @@ var PtySession = class {
5392
5624
  lastTurnCleanResult = false;
5393
5625
  socket = null;
5394
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;
5395
5633
  pty = null;
5396
5634
  tempDir = "";
5397
5635
  sawResult = false;
@@ -5507,9 +5745,18 @@ var PtySession = class {
5507
5745
  const tail = cleanTerminalOutput(this.recentOutput, MAX_DIAGNOSTIC_OUTPUT);
5508
5746
  return tail ? redact(tail).output : "";
5509
5747
  }
5510
- /** 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
+ */
5511
5754
  get sessionUuid() {
5512
- 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;
5513
5760
  }
5514
5761
  /** Fingerprint of the spawn-time options a reused process cannot change. */
5515
5762
  get spawnFingerprint() {
@@ -5524,9 +5771,19 @@ var PtySession = class {
5524
5771
  * Whether this parked process can serve a turn wanting `resume`/`fingerprint`.
5525
5772
  * Requires: live (not torn down / not exited), idle (no turn draining), a
5526
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.
5527
5783
  */
5528
5784
  canReuse(resume, fingerprint) {
5529
- 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;
5530
5787
  }
5531
5788
  get hookSocketPath() {
5532
5789
  return this.socket ? this.socket.socketPath : null;
@@ -5654,11 +5911,14 @@ var PtySession = class {
5654
5911
  return;
5655
5912
  }
5656
5913
  try {
5657
- 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) {
5658
5918
  const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
5659
5919
  await this.spawn(settingsPath, socketPath);
5660
5920
  } else {
5661
- this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
5921
+ this.tempDir = await mkdtemp(join5(sessionTempBase(), "conveyor-pty-"));
5662
5922
  await this.spawn();
5663
5923
  this.pushEvent({
5664
5924
  type: "system",
@@ -5687,6 +5947,61 @@ var PtySession = class {
5687
5947
  throw err;
5688
5948
  }
5689
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
+ }
5690
6005
  /**
5691
6006
  * Allocate the Claude-style structured-event sources: the PostToolUse hook
5692
6007
  * socket, the per-run settings file, the in-process tool servers, and the
@@ -5695,8 +6010,8 @@ var PtySession = class {
5695
6010
  * paths spawn() must wire into the child's argv/env.
5696
6011
  */
5697
6012
  async startStructuredEventSources(sessionId) {
5698
- this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
5699
- const socketPath = join4(this.tempDir, "hook.sock");
6013
+ this.tempDir = await mkdtemp(join5(sessionTempBase(), "conveyor-pty-"));
6014
+ const socketPath = join5(this.tempDir, "hook.sock");
5700
6015
  this.socket = new HookSocketServer(
5701
6016
  socketPath,
5702
6017
  (progress) => this.handleProgress(progress),
@@ -5901,16 +6216,18 @@ var PtySession = class {
5901
6216
  this.toolServers = servers;
5902
6217
  this.mcpConfigPath = mcpConfigPath;
5903
6218
  }
5904
- async spawn(settingsPath, socketPath) {
6219
+ async spawn(settingsPath, socketPath, opencodeExtras) {
6220
+ const resume = this.reportedOpenCodeId ?? this.resume;
5905
6221
  const spec = this.adapter.buildSpawn({
5906
6222
  options: this.options,
5907
- ...this.resume ? { resume: this.resume } : {},
6223
+ ...resume ? { resume } : {},
5908
6224
  ...settingsPath ? { settingsPath } : {},
5909
6225
  ...socketPath ? { hookSocketPath: socketPath } : {},
5910
6226
  // Only this config: ignore the user's ~/.claude.json / project .mcp.json
5911
6227
  // so the agent's tool set is deterministic (and a stale personal conveyor
5912
6228
  // server doesn't load).
5913
- ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
6229
+ ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {},
6230
+ ...opencodeExtras ?? {}
5914
6231
  });
5915
6232
  const spawn2 = await resolvePtySpawn();
5916
6233
  const pty = spawn2(spec.file, spec.args, {
@@ -6283,7 +6600,7 @@ var PtySession = class {
6283
6600
  // src/harness/pty/config-home-health.ts
6284
6601
  import { lstat, mkdir as mkdir4, symlink, unlink as unlink2 } from "fs/promises";
6285
6602
  import { homedir as homedir3 } from "os";
6286
- import { join as join5 } from "path";
6603
+ import { join as join6 } from "path";
6287
6604
  var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
6288
6605
  var MOUNT_DISCONNECT_MESSAGES = [
6289
6606
  "socket is not connected",
@@ -6299,10 +6616,10 @@ function isMountDisconnectError(err) {
6299
6616
  return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
6300
6617
  }
6301
6618
  function podLocalConfigHome() {
6302
- return join5(homedir3(), ".claude-local");
6619
+ return join6(homedir3(), ".claude-local");
6303
6620
  }
6304
6621
  function sharedConfigHomePath() {
6305
- return join5(homedir3(), ".claude");
6622
+ return join6(homedir3(), ".claude");
6306
6623
  }
6307
6624
  function isConfigHomeFallbackActive() {
6308
6625
  return claudeConfigHome() === podLocalConfigHome();
@@ -6333,7 +6650,7 @@ async function repointSharedConfigHomeSymlink(fallback, log) {
6333
6650
  }
6334
6651
  async function isConfigHomeMountDead(cwd) {
6335
6652
  try {
6336
- await mkdir4(join5(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
6653
+ await mkdir4(join6(claudeConfigHome(), "projects", projectSlug(cwd)), { recursive: true });
6337
6654
  return false;
6338
6655
  } catch (err) {
6339
6656
  return isMountDisconnectError(err);
@@ -6342,7 +6659,7 @@ async function isConfigHomeMountDead(cwd) {
6342
6659
  async function ensureUsableClaudeConfigHome(cwd, log) {
6343
6660
  const configHome = claudeConfigHome();
6344
6661
  try {
6345
- await mkdir4(join5(configHome, "projects", projectSlug(cwd)), { recursive: true });
6662
+ await mkdir4(join6(configHome, "projects", projectSlug(cwd)), { recursive: true });
6346
6663
  return { configHome, fellBack: false };
6347
6664
  } catch (err) {
6348
6665
  if (!isMountDisconnectError(err)) throw err;
@@ -6356,7 +6673,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
6356
6673
  }
6357
6674
  );
6358
6675
  process.env.CLAUDE_CONFIG_DIR = fallback;
6359
- await mkdir4(join5(fallback, "projects", projectSlug(cwd)), { recursive: true });
6676
+ await mkdir4(join6(fallback, "projects", projectSlug(cwd)), { recursive: true });
6360
6677
  await repointSharedConfigHomeSymlink(fallback, log);
6361
6678
  return { configHome: fallback, fellBack: true };
6362
6679
  }
@@ -6437,7 +6754,7 @@ var PtyHarness = class _PtyHarness {
6437
6754
  }
6438
6755
  }
6439
6756
  async *executeQuery(opts) {
6440
- const want = opts.resume ?? opts.options.resume;
6757
+ const want = opts.resume ?? opts.options.resume ?? this.parked?.reportedSessionId ?? void 0;
6441
6758
  const fingerprint = this.fingerprintOf(opts.options);
6442
6759
  let session;
6443
6760
  if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(opts.options)) {
@@ -6697,7 +7014,7 @@ import { spawn } from "child_process";
6697
7014
 
6698
7015
  // src/harness/pty/adapters/types.ts
6699
7016
  import { accessSync, constants, statSync } from "fs";
6700
- import { join as join6 } from "path";
7017
+ import { join as join7 } from "path";
6701
7018
  var TuiUnavailableError = class extends Error {
6702
7019
  constructor(tui, message) {
6703
7020
  super(message);
@@ -6721,7 +7038,7 @@ function findOnPath(binary, env = process.env) {
6721
7038
  }
6722
7039
  for (const dir of (env.PATH ?? "").split(":")) {
6723
7040
  if (!dir) continue;
6724
- const candidate = join6(dir, binary);
7041
+ const candidate = join7(dir, binary);
6725
7042
  if (isExecutable(candidate)) return candidate;
6726
7043
  }
6727
7044
  return null;
@@ -6729,18 +7046,18 @@ function findOnPath(binary, env = process.env) {
6729
7046
 
6730
7047
  // src/harness/pty/adapters/opencode-auth.ts
6731
7048
  import { promises as fs } from "fs";
6732
- import { dirname as dirname2, join as join7 } from "path";
7049
+ import { dirname as dirname2, join as join8 } from "path";
6733
7050
  import { homedir as homedir4 } from "os";
6734
7051
  var logger = createServiceLogger("opencode-auth");
6735
7052
  var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
6736
7053
  var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
6737
7054
  function opencodeAuthPath(env) {
6738
- const dataHome = env.XDG_DATA_HOME ?? join7(env.HOME ?? homedir4(), ".local", "share");
6739
- return join7(dataHome, "opencode", "auth.json");
7055
+ const dataHome = env.XDG_DATA_HOME ?? join8(env.HOME ?? homedir4(), ".local", "share");
7056
+ return join8(dataHome, "opencode", "auth.json");
6740
7057
  }
6741
7058
  function opencodeConfigPath(env) {
6742
- const configHome = env.XDG_CONFIG_HOME ?? join7(env.HOME ?? homedir4(), ".config");
6743
- return join7(configHome, "opencode", "opencode.json");
7059
+ const configHome = env.XDG_CONFIG_HOME ?? join8(env.HOME ?? homedir4(), ".config");
7060
+ return join8(configHome, "opencode", "opencode.json");
6744
7061
  }
6745
7062
  function parseOauthSeed(b64) {
6746
7063
  if (!b64) return null;
@@ -6883,7 +7200,7 @@ function truncatePrompt(prompt) {
6883
7200
  [prompt truncated at ${PROMPT_MAX_CHARS} characters]`;
6884
7201
  }
6885
7202
  function buildOpenCodeConfigContent(input) {
6886
- const { entries, systemPromptPath, readOnly } = input;
7203
+ const { entries, systemPromptPath, readOnly, pluginPath } = input;
6887
7204
  const mcp = {};
6888
7205
  for (const [name, entry] of Object.entries(entries)) {
6889
7206
  if (entry.type === "http") {
@@ -6905,106 +7222,18 @@ function buildOpenCodeConfigContent(input) {
6905
7222
  const config = {};
6906
7223
  if (Object.keys(mcp).length > 0) config.mcp = mcp;
6907
7224
  if (systemPromptPath) config.instructions = [systemPromptPath];
6908
- if (readOnly) {
6909
- config.permission = {
6910
- "*": "allow",
6911
- ...Object.fromEntries(MUTATING_TOOLS.map((tool2) => [tool2, "deny"]))
6912
- };
6913
- }
7225
+ if (pluginPath) config.plugin = [`file://${pluginPath}`];
7226
+ config.permission = {
7227
+ "*": "allow",
7228
+ ...readOnly ? Object.fromEntries(MUTATING_TOOLS.map((tool2) => [tool2, "deny"])) : {}
7229
+ };
6914
7230
  if (Object.keys(config).length === 0) return null;
6915
7231
  return JSON.stringify(config);
6916
7232
  }
6917
7233
 
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
7234
  // 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";
7235
+ import { mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile6 } from "fs/promises";
7236
+ import { join as join9 } from "path";
7008
7237
  var MAX_STDERR_TAIL = 4e3;
7009
7238
  var OpenCodeHeadlessHarness = class {
7010
7239
  /** NDJSON from `--format json` is a trusted structured source. */
@@ -7026,7 +7255,7 @@ var OpenCodeHeadlessHarness = class {
7026
7255
  const prompt = await collectPrompt(opts.prompt);
7027
7256
  const binary = resolveOpenCodeBinary(process.env);
7028
7257
  await prepareOpenCodeCredentials(process.env);
7029
- this.tempDir = await mkdtemp2(join8(sessionTempBase(), "opencode-headless-"));
7258
+ this.tempDir = await mkdtemp2(join9(sessionTempBase(), "opencode-headless-"));
7030
7259
  const { servers, entries } = await startToolServers(
7031
7260
  opts.options.mcpServers ?? {},
7032
7261
  this.tempDir
@@ -7108,8 +7337,8 @@ var OpenCodeHeadlessHarness = class {
7108
7337
  */
7109
7338
  async writeSystemPrompt(text) {
7110
7339
  if (!text || text.trim() === "") return null;
7111
- const path2 = join8(this.tempDir, "conveyor-instructions.md");
7112
- await writeFile4(path2, text, "utf8");
7340
+ const path2 = join9(this.tempDir, "conveyor-instructions.md");
7341
+ await writeFile6(path2, text, "utf8");
7113
7342
  return path2;
7114
7343
  }
7115
7344
  /**
@@ -7179,10 +7408,18 @@ var OpenCodeTuiAdapter = class {
7179
7408
  env;
7180
7409
  id = "opencode";
7181
7410
  capabilities = {
7182
- resume: false,
7183
- structuredEvents: false,
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.
7184
7416
  prefill: false,
7185
- passiveTurns: 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
7186
7423
  };
7187
7424
  resolveBinary(env = this.env) {
7188
7425
  const override = env.CONVEYOR_OPENCODE_BIN;
@@ -7200,16 +7437,31 @@ var OpenCodeTuiAdapter = class {
7200
7437
  const credentials = buildOpenCodeCredentialEnv(this.env);
7201
7438
  for (const key of credentials.unset) delete env[key];
7202
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;
7203
7448
  const model = resolveOpenCodeModel(this.env, input.options.model);
7204
7449
  const args = [];
7205
7450
  if (model) args.push("--model", model);
7451
+ if (input.resume) args.push("--session", input.resume);
7206
7452
  return { file: this.resolveBinary(), args, env };
7207
7453
  }
7208
7454
  async prepareEnvironment() {
7209
7455
  await prepareOpenCodeCredentials(this.env);
7210
7456
  }
7211
7457
  spawnFingerprint(input) {
7212
- return JSON.stringify(["opencode", input.model, input.cwd]);
7458
+ return JSON.stringify([
7459
+ "opencode",
7460
+ input.model,
7461
+ input.cwd,
7462
+ input.permissionMode,
7463
+ input.appendSystemPrompt ?? ""
7464
+ ]);
7213
7465
  }
7214
7466
  encodePromptBytes(text) {
7215
7467
  return buildPromptBytes(text);
@@ -10779,7 +11031,7 @@ function buildMutationTools(connection, config) {
10779
11031
  }
10780
11032
 
10781
11033
  // src/tools/attachment-tools.ts
10782
- import { basename, extname, isAbsolute, join as join9 } from "path";
11034
+ import { basename, extname, isAbsolute, join as join10 } from "path";
10783
11035
  var MIME_BY_EXT = {
10784
11036
  ".png": "image/png",
10785
11037
  ".jpg": "image/jpeg",
@@ -10830,7 +11082,7 @@ ${snippet}`;
10830
11082
  function buildUploadAttachmentTool(connection, config) {
10831
11083
  return defineContractTool(uploadAttachmentContract, async ({ path: path2, title, tags }) => {
10832
11084
  try {
10833
- const filePath = isAbsolute(path2) ? path2 : join9(config.workspaceDir, path2);
11085
+ const filePath = isAbsolute(path2) ? path2 : join10(config.workspaceDir, path2);
10834
11086
  const mimeType = inferMimeType(filePath);
10835
11087
  const info = await statWorkspacePath(filePath);
10836
11088
  if (!info.isFile) {
@@ -11342,7 +11594,7 @@ import { z as z16 } from "zod";
11342
11594
 
11343
11595
  // src/execution/context-path-verifier.ts
11344
11596
  import { readFile as readFile2 } from "fs/promises";
11345
- import { isAbsolute as isAbsolute2, join as join10, normalize } from "path";
11597
+ import { isAbsolute as isAbsolute2, join as join11, normalize } from "path";
11346
11598
  var PROBLEM_TEXT = {
11347
11599
  not_found: "does not exist in the repo",
11348
11600
  expected_folder: "is a file, not a folder \u2014 use type 'file', 'rule', or 'doc'",
@@ -11387,7 +11639,7 @@ async function verifyContextPaths(links, workspaceDir) {
11387
11639
  problems.push({ type: link.type, path: link.path, reason: shape });
11388
11640
  continue;
11389
11641
  }
11390
- const absolutePath = join10(workspaceDir, toRelativePath(link.path));
11642
+ const absolutePath = join11(workspaceDir, toRelativePath(link.path));
11391
11643
  const stat = await statWorkspacePath(absolutePath);
11392
11644
  const wantsDirectory = expectsDirectory(link.type);
11393
11645
  if (!stat.exists) {
@@ -13680,9 +13932,9 @@ async function runWithRetry(initialQuery, context, host, options) {
13680
13932
 
13681
13933
  // src/runner/query-bridge.ts
13682
13934
  var logger6 = createServiceLogger("QueryBridge");
13683
- function resolveHarnessKind(mode) {
13935
+ function resolveHarnessKind() {
13684
13936
  if (process.env.CONVEYOR_FORCE_SDK_CARDS === "1") return "sdk";
13685
- return resolveCardTui(mode) === "opencode" ? "opencode" : "pty";
13937
+ return "pty";
13686
13938
  }
13687
13939
  function resolveCardTui(mode) {
13688
13940
  if (mode === "code-review") return "claude-code";
@@ -13707,7 +13959,7 @@ var QueryBridge = class {
13707
13959
  this.mode = mode;
13708
13960
  this.runnerConfig = runnerConfig;
13709
13961
  this.callbacks = callbacks;
13710
- const harnessKind = resolveHarnessKind(runnerConfig.mode);
13962
+ const harnessKind = resolveHarnessKind();
13711
13963
  this.harnessKind = harnessKind;
13712
13964
  let bridge;
13713
13965
  if (harnessKind === "pty") {
@@ -15844,12 +16096,12 @@ var SessionRunner = class _SessionRunner {
15844
16096
  };
15845
16097
 
15846
16098
  // src/setup/config.ts
15847
- import { join as join11 } from "path";
16099
+ import { join as join12 } from "path";
15848
16100
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
15849
16101
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
15850
16102
  async function loadForwardPorts(workspaceDir) {
15851
16103
  try {
15852
- const raw = await readWorkspaceFile(join11(workspaceDir, DEVCONTAINER_PATH));
16104
+ const raw = await readWorkspaceFile(join12(workspaceDir, DEVCONTAINER_PATH));
15853
16105
  const parsed = JSON.parse(raw);
15854
16106
  const ports = (parsed.forwardPorts ?? []).filter(
15855
16107
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -15937,4 +16189,4 @@ export {
15937
16189
  loadConveyorConfig,
15938
16190
  unshallowRepo
15939
16191
  };
15940
- //# sourceMappingURL=chunk-EQO7QFGN.js.map
16192
+ //# sourceMappingURL=chunk-ZTLGCN5G.js.map