@rallycry/conveyor-agent 10.13.71 → 11.0.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.
@@ -2,21 +2,24 @@ import {
2
2
  TOMBSTONE_MESSAGE,
3
3
  isLegacyEntrypointLaunch
4
4
  } from "./chunk-R3FDJQL6.js";
5
+ import {
6
+ readAgentVersion
7
+ } from "./chunk-XORJ6SII.js";
5
8
  import {
6
9
  GitPrepJob,
7
10
  defaultGit,
8
11
  ensureDir,
9
- readAgentVersion,
10
12
  redactToken,
11
13
  reportBootMilestone
12
- } from "./chunk-QU53HND5.js";
14
+ } from "./chunk-GL2DIQEQ.js";
13
15
  import {
14
16
  workbenchPort
15
17
  } from "./chunk-KMB3BU4S.js";
16
18
  import {
17
19
  startWorkbenchServer
18
- } from "./chunk-DOB2XE2I.js";
19
- import "./chunk-GJXAAPJ6.js";
20
+ } from "./chunk-UBDSLM44.js";
21
+ import "./chunk-3F4ZZKCA.js";
22
+ import "./chunk-W4LZ7R6Z.js";
20
23
  import {
21
24
  DEFAULT_WORKBENCH_PORT
22
25
  } from "./chunk-6Q6LQBWO.js";
