@rallycry/conveyor-agent 10.13.12 → 10.13.14
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.
- package/dist/{chunk-4KIPA6LF.js → chunk-J4UH5TOO.js} +369 -466
- package/dist/chunk-J4UH5TOO.js.map +1 -0
- package/dist/{chunk-YRC57EYG.js → chunk-LZM5OKAC.js} +2 -68
- package/dist/chunk-LZM5OKAC.js.map +1 -0
- package/dist/{chunk-2K6RRTQC.js → chunk-RSIW2UCR.js} +4 -180
- package/dist/chunk-RSIW2UCR.js.map +1 -0
- package/dist/{chunk-IE7EOKMD.js → chunk-VKZ5W2VO.js} +1 -1
- package/dist/chunk-VKZ5W2VO.js.map +1 -0
- package/dist/cli.js +27 -34
- package/dist/cli.js.map +1 -1
- package/dist/{client-ICIWKSK2.js → client-VQIBGPE6.js} +3 -3
- package/dist/index.d.ts +36 -76
- package/dist/index.js +5 -17
- package/dist/index.js.map +1 -1
- package/dist/{protocol-QJRHTTMQ.js → protocol-IHRTO5C4.js} +2 -2
- package/dist/{server-UGE67KIZ.js → server-TVIXWCAI.js} +7 -72
- package/dist/server-TVIXWCAI.js.map +1 -0
- package/package.json +1 -1
- package/runtime/entrypoint.sh +24 -98
- package/dist/chunk-2K6RRTQC.js.map +0 -1
- package/dist/chunk-4KIPA6LF.js.map +0 -1
- package/dist/chunk-IE7EOKMD.js.map +0 -1
- package/dist/chunk-YRC57EYG.js.map +0 -1
- package/dist/server-UGE67KIZ.js.map +0 -1
- /package/dist/{client-ICIWKSK2.js.map → client-VQIBGPE6.js.map} +0 -0
- /package/dist/{protocol-QJRHTTMQ.js.map → protocol-IHRTO5C4.js.map} +0 -0
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-7TQO4ZF4.js";
|
|
4
4
|
import {
|
|
5
5
|
getWorkbenchClient
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LZM5OKAC.js";
|
|
7
7
|
import {
|
|
8
8
|
workbenchEnabled
|
|
9
9
|
} from "./chunk-4VUQ2NPF.js";
|
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
MAX_BETWEEN_TURN_BUFFER,
|
|
12
12
|
MAX_DIAGNOSTIC_OUTPUT,
|
|
13
13
|
buildPromptBytes,
|
|
14
|
-
buildSnapshotTar,
|
|
15
|
-
extractSnapshotTar,
|
|
16
14
|
inheritedEnv,
|
|
17
15
|
loadPtySpawn,
|
|
18
16
|
parseUserQuestions,
|
|
@@ -25,7 +23,7 @@ import {
|
|
|
25
23
|
sleep,
|
|
26
24
|
transcriptSize,
|
|
27
25
|
turnOptionsFrom
|
|
28
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-RSIW2UCR.js";
|
|
29
27
|
|
|
30
28
|
// src/setup/bootstrap.ts
|
|
31
29
|
var BOOTSTRAP_TIMEOUT_MS = 3e4;
|
|
@@ -207,7 +205,6 @@ var AgentConnection = class _AgentConnection {
|
|
|
207
205
|
modeChangeCallback = null;
|
|
208
206
|
apiKeyUpdateCallback = null;
|
|
209
207
|
pullBranchCallback = null;
|
|
210
|
-
finalizeSnapshotCallback = null;
|
|
211
208
|
runStartCommandCallback = null;
|
|
212
209
|
earlyPullBranches = [];
|
|
213
210
|
spawnReviewCallback = null;
|
|
@@ -431,9 +428,6 @@ var AgentConnection = class _AgentConnection {
|
|
|
431
428
|
if (this.probeUsageCallback) this.probeUsageCallback();
|
|
432
429
|
else this.earlyProbeUsage = true;
|
|
433
430
|
});
|
|
434
|
-
this.socket.on("session:finalizeSnapshot", () => {
|
|
435
|
-
this.finalizeSnapshotCallback?.();
|
|
436
|
-
});
|
|
437
431
|
this.socket.on("session:runStartCommand", () => {
|
|
438
432
|
this.runStartCommandCallback?.();
|
|
439
433
|
});
|
|
@@ -714,9 +708,6 @@ var AgentConnection = class _AgentConnection {
|
|
|
714
708
|
}).catch(() => {
|
|
715
709
|
});
|
|
716
710
|
}
|
|
717
|
-
onFinalizeSnapshot(callback) {
|
|
718
|
-
this.finalizeSnapshotCallback = callback;
|
|
719
|
-
}
|
|
720
711
|
onRunStartCommand(callback) {
|
|
721
712
|
this.runStartCommandCallback = callback;
|
|
722
713
|
}
|
|
@@ -1226,55 +1217,51 @@ async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
1226
1217
|
});
|
|
1227
1218
|
return stdout.toString().trim();
|
|
1228
1219
|
}
|
|
1229
|
-
async function
|
|
1230
|
-
if (!
|
|
1220
|
+
async function ensureOnTaskBranch(cwd, taskBranch, baseBranch) {
|
|
1221
|
+
if (!taskBranch) return true;
|
|
1231
1222
|
try {
|
|
1232
|
-
await
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1223
|
+
if (await getCurrentBranch(cwd) === taskBranch) return true;
|
|
1224
|
+
let existsOnOrigin = true;
|
|
1225
|
+
try {
|
|
1226
|
+
await git(cwd, [
|
|
1227
|
+
"fetch",
|
|
1228
|
+
"origin",
|
|
1229
|
+
`+refs/heads/${taskBranch}:refs/remotes/origin/${taskBranch}`
|
|
1230
|
+
]);
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
if (String(err).includes("couldn't find remote ref")) existsOnOrigin = false;
|
|
1233
|
+
else throw err;
|
|
1234
|
+
}
|
|
1235
|
+
if (existsOnOrigin) {
|
|
1236
|
+
await git(cwd, ["checkout", "-B", taskBranch, `origin/${taskBranch}`], 3e4);
|
|
1237
|
+
process.stderr.write(`[conveyor-agent] Checked out task branch ${taskBranch}
|
|
1238
|
+
`);
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
if (!baseBranch) {
|
|
1242
|
+
process.stderr.write(
|
|
1243
|
+
`[conveyor-agent] Warning: task branch ${taskBranch} missing on origin and no base branch given
|
|
1236
1244
|
`
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1245
|
+
);
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
await git(cwd, [
|
|
1249
|
+
"fetch",
|
|
1250
|
+
"origin",
|
|
1251
|
+
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`
|
|
1252
|
+
]);
|
|
1253
|
+
await git(cwd, ["checkout", "-B", taskBranch, `origin/${baseBranch}`], 3e4);
|
|
1254
|
+
await git(cwd, ["push", "-u", "origin", taskBranch], 3e4);
|
|
1243
1255
|
process.stderr.write(
|
|
1244
|
-
`[conveyor-agent]
|
|
1256
|
+
`[conveyor-agent] Created task branch ${taskBranch} from origin/${baseBranch} and pushed
|
|
1245
1257
|
`
|
|
1246
1258
|
);
|
|
1247
|
-
|
|
1248
|
-
await git(cwd, ["merge", "--abort"], 3e4);
|
|
1249
|
-
} catch {
|
|
1250
|
-
}
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1253
|
-
process.stderr.write(`[conveyor-agent] Synced with latest origin/${baseBranch}
|
|
1254
|
-
`);
|
|
1255
|
-
return true;
|
|
1256
|
-
}
|
|
1257
|
-
async function ensureOnTaskBranch(cwd, taskBranch) {
|
|
1258
|
-
if (!taskBranch) return true;
|
|
1259
|
-
const current = await getCurrentBranch(cwd);
|
|
1260
|
-
if (current === taskBranch) return true;
|
|
1261
|
-
try {
|
|
1262
|
-
await git(cwd, ["fetch", "origin", taskBranch]);
|
|
1263
|
-
} catch {
|
|
1264
|
-
process.stderr.write(`[conveyor-agent] Warning: git fetch origin ${taskBranch} failed
|
|
1265
|
-
`);
|
|
1266
|
-
return false;
|
|
1267
|
-
}
|
|
1268
|
-
try {
|
|
1269
|
-
await git(cwd, ["checkout", "-B", taskBranch, `origin/${taskBranch}`], 3e4);
|
|
1259
|
+
return true;
|
|
1270
1260
|
} catch {
|
|
1271
|
-
process.stderr.write(`[conveyor-agent] Warning:
|
|
1261
|
+
process.stderr.write(`[conveyor-agent] Warning: ensureOnTaskBranch(${taskBranch}) failed
|
|
1272
1262
|
`);
|
|
1273
1263
|
return false;
|
|
1274
1264
|
}
|
|
1275
|
-
process.stderr.write(`[conveyor-agent] Checked out task branch ${taskBranch}
|
|
1276
|
-
`);
|
|
1277
|
-
return true;
|
|
1278
1265
|
}
|
|
1279
1266
|
async function hasUncommittedChanges(cwd) {
|
|
1280
1267
|
const status = await git(cwd, ["status", "--porcelain"], GIT_SLOW_TIMEOUT_MS);
|
|
@@ -1599,206 +1586,6 @@ async function backupOtherBranches(cwd, currentBranch) {
|
|
|
1599
1586
|
return count;
|
|
1600
1587
|
}
|
|
1601
1588
|
|
|
1602
|
-
// src/runner/work-preservation/index.ts
|
|
1603
|
-
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
1604
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
1605
|
-
import { join } from "path";
|
|
1606
|
-
|
|
1607
|
-
// src/runner/work-preservation/restore-precedence.ts
|
|
1608
|
-
function selectRestoreSource(probe) {
|
|
1609
|
-
if (probe.gcsAvailable) return "gcs";
|
|
1610
|
-
if (probe.wipRestoreResult === "applied") return "wip";
|
|
1611
|
-
return "clean";
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
// src/runner/work-preservation/snapshot-transfer.ts
|
|
1615
|
-
import { randomUUID } from "crypto";
|
|
1616
|
-
import { createReadStream, createWriteStream } from "fs";
|
|
1617
|
-
import { rm } from "fs/promises";
|
|
1618
|
-
import { tmpdir } from "os";
|
|
1619
|
-
import path from "path";
|
|
1620
|
-
import { Readable } from "stream";
|
|
1621
|
-
import { pipeline } from "stream/promises";
|
|
1622
|
-
var SNAPSHOT_HTTP_TIMEOUT_MS = 6e4;
|
|
1623
|
-
var SNAPSHOT_CAPTURED_AT_HEADER = "x-snapshot-captured-at";
|
|
1624
|
-
async function putSnapshotToGcs(url, tarResult) {
|
|
1625
|
-
try {
|
|
1626
|
-
const body = Readable.toWeb(createReadStream(tarResult.tarPath));
|
|
1627
|
-
const res = await fetch(url, {
|
|
1628
|
-
method: "PUT",
|
|
1629
|
-
headers: {
|
|
1630
|
-
"Content-Type": "application/gzip",
|
|
1631
|
-
"Content-Length": String(tarResult.sizeBytes),
|
|
1632
|
-
[SNAPSHOT_CAPTURED_AT_HEADER]: String(tarResult.capturedAt)
|
|
1633
|
-
},
|
|
1634
|
-
body,
|
|
1635
|
-
duplex: "half",
|
|
1636
|
-
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
1637
|
-
});
|
|
1638
|
-
return res.ok;
|
|
1639
|
-
} catch {
|
|
1640
|
-
return false;
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
async function downloadSnapshotToTemp(url) {
|
|
1644
|
-
const tmpTar = path.join(tmpdir(), `conveyor-snapshot-restore-${randomUUID()}.tar.gz`);
|
|
1645
|
-
try {
|
|
1646
|
-
const res = await fetch(url, {
|
|
1647
|
-
method: "GET",
|
|
1648
|
-
signal: AbortSignal.timeout(SNAPSHOT_HTTP_TIMEOUT_MS)
|
|
1649
|
-
});
|
|
1650
|
-
if (!res.ok || !res.body) return null;
|
|
1651
|
-
await pipeline(
|
|
1652
|
-
Readable.fromWeb(res.body),
|
|
1653
|
-
createWriteStream(tmpTar)
|
|
1654
|
-
);
|
|
1655
|
-
return tmpTar;
|
|
1656
|
-
} catch {
|
|
1657
|
-
await rm(tmpTar, { force: true }).catch(() => {
|
|
1658
|
-
});
|
|
1659
|
-
return null;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
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
|
-
}
|
|
1689
|
-
var periodicTimer = null;
|
|
1690
|
-
var inFlightCapture = null;
|
|
1691
|
-
var inFlightGcsUpload = null;
|
|
1692
|
-
var EMPTY_GCS_RESULT = {
|
|
1693
|
-
uploaded: false,
|
|
1694
|
-
fileCount: 0,
|
|
1695
|
-
deletionCount: 0,
|
|
1696
|
-
sizeBytes: 0
|
|
1697
|
-
};
|
|
1698
|
-
async function doUploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
1699
|
-
let tarResult;
|
|
1700
|
-
try {
|
|
1701
|
-
tarResult = await captureSnapshotTar(cwd);
|
|
1702
|
-
} catch {
|
|
1703
|
-
return EMPTY_GCS_RESULT;
|
|
1704
|
-
}
|
|
1705
|
-
try {
|
|
1706
|
-
const uploaded = (opts?.forceUpload || tarResult.sizeBytes > 0) && uploadUrl ? await putSnapshotToGcs(uploadUrl, tarResult) : false;
|
|
1707
|
-
return {
|
|
1708
|
-
uploaded,
|
|
1709
|
-
fileCount: tarResult.fileCount,
|
|
1710
|
-
deletionCount: tarResult.deletionCount,
|
|
1711
|
-
sizeBytes: tarResult.sizeBytes
|
|
1712
|
-
};
|
|
1713
|
-
} finally {
|
|
1714
|
-
await tarResult.cleanup();
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
async function uploadSnapshotToGcs(cwd, uploadUrl, opts) {
|
|
1718
|
-
const previous = inFlightGcsUpload;
|
|
1719
|
-
const run = (async () => {
|
|
1720
|
-
if (previous) await previous.catch(() => {
|
|
1721
|
-
});
|
|
1722
|
-
return doUploadSnapshotToGcs(cwd, uploadUrl, opts);
|
|
1723
|
-
})();
|
|
1724
|
-
inFlightGcsUpload = run;
|
|
1725
|
-
try {
|
|
1726
|
-
return await run;
|
|
1727
|
-
} finally {
|
|
1728
|
-
if (inFlightGcsUpload === run) inFlightGcsUpload = null;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
async function captureSnapshot(ctx) {
|
|
1732
|
-
const gcs = await uploadSnapshotToGcs(ctx.cwd, ctx.snapshotUploadUrl, {
|
|
1733
|
-
forceUpload: ctx.forceUpload
|
|
1734
|
-
});
|
|
1735
|
-
const wipResult = await flushPendingChanges(ctx.cwd, {
|
|
1736
|
-
wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation periodic snapshot",
|
|
1737
|
-
refreshToken: ctx.refreshToken
|
|
1738
|
-
});
|
|
1739
|
-
return {
|
|
1740
|
-
fileCount: gcs.fileCount,
|
|
1741
|
-
deletionCount: gcs.deletionCount,
|
|
1742
|
-
sizeBytes: gcs.sizeBytes,
|
|
1743
|
-
gcsUploaded: gcs.uploaded,
|
|
1744
|
-
wipPushed: wipResult.committed || wipResult.pushed
|
|
1745
|
-
};
|
|
1746
|
-
}
|
|
1747
|
-
function startPeriodic(intervalMs, ctx) {
|
|
1748
|
-
stop();
|
|
1749
|
-
periodicTimer = setInterval(() => {
|
|
1750
|
-
if (inFlightCapture) return;
|
|
1751
|
-
const run = captureSnapshot(ctx);
|
|
1752
|
-
inFlightCapture = run;
|
|
1753
|
-
void run.catch(() => {
|
|
1754
|
-
}).finally(() => {
|
|
1755
|
-
if (inFlightCapture === run) inFlightCapture = null;
|
|
1756
|
-
});
|
|
1757
|
-
}, intervalMs);
|
|
1758
|
-
}
|
|
1759
|
-
function stop() {
|
|
1760
|
-
if (periodicTimer) {
|
|
1761
|
-
clearInterval(periodicTimer);
|
|
1762
|
-
periodicTimer = null;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
async function finalizeForSleep(ctx) {
|
|
1766
|
-
stop();
|
|
1767
|
-
if (inFlightCapture) await inFlightCapture.catch(() => {
|
|
1768
|
-
});
|
|
1769
|
-
return captureSnapshot({
|
|
1770
|
-
...ctx,
|
|
1771
|
-
wipMessage: ctx.wipMessage ?? "WIP: WorkPreservation sleep finalize snapshot",
|
|
1772
|
-
// Always stamp snapshotAt on sleep — even an empty workspace must confirm
|
|
1773
|
-
// the finalize so teardown doesn't wait out the reconciler's cap.
|
|
1774
|
-
forceUpload: true
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
async function restoreOnBoot(bundle, cwd) {
|
|
1778
|
-
let gcsAvailable = false;
|
|
1779
|
-
let gcsFileCount;
|
|
1780
|
-
if (bundle.snapshotUrl) {
|
|
1781
|
-
const tmpTar = await downloadSnapshotToTemp(bundle.snapshotUrl);
|
|
1782
|
-
if (tmpTar) {
|
|
1783
|
-
try {
|
|
1784
|
-
const result = await applySnapshotTar(cwd, tmpTar);
|
|
1785
|
-
if (result.status === "extracted") {
|
|
1786
|
-
gcsAvailable = true;
|
|
1787
|
-
gcsFileCount = result.filesExtracted;
|
|
1788
|
-
}
|
|
1789
|
-
} catch {
|
|
1790
|
-
gcsAvailable = false;
|
|
1791
|
-
} finally {
|
|
1792
|
-
await rm2(tmpTar, { force: true }).catch(() => {
|
|
1793
|
-
});
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
const wipRestoreResult = gcsAvailable ? "none" : await restoreWipSnapshot(cwd, bundle.gitPlan.branch);
|
|
1798
|
-
const source = selectRestoreSource({ gcsAvailable, wipRestoreResult });
|
|
1799
|
-
return source === "gcs" ? { source, fileCount: gcsFileCount } : { source };
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
1589
|
// ../shared/dist/index.js
|
|
1803
1590
|
import { z } from "zod";
|
|
1804
1591
|
import { z as z2 } from "zod";
|
|
@@ -2662,6 +2449,13 @@ var StartAdhocSessionRequestSchema = z4.object({
|
|
|
2662
2449
|
mode: z4.enum(["adhoc", "pm"]).optional(),
|
|
2663
2450
|
/** Base branch to check out (defaults to the project's dev branch). */
|
|
2664
2451
|
branch: z4.string().max(300).optional(),
|
|
2452
|
+
/**
|
|
2453
|
+
* Server-assembled instructions the pod's TUI auto-submits once on first boot
|
|
2454
|
+
* (headless kickoff). Used by the onboarding "Set it up for me" flow to seed a
|
|
2455
|
+
* setup-driver prompt; the session stays watchable/interactive in the Sessions
|
|
2456
|
+
* view. `ensureAdhocWorkspace` persists it and clears it after first submit.
|
|
2457
|
+
*/
|
|
2458
|
+
initialPrompt: z4.string().max(2e4).optional(),
|
|
2665
2459
|
requestingUserId: z4.string().optional()
|
|
2666
2460
|
});
|
|
2667
2461
|
var StopAdhocSessionRequestSchema = z4.object({
|
|
@@ -3299,9 +3093,9 @@ var ClaudeCodeHarness = class {
|
|
|
3299
3093
|
};
|
|
3300
3094
|
|
|
3301
3095
|
// src/harness/pty/session.ts
|
|
3302
|
-
import { randomUUID
|
|
3303
|
-
import { mkdtemp
|
|
3304
|
-
import { join as
|
|
3096
|
+
import { randomUUID } from "crypto";
|
|
3097
|
+
import { mkdtemp, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
3098
|
+
import { join as join4, dirname } from "path";
|
|
3305
3099
|
|
|
3306
3100
|
// src/harness/pty/event-queue.ts
|
|
3307
3101
|
var AsyncEventQueue = class {
|
|
@@ -3582,8 +3376,8 @@ function mapTranscriptRecord(raw) {
|
|
|
3582
3376
|
// src/harness/pty/jsonl-tailer.ts
|
|
3583
3377
|
var POLL_INTERVAL_MS = 25;
|
|
3584
3378
|
var JsonlTailer = class {
|
|
3585
|
-
constructor(
|
|
3586
|
-
this.path =
|
|
3379
|
+
constructor(path2, onEvent, onRawRecord) {
|
|
3380
|
+
this.path = path2;
|
|
3587
3381
|
this.onEvent = onEvent;
|
|
3588
3382
|
this.onRawRecord = onRawRecord;
|
|
3589
3383
|
}
|
|
@@ -3845,15 +3639,15 @@ function mapChatRecords(raw) {
|
|
|
3845
3639
|
// src/harness/pty/settings.ts
|
|
3846
3640
|
import { mkdir, writeFile, chmod } from "fs/promises";
|
|
3847
3641
|
import { homedir } from "os";
|
|
3848
|
-
import { join
|
|
3642
|
+
import { join } from "path";
|
|
3849
3643
|
function claudeConfigHome() {
|
|
3850
|
-
return process.env.CLAUDE_CONFIG_DIR ??
|
|
3644
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
3851
3645
|
}
|
|
3852
3646
|
function projectSlug(cwd) {
|
|
3853
3647
|
return cwd.replace(/\//g, "-");
|
|
3854
3648
|
}
|
|
3855
3649
|
function sessionTranscriptPath(cwd, sessionId) {
|
|
3856
|
-
return
|
|
3650
|
+
return join(claudeConfigHome(), "projects", projectSlug(cwd), `${sessionId}.jsonl`);
|
|
3857
3651
|
}
|
|
3858
3652
|
var ALLOW_RULES = [
|
|
3859
3653
|
"Bash",
|
|
@@ -4021,8 +3815,8 @@ function preToolUse(payload) {
|
|
|
4021
3815
|
}
|
|
4022
3816
|
`;
|
|
4023
3817
|
async function writeHookSettings(dir) {
|
|
4024
|
-
const helperPath =
|
|
4025
|
-
const settingsPath =
|
|
3818
|
+
const helperPath = join(dir, "hook-helper.cjs");
|
|
3819
|
+
const settingsPath = join(dir, "settings.json");
|
|
4026
3820
|
await mkdir(dir, { recursive: true });
|
|
4027
3821
|
await writeFile(helperPath, HOOK_HELPER_SOURCE, "utf8");
|
|
4028
3822
|
await chmod(helperPath, 493);
|
|
@@ -4140,7 +3934,7 @@ var PtyOutputCoalescer = class {
|
|
|
4140
3934
|
import { createServer as createServer2 } from "http";
|
|
4141
3935
|
import { z as z6 } from "zod";
|
|
4142
3936
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
4143
|
-
import { join as
|
|
3937
|
+
import { join as join2 } from "path";
|
|
4144
3938
|
import { randomBytes } from "crypto";
|
|
4145
3939
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4146
3940
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -4331,7 +4125,7 @@ async function startToolServers(mcpServers, tempDir) {
|
|
|
4331
4125
|
config[name] = { type: "http", url, headers: { Authorization: `Bearer ${token}` } };
|
|
4332
4126
|
}
|
|
4333
4127
|
if (Object.keys(config).length === 0) return { servers, mcpConfigPath: null };
|
|
4334
|
-
const mcpConfigPath =
|
|
4128
|
+
const mcpConfigPath = join2(tempDir, "mcp-config.json");
|
|
4335
4129
|
await writeFile2(mcpConfigPath, JSON.stringify({ mcpServers: config }, null, 2), "utf8");
|
|
4336
4130
|
return { servers, mcpConfigPath };
|
|
4337
4131
|
}
|
|
@@ -4407,13 +4201,13 @@ ${tail}`);
|
|
|
4407
4201
|
}
|
|
4408
4202
|
|
|
4409
4203
|
// src/harness/pty/credentials.ts
|
|
4410
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile, rm
|
|
4204
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile, rm, writeFile as writeFile3 } from "fs/promises";
|
|
4411
4205
|
import { homedir as homedir2 } from "os";
|
|
4412
|
-
import { join as
|
|
4206
|
+
import { join as join3 } from "path";
|
|
4413
4207
|
var SYNTH_TOKEN_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
4414
4208
|
var REFRESH_SKEW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4415
4209
|
function claudeCredentialsPath() {
|
|
4416
|
-
return
|
|
4210
|
+
return join3(claudeConfigHome(), ".credentials.json");
|
|
4417
4211
|
}
|
|
4418
4212
|
function isConveyorCloudEnv(env = process.env) {
|
|
4419
4213
|
return Boolean(env.CLAUDESPACE_NAME || env.CODESPACE_NAME || env.CODESPACES);
|
|
@@ -4458,13 +4252,29 @@ function planCredentialsWrite(input) {
|
|
|
4458
4252
|
if (fresh) return { action: "skip", reason: "current" };
|
|
4459
4253
|
return { action: "write", contents };
|
|
4460
4254
|
}
|
|
4461
|
-
async function readRaw(
|
|
4255
|
+
async function readRaw(path2) {
|
|
4462
4256
|
try {
|
|
4463
|
-
return await readFile(
|
|
4257
|
+
return await readFile(path2, "utf8");
|
|
4464
4258
|
} catch {
|
|
4465
4259
|
return null;
|
|
4466
4260
|
}
|
|
4467
4261
|
}
|
|
4262
|
+
function classifyTuiAuth(input) {
|
|
4263
|
+
if (!input.isCloud) return "ready";
|
|
4264
|
+
if (input.hasOauthToken || input.hasApiKey || input.credsHasAccessToken) return "ready";
|
|
4265
|
+
return "no-credential";
|
|
4266
|
+
}
|
|
4267
|
+
async function resolveTuiAuthReadiness(env = process.env, readIdentity = readCredentialsIdentity) {
|
|
4268
|
+
const isCloud = isConveyorCloudEnv(env);
|
|
4269
|
+
const credsHasAccessToken = isCloud ? Boolean((await readIdentity())?.accessToken) : false;
|
|
4270
|
+
const status = classifyTuiAuth({
|
|
4271
|
+
isCloud,
|
|
4272
|
+
hasOauthToken: Boolean(env.CLAUDE_CODE_OAUTH_TOKEN),
|
|
4273
|
+
hasApiKey: Boolean(env.ANTHROPIC_API_KEY),
|
|
4274
|
+
credsHasAccessToken
|
|
4275
|
+
});
|
|
4276
|
+
return { ready: status === "ready", status };
|
|
4277
|
+
}
|
|
4468
4278
|
async function readCredentialsIdentity() {
|
|
4469
4279
|
const parsed = parseClaudeAiOauth(await readRaw(claudeCredentialsPath()));
|
|
4470
4280
|
if (!parsed) return null;
|
|
@@ -4486,10 +4296,10 @@ async function writeWithReadBackRetry(io2, contents, delaysMs = READ_BACK_DELAYS
|
|
|
4486
4296
|
await sleep2(delaysMs[attempt]);
|
|
4487
4297
|
}
|
|
4488
4298
|
}
|
|
4489
|
-
function fsWriteIo(
|
|
4299
|
+
function fsWriteIo(path2, mode) {
|
|
4490
4300
|
return {
|
|
4491
|
-
write: (contents) => writeFile3(
|
|
4492
|
-
read: () => readRaw(
|
|
4301
|
+
write: (contents) => writeFile3(path2, contents, mode === void 0 ? "utf8" : { encoding: "utf8", mode }),
|
|
4302
|
+
read: () => readRaw(path2)
|
|
4493
4303
|
};
|
|
4494
4304
|
}
|
|
4495
4305
|
async function ensureClaudeCredentials(env = process.env) {
|
|
@@ -4499,23 +4309,23 @@ async function ensureClaudeCredentials(env = process.env) {
|
|
|
4499
4309
|
await sanitizeApprovedApiKeys(token);
|
|
4500
4310
|
}
|
|
4501
4311
|
try {
|
|
4502
|
-
const
|
|
4312
|
+
const path2 = claudeCredentialsPath();
|
|
4503
4313
|
const plan = planCredentialsWrite({
|
|
4504
4314
|
isCloud,
|
|
4505
4315
|
token,
|
|
4506
|
-
existingRaw: await readRaw(
|
|
4316
|
+
existingRaw: await readRaw(path2),
|
|
4507
4317
|
now: Date.now()
|
|
4508
4318
|
});
|
|
4509
4319
|
if (plan.action === "skip") return;
|
|
4510
4320
|
await mkdir2(claudeConfigHome(), { recursive: true });
|
|
4511
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4321
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path2, 384), plan.contents);
|
|
4512
4322
|
if (!verified) {
|
|
4513
4323
|
process.stderr.write(
|
|
4514
|
-
`[conveyor-agent] claude credentials read-back still stale after retries at ${
|
|
4324
|
+
`[conveyor-agent] claude credentials read-back still stale after retries at ${path2} \u2014 TUI may land on the login picker
|
|
4515
4325
|
`
|
|
4516
4326
|
);
|
|
4517
4327
|
}
|
|
4518
|
-
await chmod2(
|
|
4328
|
+
await chmod2(path2, 384).catch(() => {
|
|
4519
4329
|
});
|
|
4520
4330
|
} catch (err) {
|
|
4521
4331
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4544,12 +4354,12 @@ function planApprovedApiKeyCleanup(existingRaw, oauthToken) {
|
|
|
4544
4354
|
}
|
|
4545
4355
|
async function sanitizeApprovedApiKeys(oauthToken) {
|
|
4546
4356
|
try {
|
|
4547
|
-
const
|
|
4548
|
-
const cleaned = planApprovedApiKeyCleanup(await readRaw(
|
|
4357
|
+
const path2 = claudeJsonPath();
|
|
4358
|
+
const cleaned = planApprovedApiKeyCleanup(await readRaw(path2), oauthToken);
|
|
4549
4359
|
if (cleaned === null) return;
|
|
4550
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4360
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path2), cleaned);
|
|
4551
4361
|
process.stderr.write(
|
|
4552
|
-
verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${
|
|
4362
|
+
verified ? "[conveyor-agent] removed poisoned customApiKeyResponses.approved entry from .claude.json\n" : `[conveyor-agent] approved-key sanitize read-back still stale after retries at ${path2} \u2014 CLI may still see the poisoned entry
|
|
4553
4363
|
`
|
|
4554
4364
|
);
|
|
4555
4365
|
} catch (err) {
|
|
@@ -4560,7 +4370,7 @@ async function sanitizeApprovedApiKeys(oauthToken) {
|
|
|
4560
4370
|
}
|
|
4561
4371
|
function claudeJsonPath() {
|
|
4562
4372
|
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
4563
|
-
return configDir ?
|
|
4373
|
+
return configDir ? join3(configDir, ".claude.json") : join3(homedir2(), ".claude.json");
|
|
4564
4374
|
}
|
|
4565
4375
|
function asRecord(value) {
|
|
4566
4376
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
@@ -4653,7 +4463,7 @@ function planClaudeJsonSeed(existingRaw, trustCwd, oauthIdentity) {
|
|
|
4653
4463
|
return changed ? JSON.stringify(config) : null;
|
|
4654
4464
|
}
|
|
4655
4465
|
function conveyorOauthMarkerPath() {
|
|
4656
|
-
return
|
|
4466
|
+
return join3(claudeConfigHome(), "conveyor-oauth-account.json");
|
|
4657
4467
|
}
|
|
4658
4468
|
function parseOauthIdentity(raw) {
|
|
4659
4469
|
if (!raw || raw.trim() === "") return null;
|
|
@@ -4686,8 +4496,8 @@ async function persistOauthIdentityMarker(configIdentity, markerIdentity) {
|
|
|
4686
4496
|
async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
4687
4497
|
try {
|
|
4688
4498
|
if (!isConveyorCloudEnv(env)) return;
|
|
4689
|
-
const
|
|
4690
|
-
const existingRaw = await readRaw(
|
|
4499
|
+
const path2 = claudeJsonPath();
|
|
4500
|
+
const existingRaw = await readRaw(path2);
|
|
4691
4501
|
const markerIdentity = parseOauthIdentity(await readRaw(conveyorOauthMarkerPath()));
|
|
4692
4502
|
await persistOauthIdentityMarker(
|
|
4693
4503
|
extractOauthIdentity(parseClaudeJson(existingRaw)),
|
|
@@ -4695,16 +4505,16 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4695
4505
|
);
|
|
4696
4506
|
const contents = planClaudeJsonSeed(existingRaw, trustCwd, markerIdentity);
|
|
4697
4507
|
if (contents === null) return;
|
|
4698
|
-
const verified = await writeWithReadBackRetry(fsWriteIo(
|
|
4508
|
+
const verified = await writeWithReadBackRetry(fsWriteIo(path2), contents);
|
|
4699
4509
|
if (verified) {
|
|
4700
4510
|
process.stderr.write(
|
|
4701
4511
|
`[conveyor-agent] claude onboarding seeded${trustCwd ? ` (trust: ${trustCwd})` : ""}
|
|
4702
4512
|
`
|
|
4703
4513
|
);
|
|
4704
4514
|
} else {
|
|
4705
|
-
const verify = await readRaw(
|
|
4515
|
+
const verify = await readRaw(path2);
|
|
4706
4516
|
process.stderr.write(
|
|
4707
|
-
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${
|
|
4517
|
+
`[conveyor-agent] claude onboarding seed read-back MISMATCH at ${path2} after retries: wrote ${contents.length}B, read ${verify?.length ?? 0}B \u2014 CLI may see stale config and park at a startup dialog
|
|
4708
4518
|
`
|
|
4709
4519
|
);
|
|
4710
4520
|
}
|
|
@@ -4717,12 +4527,12 @@ async function ensureClaudeOnboarding(env = process.env, trustCwd) {
|
|
|
4717
4527
|
async function removeConveyorCredentials(env = process.env) {
|
|
4718
4528
|
try {
|
|
4719
4529
|
if (!isConveyorCloudEnv(env)) return;
|
|
4720
|
-
const
|
|
4721
|
-
const existing = parseClaudeAiOauth(await readRaw(
|
|
4530
|
+
const path2 = claudeCredentialsPath();
|
|
4531
|
+
const existing = parseClaudeAiOauth(await readRaw(path2));
|
|
4722
4532
|
if (existing && typeof existing.refreshToken === "string" && existing.refreshToken.length > 0) {
|
|
4723
4533
|
return;
|
|
4724
4534
|
}
|
|
4725
|
-
await
|
|
4535
|
+
await rm(path2, { force: true });
|
|
4726
4536
|
} catch (err) {
|
|
4727
4537
|
const message = err instanceof Error ? err.message : String(err);
|
|
4728
4538
|
process.stderr.write(`[conveyor-agent] claude credentials removal failed: ${message}
|
|
@@ -5030,7 +4840,7 @@ var PtySession = class {
|
|
|
5030
4840
|
const { settingsPath, socketPath } = await this.startStructuredEventSources(sessionId);
|
|
5031
4841
|
await this.spawn(settingsPath, socketPath);
|
|
5032
4842
|
} else {
|
|
5033
|
-
this.tempDir = await
|
|
4843
|
+
this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
|
|
5034
4844
|
await this.spawn();
|
|
5035
4845
|
this.pushEvent({
|
|
5036
4846
|
type: "system",
|
|
@@ -5067,8 +4877,8 @@ var PtySession = class {
|
|
|
5067
4877
|
* paths spawn() must wire into the child's argv/env.
|
|
5068
4878
|
*/
|
|
5069
4879
|
async startStructuredEventSources(sessionId) {
|
|
5070
|
-
this.tempDir = await
|
|
5071
|
-
const socketPath =
|
|
4880
|
+
this.tempDir = await mkdtemp(join4(sessionTempBase(), "conveyor-pty-"));
|
|
4881
|
+
const socketPath = join4(this.tempDir, "hook.sock");
|
|
5072
4882
|
this.socket = new HookSocketServer(
|
|
5073
4883
|
socketPath,
|
|
5074
4884
|
(progress) => this.handleProgress(progress),
|
|
@@ -5122,7 +4932,7 @@ var PtySession = class {
|
|
|
5122
4932
|
* questionnaire resolves; see the field comment). */
|
|
5123
4933
|
emitSyntheticQuestionCard(questions) {
|
|
5124
4934
|
if (questions.length === 0 || typeof this.bridge?.sendChatEvent !== "function") return;
|
|
5125
|
-
const id = `aq-${
|
|
4935
|
+
const id = `aq-${randomUUID()}`;
|
|
5126
4936
|
this.pendingSyntheticQuestionIds.push(id);
|
|
5127
4937
|
this.sendChatEvent({
|
|
5128
4938
|
kind: "tool_use",
|
|
@@ -5149,6 +4959,39 @@ var PtySession = class {
|
|
|
5149
4959
|
writeStdin(text) {
|
|
5150
4960
|
this.pty?.write(text);
|
|
5151
4961
|
}
|
|
4962
|
+
/**
|
|
4963
|
+
* Inject a follow-up message into the CURRENTLY-RUNNING turn by pasting it into
|
|
4964
|
+
* the live TUI input, exactly as a human typing mid-turn would — the CLI queues
|
|
4965
|
+
* it and picks it up at the next turn boundary. Unlike beginTurn this does NOT
|
|
4966
|
+
* reset per-turn state, allocate a new queue, or re-arm the abort listener: the
|
|
4967
|
+
* in-flight turn (and its transcript stream) keeps flowing, so the injected
|
|
4968
|
+
* message and its response ride the SAME event pipeline. Returns false when
|
|
4969
|
+
* there is no live, actively-draining turn to inject into — parked/idle
|
|
4970
|
+
* (activeQueue === null), torn down, exited, or a raw-terminal TUI that gives
|
|
4971
|
+
* us no trusted signal that a turn is running — so the caller falls back to the
|
|
4972
|
+
* abort+respawn supersede path.
|
|
4973
|
+
*/
|
|
4974
|
+
injectIntoRunningTurn(text) {
|
|
4975
|
+
if (!this.pty || this._toreDown || this.exited) return false;
|
|
4976
|
+
if (this.activeQueue === null) return false;
|
|
4977
|
+
if (!this.adapter.capabilities.structuredEvents) return false;
|
|
4978
|
+
if (!text.trim()) return false;
|
|
4979
|
+
void this.submitLivePrompt(text);
|
|
4980
|
+
return true;
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4983
|
+
* Paste + submit a prompt into the live pty WITHOUT any turn bookkeeping.
|
|
4984
|
+
* Mirrors deliverPrompt's submit path (bracketed paste, then a separate Enter
|
|
4985
|
+
* after a settle window) but never arms the submit nudge — the running turn is
|
|
4986
|
+
* already producing transcript records, so re-pressing Enter would risk
|
|
4987
|
+
* accepting an unrelated mid-turn dialog. Fire-and-forget.
|
|
4988
|
+
*/
|
|
4989
|
+
async submitLivePrompt(text) {
|
|
4990
|
+
this.writeStdin(this.adapter.encodePromptBytes(text));
|
|
4991
|
+
await sleep(resolveSubmitSettleMs());
|
|
4992
|
+
if (this._toreDown || this.exited) return;
|
|
4993
|
+
this.writeStdin("\r");
|
|
4994
|
+
}
|
|
5152
4995
|
/** Apply a relayed resize to the live pty (reconciled dims from the server). */
|
|
5153
4996
|
resizePty(cols, rows) {
|
|
5154
4997
|
if (cols <= 0 || rows <= 0) return;
|
|
@@ -5212,7 +5055,7 @@ var PtySession = class {
|
|
|
5212
5055
|
this.activeQueue?.close();
|
|
5213
5056
|
this.activeQueue = null;
|
|
5214
5057
|
if (this.tempDir) {
|
|
5215
|
-
await
|
|
5058
|
+
await rm2(this.tempDir, { recursive: true, force: true });
|
|
5216
5059
|
this.tempDir = "";
|
|
5217
5060
|
}
|
|
5218
5061
|
}
|
|
@@ -5488,7 +5331,7 @@ var PtySession = class {
|
|
|
5488
5331
|
// src/harness/pty/config-home-health.ts
|
|
5489
5332
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
5490
5333
|
import { homedir as homedir3 } from "os";
|
|
5491
|
-
import { join as
|
|
5334
|
+
import { join as join5 } from "path";
|
|
5492
5335
|
var MOUNT_DISCONNECT_CODES = /* @__PURE__ */ new Set(["ENOTCONN", "EIO", "ESTALE", "ENXIO"]);
|
|
5493
5336
|
var MOUNT_DISCONNECT_MESSAGES = [
|
|
5494
5337
|
"socket is not connected",
|
|
@@ -5504,12 +5347,12 @@ function isMountDisconnectError(err) {
|
|
|
5504
5347
|
return MOUNT_DISCONNECT_MESSAGES.some((needle) => lower.includes(needle));
|
|
5505
5348
|
}
|
|
5506
5349
|
function podLocalConfigHome() {
|
|
5507
|
-
return
|
|
5350
|
+
return join5(homedir3(), ".claude-local");
|
|
5508
5351
|
}
|
|
5509
5352
|
async function ensureUsableClaudeConfigHome(cwd, log) {
|
|
5510
5353
|
const configHome = claudeConfigHome();
|
|
5511
5354
|
try {
|
|
5512
|
-
await mkdir4(
|
|
5355
|
+
await mkdir4(join5(configHome, "projects", projectSlug(cwd)), { recursive: true });
|
|
5513
5356
|
return { configHome, fellBack: false };
|
|
5514
5357
|
} catch (err) {
|
|
5515
5358
|
if (!isMountDisconnectError(err)) throw err;
|
|
@@ -5523,7 +5366,7 @@ async function ensureUsableClaudeConfigHome(cwd, log) {
|
|
|
5523
5366
|
}
|
|
5524
5367
|
);
|
|
5525
5368
|
process.env.CLAUDE_CONFIG_DIR = fallback;
|
|
5526
|
-
await mkdir4(
|
|
5369
|
+
await mkdir4(join5(fallback, "projects", projectSlug(cwd)), { recursive: true });
|
|
5527
5370
|
return { configHome: fallback, fellBack: true };
|
|
5528
5371
|
}
|
|
5529
5372
|
}
|
|
@@ -5560,6 +5403,9 @@ var PtyHarness = class _PtyHarness {
|
|
|
5560
5403
|
endedTimer = null;
|
|
5561
5404
|
/** Passive-activity subscriber, re-attached to whichever session is parked. */
|
|
5562
5405
|
passiveHandler = null;
|
|
5406
|
+
/** Once-per-session guard so the "no Claude credential" notice isn't re-posted
|
|
5407
|
+
* on every respawn (a fresh spawn happens on each fingerprint/lineage flip). */
|
|
5408
|
+
authNoticeSent = false;
|
|
5563
5409
|
/**
|
|
5564
5410
|
* Wiggle the live pty's size so the CLI repaints its whole screen. No-op on
|
|
5565
5411
|
* the SDK harness. Falls back to the parked session so an API reconnect while
|
|
@@ -5568,6 +5414,30 @@ var PtyHarness = class _PtyHarness {
|
|
|
5568
5414
|
forceRepaint() {
|
|
5569
5415
|
(this.activeSession ?? this.parked)?.forceRepaint();
|
|
5570
5416
|
}
|
|
5417
|
+
/** Actionable message posted to the card when a spawn will park at the Claude
|
|
5418
|
+
* sign-in screen. Kept as a constant so the harness stays free of i18n deps. */
|
|
5419
|
+
static AUTH_NOT_READY_MESSAGE = "\u26A0\uFE0F This pod started the Claude Code TUI with no Claude credential, so it is parked on the sign-in screen and can't make progress. This usually means there is no usable Claude subscription token or API key configured for this project/assignee. Add or repair the Claude credential in project settings, then restart the session.";
|
|
5420
|
+
/**
|
|
5421
|
+
* Best-effort: if the TUI is about to spawn with no usable credential, post a
|
|
5422
|
+
* one-time diagnostic through the relay bridge. Never throws — a readiness
|
|
5423
|
+
* probe must not block or fail a spawn.
|
|
5424
|
+
*/
|
|
5425
|
+
async warnIfAuthNotReady() {
|
|
5426
|
+
if (this.authNoticeSent || !this.bridge?.notifyAuthNotReady) return;
|
|
5427
|
+
try {
|
|
5428
|
+
const readiness = await resolveTuiAuthReadiness();
|
|
5429
|
+
if (readiness.ready) return;
|
|
5430
|
+
this.authNoticeSent = true;
|
|
5431
|
+
this.bridge.notifyAuthNotReady(_PtyHarness.AUTH_NOT_READY_MESSAGE);
|
|
5432
|
+
_PtyHarness.log.warn(
|
|
5433
|
+
"Claude TUI spawning with no usable credential \u2014 parked at sign-in screen"
|
|
5434
|
+
);
|
|
5435
|
+
} catch (err) {
|
|
5436
|
+
_PtyHarness.log.warn("auth-readiness probe failed", {
|
|
5437
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5438
|
+
});
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5571
5441
|
async *executeQuery(opts) {
|
|
5572
5442
|
const want = opts.resume ?? opts.options.resume;
|
|
5573
5443
|
const fingerprint = this.fingerprintOf(opts.options);
|
|
@@ -5589,6 +5459,9 @@ var PtyHarness = class _PtyHarness {
|
|
|
5589
5459
|
await ensureUsableClaudeConfigHome(opts.options.cwd, _PtyHarness.log);
|
|
5590
5460
|
}
|
|
5591
5461
|
await this.adapter.prepareEnvironment({ cwd: opts.options.cwd });
|
|
5462
|
+
if (this.adapter.capabilities.structuredEvents) {
|
|
5463
|
+
await this.warnIfAuthNotReady();
|
|
5464
|
+
}
|
|
5592
5465
|
session.onExit(() => this.handleSessionExit(session));
|
|
5593
5466
|
await session.start();
|
|
5594
5467
|
}
|
|
@@ -5599,6 +5472,15 @@ var PtyHarness = class _PtyHarness {
|
|
|
5599
5472
|
* query running" (a human typed into the idle Connected-TUI). Attaches to the
|
|
5600
5473
|
* currently-parked session and to any session parked later.
|
|
5601
5474
|
*/
|
|
5475
|
+
/**
|
|
5476
|
+
* PTY-only: inject a follow-up message into the turn currently streaming
|
|
5477
|
+
* events (the active session), so the runner can add to a running turn without
|
|
5478
|
+
* aborting + respawning. Returns false when no turn is active (idle/parked) or
|
|
5479
|
+
* the paste couldn't be delivered — the caller supersedes instead.
|
|
5480
|
+
*/
|
|
5481
|
+
injectIntoRunningTurn(text) {
|
|
5482
|
+
return this.activeSession?.injectIntoRunningTurn(text) ?? false;
|
|
5483
|
+
}
|
|
5602
5484
|
onPassiveActivity(handler) {
|
|
5603
5485
|
this.passiveHandler = handler;
|
|
5604
5486
|
const unsubParked = this.parked?.onPassiveActivity(handler);
|
|
@@ -5971,20 +5853,20 @@ function formatIncidents(incidents) {
|
|
|
5971
5853
|
|
|
5972
5854
|
// src/workbench/fs.ts
|
|
5973
5855
|
import { readFile as localReadFile, readdir as localReaddir, stat as localStat } from "fs/promises";
|
|
5974
|
-
async function readWorkspaceFile(
|
|
5856
|
+
async function readWorkspaceFile(path2) {
|
|
5975
5857
|
if (workbenchEnabled()) {
|
|
5976
|
-
return (await getWorkbenchClient().readFile(
|
|
5858
|
+
return (await getWorkbenchClient().readFile(path2)).toString("utf8");
|
|
5977
5859
|
}
|
|
5978
|
-
return localReadFile(
|
|
5860
|
+
return localReadFile(path2, "utf-8");
|
|
5979
5861
|
}
|
|
5980
|
-
function readWorkspaceDir(
|
|
5981
|
-
if (workbenchEnabled()) return getWorkbenchClient().readdir(
|
|
5982
|
-
return localReaddir(
|
|
5862
|
+
function readWorkspaceDir(path2) {
|
|
5863
|
+
if (workbenchEnabled()) return getWorkbenchClient().readdir(path2);
|
|
5864
|
+
return localReaddir(path2);
|
|
5983
5865
|
}
|
|
5984
|
-
async function statWorkspacePath(
|
|
5985
|
-
if (workbenchEnabled()) return getWorkbenchClient().stat(
|
|
5866
|
+
async function statWorkspacePath(path2) {
|
|
5867
|
+
if (workbenchEnabled()) return getWorkbenchClient().stat(path2);
|
|
5986
5868
|
try {
|
|
5987
|
-
const s = await localStat(
|
|
5869
|
+
const s = await localStat(path2);
|
|
5988
5870
|
return {
|
|
5989
5871
|
exists: true,
|
|
5990
5872
|
isFile: s.isFile(),
|
|
@@ -5996,8 +5878,8 @@ async function statWorkspacePath(path3) {
|
|
|
5996
5878
|
return { exists: false, isFile: false, isDirectory: false, size: 0, mtimeMs: 0 };
|
|
5997
5879
|
}
|
|
5998
5880
|
}
|
|
5999
|
-
async function workspacePathExists(
|
|
6000
|
-
return (await statWorkspacePath(
|
|
5881
|
+
async function workspacePathExists(path2) {
|
|
5882
|
+
return (await statWorkspacePath(path2)).exists;
|
|
6001
5883
|
}
|
|
6002
5884
|
|
|
6003
5885
|
// src/execution/tag-context-resolver.ts
|
|
@@ -6368,16 +6250,30 @@ function buildPlanDocumentationSection(context) {
|
|
|
6368
6250
|
const hasPlan = !!context?.plan?.trim();
|
|
6369
6251
|
return [
|
|
6370
6252
|
``,
|
|
6371
|
-
`### Plan
|
|
6253
|
+
`### Plan first, then BUILD \u2014 the plan is a step, NOT the deliverable`,
|
|
6372
6254
|
`- The card is already In Progress and advances automatically (In Progress \u2192 Review PR when you open the PR). There is no plan-approval step.`,
|
|
6373
6255
|
...hasPlan ? [
|
|
6374
|
-
`- A plan is already saved on the card. Keep it current with update_task_plan if your approach diverges materially
|
|
6256
|
+
`- A plan is already saved on the card. Keep it current with update_task_plan if your approach diverges materially \u2014 then IMPLEMENT it in code. The saved plan is not the deliverable; the working implementation is.`
|
|
6375
6257
|
] : [
|
|
6376
|
-
`- No plan is saved yet: BEFORE writing any code, investigate briefly (search first, read only critical files) and save a concise implementation plan with update_task_plan (file:line citations).
|
|
6258
|
+
`- No plan is saved yet: BEFORE writing any code, investigate briefly (search first, read only critical files) and save a concise implementation plan with update_task_plan (file:line citations). Saving the plan is a planning step, not the goal \u2014 immediately move on to WRITING THE CODE that implements it; never pause for approval.`
|
|
6377
6259
|
],
|
|
6260
|
+
`- Your goal is to BUILD the change, not to produce a plan. After the plan is posted, actually implement it: edit source files, make the change work, then verify. Do NOT stop, go idle, or open a PR the moment the plan exists.`,
|
|
6261
|
+
`- Your pull request MUST contain the actual code implementation. Never open a plan-only or empty-diff PR \u2014 a PR that just records the plan is never the goal of auto mode unless the task explicitly asks ONLY for a plan. (If the task genuinely needs no code changes, don't open a PR at all \u2014 finish per the section below.)`,
|
|
6378
6262
|
`- Identification auto-fills title, story points, and icon with quick AI guesses. After exploring, refine the title and story points with update_task_properties if they look like placeholders. Icons are automatic \u2014 never set them.`
|
|
6379
6263
|
];
|
|
6380
6264
|
}
|
|
6265
|
+
function buildNoPrWhenNoCodeSection(baseBranch) {
|
|
6266
|
+
const base = baseBranch ?? "dev";
|
|
6267
|
+
return [
|
|
6268
|
+
``,
|
|
6269
|
+
`### A PR is NOT required \u2014 only open one for actual code changes`,
|
|
6270
|
+
`\`create_pull_request\` is for tasks that change code in the repo. Many tasks don't: support requests, config/credential help, answering a question, investigations, or research whose deliverable is an answer or a file rather than a diff.`,
|
|
6271
|
+
`- If you finish the work with NO code changes (an empty \`git diff ${base}..HEAD\`), do NOT open a PR. An empty or throwaway PR just to "complete" the workflow is wrong \u2014 a human then has to close it.`,
|
|
6272
|
+
`- Deliver the result where it belongs: post the answer/config/findings with \`post_to_chat\`, and attach any files the user should keep with \`upload_attachment\`.`,
|
|
6273
|
+
`- Then complete the card directly with \`force_update_task_status("Complete")\` \u2014 there is no PR or review step for a no-code task.`,
|
|
6274
|
+
`- When in doubt, check \`git diff ${base}..HEAD\`: a real diff means open a PR; no diff means finish in chat and mark Complete.`
|
|
6275
|
+
];
|
|
6276
|
+
}
|
|
6381
6277
|
function buildExplorationMethodology() {
|
|
6382
6278
|
return [
|
|
6383
6279
|
``,
|
|
@@ -6500,7 +6396,7 @@ function buildAutoPrompt(context, runnerMode) {
|
|
|
6500
6396
|
`If no children exist yet, break the work down now: save a parent-level plan with update_task_plan, then create child tasks with create_subtask (each with a detailed plan).`,
|
|
6501
6397
|
`Child task status lifecycle: Open \u2192 InProgress \u2192 ReviewPR \u2192 ReviewDev \u2192 Complete.`,
|
|
6502
6398
|
`Set child ordering with \`dependsOn\` (sibling ids/slugs) on \`create_subtask\` \u2014 explicit metadata the pack runner schedules off, not order described in plan text. Independent children get none and run in parallel.`
|
|
6503
|
-
] :
|
|
6399
|
+
] : buildNoPrWhenNoCodeSection(context?.baseBranch),
|
|
6504
6400
|
``,
|
|
6505
6401
|
`### Autonomous Guidelines:`,
|
|
6506
6402
|
`- Make decisions independently \u2014 do not ask the team for approval at each step`,
|
|
@@ -6510,37 +6406,40 @@ function buildAutoPrompt(context, runnerMode) {
|
|
|
6510
6406
|
if (context) parts.push(...buildPropertyInstructions(context, runnerMode));
|
|
6511
6407
|
return parts.join("\n");
|
|
6512
6408
|
}
|
|
6409
|
+
function buildBuildingPrompt(context) {
|
|
6410
|
+
const parts = [
|
|
6411
|
+
`
|
|
6412
|
+
## Mode: Building`,
|
|
6413
|
+
`You are in Building mode \u2014 executing the plan.`,
|
|
6414
|
+
`- You have full coding access (read, write, edit, bash, git)`,
|
|
6415
|
+
`- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
|
|
6416
|
+
...context?.isParentTask ? [
|
|
6417
|
+
`- You are a parent task. Use \`list_subtasks\`, \`start_child_cloud_build\`, and subtask management tools to coordinate children.`,
|
|
6418
|
+
`- Do NOT implement code directly \u2014 fire child builds and review their work.`,
|
|
6419
|
+
`- Goal: coordinate child task execution and ensure all children complete successfully`
|
|
6420
|
+
] : [
|
|
6421
|
+
`- If this is a leaf task (no children): execute the plan directly`,
|
|
6422
|
+
`- Goal: implement the plan, run scoped verification, open a PR when done`,
|
|
6423
|
+
``,
|
|
6424
|
+
`### Pre-PR Verification Checklist`,
|
|
6425
|
+
`CI runs the FULL suite (lint, typecheck, all test shards) on every PR \u2014 do not duplicate it locally. Before calling \`mcp__conveyor__create_pull_request\`, scope verification to your diff:`,
|
|
6426
|
+
`1. \`bun run check\` \u2014 lint + typecheck (fast; run whenever you changed code)`,
|
|
6427
|
+
`2. \`bun run test:affected\` \u2014 runs only the tests your diff can affect (docs-only diffs run nothing; apps/api diffs run unit tests only since CI covers the int shards; shared/db diffs escalate to the full suite automatically)`,
|
|
6428
|
+
`Docs/markdown/.claude-only changes need NO local gates \u2014 open the PR and let CI validate.`,
|
|
6429
|
+
`If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates. Never run the full \`bun run test\` for a diff confined to one package.`,
|
|
6430
|
+
`For refactors: also run \`git diff ${context?.baseBranch ?? "dev"}..HEAD\` and confirm the public API surface (exports, function signatures) has no unintended breaking changes.`,
|
|
6431
|
+
...buildNoPrWhenNoCodeSection(context?.baseBranch),
|
|
6432
|
+
...context?.isAuto || !context?.plan?.trim() ? buildPlanDocumentationSection(context) : []
|
|
6433
|
+
]
|
|
6434
|
+
];
|
|
6435
|
+
return parts.join("\n");
|
|
6436
|
+
}
|
|
6513
6437
|
function buildModePrompt(agentMode, context, runnerMode) {
|
|
6514
6438
|
switch (agentMode) {
|
|
6515
6439
|
case "discovery":
|
|
6516
6440
|
return buildDiscoveryPrompt(context, runnerMode);
|
|
6517
|
-
case "building":
|
|
6518
|
-
|
|
6519
|
-
`
|
|
6520
|
-
## Mode: Building`,
|
|
6521
|
-
`You are in Building mode \u2014 executing the plan.`,
|
|
6522
|
-
`- You have full coding access (read, write, edit, bash, git)`,
|
|
6523
|
-
`- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
|
|
6524
|
-
...context?.isParentTask ? [
|
|
6525
|
-
`- You are a parent task. Use \`list_subtasks\`, \`start_child_cloud_build\`, and subtask management tools to coordinate children.`,
|
|
6526
|
-
`- Do NOT implement code directly \u2014 fire child builds and review their work.`,
|
|
6527
|
-
`- Goal: coordinate child task execution and ensure all children complete successfully`
|
|
6528
|
-
] : [
|
|
6529
|
-
`- If this is a leaf task (no children): execute the plan directly`,
|
|
6530
|
-
`- Goal: implement the plan, run scoped verification, open a PR when done`,
|
|
6531
|
-
``,
|
|
6532
|
-
`### Pre-PR Verification Checklist`,
|
|
6533
|
-
`CI runs the FULL suite (lint, typecheck, all test shards) on every PR \u2014 do not duplicate it locally. Before calling \`mcp__conveyor__create_pull_request\`, scope verification to your diff:`,
|
|
6534
|
-
`1. \`bun run check\` \u2014 lint + typecheck (fast; run whenever you changed code)`,
|
|
6535
|
-
`2. \`bun run test:affected\` \u2014 runs only the tests your diff can affect (docs-only diffs run nothing; apps/api diffs run unit tests only since CI covers the int shards; shared/db diffs escalate to the full suite automatically)`,
|
|
6536
|
-
`Docs/markdown/.claude-only changes need NO local gates \u2014 open the PR and let CI validate.`,
|
|
6537
|
-
`If a gate fails, fix it before opening the PR. Do NOT open PRs with known failing gates. Never run the full \`bun run test\` for a diff confined to one package.`,
|
|
6538
|
-
`For refactors: also run \`git diff ${context?.baseBranch ?? "dev"}..HEAD\` and confirm the public API surface (exports, function signatures) has no unintended breaking changes.`,
|
|
6539
|
-
...context?.isAuto || !context?.plan?.trim() ? buildPlanDocumentationSection(context) : []
|
|
6540
|
-
]
|
|
6541
|
-
];
|
|
6542
|
-
return parts.join("\n");
|
|
6543
|
-
}
|
|
6441
|
+
case "building":
|
|
6442
|
+
return buildBuildingPrompt(context);
|
|
6544
6443
|
case "review":
|
|
6545
6444
|
return buildReviewPrompt(context);
|
|
6546
6445
|
case "auto":
|
|
@@ -7005,7 +6904,7 @@ function buildFreshLeafInstructions(context, isAutoMode) {
|
|
|
7005
6904
|
`Your FIRST action should be reading the relevant source files mentioned in the plan, then writing code. Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`
|
|
7006
6905
|
] : [
|
|
7007
6906
|
`No plan is saved on this card yet. Investigate the task briefly (search first, read only critical files), then save a concise implementation plan with update_task_plan (file:line citations) BEFORE you start writing code.`,
|
|
7008
|
-
`
|
|
6907
|
+
`The plan is a step, not the goal: the moment it's posted, IMPLEMENT it in code \u2014 do NOT pause, wait for approval, or treat the posted plan as the deliverable. The card is already In Progress and advances automatically.`,
|
|
7009
6908
|
`Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`
|
|
7010
6909
|
];
|
|
7011
6910
|
parts.push(
|
|
@@ -7017,9 +6916,10 @@ function buildFreshLeafInstructions(context, isAutoMode) {
|
|
|
7017
6916
|
if (isAutoMode) {
|
|
7018
6917
|
parts.push(
|
|
7019
6918
|
`
|
|
7020
|
-
CRITICAL: You are in Auto mode.
|
|
7021
|
-
`Do NOT summarize the plan or say "ready to implement" \u2014 start implementing immediately.`,
|
|
7022
|
-
`
|
|
6919
|
+
CRITICAL: You are in Auto mode. Your job is to BUILD the change, not to produce a plan \u2014 making a plan and opening a PR of the plan is NOT the goal. The plan is only an intermediate step; you must then write the code that implements it.`,
|
|
6920
|
+
`Do NOT report status, ask for confirmation, or go idle without making code changes. Do NOT summarize the plan or say "ready to implement" \u2014 start implementing immediately.`,
|
|
6921
|
+
`Your pull request MUST contain the actual code implementation. Never open a plan-only or empty-diff PR. (If the task genuinely needs no code changes, do not open a PR \u2014 deliver the result in chat and mark the card Complete.)`,
|
|
6922
|
+
`When the implementation is complete and verified, you MUST use mcp__conveyor__create_pull_request to open a PR before finishing.`,
|
|
7023
6923
|
`If you are genuinely blocked, explain the specific blocker \u2014 do not go idle silently.`
|
|
7024
6924
|
);
|
|
7025
6925
|
}
|
|
@@ -7758,7 +7658,7 @@ function buildMutationTools(connection, config) {
|
|
|
7758
7658
|
|
|
7759
7659
|
// src/tools/attachment-tools.ts
|
|
7760
7660
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
7761
|
-
import { basename, extname, isAbsolute, join as
|
|
7661
|
+
import { basename, extname, isAbsolute, join as join6 } from "path";
|
|
7762
7662
|
import { z as z10 } from "zod";
|
|
7763
7663
|
var IMAGE_MIME_BY_EXT = {
|
|
7764
7664
|
".png": "image/png",
|
|
@@ -7775,9 +7675,9 @@ function buildUploadAttachmentTool(connection, config) {
|
|
|
7775
7675
|
path: z10.string().describe("Path to the image file \u2014 absolute, or relative to the workspace root"),
|
|
7776
7676
|
title: z10.string().optional().describe("Short caption posted with the image (defaults to the file name)")
|
|
7777
7677
|
},
|
|
7778
|
-
async ({ path:
|
|
7678
|
+
async ({ path: path2, title }) => {
|
|
7779
7679
|
try {
|
|
7780
|
-
const filePath = isAbsolute(
|
|
7680
|
+
const filePath = isAbsolute(path2) ? path2 : join6(config.workspaceDir, path2);
|
|
7781
7681
|
const mimeType = IMAGE_MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
7782
7682
|
if (!mimeType) {
|
|
7783
7683
|
return textResult(
|
|
@@ -8539,7 +8439,7 @@ function createConveyorMcpServer(harness, connection, config, context, agentMode
|
|
|
8539
8439
|
|
|
8540
8440
|
// src/harness/pty/adapters/types.ts
|
|
8541
8441
|
import { accessSync, constants, statSync } from "fs";
|
|
8542
|
-
import { join as
|
|
8442
|
+
import { join as join7 } from "path";
|
|
8543
8443
|
var TuiUnavailableError = class extends Error {
|
|
8544
8444
|
constructor(tui, message) {
|
|
8545
8445
|
super(message);
|
|
@@ -8548,10 +8448,10 @@ var TuiUnavailableError = class extends Error {
|
|
|
8548
8448
|
}
|
|
8549
8449
|
tui;
|
|
8550
8450
|
};
|
|
8551
|
-
function isExecutable(
|
|
8451
|
+
function isExecutable(path2) {
|
|
8552
8452
|
try {
|
|
8553
|
-
if (!statSync(
|
|
8554
|
-
accessSync(
|
|
8453
|
+
if (!statSync(path2).isFile()) return false;
|
|
8454
|
+
accessSync(path2, constants.X_OK);
|
|
8555
8455
|
return true;
|
|
8556
8456
|
} catch {
|
|
8557
8457
|
return false;
|
|
@@ -8563,7 +8463,7 @@ function findOnPath(binary, env = process.env) {
|
|
|
8563
8463
|
}
|
|
8564
8464
|
for (const dir of (env.PATH ?? "").split(":")) {
|
|
8565
8465
|
if (!dir) continue;
|
|
8566
|
-
const candidate =
|
|
8466
|
+
const candidate = join7(dir, binary);
|
|
8567
8467
|
if (isExecutable(candidate)) return candidate;
|
|
8568
8468
|
}
|
|
8569
8469
|
return null;
|
|
@@ -9427,10 +9327,10 @@ function resolveSessionStart(lineageKey, cwd) {
|
|
|
9427
9327
|
}
|
|
9428
9328
|
return { sessionId: sessionUuid };
|
|
9429
9329
|
}
|
|
9430
|
-
function repairTornSessionFile(
|
|
9330
|
+
function repairTornSessionFile(path2) {
|
|
9431
9331
|
try {
|
|
9432
|
-
if (!existsSync2(
|
|
9433
|
-
const content = readFileSync(
|
|
9332
|
+
if (!existsSync2(path2)) return false;
|
|
9333
|
+
const content = readFileSync(path2, "utf8");
|
|
9434
9334
|
if (content.length === 0) return false;
|
|
9435
9335
|
let keepEnd = content.length;
|
|
9436
9336
|
if (!content.endsWith("\n")) {
|
|
@@ -9449,9 +9349,9 @@ function repairTornSessionFile(path3) {
|
|
|
9449
9349
|
keepEnd = prevNewline + 1;
|
|
9450
9350
|
}
|
|
9451
9351
|
if (keepEnd === content.length) return false;
|
|
9452
|
-
truncateSync(
|
|
9352
|
+
truncateSync(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
|
|
9453
9353
|
logger2.warn("Repaired torn transcript before resume", {
|
|
9454
|
-
path:
|
|
9354
|
+
path: path2,
|
|
9455
9355
|
trimmedBytes: content.length - keepEnd
|
|
9456
9356
|
});
|
|
9457
9357
|
return true;
|
|
@@ -10002,6 +9902,7 @@ function buildPtyBridge(connection) {
|
|
|
10002
9902
|
sendOutput: (data, dims) => connection.sendPtyOutput(data, dims),
|
|
10003
9903
|
sendChatEvent: (event) => connection.sendPtyChatEvent(event),
|
|
10004
9904
|
sendEnded: () => connection.sendPtyEnded(),
|
|
9905
|
+
notifyAuthNotReady: (detail) => connection.postChatMessage(detail),
|
|
10005
9906
|
onInput: (handler) => connection.onPtyInput(handler),
|
|
10006
9907
|
onResize: (handler) => connection.onPtyResize(handler)
|
|
10007
9908
|
};
|
|
@@ -10072,6 +9973,16 @@ var QueryBridge = class {
|
|
|
10072
9973
|
resume() {
|
|
10073
9974
|
this._stopped = false;
|
|
10074
9975
|
}
|
|
9976
|
+
/**
|
|
9977
|
+
* Inject a follow-up message into the turn currently running under the harness
|
|
9978
|
+
* (PTY keep-alive) instead of aborting it. Returns true when the harness fed
|
|
9979
|
+
* the message into the live TUI; false when there is no running turn to inject
|
|
9980
|
+
* into or the harness has no live terminal (SDK) — the caller then supersedes
|
|
9981
|
+
* via stop() + respawn.
|
|
9982
|
+
*/
|
|
9983
|
+
injectIntoRunningTurn(content) {
|
|
9984
|
+
return this.harness.injectIntoRunningTurn?.(content) ?? false;
|
|
9985
|
+
}
|
|
10075
9986
|
/**
|
|
10076
9987
|
* Tear down any parked/active CLI process the harness is keeping alive between
|
|
10077
9988
|
* turns (PTY keep-alive). Called by SessionRunner on stop/shutdown so the
|
|
@@ -10249,7 +10160,7 @@ var QueryBridge = class {
|
|
|
10249
10160
|
|
|
10250
10161
|
// src/runner/session-runner-helpers.ts
|
|
10251
10162
|
import { readFileSync as readFileSync2 } from "fs";
|
|
10252
|
-
import { dirname as dirname2, join as
|
|
10163
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
10253
10164
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10254
10165
|
function mapChatHistory(messages) {
|
|
10255
10166
|
if (!messages) return [];
|
|
@@ -10278,7 +10189,7 @@ function readAgentVersion() {
|
|
|
10278
10189
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
10279
10190
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
10280
10191
|
try {
|
|
10281
|
-
const pkg = JSON.parse(readFileSync2(
|
|
10192
|
+
const pkg = JSON.parse(readFileSync2(join8(here, rel), "utf-8"));
|
|
10282
10193
|
if (pkg.version) return pkg.version;
|
|
10283
10194
|
} catch {
|
|
10284
10195
|
}
|
|
@@ -10581,10 +10492,10 @@ import { access, stat as stat2 } from "fs/promises";
|
|
|
10581
10492
|
var DEFAULT_FAILED_PATH = "/workspaces/.conveyor-git-failed";
|
|
10582
10493
|
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
10583
10494
|
var DEFAULT_POLL_MS = 200;
|
|
10584
|
-
async function fileExists(
|
|
10495
|
+
async function fileExists(path2) {
|
|
10585
10496
|
try {
|
|
10586
|
-
await access(
|
|
10587
|
-
const s = await stat2(
|
|
10497
|
+
await access(path2);
|
|
10498
|
+
const s = await stat2(path2);
|
|
10588
10499
|
return s.isFile();
|
|
10589
10500
|
} catch {
|
|
10590
10501
|
return false;
|
|
@@ -10699,9 +10610,9 @@ var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
|
|
|
10699
10610
|
async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
|
|
10700
10611
|
const sockets = [];
|
|
10701
10612
|
let readable = false;
|
|
10702
|
-
for (const
|
|
10613
|
+
for (const path2 of procPaths) {
|
|
10703
10614
|
try {
|
|
10704
|
-
const content = await readFile3(
|
|
10615
|
+
const content = await readFile3(path2, "utf8");
|
|
10705
10616
|
readable = true;
|
|
10706
10617
|
sockets.push(...parseProcNetTcpListeners(content));
|
|
10707
10618
|
} catch {
|
|
@@ -10928,7 +10839,7 @@ async function handlePullBranch(workDir, branch) {
|
|
|
10928
10839
|
|
|
10929
10840
|
// src/runner/heavy-gate.ts
|
|
10930
10841
|
import { readFileSync as readFileSync3 } from "fs";
|
|
10931
|
-
import
|
|
10842
|
+
import path from "path";
|
|
10932
10843
|
var GATE_KEYS = ["heavy", "test", "typecheck", "build"];
|
|
10933
10844
|
function runDir() {
|
|
10934
10845
|
return process.env.CONVEYOR_RUN_DIR ?? "/tmp/conveyor-run";
|
|
@@ -10944,7 +10855,7 @@ function pidAlive(pid) {
|
|
|
10944
10855
|
function isHeavyGateActive() {
|
|
10945
10856
|
for (const key of GATE_KEYS) {
|
|
10946
10857
|
try {
|
|
10947
|
-
const raw = readFileSync3(
|
|
10858
|
+
const raw = readFileSync3(path.join(runDir(), `${key}.pid`), "utf8").trim();
|
|
10948
10859
|
const pid = Number.parseInt(raw, 10);
|
|
10949
10860
|
if (Number.isInteger(pid) && pid > 0 && pidAlive(pid)) return true;
|
|
10950
10861
|
} catch {
|
|
@@ -10989,6 +10900,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
10989
10900
|
portDiscovery;
|
|
10990
10901
|
/** Main event-loop lag measurement, shared with the heartbeat worker. */
|
|
10991
10902
|
loopLag = new LoopLagMonitor();
|
|
10903
|
+
/** Boot supervisor handle, set post-construction once it's known (it is
|
|
10904
|
+
* started alongside connect(), before the runner itself exists in some
|
|
10905
|
+
* call sites) — notified when the core loop goes live so it can release
|
|
10906
|
+
* the app's start command. Optional: never set in tests/paths that don't
|
|
10907
|
+
* wire a supervisor. */
|
|
10908
|
+
workspaceCommands = null;
|
|
10992
10909
|
constructor(config, callbacks, deps = {}) {
|
|
10993
10910
|
this.config = config;
|
|
10994
10911
|
this.callbacks = callbacks;
|
|
@@ -11035,6 +10952,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
11035
10952
|
get isStopped() {
|
|
11036
10953
|
return this.stopped;
|
|
11037
10954
|
}
|
|
10955
|
+
/** Wire the boot supervisor handle post-construction — cli.ts constructs it
|
|
10956
|
+
* via startWorkspaceCommandsAfterConnect() (which needs connect() to already
|
|
10957
|
+
* be callable), so it can't be a constructor dependency. */
|
|
10958
|
+
setWorkspaceCommands(supervisor) {
|
|
10959
|
+
this.workspaceCommands = supervisor;
|
|
10960
|
+
}
|
|
11038
10961
|
// ── Main lifecycle ─────────────────────────────────────────────────
|
|
11039
10962
|
/**
|
|
11040
10963
|
* Establish the API connection, wire callbacks, and join the session room.
|
|
@@ -11112,39 +11035,27 @@ var SessionRunner = class _SessionRunner {
|
|
|
11112
11035
|
await this.shutdown("error");
|
|
11113
11036
|
return;
|
|
11114
11037
|
}
|
|
11115
|
-
if (
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11038
|
+
if (this.fullContext?.githubBranch) {
|
|
11039
|
+
const ok = await ensureOnTaskBranch(
|
|
11040
|
+
this.config.workspaceDir,
|
|
11041
|
+
this.fullContext.githubBranch,
|
|
11042
|
+
this.fullContext.baseBranch ?? void 0
|
|
11043
|
+
);
|
|
11044
|
+
if (!ok) {
|
|
11045
|
+
process.stderr.write("[conveyor-agent] WARNING: task-branch checkout failed\n");
|
|
11121
11046
|
}
|
|
11122
11047
|
}
|
|
11123
11048
|
if (!this.stopped) {
|
|
11124
11049
|
this.lifecycle.startGitFlush();
|
|
11125
11050
|
}
|
|
11126
11051
|
if (this.fullContext?.githubBranch) {
|
|
11127
|
-
const
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
if (restore.source !== "clean") {
|
|
11134
|
-
process.stderr.write(
|
|
11135
|
-
`[conveyor-agent] WorkPreservation restore: source=${restore.source}${restore.fileCount === void 0 ? "" : ` files=${restore.fileCount}`}
|
|
11136
|
-
`
|
|
11137
|
-
);
|
|
11138
|
-
}
|
|
11139
|
-
} else {
|
|
11140
|
-
const restored = await restoreWipSnapshot(
|
|
11141
|
-
this.config.workspaceDir,
|
|
11142
|
-
this.fullContext.githubBranch
|
|
11143
|
-
);
|
|
11144
|
-
if (restored !== "none") {
|
|
11145
|
-
process.stderr.write(`[conveyor-agent] WIP snapshot restore: ${restored}
|
|
11052
|
+
const restored = await restoreWipSnapshot(
|
|
11053
|
+
this.config.workspaceDir,
|
|
11054
|
+
this.fullContext.githubBranch
|
|
11055
|
+
);
|
|
11056
|
+
if (restored !== "none") {
|
|
11057
|
+
process.stderr.write(`[conveyor-agent] WIP snapshot restore: ${restored}
|
|
11146
11058
|
`);
|
|
11147
|
-
}
|
|
11148
11059
|
}
|
|
11149
11060
|
}
|
|
11150
11061
|
this.mode.applyServerMode(this.fullContext?.agentMode, this.fullContext?.isAuto);
|
|
@@ -11179,6 +11090,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
11179
11090
|
`
|
|
11180
11091
|
);
|
|
11181
11092
|
}
|
|
11093
|
+
this.workspaceCommands?.notifyLoopReady();
|
|
11182
11094
|
while (!this.stopped) {
|
|
11183
11095
|
if (this._state !== "idle") await this.setState("idle");
|
|
11184
11096
|
await this.coreLoop();
|
|
@@ -11248,6 +11160,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
11248
11160
|
this.interrupted = false;
|
|
11249
11161
|
continue;
|
|
11250
11162
|
}
|
|
11163
|
+
await this.flushWipNow("WIP: end of turn");
|
|
11251
11164
|
if (!this.stopped) await this.setState("idle");
|
|
11252
11165
|
} else if (this._state === "error") {
|
|
11253
11166
|
await this.setState("idle");
|
|
@@ -11274,6 +11187,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
11274
11187
|
"[conveyor-agent] Completed \u2014 entering dormant idle (staying connected)\n"
|
|
11275
11188
|
);
|
|
11276
11189
|
}
|
|
11190
|
+
await this.flushWipNow("WIP: turn complete");
|
|
11277
11191
|
this.pendingMessages.length = 0;
|
|
11278
11192
|
if (this._state !== "idle") await this.setState("idle");
|
|
11279
11193
|
const remainingMs = Math.max(0, this.dormantDeadline - Date.now());
|
|
@@ -11427,13 +11341,36 @@ var SessionRunner = class _SessionRunner {
|
|
|
11427
11341
|
const resolve = this.inputResolver;
|
|
11428
11342
|
this.inputResolver = null;
|
|
11429
11343
|
resolve(msg);
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11344
|
+
return;
|
|
11345
|
+
}
|
|
11346
|
+
if (this._state === "running" && this.canInjectIntoRunningTurn(msg) && this.queryBridge?.injectIntoRunningTurn(msg.content)) {
|
|
11347
|
+
void this.callbacks.onEvent({
|
|
11348
|
+
type: "user_message",
|
|
11349
|
+
content: msg.content,
|
|
11350
|
+
userId: msg.userId
|
|
11351
|
+
});
|
|
11352
|
+
return;
|
|
11353
|
+
}
|
|
11354
|
+
this.pendingMessages.push(msg);
|
|
11355
|
+
if (this._state === "running" || this._state === "waiting_for_input") {
|
|
11356
|
+
this.queryBridge?.stop();
|
|
11435
11357
|
}
|
|
11436
11358
|
}
|
|
11359
|
+
/**
|
|
11360
|
+
* Whether a mid-turn message may be pasted into the live running TUI rather
|
|
11361
|
+
* than superseding the turn. Restricted to genuine same-mode user follow-ups:
|
|
11362
|
+
* a pending mode restart, an empty body, a prefill hint, or any non-"user"
|
|
11363
|
+
* source (mode_change / pty_passive / system / ci_failure / review_trigger)
|
|
11364
|
+
* must take the abort+respawn path — a mode/fingerprint change needs a fresh
|
|
11365
|
+
* spawn, and a prefill must park unsubmitted for the human.
|
|
11366
|
+
*/
|
|
11367
|
+
canInjectIntoRunningTurn(msg) {
|
|
11368
|
+
if (this.mode.pendingModeRestart) return false;
|
|
11369
|
+
if (!msg.content.trim()) return false;
|
|
11370
|
+
if (msg.delivery === "prefill") return false;
|
|
11371
|
+
if (msg.source && msg.source !== "user") return false;
|
|
11372
|
+
return true;
|
|
11373
|
+
}
|
|
11437
11374
|
// ── Query execution with abort handling ────────────────────────────
|
|
11438
11375
|
/** Run queryBridge.execute, swallowing abort errors from stop/softStop. */
|
|
11439
11376
|
async executeQuery(followUpContent, promptDelivery) {
|
|
@@ -11470,6 +11407,36 @@ var SessionRunner = class _SessionRunner {
|
|
|
11470
11407
|
}
|
|
11471
11408
|
}
|
|
11472
11409
|
// ── Stop / soft-stop ───────────────────────────────────────────────
|
|
11410
|
+
/** Shared token-refresh closure for the git-flush call sites (periodic
|
|
11411
|
+
* backstop + per-turn flush). Never throws — returns undefined so the
|
|
11412
|
+
* caller falls back to its existing remote credentials. */
|
|
11413
|
+
async refreshGithubTokenForFlush() {
|
|
11414
|
+
try {
|
|
11415
|
+
const res = await this.connection.call("refreshGithubToken", {
|
|
11416
|
+
sessionId: this.connection.sessionId
|
|
11417
|
+
});
|
|
11418
|
+
return res.token;
|
|
11419
|
+
} catch {
|
|
11420
|
+
return void 0;
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
/** Push uncommitted work to the conveyor-wip ref after a turn. Git-only (no
|
|
11424
|
+
* GCS). Primary-branch only — cheap enough to run every turn. Shares the
|
|
11425
|
+
* in-flight guard with the periodic backstop so a turn-end flush and a
|
|
11426
|
+
* timer tick never overlap. Best-effort, never throws. */
|
|
11427
|
+
async flushWipNow(wipMessage) {
|
|
11428
|
+
if (this.periodicFlushInFlight || this.stopped) return;
|
|
11429
|
+
this.periodicFlushInFlight = true;
|
|
11430
|
+
try {
|
|
11431
|
+
await flushPendingChanges(this.config.workspaceDir, {
|
|
11432
|
+
wipMessage,
|
|
11433
|
+
refreshToken: () => this.refreshGithubTokenForFlush()
|
|
11434
|
+
});
|
|
11435
|
+
} catch {
|
|
11436
|
+
} finally {
|
|
11437
|
+
this.periodicFlushInFlight = false;
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11473
11440
|
/** Periodic best-effort WIP commit + push during normal agent execution.
|
|
11474
11441
|
* Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
|
|
11475
11442
|
* the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
|
|
@@ -11488,16 +11455,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
11488
11455
|
try {
|
|
11489
11456
|
const result = await flushAllPendingWork(this.config.workspaceDir, {
|
|
11490
11457
|
wipMessage: "WIP: periodic auto-commit",
|
|
11491
|
-
refreshToken:
|
|
11492
|
-
try {
|
|
11493
|
-
const res = await this.connection.call("refreshGithubToken", {
|
|
11494
|
-
sessionId: this.connection.sessionId
|
|
11495
|
-
});
|
|
11496
|
-
return res.token;
|
|
11497
|
-
} catch {
|
|
11498
|
-
return void 0;
|
|
11499
|
-
}
|
|
11500
|
-
}
|
|
11458
|
+
refreshToken: () => this.refreshGithubTokenForFlush()
|
|
11501
11459
|
});
|
|
11502
11460
|
if (result.hadWork) {
|
|
11503
11461
|
process.stderr.write(
|
|
@@ -11505,7 +11463,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
11505
11463
|
`
|
|
11506
11464
|
);
|
|
11507
11465
|
}
|
|
11508
|
-
await this.uploadGcsSnapshotIfConfigured();
|
|
11509
11466
|
} catch {
|
|
11510
11467
|
} finally {
|
|
11511
11468
|
this.periodicFlushInFlight = false;
|
|
@@ -11531,22 +11488,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
11531
11488
|
});
|
|
11532
11489
|
}
|
|
11533
11490
|
}
|
|
11534
|
-
/** PUT the WorkPreservation snapshot tar to the bundle's capability URL
|
|
11535
|
-
* when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
|
|
11536
|
-
async uploadGcsSnapshotIfConfigured() {
|
|
11537
|
-
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
11538
|
-
if (!uploadUrl) return;
|
|
11539
|
-
try {
|
|
11540
|
-
const gcs = await uploadSnapshotToGcs(this.config.workspaceDir, uploadUrl);
|
|
11541
|
-
if (gcs.uploaded) {
|
|
11542
|
-
process.stderr.write(
|
|
11543
|
-
`[conveyor-agent] WorkPreservation GCS snapshot: files=${gcs.fileCount} bytes=${gcs.sizeBytes}
|
|
11544
|
-
`
|
|
11545
|
-
);
|
|
11546
|
-
}
|
|
11547
|
-
} catch {
|
|
11548
|
-
}
|
|
11549
|
-
}
|
|
11550
11491
|
/** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
|
|
11551
11492
|
* when a claudespace pod is killed. Must be called BEFORE stop() so the
|
|
11552
11493
|
* connection is still alive for token refresh. Never throws. */
|
|
@@ -11571,7 +11512,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
11571
11512
|
`
|
|
11572
11513
|
);
|
|
11573
11514
|
}
|
|
11574
|
-
await this.uploadGcsSnapshotIfConfigured();
|
|
11575
11515
|
} catch (err) {
|
|
11576
11516
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11577
11517
|
process.stderr.write(`[conveyor-agent] Shutdown git flush failed: ${msg}
|
|
@@ -11719,37 +11659,6 @@ var SessionRunner = class _SessionRunner {
|
|
|
11719
11659
|
this.connection.onPullBranch(({ branch }) => {
|
|
11720
11660
|
void handlePullBranch(this.config.workspaceDir, branch);
|
|
11721
11661
|
});
|
|
11722
|
-
this.connection.onFinalizeSnapshot(() => void this.finalizeSnapshotNow());
|
|
11723
|
-
}
|
|
11724
|
-
/** Eager finalize snapshot triggered by the reconciler's sleep signal
|
|
11725
|
-
* (session:finalizeSnapshot). Runs a full workspace capture and uploads it
|
|
11726
|
-
* NOW so the sleep confirms on this snapshot instead of waiting for the
|
|
11727
|
-
* ~2min periodic flush. Sidecar DB state is intentionally NOT captured — it
|
|
11728
|
-
* is no longer durable across sleep/wake (#2623); only the agent's
|
|
11729
|
-
* /workspace rides the snapshot. Best-effort: on failure the
|
|
11730
|
-
* periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
|
|
11731
|
-
async finalizeSnapshotNow() {
|
|
11732
|
-
const uploadUrl = process.env.CONVEYOR_SNAPSHOT_UPLOAD_URL;
|
|
11733
|
-
if (!uploadUrl || this.stopped) return;
|
|
11734
|
-
try {
|
|
11735
|
-
await finalizeForSleep({
|
|
11736
|
-
cwd: this.config.workspaceDir,
|
|
11737
|
-
branch: this.fullContext?.githubBranch ?? "",
|
|
11738
|
-
snapshotUploadUrl: uploadUrl,
|
|
11739
|
-
refreshToken: async () => {
|
|
11740
|
-
try {
|
|
11741
|
-
const res = await this.connection.call("refreshGithubToken", {
|
|
11742
|
-
sessionId: this.connection.sessionId
|
|
11743
|
-
});
|
|
11744
|
-
return res.token;
|
|
11745
|
-
} catch {
|
|
11746
|
-
return void 0;
|
|
11747
|
-
}
|
|
11748
|
-
}
|
|
11749
|
-
});
|
|
11750
|
-
process.stderr.write("[conveyor-agent] Finalize snapshot uploaded on sleep signal\n");
|
|
11751
|
-
} catch {
|
|
11752
|
-
}
|
|
11753
11662
|
}
|
|
11754
11663
|
/** Proactively refresh the GitHub token before the 1-hour expiry. */
|
|
11755
11664
|
async refreshGithubToken() {
|
|
@@ -11845,12 +11754,12 @@ var SessionRunner = class _SessionRunner {
|
|
|
11845
11754
|
};
|
|
11846
11755
|
|
|
11847
11756
|
// src/setup/config.ts
|
|
11848
|
-
import { join as
|
|
11757
|
+
import { join as join9 } from "path";
|
|
11849
11758
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
11850
11759
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
11851
11760
|
async function loadForwardPorts(workspaceDir) {
|
|
11852
11761
|
try {
|
|
11853
|
-
const raw = await readWorkspaceFile(
|
|
11762
|
+
const raw = await readWorkspaceFile(join9(workspaceDir, DEVCONTAINER_PATH));
|
|
11854
11763
|
const parsed = JSON.parse(raw);
|
|
11855
11764
|
const ports = (parsed.forwardPorts ?? []).filter(
|
|
11856
11765
|
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
@@ -11938,16 +11847,10 @@ export {
|
|
|
11938
11847
|
sampleKeyUsage,
|
|
11939
11848
|
awaitGitReady,
|
|
11940
11849
|
PortDiscovery,
|
|
11941
|
-
uploadSnapshotToGcs,
|
|
11942
|
-
captureSnapshot,
|
|
11943
|
-
startPeriodic,
|
|
11944
|
-
stop,
|
|
11945
|
-
finalizeForSleep,
|
|
11946
|
-
restoreOnBoot,
|
|
11947
11850
|
SessionRunner,
|
|
11948
11851
|
loadForwardPorts,
|
|
11949
11852
|
buildSessionPreviewPorts,
|
|
11950
11853
|
loadConveyorConfig,
|
|
11951
11854
|
unshallowRepo
|
|
11952
11855
|
};
|
|
11953
|
-
//# sourceMappingURL=chunk-
|
|
11856
|
+
//# sourceMappingURL=chunk-J4UH5TOO.js.map
|