@rallycry/conveyor-agent 5.9.4 → 5.10.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.
@@ -454,9 +454,15 @@ var ProjectConnection = class {
454
454
  shutdownCallback = null;
455
455
  chatMessageCallback = null;
456
456
  earlyChatMessages = [];
457
+ auditRequestCallback = null;
458
+ // Branch switching callbacks
459
+ onSwitchBranch = null;
460
+ onSyncEnvironment = null;
461
+ onGetEnvStatus = null;
457
462
  constructor(config) {
458
463
  this.config = config;
459
464
  }
465
+ // oxlint-disable-next-line max-lines-per-function -- socket event registration requires co-located handlers
460
466
  connect() {
461
467
  return new Promise((resolve2, reject) => {
462
468
  let settled = false;
@@ -496,6 +502,30 @@ var ProjectConnection = class {
496
502
  this.earlyChatMessages.push(msg);
497
503
  }
498
504
  });
505
+ this.socket.on("projectRunner:auditTags", (data) => {
506
+ if (this.auditRequestCallback) {
507
+ this.auditRequestCallback(data);
508
+ }
509
+ });
510
+ this.socket.on(
511
+ "projectRunner:switchBranch",
512
+ (data, cb) => {
513
+ if (this.onSwitchBranch) this.onSwitchBranch(data, cb);
514
+ else cb({ ok: false, error: "switchBranch handler not registered" });
515
+ }
516
+ );
517
+ this.socket.on("projectRunner:syncEnvironment", (cb) => {
518
+ if (this.onSyncEnvironment) this.onSyncEnvironment(cb);
519
+ else cb({ ok: false, error: "syncEnvironment handler not registered" });
520
+ });
521
+ this.socket.on("projectRunner:getEnvStatus", (cb) => {
522
+ if (this.onGetEnvStatus) this.onGetEnvStatus(cb);
523
+ else cb({ ok: false, data: void 0 });
524
+ });
525
+ this.socket.on(
526
+ "projectRunner:runAuthTokenCommand",
527
+ (data, cb) => this.handleRunAuthTokenCommand(data.userEmail, cb)
528
+ );
499
529
  this.socket.on("connect", () => {
500
530
  if (!settled) {
501
531
  settled = true;
@@ -527,6 +557,13 @@ var ProjectConnection = class {
527
557
  }
528
558
  this.earlyChatMessages = [];
529
559
  }
560
+ onAuditRequest(callback) {
561
+ this.auditRequestCallback = callback;
562
+ }
563
+ emitAuditResult(data) {
564
+ if (!this.socket) return;
565
+ this.socket.emit("conveyor:tagAuditResult", data);
566
+ }
530
567
  sendHeartbeat() {
531
568
  if (!this.socket) return;
532
569
  this.socket.emit("projectRunner:heartbeat", {});
@@ -588,6 +625,46 @@ var ProjectConnection = class {
588
625
  );
589
626
  });
590
627
  }
628
+ emitNewCommitsDetected(data) {
629
+ if (!this.socket) return;
630
+ this.socket.emit("projectRunner:newCommitsDetected", data);
631
+ }
632
+ emitEnvironmentReady(data) {
633
+ if (!this.socket) return;
634
+ this.socket.emit("projectRunner:environmentReady", data);
635
+ }
636
+ emitEnvSwitchProgress(data) {
637
+ if (!this.socket) return;
638
+ this.socket.emit("projectRunner:envSwitchProgress", data);
639
+ }
640
+ handleRunAuthTokenCommand(userEmail, cb) {
641
+ try {
642
+ if (process.env.CODESPACES !== "true") {
643
+ cb({ ok: false, error: "Auth token command only available in codespace environments" });
644
+ return;
645
+ }
646
+ const authCmd = process.env.CONVEYOR_AUTH_TOKEN_COMMAND;
647
+ if (!authCmd) {
648
+ cb({ ok: false, error: "CONVEYOR_AUTH_TOKEN_COMMAND not configured" });
649
+ return;
650
+ }
651
+ const cwd = this.config.projectDir ?? process.cwd();
652
+ const token = runAuthTokenCommand(authCmd, userEmail, cwd);
653
+ if (!token) {
654
+ cb({
655
+ ok: false,
656
+ error: `Auth token command returned empty output. Command: ${authCmd}`
657
+ });
658
+ return;
659
+ }
660
+ cb({ ok: true, token });
661
+ } catch (error) {
662
+ cb({
663
+ ok: false,
664
+ error: error instanceof Error ? error.message : "Auth token command failed"
665
+ });
666
+ }
667
+ }
591
668
  disconnect() {
592
669
  this.socket?.disconnect();
593
670
  this.socket = null;
@@ -1178,6 +1255,184 @@ After addressing the feedback, resume your autonomous loop: call list_subtasks a
1178
1255
  return parts;
1179
1256
  }
1180
1257
 
1258
+ // src/execution/tag-context-resolver.ts
1259
+ import { readFile as readFile2, readdir } from "fs/promises";
1260
+ var TYPE_PRIORITY = { rule: 0, file: 1, folder: 2, doc: 3 };
1261
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
1262
+ ".png",
1263
+ ".jpg",
1264
+ ".jpeg",
1265
+ ".gif",
1266
+ ".webp",
1267
+ ".ico",
1268
+ ".svg",
1269
+ ".bmp",
1270
+ ".mp3",
1271
+ ".mp4",
1272
+ ".wav",
1273
+ ".avi",
1274
+ ".mov",
1275
+ ".pdf",
1276
+ ".zip",
1277
+ ".tar",
1278
+ ".gz",
1279
+ ".woff",
1280
+ ".woff2",
1281
+ ".ttf",
1282
+ ".eot",
1283
+ ".otf",
1284
+ ".exe",
1285
+ ".dll",
1286
+ ".so",
1287
+ ".dylib",
1288
+ ".wasm"
1289
+ ]);
1290
+ function isBinaryPath(filePath) {
1291
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
1292
+ return BINARY_EXTENSIONS.has(ext);
1293
+ }
1294
+ function getContextBudgetChars(_model, betas) {
1295
+ const contextWindow = betas?.some((b) => b.includes("context-1m")) ? 1e6 : 2e5;
1296
+ return contextWindow * 0.25 * 4;
1297
+ }
1298
+ async function readFileContent(filePath, maxChars) {
1299
+ try {
1300
+ if (isBinaryPath(filePath)) return null;
1301
+ const content = await readFile2(filePath, "utf-8");
1302
+ if (content.length > maxChars) {
1303
+ const omitted = content.length - maxChars;
1304
+ return content.slice(0, maxChars) + `
1305
+ [... truncated, ${omitted} chars omitted]`;
1306
+ }
1307
+ return content;
1308
+ } catch {
1309
+ return null;
1310
+ }
1311
+ }
1312
+ async function readFolderListing(folderPath) {
1313
+ try {
1314
+ const entries = await readdir(folderPath);
1315
+ return `Files: ${entries.join(", ")}`;
1316
+ } catch {
1317
+ return null;
1318
+ }
1319
+ }
1320
+ async function resolveEntry(entry, budget) {
1321
+ const result = {
1322
+ type: entry.type,
1323
+ path: entry.path,
1324
+ label: entry.label,
1325
+ content: null,
1326
+ charCount: 0
1327
+ };
1328
+ if (budget.remaining <= 0) return result;
1329
+ if (entry.type === "doc") {
1330
+ return result;
1331
+ }
1332
+ if (entry.type === "folder") {
1333
+ const listing = await readFolderListing(entry.path);
1334
+ if (listing) {
1335
+ result.content = listing;
1336
+ result.charCount = listing.length;
1337
+ budget.remaining -= listing.length;
1338
+ }
1339
+ return result;
1340
+ }
1341
+ const maxChars = Math.floor(budget.remaining * 0.5) || budget.remaining;
1342
+ const content = await readFileContent(entry.path, maxChars);
1343
+ if (content) {
1344
+ result.content = content;
1345
+ result.charCount = content.length;
1346
+ budget.remaining -= content.length;
1347
+ }
1348
+ return result;
1349
+ }
1350
+ function formatEntry(entry) {
1351
+ const label = entry.label ? ` (${entry.label})` : "";
1352
+ const header = `#### ${entry.path}${label} (${entry.type})`;
1353
+ if (entry.content === null) {
1354
+ return `> ${entry.type}: ${entry.path}${label}`;
1355
+ }
1356
+ if (entry.type === "folder") {
1357
+ return `${header}
1358
+ ${entry.content}`;
1359
+ }
1360
+ return `${header}
1361
+ \`\`\`
1362
+ ${entry.content}
1363
+ \`\`\``;
1364
+ }
1365
+ function formatResolvedTags(resolved) {
1366
+ const parts = [`
1367
+ ## Tag Context`];
1368
+ for (const tag of resolved) {
1369
+ if (tag.entries.length === 0) continue;
1370
+ const desc = tag.description ? ` \u2014 ${tag.description}` : "";
1371
+ parts.push(`
1372
+ ### Tag: "${tag.tagName}"${desc}`);
1373
+ const contentEntries = tag.entries.filter((e) => e.content !== null);
1374
+ const pointerEntries = tag.entries.filter((e) => e.content === null);
1375
+ for (const entry of contentEntries) {
1376
+ parts.push(`
1377
+ ${formatEntry(entry)}`);
1378
+ }
1379
+ if (pointerEntries.length > 0) {
1380
+ parts.push(``);
1381
+ parts.push(`> Budget limit reached. Remaining linked files (read manually if needed):`);
1382
+ for (const entry of pointerEntries) {
1383
+ parts.push(formatEntry(entry));
1384
+ }
1385
+ }
1386
+ }
1387
+ return parts.join("\n");
1388
+ }
1389
+ async function resolveTagContext(projectTags, taskTagIds, model, betas) {
1390
+ if (!projectTags?.length || !taskTagIds.length) {
1391
+ return { injectedSection: "", stats: { injected: 0, skipped: 0 } };
1392
+ }
1393
+ const taskTagIdSet = new Set(taskTagIds);
1394
+ const assignedTags = projectTags.filter((t) => taskTagIdSet.has(t.id));
1395
+ if (assignedTags.length === 0) {
1396
+ return { injectedSection: "", stats: { injected: 0, skipped: 0 } };
1397
+ }
1398
+ const allEntries = [];
1399
+ for (let i = 0; i < assignedTags.length; i++) {
1400
+ const tag = assignedTags[i];
1401
+ if (!tag.contextPaths?.length) continue;
1402
+ const sorted = [...tag.contextPaths].sort(
1403
+ (a, b) => (TYPE_PRIORITY[a.type] ?? 99) - (TYPE_PRIORITY[b.type] ?? 99)
1404
+ );
1405
+ for (const entry of sorted) {
1406
+ allEntries.push({ tagIndex: i, entry });
1407
+ }
1408
+ }
1409
+ if (allEntries.length === 0) {
1410
+ return { injectedSection: "", stats: { injected: 0, skipped: 0 } };
1411
+ }
1412
+ const budgetChars = getContextBudgetChars(model, betas);
1413
+ const budget = { remaining: budgetChars };
1414
+ let injected = 0;
1415
+ let skipped = 0;
1416
+ const resolved = assignedTags.map((t) => ({
1417
+ tagName: t.name,
1418
+ description: t.description,
1419
+ entries: []
1420
+ }));
1421
+ for (const { tagIndex, entry } of allEntries) {
1422
+ const result = await resolveEntry(entry, budget);
1423
+ resolved[tagIndex].entries.push(result);
1424
+ if (result.content === null) {
1425
+ skipped++;
1426
+ } else {
1427
+ injected++;
1428
+ }
1429
+ }
1430
+ return {
1431
+ injectedSection: formatResolvedTags(resolved),
1432
+ stats: { injected, skipped }
1433
+ };
1434
+ }
1435
+
1181
1436
  // src/execution/mode-prompt.ts