@@ -98,6 +101,8 @@ function decodeJwtClaims(jwt) {
98
101
  function resolveModeEnv(claims) {
99
102
  if (claims.mode === "review") return { CONVEYOR_MODE: "code-review" };
100
103
  if (claims.mode === "pack") return { CONVEYOR_MODE: "pack" };
104
+ if (claims.mode === "plan") return { CONVEYOR_MODE: "plan" };
105
+ if (claims.mode === "serving") return { CONVEYOR_MODE: "serving" };
101
106
  if (claims.mode === "adhoc") return { CONVEYOR_MODE: "adhoc" };
102
107
  if (!claims.taskId && claims.projectId) return { CONVEYOR_MODE: "pm" };
103
108
  return {};
@@ -849,6 +854,7 @@ async function startWorkspaceSshd(exec, log, port = process.env.CONVEYOR_WORKSPA
849
854
  // src/boot/workspace-extras.ts
850
855
  import {
851
856
  appendFileSync as appendFileSync3,
857
+ cpSync,
852
858
  existsSync as existsSync3,
853
859
  lstatSync as lstatSync2,
854
860
  mkdirSync as mkdirSync3,
@@ -859,7 +865,8 @@ import {
859
865
  statSync as statSync3,
860
866
  symlinkSync as symlinkSync2
861
867
  } from "fs";
862
- import { join as join5 } from "path";
868
+ import { dirname as dirname2, join as join5 } from "path";
869
+ import { fileURLToPath } from "url";
863
870
  function errText(err) {
864
871
  return redactToken(err instanceof Error ? err.message : String(err));
865
872
  }
@@ -1013,6 +1020,49 @@ function linkGrimoireSkills(workspace, log) {
1013
1020
  excludeLinkedSkills(workspace, linkedNames, log);
1014
1021
  log.info(`[boot] Linked ${linkedNames.length} grimoire skills into ${targetDir}`);
1015
1022
  }
1023
+ function bundledSkillsDir(moduleUrl = import.meta.url) {
1024
+ try {
1025
+ const dir = join5(dirname2(fileURLToPath(moduleUrl)), "..", "skills");
1026
+ return isDir2(dir) ? dir : null;
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function materializeBundledSkills(workspace, log, sourceDirOverride) {
1032
+ const sourceDir = sourceDirOverride === void 0 ? bundledSkillsDir() : sourceDirOverride;
1033
+ if (!sourceDir) {
1034
+ log.warn(
1035
+ "[boot] WARN: no bundled skills payload in this agent build. Cloud modes that invoke a skill will have no workflow to follow. This is a packaging fault (tsup copySkillsPayload did not run), not a workspace problem."
1036
+ );
1037
+ return;
1038
+ }
1039
+ const targetDir = join5(workspace, ".claude", "skills");
1040
+ try {
1041
+ mkdirSync3(targetDir, { recursive: true });
1042
+ } catch {
1043
+ log.warn(`[boot] WARN: unable to create ${targetDir}; skipping bundled skills`);
1044
+ return;
1045
+ }
1046
+ const copied = [];
1047
+ let names = [];
1048
+ try {
1049
+ names = readdirSync2(sourceDir);
1050
+ } catch {
1051
+ return;
1052
+ }
1053
+ for (const name of names) {
1054
+ if (!isDir2(join5(sourceDir, name))) continue;
1055
+ const target = join5(targetDir, name);
1056
+ if (existsSync3(target) || isSymlink(target)) continue;
1057
+ try {
1058
+ cpSync(join5(sourceDir, name), target, { recursive: true, dereference: true });
1059
+ copied.push(name);
1060
+ } catch {
1061
+ }
1062
+ }
1063
+ excludeLinkedSkills(workspace, copied, log);
1064
+ log.info(`[boot] Materialized ${copied.length} bundled skills into ${targetDir}`);
1065
+ }
1016
1066
  async function neutralizeGrimoirePointer(workspace, git, log) {
1017
1067
  if (!existsSync3(join5(workspace, ".claude", "grimoire"))) return;
1018
1068
  try {
@@ -1054,6 +1104,7 @@ async function runPreReadyBinds(workspace, bundle, git, log) {
1054
1104
  bindGraphifyBundle(workspace, graphifyEnv, log);
1055
1105
  await ensureGrimoireSubmodule(workspace, bundle.githubToken ?? "", git, log);
1056
1106
  linkGrimoireSkills(workspace, log);
1107
+ materializeBundledSkills(workspace, log);
1057
1108
  await neutralizeGrimoirePointer(workspace, git, log);
1058
1109
  }
1059
1110
  async function cloneReferenceRepos(refsJson, git, log, referencesDir = "/workspaces/references") {
@@ -0,0 +1,291 @@
1
+ import {
2
+ ghHostsManagedByConveyor,
3
+ githubTokenFilePath
4
+ } from "./chunk-W4LZ7R6Z.js";
5
+
6
+ // src/harness/pty/pty-support.ts
7
+ import { stat } from "fs/promises";
8
+ import { tmpdir } from "os";
9
+
10
+ // src/harness/pty/spawn-args.ts
11
+ function resolveClaudeBinary() {
12
+ return process.env.CONVEYOR_CLAUDE_BIN ?? "claude";
13
+ }
14
+ function buildSpawnArgs(input) {
15
+ const args = [];
16
+ if (input.resume) {
17
+ args.push("--resume", input.resume);
18
+ } else if (input.sessionId) {
19
+ args.push("--session-id", input.sessionId);
20
+ }
21
+ args.push("--model", input.model);
22
+ if (input.permissionMode === "bypassPermissions") {
23
+ args.push("--dangerously-skip-permissions");
24
+ } else {
25
+ args.push("--permission-mode", "plan");
26
+ }
27
+ args.push("--settings", input.settingsPath);
28
+ if (input.appendSystemPrompt) {
29
+ args.push("--append-system-prompt", input.appendSystemPrompt);
30
+ }
31
+ if (input.mcpConfigPath) {
32
+ args.push("--mcp-config", input.mcpConfigPath);
33
+ if (input.strictMcpConfig) {
34
+ args.push("--strict-mcp-config");
35
+ }
36
+ }
37
+ return args;
38
+ }
39
+ function spawnOptionsFingerprint(input) {
40
+ return JSON.stringify([
41
+ input.model,
42
+ input.permissionMode,
43
+ input.appendSystemPrompt ?? "",
44
+ input.cwd
45
+ ]);
46
+ }
47
+ var ANSI_CSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
48
+ function cleanTerminalOutput(raw, maxChars = 1200) {
49
+ const noAnsi = raw.replace(ANSI_CSI, "");
50
+ let out = "";
51
+ for (const ch of noAnsi) {
52
+ const code = ch.charCodeAt(0);
53
+ if (ch === "\r" || ch === "\n") out += "\n";
54
+ else if (ch === " ") out += ch;
55
+ else if (code < 32 || code === 127) continue;
56
+ else out += ch;
57
+ }
58
+ const lines = out.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
59
+ const text = lines.join("\n").trim();
60
+ return text.length > maxChars ? `\u2026${text.slice(-maxChars)}` : text;
61
+ }
62
+ function isMissingBinaryFailure(tail) {
63
+ return /execvp\(\d+\) failed|no such file or directory|command not found/i.test(tail);
64
+ }
65
+ function buildExitErrors(exitCode, rawOutput, binary) {
66
+ const errors = [`claude exited (code ${exitCode}) without a result`];
67
+ const tail = cleanTerminalOutput(rawOutput);
68
+ if (isMissingBinaryFailure(tail)) {
69
+ errors.push(
70
+ `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.`
71
+ );
72
+ }
73
+ if (tail) {
74
+ errors.push(`Last terminal output before exit:
75
+ ${tail}`);
76
+ }
77
+ return errors;
78
+ }
79
+
80
+ // src/harness/pty/pty-support.ts
81
+ var MAX_DIAGNOSTIC_OUTPUT = 4e3;
82
+ var MAX_BETWEEN_TURN_BUFFER = 500;
83
+ var SUBMIT_SETTLE_MS = 300;
84
+ var SUBMIT_NUDGE_INTERVAL_MS = 2e3;
85
+ var SUBMIT_NUDGE_MAX_PRESSES = 5;
86
+ var SUBMIT_NUDGE_SLOW_INTERVAL_MS = 5e3;
87
+ var SUBMIT_NUDGE_WINDOW_MS = 9e4;
88
+ var SUBMIT_REDELIVERY_MAX_ATTEMPTS = 2;
89
+ var PLAN_DIALOG_FIRST_PRESS_MS = 700;
90
+ var PLAN_DIALOG_INTERVAL_MS = 1500;
91
+ var PLAN_DIALOG_SLOW_INTERVAL_MS = 5e3;
92
+ var PLAN_DIALOG_FAST_WINDOW_MS = 1e4;
93
+ var PLAN_DIALOG_WINDOW_MS = 9e4;
94
+ var RAW_TUI_PROBE_SENTINEL = "zqx";
95
+ var RAW_TUI_PROBE_ACK_MS = 500;
96
+ var RAW_TUI_PROBE_RETRY_MS = 250;
97
+ var RAW_TUI_PROBE_POLL_MS = 20;
98
+ var RAW_TUI_FIRST_OUTPUT_MAX_MS = 1e4;
99
+ var RAW_TUI_INPUT_LIVE_MAX_MS = 2e4;
100
+ function envMs(name, fallback) {
101
+ const raw = Number(process.env[name]);
102
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
103
+ }
104
+ function resolveSubmitSettleMs() {
105
+ return envMs("CONVEYOR_PTY_SUBMIT_SETTLE_MS", SUBMIT_SETTLE_MS);
106
+ }
107
+ function resolveRawTuiProbeTiming() {
108
+ const sentinel = process.env.CONVEYOR_PTY_RAW_PROBE_SENTINEL;
109
+ return {
110
+ // A sentinel must be non-empty (it is what we search for) and must stay
111
+ // free of digits and control chars — see the note on menu dialogs above.
112
+ sentinel: sentinel && /^[a-z]{1,8}$/.test(sentinel) ? sentinel : RAW_TUI_PROBE_SENTINEL,
113
+ ackMs: envMs("CONVEYOR_PTY_RAW_PROBE_ACK_MS", RAW_TUI_PROBE_ACK_MS),
114
+ retryMs: envMs("CONVEYOR_PTY_RAW_PROBE_RETRY_MS", RAW_TUI_PROBE_RETRY_MS),
115
+ pollMs: envMs("CONVEYOR_PTY_RAW_PROBE_POLL_MS", RAW_TUI_PROBE_POLL_MS),
116
+ firstOutputMaxMs: envMs("CONVEYOR_PTY_RAW_FIRST_OUTPUT_MAX_MS", RAW_TUI_FIRST_OUTPUT_MAX_MS),
117
+ maxMs: envMs("CONVEYOR_PTY_RAW_INPUT_LIVE_MAX_MS", RAW_TUI_INPUT_LIVE_MAX_MS)
118
+ };
119
+ }
120
+ function sentinelEchoed(rawOutput, sentinel) {
121
+ return cleanTerminalOutput(rawOutput, Number.MAX_SAFE_INTEGER).includes(sentinel);
122
+ }
123
+ var DEC_PRIVATE_MODE = new RegExp(`${String.fromCharCode(27)}\\[\\?[0-9;]+[hl]`);
124
+ function sawTerminalSetup(rawOutput) {
125
+ return DEC_PRIVATE_MODE.test(rawOutput);
126
+ }
127
+ function needsRawReadyGate(caps) {
128
+ return caps.rawPromptGate;
129
+ }
130
+ function resolveSubmitNudgeTiming() {
131
+ return {
132
+ intervalMs: envMs("CONVEYOR_PTY_NUDGE_INTERVAL_MS", SUBMIT_NUDGE_INTERVAL_MS),
133
+ slowIntervalMs: envMs("CONVEYOR_PTY_NUDGE_SLOW_INTERVAL_MS", SUBMIT_NUDGE_SLOW_INTERVAL_MS),
134
+ maxPresses: SUBMIT_NUDGE_MAX_PRESSES,
135
+ windowMs: envMs("CONVEYOR_PTY_NUDGE_WINDOW_MS", SUBMIT_NUDGE_WINDOW_MS)
136
+ };
137
+ }
138
+ function resolveSubmitRedeliveryMaxAttempts() {
139
+ const value = process.env.CONVEYOR_PTY_SUBMIT_REDELIVERY_MAX;
140
+ if (!value) return SUBMIT_REDELIVERY_MAX_ATTEMPTS;
141
+ const raw = Number(value);
142
+ return Number.isFinite(raw) && raw >= 0 ? raw : SUBMIT_REDELIVERY_MAX_ATTEMPTS;
143
+ }
144
+ function resolvePlanDialogTiming() {
145
+ return {
146
+ firstPressMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FIRST_PRESS_MS", PLAN_DIALOG_FIRST_PRESS_MS),
147
+ intervalMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_INTERVAL_MS", PLAN_DIALOG_INTERVAL_MS),
148
+ slowIntervalMs: envMs(
149
+ "CONVEYOR_PTY_PLAN_DIALOG_SLOW_INTERVAL_MS",
150
+ PLAN_DIALOG_SLOW_INTERVAL_MS
151
+ ),
152
+ fastWindowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_FAST_WINDOW_MS", PLAN_DIALOG_FAST_WINDOW_MS),
153
+ windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
154
+ };
155
+ }
156
+ function turnOptionsFrom(options) {
157
+ return {
158
+ canUseTool: options.canUseTool,
159
+ promptDelivery: options.promptDelivery,
160
+ planDialogAutoAccept: options.planDialogAutoAccept,
161
+ abortController: options.abortController
162
+ };
163
+ }
164
+ var KILL_ESCALATION_MS = 5e3;
165
+ function killPtyWithEscalation(pty, hasExited, escalationMs = KILL_ESCALATION_MS) {
166
+ try {
167
+ pty.kill();
168
+ } catch {
169
+ }
170
+ const timer = setTimeout(() => {
171
+ if (hasExited()) return;
172
+ try {
173
+ pty.kill("SIGKILL");
174
+ } catch {
175
+ }
176
+ }, escalationMs);
177
+ timer.unref?.();
178
+ }
179
+ function isRecord(value) {
180
+ return typeof value === "object" && value !== null;
181
+ }
182
+ function extractSpawn(mod) {
183
+ if (!isRecord(mod)) return null;
184
+ if (typeof mod.spawn === "function") return mod.spawn;
185
+ const def = mod.default;
186
+ if (isRecord(def) && typeof def.spawn === "function") return def.spawn;
187
+ return null;
188
+ }
189
+ async function loadPtySpawn() {
190
+ const mod = await import("node-pty");
191
+ const spawn = extractSpawn(mod);
192
+ if (!spawn) throw new Error("node-pty: spawn export not found");
193
+ return spawn;
194
+ }
195
+ async function resolvePtySpawn() {
196
+ const { workbenchEnabled } = await import("./mode-ZJSOSLGU.js");
197
+ if (!workbenchEnabled()) return loadPtySpawn();
198
+ const { getWorkbenchClient } = await import("./client-IG6C5F2G.js");
199
+ return (file, args, options) => getWorkbenchClient().spawnPty(file, args, options);
200
+ }
201
+ function sessionTempBase() {
202
+ return process.env.CONVEYOR_SHARED_DIR ?? tmpdir();
203
+ }
204
+ function inheritedEnv(socketPath) {
205
+ const env = {};
206
+ for (const [key, value] of Object.entries(process.env)) {
207
+ if (typeof value === "string") env[key] = value;
208
+ }
209
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
210
+ delete env.ANTHROPIC_API_KEY;
211
+ }
212
+ if (ghHostsManagedByConveyor() && env.CONVEYOR_KEEP_GH_TOKEN_ENV !== "1") {
213
+ delete env.GH_TOKEN;
214
+ delete env.GITHUB_TOKEN;
215
+ delete env.CONVEYOR_GITHUB_TOKEN;
216
+ env.CONVEYOR_GITHUB_TOKEN_FILE = githubTokenFilePath();
217
+ }
218
+ if (socketPath) {
219
+ env.CONVEYOR_HOOK_SOCKET = socketPath;
220
+ }
221
+ env.MCP_TIMEOUT ??= "60000";
222
+ env.MCP_TOOL_TIMEOUT ??= "180000";
223
+ return env;
224
+ }
225
+ function buildPromptBytes(text) {
226
+ return `\x1B[200~${text}\x1B[201~`;
227
+ }
228
+ function renderPromptContentText(content) {
229
+ return content.map((block) => {
230
+ const b = block;
231
+ if (b?.type === "text" && typeof b.text === "string") return b.text;
232
+ if (b?.type === "image") {
233
+ return `[Image attachment \u2014 use list_task_files / get_attachment to view]`;
234
+ }
235
+ return JSON.stringify(block);
236
+ }).join("\n\n");
237
+ }
238
+ async function transcriptSize(path) {
239
+ try {
240
+ return (await stat(path)).size;
241
+ } catch {
242
+ return 0;
243
+ }
244
+ }
245
+ function parseUserQuestions(input) {
246
+ if (!Array.isArray(input.questions)) return [];
247
+ const questions = [];
248
+ for (const entry of input.questions) {
249
+ if (!isRecord(entry)) continue;
250
+ if (typeof entry.question !== "string") continue;
251
+ const options = Array.isArray(entry.options) ? entry.options.filter(isRecord).filter((o) => typeof o.label === "string").map((o) => ({
252
+ label: o.label,
253
+ description: typeof o.description === "string" ? o.description : ""
254
+ })) : [];
255
+ questions.push({
256
+ question: entry.question,
257
+ header: typeof entry.header === "string" ? entry.header : "",
258
+ options,
259
+ ...typeof entry.multiSelect === "boolean" ? { multiSelect: entry.multiSelect } : {}
260
+ });
261
+ }
262
+ return questions;
263
+ }
264
+
265
+ export {
266
+ resolveClaudeBinary,
267
+ buildSpawnArgs,
268
+ spawnOptionsFingerprint,
269
+ cleanTerminalOutput,
270
+ buildExitErrors,
271
+ MAX_DIAGNOSTIC_OUTPUT,
272
+ MAX_BETWEEN_TURN_BUFFER,
273
+ resolveSubmitSettleMs,
274
+ resolveRawTuiProbeTiming,
275
+ sentinelEchoed,
276
+ sawTerminalSetup,
277
+ needsRawReadyGate,
278
+ resolveSubmitNudgeTiming,
279
+ resolveSubmitRedeliveryMaxAttempts,
280
+ resolvePlanDialogTiming,
281
+ turnOptionsFrom,
282
+ killPtyWithEscalation,
283
+ loadPtySpawn,
284
+ resolvePtySpawn,
285
+ sessionTempBase,
286
+ inheritedEnv,
287
+ buildPromptBytes,
288
+ renderPromptContentText,
289
+ transcriptSize,
290
+ parseUserQuestions
291
+ };
@@ -2,61 +2,116 @@ import {
2
2
  gitCredentialHelper,
3
3
  syncGithubTokenFiles,
4
4
  writeGitCredential
5
- } from "./chunk-GJXAAPJ6.js";
5
+ } from "./chunk-W4LZ7R6Z.js";
6
6
 
7
- // src/runner/session-runner-helpers.ts
8
- import { readFileSync } from "fs";
9
- import { dirname, join } from "path";
10
- import { fileURLToPath } from "url";
11
- function mapChatHistory(messages) {
12
- if (!messages) return [];
13
- return messages.map((m) => ({
14
- id: m.id,
15
- role: m.role ?? "user",
16
- content: m.content ?? "",
17
- userId: m.userId,
18
- userName: m.user?.name ?? void 0,
19
- createdAt: m.createdAt,
20
- ...m.source ? { source: m.source } : {},
21
- ...m.files && m.files.length > 0 ? {
22
- files: m.files.map((f) => ({
23
- fileId: f.id,
24
- fileName: f.fileName,
25
- mimeType: f.mimeType,
26
- fileSize: f.fileSize,
27
- downloadUrl: f.downloadUrl ?? "",
28
- content: f.content,
29
- contentEncoding: f.contentEncoding
30
- }))
31
- } : {}
32
- }));
7
+ // src/boot/git-prep.ts
8
+ import { execFile } from "child_process";
9
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
10
+ import { join as join2 } from "path";
11
+ import { promisify } from "util";
12
+
13
+ // src/boot/commit-trailers.ts
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { dirname, isAbsolute, join } from "path";
16
+ var HOOK_MARKER = "conveyor-commit-trailers";
17
+ var realFs = {
18
+ exists: (path) => existsSync(path),
19
+ read: (path) => readFileSync(path, "utf8"),
20
+ write: (path, content) => writeFileSync(path, content, "utf8"),
21
+ mkdirp: (path) => mkdirSync(path, { recursive: true }),
22
+ chmodExec: (path) => chmodSync(path, 493)
23
+ };
24
+ function buildHookScript() {
25
+ return `#!/bin/sh
26
+ # ${HOOK_MARKER} \u2014 stamps Conveyor card/driver/session provenance on each commit.
27
+ # Generated by @rallycry/conveyor-agent (boot/commit-trailers.ts); edits are
28
+ # overwritten on the next pod boot.
29
+ #
30
+ # Every path exits 0 on purpose: losing a commit to attribution bookkeeping
31
+ # would be a far worse outcome than losing the attribution.
32
+ MSG="$1"
33
+ [ -n "$MSG" ] || exit 0
34
+
35
+ stamp() {
36
+ [ -n "$2" ] || return 0
37
+ git interpret-trailers --in-place --if-exists doNothing --trailer "$1: $2" "$MSG" 2>/dev/null || true
38
+ }
39
+
40
+ stamp "Conveyor-Card" "$CONVEYOR_CARD_SLUG"
41
+ stamp "Conveyor-Driver" "$CONVEYOR_DRIVER_ID"
42
+ stamp "Conveyor-Session" "$CONVEYOR_SESSION_ID"
43
+ exit 0
44
+ `;
45
+ }
46
+ function resolveHookTargets(repoDir, hooksPath, hasHusky) {
47
+ const huskyTarget = { hookPath: join(repoDir, ".husky", "commit-msg"), huskyManaged: true };
48
+ if (hooksPath?.replace(/\/+$/, "").endsWith(".husky/_")) return [huskyTarget];
49
+ const dir = hooksPath ? isAbsolute(hooksPath) ? hooksPath : join(repoDir, hooksPath) : join(repoDir, ".git", "hooks");
50
+ const resolved = { hookPath: join(dir, "commit-msg"), huskyManaged: false };
51
+ return hasHusky ? [resolved, huskyTarget] : [resolved];
52
+ }
53
+ function excludeFromRepo(repoDir, relPath, fs) {
54
+ const excludePath = join(repoDir, ".git", "info", "exclude");
55
+ const existing = fs.exists(excludePath) ? fs.read(excludePath) : "";
56
+ if (existing.split("\n").some((line) => line.trim() === relPath)) return;
57
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
58
+ fs.write(excludePath, `${existing}${separator}${relPath}
59
+ `);
60
+ }
61
+ function declaresHusky(repoDir, fs) {
62
+ if (fs.exists(join(repoDir, ".husky"))) return true;
63
+ const pkgPath = join(repoDir, "package.json");
64
+ if (!fs.exists(pkgPath)) return false;
65
+ try {
66
+ const pkg = JSON.parse(fs.read(pkgPath));
67
+ if (pkg.devDependencies?.husky ?? pkg.dependencies?.husky) return true;
68
+ return Boolean(pkg.scripts?.prepare?.includes("husky"));
69
+ } catch {
70
+ return false;
71
+ }
33
72
  }
34
- function readAgentVersion() {
73
+ async function installCommitTrailerHook(deps) {
74
+ const { git, repoDir, log } = deps;
75
+ const env = deps.env ?? process.env;
76
+ const fs = deps.fs ?? realFs;
77
+ if (!env.CONVEYOR_CARD_SLUG) return { state: "skipped", reason: "no-card" };
35
78
  try {
36
- const here = dirname(fileURLToPath(import.meta.url));
37
- for (const rel of ["../package.json", "../../package.json"]) {
38
- try {
39
- const pkg = JSON.parse(readFileSync(join(here, rel), "utf-8"));
40
- if (pkg.version) return pkg.version;
41
- } catch {
79
+ let hooksPath = null;
80
+ try {
81
+ const { stdout } = await git(["config", "--get", "core.hooksPath"], { cwd: repoDir });
82
+ hooksPath = stdout.trim() || null;
83
+ } catch {
84
+ }
85
+ const targets = resolveHookTargets(repoDir, hooksPath, declaresHusky(repoDir, fs));
86
+ const written = [];
87
+ for (const { hookPath, huskyManaged } of targets) {
88
+ if (fs.exists(hookPath) && !fs.read(hookPath).includes(HOOK_MARKER)) {
89
+ log.warn(`[boot] WARN: commit-msg hook already present at ${hookPath}; leaving it alone`);
90
+ continue;
42
91
  }
92
+ fs.mkdirp(dirname(hookPath));
93
+ fs.write(hookPath, buildHookScript());
94
+ fs.chmodExec(hookPath);
95
+ if (huskyManaged) excludeFromRepo(repoDir, ".husky/commit-msg", fs);
96
+ written.push(hookPath);
43
97
  }
44
- } catch {
98
+ if (written.length === 0) return { state: "skipped", reason: "project-hook" };
99
+ log.info(`[boot] Commit trailers installed (${written.join(", ")})`);
100
+ return { state: "installed", paths: written };
101
+ } catch (err) {
102
+ const reason = err instanceof Error ? err.message : String(err);
103
+ log.warn(`[boot] WARN: commit trailer hook not installed: ${reason}`);
104
+ return { state: "failed", reason };
45
105
  }
46
- return null;
47
106
  }
48
107
 
49
108
  // src/boot/git-prep.ts
50
- import { execFile } from "child_process";
51
- import { existsSync, mkdirSync } from "fs";
52
- import { join as join2 } from "path";
53
- import { promisify } from "util";
54
109
  var QUICK_GIT_TIMEOUT_MS = 6e4;
55
110
  var FETCH_TIMEOUT_MS = 3e5;
56
111
  var CLONE_TIMEOUT_MS = 6e5;
57
112
  var execFileAsync = promisify(execFile);
58
113
  function ensureDir(dir) {
59
- mkdirSync(dir, { recursive: true });
114
+ mkdirSync2(dir, { recursive: true });
60
115
  }
61
116
  function defaultGit(args, opts = {}) {
62
117
  return execFileAsync("git", args, {
@@ -185,6 +240,22 @@ async function clonePostAssignment(deps, paths) {
185
240
  }
186
241
  return syncTaskBranchToRepo(deps, paths);
187
242
  }
243
+ async function withCommitTrailers(deps, paths, state) {
244
+ if (state.state !== "ready") return state;
245
+ await installCommitTrailerHook({
246
+ git: deps.git,
247
+ repoDir: paths.repoDir,
248
+ log: deps.log,
249
+ // The bundle, NOT process.env: git prep runs inside the workbench sidecar,
250
+ // whose own env holds only CONVEYOR_API_URL and POD_BOOTSTRAP_TOKEN.
251
+ // `bundle.envVars` (where CONVEYOR_CARD_SLUG rides) is merged into
252
+ // `ctx.childEnv` for the separate agent container and is never written back
253
+ // onto this process — so reading process.env here saw nothing and skipped
254
+ // the install on every real pod.
255
+ env: { ...process.env, ...deps.bundle.envVars }
256
+ });
257
+ return state;
258
+ }
188
259
  async function prepareWorkspaceGit(deps) {
189
260
  const { bundle, log } = deps;
190
261
  const workspacesDir = deps.workspacesDir ?? "/workspaces";
@@ -207,17 +278,17 @@ async function prepareWorkspaceGit(deps) {
207
278
  if (bundle.gitPlan.provider === "github") {
208
279
  syncGithubTokenFiles(bundle.githubToken ?? credential.secret);
209
280
  }
210
- if (existsSync(join2(repoDir, ".git"))) {
281
+ if (existsSync2(join2(repoDir, ".git"))) {
211
282
  log.info(
212
283
  deps.podImage ? `[boot] Pod image \u2014 updating repo to latest (branch=${branch})...` : `[boot] Repo present (non-pod-image) \u2014 updating repo to latest (branch=${branch})...`
213
284
  );
214
- return await syncTaskBranchToRepo(deps, paths);
285
+ return await withCommitTrailers(deps, paths, await syncTaskBranchToRepo(deps, paths));
215
286
  }
216
287
  try {
217
- mkdirSync(workspacesDir, { recursive: true });
288
+ mkdirSync2(workspacesDir, { recursive: true });
218
289
  } catch {
219
290
  }
220
- return await clonePostAssignment(deps, paths);
291
+ return await withCommitTrailers(deps, paths, await clonePostAssignment(deps, paths));
221
292
  } catch (err) {
222
293
  log.error(`[boot] ERROR: git prep failed unexpectedly: ${errText(err)}`);
223
294
  return { state: "failed", reason: "git prep failed unexpectedly" };
@@ -318,8 +389,6 @@ async function reportBootMilestone(opts) {
318
389
  }
319
390
 
320
391
  export {
321
- mapChatHistory,
322
- readAgentVersion,
323
392
  FETCH_TIMEOUT_MS,
324
393
  CLONE_TIMEOUT_MS,
325
394
  ensureDir,