@rallycry/conveyor-agent 10.13.9 → 10.13.11

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.
@@ -0,0 +1,451 @@
1
+ // src/harness/pty/pty-support.ts
2
+ import { stat } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+
5
+ // src/utils/sleep.ts
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => {
8
+ setTimeout(resolve, ms);
9
+ });
10
+ }
11
+
12
+ // src/harness/pty/pty-support.ts
13
+ var MAX_DIAGNOSTIC_OUTPUT = 4e3;
14
+ var MAX_BETWEEN_TURN_BUFFER = 500;
15
+ var SUBMIT_SETTLE_MS = 300;
16
+ var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
17
+ var SUBMIT_NUDGE_MAX_PRESSES = 5;
18
+ var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
19
+ var SUBMIT_NUDGE_WINDOW_MS = 9e4;
20
+ var PLAN_DIALOG_FIRST_PRESS_MS = 700;
21
+ var PLAN_DIALOG_INTERVAL_MS = 1500;
22
+ var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
23
+ var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
24
+ var PLAN_DIALOG_WINDOW_MS = 9e4;
25
+ function envMs(name, fallback) {
26
+ const raw = Number(process.env[name]);
27
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
28
+ }
29
+ function resolveSubmitSettleMs() {
30
+ return envMs("CONVEYOR_PTY_SUBMIT_SETTLE_MS", SUBMIT_SETTLE_MS);
31
+ }
32
+ function resolveSubmitNudgeTiming() {
33
+ return {
34
+ intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
35
+ slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
36
+ maxPresses: SUBMIT_NUDGE_MAX_PRESSES,
37
+ windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
38
+ };
39
+ }
40
+ function resolvePlanDialogTiming() {
41
+ return {
42
+ firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
43
+ intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
44
+ slowIntervalMs: envMs(
45
+ "CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS",
46
+ PLAN_DIALOG_SLOW_INTERVAL_MS
47
+ ),
48
+ fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
49
+ windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
50
+ };
51
+ }
52
+ function turnOptionsFrom(options) {
53
+ return {
54
+ canUseTool: options.canUseTool,
55
+ promptDelivery: options.promptDelivery,
56
+ planDialogAutoAccept: options.planDialogAutoAccept,
57
+ abortController: options.abortController
58
+ };
59
+ }
60
+ function isRecord(value) {
61
+ return typeof value === "object" && value !== null;
62
+ }
63
+ function extractSpawn(mod) {
64
+ if (!isRecord(mod)) return null;
65
+ if (typeof mod.spawn === "function") return mod.spawn;
66
+ const def = mod.default;
67
+ if (isRecord(def) && typeof def.spawn === "function") return def.spawn;
68
+ return null;
69
+ }
70
+ async function loadPtySpawn() {
71
+ const mod = await import("node-pty");
72
+ const spawn2 = extractSpawn(mod);
73
+ if (!spawn2) throw new Error("node-pty: spawn export not found");
74
+ return spawn2;
75
+ }
76
+ async function resolvePtySpawn() {
77
+ const { workbenchEnabled } = await import("./mode-6RL3SVMV.js");
78
+ if (!workbenchEnabled()) return loadPtySpawn();
79
+ const { getWorkbenchClient } = await import("./client-ICIWKSK2.js");
80
+ return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);
81
+ }
82
+ function sessionTempBase() {
83
+ return process.env.CONVEYOR_SHARED_DIR ?? tmpdir();
84
+ }
85
+ function inheritedEnv(socketPath) {
86
+ const env = {};
87
+ for (const [key, value] of Object.entries(process.env)) {
88
+ if (typeof value === "string") env[key] = value;
89
+ }
90
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
91
+ delete env.ANTHROPIC_API_KEY;
92
+ }
93
+ if (socketPath) {
94
+ env.CONVEYOR_HOOK_SOCKET = socketPath;
95
+ }
96
+ env.MCP_TIMEOUT ??= "60000";
97
+ env.MCP_TOOL_TIMEOUT ??= "180000";
98
+ return env;
99
+ }
100
+ function buildPromptBytes(text) {
101
+ return `\x1B[200~${text}\x1B[201~`;
102
+ }
103
+ function renderPromptContentText(content) {
104
+ return content.map((block) => {
105
+ const b = block;
106
+ if (b?.type === "text" && typeof b.text === "string") return b.text;
107
+ if (b?.type === "image") {
108
+ return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
109
+ }
110
+ return JSON.stringify(block);
111
+ }).join("\n\n");
112
+ }
113
+ async function transcriptSize(path2) {
114
+ try {
115
+ return (await stat(path2)).size;
116
+ } catch {
117
+ return 0;
118
+ }
119
+ }
120
+ function parseUserQuestions(input) {
121
+ if (!Array.isArray(input.questions)) return [];
122
+ const questions = [];
123
+ for (const entry of input.questions) {
124
+ if (!isRecord(entry)) continue;
125
+ if (typeof entry.question !== "string") continue;
126
+ const options = Array.isArray(entry.options) ? entry.options.filter(isRecord).filter((o) => typeof o.label === "string").map((o) => ({
127
+ label: o.label,
128
+ description: typeof o.description === "string" ? o.description : ""
129
+ })) : [];
130
+ questions.push({
131
+ question: entry.question,
132
+ header: typeof entry.header === "string" ? entry.header : "",
133
+ options,
134
+ ...typeof entry.multiSelect === "boolean" ? { multiSelect: entry.multiSelect } : {}
135
+ });
136
+ }
137
+ return questions;
138
+ }
139
+
140
+ // src/runner/work-preservation/snapshot-tar.ts
141
+ import { execFile } from "child_process";
142
+ import { createReadStream, createWriteStream } from "fs";
143
+ import { mkdtemp, rm, stat as stat2, writeFile } from "fs/promises";
144
+ import { tmpdir as tmpdir2 } from "os";
145
+ import path from "path";
146
+ import { pipeline } from "stream/promises";
147
+ import { promisify } from "util";
148
+ import { createGzip } from "zlib";
149
+ import * as tar from "tar";
150
+
151
+ // src/runner/work-preservation/snapshot-artifact.ts
152
+ function planSnapshotFiles(git) {
153
+ const include = git.trackedAndUntracked();
154
+ const includeSet = new Set(include);
155
+ const deletions = [...new Set(git.deletedSinceHead())].filter((path2) => !includeSet.has(path2));
156
+ return { include, deletions };
157
+ }
158
+
159
+ // src/runner/work-preservation/snapshot-tar.ts
160
+ var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
161
+ var LEGACY_PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
162
+ var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
163
+ function parseStatusPorcelainZ(raw) {
164
+ const includes = [];
165
+ const deletions = [];
166
+ const tokens = raw.split("\0");
167
+ for (let i = 0; i < tokens.length; i++) {
168
+ const token = tokens[i];
169
+ if (!token || token.length < 4) continue;
170
+ const x = token[0];
171
+ const y = token[1];
172
+ const filePath = token.slice(3);
173
+ if (x === "R" || x === "C") {
174
+ const origPath = tokens[++i];
175
+ if (x === "R" && origPath) deletions.push(origPath);
176
+ }
177
+ if (x === "!") continue;
178
+ const missingFromWorktree = y === "D" || x === "D" && y === " ";
179
+ if (missingFromWorktree) deletions.push(filePath);
180
+ else includes.push(filePath);
181
+ }
182
+ return { includes, deletions };
183
+ }
184
+ var execFileAsync = promisify(execFile);
185
+ var GIT_TIMEOUT_MS = 12e4;
186
+ async function realGitSurface(cwd) {
187
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "-z", "-uall"], {
188
+ cwd,
189
+ timeout: GIT_TIMEOUT_MS,
190
+ maxBuffer: STATUS_MAX_BUFFER
191
+ });
192
+ const cached = parseStatusPorcelainZ(stdout.toString());
193
+ return {
194
+ trackedAndUntracked: () => cached.includes,
195
+ deletedSinceHead: () => cached.deletions
196
+ };
197
+ }
198
+ async function gitHead(cwd) {
199
+ try {
200
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
201
+ cwd,
202
+ timeout: GIT_TIMEOUT_MS
203
+ });
204
+ return stdout.toString().trim() || null;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ async function buildSnapshotTar(cwd) {
210
+ const staging = await mkdtemp(path.join(tmpdir2(), "conveyor-snapshot-"));
211
+ const cleanup = async () => {
212
+ await rm(staging, { recursive: true, force: true }).catch(() => {
213
+ });
214
+ };
215
+ try {
216
+ const plan = planSnapshotFiles(await realGitSurface(cwd));
217
+ const head = await gitHead(cwd);
218
+ if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
219
+ const capturedAt = Date.now();
220
+ const manifest = {
221
+ head,
222
+ deletions: plan.deletions,
223
+ capturedAt
224
+ };
225
+ await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
226
+ const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
227
+ const rawTarPath = path.join(staging, "snapshot.tar");
228
+ await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
229
+ if (plan.include.length > 0) {
230
+ await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
231
+ }
232
+ const tarPath = path.join(staging, "snapshot.tar.gz");
233
+ await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
234
+ await rm(rawTarPath, { force: true });
235
+ const { size } = await stat2(tarPath);
236
+ return {
237
+ tarPath,
238
+ sizeBytes: size,
239
+ fileCount: plan.include.length,
240
+ deletionCount: plan.deletions.length,
241
+ capturedAt,
242
+ cleanup
243
+ };
244
+ } catch (err) {
245
+ await cleanup();
246
+ throw err;
247
+ }
248
+ }
249
+ function isWithinWorkspace(cwd, rel) {
250
+ if (!rel || path.isAbsolute(rel)) return false;
251
+ const root = path.resolve(cwd);
252
+ const abs = path.resolve(root, rel);
253
+ return abs !== root && abs.startsWith(root + path.sep);
254
+ }
255
+ async function readSnapshotArchive(tarPath) {
256
+ let manifestRaw = null;
257
+ try {
258
+ await tar.list({
259
+ file: tarPath,
260
+ onReadEntry: (entry) => {
261
+ if (entry.path === SNAPSHOT_MANIFEST_NAME) {
262
+ const chunks = [];
263
+ entry.on("data", (chunk) => chunks.push(chunk));
264
+ entry.on("end", () => {
265
+ manifestRaw = Buffer.concat(chunks);
266
+ });
267
+ }
268
+ }
269
+ });
270
+ } catch {
271
+ return null;
272
+ }
273
+ if (!manifestRaw) return null;
274
+ try {
275
+ const parsed = JSON.parse(manifestRaw.toString("utf8"));
276
+ if (typeof parsed.head !== "string" || !parsed.head) return null;
277
+ return {
278
+ head: parsed.head,
279
+ deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
280
+ capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0
281
+ };
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ async function extractSnapshotTar(cwd, tarPath) {
287
+ const manifest = await readSnapshotArchive(tarPath);
288
+ if (!manifest) return { status: "invalid" };
289
+ const currentHead = await gitHead(cwd);
290
+ if (manifest.head !== currentHead) {
291
+ return { status: "stale-head", snapshotHead: manifest.head, currentHead };
292
+ }
293
+ let filesExtracted = 0;
294
+ await tar.extract({
295
+ file: tarPath,
296
+ cwd,
297
+ filter: (entryPath) => {
298
+ if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {
299
+ return false;
300
+ }
301
+ filesExtracted++;
302
+ return true;
303
+ }
304
+ });
305
+ let deletionsApplied = 0;
306
+ for (const rel of manifest.deletions) {
307
+ if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
308
+ await rm(path.resolve(cwd, rel), { recursive: true, force: true });
309
+ deletionsApplied++;
310
+ }
311
+ return { status: "extracted", filesExtracted, deletionsApplied };
312
+ }
313
+
314
+ // src/setup/commands.ts
315
+ import { spawn, execSync } from "child_process";
316
+ var PROCESS_TERMINATION_GRACE_MS = 5e3;
317
+ function abortError() {
318
+ const error = new Error("Operation aborted");
319
+ error.name = "AbortError";
320
+ return error;
321
+ }
322
+ function signalProcessGroup(child, signal) {
323
+ try {
324
+ if (child.pid) process.kill(-child.pid, signal);
325
+ else child.kill(signal);
326
+ } catch {
327
+ try {
328
+ child.kill(signal);
329
+ } catch {
330
+ }
331
+ }
332
+ }
333
+ function terminateProcessGroup(child, graceMs = PROCESS_TERMINATION_GRACE_MS) {
334
+ if (child.exitCode !== null) return Promise.resolve();
335
+ return new Promise((resolve) => {
336
+ let settled = false;
337
+ const finish = () => {
338
+ if (settled) return;
339
+ settled = true;
340
+ clearTimeout(timer);
341
+ child.removeListener("exit", finish);
342
+ resolve();
343
+ };
344
+ const timer = setTimeout(() => {
345
+ signalProcessGroup(child, "SIGKILL");
346
+ finish();
347
+ }, graceMs);
348
+ timer.unref();
349
+ child.once("exit", finish);
350
+ signalProcessGroup(child, "SIGTERM");
351
+ });
352
+ }
353
+ function runSetupCommand(cmd, cwd, onOutput, signal) {
354
+ if (signal?.aborted) return Promise.reject(abortError());
355
+ return new Promise((resolve, reject) => {
356
+ const child = spawn("sh", ["-c", cmd], {
357
+ cwd,
358
+ stdio: ["ignore", "pipe", "pipe"],
359
+ detached: true,
360
+ env: { ...process.env }
361
+ });
362
+ let settled = false;
363
+ let aborting = false;
364
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
365
+ const settle = (error) => {
366
+ if (settled) return;
367
+ settled = true;
368
+ cleanup();
369
+ if (error) reject(error);
370
+ else resolve();
371
+ };
372
+ const onAbort = () => {
373
+ if (settled || aborting) return;
374
+ aborting = true;
375
+ void terminateProcessGroup(child).then(() => settle(abortError()));
376
+ };
377
+ signal?.addEventListener("abort", onAbort, { once: true });
378
+ if (signal?.aborted) onAbort();
379
+ child.stdout.on("data", (chunk) => {
380
+ if (aborting || signal?.aborted) return;
381
+ onOutput("stdout", chunk.toString());
382
+ });
383
+ child.stderr.on("data", (chunk) => {
384
+ if (aborting || signal?.aborted) return;
385
+ onOutput("stderr", chunk.toString());
386
+ });
387
+ child.on("close", (code) => {
388
+ if (aborting) return;
389
+ settle(code === 0 ? void 0 : new Error(`Setup command exited with code ${code}`));
390
+ });
391
+ child.on("error", (err) => {
392
+ if (!aborting) settle(err);
393
+ });
394
+ });
395
+ }
396
+ var AUTH_TOKEN_TIMEOUT_MS = 3e4;
397
+ function runAuthTokenCommand(cmd, userEmail, cwd) {
398
+ try {
399
+ const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {
400
+ cwd,
401
+ timeout: AUTH_TOKEN_TIMEOUT_MS,
402
+ stdio: ["ignore", "pipe", "ignore"],
403
+ env: { ...process.env }
404
+ });
405
+ const token = output.toString().trim();
406
+ return token || null;
407
+ } catch {
408
+ return null;
409
+ }
410
+ }
411
+ function runStartCommand(cmd, cwd, onOutput) {
412
+ const child = spawn("sh", ["-c", cmd], {
413
+ cwd,
414
+ stdio: ["ignore", "pipe", "pipe"],
415
+ detached: true,
416
+ env: { ...process.env }
417
+ });
418
+ child.stdout.on("data", (chunk) => {
419
+ onOutput("stdout", chunk.toString());
420
+ });
421
+ child.stderr.on("data", (chunk) => {
422
+ onOutput("stderr", chunk.toString());
423
+ });
424
+ child.unref();
425
+ return child;
426
+ }
427
+
428
+ export {
429
+ sleep,
430
+ MAX_DIAGNOSTIC_OUTPUT,
431
+ MAX_BETWEEN_TURN_BUFFER,
432
+ resolveSubmitSettleMs,
433
+ resolveSubmitNudgeTiming,
434
+ resolvePlanDialogTiming,
435
+ turnOptionsFrom,
436
+ loadPtySpawn,
437
+ resolvePtySpawn,
438
+ sessionTempBase,
439
+ inheritedEnv,
440
+ buildPromptBytes,
441
+ renderPromptContentText,
442
+ transcriptSize,
443
+ parseUserQuestions,
444
+ buildSnapshotTar,
445
+ extractSnapshotTar,
446
+ terminateProcessGroup,
447
+ runSetupCommand,
448
+ runAuthTokenCommand,
449
+ runStartCommand
450
+ };
451
+ //# sourceMappingURL=chunk-2K6RRTQC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/harness/pty/pty-support.ts","../src/utils/sleep.ts","../src/runner/work-preservation/snapshot-tar.ts","../src/runner/work-preservation/snapshot-artifact.ts","../src/setup/commands.ts"],"sourcesContent":["/**\n * Pure support helpers for PtySession — node-pty process loading, prompt-byte\n * encoding, the per-turn options projection, and the AskUserQuestion parser.\n * Extracted from `session.ts` so the orchestrator stays focused on lifecycle;\n * everything here is side-effect-free (or a thin wrapper over one) and unit\n * tested in isolation.\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport type { HarnessQueryOptions, HarnessUserQuestion } from \"../types.js\";\n\n// Cap on the rolling tail of raw terminal output retained for diagnostics when\n// `claude` exits without a result. Bounded so a long-running session can't grow\n// this unboundedly; only the most recent bytes (where a failure surfaces) matter.\nexport const MAX_DIAGNOSTIC_OUTPUT = 4000;\n\n// Cap on transcript events buffered while no turn is draining (the parked\n// window between a completed turn and the next beginTurn / a passive turn).\n// Bounded, drop-oldest — a stale buffered `result` must never terminate a\n// fresh turn, and beginTurn clears the buffer anyway.\nexport const MAX_BETWEEN_TURN_BUFFER = 500;\n\n/** Settle window between the paste write and the submitting Enter write. */\nexport const SUBMIT_SETTLE_MS = 300;\n// Re-press cadence/bounds for the submit nudge (see armSubmitNudge). Fast\n// presses cover the Enter-swallowed-at-startup race; the slow phase covers\n// startup dialogs that render tens of seconds in on a cold pod (folder\n// trust, onboarding) — observed live 2026-07-02 parking code reviews ~13min+\n// when the trust dialog mounted after the fast window had expired.\nexport const SUBMIT_NUDGE_INTERVAL_MS = 2000;\nexport const SUBMIT_NUDGE_MAX_PRESSES = 5;\nexport const SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5000;\nexport const SUBMIT_NUDGE_WINDOW_MS = 90_000;\n\n// Plan-dialog auto-accept press cadence/bounds (see armPlanDialogAutoAccept).\n// The dialog usually renders within a second of the ExitPlanMode hook verdict,\n// but the window is deliberately wide: the only stop signal is the ExitPlanMode\n// PostToolUse envelope, and a missed dialog parks an auto card indefinitely\n// (observed live 2026-07-07: an approval dialog sat 42 minutes until a human\n// pressed Enter).\nexport const PLAN_DIALOG_FIRST_PRESS_MS = 700;\nexport const PLAN_DIALOG_INTERVAL_MS = 1500;\nexport const PLAN_DIALOG_SLOW_INTERVAL_MS = 5000;\nexport const PLAN_DIALOG_FAST_WINDOW_MS = 10_000;\nexport const PLAN_DIALOG_WINDOW_MS = 90_000;\n\nfunction envMs(name: string, fallback: number): number {\n const raw = Number(process.env[name]);\n return Number.isFinite(raw) && raw > 0 ? raw : fallback;\n}\n\nexport function resolveSubmitSettleMs(): number {\n return envMs(\"CONVEYOR_PTY_SUBMIT_SETTLE_MS\", SUBMIT_SETTLE_MS);\n}\n\n/**\n * Effective submit-nudge timing. The exported constants above are the production\n * defaults; `CONVEYOR_PTY_NUDGE_INTERVAL_MS` / `_SLOW_INTERVAL_MS` / `_WINDOW_MS`\n * override them. Resolved at nudge-arm time (not import) so an integration test\n * can drive the two-phase fast→slow behavior in milliseconds instead of ~15s of\n * real wall-clock. `maxPresses` is deliberately NOT overridable — the fast→slow\n * phase-boundary count is the behavior under test.\n */\nexport function resolveSubmitNudgeTiming(): {\n intervalMs: number;\n slowIntervalMs: number;\n maxPresses: number;\n windowMs: number;\n} {\n return {\n intervalMs: envMs(\"CONVEYOR_PTY_NUDGE_INTERVAL_MS\", SUBMIT_NUDGE_INTERVAL_MS),\n slowIntervalMs: envMs(\"CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS\", SUBMIT_NUDGE_SLOW_INTERVAL_MS),\n maxPresses: SUBMIT_NUDGE_MAX_PRESSES,\n windowMs: envMs(\"CONVEYOR_PTY_NUDGE_WINDOW_MS\", SUBMIT_NUDGE_WINDOW_MS),\n };\n}\n\nexport function resolvePlanDialogTiming(): {\n firstPressMs: number;\n intervalMs: number;\n slowIntervalMs: number;\n fastWindowMs: number;\n windowMs: number;\n} {\n return {\n firstPressMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS\", PLAN_DIALOG_FIRST_PRESS_MS),\n intervalMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS\", PLAN_DIALOG_INTERVAL_MS),\n slowIntervalMs: envMs(\n \"CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS\",\n PLAN_DIALOG_SLOW_INTERVAL_MS,\n ),\n fastWindowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS\", PLAN_DIALOG_FAST_WINDOW_MS),\n windowMs: envMs(\"CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS\", PLAN_DIALOG_WINDOW_MS),\n };\n}\n\n/**\n * The per-turn subset of HarnessQueryOptions — everything that legitimately\n * changes from one turn to the next while the same `claude` process serves them\n * all. The process-level fields (model, cwd, sessionId, appendSystemPrompt, …)\n * are fixed at spawn and captured separately in the spawn options.\n */\nexport interface TurnOptions {\n canUseTool?: HarnessQueryOptions[\"canUseTool\"];\n promptDelivery?: \"submit\" | \"prefill\";\n planDialogAutoAccept?: boolean;\n abortController?: AbortController;\n}\n\nexport function turnOptionsFrom(options: HarnessQueryOptions): TurnOptions {\n return {\n canUseTool: options.canUseTool,\n promptDelivery: options.promptDelivery,\n planDialogAutoAccept: options.planDialogAutoAccept,\n abortController: options.abortController,\n };\n}\n\nexport interface PtyProcess {\n onData(listener: (data: string) => void): void;\n onExit(listener: (event: { exitCode: number }) => void): void;\n write(data: string): void;\n resize(cols: number, rows: number): void;\n kill(signal?: string): void;\n}\n\nexport interface PtySpawnOptions {\n name: string;\n cols: number;\n rows: number;\n cwd: string;\n env: Record<string, string>;\n}\n\nexport type PtySpawn = (file: string, args: string[], options: PtySpawnOptions) => PtyProcess;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction extractSpawn(mod: unknown): PtySpawn | null {\n if (!isRecord(mod)) return null;\n if (typeof mod.spawn === \"function\") return mod.spawn as PtySpawn;\n const def = mod.default;\n if (isRecord(def) && typeof def.spawn === \"function\") return def.spawn as PtySpawn;\n return null;\n}\n\nexport async function loadPtySpawn(): Promise<PtySpawn> {\n const mod: unknown = await import(\"node-pty\");\n const spawn = extractSpawn(mod);\n if (!spawn) throw new Error(\"node-pty: spawn export not found\");\n return spawn;\n}\n\n/**\n * The spawn implementation for this pod topology: node-pty locally, or the\n * workbench launcher's remote PTY in split-mode pods (the CLI must run in the\n * workbench container, next to the repo and inside the restartable cgroup).\n * Same PtySpawn contract either way — PtySession is topology-blind.\n */\nexport async function resolvePtySpawn(): Promise<PtySpawn> {\n const { workbenchEnabled } = await import(\"../../workbench/mode.js\");\n if (!workbenchEnabled()) return loadPtySpawn();\n const { getWorkbenchClient } = await import(\"../../workbench/client.js\");\n return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);\n}\n\n/**\n * Base directory for a PTY session's control files (settings.json,\n * mcp-config, hook socket). In split-mode pods this MUST be the shared\n * emptyDir — the spawned CLI reads these paths from the workbench container,\n * and the hook helper connects back over the unix socket, which crosses\n * containers only on a shared volume.\n */\nexport function sessionTempBase(): string {\n return process.env.CONVEYOR_SHARED_DIR ?? tmpdir();\n}\n\nexport function inheritedEnv(socketPath?: string): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [key, value] of Object.entries(process.env)) {\n if (typeof value === \"string\") env[key] = value;\n }\n // Do NOT force CLAUDE_CONFIG_DIR. Forcing it to ~/.claude makes the CLI read\n // <dir>/.claude.json — a fresh, un-onboarded config — instead of the user's\n // real ~/.claude.json, which re-triggers the first-run theme picker + login\n // even though valid credentials already exist in ~/.claude/. The loop above\n // already propagates an explicitly-set CLAUDE_CONFIG_DIR (pods/codespaces),\n // and the config dir is unchanged either way, so transcript tailing (which\n // uses claudeConfigHome()) still resolves to the same projects/ directory.\n // Belt-and-braces against the ANTHROPIC_API_KEY leak: when a subscription\n // OAuth token is in play, the TUI authenticates via the synthesized\n // credentials.json — an inherited ANTHROPIC_API_KEY would be treated as an\n // EXTERNAL API key and, once interactively approved, poison auth (the token\n // is sent via x-api-key → 401 forever). The API bundle already stops serving\n // the OAuth token under that name; stripping it here means no upstream leak\n // can reach the spawned CLI regardless. The two auth modes are mutually\n // exclusive (see session-runner's onApiKeyUpdate), so a real api_key launch\n // has no CLAUDE_CODE_OAUTH_TOKEN and keeps its ANTHROPIC_API_KEY.\n if (env.CLAUDE_CODE_OAUTH_TOKEN) {\n delete env.ANTHROPIC_API_KEY;\n }\n if (socketPath) {\n env.CONVEYOR_HOOK_SOCKET = socketPath;\n }\n // The conveyor MCP tool handlers proxy over the agent's API socket, and a\n // mid-flap call can block up to ~50s (20s reconnect-wait + 30s ack) before\n // failing with a retryable error. The CLI's default MCP timeouts are\n // shorter — hitting them made it abandon the MCP session mid-flap\n // (2026-07-08 wedge). Default both timeouts past the worst-case hang;\n // operator-set values win via the inherit loop above.\n env.MCP_TIMEOUT ??= \"60000\";\n env.MCP_TOOL_TIMEOUT ??= \"180000\";\n return env;\n}\n\n/**\n * The bracketed-paste bytes written to the CLI's stdin to deliver a prompt.\n * The text is wrapped in paste markers so embedded newlines land as literal\n * lines in the input box instead of submitting it early. The submitting Enter\n * is deliberately NOT part of these bytes: deliverPrompt() sends it as a\n * separate write after a settle window, because an Enter folded into the same\n * chunk as the paste can be swallowed by the CLI's paste-burst handling —\n * observed in production as an auto-mode prompt parked unsubmitted in the\n * input box. \"prefill\" delivery never sends an Enter at all.\n */\nexport function buildPromptBytes(text: string): string {\n return `\\x1b[200~${text}\\x1b[201~`;\n}\n\n/**\n * Render structured (multimodal) prompt content as pasteable text. Image\n * blocks are replaced with a tool reference — their base64 payload must NEVER\n * be pasted into the TUI input box. Upstream prompt builders already skip\n * image blocks for the PTY harness; this is the last line of defense.\n */\nexport function renderPromptContentText(content: unknown[]): string {\n return content\n .map((block) => {\n const b = block as { type?: string; text?: string };\n if (b?.type === \"text\" && typeof b.text === \"string\") return b.text;\n if (b?.type === \"image\") {\n return `[Image attachment — use list_task_files / get_attachment to view]`;\n }\n return JSON.stringify(block);\n })\n .join(\"\\n\\n\");\n}\n\nexport { sleep } from \"../../utils/sleep.js\";\n\n/** Current size of the transcript, or 0 if it does not exist yet. */\nexport async function transcriptSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Defensive extraction of AskUserQuestion's `questions` input from the hook\n * payload. Malformed entries are dropped rather than thrown — the event is\n * observe-only, so an empty list still arms the waiting_for_input report.\n */\nexport function parseUserQuestions(input: Record<string, unknown>): HarnessUserQuestion[] {\n if (!Array.isArray(input.questions)) return [];\n const questions: HarnessUserQuestion[] = [];\n for (const entry of input.questions) {\n if (!isRecord(entry)) continue;\n if (typeof entry.question !== \"string\") continue;\n const options = Array.isArray(entry.options)\n ? entry.options\n .filter(isRecord)\n .filter((o) => typeof o.label === \"string\")\n .map((o) => ({\n label: o.label as string,\n description: typeof o.description === \"string\" ? o.description : \"\",\n }))\n : [];\n questions.push({\n question: entry.question,\n header: typeof entry.header === \"string\" ? entry.header : \"\",\n options,\n ...(typeof entry.multiSelect === \"boolean\" ? { multiSelect: entry.multiSelect } : {}),\n });\n }\n return questions;\n}\n","export function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import { execFile } from \"node:child_process\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { mkdtemp, rm, stat, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { promisify } from \"node:util\";\nimport { createGzip } from \"node:zlib\";\nimport * as tar from \"tar\";\nimport { planSnapshotFiles, type GitSurface } from \"./snapshot-artifact.js\";\n\n/**\n * In-archive manifest binding a snapshot to the HEAD it was captured on and\n * carrying the deletion list. Lives ONLY inside the tar (assembled in a temp\n * staging dir) — never written into the workspace tree.\n */\nexport const SNAPSHOT_MANIFEST_NAME = \".conveyor-snapshot-manifest.json\";\n\n/**\n * Legacy in-archive name for the postgres dump that pre-#2623 agents bundled\n * into the snapshot tar. Sidecar state is no longer durable, so we neither\n * write nor restore this anymore — but a snapshot captured by an OLDER agent\n * (still sitting on the fixed per-workspace GCS object until the next finalize)\n * can carry one. Extraction must keep SKIPPING this name so the stale dump can\n * never land in the worktree and ride the wip stash's `git add -A` to GitHub.\n */\nconst LEGACY_PG_DUMP_FILENAME = \".conveyor-pgdump.sql\";\n\nexport interface SnapshotManifest {\n /** `git rev-parse HEAD` at capture time — restore refuses to extract over a\n * different HEAD (a stale delta over a newer clone clobbers newer work). */\n head: string;\n /** Repo-relative paths restore must delete after extraction. */\n deletions: string[];\n /** Epoch ms when the tar was BUILT (not uploaded) — also sent as the\n * `x-snapshot-captured-at` header so the API can refuse out-of-order PUTs. */\n capturedAt: number;\n}\n\n// git status --porcelain=v1 -z default maxBuffer (1MB) is too small for large\n// dirty trees; 64MB of NUL-delimited paths covers any realistic worktree.\nconst STATUS_MAX_BUFFER = 64 * 1024 * 1024;\n\n/**\n * Parse `git status --porcelain=v1 -z -uall` output. NUL-delimited and\n * unquoted, so unicode/space/quote filenames arrive verbatim (the LF-based\n * parse broke them via C-style quoting). Rename/copy entries deliver TWO\n * NUL-separated fields — `XY <new>\\0<orig>\\0`; the NEW path is included and,\n * for renames, the ORIGINAL path joins the deletion list (it exists in HEAD\n * but no longer on disk).\n */\nexport function parseStatusPorcelainZ(raw: string): { includes: string[]; deletions: string[] } {\n const includes: string[] = [];\n const deletions: string[] = [];\n const tokens = raw.split(\"\\0\");\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (!token || token.length < 4) continue;\n const x = token[0];\n const y = token[1];\n const filePath = token.slice(3);\n if (x === \"R\" || x === \"C\") {\n const origPath = tokens[++i];\n if (x === \"R\" && origPath) deletions.push(origPath);\n }\n // Ignored entries (only present with --ignored) never enter the plan.\n if (x === \"!\") continue;\n // The file is absent from the worktree when the worktree (Y) column is D\n // (covers ` D`, `AD`, `MD`, `RD`, `DD`), or on a staged-only deletion\n // (`D ` — deleted from index, gone from disk, worktree column blank).\n const missingFromWorktree = y === \"D\" || (x === \"D\" && y === \" \");\n if (missingFromWorktree) deletions.push(filePath);\n else includes.push(filePath);\n }\n return { includes, deletions };\n}\n\n// Async git (execFile, no shell): a synchronous status/rev-parse here would\n// freeze the agent's event loop for the duration — on an IO-starved pod (the\n// exact moment snapshots run) that starves the socket heartbeat and gets the\n// session restarted. See runner/git-utils.ts for the full rationale.\nconst execFileAsync = promisify(execFile);\nconst GIT_TIMEOUT_MS = 120_000;\n\n/** Single `git status --porcelain=v1 -z -uall` read feeding both GitSurface\n * views — includes and deletions must come from one consistent status read.\n * Loaded up-front (async); the returned surface serves it synchronously. */\nasync function realGitSurface(cwd: string): Promise<GitSurface> {\n const { stdout } = await execFileAsync(\"git\", [\"status\", \"--porcelain=v1\", \"-z\", \"-uall\"], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: STATUS_MAX_BUFFER,\n });\n const cached = parseStatusPorcelainZ(stdout.toString());\n return {\n trackedAndUntracked: () => cached.includes,\n deletedSinceHead: () => cached.deletions,\n };\n}\n\nasync function gitHead(cwd: string): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"HEAD\"], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n });\n return stdout.toString().trim() || null;\n } catch {\n return null;\n }\n}\n\nexport interface SnapshotTarResult {\n /** Gzipped tar on disk inside a private temp staging dir. Callers stream it\n * (never buffer the whole archive) and MUST call cleanup() when done. */\n tarPath: string;\n sizeBytes: number;\n fileCount: number;\n deletionCount: number;\n /** Epoch ms the tar was built — the capture generation marker. */\n capturedAt: number;\n cleanup(): Promise<void>;\n}\n\n/**\n * Assemble the snapshot archive: the manifest staged in a temp dir, worktree\n * files appended from the repo. The archive is written to disk and returned as\n * a path so upload can stream it — the tar is never held in memory (pods have\n * an OOM history; a single whole-tar Buffer was up to 500MB).\n */\nexport async function buildSnapshotTar(cwd: string): Promise<SnapshotTarResult> {\n const staging = await mkdtemp(path.join(tmpdir(), \"conveyor-snapshot-\"));\n const cleanup = async (): Promise<void> => {\n await rm(staging, { recursive: true, force: true }).catch(() => {});\n };\n\n try {\n const plan = planSnapshotFiles(await realGitSurface(cwd));\n const head = await gitHead(cwd);\n if (!head) throw new Error(\"cannot snapshot a repo without a resolvable HEAD\");\n const capturedAt = Date.now();\n\n const manifest: SnapshotManifest = {\n head,\n deletions: plan.deletions,\n capturedAt,\n };\n await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), \"utf8\");\n\n const stagedEntries = [SNAPSHOT_MANIFEST_NAME];\n\n // tar.replace can't operate on a compressed archive, so: create the\n // uncompressed tar from the staging entries, append the repo files with\n // cwd = the repo, then gzip file-to-file (all streaming).\n const rawTarPath = path.join(staging, \"snapshot.tar\");\n await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);\n if (plan.include.length > 0) {\n await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);\n }\n\n const tarPath = path.join(staging, \"snapshot.tar.gz\");\n await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));\n await rm(rawTarPath, { force: true });\n\n const { size } = await stat(tarPath);\n return {\n tarPath,\n sizeBytes: size,\n fileCount: plan.include.length,\n deletionCount: plan.deletions.length,\n capturedAt,\n cleanup,\n };\n } catch (err) {\n await cleanup();\n throw err;\n }\n}\n\nexport type SnapshotExtractResult =\n | {\n status: \"extracted\";\n filesExtracted: number;\n deletionsApplied: number;\n }\n /** Manifest HEAD differs from the current clone's HEAD — extracting would\n * smear an old delta over newer commits. The tree is NOT touched. */\n | { status: \"stale-head\"; snapshotHead: string; currentHead: string | null }\n /** Missing/unreadable manifest — not a trustworthy snapshot. Tree untouched. */\n | { status: \"invalid\" };\n\n/** Repo-relative deletion entries must resolve inside the workspace — a\n * malicious/corrupt manifest must never delete outside cwd. */\nfunction isWithinWorkspace(cwd: string, rel: string): boolean {\n if (!rel || path.isAbsolute(rel)) return false;\n const root = path.resolve(cwd);\n const abs = path.resolve(root, rel);\n return abs !== root && abs.startsWith(root + path.sep);\n}\n\n/**\n * Single `tar.list` pass over the archive to parse the manifest. Returns the\n * validated manifest, or null when it is missing/unreadable/lacks a HEAD.\n */\nasync function readSnapshotArchive(tarPath: string): Promise<SnapshotManifest | null> {\n let manifestRaw: Buffer | null = null;\n\n try {\n await tar.list({\n file: tarPath,\n onReadEntry: (entry) => {\n if (entry.path === SNAPSHOT_MANIFEST_NAME) {\n const chunks: Buffer[] = [];\n entry.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n entry.on(\"end\", () => {\n manifestRaw = Buffer.concat(chunks);\n });\n }\n },\n });\n } catch {\n return null;\n }\n\n if (!manifestRaw) return null;\n try {\n const parsed = JSON.parse((manifestRaw as Buffer).toString(\"utf8\")) as SnapshotManifest;\n if (typeof parsed.head !== \"string\" || !parsed.head) return null;\n return {\n head: parsed.head,\n deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],\n capturedAt: typeof parsed.capturedAt === \"number\" ? parsed.capturedAt : 0,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Extract a snapshot archive (path to a .tar.gz on disk) into the workspace.\n * Order matters: the manifest is read and the HEAD binding checked BEFORE any\n * tree mutation — a stale or invalid snapshot leaves the tree byte-identical.\n * A valid snapshot with zero files/deletions is still a SUCCESS (a clean tree\n * at capture time is a legitimate snapshot, not a failure).\n */\nexport async function extractSnapshotTar(\n cwd: string,\n tarPath: string,\n): Promise<SnapshotExtractResult> {\n const manifest = await readSnapshotArchive(tarPath);\n if (!manifest) return { status: \"invalid\" };\n\n const currentHead = await gitHead(cwd);\n if (manifest.head !== currentHead) {\n return { status: \"stale-head\", snapshotHead: manifest.head, currentHead };\n }\n\n let filesExtracted = 0;\n await tar.extract({\n file: tarPath,\n cwd,\n filter: (entryPath) => {\n // Manifest is archive metadata and never lands in cwd. A legacy pg dump\n // (bundled by pre-#2623 agents) is likewise skipped — sidecar state is no\n // longer restored, and a dump in the worktree would ride the next wip\n // stash's `git add -A` to GitHub.\n if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {\n return false;\n }\n filesExtracted++;\n return true;\n },\n });\n\n let deletionsApplied = 0;\n for (const rel of manifest.deletions) {\n if (typeof rel !== \"string\" || !isWithinWorkspace(cwd, rel)) continue;\n await rm(path.resolve(cwd, rel), { recursive: true, force: true });\n deletionsApplied++;\n }\n\n return { status: \"extracted\", filesExtracted, deletionsApplied };\n}\n\n/** Test seam: read a manifest back out of an archive without extracting. */\nexport function readSnapshotManifest(tarPath: string): Promise<SnapshotManifest | null> {\n return readSnapshotArchive(tarPath);\n}\n\n/** Test seam: list entry paths in an archive without extracting. */\nexport async function listSnapshotEntries(tarPath: string): Promise<string[]> {\n const entries: string[] = [];\n await tar.list({\n file: tarPath,\n onReadEntry: (entry) => {\n entries.push(String(entry.path));\n },\n });\n return entries;\n}\n","/**\n * Pure planning logic for the WorkPreservation source-tree snapshot:\n * tracked + untracked files are included in the archive, while deleted paths\n * are recorded separately for restore-time removal.\n */\n\nexport interface GitSurface {\n trackedAndUntracked(): string[];\n deletedSinceHead(): string[];\n}\n\nexport interface SnapshotFilePlan {\n include: string[];\n deletions: string[];\n}\n\nexport function planSnapshotFiles(git: GitSurface): SnapshotFilePlan {\n const include = git.trackedAndUntracked();\n const includeSet = new Set(include);\n const deletions = [...new Set(git.deletedSinceHead())].filter((path) => !includeSet.has(path));\n return { include, deletions };\n}\n","import { spawn, execSync, type ChildProcess } from \"node:child_process\";\n\n/**\n * The structural surface of a start-command child the supervisor manages —\n * satisfied by a local ChildProcess and by the workbench client's\n * RemoteProcessHandle (split-mode pods, where the process lives in the\n * workbench container).\n */\nexport interface ManagedChildProcess extends NodeJS.EventEmitter {\n pid?: number | undefined;\n exitCode: number | null;\n kill(signal?: NodeJS.Signals | number): boolean;\n}\n\nconst PROCESS_TERMINATION_GRACE_MS = 5_000;\n\nfunction abortError(): Error {\n const error = new Error(\"Operation aborted\");\n error.name = \"AbortError\";\n return error;\n}\n\nfunction signalProcessGroup(child: ManagedChildProcess, signal: NodeJS.Signals): void {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n try {\n child.kill(signal);\n } catch {\n // The process already exited.\n }\n }\n}\n\nexport function terminateProcessGroup(\n child: ManagedChildProcess | ChildProcess,\n graceMs = PROCESS_TERMINATION_GRACE_MS,\n): Promise<void> {\n if (child.exitCode !== null) return Promise.resolve();\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n child.removeListener(\"exit\", finish);\n resolve();\n };\n const timer = setTimeout(() => {\n signalProcessGroup(child, \"SIGKILL\");\n finish();\n }, graceMs);\n timer.unref();\n child.once(\"exit\", finish);\n signalProcessGroup(child, \"SIGTERM\");\n });\n}\n\nexport function runSetupCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n signal?: AbortSignal,\n): Promise<void> {\n if (signal?.aborted) return Promise.reject(abortError());\n return new Promise((resolve, reject) => {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n let settled = false;\n let aborting = false;\n const cleanup = (): void => signal?.removeEventListener(\"abort\", onAbort);\n const settle = (error?: Error): void => {\n if (settled) return;\n settled = true;\n cleanup();\n if (error) reject(error);\n else resolve();\n };\n const onAbort = (): void => {\n if (settled || aborting) return;\n aborting = true;\n void terminateProcessGroup(child).then(() => settle(abortError()));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (signal?.aborted) onAbort();\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n if (aborting || signal?.aborted) return;\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.on(\"close\", (code) => {\n if (aborting) return;\n settle(code === 0 ? undefined : new Error(`Setup command exited with code ${code}`));\n });\n\n child.on(\"error\", (err) => {\n if (!aborting) settle(err);\n });\n });\n}\n\nconst AUTH_TOKEN_TIMEOUT_MS = 30_000;\n\nexport function runAuthTokenCommand(cmd: string, userEmail: string, cwd: string): string | null {\n try {\n const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {\n cwd,\n timeout: AUTH_TOKEN_TIMEOUT_MS,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n env: { ...process.env },\n });\n const token = output.toString().trim();\n return token || null;\n } catch {\n return null;\n }\n}\n\nexport function runStartCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n): ChildProcess {\n const child = spawn(\"sh\", [\"-c\", cmd], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: { ...process.env },\n });\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stdout\", chunk.toString());\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n onOutput(\"stderr\", chunk.toString());\n });\n\n child.unref();\n return child;\n}\n"],"mappings":"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","names":["spawn","path","stat","tmpdir","path","tmpdir","stat"]}
@@ -0,0 +1,25 @@
1
+ // src/workbench/mode.ts
2
+ function workbenchPort() {
3
+ const raw = process.env.CONVEYOR_WORKBENCH_PORT;
4
+ if (!raw) return null;
5
+ const port = Number(raw);
6
+ return Number.isInteger(port) && port > 0 ? port : null;
7
+ }
8
+ function workbenchEnabled() {
9
+ if (process.env.CONVEYOR_MODE === "workbench") return false;
10
+ return workbenchPort() !== null;
11
+ }
12
+ function workbenchToken() {
13
+ return process.env.CONVEYOR_WORKBENCH_TOKEN ?? process.env.POD_BOOTSTRAP_TOKEN ?? "";
14
+ }
15
+ function sharedDir() {
16
+ return process.env.CONVEYOR_SHARED_DIR ?? null;
17
+ }
18
+
19
+ export {
20
+ workbenchPort,
21
+ workbenchEnabled,
22
+ workbenchToken,
23
+ sharedDir
24
+ };
25
+ //# sourceMappingURL=chunk-4VUQ2NPF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workbench/mode.ts"],"sourcesContent":["/**\n * Split-mode detection. The pod spec sets CONVEYOR_WORKBENCH_PORT (+ role,\n * shared dir, bootstrap token) on both containers when the workbench split is\n * enabled for the project; with the env absent, every routing seam falls back\n * to today's local behavior — monolith pods, codespaces, and local dev are\n * byte-identical to the pre-split code paths.\n */\n\nexport function workbenchPort(): number | null {\n const raw = process.env.CONVEYOR_WORKBENCH_PORT;\n if (!raw) return null;\n const port = Number(raw);\n return Number.isInteger(port) && port > 0 ? port : null;\n}\n\nexport function workbenchEnabled(): boolean {\n // The workbench container itself must never route back into the launcher —\n // its supervisor/setup code (if any ever runs there) executes locally.\n if (process.env.CONVEYOR_MODE === \"workbench\") return false;\n return workbenchPort() !== null;\n}\n\n/** Shared secret for the launcher. The pod bootstrap token is already present\n * in both containers' env; CONVEYOR_WORKBENCH_TOKEN wins for tests. */\nexport function workbenchToken(): string {\n return process.env.CONVEYOR_WORKBENCH_TOKEN ?? process.env.POD_BOOTSTRAP_TOKEN ?? \"\";\n}\n\n/** Shared emptyDir visible to both containers (hook sockets, PTY temp dirs,\n * git-ready markers). Null outside split mode. */\nexport function sharedDir(): string | null {\n return process.env.CONVEYOR_SHARED_DIR ?? null;\n}\n"],"mappings":";AAQO,SAAS,gBAA+B;AAC7C,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,OAAO,GAAG;AACvB,SAAO,OAAO,UAAU,IAAI,KAAK,OAAO,IAAI,OAAO;AACrD;AAEO,SAAS,mBAA4B;AAG1C,MAAI,QAAQ,IAAI,kBAAkB,YAAa,QAAO;AACtD,SAAO,cAAc,MAAM;AAC7B;AAIO,SAAS,iBAAyB;AACvC,SAAO,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,uBAAuB;AACpF;AAIO,SAAS,YAA2B;AACzC,SAAO,QAAQ,IAAI,uBAAuB;AAC5C;","names":[]}