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