1182
1437
  function buildPropertyInstructions(context) {
1183
1438
  const parts = [];
@@ -1206,6 +1461,12 @@ function buildPropertyInstructions(context) {
1206
1461
  for (const tag of context.projectTags) {
1207
1462
  const desc = tag.description ? ` \u2014 ${tag.description}` : "";
1208
1463
  parts.push(`- ID: "${tag.id}", Name: "${tag.name}"${desc}`);
1464
+ if (tag.contextPaths?.length) {
1465
+ for (const link of tag.contextPaths) {
1466
+ const label = link.label ? ` (${link.label})` : "";
1467
+ parts.push(` \u2192 ${link.type}: ${link.path}${label}`);
1468
+ }
1469
+ }
1209
1470
  }
1210
1471
  }
1211
1472
  return parts;
@@ -1351,6 +1612,14 @@ Project Agents:`);
1351
1612
  parts.push(formatProjectAgentLine(pa));
1352
1613
  }
1353
1614
  }
1615
+ if (context.projectObjectives && context.projectObjectives.length > 0) {
1616
+ parts.push(`
1617
+ Project Objectives:`);
1618
+ for (const obj of context.projectObjectives) {
1619
+ const dates = `${obj.startDate.split("T")[0]} to ${obj.endDate.split("T")[0]}`;
1620
+ parts.push(`- **${obj.name}** (${dates})${obj.description ? ": " + obj.description : ""}`);
1621
+ }
1622
+ }
1354
1623
  return parts;
1355
1624
  }
1356
1625
  function buildActivePreamble(context, workspaceDir) {
@@ -1588,7 +1857,17 @@ function formatChatHistory(chatHistory) {
1588
1857
  }
1589
1858
  return parts;
1590
1859
  }
1591
- function buildTaskBody(context) {
1860
+ async function resolveTaskTagContext(context) {
1861
+ if (!context.projectTags?.length || !context.taskTagIds?.length) return null;
1862
+ const { injectedSection } = await resolveTagContext(
1863
+ context.projectTags,
1864
+ context.taskTagIds,
1865
+ context.model,
1866
+ context.agentSettings?.betas
1867
+ );
1868
+ return injectedSection || null;
1869
+ }
1870
+ async function buildTaskBody(context) {
1592
1871
  const parts = [];
1593
1872
  parts.push(`# Task: ${context.title}`);
1594
1873
  if (context.description) {
@@ -1616,6 +1895,8 @@ ${context.plan}`);
1616
1895
  parts.push(`- [${icon}] \`${ref.path}\``);
1617
1896
  }
1618
1897
  }
1898
+ const tagSection = await resolveTaskTagContext(context);
1899
+ if (tagSection) parts.push(tagSection);
1619
1900
  if (context.chatHistory.length > 0) {
1620
1901
  parts.push(...formatChatHistory(context.chatHistory));
1621
1902
  }
@@ -1767,7 +2048,7 @@ function buildInstructions(mode, context, scenario, agentMode) {
1767
2048
  parts.push(...buildFeedbackInstructions(context, isPm));
1768
2049
  return parts;
1769
2050
  }
1770
- function buildInitialPrompt(mode, context, isAuto, agentMode) {
2051
+ async function buildInitialPrompt(mode, context, isAuto, agentMode) {
1771
2052
  const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
1772
2053
  if (!isPackRunner) {
1773
2054
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode);
@@ -1775,7 +2056,7 @@ function buildInitialPrompt(mode, context, isAuto, agentMode) {
1775
2056
  }
1776
2057
  const isPm = mode === "pm";
1777
2058
  const scenario = detectRelaunchScenario(context, isPm);
1778
- const body = buildTaskBody(context);
2059
+ const body = await buildTaskBody(context);
1779
2060
  const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
1780
2061
  return [...body, ...instructions].join("\n");
1781
2062
  }
@@ -2684,13 +2965,13 @@ function buildMultimodalPrompt(textPrompt, context, skipImages = false) {
2684
2965
  }
2685
2966
  return blocks;
2686
2967
  }
2687
- function buildFollowUpPrompt(host, context, followUpContent) {
2968
+ async function buildFollowUpPrompt(host, context, followUpContent) {
2688
2969
  const isPmMode = host.config.mode === "pm";
2689
2970
  const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
2690
2971
  const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
2691
2972
  (b) => b.type === "image"
2692
2973
  );
2693
- const textPrompt = isPmMode ? `${buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode)}
2974
+ const textPrompt = isPmMode ? `${await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode)}
2694
2975
 
2695
2976
  ---
2696
2977
 
