@rallycry/conveyor-agent 10.2.4 → 10.3.0

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.
@@ -1909,6 +1909,7 @@ async function restoreOnBoot(bundle, cwd) {
1909
1909
  import { z } from "zod";
1910
1910
  import { z as z2 } from "zod";
1911
1911
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
1912
+ var TUI_KINDS = ["claude-code", "opencode"];
1912
1913
  var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
1913
1914
  var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
1914
1915
  var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
@@ -2034,6 +2035,10 @@ var SessionStopRequestSchema = z.object({
2034
2035
  sessionId: z.string(),
2035
2036
  reason: z.string().optional()
2036
2037
  });
2038
+ var EndReviewSessionRequestSchema = z.object({
2039
+ sessionId: z.string(),
2040
+ reason: z.enum(["approved", "changes_requested", "finished"]).optional()
2041
+ });
2037
2042
  var ConnectAgentRequestSchema = z.object({
2038
2043
  sessionId: z.string()
2039
2044
  });
@@ -2444,9 +2449,14 @@ var ListMyLiveSessionsRequestSchema = z2.object({
2444
2449
  /** Admin-only: list another member's sessions instead of the caller's. */
2445
2450
  targetUserId: z2.string().optional()
2446
2451
  });
2452
+ var GetProjectAvailableTuisRequestSchema = z2.object({
2453
+ projectId: z2.string()
2454
+ });
2447
2455
  var StartAdhocSessionRequestSchema = z2.object({
2448
2456
  projectId: z2.string(),
2449
2457
  label: z2.string().max(200).optional(),
2458
+ /** Coding-agent key to launch under — validated pick-time (ownership + TUI availability) in the handler. */
2459
+ codingAgentKeyId: z2.string().optional(),
2450
2460
  /**
2451
2461
  * Session role. Constrained: other task-less modes fall through to the pm
2452
2462
  * runner in the pod entrypoint, and "review" would crash without a task.
@@ -2641,9 +2651,15 @@ var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
2641
2651
  var TASK_CHAT_HISTORY_LIMIT = 20;
2642
2652
  var PM_CHAT_HISTORY_LIMIT = 40;
2643
2653
  var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
2654
+ var PRE_BUILD_TASK_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
2655
+ function hasTaskPlan(plan) {
2656
+ return !!plan?.trim();
2657
+ }
2658
+ function taskNeedsPlanning(task) {
2659
+ return !hasTaskPlan(task.plan) && PRE_BUILD_TASK_STATUSES.has(task.status) && !task.githubPRUrl;
2660
+ }
2644
2661
 
2645
2662
  // src/runner/mode-controller.ts
2646
- var FRESH_AUTO_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
2647
2663
  var ModeController = class {
2648
2664
  _mode;
2649
2665
  _hasExitedPlanMode = false;
@@ -2728,28 +2744,28 @@ var ModeController = class {
2728
2744
  return this._mode;
2729
2745
  }
2730
2746
  /**
2731
- * A FRESH auto task runs the read-only plan turn first (`--permission-mode plan`):
2732
- * resolveInitialMode leaves it in `auto` with `_hasExitedPlanMode = false`, and
2733
- * once the agent finishes it calls ExitPlanMode → building. Only tasks that have
2734
- * already progressed past the fresh stage bypass planning and build immediately
2735
- * with `--dangerously-skip-permissions`:
2747
+ * An auto task that still NEEDS PLANNING (no plan saved, pre-build status, no
2748
+ * PR see `taskNeedsPlanning` in @project/shared) runs the read-only plan turn
2749
+ * first (`--permission-mode plan`): resolveInitialMode leaves it in `auto` with
2750
+ * `_hasExitedPlanMode = false`, and once the agent finishes it calls
2751
+ * ExitPlanMode → building. Everything else bypasses planning and builds
2752
+ * immediately with `--dangerously-skip-permissions`:
2736
2753
  *
2737
- * - a build already underway (status past `Planning`/`Open`),
2738
- * - a PR already open (safety net if status lags),
2739
- * - a release (booted `InProgress`), or
2740
- * - a pod restart mid-build (DB `agentMode` stays `"auto"` but status advanced).
2754
+ * - a plan already on the card (e.g. a Planning card planned by a human/PM),
2755
+ * - a build already underway (status past `Planning`/`Open` releases booted
2756
+ * `InProgress`, pod restarts mid-build where DB `agentMode` stays `"auto"`),
2757
+ * - a PR already open (safety net if status lags).
2741
2758
  *
2742
2759
  * Runtime Build-after-Discovery never reaches this gate as `auto` — the server's
2743
- * `resolveModeToSend` rewrites it to `"building"` for any non-`Planning` task.
2760
+ * `resolveModeToSend` rewrites it to `"building"` for any task with a plan.
2744
2761
  */
2745
2762
  canBypassPlanning(context) {
2746
2763
  if (this._mode !== "auto") return false;
2747
- if (!FRESH_AUTO_STATUSES.has(context.status)) return true;
2748
- return !!context.githubPRUrl;
2764
+ return !taskNeedsPlanning(context);
2749
2765
  }
2750
2766
  // ── Mode transitions ───────────────────────────────────────────────
2751
2767
  /** Handle mode change from server */
2752
- handleModeChange(newMode) {
2768
+ handleModeChange(newMode, context) {
2753
2769
  if (newMode === this._mode) return { type: "noop" };
2754
2770
  if (this._runnerMode === "task" && newMode === "review") {
2755
2771
  this._mode = newMode;
@@ -2764,7 +2780,7 @@ var ModeController = class {
2764
2780
  if (this._runnerMode === "task" && newMode === "auto") {
2765
2781
  this._mode = newMode;
2766
2782
  this._isAuto = true;
2767
- this._hasExitedPlanMode = true;
2783
+ this._hasExitedPlanMode = !context || !taskNeedsPlanning(context);
2768
2784
  return { type: "restart_query", newMode: "auto" };
2769
2785
  }
2770
2786
  if (this._mode === "auto" && this._hasExitedPlanMode && newMode === "building") {
@@ -2935,6 +2951,11 @@ var Lifecycle = class {
2935
2951
  };
2936
2952
 
2937
2953
  // src/harness/types.ts
2954
+ function isExternalMcpStdioServer(value) {
2955
+ if (typeof value !== "object" || value === null) return false;
2956
+ const v = value;
2957
+ return v.type === "stdio" && typeof v.command === "string";
2958
+ }
2938
2959
  function defineTool(name, description, schema, handler, options) {
2939
2960
  return {
2940
2961
  name,
@@ -2977,9 +2998,9 @@ var ClaudeCodeHarness = class {
2977
2998
  };
2978
2999
 
2979
3000
  // src/harness/pty/session.ts
2980
- import { mkdtemp as mkdtemp2, mkdir as mkdir2, rm as rm4 } from "fs/promises";
3001
+ import { mkdtemp as mkdtemp2, mkdir as mkdir3, rm as rm5 } from "fs/promises";
2981
3002
  import { tmpdir as tmpdir3 } from "os";
2982
- import { join as join3, dirname } from "path";
3003
+ import { join as join4, dirname } from "path";
2983
3004
 
2984
3005
  // src/harness/pty/event-queue.ts
2985
3006
  var AsyncEventQueue = class {
@@ -3442,76 +3463,6 @@ function mapChatRecords(raw) {
3442
3463
  }
3443
3464
  }
3444
3465
 
3445
- // src/harness/pty/spawn-args.ts
3446
- function resolveClaudeBinary() {
3447
- return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
3448
- }
3449
- function buildSpawnArgs(input) {
3450
- const args = [];
3451
- if (input.resume) {
3452
- args.push("--resume", input.resume);
3453
- } else if (input.sessionId) {
3454
- args.push("--session-id", input.sessionId);
3455
- }
3456
- args.push("--model", input.model);
3457
- if (input.permissionMode === "bypassPermissions") {
3458
- args.push("--dangerously-skip-permissions");
3459
- } else {
3460
- args.push("--permission-mode", "plan");
3461
- }
3462
- args.push("--settings", input.settingsPath);
3463
- if (input.appendSystemPrompt) {
3464
- args.push("--append-system-prompt", input.appendSystemPrompt);
3465
- }
3466
- if (input.mcpConfigPath) {
3467
- args.push("--mcp-config", input.mcpConfigPath);
3468
- if (input.strictMcpConfig) {
3469
- args.push("--strict-mcp-config");
3470
- }
3471
- }
3472
- return args;
3473
- }
3474
- function spawnOptionsFingerprint(input) {
3475
- return JSON.stringify([
3476
- input.model,
3477
- input.permissionMode,
3478
- input.appendSystemPrompt ?? "",
3479
- input.cwd
3480
- ]);
3481
- }
3482
- var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
3483
- function cleanTerminalOutput(raw, maxChars = 1200) {
3484
- const noAnsi = raw.replace(ANSI_CSI, "");
3485
- let out = "";
3486
- for (const ch of noAnsi) {
3487
- const code = ch.charCodeAt(0);
3488
- if (ch === "\r" || ch === "\n") out += "\n";
3489
- else if (ch === " ") out += ch;
3490
- else if (code < 32 || code === 127) continue;
3491
- else out += ch;
3492
- }
3493
- const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
3494
- const text = lines.join("\n").trim();
3495
- return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
3496
- }
3497
- function isMissingBinaryFailure(tail) {
3498
- return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
3499
- }
3500
- function buildExitErrors(exitCode, rawOutput, binary) {
3501
- const errors = [`claude exited (code ${exitCode}) without a result`];
3502
- const tail = cleanTerminalOutput(rawOutput);
3503
- if (isMissingBinaryFailure(tail)) {
3504
- errors.push(
3505
- `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.`
3506
- );
3507
- }
3508
- if (tail) {
3509
- errors.push(`Last terminal output before exit:
3510
- ${tail}`);
3511
- }
3512
- return errors;
3513
- }
3514
-
3515
3466
  // src/harness/pty/settings.ts
3516
3467
  import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
3517
3468
  import { homedir } from "os";
@@ -3969,6 +3920,15 @@ async function startToolServers(mcpServers, tempDir) {
3969
3920
  const servers = [];
3970
3921
  const config = {};
3971
3922
  for (const [name, handle] of Object.entries(mcpServers)) {
3923
+ if (isExternalMcpStdioServer(handle)) {
3924
+ config[name] = {
3925
+ type: "stdio",
3926
+ command: handle.command,
3927
+ ...handle.args ? { args: handle.args } : {},
3928
+ ...handle.env ? { env: handle.env } : {}
3929
+ };
3930
+ continue;
3931
+ }
3972
3932
  const tools = handle instanceof PtyMcpServer ? handle.tools : [];
3973
3933
  if (tools.length === 0) continue;
3974
3934
  const server = new PtyToolServer(name, tools);
@@ -3976,10 +3936,242 @@ async function startToolServers(mcpServers, tempDir) {
3976
3936
  servers.push(server);
3977
3937
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
3978
3938
  }
3979
- if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
3980
- const mcpConfigPath = join2(tempDir, "mcp-config.json");
3981
- await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
3982
- return { servers, mcpConfigPath };
3939
+ if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
3940
+ const mcpConfigPath = join2(tempDir, "mcp-config.json");
3941
+ await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
3942
+ return { servers, mcpConfigPath };
3943
+ }
3944
+
3945
+ // src/harness/pty/spawn-args.ts
3946
+ function buildSpawnArgs(input) {
3947
+ const args = [];
3948
+ if (input.resume) {
3949
+ args.push("--resume", input.resume);
3950
+ } else if (input.sessionId) {
3951
+ args.push("--session-id", input.sessionId);
3952
+ }
3953
+ args.push("--model", input.model);
3954
+ if (input.permissionMode === "bypassPermissions") {
3955
+ args.push("--dangerously-skip-permissions");
3956
+ } else {
3957
+ args.push("--permission-mode", "plan");
3958
+ }
3959
+ args.push("--settings", input.settingsPath);
3960
+ if (input.appendSystemPrompt) {
3961
+ args.push("--append-system-prompt", input.appendSystemPrompt);
3962
+ }
3963
+ if (input.mcpConfigPath) {
3964
+ args.push("--mcp-config", input.mcpConfigPath);
3965
+ if (input.strictMcpConfig) {
3966
+ args.push("--strict-mcp-config");
3967
+ }
3968
+ }
3969
+ return args;
3970
+ }
3971
+ function spawnOptionsFingerprint(input) {
3972
+ return JSON.stringify([
3973
+ input.model,
3974
+ input.permissionMode,
3975
+ input.appendSystemPrompt ?? "",
3976
+ input.cwd
3977
+ ]);
3978
+ }
3979
+ var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
3980
+ function cleanTerminalOutput(raw, maxChars = 1200) {
3981
+ const noAnsi = raw.replace(ANSI_CSI, "");
3982
+ let out = "";
3983
+ for (const ch of noAnsi) {
3984
+ const code = ch.charCodeAt(0);
3985
+ if (ch === "\r" || ch === "\n") out += "\n";
3986
+ else if (ch === " ") out += ch;
3987
+ else if (code < 32 || code === 127) continue;
3988
+ else out += ch;
3989
+ }
3990
+ const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
3991
+ const text = lines.join("\n").trim();
3992
+ return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
3993
+ }
3994
+ function isMissingBinaryFailure(tail) {
3995
+ return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
3996
+ }
3997
+ function buildExitErrors(exitCode, rawOutput, binary) {
3998
+ const errors = [`claude exited (code ${exitCode}) without a result`];
3999
+ const tail = cleanTerminalOutput(rawOutput);
4000
+ if (isMissingBinaryFailure(tail)) {
4001
+ errors.push(
4002
+ `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.`
4003
+ );
4004
+ }
4005
+ if (tail) {
4006
+ errors.push(`Last terminal output before exit:
4007
+ ${tail}`);
4008
+ }
4009
+ return errors;
4010
+ }
4011
+
4012
+ // src/harness/pty/credentials.ts
4013
+ import { chmod as chmod2, mkdir as mkdir2, readFile, rm as rm4, writeFile as writeFile4 } from "fs/promises";
4014
+ import { homedir as homedir2 } from "os";
4015
+ import { join as join3 } from "path";
4016
+ var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
4017
+ var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
4018
+ function claudeCredentialsPath() {
4019
+ return join3(claudeConfigHome(), ".credentials.json");
4020
+ }
4021
+ function isConveyorCloudEnv(env = process.env) {
4022
+ return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
4023
+ }
4024
+ function parseClaudeAiOauth(raw) {
4025
+ if (!raw || raw.trim() === "") return null;
4026
+ try {
4027
+ const parsed = JSON.parse(raw);
4028
+ if (typeof parsed !== "object" || parsed === null) return null;
4029
+ const oauth = parsed.claudeAiOauth;
4030
+ if (typeof oauth !== "object" || oauth === null) return null;
4031
+ const record = oauth;
4032
+ return {
4033
+ accessToken: record.accessToken,
4034
+ refreshToken: record.refreshToken,
4035
+ expiresAt: record.expiresAt
4036
+ };
4037
+ } catch {
4038
+ return null;
4039
+ }
4040
+ }
4041
+ function buildSynthesizedCredentials(token, now) {
4042
+ return JSON.stringify({
4043
+ claudeAiOauth: {
4044
+ accessToken: token,
4045
+ expiresAt: now + SYNTH_TOKEN_TTL_MS,
4046
+ scopes: ["user:inference", "user:profile"],
4047
+ subscriptionType: "max"
4048
+ }
4049
+ });
4050
+ }
4051
+ function planCredentialsWrite(input) {
4052
+ if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
4053
+ if (!input.token) return { action: "skip", reason: "no-token" };
4054
+ const contents = buildSynthesizedCredentials(input.token, input.now);
4055
+ const existing = parseClaudeAiOauth(input.existingRaw);
4056
+ if (!existing) return { action: "write", contents };
4057
+ if (typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
4058
+ return { action: "skip", reason: "foreign-credentials" };
4059
+ }
4060
+ const fresh = existing.accessToken === input.token && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
4061
+ if (fresh) return { action: "skip", reason: "current" };
4062
+ return { action: "write", contents };
4063
+ }
4064
+ async function readRaw(path4) {
4065
+ try {
4066
+ return await readFile(path4, "utf8");
4067
+ } catch {
4068
+ return null;
4069
+ }
4070
+ }
4071
+ async function ensureClaudeCredentials(env = process.env) {
4072
+ try {
4073
+ const path4 = claudeCredentialsPath();
4074
+ const plan = planCredentialsWrite({
4075
+ isCloud: isConveyorCloudEnv(env),
4076
+ token: env.CLAUDE_CODE_OAUTH_TOKEN,
4077
+ existingRaw: await readRaw(path4),
4078
+ now: Date.now()
4079
+ });
4080
+ if (plan.action === "skip") return;
4081
+ await mkdir2(claudeConfigHome(), { recursive: true });
4082
+ await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
4083
+ await chmod2(path4, 384).catch(() => {
4084
+ });
4085
+ } catch (err) {
4086
+ const message = err instanceof Error ? err.message : String(err);
4087
+ process.stderr.write(`[conveyor-agent] claude credentials sync failed: ${message}
4088
+ `);
4089
+ }
4090
+ }
4091
+ function claudeJsonPath() {
4092
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
4093
+ return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
4094
+ }
4095
+ function asRecord(value) {
4096
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
4097
+ }
4098
+ function parseClaudeJson(existingRaw) {
4099
+ if (!existingRaw || existingRaw.trim() === "") return {};
4100
+ try {
4101
+ return asRecord(JSON.parse(existingRaw));
4102
+ } catch {
4103
+ return {};
4104
+ }
4105
+ }
4106
+ function seedWorkspaceTrust(config, trustCwd) {
4107
+ const projects = asRecord(config.projects);
4108
+ const entry = asRecord(projects[trustCwd]);
4109
+ if (entry.hasTrustDialogAccepted === true) return false;
4110
+ entry.hasTrustDialogAccepted = true;
4111
+ projects[trustCwd] = entry;
4112
+ config.projects = projects;
4113
+ return true;
4114
+ }
4115
+ function planClaudeJsonSeed(existingRaw, trustCwd) {
4116
+ const config = parseClaudeJson(existingRaw);
4117
+ const isFresh = Object.keys(config).length === 0;
4118
+ let changed = false;
4119
+ if (config.hasCompletedOnboarding !== true) {
4120
+ config.hasCompletedOnboarding = true;
4121
+ changed = true;
4122
+ }
4123
+ if (config.bypassPermissionsModeAccepted !== true) {
4124
+ config.bypassPermissionsModeAccepted = true;
4125
+ changed = true;
4126
+ }
4127
+ if (isFresh && typeof config.theme !== "string") {
4128
+ config.theme = "dark";
4129
+ changed = true;
4130
+ }
4131
+ if (trustCwd && seedWorkspaceTrust(config, trustCwd)) {
4132
+ changed = true;
4133
+ }
4134
+ return changed ? JSON.stringify(config) : null;
4135
+ }
4136
+ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4137
+ try {
4138
+ if (!isConveyorCloudEnv(env)) return;
4139
+ const path4 = claudeJsonPath();
4140
+ const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
4141
+ if (contents === null) return;
4142
+ await writeFile4(path4, contents, "utf8");
4143
+ const verify = await readRaw(path4);
4144
+ if (verify === contents) {
4145
+ process.stderr.write(
4146
+ `[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
4147
+ `
4148
+ );
4149
+ } else {
4150
+ process.stderr.write(
4151
+ `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4152
+ `
4153
+ );
4154
+ }
4155
+ } catch (err) {
4156
+ const message = err instanceof Error ? err.message : String(err);
4157
+ process.stderr.write(`[conveyor-agent] claude onboarding seed failed: ${message}
4158
+ `);
4159
+ }
4160
+ }
4161
+ async function removeConveyorCredentials(env = process.env) {
4162
+ try {
4163
+ if (!isConveyorCloudEnv(env)) return;
4164
+ const path4 = claudeCredentialsPath();
4165
+ const existing = parseClaudeAiOauth(await readRaw(path4));
4166
+ if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
4167
+ return;
4168
+ }
4169
+ await rm4(path4, { force: true });
4170
+ } catch (err) {
4171
+ const message = err instanceof Error ? err.message : String(err);
4172
+ process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
4173
+ `);
4174
+ }
3983
4175
  }
3984
4176
 
3985
4177
  // src/harness/pty/pty-support.ts
@@ -3996,11 +4188,14 @@ var PLAN_DIALOG_INTERVAL_MS = 1500;
3996
4188
  var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
3997
4189
  var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
3998
4190
  var PLAN_DIALOG_WINDOW_MS = 9e4;
4191
+ function envMs(name, fallback) {
4192
+ const raw = Number(process.env[name]);
4193
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
4194
+ }
4195
+ function resolveSubmitSettleMs() {
4196
+ return envMs("CONVEYOR_PTY_SUBMIT_SETTLE_MS", SUBMIT_SETTLE_MS);
4197
+ }
3999
4198
  function resolveSubmitNudgeTiming() {
4000
- const envMs = (name, fallback) => {
4001
- const raw = Number(process.env[name]);
4002
- return Number.isFinite(raw) && raw > 0 ? raw : fallback;
4003
- };
4004
4199
  return {
4005
4200
  intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
4006
4201
  slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
@@ -4008,6 +4203,15 @@ function resolveSubmitNudgeTiming() {
4008
4203
  windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
4009
4204
  };
4010
4205
  }
4206
+ function resolvePlanDialogTiming() {
4207
+ return {
4208
+ firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
4209
+ intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
4210
+ slowIntervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS", PLAN_DIALOG_SLOW_INTERVAL_MS),
4211
+ fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
4212
+ windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
4213
+ };
4214
+ }
4011
4215
  function turnOptionsFrom(options) {
4012
4216
  return {
4013
4217
  canUseTool: options.canUseTool,
@@ -4037,7 +4241,9 @@ function inheritedEnv(socketPath) {
4037
4241
  for (const [key, value] of Object.entries(process.env)) {
4038
4242
  if (typeof value === "string") env[key] = value;
4039
4243
  }
4040
- env.CONVEYOR_HOOK_SOCKET = socketPath;
4244
+ if (socketPath) {
4245
+ env.CONVEYOR_HOOK_SOCKET = socketPath;
4246
+ }
4041
4247
  env.MCP_TIMEOUT ??= "60000";
4042
4248
  env.MCP_TOOL_TIMEOUT ??= "180000";
4043
4249
  return env;
@@ -4087,18 +4293,65 @@ function parseUserQuestions(input) {
4087
4293
  return questions;
4088
4294
  }
4089
4295
 
4296
+ // src/harness/pty/adapters/claude.ts
4297
+ var ClaudeTuiAdapter = class {
4298
+ id = "claude-code";
4299
+ capabilities = {
4300
+ resume: true,
4301
+ structuredEvents: true,
4302
+ prefill: true,
4303
+ passiveTurns: true
4304
+ };
4305
+ resolveBinary(env = process.env) {
4306
+ return env.CONVEYOR_CLAUDE_BIN ?? "claude";
4307
+ }
4308
+ buildSpawn(input) {
4309
+ const { options, resume } = input;
4310
+ const args = buildSpawnArgs({
4311
+ ...resume ? { resume } : {},
4312
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
4313
+ model: options.model,
4314
+ permissionMode: options.permissionMode,
4315
+ settingsPath: input.settingsPath ?? "",
4316
+ ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
4317
+ ...input.mcpConfigPath ? { mcpConfigPath: input.mcpConfigPath, strictMcpConfig: true } : {}
4318
+ });
4319
+ return {
4320
+ file: this.resolveBinary(),
4321
+ args,
4322
+ env: inheritedEnv(input.hookSocketPath)
4323
+ };
4324
+ }
4325
+ async prepareEnvironment(opts) {
4326
+ const env = opts.env ?? process.env;
4327
+ await ensureClaudeCredentials(env);
4328
+ await ensureClaudeOnboarding(env, opts.cwd);
4329
+ }
4330
+ spawnFingerprint(input) {
4331
+ return spawnOptionsFingerprint(input);
4332
+ }
4333
+ encodePromptBytes(text) {
4334
+ return buildPromptBytes(text);
4335
+ }
4336
+ buildExitErrors(exitCode, rawOutput) {
4337
+ return buildExitErrors(exitCode, rawOutput, this.resolveBinary());
4338
+ }
4339
+ };
4340
+
4090
4341
  // src/harness/pty/session.ts
4091
4342
  var PtySession = class {
4092
- constructor(prompt, options, resume, bridge) {
4343
+ constructor(prompt, options, resume, bridge, adapter = new ClaudeTuiAdapter()) {
4093
4344
  this.options = options;
4094
4345
  this.resume = resume;
4095
4346
  this.bridge = bridge;
4347
+ this.adapter = adapter;
4096
4348
  this.turnPrompt = prompt;
4097
4349
  this.turn = turnOptionsFrom(options);
4098
4350
  }
4099
4351
  options;
4100
4352
  resume;
4101
4353
  bridge;
4354
+ adapter;
4102
4355
  // The current turn's event stream. Null between turns (process parked) — the
4103
4356
  // process outlives any single turn, so the queue is per-turn, not per-process.
4104
4357
  activeQueue = null;
@@ -4184,7 +4437,7 @@ var PtySession = class {
4184
4437
  }
4185
4438
  /** Fingerprint of the spawn-time options a reused process cannot change. */
4186
4439
  get spawnFingerprint() {
4187
- return spawnOptionsFingerprint({
4440
+ return this.adapter.spawnFingerprint({
4188
4441
  model: this.options.model,
4189
4442
  permissionMode: this.options.permissionMode,
4190
4443
  ...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
@@ -4321,29 +4574,19 @@ var PtySession = class {
4321
4574
  return;
4322
4575
  }
4323
4576
  try {
4324
- this.tempDir = await mkdtemp2(join3(tmpdir3(), "conveyor-pty-"));
4325
- const socketPath = join3(this.tempDir, "hook.sock");
4326
- this.socket = new HookSocketServer(
4327
- socketPath,
4328
- (progress) => this.handleProgress(progress),
4329
- (request) => this.handlePreToolUse(request)
4330
- );
4331
- await this.socket.listen();
4332
- const { settingsPath } = await writeHookSettings(this.tempDir);
4333
- await this.setupToolServers();
4334
- const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
4335
- await mkdir2(dirname(transcriptPath), { recursive: true });
4336
- const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
4337
- const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
4338
- this.tailer = new JsonlTailer(
4339
- transcriptPath,
4340
- (event) => this.handleTranscriptEvent(event),
4341
- sendChat ? (raw) => {
4342
- for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
4343
- } : void 0
4344
- );
4345
- this.tailer.start(startOffset);
4346
- await this.spawn(settingsPath, socketPath);
4577
+ if (this.adapter.capabilities.structuredEvents) {
4578
+ const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
4579
+ await this.spawn(settingsPath, socketPath);
4580
+ } else {
4581
+ this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
4582
+ await this.spawn();
4583
+ this.pushEvent({
4584
+ type: "system",
4585
+ subtype: "init",
4586
+ session_id: sessionId,
4587
+ model: this.options.model
4588
+ });
4589
+ }
4347
4590
  if (signal) {
4348
4591
  this.abortHandler = () => {
4349
4592
  void this.teardown();
@@ -4364,6 +4607,38 @@ var PtySession = class {
4364
4607
  throw err;
4365
4608
  }
4366
4609
  }
4610
+ /**
4611
+ * Allocate the Claude-style structured-event sources: the PostToolUse hook
4612
+ * socket, the per-run settings file, the in-process tool servers, and the
4613
+ * transcript tailer. Only structured-events adapters call this — raw-terminal
4614
+ * adapters have no trusted event source and skip it entirely. Returns the
4615
+ * paths spawn() must wire into the child's argv/env.
4616
+ */
4617
+ async startStructuredEventSources(sessionId) {
4618
+ this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
4619
+ const socketPath = join4(this.tempDir, "hook.sock");
4620
+ this.socket = new HookSocketServer(
4621
+ socketPath,
4622
+ (progress) => this.handleProgress(progress),
4623
+ (request) => this.handlePreToolUse(request)
4624
+ );
4625
+ await this.socket.listen();
4626
+ const { settingsPath } = await writeHookSettings(this.tempDir);
4627
+ await this.setupToolServers();
4628
+ const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
4629
+ await mkdir3(dirname(transcriptPath), { recursive: true });
4630
+ const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
4631
+ const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
4632
+ this.tailer = new JsonlTailer(
4633
+ transcriptPath,
4634
+ (event) => this.handleTranscriptEvent(event),
4635
+ sendChat ? (raw) => {
4636
+ for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
4637
+ } : void 0
4638
+ );
4639
+ this.tailer.start(startOffset);
4640
+ return { settingsPath, socketPath };
4641
+ }
4367
4642
  writeStdin(text) {
4368
4643
  this.pty?.write(text);
4369
4644
  }
@@ -4429,7 +4704,7 @@ var PtySession = class {
4429
4704
  this.activeQueue?.close();
4430
4705
  this.activeQueue = null;
4431
4706
  if (this.tempDir) {
4432
- await rm4(this.tempDir, { recursive: true, force: true });
4707
+ await rm5(this.tempDir, { recursive: true, force: true });
4433
4708
  this.tempDir = "";
4434
4709
  }
4435
4710
  }
@@ -4448,25 +4723,23 @@ var PtySession = class {
4448
4723
  this.mcpConfigPath = mcpConfigPath;
4449
4724
  }
4450
4725
  async spawn(settingsPath, socketPath) {
4451
- const args = buildSpawnArgs({
4452
- resume: this.resume,
4453
- sessionId: this.options.sessionId,
4454
- model: this.options.model,
4455
- permissionMode: this.options.permissionMode,
4456
- settingsPath,
4457
- ...this.options.appendSystemPrompt ? { appendSystemPrompt: this.options.appendSystemPrompt } : {},
4726
+ const spec = this.adapter.buildSpawn({
4727
+ options: this.options,
4728
+ ...this.resume ? { resume: this.resume } : {},
4729
+ ...settingsPath ? { settingsPath } : {},
4730
+ ...socketPath ? { hookSocketPath: socketPath } : {},
4458
4731
  // Only this config: ignore the user's ~/.claude.json / project .mcp.json
4459
4732
  // so the agent's tool set is deterministic (and a stale personal conveyor
4460
4733
  // server doesn't load).
4461
- ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath, strictMcpConfig: true } : {}
4734
+ ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
4462
4735
  });
4463
4736
  const spawn2 = await loadPtySpawn();
4464
- const pty = spawn2(resolveClaudeBinary(), args, {
4737
+ const pty = spawn2(spec.file, spec.args, {
4465
4738
  name: "xterm-color",
4466
4739
  cols: this.cols,
4467
4740
  rows: this.rows,
4468
4741
  cwd: this.options.cwd,
4469
- env: inheritedEnv(socketPath)
4742
+ env: spec.env
4470
4743
  });
4471
4744
  const bridge = this.bridge;
4472
4745
  if (bridge) {
@@ -4487,12 +4760,13 @@ var PtySession = class {
4487
4760
  this.pty = pty;
4488
4761
  }
4489
4762
  async deliverPrompt(text) {
4490
- this.writeStdin(buildPromptBytes(text));
4763
+ if (text === "" && !this.adapter.capabilities.prefill) return;
4764
+ this.writeStdin(this.adapter.encodePromptBytes(text));
4491
4765
  if (this.turn.promptDelivery === "prefill") return;
4492
- await sleep3(SUBMIT_SETTLE_MS);
4766
+ await sleep3(resolveSubmitSettleMs());
4493
4767
  if (this._toreDown) return;
4494
4768
  this.writeStdin("\r");
4495
- this.armSubmitNudge();
4769
+ if (this.adapter.capabilities.structuredEvents) this.armSubmitNudge();
4496
4770
  }
4497
4771
  async feedPrompt() {
4498
4772
  if (typeof this.turnPrompt === "string") {
@@ -4606,10 +4880,11 @@ var PtySession = class {
4606
4880
  if (this.pendingPlanApproval) return;
4607
4881
  this.pendingPlanApproval = true;
4608
4882
  const startedAt = Date.now();
4883
+ const { firstPressMs, intervalMs, slowIntervalMs, fastWindowMs, windowMs } = resolvePlanDialogTiming();
4609
4884
  const press = () => {
4610
4885
  if (this._toreDown || !this.pendingPlanApproval) return;
4611
4886
  const elapsed = Date.now() - startedAt;
4612
- if (elapsed >= PLAN_DIALOG_WINDOW_MS) {
4887
+ if (elapsed >= windowMs) {
4613
4888
  process.stderr.write(
4614
4889
  "[PtySession] plan-dialog auto-accept window expired without ExitPlanMode executing \u2014 the approval dialog may still be parked\n"
4615
4890
  );
@@ -4617,10 +4892,10 @@ var PtySession = class {
4617
4892
  return;
4618
4893
  }
4619
4894
  this.writeStdin("\r");
4620
- const interval = elapsed < PLAN_DIALOG_FAST_WINDOW_MS ? PLAN_DIALOG_INTERVAL_MS : PLAN_DIALOG_SLOW_INTERVAL_MS;
4895
+ const interval = elapsed < fastWindowMs ? intervalMs : slowIntervalMs;
4621
4896
  this.planApprovalTimer = setTimeout(press, interval);
4622
4897
  };
4623
- this.planApprovalTimer = setTimeout(press, PLAN_DIALOG_FIRST_PRESS_MS);
4898
+ this.planApprovalTimer = setTimeout(press, firstPressMs);
4624
4899
  }
4625
4900
  disarmPlanDialogAutoAccept() {
4626
4901
  this.pendingPlanApproval = false;
@@ -4649,11 +4924,22 @@ var PtySession = class {
4649
4924
  this.tailer.close();
4650
4925
  await this.tailer.flush();
4651
4926
  }
4652
- if (!this.sawResult && this.activeQueue) {
4927
+ if (!this.adapter.capabilities.structuredEvents) {
4928
+ if (exitCode === 0) {
4929
+ this.pushEvent({ type: "result", subtype: "success", result: "", total_cost_usd: 0 });
4930
+ } else {
4931
+ this.pushEvent({
4932
+ type: "result",
4933
+ subtype: "error",
4934
+ errors: this.adapter.buildExitErrors(exitCode, this.recentOutput)
4935
+ });
4936
+ }
4937
+ this.endTurn(exitCode === 0);
4938
+ } else if (!this.sawResult && this.activeQueue) {
4653
4939
  this.activeQueue.push({
4654
4940
  type: "result",
4655
4941
  subtype: "error",
4656
- errors: buildExitErrors(exitCode, this.recentOutput, resolveClaudeBinary())
4942
+ errors: this.adapter.buildExitErrors(exitCode, this.recentOutput)
4657
4943
  });
4658
4944
  }
4659
4945
  for (const listener of this.exitListeners) listener(exitCode);
@@ -4662,191 +4948,29 @@ var PtySession = class {
4662
4948
  }
4663
4949
  };
4664
4950
 
4665
- // src/harness/pty/credentials.ts
4666
- import { chmod as chmod2, mkdir as mkdir3, readFile, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4667
- import { homedir as homedir2 } from "os";
4668
- import { join as join4 } from "path";
4669
- var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
4670
- var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
4671
- function claudeCredentialsPath() {
4672
- return join4(claudeConfigHome(), ".credentials.json");
4673
- }
4674
- function isConveyorCloudEnv(env = process.env) {
4675
- return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
4676
- }
4677
- function parseClaudeAiOauth(raw) {
4678
- if (!raw || raw.trim() === "") return null;
4679
- try {
4680
- const parsed = JSON.parse(raw);
4681
- if (typeof parsed !== "object" || parsed === null) return null;
4682
- const oauth = parsed.claudeAiOauth;
4683
- if (typeof oauth !== "object" || oauth === null) return null;
4684
- const record = oauth;
4685
- return {
4686
- accessToken: record.accessToken,
4687
- refreshToken: record.refreshToken,
4688
- expiresAt: record.expiresAt
4689
- };
4690
- } catch {
4691
- return null;
4692
- }
4693
- }
4694
- function buildSynthesizedCredentials(token, now) {
4695
- return JSON.stringify({
4696
- claudeAiOauth: {
4697
- accessToken: token,
4698
- expiresAt: now + SYNTH_TOKEN_TTL_MS,
4699
- scopes: ["user:inference", "user:profile"],
4700
- subscriptionType: "max"
4701
- }
4702
- });
4703
- }
4704
- function planCredentialsWrite(input) {
4705
- if (!input.isCloud) return { action: "skip", reason: "not-cloud" };
4706
- if (!input.token) return { action: "skip", reason: "no-token" };
4707
- const contents = buildSynthesizedCredentials(input.token, input.now);
4708
- const existing = parseClaudeAiOauth(input.existingRaw);
4709
- if (!existing) return { action: "write", contents };
4710
- if (typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
4711
- return { action: "skip", reason: "foreign-credentials" };
4712
- }
4713
- const fresh = existing.accessToken === input.token && typeof existing.expiresAt === "number" && existing.expiresAt > input.now + REFRESH_SKEW_MS;
4714
- if (fresh) return { action: "skip", reason: "current" };
4715
- return { action: "write", contents };
4716
- }
4717
- async function readRaw(path4) {
4718
- try {
4719
- return await readFile(path4, "utf8");
4720
- } catch {
4721
- return null;
4722
- }
4723
- }
4724
- async function ensureClaudeCredentials(env = process.env) {
4725
- try {
4726
- const path4 = claudeCredentialsPath();
4727
- const plan = planCredentialsWrite({
4728
- isCloud: isConveyorCloudEnv(env),
4729
- token: env.CLAUDE_CODE_OAUTH_TOKEN,
4730
- existingRaw: await readRaw(path4),
4731
- now: Date.now()
4732
- });
4733
- if (plan.action === "skip") return;
4734
- await mkdir3(claudeConfigHome(), { recursive: true });
4735
- await writeFile4(path4, plan.contents, { encoding: "utf8", mode: 384 });
4736
- await chmod2(path4, 384).catch(() => {
4737
- });
4738
- } catch (err) {
4739
- const message = err instanceof Error ? err.message : String(err);
4740
- process.stderr.write(`[conveyor-agent] claude credentials sync failed: ${message}
4741
- `);
4742
- }
4743
- }
4744
- function claudeJsonPath() {
4745
- const configDir = process.env.CLAUDE_CONFIG_DIR;
4746
- return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
4747
- }
4748
- function asRecord(value) {
4749
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
4750
- }
4751
- function parseClaudeJson(existingRaw) {
4752
- if (!existingRaw || existingRaw.trim() === "") return {};
4753
- try {
4754
- return asRecord(JSON.parse(existingRaw));
4755
- } catch {
4756
- return {};
4757
- }
4758
- }
4759
- function seedWorkspaceTrust(config, trustCwd) {
4760
- const projects = asRecord(config.projects);
4761
- const entry = asRecord(projects[trustCwd]);
4762
- if (entry.hasTrustDialogAccepted === true) return false;
4763
- entry.hasTrustDialogAccepted = true;
4764
- projects[trustCwd] = entry;
4765
- config.projects = projects;
4766
- return true;
4767
- }
4768
- function planClaudeJsonSeed(existingRaw, trustCwd) {
4769
- const config = parseClaudeJson(existingRaw);
4770
- const isFresh = Object.keys(config).length === 0;
4771
- let changed = false;
4772
- if (config.hasCompletedOnboarding !== true) {
4773
- config.hasCompletedOnboarding = true;
4774
- changed = true;
4775
- }
4776
- if (config.bypassPermissionsModeAccepted !== true) {
4777
- config.bypassPermissionsModeAccepted = true;
4778
- changed = true;
4779
- }
4780
- if (isFresh && typeof config.theme !== "string") {
4781
- config.theme = "dark";
4782
- changed = true;
4783
- }
4784
- if (trustCwd && seedWorkspaceTrust(config, trustCwd)) {
4785
- changed = true;
4786
- }
4787
- return changed ? JSON.stringify(config) : null;
4788
- }
4789
- async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4790
- try {
4791
- if (!isConveyorCloudEnv(env)) return;
4792
- const path4 = claudeJsonPath();
4793
- const contents = planClaudeJsonSeed(await readRaw(path4), trustCwd);
4794
- if (contents === null) return;
4795
- await writeFile4(path4, contents, "utf8");
4796
- const verify = await readRaw(path4);
4797
- if (verify === contents) {
4798
- process.stderr.write(
4799
- `[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
4800
- `
4801
- );
4802
- } else {
4803
- process.stderr.write(
4804
- `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4}: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4805
- `
4806
- );
4807
- }
4808
- } catch (err) {
4809
- const message = err instanceof Error ? err.message : String(err);
4810
- process.stderr.write(`[conveyor-agent] claude onboarding seed failed: ${message}
4811
- `);
4812
- }
4813
- }
4814
- async function removeConveyorCredentials(env = process.env) {
4815
- try {
4816
- if (!isConveyorCloudEnv(env)) return;
4817
- const path4 = claudeCredentialsPath();
4818
- const existing = parseClaudeAiOauth(await readRaw(path4));
4819
- if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
4820
- return;
4821
- }
4822
- await rm5(path4, { force: true });
4823
- } catch (err) {
4824
- const message = err instanceof Error ? err.message : String(err);
4825
- process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
4826
- `);
4827
- }
4828
- }
4829
-
4830
4951
  // src/harness/pty/index.ts
4831
4952
  var ENDED_GRACE_MS = 4e3;
4832
- function fingerprintOf(options) {
4833
- return spawnOptionsFingerprint({
4834
- model: options.model,
4835
- permissionMode: options.permissionMode,
4836
- ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
4837
- cwd: options.cwd
4838
- });
4839
- }
4840
4953
  var PtyHarness = class {
4841
4954
  /**
4842
4955
  * `bridge` relays raw terminal I/O to/from the S2 server (and on to the S5
4843
4956
  * terminal). It is undefined for SDK-only callers and PTY runs that never
4844
4957
  * attach a relay; the session simply discards stdout in that case.
4845
4958
  */
4846
- constructor(bridge) {
4959
+ constructor(bridge, adapter = new ClaudeTuiAdapter()) {
4847
4960
  this.bridge = bridge;
4961
+ this.adapter = adapter;
4848
4962
  }
4849
4963
  bridge;
4964
+ adapter;
4965
+ /** Fingerprint of the spawn-time options a reused process cannot change. */
4966
+ fingerprintOf(options) {
4967
+ return this.adapter.spawnFingerprint({
4968
+ model: options.model,
4969
+ permissionMode: options.permissionMode,
4970
+ ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
4971
+ cwd: options.cwd
4972
+ });
4973
+ }
4850
4974
  /** The session currently streaming events, if any — repaint target. */
4851
4975
  activeSession = null;
4852
4976
  /** A completed-but-alive session kept for the next turn (keep-alive). */
@@ -4865,7 +4989,7 @@ var PtyHarness = class {
4865
4989
  }
4866
4990
  async *executeQuery(opts) {
4867
4991
  const want = opts.resume ?? opts.options.resume;
4868
- const fingerprint = fingerprintOf(opts.options);
4992
+ const fingerprint = this.fingerprintOf(opts.options);
4869
4993
  let session;
4870
4994
  if (this.parked?.canReuse(want, fingerprint)) {
4871
4995
  session = this.parked;
@@ -4879,9 +5003,8 @@ var PtyHarness = class {
4879
5003
  this.cancelEndedTimer();
4880
5004
  await stale.teardown();
4881
5005
  }
4882
- session = new PtySession(opts.prompt, opts.options, want, this.bridge);
4883
- await ensureClaudeCredentials();
4884
- await ensureClaudeOnboarding(process.env, opts.options.cwd);
5006
+ session = new PtySession(opts.prompt, opts.options, want, this.bridge, this.adapter);
5007
+ await this.adapter.prepareEnvironment({ cwd: opts.options.cwd });
4885
5008
  session.onExit(() => this.handleSessionExit(session));
4886
5009
  await session.start();
4887
5010
  }
@@ -5055,7 +5178,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
5055
5178
  ` - "ReviewDev" / "Complete": Already done. Skip.`,
5056
5179
  ` - "Planning": Not ready. If blocking progress, notify team.`,
5057
5180
  ``,
5058
- `3. Fire ALL ready "Open" tasks whose dependencies are met, not just one. Independent tasks can run in parallel.`,
5181
+ `3. Fire ALL ready "Open" tasks whose dependencies are met, not just one \u2014 independent tasks run in parallel. There is a concurrency limit: if start_child_cloud_build returns a PACK_CHILD_LIMIT error, that is backpressure, not a failure. Merge or wait on in-flight children, then start more as slots free up.`,
5059
5182
  ``,
5060
5183
  `4. After merging a PR: run \`git pull origin ${context.baseBranch}\` then re-check list_subtasks \u2014 previously blocked tasks may now be ready.`,
5061
5184
  ``,
@@ -5064,9 +5187,9 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
5064
5187
  `6. When ALL children are in "ReviewDev" or "Complete" (no "Open", "InProgress", or "ReviewPR" remaining): do a final review, summarize results in chat, and mark this parent task complete with force_update_task_status("Complete").`,
5065
5188
  ``,
5066
5189
  `## Important Rules`,
5067
- `- When dependencies are set on children, use them to determine execution order. Fire all ready tasks in parallel.`,
5190
+ `- When dependencies are set on children, use them to determine execution order. Fire all ready tasks in parallel (up to the PACK_CHILD_LIMIT backpressure \u2014 see the loop above).`,
5068
5191
  `- When NO dependencies are set on any children, fall back to ordinal order (one at a time). This preserves backward compatibility.`,
5069
- `- After firing builds OR when waiting on CI, explicitly state you are going idle. The system will disconnect you and relaunch when there's a status change.`,
5192
+ `- After firing builds OR when waiting on CI, explicitly state you are going idle. Go idle when waiting \u2014 the system wakes you (or relaunches this environment) when a child changes status.`,
5070
5193
  `- Do NOT attempt to write code yourself. Your role is coordination only.`,
5071
5194
  `- If a child is stuck in "InProgress" for an unusually long time, use get_execution_logs(childTaskId) to check its logs and escalate to the team if it appears stuck.`,
5072
5195
  `- You can use get_task(childTaskId) to get a child's full details including PR URL and branch.`,
@@ -5978,7 +6101,7 @@ Git safety \u2014 STRICT rules:`,
5978
6101
  function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
5979
6102
  const isPm = mode === "pm";
5980
6103
  const isPmActive = isPm && agentMode === "building";
5981
- const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
6104
+ const isPackRunner = mode === "pack" || isPm && !!config.isAuto && !!context.isParentTask;
5982
6105
  if (isPackRunner) {
5983
6106
  return buildPackRunnerSystemPrompt(context, config, setupLog);
5984
6107
  }
@@ -6307,7 +6430,7 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
6307
6430
  return parts;
6308
6431
  }
6309
6432
  function buildIdleRelaunchInstructions(context, isPm, agentMode, isAuto) {
6310
- if (isPm && agentMode === "auto" && context.status === "Planning") {
6433
+ if (isPm && agentMode === "auto" && PRE_BUILD_TASK_STATUSES.has(context.status ?? "")) {
6311
6434
  if (context.plan?.trim()) {
6312
6435
  return [
6313
6436
  `You were relaunched in auto mode. A plan already exists for this task.`,
@@ -6369,7 +6492,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
6369
6492
  return parts;
6370
6493
  }
6371
6494
  async function buildInitialPrompt(mode, context, isAuto, agentMode) {
6372
- const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
6495
+ const isPackRunner = mode === "pack" || mode === "pm" && !!isAuto && !!context.isParentTask;
6373
6496
  if (!isPackRunner) {
6374
6497
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
6375
6498
  if (sessionRelaunch) return sessionRelaunch;
@@ -7476,6 +7599,12 @@ function buildDiscoveryTools(connection) {
7476
7599
 
7477
7600
  // src/tools/code-review-tools.ts
7478
7601
  import { z as z10 } from "zod";
7602
+ async function endReviewSession(connection, reason) {
7603
+ await connection.call("endReviewSession", {
7604
+ sessionId: connection.sessionId,
7605
+ reason
7606
+ });
7607
+ }
7479
7608
  function buildCodeReviewTools(connection) {
7480
7609
  return [
7481
7610
  defineTool(
@@ -7498,6 +7627,7 @@ ${summary}`;
7498
7627
  result: "approved",
7499
7628
  summary
7500
7629
  });
7630
+ await endReviewSession(connection, "approved");
7501
7631
  return textResult("Code review approved. Exiting.");
7502
7632
  }
7503
7633
  ),
@@ -7536,6 +7666,7 @@ ${issueLines}`;
7536
7666
  summary,
7537
7667
  issues
7538
7668
  });
7669
+ await endReviewSession(connection, "changes_requested");
7539
7670
  return textResult("Code review complete \u2014 changes requested. Exiting.");
7540
7671
  }
7541
7672
  )
@@ -7550,6 +7681,9 @@ function getTaskModeTools(agentMode, connection) {
7550
7681
  return [];
7551
7682
  }
7552
7683
  function getModeTools(agentMode, connection, config, context) {
7684
+ if (config.mode === "pack") {
7685
+ return buildPmTools(connection, { includePackTools: true });
7686
+ }
7553
7687
  if (config.mode === "task") return getTaskModeTools(agentMode, connection);
7554
7688
  switch (agentMode) {
7555
7689
  case "building":
@@ -7581,6 +7715,49 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
7581
7715
  });
7582
7716
  }
7583
7717
 
7718
+ // src/harness/pty/adapters/types.ts
7719
+ import { accessSync, constants, statSync } from "fs";
7720
+ import { join as join6 } from "path";
7721
+ var TuiUnavailableError = class extends Error {
7722
+ constructor(tui, message) {
7723
+ super(message);
7724
+ this.tui = tui;
7725
+ this.name = "TuiUnavailableError";
7726
+ }
7727
+ tui;
7728
+ };
7729
+ function isExecutable(path4) {
7730
+ try {
7731
+ if (!statSync(path4).isFile()) return false;
7732
+ accessSync(path4, constants.X_OK);
7733
+ return true;
7734
+ } catch {
7735
+ return false;
7736
+ }
7737
+ }
7738
+ function findOnPath(binary, env = process.env) {
7739
+ if (binary.includes("/")) {
7740
+ return isExecutable(binary) ? binary : null;
7741
+ }
7742
+ for (const dir of (env.PATH ?? "").split(":")) {
7743
+ if (!dir) continue;
7744
+ const candidate = join6(dir, binary);
7745
+ if (isExecutable(candidate)) return candidate;
7746
+ }
7747
+ return null;
7748
+ }
7749
+
7750
+ // src/execution/playwright-mcp.ts
7751
+ var PLAYWRIGHT_MCP_BINARIES = ["playwright-mcp", "mcp-server-playwright"];
7752
+ var PLAYWRIGHT_MCP_ARGS = ["--browser", "chromium", "--headless", "--no-sandbox", "--isolated"];
7753
+ function resolvePlaywrightMcpServer(env = process.env) {
7754
+ for (const binary of PLAYWRIGHT_MCP_BINARIES) {
7755
+ const command = findOnPath(binary, env);
7756
+ if (command) return { type: "stdio", command, args: [...PLAYWRIGHT_MCP_ARGS] };
7757
+ }
7758
+ return null;
7759
+ }
7760
+
7584
7761
  // src/execution/event-handlers.ts
7585
7762
  var logger = createServiceLogger("event-handlers");
7586
7763
  function safeVoid(promise, context) {
@@ -8474,7 +8651,13 @@ function buildQueryOptions(host, context) {
8474
8651
  planDialogAutoAccept: mode === "auto" && !host.hasExitedPlanMode,
8475
8652
  tools: { type: "preset", preset: "claude_code" },
8476
8653
  mcpServers: {
8477
- conveyor: createConveyorMcpServer(host.harness, host.connection, host.config, context, mode)
8654
+ conveyor: createConveyorMcpServer(host.harness, host.connection, host.config, context, mode),
8655
+ // Baked-in browser automation (claudespace pods) — omitted when the
8656
+ // binary isn't on PATH (dev machines, legacy images).
8657
+ ...(() => {
8658
+ const playwright = resolvePlaywrightMcpServer();
8659
+ return playwright ? { playwright } : {};
8660
+ })()
8478
8661
  },
8479
8662
  sandbox: context.useSandbox ? { enabled: true } : { enabled: false },
8480
8663
  hooks: buildHooks(host),
@@ -9251,7 +9434,7 @@ var QueryBridge = class {
9251
9434
 
9252
9435
  // src/runner/session-runner-helpers.ts
9253
9436
  import { readFileSync as readFileSync2 } from "fs";
9254
- import { dirname as dirname2, join as join6 } from "path";
9437
+ import { dirname as dirname2, join as join7 } from "path";
9255
9438
  import { fileURLToPath as fileURLToPath2 } from "url";
9256
9439
  function mapChatHistory(messages) {
9257
9440
  if (!messages) return [];
@@ -9280,7 +9463,7 @@ function readAgentVersion() {
9280
9463
  const here = dirname2(fileURLToPath2(import.meta.url));
9281
9464
  for (const rel of ["../package.json", "../../package.json"]) {
9282
9465
  try {
9283
- const pkg = JSON.parse(readFileSync2(join6(here, rel), "utf-8"));
9466
+ const pkg = JSON.parse(readFileSync2(join7(here, rel), "utf-8"));
9284
9467
  if (pkg.version) return pkg.version;
9285
9468
  } catch {
9286
9469
  }
@@ -9368,8 +9551,22 @@ async function sampleKeyUsage(token) {
9368
9551
  import { readFile as readFile4 } from "fs/promises";
9369
9552
  import { execFile as execFile3 } from "child_process";
9370
9553
  var PROC_TCP_LISTEN_STATE = "0A";
9554
+ function isLoopbackHexAddress(hex) {
9555
+ const addr = hex.toUpperCase();
9556
+ if (addr.length === 8) {
9557
+ return addr.slice(6, 8) === "7F";
9558
+ }
9559
+ if (addr.length === 32) {
9560
+ if (addr === "00000000000000000000000001000000") return true;
9561
+ if (addr.slice(0, 16) === "0000000000000000" && addr.slice(16, 24) === "FFFF0000") {
9562
+ return addr.slice(30, 32) === "7F";
9563
+ }
9564
+ return false;
9565
+ }
9566
+ return false;
9567
+ }
9371
9568
  function parseProcNetTcpListeners(content) {
9372
- const ports = [];
9569
+ const sockets = [];
9373
9570
  const lines = content.split("\n");
9374
9571
  for (let i = 1; i < lines.length; i++) {
9375
9572
  const line = lines[i];
@@ -9378,26 +9575,40 @@ function parseProcNetTcpListeners(content) {
9378
9575
  if (cols.length < 4 || cols[3] !== PROC_TCP_LISTEN_STATE) continue;
9379
9576
  const local = cols[1];
9380
9577
  if (!local) continue;
9381
- const portHex = local.split(":").pop();
9382
- if (!portHex) continue;
9578
+ const [addrHex, portHex] = local.split(":");
9579
+ if (!addrHex || !portHex) continue;
9383
9580
  const port = Number.parseInt(portHex, 16);
9384
- if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.push(port);
9581
+ if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
9582
+ sockets.push({ port, loopback: isLoopbackHexAddress(addrHex) });
9583
+ }
9584
+ return sockets;
9585
+ }
9586
+ function collectScan(sockets) {
9587
+ const ports = /* @__PURE__ */ new Set();
9588
+ const hasExternal = /* @__PURE__ */ new Set();
9589
+ for (const { port, loopback } of sockets) {
9590
+ ports.add(port);
9591
+ if (!loopback) hasExternal.add(port);
9385
9592
  }
9386
- return ports;
9593
+ const loopbackOnly = /* @__PURE__ */ new Set();
9594
+ for (const port of ports) {
9595
+ if (!hasExternal.has(port)) loopbackOnly.add(port);
9596
+ }
9597
+ return { ports, loopbackOnly };
9387
9598
  }
9388
9599
  var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
9389
9600
  async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
9390
- const ports = /* @__PURE__ */ new Set();
9601
+ const sockets = [];
9391
9602
  let readable = false;
9392
9603
  for (const path4 of procPaths) {
9393
9604
  try {
9394
9605
  const content = await readFile4(path4, "utf8");
9395
9606
  readable = true;
9396
- for (const port of parseProcNetTcpListeners(content)) ports.add(port);
9607
+ sockets.push(...parseProcNetTcpListeners(content));
9397
9608
  } catch {
9398
9609
  }
9399
9610
  }
9400
- return readable ? ports : null;
9611
+ return readable ? collectScan(sockets) : null;
9401
9612
  }
9402
9613
  async function readNetstatListeningPorts() {
9403
9614
  const output = await new Promise((resolve) => {
@@ -9406,17 +9617,21 @@ async function readNetstatListeningPorts() {
9406
9617
  });
9407
9618
  });
9408
9619
  if (output === null) return null;
9409
- const ports = /* @__PURE__ */ new Set();
9620
+ const sockets = [];
9410
9621
  for (const line of output.split("\n")) {
9411
9622
  if (!line.includes("LISTEN")) continue;
9412
9623
  const cols = line.trim().split(/\s+/);
9413
9624
  const local = cols[3];
9414
9625
  if (!local) continue;
9415
- const portStr = local.split(".").pop();
9416
- const port = Number(portStr);
9417
- if (Number.isInteger(port) && port >= 1 && port <= 65535) ports.add(port);
9626
+ const lastDot = local.lastIndexOf(".");
9627
+ if (lastDot < 0) continue;
9628
+ const host = local.slice(0, lastDot);
9629
+ const port = Number(local.slice(lastDot + 1));
9630
+ if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
9631
+ const loopback = host.startsWith("127.") || host === "::1" || host === "localhost";
9632
+ sockets.push({ port, loopback });
9418
9633
  }
9419
- return ports;
9634
+ return collectScan(sockets);
9420
9635
  }
9421
9636
  async function readListeningPorts() {
9422
9637
  const proc = await readProcListeningPorts();
@@ -9440,6 +9655,8 @@ var PortDiscovery = class {
9440
9655
  log;
9441
9656
  baseline = null;
9442
9657
  tracked = /* @__PURE__ */ new Map();
9658
+ /** Loopback-only candidates already warned about (once per port). */
9659
+ warnedLoopback = /* @__PURE__ */ new Set();
9443
9660
  timer = null;
9444
9661
  ticking = false;
9445
9662
  disabled = false;
@@ -9467,7 +9684,7 @@ var PortDiscovery = class {
9467
9684
  this.log("Port discovery disabled: no listening-socket source available");
9468
9685
  return;
9469
9686
  }
9470
- this.baseline = baseline;
9687
+ this.baseline = baseline.ports;
9471
9688
  this.timer = setInterval(() => void this.tick(), this.intervalMs);
9472
9689
  this.timer.unref?.();
9473
9690
  }
@@ -9504,8 +9721,21 @@ var PortDiscovery = class {
9504
9721
  return true;
9505
9722
  }
9506
9723
  updateTracking(current) {
9507
- for (const port of current) {
9724
+ const reachable = /* @__PURE__ */ new Set();
9725
+ for (const port of current.ports) {
9508
9726
  if (!this.isCandidate(port)) continue;
9727
+ if (current.loopbackOnly.has(port)) {
9728
+ if (!this.warnedLoopback.has(port)) {
9729
+ this.warnedLoopback.add(port);
9730
+ this.log(
9731
+ `Port ${port} is listening on loopback only and cannot be previewed \u2014 bind 0.0.0.0 (or the pod IP) to make it reachable through the preview proxy`
9732
+ );
9733
+ }
9734
+ continue;
9735
+ }
9736
+ reachable.add(port);
9737
+ }
9738
+ for (const port of reachable) {
9509
9739
  const entry = this.tracked.get(port);
9510
9740
  if (!entry) {
9511
9741
  this.tracked.set(port, { seen: 1, missed: 0, confirmed: false, detectedAt: "" });
@@ -9519,7 +9749,7 @@ var PortDiscovery = class {
9519
9749
  }
9520
9750
  }
9521
9751
  for (const [port, entry] of this.tracked) {
9522
- if (current.has(port)) continue;
9752
+ if (reachable.has(port)) continue;
9523
9753
  entry.missed += 1;
9524
9754
  entry.seen = 0;
9525
9755
  if (entry.missed >= CONFIRM_SCANS || !entry.confirmed) this.tracked.delete(port);
@@ -9821,7 +10051,7 @@ var SessionRunner = class _SessionRunner {
9821
10051
  }
9822
10052
  this.mode.applyServerMode(this.fullContext?.agentMode, this.fullContext?.isAuto);
9823
10053
  this.mode.resolveInitialMode(this.taskContext);
9824
- if (this.fullContext?.isAuto && this.taskContext.status === "Open" && this.mode.isBuildCapable) {
10054
+ if (this.fullContext?.isAuto && PRE_BUILD_TASK_STATUSES.has(this.taskContext.status) && this.mode.isBuildCapable) {
9825
10055
  void this.connection.triggerIdentification().catch(() => {
9826
10056
  });
9827
10057
  }
@@ -10292,11 +10522,12 @@ var SessionRunner = class _SessionRunner {
10292
10522
  * help) it's waiting on a human, not silently stuck.
10293
10523
  *
10294
10524
  * - "pr": In Progress with no open PR (build finished without shipping).
10295
- * - "planning": still Planning without (identified + saved plan). A fresh auto
10296
- * agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
10297
- * → clean-exit → its session Ends → the workspace is reaped "agent_gone"
10298
- * before a plan ever landed. Nudging (and, on exhaustion, going dormant)
10299
- * keeps the session Active so the pod is never prematurely slept.
10525
+ * - "planning": still in a pre-build status (Planning/Open) without
10526
+ * (identified + saved plan). A fresh auto agent that finishes its plan turn
10527
+ * WITHOUT ExitPlanMode would otherwise idle → clean-exit → its session Ends
10528
+ * → the workspace is reaped "agent_gone" before a plan ever landed. Nudging
10529
+ * (and, on exhaustion, going dormant) keeps the session Active so the pod
10530
+ * is never prematurely slept.
10300
10531
  */
10301
10532
  autoStuckKind() {
10302
10533
  if (!this.mode.isAuto || this.stopped) return null;
@@ -10306,7 +10537,7 @@ var SessionRunner = class _SessionRunner {
10306
10537
  if (!this.taskContext) return null;
10307
10538
  const { status, storyPointId, plan, githubPRUrl } = this.taskContext;
10308
10539
  if (status === "InProgress" && !githubPRUrl) return "pr";
10309
- if (status === "Planning" && !(storyPointId !== null && !!plan?.trim())) {
10540
+ if (PRE_BUILD_TASK_STATUSES.has(status) && !(storyPointId !== null && !!plan?.trim())) {
10310
10541
  return "planning";
10311
10542
  }
10312
10543
  return null;
@@ -10478,7 +10709,7 @@ var SessionRunner = class _SessionRunner {
10478
10709
  this.connection.onSoftStop(() => this.softStop());
10479
10710
  this.connection.onReconnected = () => this.queryBridge?.forceRepaint();
10480
10711
  this.connection.onModeChange((data) => {
10481
- const action = this.mode.handleModeChange(data.agentMode);
10712
+ const action = this.mode.handleModeChange(data.agentMode, this.taskContext);
10482
10713
  if (action.type === "start_auto") {
10483
10714
  this.connection.emitModeChanged(this.mode.effectiveMode);
10484
10715
  this.softStop();
@@ -10635,12 +10866,12 @@ var SessionRunner = class _SessionRunner {
10635
10866
 
10636
10867
  // src/setup/config.ts
10637
10868
  import { readFile as readFile5 } from "fs/promises";
10638
- import { join as join7 } from "path";
10869
+ import { join as join8 } from "path";
10639
10870
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
10640
10871
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
10641
10872
  async function loadForwardPorts(workspaceDir) {
10642
10873
  try {
10643
- const raw = await readFile5(join7(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10874
+ const raw = await readFile5(join8(workspaceDir, DEVCONTAINER_PATH), "utf-8");
10644
10875
  const parsed = JSON.parse(raw);
10645
10876
  const ports = (parsed.forwardPorts ?? []).filter(
10646
10877
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -10758,8 +10989,13 @@ export {
10758
10989
  applyBootstrapToEnv,
10759
10990
  AgentConnection,
10760
10991
  DEFAULT_SONNET_MODEL,
10992
+ TUI_KINDS,
10761
10993
  DEFAULT_LIFECYCLE_CONFIG,
10762
10994
  Lifecycle,
10995
+ cleanTerminalOutput,
10996
+ inheritedEnv,
10997
+ buildPromptBytes,
10998
+ ClaudeTuiAdapter,
10763
10999
  PtyHarness,
10764
11000
  createServiceLogger,
10765
11001
  hasUncommittedChanges,
@@ -10769,6 +11005,9 @@ export {
10769
11005
  updateRemoteToken,
10770
11006
  flushPendingChanges,
10771
11007
  pushToOrigin,
11008
+ TuiUnavailableError,
11009
+ findOnPath,
11010
+ resolvePlaywrightMcpServer,
10772
11011
  resolveSessionStart,
10773
11012
  sampleKeyUsage,
10774
11013
  awaitGitReady,
@@ -10787,4 +11026,4 @@ export {
10787
11026
  runStartCommand,
10788
11027
  unshallowRepo
10789
11028
  };
10790
- //# sourceMappingURL=chunk-DEMRCBJN.js.map
11029
+ //# sourceMappingURL=chunk-AMPYOJJC.js.map