@rallycry/conveyor-agent 10.13.8 → 10.13.10

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.
@@ -1,13 +1,31 @@
1
1
  import {
2
2
  LoopLagMonitor
3
3
  } from "./chunk-7TQO4ZF4.js";
4
-
5
- // src/utils/sleep.ts
6
- function sleep(ms) {
7
- return new Promise((resolve) => {
8
- setTimeout(resolve, ms);
9
- });
10
- }
4
+ import {
5
+ getWorkbenchClient
6
+ } from "./chunk-YRC57EYG.js";
7
+ import {
8
+ workbenchEnabled
9
+ } from "./chunk-4VUQ2NPF.js";
10
+ import {
11
+ MAX_BETWEEN_TURN_BUFFER,
12
+ MAX_DIAGNOSTIC_OUTPUT,
13
+ buildPromptBytes,
14
+ buildSnapshotTar,
15
+ extractSnapshotTar,
16
+ inheritedEnv,
17
+ loadPtySpawn,
18
+ parseUserQuestions,
19
+ renderPromptContentText,
20
+ resolvePlanDialogTiming,
21
+ resolvePtySpawn,
22
+ resolveSubmitNudgeTiming,
23
+ resolveSubmitSettleMs,
24
+ sessionTempBase,
25
+ sleep,
26
+ transcriptSize,
27
+ turnOptionsFrom
28
+ } from "./chunk-2K6RRTQC.js";
11
29
 
12
30
  // src/setup/bootstrap.ts
13
31
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -1193,6 +1211,14 @@ var GIT_TIMEOUT_MS = 6e4;
1193
1211
  var GIT_SLOW_TIMEOUT_MS = 12e4;
1194
1212
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
1195
1213
  async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