@@ -2719,7 +3000,7 @@ async function runSdkQuery(host, context, followUpContent) {
2719
3000
  const options = buildQueryOptions(host, context);
2720
3001
  const resume = context.claudeSessionId ?? void 0;
2721
3002
  if (followUpContent) {
2722
- const prompt = buildFollowUpPrompt(host, context, followUpContent);
3003
+ const prompt = await buildFollowUpPrompt(host, context, followUpContent);
2723
3004
  const agentQuery = query({
2724
3005
  prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
2725
3006
  options: { ...options, resume }
@@ -2733,7 +3014,12 @@ async function runSdkQuery(host, context, followUpContent) {
2733
3014
  } else if (isDiscoveryLike) {
2734
3015
  return;
2735
3016
  } else {
2736
- const initialPrompt = buildInitialPrompt(host.config.mode, context, host.config.isAuto, mode);
3017
+ const initialPrompt = await buildInitialPrompt(
3018
+ host.config.mode,
3019
+ context,
3020
+ host.config.isAuto,
3021
+ mode
3022
+ );
2737
3023
  const prompt = buildMultimodalPrompt(initialPrompt, context);
2738
3024
  const agentQuery = query({
2739
3025
  prompt: host.createInputStream(prompt),
@@ -2750,14 +3036,14 @@ async function runSdkQuery(host, context, followUpContent) {
2750
3036
  host.syncPlanFile();
2751
3037
  }
2752
3038
  }
2753
- function buildRetryQuery(host, context, options, lastErrorWasImage) {
3039
+ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
2754
3040
  if (lastErrorWasImage) {
2755
3041
  host.connection.postChatMessage(
2756
3042
  "An attached image could not be processed. Retrying without images..."
2757
3043
  );
2758
3044
  }
2759
3045
  const retryPrompt = buildMultimodalPrompt(
2760
- buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
3046
+ await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2761
3047
  context,
2762
3048
  lastErrorWasImage
2763
3049
  );
@@ -2766,11 +3052,11 @@ function buildRetryQuery(host, context, options, lastErrorWasImage) {
2766
3052
  options: { ...options, resume: void 0 }
2767
3053
  });
2768
3054
  }
2769
- function handleStaleSession(context, host, options) {
3055
+ async function handleStaleSession(context, host, options) {
2770
3056
  context.claudeSessionId = null;
2771
3057
  host.connection.storeSessionId("");
2772
3058
  const freshPrompt = buildMultimodalPrompt(
2773
- buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
3059
+ await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
2774
3060
  context
2775
3061
  );
2776
3062
  const freshQuery = query({
@@ -2836,7 +3122,7 @@ async function runWithRetry(initialQuery, context, host, options) {
2836
3122
  let lastErrorWasImage = false;
2837
3123
  for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
2838
3124
  if (host.isStopped()) return;
2839
- const agentQuery = attempt === 0 ? initialQuery : buildRetryQuery(host, context, options, lastErrorWasImage);
3125
+ const agentQuery = attempt === 0 ? initialQuery : await buildRetryQuery(host, context, options, lastErrorWasImage);
2840
3126
  try {
2841
3127
  const { retriable, resultSummary, modeRestart, rateLimitResetsAt, staleSession } = await processEvents(agentQuery, context, host);
2842
3128
  if (modeRestart || host.isStopped()) return;
@@ -3678,13 +3964,118 @@ var AgentRunner = class {
3678
3964
 
3679
3965
  // src/runner/project-runner.ts
3680
3966
  import { fork } from "child_process";
3681
- import { execSync as execSync5 } from "child_process";
3967
+ import { execSync as execSync6 } from "child_process";
3682
3968
  import * as path from "path";
3683
3969
  import { fileURLToPath } from "url";
3684
3970
 
3971
+ // src/runner/commit-watcher.ts
3972
+ import { execSync as execSync5 } from "child_process";
3973
+ var logger3 = createServiceLogger("CommitWatcher");
3974
+ var CommitWatcher = class {
3975
+ constructor(config, callbacks) {
3976
+ this.config = config;
3977
+ this.callbacks = callbacks;
3978
+ }
3979
+ interval = null;
3980
+ lastKnownRemoteSha = null;
3981
+ branch = null;
3982
+ debounceTimer = null;
3983
+ isSyncing = false;
3984
+ start(branch) {
3985
+ this.stop();
3986
+ this.branch = branch;
3987
+ this.lastKnownRemoteSha = this.getLocalHeadSha();
3988
+ this.interval = setInterval(() => void this.poll(), this.config.pollIntervalMs);
3989
+ logger3.info("Commit watcher started", {
3990
+ branch,
3991
+ baseSha: this.lastKnownRemoteSha?.slice(0, 8)
3992
+ });
3993
+ }
3994
+ stop() {
3995
+ if (this.interval) clearInterval(this.interval);
3996
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
3997
+ this.interval = null;
3998
+ this.debounceTimer = null;
3999
+ this.branch = null;
4000
+ this.lastKnownRemoteSha = null;
4001
+ this.isSyncing = false;
4002
+ }
4003
+ getLocalHeadSha() {
4004
+ return execSync5("git rev-parse HEAD", {
4005
+ cwd: this.config.projectDir,
4006
+ stdio: ["ignore", "pipe", "ignore"]
4007
+ }).toString().trim();
4008
+ }
4009
+ poll() {
4010
+ if (!this.branch || this.isSyncing) return;
4011
+ try {
4012
+ execSync5(`git fetch origin ${this.branch} --quiet`, {
4013
+ cwd: this.config.projectDir,
4014
+ stdio: "ignore",
4015
+ timeout: 3e4
4016
+ });
4017
+ const remoteSha = execSync5(`git rev-parse origin/${this.branch}`, {
4018
+ cwd: this.config.projectDir,
4019
+ stdio: ["ignore", "pipe", "ignore"]
4020
+ }).toString().trim();
4021
+ if (remoteSha !== this.lastKnownRemoteSha) {
4022
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
4023
+ this.debounceTimer = setTimeout(
4024
+ () => void this.handleNewCommits(remoteSha),
4025
+ this.config.debounceMs
4026
+ );
4027
+ }
4028
+ } catch {
4029
+ }
4030
+ }
4031
+ async handleNewCommits(remoteSha) {
4032
+ if (!this.branch) return;
4033
+ const previousSha = this.lastKnownRemoteSha ?? "HEAD";
4034
+ let commitCount = 1;
4035
+ let latestMessage = "";
4036
+ let latestAuthor = "";
4037
+ try {
4038
+ const countOutput = execSync5(`git rev-list --count ${previousSha}..origin/${this.branch}`, {
4039
+ cwd: this.config.projectDir,
4040
+ stdio: ["ignore", "pipe", "ignore"]
4041
+ }).toString().trim();
4042
+ commitCount = parseInt(countOutput, 10) || 1;
4043
+ const logOutput = execSync5(`git log -1 --format="%s|||%an" origin/${this.branch}`, {
4044
+ cwd: this.config.projectDir,
4045
+ stdio: ["ignore", "pipe", "ignore"]
4046
+ }).toString().trim();
4047
+ const parts = logOutput.split("|||");
4048
+ latestMessage = parts[0] ?? "";
4049
+ latestAuthor = parts[1] ?? "";
4050
+ } catch {
4051
+ }
4052
+ this.lastKnownRemoteSha = remoteSha;
4053
+ this.isSyncing = true;
4054
+ logger3.info("New commits detected", {
4055
+ branch: this.branch,
4056
+ commitCount,
4057
+ sha: remoteSha.slice(0, 8)
4058
+ });
4059
+ try {
4060
+ await this.callbacks.onNewCommits({
4061
+ branch: this.branch,
4062
+ previousSha,
4063
+ newCommitSha: remoteSha,
4064
+ commitCount,
4065
+ latestMessage,
4066
+ latestAuthor
4067
+ });
4068
+ } catch (err) {
4069
+ logger3.error("Error handling new commits", errorMeta(err));
4070
+ } finally {
4071
+ this.isSyncing = false;
4072
+ }
4073
+ }
4074
+ };
4075
+
3685
4076
  // src/runner/project-chat-handler.ts
3686
4077
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
3687
- var logger3 = createServiceLogger("ProjectChat");
4078
+ var logger4 = createServiceLogger("ProjectChat");
3688
4079
  var FALLBACK_MODEL = "claude-sonnet-4-20250514";
3689
4080
  function buildSystemPrompt2(projectDir, agentCtx) {
3690
4081
  const parts = [];
@@ -3737,7 +4128,7 @@ function processContentBlock(block, responseParts, turnToolCalls) {
3737
4128
  input: inputStr.slice(0, 1e4),
3738
4129
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3739
4130
  });
3740
- logger3.debug("Tool use", { tool: block.name });
4131
+ logger4.debug("Tool use", { tool: block.name });
3741
4132
  }
3742
4133
  }
3743
4134
  async function fetchContext(connection) {
@@ -3745,13 +4136,13 @@ async function fetchContext(connection) {
3745
4136
  try {
3746
4137
  agentCtx = await connection.fetchAgentContext();
3747
4138
  } catch {
3748
- logger3.warn("Could not fetch agent context, using defaults");
4139
+ logger4.warn("Could not fetch agent context, using defaults");
3749
4140
  }
3750
4141
  let chatHistory = [];
3751
4142
  try {
3752
4143
  chatHistory = await connection.fetchChatHistory(30);
3753
4144
  } catch {
3754
- logger3.warn("Could not fetch chat history, proceeding without it");
4145
+ logger4.warn("Could not fetch chat history, proceeding without it");
3755
4146
  }
3756
4147
  return { agentCtx, chatHistory };
3757
4148
  }
@@ -3804,6 +4195,7 @@ async function runChatQuery(message, connection, projectDir) {
3804
4195
  const { agentCtx, chatHistory } = await fetchContext(connection);
3805
4196
  const options = buildChatQueryOptions(agentCtx, projectDir);
3806
4197
  const prompt = buildPrompt(message, chatHistory);
4198
+ connection.emitAgentStatus("running");
3807
4199
  const events = query2({ prompt, options });
3808
4200
  const responseParts = [];
3809
4201
  const turnToolCalls = [];
@@ -3821,11 +4213,12 @@ async function runChatQuery(message, connection, projectDir) {
3821
4213
  }
3822
4214
  }
3823
4215
  async function handleProjectChatMessage(message, connection, projectDir) {
3824
- connection.emitAgentStatus("busy");
4216
+ connection.emitAgentStatus("fetching_context");
3825
4217
  try {
3826
4218
  await runChatQuery(message, connection, projectDir);
3827
4219
  } catch (error) {
3828
- logger3.error("Failed to handle message", errorMeta(error));
4220
+ logger4.error("Failed to handle message", errorMeta(error));
4221
+ connection.emitAgentStatus("error");
3829
4222
  try {
3830
4223
  await connection.emitChatMessage(
3831
4224
  "I encountered an error processing your message. Please try again."
@@ -3837,13 +4230,360 @@ async function handleProjectChatMessage(message, connection, projectDir) {
3837
4230
  }
3838
4231
  }
3839
4232
 
4233
+ // src/runner/project-audit-handler.ts
4234
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
4235
+
4236
+ // src/tools/audit-tools.ts
4237
+ import { randomUUID as randomUUID3 } from "crypto";
4238
+ import { tool as tool4, createSdkMcpServer as createSdkMcpServer2 } from "@anthropic-ai/claude-agent-sdk";
4239
+ import { z as z4 } from "zod";
4240
+ function mapCreateTag(input) {
4241
+ return {
4242
+ type: "create_tag",
4243
+ tagName: input.name,
4244
+ suggestion: `Create new tag "${input.name}"${input.description ? `: ${input.description}` : ""}`,
4245
+ reasoning: input.reasoning,
4246
+ payload: { name: input.name, color: input.color ?? "#6B7280", description: input.description }
4247
+ };
4248
+ }
4249
+ function mapUpdateDescription(input) {
4250
+ return {
4251
+ type: "update_description",
4252
+ tagId: input.tagId,
4253
+ tagName: input.tagName,
4254
+ suggestion: `Update description for "${input.tagName}"`,
4255
+ reasoning: input.reasoning,
4256
+ payload: { description: input.description }
4257
+ };
4258
+ }
4259
+ function mapContextLink(input) {
4260
+ return {
4261
+ type: "add_context_link",
4262
+ tagId: input.tagId,
4263
+ tagName: input.tagName,
4264
+ suggestion: `Link ${input.linkType}:${input.path} to "${input.tagName}"`,
4265
+ reasoning: input.reasoning,
4266
+ payload: { contextLink: { type: input.linkType, path: input.path, label: input.label } }
4267
+ };
4268
+ }
4269
+ function mapDocGap(input) {
4270
+ return {
4271
+ type: "documentation_gap",
4272
+ tagId: input.tagId,
4273
+ tagName: input.tagName,
4274
+ suggestion: `Documentation gap: ${input.filePath} (${input.readCount} reads)`,
4275
+ reasoning: input.reasoning,
4276
+ payload: {
4277
+ filePath: input.filePath,
4278
+ readCount: input.readCount,
4279
+ suggestedAction: input.suggestedAction
4280
+ }
4281
+ };
4282
+ }
4283
+ function mapMergeTags(input) {
4284
+ return {
4285
+ type: "merge_tags",
4286
+ tagId: input.tagId,
4287
+ tagName: input.tagName,
4288
+ suggestion: `Merge "${input.tagName}" into "${input.mergeIntoTagName}"`,
4289
+ reasoning: input.reasoning,
4290
+ payload: { mergeIntoTagId: input.mergeIntoTagId }
4291
+ };
4292
+ }
4293
+ function mapRenameTag(input) {
4294
+ return {
4295
+ type: "rename_tag",
4296
+ tagId: input.tagId,
4297
+ tagName: input.tagName,
4298
+ suggestion: `Rename "${input.tagName}" to "${input.newName}"`,
4299
+ reasoning: input.reasoning,
4300
+ payload: { newName: input.newName }
4301
+ };
4302
+ }
4303
+ var TOOL_MAPPERS = {
4304
+ recommend_create_tag: mapCreateTag,
4305
+ recommend_update_description: mapUpdateDescription,
4306
+ recommend_context_link: mapContextLink,
4307
+ flag_documentation_gap: mapDocGap,
4308
+ recommend_merge_tags: mapMergeTags,
4309
+ recommend_rename_tag: mapRenameTag
4310
+ };
4311
+ function collectRecommendation(toolName, input, collector) {
4312
+ const mapper = TOOL_MAPPERS[toolName];
4313
+ if (!mapper) return JSON.stringify({ error: `Unknown tool: ${toolName}` });
4314
+ const rec = { id: randomUUID3(), ...mapper(input) };
4315
+ collector.recommendations.push(rec);
4316
+ return JSON.stringify({ success: true, recommendationId: rec.id });
4317
+ }
4318
+ function createAuditMcpServer(collector) {
4319
+ const auditTools = [
4320
+ tool4(
4321
+ "recommend_create_tag",
4322
+ "Recommend creating a new tag for an uncovered subsystem or area",
4323
+ {
4324
+ name: z4.string().describe("Proposed tag name (lowercase, hyphenated)"),
4325
+ color: z4.string().optional().describe("Hex color code"),
4326
+ description: z4.string().describe("What this tag covers"),
4327
+ reasoning: z4.string().describe("Why this tag should be created")
4328
+ },
4329
+ async (args) => {
4330
+ const result = collectRecommendation(
4331
+ "recommend_create_tag",
4332
+ args,
4333
+ collector
4334
+ );
4335
+ return { content: [{ type: "text", text: result }] };
4336
+ }
4337
+ ),
4338
+ tool4(
4339
+ "recommend_update_description",
4340
+ "Recommend updating a tag's description to better reflect its scope",
4341
+ {
4342
+ tagId: z4.string(),
4343
+ tagName: z4.string(),
4344
+ description: z4.string().describe("Proposed new description"),
4345
+ reasoning: z4.string()
4346
+ },
4347
+ async (args) => {
4348
+ const result = collectRecommendation(
4349
+ "recommend_update_description",
4350
+ args,
4351
+ collector
4352
+ );
4353
+ return { content: [{ type: "text", text: result }] };
4354
+ }
4355
+ ),
4356
+ tool4(
4357
+ "recommend_context_link",
4358
+ "Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
4359
+ {
4360
+ tagId: z4.string(),
4361
+ tagName: z4.string(),
4362
+ linkType: z4.enum(["rule", "doc", "file", "folder"]),
4363
+ path: z4.string(),
4364
+ label: z4.string().optional(),
4365
+ reasoning: z4.string()
4366
+ },
4367
+ async (args) => {
4368
+ const result = collectRecommendation(
4369
+ "recommend_context_link",
4370
+ args,
4371
+ collector
4372
+ );
4373
+ return { content: [{ type: "text", text: result }] };
4374
+ }
4375
+ ),
4376
+ tool4(
4377
+ "flag_documentation_gap",
4378
+ "Flag a file that agents read heavily but has no tag documentation linked",
4379
+ {
4380
+ tagName: z4.string().describe("Tag whose agents read this file"),
4381
+ tagId: z4.string().optional(),
4382
+ filePath: z4.string(),
4383
+ readCount: z4.number(),
4384
+ suggestedAction: z4.string().describe("What doc or rule should be created"),
4385
+ reasoning: z4.string()
4386
+ },
4387
+ async (args) => {
4388
+ const result = collectRecommendation(
4389
+ "flag_documentation_gap",
4390
+ args,
4391
+ collector
4392
+ );
4393
+ return { content: [{ type: "text", text: result }] };
4394
+ }
4395
+ ),
4396
+ tool4(
4397
+ "recommend_merge_tags",
4398
+ "Recommend merging one tag into another",
4399
+ {
4400
+ tagId: z4.string().describe("Tag ID to be merged (removed after merge)"),
4401
+ tagName: z4.string().describe("Name of the tag to be merged"),
4402
+ mergeIntoTagId: z4.string().describe("Tag ID to merge into (kept)"),
4403
+ mergeIntoTagName: z4.string(),
4404
+ reasoning: z4.string()
4405
+ },
4406
+ async (args) => {
4407
+ const result = collectRecommendation(
4408
+ "recommend_merge_tags",
4409
+ args,
4410
+ collector
4411
+ );
4412
+ return { content: [{ type: "text", text: result }] };
4413
+ }
4414
+ ),
4415
+ tool4(
4416
+ "recommend_rename_tag",
4417
+ "Recommend renaming a tag",
4418
+ {
4419
+ tagId: z4.string(),
4420
+ tagName: z4.string().describe("Current tag name"),
4421
+ newName: z4.string().describe("Proposed new name"),
4422
+ reasoning: z4.string()
4423
+ },
4424
+ async (args) => {
4425
+ const result = collectRecommendation(
4426
+ "recommend_rename_tag",
4427
+ args,
4428
+ collector
4429
+ );
4430
+ return { content: [{ type: "text", text: result }] };
4431
+ }
4432
+ ),
4433
+ tool4(
4434
+ "complete_audit",
4435
+ "Signal that the audit is complete with a summary of all findings",
4436
+ { summary: z4.string().describe("Brief overview of all findings") },
4437
+ async (args) => {
4438
+ collector.complete = true;
4439
+ collector.summary = args.summary ?? "Audit completed.";
4440
+ return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
4441
+ }
4442
+ )
4443
+ ];
4444
+ return createSdkMcpServer2({
4445
+ name: "tag-audit",
4446
+ tools: auditTools
4447
+ });
4448
+ }
4449
+
4450
+ // src/runner/project-audit-handler.ts
4451
+ var logger5 = createServiceLogger("ProjectAudit");
4452
+ var FALLBACK_MODEL2 = "claude-sonnet-4-20250514";
4453
+ function buildTagSection(tags) {
4454
+ if (tags.length === 0) return "No tags configured yet.";
4455
+ return tags.map((t) => {
4456
+ const paths = t.contextPaths ?? [];
4457
+ const pathStr = paths.length > 0 ? `
4458
+ Context links: ${paths.map((p) => `${p.type}:${p.path}`).join(", ")}` : "";
4459
+ return ` - ${t.name} (id: ${t.id})${t.description ? `: ${t.description}` : " [no description]"}${pathStr}
4460
+ Active tasks: ${t.activeTaskCount}`;
4461
+ }).join("\n");
4462
+ }
4463
+ function buildHeatmapSection(entries) {
4464
+ if (entries.length === 0) return "No file read analytics data available.";
4465
+ return entries.slice(0, 50).map((e) => {
4466
+ const tagBreakdown = Object.entries(e.byTag).sort(([, a], [, b]) => b - a).map(([tag, count]) => `${tag}:${count}`).join(", ");
4467
+ return ` ${e.filePath} \u2014 ${e.totalReads} reads${tagBreakdown ? ` (${tagBreakdown})` : ""}`;
4468
+ }).join("\n");
4469
+ }
4470
+ function buildAuditSystemPrompt(projectName, tags, heatmapData, projectDir) {
4471
+ return [
4472
+ "You are a project organization expert analyzing tag taxonomy for a software project.",
4473
+ "Tags are used to categorize tasks and link relevant documentation/rules/files to subsystems.",
4474
+ "",
4475
+ `PROJECT: ${projectName}`,
4476
+ "",
4477
+ `EXISTING TAGS (${tags.length}):`,
4478
+ buildTagSection(tags),
4479
+ "",
4480
+ "FILE READ ANALYTICS (what agents actually read, by tag):",
4481
+ buildHeatmapSection(heatmapData),
4482
+ "",
4483
+ `You have full access to the codebase at: ${projectDir}`,
4484
+ "Use your file reading and searching tools to understand the codebase structure,",
4485
+ "module boundaries, and architectural patterns before making recommendations.",
4486
+ "",
4487
+ "ANALYSIS TASKS:",
4488
+ "1. Read actual source files to understand code areas and module boundaries",
4489
+ "2. Search for imports, class definitions, and architectural patterns",
4490
+ "3. Coverage: Are all major subsystems/services represented by tags?",
4491
+ "4. Descriptions: Do all tags have clear, useful descriptions?",
4492
+ "5. Context Links: Are relevant rules/docs/folders linked to tags via contextPaths?",
4493
+ "6. Documentation Gaps: Which high-read files lack linked documentation?",
4494
+ "7. Cleanup: Any tags that should be merged, renamed, or removed?",
4495
+ "",
4496
+ "Use the tag-audit MCP tools to submit each recommendation.",
4497
+ "Call complete_audit when you are done with a thorough summary.",
4498
+ "Be comprehensive \u2014 recommend all improvements your analysis supports.",
4499
+ "Analyze actual file contents, not just file names."
4500
+ ].join("\n");
4501
+ }
4502
+ async function runAuditQuery(request, connection, projectDir) {
4503
+ let agentCtx = null;
4504
+ try {
4505
+ agentCtx = await connection.fetchAgentContext();
4506
+ } catch {
4507
+ logger5.warn("Could not fetch agent context for audit, using defaults");
4508
+ }
4509
+ const model = agentCtx?.model || FALLBACK_MODEL2;
4510
+ const settings = agentCtx?.agentSettings ?? {};
4511
+ const collector = {
4512
+ recommendations: [],
4513
+ summary: "Audit completed.",
4514
+ complete: false
4515
+ };
4516
+ const systemPrompt = buildAuditSystemPrompt(
4517
+ request.projectName,
4518
+ request.tags,
4519
+ request.fileHeatmap,
4520
+ projectDir
4521
+ );
4522
+ const userPrompt = [
4523
+ "Analyze the project's tag taxonomy and submit recommendations using the tag-audit MCP tools.",
4524
+ `There are currently ${request.tags.length} tags configured.`,
4525
+ request.fileHeatmap.length > 0 ? `File analytics show ${request.fileHeatmap.length} files with read activity.` : "No file read analytics available.",
4526
+ "",
4527
+ "Start by exploring the codebase structure, then analyze each tag for accuracy and completeness.",
4528
+ "Call complete_audit when done."
4529
+ ].join("\n");
4530
+ const events = query3({
4531
+ prompt: userPrompt,
4532
+ options: {
4533
+ model,
4534
+ systemPrompt: { type: "preset", preset: "claude_code", append: systemPrompt },
4535
+ cwd: projectDir,
4536
+ permissionMode: "bypassPermissions",
4537
+ allowDangerouslySkipPermissions: true,
4538
+ tools: { type: "preset", preset: "claude_code" },
4539
+ mcpServers: { "tag-audit": createAuditMcpServer(collector) },
4540
+ maxTurns: settings.maxTurns ?? 30,
4541
+ maxBudgetUsd: settings.maxBudgetUsd ?? 5,
4542
+ effort: settings.effort,
4543
+ thinking: settings.thinking
4544
+ }
4545
+ });
4546
+ for await (const event of events) {
4547
+ if (event.type === "result") break;
4548
+ }
4549
+ return collector;
4550
+ }
4551
+ async function handleProjectAuditRequest(request, connection, projectDir) {
4552
+ connection.emitAgentStatus("busy");
4553
+ try {
4554
+ const collector = await runAuditQuery(request, connection, projectDir);
4555
+ const result = {
4556
+ recommendations: collector.recommendations,
4557
+ summary: collector.summary,
4558
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
4559
+ };
4560
+ logger5.info("Tag audit completed", {
4561
+ requestId: request.requestId,
4562
+ recommendationCount: result.recommendations.length
4563
+ });
4564
+ connection.emitAuditResult({ requestId: request.requestId, result });
4565
+ } catch (error) {
4566
+ logger5.error("Tag audit failed", {
4567
+ requestId: request.requestId,
4568
+ ...errorMeta(error)
4569
+ });
4570
+ connection.emitAuditResult({
4571
+ requestId: request.requestId,
4572
+ error: error instanceof Error ? error.message : "Tag audit failed"
4573
+ });
4574
+ } finally {
4575
+ connection.emitAgentStatus("idle");
4576
+ }
4577
+ }
4578
+
3840
4579
  // src/runner/project-runner.ts
3841
- var logger4 = createServiceLogger("ProjectRunner");
4580
+ var logger6 = createServiceLogger("ProjectRunner");
3842
4581
  var __filename = fileURLToPath(import.meta.url);
3843
4582
  var __dirname = path.dirname(__filename);
3844
4583
  var HEARTBEAT_INTERVAL_MS2 = 3e4;
3845
4584
  var MAX_CONCURRENT = Math.max(1, parseInt(process.env.CONVEYOR_MAX_CONCURRENT ?? "10", 10) || 10);
3846
4585
  var STOP_TIMEOUT_MS = 3e4;
4586
+ var START_CMD_KILL_TIMEOUT_MS = 5e3;
3847
4587
  function setupWorkDir(projectDir, assignment) {
3848
4588
  const { taskId, branch, devBranch, useWorktree } = assignment;
3849
4589
  const shortId = taskId.slice(0, 8);
@@ -3856,12 +4596,12 @@ function setupWorkDir(projectDir, assignment) {
3856
4596
  }
3857
4597
  if (branch && branch !== devBranch) {
3858
4598
  try {
3859
- execSync5(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
4599
+ execSync6(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
3860
4600
  } catch {
3861
4601
  try {
3862
- execSync5(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
4602
+ execSync6(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
3863
4603
  } catch {
3864
- logger4.warn("Could not checkout branch", { taskId: shortId, branch });
4604
+ logger6.warn("Could not checkout branch", { taskId: shortId, branch });
3865
4605
  }
3866
4606
  }
3867
4607
  }
@@ -3900,13 +4640,13 @@ function spawnChildAgent(assignment, workDir) {
3900
4640
  child.stdout?.on("data", (data) => {
3901
4641
  const lines = data.toString().trimEnd().split("\n");
3902
4642
  for (const line of lines) {
3903
- logger4.info(line, { taskId: shortId });
4643
+ logger6.info(line, { taskId: shortId });
3904
4644
  }
3905
4645
  });
3906
4646
  child.stderr?.on("data", (data) => {
3907
4647
  const lines = data.toString().trimEnd().split("\n");
3908
4648
  for (const line of lines) {
3909
- logger4.error(line, { taskId: shortId });
4649
+ logger6.error(line, { taskId: shortId });
3910
4650
  }
3911
4651
  });
3912
4652
  return child;
@@ -3918,31 +4658,348 @@ var ProjectRunner = class {
3918
4658
  heartbeatTimer = null;
3919
4659
  stopping = false;
3920
4660
  resolveLifecycle = null;
4661
+ // Start command process management
4662
+ startCommandChild = null;
4663
+ startCommandRunning = false;
4664
+ setupComplete = false;
4665
+ branchSwitchCommand;
4666
+ commitWatcher;
3921
4667
  constructor(config) {
3922
4668
  this.projectDir = config.projectDir;
3923
4669
  this.connection = new ProjectConnection({
3924
4670
  apiUrl: config.conveyorApiUrl,
3925
4671
  projectToken: config.projectToken,
3926
- projectId: config.projectId
4672
+ projectId: config.projectId,
4673
+ projectDir: config.projectDir
3927
4674
  });
4675
+ this.commitWatcher = new CommitWatcher(
4676
+ {
4677
+ projectDir: this.projectDir,
4678
+ pollIntervalMs: Number(process.env.CONVEYOR_COMMIT_POLL_INTERVAL) || 1e4,
4679
+ debounceMs: 3e3
4680
+ },
4681
+ {
4682
+ onNewCommits: async (data) => {
4683
+ this.connection.emitNewCommitsDetected({
4684
+ branch: data.branch,
4685
+ commitCount: data.commitCount,
4686
+ latestCommit: {
4687
+ sha: data.newCommitSha,
4688
+ message: data.latestMessage,
4689
+ author: data.latestAuthor
4690
+ },
4691
+ autoSyncing: true
4692
+ });
4693
+ const startTime = Date.now();
4694
+ const stepsRun = await this.smartSync(data.previousSha, data.newCommitSha, data.branch);
4695
+ this.connection.emitEnvironmentReady({
4696
+ branch: data.branch,
4697
+ commitsSynced: data.commitCount,
4698
+ syncDurationMs: Date.now() - startTime,
4699
+ stepsRun
4700
+ });
4701
+ }
4702
+ }
4703
+ );
3928
4704
  }
3929
4705
  checkoutWorkspaceBranch() {
3930
4706
  const workspaceBranch = process.env.CONVEYOR_WORKSPACE_BRANCH;
3931
4707
  if (!workspaceBranch) return;
3932
4708
  try {
3933
- execSync5(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
3934
- execSync5(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
3935
- logger4.info("Checked out workspace branch", { workspaceBranch });
4709
+ execSync6(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
4710
+ execSync6(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
4711
+ logger6.info("Checked out workspace branch", { workspaceBranch });
3936
4712
  } catch (err) {
3937
- logger4.warn("Failed to checkout workspace branch, continuing on current branch", {
4713
+ logger6.warn("Failed to checkout workspace branch, continuing on current branch", {
3938
4714
  workspaceBranch,
3939
4715
  ...errorMeta(err)
3940
4716
  });
3941
4717
  }
3942
4718
  }
4719
+ async executeSetupCommand() {
4720
+ const cmd = process.env.CONVEYOR_SETUP_COMMAND;
4721
+ if (!cmd) return;
4722
+ logger6.info("Running setup command", { command: cmd });
4723
+ try {
4724
+ await runSetupCommand(cmd, this.projectDir, (stream, data) => {
4725
+ this.connection.emitEvent({ type: "setup_output", stream, data });
4726
+ (stream === "stderr" ? process.stderr : process.stdout).write(data);
4727
+ });
4728
+ logger6.info("Setup command completed");
4729
+ } catch (error) {
4730
+ logger6.error("Setup command failed", errorMeta(error));
4731
+ this.connection.emitEvent({
4732
+ type: "setup_error",
4733
+ message: error instanceof Error ? error.message : "Setup command failed"
4734
+ });
4735
+ throw error;
4736
+ }
4737
+ }
4738
+ executeStartCommand() {
4739
+ const cmd = process.env.CONVEYOR_START_COMMAND;
4740
+ if (!cmd) return;
4741
+ logger6.info("Running start command", { command: cmd });
4742
+ const child = runStartCommand(cmd, this.projectDir, (stream, data) => {
4743
+ this.connection.emitEvent({ type: "start_command_output", stream, data });
4744
+ (stream === "stderr" ? process.stderr : process.stdout).write(data);
4745
+ });
4746
+ this.startCommandChild = child;
4747
+ this.startCommandRunning = true;
4748
+ child.on("exit", (code, signal) => {
4749
+ this.startCommandRunning = false;
4750
+ this.startCommandChild = null;
4751
+ logger6.info("Start command exited", { code, signal });
4752
+ this.connection.emitEvent({
4753
+ type: "start_command_exited",
4754
+ code,
4755
+ signal,
4756
+ message: `Start command exited with code ${code}`
4757
+ });
4758
+ });
4759
+ child.on("error", (err) => {
4760
+ this.startCommandRunning = false;
4761
+ this.startCommandChild = null;
4762
+ logger6.error("Start command error", errorMeta(err));
4763
+ });
4764
+ }
4765
+ async killStartCommand() {
4766
+ const child = this.startCommandChild;
4767
+ if (!child || !this.startCommandRunning) return;
4768
+ logger6.info("Killing start command");
4769
+ try {
4770
+ if (child.pid) process.kill(-child.pid, "SIGTERM");
4771
+ } catch {
4772
+ child.kill("SIGTERM");
4773
+ }
4774
+ await new Promise((resolve2) => {
4775
+ const timer = setTimeout(() => {
4776
+ if (this.startCommandRunning && child.pid) {
4777
+ try {
4778
+ process.kill(-child.pid, "SIGKILL");
4779
+ } catch {
4780
+ child.kill("SIGKILL");
4781
+ }
4782
+ }
4783
+ resolve2();
4784
+ }, START_CMD_KILL_TIMEOUT_MS);
4785
+ child.on("exit", () => {
4786
+ clearTimeout(timer);
4787
+ resolve2();
4788
+ });
4789
+ });
4790
+ this.startCommandChild = null;
4791
+ this.startCommandRunning = false;
4792
+ }
4793
+ async restartStartCommand() {
4794
+ await this.killStartCommand();
4795
+ this.executeStartCommand();
4796
+ }
4797
+ getEnvironmentStatus() {
4798
+ let currentBranch = "unknown";
4799
+ try {
4800
+ currentBranch = execSync6("git branch --show-current", {
4801
+ cwd: this.projectDir,
4802
+ stdio: ["ignore", "pipe", "ignore"]
4803
+ }).toString().trim();
4804
+ } catch {
4805
+ }
4806
+ return {
4807
+ setupComplete: this.setupComplete,
4808
+ startCommandRunning: this.startCommandRunning,
4809
+ currentBranch,
4810
+ previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || null
4811
+ };
4812
+ }
4813
+ getCurrentBranch() {
4814
+ try {
4815
+ return execSync6("git branch --show-current", {
4816
+ cwd: this.projectDir,
4817
+ stdio: ["ignore", "pipe", "ignore"]
4818
+ }).toString().trim() || null;
4819
+ } catch {
4820
+ return null;
4821
+ }
4822
+ }
4823
+ // oxlint-disable-next-line max-lines-per-function, complexity -- sequential sync steps with per-step error handling
4824
+ async smartSync(previousSha, newSha, branch) {
4825
+ const stepsRun = [];
4826
+ const status = execSync6("git status --porcelain", {
4827
+ cwd: this.projectDir,
4828
+ stdio: ["ignore", "pipe", "ignore"]
4829
+ }).toString().trim();
4830
+ if (status) {
4831
+ this.connection.emitEvent({
4832
+ type: "commit_watch_warning",
4833
+ message: "Working tree has uncommitted changes. Auto-pull skipped."
4834
+ });
4835
+ return ["skipped:dirty_tree"];
4836
+ }
4837
+ await this.killStartCommand();
4838
+ this.connection.emitEnvSwitchProgress({ step: "pull", status: "running" });
4839
+ try {
4840
+ execSync6(`git pull origin ${branch}`, {
4841
+ cwd: this.projectDir,
4842
+ stdio: "pipe",
4843
+ timeout: 6e4
4844
+ });
4845
+ stepsRun.push("pull");
4846
+ this.connection.emitEnvSwitchProgress({ step: "pull", status: "success" });
4847
+ } catch (err) {
4848
+ const message = err instanceof Error ? err.message : "Pull failed";
4849
+ this.connection.emitEnvSwitchProgress({ step: "pull", status: "error", message });
4850
+ logger6.error("Git pull failed during commit sync", errorMeta(err));
4851
+ this.executeStartCommand();
4852
+ return ["error:pull"];
4853
+ }
4854
+ let changedFiles = [];
4855
+ try {
4856
+ changedFiles = execSync6(`git diff --name-only ${previousSha}..${newSha}`, {
4857
+ cwd: this.projectDir,
4858
+ stdio: ["ignore", "pipe", "ignore"]
4859
+ }).toString().trim().split("\n").filter(Boolean);
4860
+ } catch {
4861
+ }
4862
+ const needsInstall = changedFiles.some(
4863
+ (f) => f === "package.json" || f === "bun.lockb" || f === "bunfig.toml" || f.endsWith("/package.json") || f.endsWith("/bun.lockb")
4864
+ );
4865
+ const needsPrisma = changedFiles.some(
4866
+ (f) => f.includes("prisma/schema.prisma") || f.includes("prisma/migrations/")
4867
+ );
4868
+ const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
4869
+ if (cmd && (needsInstall || needsPrisma)) {
4870
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
4871
+ try {
4872
+ await runSetupCommand(cmd, this.projectDir, (stream, data) => {
4873
+ this.connection.emitEvent({ type: "sync_output", stream, data });
4874
+ });
4875
+ stepsRun.push("branchSwitchCommand");
4876
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
4877
+ } catch (err) {
4878
+ const message = err instanceof Error ? err.message : "Sync command failed";
4879
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
4880
+ logger6.error("Branch switch command failed during commit sync", errorMeta(err));
4881
+ }
4882
+ } else if (!cmd) {
4883
+ if (needsInstall) {
4884
+ this.connection.emitEnvSwitchProgress({ step: "install", status: "running" });
4885
+ try {
4886
+ execSync6("bun install", { cwd: this.projectDir, timeout: 12e4, stdio: "pipe" });
4887
+ stepsRun.push("install");
4888
+ this.connection.emitEnvSwitchProgress({ step: "install", status: "success" });
4889
+ } catch (err) {
4890
+ const message = err instanceof Error ? err.message : "Install failed";
4891
+ this.connection.emitEnvSwitchProgress({ step: "install", status: "error", message });
4892
+ logger6.error("bun install failed during commit sync", errorMeta(err));
4893
+ }
4894
+ }
4895
+ if (needsPrisma) {
4896
+ this.connection.emitEnvSwitchProgress({ step: "prisma", status: "running" });
4897
+ try {
4898
+ execSync6("bunx prisma generate", {
4899
+ cwd: this.projectDir,
4900
+ timeout: 6e4,
4901
+ stdio: "pipe"
4902
+ });
4903
+ execSync6("bunx prisma db push --accept-data-loss", {
4904
+ cwd: this.projectDir,
4905
+ timeout: 6e4,
4906
+ stdio: "pipe"
4907
+ });
4908
+ stepsRun.push("prisma");
4909
+ this.connection.emitEnvSwitchProgress({ step: "prisma", status: "success" });
4910
+ } catch (err) {
4911
+ const message = err instanceof Error ? err.message : "Prisma sync failed";
4912
+ this.connection.emitEnvSwitchProgress({ step: "prisma", status: "error", message });
4913
+ logger6.error("Prisma sync failed during commit sync", errorMeta(err));
4914
+ }
4915
+ }
4916
+ }
4917
+ this.executeStartCommand();
4918
+ stepsRun.push("startCommand");
4919
+ return stepsRun;
4920
+ }
4921
+ async handleSwitchBranch(data, callback) {
4922
+ const { branch, syncAfter } = data;
4923
+ try {
4924
+ this.connection.emitEnvSwitchProgress({ step: "fetch", status: "running" });
4925
+ try {
4926
+ execSync6("git fetch origin", { cwd: this.projectDir, stdio: "pipe" });
4927
+ } catch {
4928
+ logger6.warn("Git fetch failed during branch switch");
4929
+ }
4930
+ this.connection.emitEnvSwitchProgress({ step: "fetch", status: "success" });
4931
+ this.connection.emitEnvSwitchProgress({ step: "checkout", status: "running" });
4932
+ try {
4933
+ execSync6(`git checkout ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
4934
+ } catch (err) {
4935
+ const message = err instanceof Error ? err.message : "Checkout failed";
4936
+ this.connection.emitEnvSwitchProgress({ step: "checkout", status: "error", message });
4937
+ callback({ ok: false, error: `Failed to checkout branch: ${message}` });
4938
+ return;
4939
+ }
4940
+ try {
4941
+ execSync6(`git pull origin ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
4942
+ } catch {
4943
+ logger6.warn("Git pull failed during branch switch", { branch });
4944
+ }
4945
+ this.connection.emitEnvSwitchProgress({ step: "checkout", status: "success" });
4946
+ if (syncAfter !== false) {
4947
+ await this.handleSyncEnvironment();
4948
+ }
4949
+ this.commitWatcher.start(branch);
4950
+ callback({ ok: true, data: this.getEnvironmentStatus() });
4951
+ } catch (err) {
4952
+ const message = err instanceof Error ? err.message : "Branch switch failed";
4953
+ logger6.error("Branch switch failed", errorMeta(err));
4954
+ callback({ ok: false, error: message });
4955
+ }
4956
+ }
4957
+ async handleSyncEnvironment(callback) {
4958
+ try {
4959
+ await this.killStartCommand();
4960
+ const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
4961
+ if (cmd) {
4962
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
4963
+ try {
4964
+ await runSetupCommand(cmd, this.projectDir, (stream, data) => {
4965
+ this.connection.emitEvent({ type: "sync_output", stream, data });
4966
+ (stream === "stderr" ? process.stderr : process.stdout).write(data);
4967
+ });
4968
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
4969
+ } catch (err) {
4970
+ const message = err instanceof Error ? err.message : "Sync command failed";
4971
+ this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
4972
+ logger6.error("Branch switch sync command failed", errorMeta(err));
4973
+ }
4974
+ }
4975
+ this.executeStartCommand();
4976
+ this.connection.emitEnvSwitchProgress({ step: "startCommand", status: "success" });
4977
+ callback?.({ ok: true, data: this.getEnvironmentStatus() });
4978
+ } catch (err) {
4979
+ const message = err instanceof Error ? err.message : "Sync failed";
4980
+ logger6.error("Environment sync failed", errorMeta(err));
4981
+ callback?.({ ok: false, error: message });
4982
+ }
4983
+ }
4984
+ handleGetEnvStatus(callback) {
4985
+ callback({ ok: true, data: this.getEnvironmentStatus() });
4986
+ }
3943
4987
  async start() {
3944
4988
  this.checkoutWorkspaceBranch();
3945
4989
  await this.connection.connect();
4990
+ try {
4991
+ await this.executeSetupCommand();
4992
+ this.executeStartCommand();
4993
+ this.setupComplete = true;
4994
+ this.connection.emitEvent({
4995
+ type: "setup_complete",
4996
+ previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || void 0,
4997
+ startCommandRunning: this.startCommandRunning
4998
+ });
4999
+ } catch (error) {
5000
+ logger6.error("Environment setup failed", errorMeta(error));
5001
+ this.setupComplete = false;
5002
+ }
3946
5003
  this.connection.onTaskAssignment((assignment) => {
3947
5004
  this.handleAssignment(assignment);
3948
5005
  });
@@ -3950,17 +5007,40 @@ var ProjectRunner = class {
3950
5007
  this.handleStopTask(data.taskId);
3951
5008
  });
3952
5009
  this.connection.onShutdown(() => {
3953
- logger4.info("Received shutdown signal from server");
5010
+ logger6.info("Received shutdown signal from server");
3954
5011
  void this.stop();
3955
5012
  });
3956
5013
  this.connection.onChatMessage((msg) => {
3957
- logger4.debug("Received project chat message");
5014
+ logger6.debug("Received project chat message");
3958
5015
  void handleProjectChatMessage(msg, this.connection, this.projectDir);
3959
5016
  });
5017
+ this.connection.onAuditRequest((request) => {
5018
+ logger6.debug("Received tag audit request", { requestId: request.requestId });
5019
+ void handleProjectAuditRequest(request, this.connection, this.projectDir);
5020
+ });
5021
+ this.connection.onSwitchBranch = (data, cb) => {
5022
+ void this.handleSwitchBranch(data, cb);
5023
+ };
5024
+ this.connection.onSyncEnvironment = (cb) => {
5025
+ void this.handleSyncEnvironment(cb);
5026
+ };
5027
+ this.connection.onGetEnvStatus = (cb) => {
5028
+ this.handleGetEnvStatus(cb);
5029
+ };
5030
+ try {
5031
+ const context = await this.connection.fetchAgentContext();
5032
+ this.branchSwitchCommand = context?.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
5033
+ } catch {
5034
+ this.branchSwitchCommand = process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
5035
+ }
3960
5036
  this.heartbeatTimer = setInterval(() => {
3961
5037
  this.connection.sendHeartbeat();
3962
5038
  }, HEARTBEAT_INTERVAL_MS2);
3963
- logger4.info("Connected, waiting for task assignments");
5039
+ const currentBranch = this.getCurrentBranch();
5040
+ if (currentBranch) {
5041
+ this.commitWatcher.start(currentBranch);
5042
+ }
5043
+ logger6.info("Connected, waiting for task assignments");
3964
5044
  await new Promise((resolve2) => {
3965
5045
  this.resolveLifecycle = resolve2;
3966
5046
  process.on("SIGTERM", () => void this.stop());
@@ -3971,11 +5051,11 @@ var ProjectRunner = class {
3971
5051
  const { taskId, mode } = assignment;
3972
5052
  const shortId = taskId.slice(0, 8);
3973
5053
  if (this.activeAgents.has(taskId)) {
3974
- logger4.info("Task already running, skipping", { taskId: shortId });
5054
+ logger6.info("Task already running, skipping", { taskId: shortId });
3975
5055
  return;
3976
5056
  }
3977
5057
  if (this.activeAgents.size >= MAX_CONCURRENT) {
3978
- logger4.warn("Max concurrent agents reached, rejecting task", {
5058
+ logger6.warn("Max concurrent agents reached, rejecting task", {
3979
5059
  maxConcurrent: MAX_CONCURRENT,
3980
5060
  taskId: shortId
3981
5061
  });
@@ -3984,9 +5064,9 @@ var ProjectRunner = class {
3984
5064
  }
3985
5065
  try {
3986
5066
  try {
3987
- execSync5("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
5067
+ execSync6("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
3988
5068
  } catch {
3989
- logger4.warn("Git fetch failed", { taskId: shortId });
5069
+ logger6.warn("Git fetch failed", { taskId: shortId });
3990
5070
  }
3991
5071
  const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
3992
5072
  const child = spawnChildAgent(assignment, workDir);
@@ -3997,12 +5077,12 @@ var ProjectRunner = class {
3997
5077
  usesWorktree
3998
5078
  });
3999
5079
  this.connection.emitTaskStarted(taskId);
4000
- logger4.info("Started task", { taskId: shortId, mode, workDir });
5080
+ logger6.info("Started task", { taskId: shortId, mode, workDir });
4001
5081
  child.on("exit", (code) => {
4002
5082
  this.activeAgents.delete(taskId);
4003
5083
  const reason = code === 0 ? "completed" : `exited with code ${code}`;
4004
5084
  this.connection.emitTaskStopped(taskId, reason);
4005
- logger4.info("Task exited", { taskId: shortId, reason });
5085
+ logger6.info("Task exited", { taskId: shortId, reason });
4006
5086
  if (code === 0 && usesWorktree) {
4007
5087
  try {
4008
5088
  removeWorktree(this.projectDir, taskId);
@@ -4011,7 +5091,7 @@ var ProjectRunner = class {
4011
5091
  }
4012
5092
  });
4013
5093
  } catch (error) {
4014
- logger4.error("Failed to start task", {
5094
+ logger6.error("Failed to start task", {
4015
5095
  taskId: shortId,
4016
5096
  ...errorMeta(error)
4017
5097
  });
@@ -4025,7 +5105,7 @@ var ProjectRunner = class {
4025
5105
  const agent = this.activeAgents.get(taskId);
4026
5106
  if (!agent) return;
4027
5107
  const shortId = taskId.slice(0, 8);
4028
- logger4.info("Stopping task", { taskId: shortId });
5108
+ logger6.info("Stopping task", { taskId: shortId });
4029
5109
  agent.process.kill("SIGTERM");
4030
5110
  const timer = setTimeout(() => {
4031
5111
  if (this.activeAgents.has(taskId)) {
@@ -4045,7 +5125,9 @@ var ProjectRunner = class {
4045
5125
  async stop() {
4046
5126
  if (this.stopping) return;
4047
5127
  this.stopping = true;
4048
- logger4.info("Shutting down");
5128
+ logger6.info("Shutting down");
5129
+ this.commitWatcher.stop();
5130
+ await this.killStartCommand();
4049
5131
  if (this.heartbeatTimer) {
4050
5132
  clearInterval(this.heartbeatTimer);
4051
5133
  this.heartbeatTimer = null;
@@ -4070,7 +5152,7 @@ var ProjectRunner = class {
4070
5152
  })
4071
5153
  ]);
4072
5154
  this.connection.disconnect();
4073
- logger4.info("Shutdown complete");
5155
+ logger6.info("Shutdown complete");
4074
5156
  if (this.resolveLifecycle) {
4075
5157
  this.resolveLifecycle();
4076
5158
  this.resolveLifecycle = null;
@@ -4156,4 +5238,4 @@ export {
4156
5238
  ProjectRunner,
4157
5239
  FileCache
4158
5240
  };
4159
- //# sourceMappingURL=chunk-N3TSLBSH.js.map
5241
+ //# sourceMappingURL=chunk-MRTSBPY7.js.map