1214
+ if (workbenchEnabled()) {
1215
+ const { stdout: stdout2 } = await getWorkbenchClient().execFile("git", args, {
1216
+ cwd,
1217
+ timeout: timeoutMs,
1218
+ maxBuffer: GIT_MAX_BUFFER
1219
+ });
1220
+ return stdout2.trim();
1221
+ }
1196
1222
  const { stdout } = await execFileAsync("git", args, {
1197
1223
  cwd,
1198
1224
  timeout: timeoutMs,
@@ -1574,7 +1600,9 @@ async function backupOtherBranches(cwd, currentBranch) {
1574
1600
  }
1575
1601
 
1576
1602
  // src/runner/work-preservation/index.ts
1577
- import { rm as rm3 } from "fs/promises";
1603
+ import { mkdtemp, rm as rm2 } from "fs/promises";
1604
+ import { tmpdir as tmpdir2 } from "os";
1605
+ import { join } from "path";
1578
1606
 
1579
1607
  // src/runner/work-preservation/restore-precedence.ts
1580
1608
  function selectRestoreSource(probe) {
@@ -1583,193 +1611,19 @@ function selectRestoreSource(probe) {
1583
1611
  return "clean";
1584
1612
  }
1585
1613
 
1586
- // src/runner/work-preservation/snapshot-tar.ts
1587
- import { execFile as execFile2 } from "child_process";
1614
+ // src/runner/work-preservation/snapshot-transfer.ts
1615
+ import { randomUUID } from "crypto";
1588
1616
  import { createReadStream, createWriteStream } from "fs";
1589
- import { mkdtemp, rm, stat, writeFile } from "fs/promises";
1617
+ import { rm } from "fs/promises";
1590
1618
  import { tmpdir } from "os";
1591
1619
  import path from "path";
1592
- import { pipeline } from "stream/promises";
1593
- import { promisify as promisify2 } from "util";
1594
- import { createGzip } from "zlib";
1595
- import * as tar from "tar";
1596
-
1597
- // src/runner/work-preservation/snapshot-artifact.ts
1598
- function planSnapshotFiles(git2) {
1599
- const include = git2.trackedAndUntracked();
1600
- const includeSet = new Set(include);
1601
- const deletions = [...new Set(git2.deletedSinceHead())].filter((path4) => !includeSet.has(path4));
1602
- return { include, deletions };
1603
- }
1604
-
1605
- // src/runner/work-preservation/snapshot-tar.ts
1606
- var SNAPSHOT_MANIFEST_NAME = ".conveyor-snapshot-manifest.json";
1607
- var LEGACY_PG_DUMP_FILENAME = ".conveyor-pgdump.sql";
1608
- var STATUS_MAX_BUFFER = 64 * 1024 * 1024;
1609
- function parseStatusPorcelainZ(raw) {
1610
- const includes = [];
1611
- const deletions = [];
1612
- const tokens = raw.split("\0");
1613
- for (let i = 0; i < tokens.length; i++) {
1614
- const token = tokens[i];
1615
- if (!token || token.length < 4) continue;
1616
- const x = token[0];
1617
- const y = token[1];
1618
- const filePath = token.slice(3);
1619
- if (x === "R" || x === "C") {
1620
- const origPath = tokens[++i];
1621
- if (x === "R" && origPath) deletions.push(origPath);
1622
- }
1623
- if (x === "!") continue;
1624
- const missingFromWorktree = y === "D" || x === "D" && y === " ";
1625
- if (missingFromWorktree) deletions.push(filePath);
1626
- else includes.push(filePath);
1627
- }
1628
- return { includes, deletions };
1629
- }
1630
- var execFileAsync2 = promisify2(execFile2);
1631
- var GIT_TIMEOUT_MS2 = 12e4;
1632
- async function realGitSurface(cwd) {
1633
- const { stdout } = await execFileAsync2("git", ["status", "--porcelain=v1", "-z", "-uall"], {
1634
- cwd,
1635
- timeout: GIT_TIMEOUT_MS2,
1636
- maxBuffer: STATUS_MAX_BUFFER
1637
- });
1638
- const cached = parseStatusPorcelainZ(stdout.toString());
1639
- return {
1640
- trackedAndUntracked: () => cached.includes,
1641
- deletedSinceHead: () => cached.deletions
1642
- };
1643
- }
1644
- async function gitHead(cwd) {
1645
- try {
1646
- const { stdout } = await execFileAsync2("git", ["rev-parse", "HEAD"], {
1647
- cwd,
1648
- timeout: GIT_TIMEOUT_MS2
1649
- });
1650
- return stdout.toString().trim() || null;
1651
- } catch {
1652
- return null;
1653
- }
1654
- }
1655
- async function buildSnapshotTar(cwd) {
1656
- const staging = await mkdtemp(path.join(tmpdir(), "conveyor-snapshot-"));
1657
- const cleanup = async () => {
1658
- await rm(staging, { recursive: true, force: true }).catch(() => {
1659
- });
1660
- };
1661
- try {
1662
- const plan = planSnapshotFiles(await realGitSurface(cwd));
1663
- const head = await gitHead(cwd);
1664
- if (!head) throw new Error("cannot snapshot a repo without a resolvable HEAD");
1665
- const capturedAt = Date.now();
1666
- const manifest = {
1667
- head,
1668
- deletions: plan.deletions,
1669
- capturedAt
1670
- };
1671
- await writeFile(path.join(staging, SNAPSHOT_MANIFEST_NAME), JSON.stringify(manifest), "utf8");
1672
- const stagedEntries = [SNAPSHOT_MANIFEST_NAME];
1673
- const rawTarPath = path.join(staging, "snapshot.tar");
1674
- await tar.create({ cwd: staging, file: rawTarPath, portable: true }, stagedEntries);
1675
- if (plan.include.length > 0) {
1676
- await tar.replace({ file: rawTarPath, cwd, portable: true }, plan.include);
1677
- }
1678
- const tarPath = path.join(staging, "snapshot.tar.gz");
1679
- await pipeline(createReadStream(rawTarPath), createGzip(), createWriteStream(tarPath));
1680
- await rm(rawTarPath, { force: true });
1681
- const { size } = await stat(tarPath);
1682
- return {
1683
- tarPath,
1684
- sizeBytes: size,
1685
- fileCount: plan.include.length,
1686
- deletionCount: plan.deletions.length,
1687
- capturedAt,
1688
- cleanup
1689
- };
1690
- } catch (err) {
1691
- await cleanup();
1692
- throw err;
1693
- }
1694
- }
1695
- function isWithinWorkspace(cwd, rel) {
1696
- if (!rel || path.isAbsolute(rel)) return false;
1697
- const root = path.resolve(cwd);
1698
- const abs = path.resolve(root, rel);
1699
- return abs !== root && abs.startsWith(root + path.sep);
1700
- }
1701
- async function readSnapshotArchive(tarPath) {
1702
- let manifestRaw = null;
1703
- try {
1704
- await tar.list({
1705
- file: tarPath,
1706
- onReadEntry: (entry) => {
1707
- if (entry.path === SNAPSHOT_MANIFEST_NAME) {
1708
- const chunks = [];
1709
- entry.on("data", (chunk) => chunks.push(chunk));
1710
- entry.on("end", () => {
1711
- manifestRaw = Buffer.concat(chunks);
1712
- });
1713
- }
1714
- }
1715
- });
1716
- } catch {
1717
- return null;
1718
- }
1719
- if (!manifestRaw) return null;
1720
- try {
1721
- const parsed = JSON.parse(manifestRaw.toString("utf8"));
1722
- if (typeof parsed.head !== "string" || !parsed.head) return null;
1723
- return {
1724
- head: parsed.head,
1725
- deletions: Array.isArray(parsed.deletions) ? parsed.deletions : [],
1726
- capturedAt: typeof parsed.capturedAt === "number" ? parsed.capturedAt : 0
1727
- };
1728
- } catch {
1729
- return null;
1730
- }
1731
- }
1732
- async function extractSnapshotTar(cwd, tarPath) {
1733
- const manifest = await readSnapshotArchive(tarPath);
1734
- if (!manifest) return { status: "invalid" };
1735
- const currentHead = await gitHead(cwd);
1736
- if (manifest.head !== currentHead) {
1737
- return { status: "stale-head", snapshotHead: manifest.head, currentHead };
1738
- }
1739
- let filesExtracted = 0;
1740
- await tar.extract({
1741
- file: tarPath,
1742
- cwd,
1743
- filter: (entryPath) => {
1744
- if (entryPath === SNAPSHOT_MANIFEST_NAME || entryPath === LEGACY_PG_DUMP_FILENAME) {
1745
- return false;
1746
- }
1747
- filesExtracted++;
1748
- return true;
1749
- }
1750
- });
1751
- let deletionsApplied = 0;
1752
- for (const rel of manifest.deletions) {
1753
- if (typeof rel !== "string" || !isWithinWorkspace(cwd, rel)) continue;
1754
- await rm(path.resolve(cwd, rel), { recursive: true, force: true });
1755
- deletionsApplied++;
1756
- }
1757
- return { status: "extracted", filesExtracted, deletionsApplied };
1758
- }
1759
-
1760
- // src/runner/work-preservation/snapshot-transfer.ts
1761
- import { randomUUID } from "crypto";
1762
- import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "fs";
1763
- import { rm as rm2 } from "fs/promises";
1764
- import { tmpdir as tmpdir2 } from "os";
1765
- import path2 from "path";
1766
1620
  import { Readable } from "stream";
1767
- import { pipeline as pipeline2 } from "stream/promises";
1621
+ import { pipeline } from "stream/promises";
1768
1622
  var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
1769
1623
  var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
1770
1624
  async function putSnapshotToGcs(url, tarResult) {
1771
1625
  try {
1772
- const body = Readable.toWeb(createReadStream2(tarResult.tarPath));
1626
+ const body = Readable.toWeb(createReadStream(tarResult.tarPath));
1773
1627
  const res = await fetch(url, {
1774
1628
  method: "PUT",
1775
1629
  headers: {
@@ -1787,26 +1641,51 @@ async function putSnapshotToGcs(url, tarResult) {
1787
1641
  }
1788
1642
  }
1789
1643
  async function downloadSnapshotToTemp(url) {
1790
- const tmpTar = path2.join(tmpdir2(), `conveyor-snapshot-restore-${randomUUID()}.tar.gz`);
1644
+ const tmpTar = path.join(tmpdir(), `conveyor-snapshot-restore-${randomUUID()}.tar.gz`);
1791
1645
  try {
1792
1646
  const res = await fetch(url, {
1793
1647
  method: "GET",
1794
1648
  signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
1795
1649
  });
1796
1650
  if (!res.ok || !res.body) return null;
1797
- await pipeline2(
1651
+ await pipeline(
1798
1652
  Readable.fromWeb(res.body),
1799
- createWriteStream2(tmpTar)
1653
+ createWriteStream(tmpTar)
1800
1654
  );
1801
1655
  return tmpTar;
1802
1656
  } catch {
1803
- await rm2(tmpTar, { force: true }).catch(() => {
1657
+ await rm(tmpTar, { force: true }).catch(() => {
1804
1658
  });
1805
1659
  return null;
1806
1660
  }
1807
1661
  }
1808
1662
 
1809
1663
  // src/runner/work-preservation/index.ts
1664
+ async function captureSnapshotTar(cwd) {
1665
+ if (!workbenchEnabled()) return buildSnapshotTar(cwd);
1666
+ const staging = await mkdtemp(join(tmpdir2(), "conveyor-snapshot-remote-"));
1667
+ const cleanup = async () => {
1668
+ await rm2(staging, { recursive: true, force: true }).catch(() => {
1669
+ });
1670
+ };
1671
+ try {
1672
+ const meta = await getWorkbenchClient().buildSnapshot(cwd, join(staging, "snapshot.tar.gz"));
1673
+ return {
1674
+ tarPath: meta.tarPath,
1675
+ sizeBytes: meta.sizeBytes,
1676
+ fileCount: meta.fileCount,
1677
+ deletionCount: meta.deletionCount,
1678
+ capturedAt: meta.capturedAt,
1679
+ cleanup
1680
+ };
1681
+ } catch (err) {
1682
+ await cleanup();
1683
+ throw err;
1684
+ }
1685
+ }
1686
+ function applySnapshotTar(cwd, tarPath) {
1687
+ return workbenchEnabled() ? getWorkbenchClient().extractSnapshot(cwd, tarPath) : extractSnapshotTar(cwd, tarPath);
1688
+ }
1810
1689
  var periodicTimer = null;
1811
1690
  var inFlightCapture = null;
1812
1691
  var inFlightGcsUpload = null;
@@ -1819,7 +1698,7 @@ var EMPTY_GCS_RESULT = {
1819
1698
  async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
1820
1699
  let tarResult;
1821
1700
  try {
1822
- tarResult = await buildSnapshotTar(cwd);
1701
+ tarResult = await captureSnapshotTar(cwd);
1823
1702
  } catch {
1824
1703
  return EMPTY_GCS_RESULT;
1825
1704
  }
@@ -1902,7 +1781,7 @@ async function restoreOnBoot(bundle, cwd) {
1902
1781
  const tmpTar = await downloadSnapshotToTemp(bundle.snapshotUrl);
1903
1782
  if (tmpTar) {
1904
1783
  try {
1905
- const result = await extractSnapshotTar(cwd, tmpTar);
1784
+ const result = await applySnapshotTar(cwd, tmpTar);
1906
1785
  if (result.status === "extracted") {
1907
1786
  gcsAvailable = true;
1908
1787
  gcsFileCount = result.filesExtracted;
@@ -1910,7 +1789,7 @@ async function restoreOnBoot(bundle, cwd) {
1910
1789
  } catch {
1911
1790
  gcsAvailable = false;
1912
1791
  } finally {
1913
- await rm3(tmpTar, { force: true }).catch(() => {
1792
+ await rm2(tmpTar, { force: true }).catch(() => {
1914
1793
  });
1915
1794
  }
1916
1795
  }
@@ -2318,7 +2197,10 @@ var UpdateTaskPropertiesRequestSchema = z3.object({
2318
2197
  tagIds: z3.array(z3.string()).optional(),
2319
2198
  tagNames: z3.array(z3.string()).optional(),
2320
2199
  githubPRUrl: z3.string().url().optional(),
2321
- githubBranch: z3.string().optional()
2200
+ githubBranch: z3.string().optional(),
2201
+ // Canonical risk level, or null to clear — same semantics as the headless
2202
+ // update_task boundary (resolved to the project's Risk row in the handler).
2203
+ risk: riskLevelSchema.nullable().optional()
2322
2204
  });
2323
2205
  var ListIconsRequestSchema = z3.object({
2324
2206
  sessionId: z3.string()
@@ -2633,6 +2515,9 @@ var GetProjectSummaryRequestSchema = z4.object({
2633
2515
  var GetProjectOnboardingStatusRequestSchema = z4.object({
2634
2516
  projectId: z4.string()
2635
2517
  });
2518
+ var GetProjectOnboardingStepRequestSchema = z4.object({
2519
+ projectId: z4.string()
2520
+ });
2636
2521
  var GetProjectConnectUrlsRequestSchema = z4.object({
2637
2522
  projectId: z4.string()
2638
2523
  });
@@ -2650,7 +2535,13 @@ var UpdateProjectTaskRequestSchema = z4.object({
2650
2535
  projectId: z4.string(),
2651
2536
  taskId: z4.string(),
2652
2537
  title: z4.string().optional(),
2538
+ description: z4.string().optional(),
2653
2539
  plan: z4.string().optional(),
2540
+ // Enum validation lives at the MCP tool layer (mirrors createProjectTask);
2541
+ // the handler routes through the shared updateStatus core (InProgress
2542
+ // dependency check + cleanup/board/Slack side effects), not the stricter
2543
+ // card-type-validating path the Socket.IO updateTaskStatus mutation uses.
2544
+ status: z4.string().optional(),
2654
2545
  // Canonical risk level, or null to clear. Resolved to the project's
2655
2546
  // configured Risk row (by rank) in the handler.
2656
2547
  risk: riskLevelSchema.nullable().optional(),
@@ -2660,9 +2551,9 @@ var UpdateProjectTaskRequestSchema = z4.object({
2660
2551
  subProjectId: z4.string().nullable().optional(),
2661
2552
  requestingUserId: z4.string().optional()
2662
2553
  }).strict().refine(
2663
- (v) => v.title !== void 0 || v.plan !== void 0 || v.risk !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0,
2554
+ (v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0,
2664
2555
  {
2665
- message: "update_task requires at least one field to change (title, plan, risk, assignedUserId, or subProjectId)"
2556
+ message: "update_task requires at least one field to change (title, description, plan, status, risk, assignedUserId, or subProjectId)"
2666
2557
  }
2667
2558
  );
2668
2559
  var TransitionProjectTaskStatusRequestSchema = z4.object({
@@ -2814,6 +2705,9 @@ var UpdateProjectSubtaskRequestSchema = z4.object({
2814
2705
  ordinal: z4.number().int().nonnegative().optional(),
2815
2706
  storyPointValue: z4.number().int().positive().optional(),
2816
2707
  followParentStatus: z4.boolean().optional(),
2708
+ /** Replace-set of sibling subtask ids/slugs this subtask blocks on ([] clears).
2709
+ * Mirrors the in-pod updateSubtask semantics. */
2710
+ dependsOn: z4.array(z4.string().min(1)).max(32).optional(),
2817
2711
  requestingUserId: z4.string().optional()
2818
2712
  });
2819
2713
  var DeleteProjectSubtaskRequestSchema = z4.object({
@@ -3389,9 +3283,9 @@ var ClaudeCodeHarness = class {
3389
3283
  };
3390
3284
 
3391
3285
  // src/harness/pty/session.ts
3392
- import { mkdtemp as mkdtemp2, mkdir as mkdir3, rm as rm5 } from "fs/promises";
3393
- import { tmpdir as tmpdir3 } from "os";
3394
- import { join as join4, dirname } from "path";
3286
+ import { randomUUID as randomUUID2 } from "crypto";
3287
+ import { mkdtemp as mkdtemp2, mkdir as mkdir3, rm as rm4 } from "fs/promises";
3288
+ import { join as join5, dirname } from "path";
3395
3289
 
3396
3290
  // src/harness/pty/event-queue.ts
3397
3291
  var AsyncEventQueue = class {
@@ -3672,8 +3566,8 @@ function mapTranscriptRecord(raw) {
3672
3566
  // src/harness/pty/jsonl-tailer.ts
3673
3567
  var POLL_INTERVAL_MS = 25;
3674
3568
  var JsonlTailer = class {
3675
- constructor(path4, onEvent, onRawRecord) {
3676
- this.path = path4;
3569
+ constructor(path3, onEvent, onRawRecord) {
3570
+ this.path = path3;
3677
3571
  this.onEvent = onEvent;
3678
3572
  this.onRawRecord = onRawRecord;
3679
3573
  }
@@ -3807,6 +3701,27 @@ function stringField2(record, ...keys) {
3807
3701
  function truncate(text, max) {
3808
3702
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3809
3703
  }
3704
+ function compactQuestionsJson(questions) {
3705
+ const serialize = (qs) => JSON.stringify({ questions: qs });
3706
+ const withDescriptions = (max) => questions.map((q) => ({
3707
+ ...q,
3708
+ options: q.options.map((o) => ({ ...o, description: truncate(o.description, max) }))
3709
+ }));
3710
+ const full = serialize(questions);
3711
+ if (full.length <= TOOL_INPUT_MAX) return full;
3712
+ const shortened = serialize(withDescriptions(80));
3713
+ if (shortened.length <= TOOL_INPUT_MAX) return shortened;
3714
+ const bare = serialize(withDescriptions(0));
3715
+ if (bare.length <= TOOL_INPUT_MAX) return bare;
3716
+ return bare.slice(0, TOOL_INPUT_MAX);
3717
+ }
3718
+ function compactToolInput(name, input) {
3719
+ if (name === "AskUserQuestion" && isRecord2(input)) {
3720
+ const questions = parseUserQuestions(input);
3721
+ if (questions.length > 0) return compactQuestionsJson(questions);
3722
+ }
3723
+ return JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX);
3724
+ }
3810
3725
  function isNonConversationText(text) {
3811
3726
  const trimmed = text.trimStart();
3812
3727
  return trimmed.startsWith("<command-name>") || trimmed.startsWith("<local-command-") || trimmed.startsWith("<task-notification>");
@@ -3843,7 +3758,7 @@ function mapAssistant2(record) {
3843
3758
  const event = {
3844
3759
  kind: "tool_use",
3845
3760
  name: truncate(name, 200),
3846
- input: JSON.stringify(input ?? {}).slice(0, TOOL_INPUT_MAX)
3761
+ input: compactToolInput(name, input)
3847
3762
  };
3848
3763
  const id = stringField2(raw, "id");
3849
3764
  if (id !== void 0) event.id = id;
@@ -3912,17 +3827,17 @@ function mapChatRecords(raw) {
3912
3827
  }
3913
3828
 
3914
3829
  // src/harness/pty/settings.ts
3915
- import { mkdir, writeFile as writeFile2, chmod } from "fs/promises";
3830
+ import { mkdir, writeFile, chmod } from "fs/promises";
3916
3831
  import { homedir } from "os";
3917
- import { join } from "path";
3832
+ import { join as join2 } from "path";
3918
3833
  function claudeConfigHome() {
3919
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
3834
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
3920
3835
  }
3921
3836
  function projectSlug(cwd) {
3922
3837
  return cwd.replace(/\//g, "-");
3923
3838
  }
3924
3839
  function sessionTranscriptPath(cwd, sessionId) {
3925
- return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
3840
+ return join2(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
3926
3841
  }
3927
3842
  var ALLOW_RULES = [
3928
3843
  "Bash",
@@ -4090,10 +4005,10 @@ function preToolUse(payload) {
4090
4005
  }
4091
4006
  `;
4092
4007
  async function writeHookSettings(dir) {
4093
- const helperPath = join(dir, "hook-helper.cjs");
4094
- const settingsPath = join(dir, "settings.json");
4008
+ const helperPath = join2(dir, "hook-helper.cjs");
4009
+ const settingsPath = join2(dir, "settings.json");
4095
4010
  await mkdir(dir, { recursive: true });
4096
- await writeFile2(helperPath, HOOK_HELPER_SOURCE, "utf8");
4011
+ await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
4097
4012
  await chmod(helperPath, 493);
4098
4013
  const settings = {
4099
4014
  // Pre-accept Claude Code's "Bypass Permissions mode" disclaimer. Build-capable
@@ -4149,7 +4064,7 @@ async function writeHookSettings(dir) {
4149
4064
  ]
4150
4065
  }
4151
4066
  };
4152
- await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4067
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
4153
4068
  return { settingsPath, helperPath };
4154
4069
  }
4155
4070
 
@@ -4208,8 +4123,8 @@ var PtyOutputCoalescer = class {
4208
4123
  // src/harness/pty/tool-server.ts
4209
4124
  import { createServer as createServer2 } from "http";
4210
4125
  import { z as z6 } from "zod";
4211
- import { writeFile as writeFile3 } from "fs/promises";
4212
- import { join as join2 } from "path";
4126
+ import { writeFile as writeFile2 } from "fs/promises";
4127
+ import { join as join3 } from "path";
4213
4128
  import { randomBytes } from "crypto";
4214
4129
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4215
4130
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -4400,8 +4315,8 @@ async function startToolServers(mcpServers, tempDir) {
4400
4315
  config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
4401
4316
  }
4402
4317
  if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
4403
- const mcpConfigPath = join2(tempDir, "mcp-config.json");
4404
- await writeFile3(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
4318
+ const mcpConfigPath = join3(tempDir, "mcp-config.json");
4319
+ await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
4405
4320
  return { servers, mcpConfigPath };
4406
4321
  }
4407
4322
 
@@ -4476,13 +4391,13 @@ ${tail}`);
4476
4391
  }
4477
4392
 
4478
4393
  // src/harness/pty/credentials.ts
4479
- import { chmod as chmod2, mkdir as mkdir2, readFile, rm as rm4, writeFile as writeFile4 } from "fs/promises";
4394
+ import { chmod as chmod2, mkdir as mkdir2, readFile, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4480
4395
  import { homedir as homedir2 } from "os";
4481
- import { join as join3 } from "path";
4396
+ import { join as join4 } from "path";
4482
4397
  var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
4483
4398
  var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
4484
4399
  function claudeCredentialsPath() {
4485
- return join3(claudeConfigHome(), ".credentials.json");
4400
+ return join4(claudeConfigHome(), ".credentials.json");
4486
4401
  }
4487
4402
  function isConveyorCloudEnv(env = process.env) {
4488
4403
  return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
@@ -4527,9 +4442,9 @@ function planCredentialsWrite(input) {
4527
4442
  if (fresh) return { action: "skip", reason: "current" };
4528
4443
  return { action: "write", contents };
4529
4444
  }
4530
- async function readRaw(path4) {
4445
+ async function readRaw(path3) {
4531
4446
  try {
4532
- return await readFile(path4, "utf8");
4447
+ return await readFile(path3, "utf8");
4533
4448
  } catch {
4534
4449
  return null;
4535
4450
  }
@@ -4555,10 +4470,10 @@ async function writeWithReadBackRetry(io2, contents, delaysMs = READ_BACK_DELAYS
4555
4470
  await sleep2(delaysMs[attempt]);
4556
4471
  }
4557
4472
  }
4558
- function fsWriteIo(path4, mode) {
4473
+ function fsWriteIo(path3, mode) {
4559
4474
  return {
4560
- write: (contents) => writeFile4(path4, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
4561
- read: () => readRaw(path4)
4475
+ write: (contents) => writeFile3(path3, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
4476
+ read: () => readRaw(path3)
4562
4477
  };
4563
4478
  }
4564
4479
  async function ensureClaudeCredentials(env = process.env) {
@@ -4568,23 +4483,23 @@ async function ensureClaudeCredentials(env = process.env) {
4568
4483
  await sanitizeApprovedApiKeys(token);
4569
4484
  }
4570
4485
  try {
4571
- const path4 = claudeCredentialsPath();
4486
+ const path3 = claudeCredentialsPath();
4572
4487
  const plan = planCredentialsWrite({
4573
4488
  isCloud,
4574
4489
  token,
4575
- existingRaw: await readRaw(path4),
4490
+ existingRaw: await readRaw(path3),
4576
4491
  now: Date.now()
4577
4492
  });
4578
4493
  if (plan.action === "skip") return;
4579
4494
  await mkdir2(claudeConfigHome(), { recursive: true });
4580
- const verified = await writeWithReadBackRetry(fsWriteIo(path4, 384), plan.contents);
4495
+ const verified = await writeWithReadBackRetry(fsWriteIo(path3, 384), plan.contents);
4581
4496
  if (!verified) {
4582
4497
  process.stderr.write(
4583
- `[conveyor-agent] claude credentials read-back still stale after retries at ${path4} \u2014 TUI may land on the login picker
4498
+ `[conveyor-agent] claude credentials read-back still stale after retries at ${path3} \u2014 TUI may land on the login picker
4584
4499
  `
4585
4500
  );
4586
4501
  }
4587
- await chmod2(path4, 384).catch(() => {
4502
+ await chmod2(path3, 384).catch(() => {
4588
4503
  });
4589
4504
  } catch (err) {
4590
4505
  const message = err instanceof Error ? err.message : String(err);
@@ -4613,12 +4528,12 @@ function planApprovedApiKeyCleanup(existingRaw, oauthToken) {
4613
4528
  }
4614
4529
  async function sanitizeApprovedApiKeys(oauthToken) {
4615
4530
  try {
4616
- const path4 = claudeJsonPath();
4617
- const cleaned = planApprovedApiKeyCleanup(await readRaw(path4), oauthToken);
4531
+ const path3 = claudeJsonPath();
4532
+ const cleaned = planApprovedApiKeyCleanup(await readRaw(path3), oauthToken);
4618
4533
  if (cleaned === null) return;
4619
- const verified = await writeWithReadBackRetry(fsWriteIo(path4), cleaned);
4534
+ const verified = await writeWithReadBackRetry(fsWriteIo(path3), cleaned);
4620
4535
  process.stderr.write(
4621
- verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${path4} \u2014 CLI may still see the poisoned entry
4536
+ verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${path3} \u2014 CLI may still see the poisoned entry
4622
4537
  `
4623
4538
  );
4624
4539
  } catch (err) {
@@ -4629,7 +4544,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
4629
4544
  }
4630
4545
  function claudeJsonPath() {
4631
4546
  const configDir = process.env.CLAUDE_CONFIG_DIR;
4632
- return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
4547
+ return configDir ? join4(configDir, ".claude.json") : join4(homedir2(), ".claude.json");
4633
4548
  }
4634
4549
  function asRecord(value) {
4635
4550
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
@@ -4722,7 +4637,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
4722
4637
  return changed ? JSON.stringify(config) : null;
4723
4638
  }
4724
4639
  function conveyorOauthMarkerPath() {
4725
- return join3(claudeConfigHome(), "conveyor-oauth-account.json");
4640
+ return join4(claudeConfigHome(), "conveyor-oauth-account.json");
4726
4641
  }
4727
4642
  function parseOauthIdentity(raw) {
4728
4643
  if (!raw || raw.trim() === "") return null;
@@ -4755,8 +4670,8 @@ async function persistOauthIdentityMarker(configIdentity, markerIdentity) {
4755
4670
  async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4756
4671
  try {
4757
4672
  if (!isConveyorCloudEnv(env)) return;
4758
- const path4 = claudeJsonPath();
4759
- const existingRaw = await readRaw(path4);
4673
+ const path3 = claudeJsonPath();
4674
+ const existingRaw = await readRaw(path3);
4760
4675
  const markerIdentity = parseOauthIdentity(await readRaw(conveyorOauthMarkerPath()));
4761
4676
  await persistOauthIdentityMarker(
4762
4677
  extractOauthIdentity(parseClaudeJson(existingRaw)),
@@ -4764,16 +4679,16 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4764
4679
  );
4765
4680
  const contents = planClaudeJsonSeed(existingRaw, trustCwd, markerIdentity);
4766
4681
  if (contents === null) return;
4767
- const verified = await writeWithReadBackRetry(fsWriteIo(path4), contents);
4682
+ const verified = await writeWithReadBackRetry(fsWriteIo(path3), contents);
4768
4683
  if (verified) {
4769
4684
  process.stderr.write(
4770
4685
  `[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
4771
4686
  `
4772
4687
  );
4773
4688
  } else {
4774
- const verify = await readRaw(path4);
4689
+ const verify = await readRaw(path3);
4775
4690
  process.stderr.write(
4776
- `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path4} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4691
+ `[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path3} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
4777
4692
  `
4778
4693
  );
4779
4694
  }
@@ -4786,12 +4701,12 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
4786
4701
  async function removeConveyorCredentials(env = process.env) {
4787
4702
  try {
4788
4703
  if (!isConveyorCloudEnv(env)) return;
4789
- const path4 = claudeCredentialsPath();
4790
- const existing = parseClaudeAiOauth(await readRaw(path4));
4704
+ const path3 = claudeCredentialsPath();
4705
+ const existing = parseClaudeAiOauth(await readRaw(path3));
4791
4706
  if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
4792
4707
  return;
4793
4708
  }
4794
- await rm4(path4, { force: true });
4709
+ await rm3(path3, { force: true });
4795
4710
  } catch (err) {
4796
4711
  const message = err instanceof Error ? err.message : String(err);
4797
4712
  process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
@@ -4799,126 +4714,6 @@ async function removeConveyorCredentials(env = process.env) {
4799
4714
  }
4800
4715
  }
4801
4716
 
4802
- // src/harness/pty/pty-support.ts
4803
- import { stat as stat2 } from "fs/promises";
4804
- var MAX_DIAGNOSTIC_OUTPUT = 4e3;
4805
- var MAX_BETWEEN_TURN_BUFFER = 500;
4806
- var SUBMIT_SETTLE_MS = 300;
4807
- var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
4808
- var SUBMIT_NUDGE_MAX_PRESSES = 5;
4809
- var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
4810
- var SUBMIT_NUDGE_WINDOW_MS = 9e4;
4811
- var PLAN_DIALOG_FIRST_PRESS_MS = 700;
4812
- var PLAN_DIALOG_INTERVAL_MS = 1500;
4813
- var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
4814
- var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
4815
- var PLAN_DIALOG_WINDOW_MS = 9e4;
4816
- function envMs(name, fallback) {
4817
- const raw = Number(process.env[name]);
4818
- return Number.isFinite(raw) && raw > 0 ? raw : fallback;
4819
- }
4820
- function resolveSubmitSettleMs() {
4821
- return envMs("CONVEYOR_PTY_SUBMIT_SETTLE_MS", SUBMIT_SETTLE_MS);
4822
- }
4823
- function resolveSubmitNudgeTiming() {
4824
- return {
4825
- intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
4826
- slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
4827
- maxPresses: SUBMIT_NUDGE_MAX_PRESSES,
4828
- windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
4829
- };
4830
- }
4831
- function resolvePlanDialogTiming() {
4832
- return {
4833
- firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
4834
- intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
4835
- slowIntervalMs: envMs(
4836
- "CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS",
4837
- PLAN_DIALOG_SLOW_INTERVAL_MS
4838
- ),
4839
- fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
4840
- windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
4841
- };
4842
- }
4843
- function turnOptionsFrom(options) {
4844
- return {
4845
- canUseTool: options.canUseTool,
4846
- promptDelivery: options.promptDelivery,
4847
- planDialogAutoAccept: options.planDialogAutoAccept,
4848
- abortController: options.abortController
4849
- };
4850
- }
4851
- function isRecord3(value) {
4852
- return typeof value === "object" && value !== null;
4853
- }
4854
- function extractSpawn(mod) {
4855
- if (!isRecord3(mod)) return null;
4856
- if (typeof mod.spawn === "function") return mod.spawn;
4857
- const def = mod.default;
4858
- if (isRecord3(def) && typeof def.spawn === "function") return def.spawn;
4859
- return null;
4860
- }
4861
- async function loadPtySpawn() {
4862
- const mod = await import("node-pty");
4863
- const spawn2 = extractSpawn(mod);
4864
- if (!spawn2) throw new Error("node-pty: spawn export not found");
4865
- return spawn2;
4866
- }
4867
- function inheritedEnv(socketPath) {
4868
- const env = {};
4869
- for (const [key, value] of Object.entries(process.env)) {
4870
- if (typeof value === "string") env[key] = value;
4871
- }
4872
- if (env.CLAUDE_CODE_OAUTH_TOKEN) {
4873
- delete env.ANTHROPIC_API_KEY;
4874
- }
4875
- if (socketPath) {
4876
- env.CONVEYOR_HOOK_SOCKET = socketPath;
4877
- }
4878
- env.MCP_TIMEOUT ??= "60000";
4879
- env.MCP_TOOL_TIMEOUT ??= "180000";
4880
- return env;
4881
- }
4882
- function buildPromptBytes(text) {
4883
- return `\x1B[200~${text}\x1B[201~`;
4884
- }
4885
- function renderPromptContentText(content) {
4886
- return content.map((block) => {
4887
- const b = block;
4888
- if (b?.type === "text" && typeof b.text === "string") return b.text;
4889
- if (b?.type === "image") {
4890
- return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
4891
- }
4892
- return JSON.stringify(block);
4893
- }).join("\n\n");
4894
- }
4895
- async function transcriptSize(path4) {
4896
- try {
4897
- return (await stat2(path4)).size;
4898
- } catch {
4899
- return 0;
4900
- }
4901
- }
4902
- function parseUserQuestions(input) {
4903
- if (!Array.isArray(input.questions)) return [];
4904
- const questions = [];
4905
- for (const entry of input.questions) {
4906
- if (!isRecord3(entry)) continue;
4907
- if (typeof entry.question !== "string") continue;
4908
- const options = Array.isArray(entry.options) ? entry.options.filter(isRecord3).filter((o) => typeof o.label === "string").map((o) => ({
4909
- label: o.label,
4910
- description: typeof o.description === "string" ? o.description : ""
4911
- })) : [];
4912
- questions.push({
4913
- question: entry.question,
4914
- header: typeof entry.header === "string" ? entry.header : "",
4915
- options,
4916
- ...typeof entry.multiSelect === "boolean" ? { multiSelect: entry.multiSelect } : {}
4917
- });
4918
- }
4919
- return questions;
4920
- }
4921
-
4922
4717
  // src/harness/pty/adapters/claude.ts
4923
4718
  var ClaudeTuiAdapter = class {
4924
4719
  id = "claude-code";
@@ -5029,6 +4824,18 @@ var PtySession = class {
5029
4824
  // Submit-nudge state (see armSubmitNudge).
5030
4825
  pendingSubmitNudge = false;
5031
4826
  submitNudgeTimer = null;
4827
+ // Synthetic AskUserQuestion chat-card state. The CLI does NOT flush the
4828
+ // assistant record holding a pending AskUserQuestion tool_use to the
4829
+ // transcript until the questionnaire resolves (verified live on CLI 2.1.209:
4830
+ // dialog parked on screen, transcript untouched) — so a transcript-derived
4831
+ // question card could only ever render AFTER the human answered in the raw
4832
+ // terminal. Instead the PreToolUse hook (which fires at ask time and carries
4833
+ // the full questions input) emits a synthetic `tool_use` chat event under an
4834
+ // `aq-…` id. When the real records eventually flush, the duplicate tool_use
4835
+ // is dropped (FIFO match below) and its tool_result is re-pointed at the
4836
+ // synthetic id so the card flips to answered.
4837
+ pendingSyntheticQuestionIds = [];
4838
+ questionResultRemap = /* @__PURE__ */ new Map();
5032
4839
  // Per-turn state: the prompt to feed and the per-turn options subset. Both
5033
4840
  // start from the constructor args (turn 1) and are replaced by beginTurn.
5034
4841
  turnPrompt;
@@ -5150,6 +4957,7 @@ var PtySession = class {
5150
4957
  this.passiveSignaled = false;
5151
4958
  this.disarmSubmitNudge();
5152
4959
  this.disarmPlanDialogAutoAccept();
4960
+ this.closeSyntheticQuestionCards();
5153
4961
  }
5154
4962
  /**
5155
4963
  * (Re)register the abort→teardown listener on the current turn's controller,
@@ -5181,6 +4989,7 @@ var PtySession = class {
5181
4989
  */
5182
4990
  endTurn(clean) {
5183
4991
  this.lastTurnCleanResult = clean;
4992
+ this.closeSyntheticQuestionCards();
5184
4993
  this.activeQueue?.close();
5185
4994
  this.activeQueue = null;
5186
4995
  if (this.abortHandler && this.turn.abortController) {
@@ -5205,7 +5014,7 @@ var PtySession = class {
5205
5014
  const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
5206
5015
  await this.spawn(settingsPath, socketPath);
5207
5016
  } else {
5208
- this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
5017
+ this.tempDir = await mkdtemp2(join5(sessionTempBase(), "conveyor-pty-"));
5209
5018
  await this.spawn();
5210
5019
  this.pushEvent({
5211
5020
  type: "system",
@@ -5242,8 +5051,8 @@ var PtySession = class {
5242
5051
  * paths spawn() must wire into the child's argv/env.
5243
5052
  */
5244
5053
  async startStructuredEventSources(sessionId) {
5245
- this.tempDir = await mkdtemp2(join4(tmpdir3(), "conveyor-pty-"));
5246
- const socketPath = join4(this.tempDir, "hook.sock");
5054
+ this.tempDir = await mkdtemp2(join5(sessionTempBase(), "conveyor-pty-"));
5055
+ const socketPath = join5(this.tempDir, "hook.sock");
5247
5056
  this.socket = new HookSocketServer(
5248
5057
  socketPath,
5249
5058
  (progress) => this.handleProgress(progress),
@@ -5255,17 +5064,72 @@ var PtySession = class {
5255
5064
  const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
5256
5065
  await mkdir3(dirname(transcriptPath), { recursive: true });
5257
5066
  const startOffset = this.resume ? await transcriptSize(transcriptPath) : 0;
5258
- const sendChat = this.bridge?.sendChatEvent?.bind(this.bridge);
5259
5067
  this.tailer = new JsonlTailer(
5260
5068
  transcriptPath,
5261
5069
  (event) => this.handleTranscriptEvent(event),
5262
- sendChat ? (raw) => {
5263
- for (const chatEvent of mapChatRecords(raw)) sendChat(chatEvent);
5264
- } : void 0
5070
+ typeof this.bridge?.sendChatEvent === "function" ? (raw) => this.relayChatRecord(raw) : void 0
5265
5071
  );
5266
5072
  this.tailer.start(startOffset);
5267
5073
  return { settingsPath, socketPath };
5268
5074
  }
5075
+ /**
5076
+ * Project a tailed transcript record to chat events, reconciling them with
5077
+ * any synthetic question card already emitted at hook time: the flushed
5078
+ * AskUserQuestion `tool_use` duplicate is dropped (its real id remembered),
5079
+ * and the paired `tool_result` is re-pointed at the synthetic id so the
5080
+ * live-rendered card is the one that flips to answered.
5081
+ */
5082
+ relayChatRecord(raw) {
5083
+ for (const event of mapChatRecords(raw)) {
5084
+ if (event.kind === "tool_use" && event.name === "AskUserQuestion") {
5085
+ const syntheticId = this.pendingSyntheticQuestionIds.shift();
5086
+ if (syntheticId) {
5087
+ if (event.id) this.questionResultRemap.set(event.id, syntheticId);
5088
+ continue;
5089
+ }
5090
+ } else if (event.kind === "tool_result" && event.toolUseId) {
5091
+ const syntheticId = this.questionResultRemap.get(event.toolUseId);
5092
+ if (syntheticId) {
5093
+ this.questionResultRemap.delete(event.toolUseId);
5094
+ this.sendChatEvent({ ...event, toolUseId: syntheticId });
5095
+ continue;
5096
+ }
5097
+ }
5098
+ this.sendChatEvent(event);
5099
+ }
5100
+ }
5101
+ sendChatEvent(event) {
5102
+ this.bridge?.sendChatEvent?.(event);
5103
+ }
5104
+ /** Render the question card in the web chat NOW — at hook time — instead of
5105
+ * whenever the CLI flushes the transcript records (which is only after the
5106
+ * questionnaire resolves; see the field comment). */
5107
+ emitSyntheticQuestionCard(questions) {
5108
+ if (questions.length === 0 || typeof this.bridge?.sendChatEvent !== "function") return;
5109
+ const id = `aq-${randomUUID2()}`;
5110
+ this.pendingSyntheticQuestionIds.push(id);
5111
+ this.sendChatEvent({
5112
+ kind: "tool_use",
5113
+ name: "AskUserQuestion",
5114
+ input: compactQuestionsJson(questions),
5115
+ id
5116
+ });
5117
+ }
5118
+ /**
5119
+ * Close any still-open synthetic question cards. The questionnaire can only
5120
+ * outlive its card via a path that never flushes the paired records — Esc /
5121
+ * interrupt, a superseding turn, or process teardown — so an answering
5122
+ * tool_result will never arrive for these ids; emit one so the web card
5123
+ * stops soliciting input for a dialog that no longer exists.
5124
+ */
5125
+ closeSyntheticQuestionCards() {
5126
+ const orphaned = [...this.pendingSyntheticQuestionIds, ...this.questionResultRemap.values()];
5127
+ this.pendingSyntheticQuestionIds = [];
5128
+ this.questionResultRemap.clear();
5129
+ for (const toolUseId of orphaned) {
5130
+ this.sendChatEvent({ kind: "tool_result", toolUseId, output: "", isError: false });
5131
+ }
5132
+ }
5269
5133
  writeStdin(text) {
5270
5134
  this.pty?.write(text);
5271
5135
  }
@@ -5299,6 +5163,7 @@ var PtySession = class {
5299
5163
  this._toreDown = true;
5300
5164
  this.disarmPlanDialogAutoAccept();
5301
5165
  this.disarmSubmitNudge();
5166
+ this.closeSyntheticQuestionCards();
5302
5167
  this.unsubInput?.();
5303
5168
  this.unsubInput = null;
5304
5169
  this.unsubResize?.();
@@ -5331,7 +5196,7 @@ var PtySession = class {
5331
5196
  this.activeQueue?.close();
5332
5197
  this.activeQueue = null;
5333
5198
  if (this.tempDir) {
5334
- await rm5(this.tempDir, { recursive: true, force: true });
5199
+ await rm4(this.tempDir, { recursive: true, force: true });
5335
5200
  this.tempDir = "";
5336
5201
  }
5337
5202
  }
@@ -5360,8 +5225,8 @@ var PtySession = class {
5360
5225
  // server doesn't load).
5361
5226
  ...this.mcpConfigPath ? { mcpConfigPath: this.mcpConfigPath } : {}
5362
5227
  });
5363
- const spawn2 = await loadPtySpawn();
5364
- const pty = spawn2(spec.file, spec.args, {
5228
+ const spawn = await resolvePtySpawn();
5229
+ const pty = spawn(spec.file, spec.args, {
5365
5230
  name: "xterm-color",
5366
5231
  cols: this.cols,
5367
5232
  rows: this.rows,
@@ -5469,10 +5334,9 @@ var PtySession = class {
5469
5334
  if (request.tool_name === "AskUserQuestion") {
5470
5335
  this.disarmSubmitNudge();
5471
5336
  this.disarmPlanDialogAutoAccept();
5472
- this.pushEvent({
5473
- type: "user_question",
5474
- questions: parseUserQuestions(request.tool_input)
5475
- });
5337
+ const questions = parseUserQuestions(request.tool_input);
5338
+ this.pushEvent({ type: "user_question", questions });
5339
+ this.emitSyntheticQuestionCard(questions);
5476
5340
  return { decision: "allow" };
5477
5341
  }
5478
5342
  const canUseTool = this.turn.canUseTool;
@@ -5608,7 +5472,7 @@ var PtySession = class {
5608
5472
  // src/harness/pty/config-home-health.ts
5609
5473
  import { mkdir as mkdir4 } from "fs/promises";
5610
5474
  import { homedir as homedir3 } from "os";
5611
- import { join as join5 } from "path";
5475
+ import { join as join6 } from "path";
5612
5476
  var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
5613
5477
  var MOUNT_DISCONNECT_MESSAGES = [
5614
5478
  "socket is not connected",
@@ -5624,12 +5488,12 @@ function isMountDisconnectError(err) {
5624
5488
  return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
5625
5489
  }
5626
5490
  function podLocalConfigHome() {
5627
- return join5(homedir3(), ".claude-local");
5491
+ return join6(homedir3(), ".claude-local");
5628
5492
  }
5629
5493
  async function ensureUsableClaudeConfigHome(cwd, log) {
5630
5494
  const configHome = claudeConfigHome();
5631
5495
  try {
5632
- await mkdir4(join5(configHome, "projects", projectSlug(cwd)), { recursive: true });
5496
+ await mkdir4(join6(configHome, "projects", projectSlug(cwd)), { recursive: true });
5633
5497
  return { configHome, fellBack: false };
5634
5498
  } catch (err) {
5635
5499
  if (!isMountDisconnectError(err)) throw err;
@@ -5643,7 +5507,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
5643
5507
  }
5644
5508
  );
5645
5509
  process.env.CLAUDE_CONFIG_DIR = fallback;
5646
- await mkdir4(join5(fallback, "projects", projectSlug(cwd)), { recursive: true });
5510
+ await mkdir4(join6(fallback, "projects", projectSlug(cwd)), { recursive: true });
5647
5511
  return { configHome: fallback, fellBack: true };
5648
5512
  }
5649
5513
  }
@@ -6089,8 +5953,38 @@ function formatIncidents(incidents) {
6089
5953
  return parts;
6090
5954
  }
6091
5955
 
5956
+ // src/workbench/fs.ts
5957
+ import { readFile as localReadFile, readdir as localReaddir, stat as localStat } from "fs/promises";
5958
+ async function readWorkspaceFile(path3) {
5959
+ if (workbenchEnabled()) {
5960
+ return (await getWorkbenchClient().readFile(path3)).toString("utf8");
5961
+ }
5962
+ return localReadFile(path3, "utf-8");
5963
+ }
5964
+ function readWorkspaceDir(path3) {
5965
+ if (workbenchEnabled()) return getWorkbenchClient().readdir(path3);
5966
+ return localReaddir(path3);
5967
+ }
5968
+ async function statWorkspacePath(path3) {
5969
+ if (workbenchEnabled()) return getWorkbenchClient().stat(path3);
5970
+ try {
5971
+ const s = await localStat(path3);
5972
+ return {
5973
+ exists: true,
5974
+ isFile: s.isFile(),
5975
+ isDirectory: s.isDirectory(),
5976
+ size: s.size,
5977
+ mtimeMs: s.mtimeMs
5978
+ };
5979
+ } catch {
5980
+ return { exists: false, isFile: false, isDirectory: false, size: 0, mtimeMs: 0 };
5981
+ }
5982
+ }
5983
+ async function workspacePathExists(path3) {
5984
+ return (await statWorkspacePath(path3)).exists;
5985
+ }
5986
+
6092
5987
  // src/execution/tag-context-resolver.ts
6093
- import { readFile as readFile2, readdir, stat as stat3 } from "fs/promises";
6094
5988
  var TYPE_PRIORITY = { rule: 0, file: 1, folder: 2, doc: 3 };
6095
5989
  var SUMMARY_SCAN_CHARS = 4e3;
6096
5990
  var SUMMARY_MAX_CHARS = 160;
@@ -6162,18 +6056,14 @@ function truncateSummary(text) {
6162
6056
  async function readFileSummary(filePath) {
6163
6057
  try {
6164
6058
  if (isBinaryPath(filePath)) return null;
6165
- let mtimeMs;
6166
- try {
6167
- const st = await stat3(filePath);
6168
- mtimeMs = st.mtimeMs;
6169
- } catch {
6170
- return null;
6171
- }
6059
+ const st = await statWorkspacePath(filePath);
6060
+ if (!st.exists) return null;
6061
+ const mtimeMs = st.mtimeMs;
6172
6062
  const cached = fileSummaryCache.get(filePath);
6173
6063
  if (cached && cached.mtimeMs === mtimeMs) {
6174
6064
  return cached.summary;
6175
6065
  }
6176
- const raw = await readFile2(filePath, "utf-8");
6066
+ const raw = await readWorkspaceFile(filePath);
6177
6067
  fileReadCount++;
6178
6068
  const summary = deriveSummary(raw.slice(0, SUMMARY_SCAN_CHARS));
6179
6069
  fileSummaryCache.set(filePath, { mtimeMs, summary });
@@ -6184,18 +6074,14 @@ async function readFileSummary(filePath) {
6184
6074
  }
6185
6075
  async function readFolderListing(folderPath) {
6186
6076
  try {
6187
- let mtimeMs;
6188
- try {
6189
- const st = await stat3(folderPath);
6190
- mtimeMs = st.mtimeMs;
6191
- } catch {
6192
- return null;
6193
- }
6077
+ const st = await statWorkspacePath(folderPath);
6078
+ if (!st.exists) return null;
6079
+ const mtimeMs = st.mtimeMs;
6194
6080
  const cached = folderListingCache.get(folderPath);
6195
6081
  if (cached && cached.mtimeMs === mtimeMs) {
6196
6082
  return cached.listing;
6197
6083
  }
6198
- const entries = await readdir(folderPath);
6084
+ const entries = await readWorkspaceDir(folderPath);
6199
6085
  folderReadCount++;
6200
6086
  const listing = `Files: ${entries.join(", ")}`;
6201
6087
  folderListingCache.set(folderPath, { mtimeMs, listing });
@@ -7582,7 +7468,16 @@ function buildForceUpdateTaskStatusTool(connection) {
7582
7468
  "force_update_task_status",
7583
7469
  "EMERGENCY ONLY: force-override a task's Kanban status. Use when an automatic transition failed and the task is wedged. Normal flow transitions status automatically.",
7584
7470
  {
7585
- status: z9.enum(["InProgress", "ReviewPR", "ReviewDev", "Complete"]).describe("The new status for the task"),
7471
+ status: z9.enum([
7472
+ "Planning",
7473
+ "Open",
7474
+ "InProgress",
7475
+ "ReviewPR",
7476
+ "ReviewDev",
7477
+ "ReviewLive",
7478
+ "Complete",
7479
+ "Cancelled"
7480
+ ]).describe("The new status for the task"),
7586
7481
  task_id: z9.string().optional().describe("Child task ID to update. Omit to update the current task.")
7587
7482
  },
7588
7483
  async ({ status, task_id }) => {
@@ -7846,8 +7741,8 @@ function buildMutationTools(connection, config) {
7846
7741
  }
7847
7742
 
7848
7743
  // src/tools/attachment-tools.ts
7849
- import { readFile as readFile3, stat as stat4 } from "fs/promises";
7850
- import { basename, extname, isAbsolute, join as join6 } from "path";
7744
+ import { readFile as readFile2, stat } from "fs/promises";
7745
+ import { basename, extname, isAbsolute, join as join7 } from "path";
7851
7746
  import { z as z10 } from "zod";
7852
7747
  var IMAGE_MIME_BY_EXT = {
7853
7748
  ".png": "image/png",
@@ -7864,16 +7759,16 @@ function buildUploadAttachmentTool(connection, config) {
7864
7759
  path: z10.string().describe("Path to the image file \u2014 absolute, or relative to the workspace root"),
7865
7760
  title: z10.string().optional().describe("Short caption posted with the image (defaults to the file name)")
7866
7761
  },
7867
- async ({ path: path4, title }) => {
7762
+ async ({ path: path3, title }) => {
7868
7763
  try {
7869
- const filePath = isAbsolute(path4) ? path4 : join6(config.workspaceDir, path4);
7764
+ const filePath = isAbsolute(path3) ? path3 : join7(config.workspaceDir, path3);
7870
7765
  const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
7871
7766
  if (!mimeType) {
7872
7767
  return textResult(
7873
7768
  `Unsupported file type "${extname(filePath) || "(none)"}". Supported: ${Object.keys(IMAGE_MIME_BY_EXT).join(", ")}`
7874
7769
  );
7875
7770
  }
7876
- const info = await stat4(filePath).catch(() => null);
7771
+ const info = await stat(filePath).catch(() => null);
7877
7772
  if (!info?.isFile()) {
7878
7773
  return textResult(`File not found: ${filePath}`);
7879
7774
  }
@@ -7889,7 +7784,7 @@ function buildUploadAttachmentTool(connection, config) {
7889
7784
  mimeType,
7890
7785
  fileSize: info.size
7891
7786
  });
7892
- const body = await readFile3(filePath);
7787
+ const body = await readFile2(filePath);
7893
7788
  const res = await fetch(uploadUrl, {
7894
7789
  method: "PUT",
7895
7790
  headers: { "Content-Type": mimeType },
@@ -8399,42 +8294,52 @@ function buildPmTools(connection, options) {
8399
8294
  // src/tools/discovery-tools.ts
8400
8295
  import { z as z13 } from "zod";
8401
8296
  var SP_DESCRIPTION2 = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique). The key is 'storyPointValue' \u2014 not 'storyPoints'.";
8402
- var VALID_PROPERTY_KEYS = "title, storyPointValue, tagNames, githubPRUrl, githubBranch";
8297
+ var VALID_PROPERTY_KEYS = "title, storyPointValue, tagNames, githubPRUrl, githubBranch, risk";
8298
+ function describeUpdatedFields(p) {
8299
+ const fields = [];
8300
+ if (p.title !== void 0) fields.push(`title to "${p.title}"`);
8301
+ if (p.storyPointValue !== void 0) fields.push(`story points to ${p.storyPointValue}`);
8302
+ if (p.tagNames !== void 0) fields.push(`tags (${p.tagNames.length} tag(s))`);
8303
+ if (p.githubPRUrl !== void 0) fields.push(`PR link to "${p.githubPRUrl}"`);
8304
+ if (p.githubBranch !== void 0) fields.push(`branch to "${p.githubBranch}"`);
8305
+ if (p.risk !== void 0) fields.push(`risk to ${p.risk ?? "cleared"}`);
8306
+ return fields;
8307
+ }
8403
8308
  function buildDiscoveryTools(connection) {
8404
8309
  return [
8405
8310
  defineTool(
8406
8311
  "update_task_properties",
8407
- "Set one or more task properties in a single call. Valid keys: title, storyPointValue, tagNames, githubPRUrl, githubBranch. All are optional \u2014 include only the ones you want to update (at least one). Unknown keys are rejected.",
8312
+ "Set one or more task properties in a single call. Valid keys: title, storyPointValue, tagNames, githubPRUrl, githubBranch, risk. All are optional \u2014 include only the ones you want to update (at least one). Unknown keys are rejected.",
8408
8313
  {
8409
8314
  title: z13.string().optional().describe("The new task title"),
8410
8315
  storyPointValue: z13.number().optional().describe(SP_DESCRIPTION2),
8411
8316
  tagNames: z13.array(z13.string()).optional().describe("Array of tag names to assign"),
8412
8317
  githubPRUrl: z13.string().url().optional().describe("GitHub pull request URL to link to this task"),
8413
- githubBranch: z13.string().optional().describe("Set the GitHub branch name for this task (e.g. 'conveyor/my-feature-abc123')")
8318
+ githubBranch: z13.string().optional().describe("Set the GitHub branch name for this task (e.g. 'conveyor/my-feature-abc123')"),
8319
+ risk: z13.enum(["critical", "high", "medium", "low"]).nullable().optional().describe(
8320
+ "Risk level \u2014 how much important surface the task touches (critical/high/medium/low). Pass null to clear."
8321
+ )
8414
8322
  },
8415
- async ({ title, storyPointValue, tagNames, githubPRUrl, githubBranch }) => {
8323
+ async ({ title, storyPointValue, tagNames, githubPRUrl, githubBranch, risk }) => {
8416
8324
  try {
8417
- const nothingToUpdate = title === void 0 && storyPointValue === void 0 && tagNames === void 0 && githubPRUrl === void 0 && githubBranch === void 0;
8418
- if (nothingToUpdate) {
8325
+ const params = {
8326
+ title,
8327
+ storyPointValue,
8328
+ tagNames,
8329
+ githubPRUrl,
8330
+ githubBranch,
8331
+ risk
8332
+ };
8333
+ const updatedFields = describeUpdatedFields(params);
8334
+ if (updatedFields.length === 0) {
8419
8335
  return textResult(
8420
8336
  `No task properties were updated: none of the recognized keys were provided. Valid keys: ${VALID_PROPERTY_KEYS}. (Story points are set via 'storyPointValue', not 'storyPoints'.)`
8421
8337
  );
8422
8338
  }
8423
8339
  await connection.call("updateTaskProperties", {
8424
8340
  sessionId: connection.sessionId,
8425
- title,
8426
- storyPointValue,
8427
- tagNames,
8428
- githubPRUrl,
8429
- githubBranch
8341
+ ...params
8430
8342
  });
8431
- const updatedFields = [];
8432
- if (title !== void 0) updatedFields.push(`title to "${title}"`);
8433
- if (storyPointValue !== void 0)
8434
- updatedFields.push(`story points to ${storyPointValue}`);
8435
- if (tagNames !== void 0) updatedFields.push(`tags (${tagNames.length} tag(s))`);
8436
- if (githubPRUrl !== void 0) updatedFields.push(`PR link to "${githubPRUrl}"`);
8437
- if (githubBranch !== void 0) updatedFields.push(`branch to "${githubBranch}"`);
8438
8343
  return textResult(`Task properties updated: ${updatedFields.join(", ")}`);
8439
8344
  } catch (error) {
8440
8345
  return textResult(
@@ -8618,7 +8523,7 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
8618
8523
 
8619
8524
  // src/harness/pty/adapters/types.ts
8620
8525
  import { accessSync, constants, statSync } from "fs";
8621
- import { join as join7 } from "path";
8526
+ import { join as join8 } from "path";
8622
8527
  var TuiUnavailableError = class extends Error {
8623
8528
  constructor(tui, message) {
8624
8529
  super(message);
@@ -8627,10 +8532,10 @@ var TuiUnavailableError = class extends Error {
8627
8532
  }
8628
8533
  tui;
8629
8534
  };
8630
- function isExecutable(path4) {
8535
+ function isExecutable(path3) {
8631
8536
  try {
8632
- if (!statSync(path4).isFile()) return false;
8633
- accessSync(path4, constants.X_OK);
8537
+ if (!statSync(path3).isFile()) return false;
8538
+ accessSync(path3, constants.X_OK);
8634
8539
  return true;
8635
8540
  } catch {
8636
8541
  return false;
@@ -8642,7 +8547,7 @@ function findOnPath(binary, env = process.env) {
8642
8547
  }
8643
8548
  for (const dir of (env.PATH ?? "").split(":")) {
8644
8549
  if (!dir) continue;
8645
- const candidate = join7(dir, binary);
8550
+ const candidate = join8(dir, binary);
8646
8551
  if (isExecutable(candidate)) return candidate;
8647
8552
  }
8648
8553
  return null;
@@ -8974,14 +8879,14 @@ function flushPendingToolCalls(host, turnToolCalls) {
8974
8879
  }
8975
8880
  const outputsByTool = /* @__PURE__ */ new Map();
8976
8881
  for (const entry of host.pendingToolOutputs) {
8977
- const list2 = outputsByTool.get(entry.tool) ?? [];
8978
- list2.push(entry.output);
8979
- outputsByTool.set(entry.tool, list2);
8882
+ const list = outputsByTool.get(entry.tool) ?? [];
8883
+ list.push(entry.output);
8884
+ outputsByTool.set(entry.tool, list);
8980
8885
  }
8981
8886
  for (const call of turnToolCalls) {
8982
- const list2 = outputsByTool.get(call.tool);
8983
- if (list2 && list2.length > 0) {
8984
- call.output = list2.shift();
8887
+ const list = outputsByTool.get(call.tool);
8888
+ if (list && list.length > 0) {
8889
+ call.output = list.shift();
8985
8890
  }
8986
8891
  }
8987
8892
  host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
@@ -9506,10 +9411,10 @@ function resolveSessionStart(lineageKey, cwd) {
9506
9411
  }
9507
9412
  return { sessionId: sessionUuid };
9508
9413
  }
9509
- function repairTornSessionFile(path4) {
9414
+ function repairTornSessionFile(path3) {
9510
9415
  try {
9511
- if (!existsSync2(path4)) return false;
9512
- const content = readFileSync(path4, "utf8");
9416
+ if (!existsSync2(path3)) return false;
9417
+ const content = readFileSync(path3, "utf8");
9513
9418
  if (content.length === 0) return false;
9514
9419
  let keepEnd = content.length;
9515
9420
  if (!content.endsWith("\n")) {
@@ -9528,9 +9433,9 @@ function repairTornSessionFile(path4) {
9528
9433
  keepEnd = prevNewline + 1;
9529
9434
  }
9530
9435
  if (keepEnd === content.length) return false;
9531
- truncateSync(path4, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
9436
+ truncateSync(path3, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
9532
9437
  logger2.warn("Repaired torn transcript before resume", {
9533
- path: path4,
9438
+ path: path3,
9534
9439
  trimmedBytes: content.length - keepEnd
9535
9440
  });
9536
9441
  return true;
@@ -10328,7 +10233,7 @@ var QueryBridge = class {
10328
10233
 
10329
10234
  // src/runner/session-runner-helpers.ts
10330
10235
  import { readFileSync as readFileSync2 } from "fs";
10331
- import { dirname as dirname2, join as join8 } from "path";
10236
+ import { dirname as dirname2, join as join9 } from "path";
10332
10237
  import { fileURLToPath as fileURLToPath2 } from "url";
10333
10238
  function mapChatHistory(messages) {
10334
10239
  if (!messages) return [];
@@ -10357,7 +10262,7 @@ function readAgentVersion() {
10357
10262
  const here = dirname2(fileURLToPath2(import.meta.url));
10358
10263
  for (const rel of ["../package.json", "../../package.json"]) {
10359
10264
  try {
10360
- const pkg = JSON.parse(readFileSync2(join8(here, rel), "utf-8"));
10265
+ const pkg = JSON.parse(readFileSync2(join9(here, rel), "utf-8"));
10361
10266
  if (pkg.version) return pkg.version;
10362
10267
  } catch {
10363
10268
  }
@@ -10563,10 +10468,10 @@ var UsageProbeRun = class {
10563
10468
  }
10564
10469
  };
10565
10470
  async function runUsageProbe(deps = {}) {
10566
- let spawn2 = deps.spawn;
10567
- if (!spawn2) {
10471
+ let spawn = deps.spawn;
10472
+ if (!spawn) {
10568
10473
  try {
10569
- spawn2 = await loadPtySpawn();
10474
+ spawn = await loadPtySpawn();
10570
10475
  } catch {
10571
10476
  return "";
10572
10477
  }
@@ -10582,7 +10487,7 @@ async function runUsageProbe(deps = {}) {
10582
10487
  return new Promise((resolve) => {
10583
10488
  let child;
10584
10489
  try {
10585
- child = spawn2(binary, [], {
10490
+ child = spawn(binary, [], {
10586
10491
  name: "xterm-256color",
10587
10492
  cols: 120,
10588
10493
  rows: 45,
@@ -10656,14 +10561,14 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
10656
10561
  }
10657
10562
 
10658
10563
  // src/setup/git-ready.ts
10659
- import { access, stat as stat5 } from "fs/promises";
10564
+ import { access, stat as stat2 } from "fs/promises";
10660
10565
  var DEFAULT_FAILED_PATH = "/workspaces/.conveyor-git-failed";
10661
10566
  var DEFAULT_TIMEOUT_MS = 6e5;
10662
10567
  var DEFAULT_POLL_MS = 200;
10663
- async function fileExists(path4) {
10568
+ async function fileExists(path3) {
10664
10569
  try {
10665
- await access(path4);
10666
- const s = await stat5(path4);
10570
+ await access(path3);
10571
+ const s = await stat2(path3);
10667
10572
  return s.isFile();
10668
10573
  } catch {
10669
10574
  return false;
@@ -10726,8 +10631,8 @@ async function pollForMarkers(markerPath, failedPath, timeoutMs, pollMs, onLog,
10726
10631
  }
10727
10632
 
10728
10633
  // src/runner/port-discovery.ts
10729
- import { readFile as readFile4 } from "fs/promises";
10730
- import { execFile as execFile3 } from "child_process";
10634
+ import { readFile as readFile3 } from "fs/promises";
10635
+ import { execFile as execFile2 } from "child_process";
10731
10636
  var PROC_TCP_LISTEN_STATE = "0A";
10732
10637
  function isLoopbackHexAddress(hex) {
10733
10638
  const addr = hex.toUpperCase();
@@ -10778,9 +10683,9 @@ var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
10778
10683
  async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
10779
10684
  const sockets = [];
10780
10685
  let readable = false;
10781
- for (const path4 of procPaths) {
10686
+ for (const path3 of procPaths) {
10782
10687
  try {
10783
- const content = await readFile4(path4, "utf8");
10688
+ const content = await readFile3(path3, "utf8");
10784
10689
  readable = true;
10785
10690
  sockets.push(...parseProcNetTcpListeners(content));
10786
10691
  } catch {
@@ -10790,7 +10695,7 @@ async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
10790
10695
  }
10791
10696
  async function readNetstatListeningPorts() {
10792
10697
  const output = await new Promise((resolve) => {
10793
- execFile3("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
10698
+ execFile2("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
10794
10699
  resolve(err ? null : stdout);
10795
10700
  });
10796
10701
  });
@@ -10963,9 +10868,9 @@ var PortDiscovery = class {
10963
10868
  };
10964
10869
 
10965
10870
  // src/runner/parent-pull-handler.ts
10966
- import { execFile as execFile4 } from "child_process";
10967
- import { promisify as promisify3 } from "util";
10968
- var execFileAsync3 = promisify3(execFile4);
10871
+ import { execFile as execFile3 } from "child_process";
10872
+ import { promisify as promisify2 } from "util";
10873
+ var execFileAsync2 = promisify2(execFile3);
10969
10874
  async function handlePullBranch(workDir, branch) {
10970
10875
  if (!branch) return;
10971
10876
  const current = await getCurrentBranch(workDir);
@@ -10984,14 +10889,14 @@ async function handlePullBranch(workDir, branch) {
10984
10889
  return;
10985
10890
  }
10986
10891
  try {
10987
- await execFileAsync3("git", ["fetch", "origin", branch], { cwd: workDir, timeout: 6e4 });
10892
+ await execFileAsync2("git", ["fetch", "origin", branch], { cwd: workDir, timeout: 6e4 });
10988
10893
  } catch {
10989
10894
  process.stderr.write(`[conveyor-agent] pull_branch: fetch failed for ${branch}
10990
10895
  `);
10991
10896
  return;
10992
10897
  }
10993
10898
  try {
10994
- await execFileAsync3("git", ["pull", "--ff-only", "origin", branch], {
10899
+ await execFileAsync2("git", ["pull", "--ff-only", "origin", branch], {
10995
10900
  cwd: workDir,
10996
10901
  timeout: 6e4
10997
10902
  });
@@ -11007,7 +10912,7 @@ async function handlePullBranch(workDir, branch) {
11007
10912
 
11008
10913
  // src/runner/heavy-gate.ts
11009
10914
  import { readFileSync as readFileSync3 } from "fs";
11010
- import path3 from "path";
10915
+ import path2 from "path";
11011
10916
  var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
11012
10917
  function runDir() {
11013
10918
  return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
@@ -11023,7 +10928,7 @@ function pidAlive(pid) {
11023
10928
  function isHeavyGateActive() {
11024
10929
  for (const key of GATE_KEYS) {
11025
10930
  try {
11026
- const raw = readFileSync3(path3.join(runDir(), `${key}.pid`), "utf8").trim();
10931
+ const raw = readFileSync3(path2.join(runDir(), `${key}.pid`), "utf8").trim();
11027
10932
  const pid = Number.parseInt(raw, 10);
11028
10933
  if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
11029
10934
  } catch {
@@ -11924,13 +11829,12 @@ var SessionRunner = class _SessionRunner {
11924
11829
  };
11925
11830
 
11926
11831
  // src/setup/config.ts
11927
- import { readFile as readFile5 } from "fs/promises";
11928
- import { join as join9 } from "path";
11832
+ import { join as join10 } from "path";
11929
11833
  var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
11930
11834
  var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
11931
11835
  async function loadForwardPorts(workspaceDir) {
11932
11836
  try {
11933
- const raw = await readFile5(join9(workspaceDir, DEVCONTAINER_PATH), "utf-8");
11837
+ const raw = await readWorkspaceFile(join10(workspaceDir, DEVCONTAINER_PATH));
11934
11838
  const parsed = JSON.parse(raw);
11935
11839
  const ports = (parsed.forwardPorts ?? []).filter(
11936
11840
  (p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
@@ -11971,125 +11875,11 @@ function loadConveyorConfig() {
11971
11875
  return null;
11972
11876
  }
11973
11877
 
11974
- // src/setup/commands.ts
11975
- import { spawn, execSync } from "child_process";
11976
- var PROCESS_TERMINATION_GRACE_MS = 5e3;
11977
- function abortError2() {
11978
- const error = new Error("Operation aborted");
11979
- error.name = "AbortError";
11980
- return error;
11981
- }
11982
- function signalProcessGroup(child, signal) {
11983
- try {
11984
- if (child.pid) process.kill(-child.pid, signal);
11985
- else child.kill(signal);
11986
- } catch {
11987
- try {
11988
- child.kill(signal);
11989
- } catch {
11990
- }
11991
- }
11992
- }
11993
- function terminateProcessGroup(child, graceMs = PROCESS_TERMINATION_GRACE_MS) {
11994
- if (child.exitCode !== null) return Promise.resolve();
11995
- return new Promise((resolve) => {
11996
- let settled = false;
11997
- const finish = () => {
11998
- if (settled) return;
11999
- settled = true;
12000
- clearTimeout(timer);
12001
- child.removeListener("exit", finish);
12002
- resolve();
12003
- };
12004
- const timer = setTimeout(() => {
12005
- signalProcessGroup(child, "SIGKILL");
12006
- finish();
12007
- }, graceMs);
12008
- timer.unref();
12009
- child.once("exit", finish);
12010
- signalProcessGroup(child, "SIGTERM");
12011
- });
12012
- }
12013
- function runSetupCommand(cmd, cwd, onOutput, signal) {
12014
- if (signal?.aborted) return Promise.reject(abortError2());
12015
- return new Promise((resolve, reject) => {
12016
- const child = spawn("sh", ["-c", cmd], {
12017
- cwd,
12018
- stdio: ["ignore", "pipe", "pipe"],
12019
- detached: true,
12020
- env: { ...process.env }
12021
- });
12022
- let settled = false;
12023
- let aborting = false;
12024
- const cleanup = () => signal?.removeEventListener("abort", onAbort);
12025
- const settle = (error) => {
12026
- if (settled) return;
12027
- settled = true;
12028
- cleanup();
12029
- if (error) reject(error);
12030
- else resolve();
12031
- };
12032
- const onAbort = () => {
12033
- if (settled || aborting) return;
12034
- aborting = true;
12035
- void terminateProcessGroup(child).then(() => settle(abortError2()));
12036
- };
12037
- signal?.addEventListener("abort", onAbort, { once: true });
12038
- if (signal?.aborted) onAbort();
12039
- child.stdout.on("data", (chunk) => {
12040
- if (aborting || signal?.aborted) return;
12041
- onOutput("stdout", chunk.toString());
12042
- });
12043
- child.stderr.on("data", (chunk) => {
12044
- if (aborting || signal?.aborted) return;
12045
- onOutput("stderr", chunk.toString());
12046
- });
12047
- child.on("close", (code) => {
12048
- if (aborting) return;
12049
- settle(code === 0 ? void 0 : new Error(`Setup command exited with code ${code}`));
12050
- });
12051
- child.on("error", (err) => {
12052
- if (!aborting) settle(err);
12053
- });
12054
- });
12055
- }
12056
- var AUTH_TOKEN_TIMEOUT_MS = 3e4;
12057
- function runAuthTokenCommand(cmd, userEmail, cwd) {
12058
- try {
12059
- const output = execSync(`${cmd} ${JSON.stringify(userEmail)}`, {
12060
- cwd,
12061
- timeout: AUTH_TOKEN_TIMEOUT_MS,
12062
- stdio: ["ignore", "pipe", "ignore"],
12063
- env: { ...process.env }
12064
- });
12065
- const token = output.toString().trim();
12066
- return token || null;
12067
- } catch {
12068
- return null;
12069
- }
12070
- }
12071
- function runStartCommand(cmd, cwd, onOutput) {
12072
- const child = spawn("sh", ["-c", cmd], {
12073
- cwd,
12074
- stdio: ["ignore", "pipe", "pipe"],
12075
- detached: true,
12076
- env: { ...process.env }
12077
- });
12078
- child.stdout.on("data", (chunk) => {
12079
- onOutput("stdout", chunk.toString());
12080
- });
12081
- child.stderr.on("data", (chunk) => {
12082
- onOutput("stderr", chunk.toString());
12083
- });
12084
- child.unref();
12085
- return child;
12086
- }
12087
-
12088
11878
  // src/setup/codespace.ts
12089
- import { execSync as execSync2 } from "child_process";
11879
+ import { execSync } from "child_process";
12090
11880
  function unshallowRepo(workspaceDir) {
12091
11881
  try {
12092
- execSync2("git fetch --unshallow", {
11882
+ execSync("git fetch --unshallow", {
12093
11883
  cwd: workspaceDir,
12094
11884
  timeout: 6e4,
12095
11885
  stdio: "ignore"
@@ -12110,12 +11900,10 @@ export {
12110
11900
  cleanTerminalOutput,
12111
11901
  buildSynthesizedCredentials,
12112
11902
  claudeJsonPath,
12113
- loadPtySpawn,
12114
- inheritedEnv,
12115
- buildPromptBytes,
12116
11903
  ClaudeTuiAdapter,
12117
11904
  createServiceLogger,
12118
11905
  PtyHarness,
11906
+ workspacePathExists,
12119
11907
  textResult,
12120
11908
  GIT_TIMEOUT_MS,
12121
11909
  hasUncommittedChanges,
@@ -12144,10 +11932,6 @@ export {
12144
11932
  loadForwardPorts,
12145
11933
  buildSessionPreviewPorts,
12146
11934
  loadConveyorConfig,
12147
- terminateProcessGroup,
12148
- runSetupCommand,
12149
- runAuthTokenCommand,
12150
- runStartCommand,
12151
11935
  unshallowRepo
12152
11936
  };
12153
- //# sourceMappingURL=chunk-AJZIO5QI.js.map
11937
+ //# sourceMappingURL=chunk-UVR5XFE4.js.map