@rallycry/conveyor-agent 5.9.4 → 5.10.2
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-N3TSLBSH.js → chunk-WPQXAPVA.js} +1162 -44
- package/dist/chunk-WPQXAPVA.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-N3TSLBSH.js.map +0 -1
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4145
|
+
logger4.warn("Could not fetch chat history, proceeding without it");
|
|
3755
4146
|
}
|
|
3756
4147
|
return { agentCtx, chatHistory };
|
|
3757
4148
|
}
|
|
@@ -3775,6 +4166,41 @@ function buildChatQueryOptions(agentCtx, projectDir) {
|
|
|
3775
4166
|
thinking: settings.thinking
|
|
3776
4167
|
};
|
|
3777
4168
|
}
|
|
4169
|
+
function emitResultCostAndContext(event, connection) {
|
|
4170
|
+
const resultEvent = event;
|
|
4171
|
+
if (resultEvent.total_cost_usd !== void 0 && resultEvent.total_cost_usd > 0) {
|
|
4172
|
+
connection.emitEvent({
|
|
4173
|
+
type: "cost_update",
|
|
4174
|
+
costUsd: resultEvent.total_cost_usd
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
if (resultEvent.modelUsage && typeof resultEvent.modelUsage === "object") {
|
|
4178
|
+
const modelUsage = resultEvent.modelUsage;
|
|
4179
|
+
let contextWindow = 0;
|
|
4180
|
+
let totalInputTokens = 0;
|
|
4181
|
+
let totalCacheRead = 0;
|
|
4182
|
+
let totalCacheCreation = 0;
|
|
4183
|
+
for (const data of Object.values(modelUsage)) {
|
|
4184
|
+
const d = data;
|
|
4185
|
+
totalInputTokens += d.inputTokens ?? 0;
|
|
4186
|
+
totalCacheRead += d.cacheReadInputTokens ?? 0;
|
|
4187
|
+
totalCacheCreation += d.cacheCreationInputTokens ?? 0;
|
|
4188
|
+
const cw = d.contextWindow ?? 0;
|
|
4189
|
+
if (cw > contextWindow) contextWindow = cw;
|
|
4190
|
+
}
|
|
4191
|
+
if (contextWindow > 0) {
|
|
4192
|
+
const queryInputTokens = totalInputTokens + totalCacheRead + totalCacheCreation;
|
|
4193
|
+
connection.emitEvent({
|
|
4194
|
+
type: "context_update",
|
|
4195
|
+
contextTokens: queryInputTokens,
|
|
4196
|
+
contextWindow,
|
|
4197
|
+
inputTokens: totalInputTokens,
|
|
4198
|
+
cacheReadInputTokens: totalCacheRead,
|
|
4199
|
+
cacheCreationInputTokens: totalCacheCreation
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
3778
4204
|
function processEventStream(event, connection, responseParts, turnToolCalls, isTyping) {
|
|
3779
4205
|
if (event.type === "assistant") {
|
|
3780
4206
|
if (!isTyping.value) {
|
|
@@ -3796,6 +4222,7 @@ function processEventStream(event, connection, responseParts, turnToolCalls, isT
|
|
|
3796
4222
|
connection.emitEvent({ type: "agent_typing_stop" });
|
|
3797
4223
|
isTyping.value = false;
|
|
3798
4224
|
}
|
|
4225
|
+
emitResultCostAndContext(event, connection);
|
|
3799
4226
|
return true;
|
|
3800
4227
|
}
|
|
3801
4228
|
return false;
|
|
@@ -3804,6 +4231,7 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
3804
4231
|
const { agentCtx, chatHistory } = await fetchContext(connection);
|
|
3805
4232
|
const options = buildChatQueryOptions(agentCtx, projectDir);
|
|
3806
4233
|
const prompt = buildPrompt(message, chatHistory);
|
|
4234
|
+
connection.emitAgentStatus("running");
|
|
3807
4235
|
const events = query2({ prompt, options });
|
|
3808
4236
|
const responseParts = [];
|
|
3809
4237
|
const turnToolCalls = [];
|
|
@@ -3821,11 +4249,12 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
3821
4249
|
}
|
|
3822
4250
|
}
|
|
3823
4251
|
async function handleProjectChatMessage(message, connection, projectDir) {
|
|
3824
|
-
connection.emitAgentStatus("
|
|
4252
|
+
connection.emitAgentStatus("fetching_context");
|
|
3825
4253
|
try {
|
|
3826
4254
|
await runChatQuery(message, connection, projectDir);
|
|
3827
4255
|
} catch (error) {
|
|
3828
|
-
|
|
4256
|
+
logger4.error("Failed to handle message", errorMeta(error));
|
|
4257
|
+
connection.emitAgentStatus("error");
|
|
3829
4258
|
try {
|
|
3830
4259
|
await connection.emitChatMessage(
|
|
3831
4260
|
"I encountered an error processing your message. Please try again."
|
|
@@ -3837,13 +4266,360 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
3837
4266
|
}
|
|
3838
4267
|
}
|
|
3839
4268
|
|
|
4269
|
+
// src/runner/project-audit-handler.ts
|
|
4270
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
4271
|
+
|
|
4272
|
+
// src/tools/audit-tools.ts
|
|
4273
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4274
|
+
import { tool as tool4, createSdkMcpServer as createSdkMcpServer2 } from "@anthropic-ai/claude-agent-sdk";
|
|
4275
|
+
import { z as z4 } from "zod";
|
|
4276
|
+
function mapCreateTag(input) {
|
|
4277
|
+
return {
|
|
4278
|
+
type: "create_tag",
|
|
4279
|
+
tagName: input.name,
|
|
4280
|
+
suggestion: `Create new tag "${input.name}"${input.description ? `: ${input.description}` : ""}`,
|
|
4281
|
+
reasoning: input.reasoning,
|
|
4282
|
+
payload: { name: input.name, color: input.color ?? "#6B7280", description: input.description }
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
function mapUpdateDescription(input) {
|
|
4286
|
+
return {
|
|
4287
|
+
type: "update_description",
|
|
4288
|
+
tagId: input.tagId,
|
|
4289
|
+
tagName: input.tagName,
|
|
4290
|
+
suggestion: `Update description for "${input.tagName}"`,
|
|
4291
|
+
reasoning: input.reasoning,
|
|
4292
|
+
payload: { description: input.description }
|
|
4293
|
+
};
|
|
4294
|
+
}
|
|
4295
|
+
function mapContextLink(input) {
|
|
4296
|
+
return {
|
|
4297
|
+
type: "add_context_link",
|
|
4298
|
+
tagId: input.tagId,
|
|
4299
|
+
tagName: input.tagName,
|
|
4300
|
+
suggestion: `Link ${input.linkType}:${input.path} to "${input.tagName}"`,
|
|
4301
|
+
reasoning: input.reasoning,
|
|
4302
|
+
payload: { contextLink: { type: input.linkType, path: input.path, label: input.label } }
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
function mapDocGap(input) {
|
|
4306
|
+
return {
|
|
4307
|
+
type: "documentation_gap",
|
|
4308
|
+
tagId: input.tagId,
|
|
4309
|
+
tagName: input.tagName,
|
|
4310
|
+
suggestion: `Documentation gap: ${input.filePath} (${input.readCount} reads)`,
|
|
4311
|
+
reasoning: input.reasoning,
|
|
4312
|
+
payload: {
|
|
4313
|
+
filePath: input.filePath,
|
|
4314
|
+
readCount: input.readCount,
|
|
4315
|
+
suggestedAction: input.suggestedAction
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4319
|
+
function mapMergeTags(input) {
|
|
4320
|
+
return {
|
|
4321
|
+
type: "merge_tags",
|
|
4322
|
+
tagId: input.tagId,
|
|
4323
|
+
tagName: input.tagName,
|
|
4324
|
+
suggestion: `Merge "${input.tagName}" into "${input.mergeIntoTagName}"`,
|
|
4325
|
+
reasoning: input.reasoning,
|
|
4326
|
+
payload: { mergeIntoTagId: input.mergeIntoTagId }
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
function mapRenameTag(input) {
|
|
4330
|
+
return {
|
|
4331
|
+
type: "rename_tag",
|
|
4332
|
+
tagId: input.tagId,
|
|
4333
|
+
tagName: input.tagName,
|
|
4334
|
+
suggestion: `Rename "${input.tagName}" to "${input.newName}"`,
|
|
4335
|
+
reasoning: input.reasoning,
|
|
4336
|
+
payload: { newName: input.newName }
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
var TOOL_MAPPERS = {
|
|
4340
|
+
recommend_create_tag: mapCreateTag,
|
|
4341
|
+
recommend_update_description: mapUpdateDescription,
|
|
4342
|
+
recommend_context_link: mapContextLink,
|
|
4343
|
+
flag_documentation_gap: mapDocGap,
|
|
4344
|
+
recommend_merge_tags: mapMergeTags,
|
|
4345
|
+
recommend_rename_tag: mapRenameTag
|
|
4346
|
+
};
|
|
4347
|
+
function collectRecommendation(toolName, input, collector) {
|
|
4348
|
+
const mapper = TOOL_MAPPERS[toolName];
|
|
4349
|
+
if (!mapper) return JSON.stringify({ error: `Unknown tool: ${toolName}` });
|
|
4350
|
+
const rec = { id: randomUUID3(), ...mapper(input) };
|
|
4351
|
+
collector.recommendations.push(rec);
|
|
4352
|
+
return JSON.stringify({ success: true, recommendationId: rec.id });
|
|
4353
|
+
}
|
|
4354
|
+
function createAuditMcpServer(collector) {
|
|
4355
|
+
const auditTools = [
|
|
4356
|
+
tool4(
|
|
4357
|
+
"recommend_create_tag",
|
|
4358
|
+
"Recommend creating a new tag for an uncovered subsystem or area",
|
|
4359
|
+
{
|
|
4360
|
+
name: z4.string().describe("Proposed tag name (lowercase, hyphenated)"),
|
|
4361
|
+
color: z4.string().optional().describe("Hex color code"),
|
|
4362
|
+
description: z4.string().describe("What this tag covers"),
|
|
4363
|
+
reasoning: z4.string().describe("Why this tag should be created")
|
|
4364
|
+
},
|
|
4365
|
+
async (args) => {
|
|
4366
|
+
const result = collectRecommendation(
|
|
4367
|
+
"recommend_create_tag",
|
|
4368
|
+
args,
|
|
4369
|
+
collector
|
|
4370
|
+
);
|
|
4371
|
+
return { content: [{ type: "text", text: result }] };
|
|
4372
|
+
}
|
|
4373
|
+
),
|
|
4374
|
+
tool4(
|
|
4375
|
+
"recommend_update_description",
|
|
4376
|
+
"Recommend updating a tag's description to better reflect its scope",
|
|
4377
|
+
{
|
|
4378
|
+
tagId: z4.string(),
|
|
4379
|
+
tagName: z4.string(),
|
|
4380
|
+
description: z4.string().describe("Proposed new description"),
|
|
4381
|
+
reasoning: z4.string()
|
|
4382
|
+
},
|
|
4383
|
+
async (args) => {
|
|
4384
|
+
const result = collectRecommendation(
|
|
4385
|
+
"recommend_update_description",
|
|
4386
|
+
args,
|
|
4387
|
+
collector
|
|
4388
|
+
);
|
|
4389
|
+
return { content: [{ type: "text", text: result }] };
|
|
4390
|
+
}
|
|
4391
|
+
),
|
|
4392
|
+
tool4(
|
|
4393
|
+
"recommend_context_link",
|
|
4394
|
+
"Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
|
|
4395
|
+
{
|
|
4396
|
+
tagId: z4.string(),
|
|
4397
|
+
tagName: z4.string(),
|
|
4398
|
+
linkType: z4.enum(["rule", "doc", "file", "folder"]),
|
|
4399
|
+
path: z4.string(),
|
|
4400
|
+
label: z4.string().optional(),
|
|
4401
|
+
reasoning: z4.string()
|
|
4402
|
+
},
|
|
4403
|
+
async (args) => {
|
|
4404
|
+
const result = collectRecommendation(
|
|
4405
|
+
"recommend_context_link",
|
|
4406
|
+
args,
|
|
4407
|
+
collector
|
|
4408
|
+
);
|
|
4409
|
+
return { content: [{ type: "text", text: result }] };
|
|
4410
|
+
}
|
|
4411
|
+
),
|
|
4412
|
+
tool4(
|
|
4413
|
+
"flag_documentation_gap",
|
|
4414
|
+
"Flag a file that agents read heavily but has no tag documentation linked",
|
|
4415
|
+
{
|
|
4416
|
+
tagName: z4.string().describe("Tag whose agents read this file"),
|
|
4417
|
+
tagId: z4.string().optional(),
|
|
4418
|
+
filePath: z4.string(),
|
|
4419
|
+
readCount: z4.number(),
|
|
4420
|
+
suggestedAction: z4.string().describe("What doc or rule should be created"),
|
|
4421
|
+
reasoning: z4.string()
|
|
4422
|
+
},
|
|
4423
|
+
async (args) => {
|
|
4424
|
+
const result = collectRecommendation(
|
|
4425
|
+
"flag_documentation_gap",
|
|
4426
|
+
args,
|
|
4427
|
+
collector
|
|
4428
|
+
);
|
|
4429
|
+
return { content: [{ type: "text", text: result }] };
|
|
4430
|
+
}
|
|
4431
|
+
),
|
|
4432
|
+
tool4(
|
|
4433
|
+
"recommend_merge_tags",
|
|
4434
|
+
"Recommend merging one tag into another",
|
|
4435
|
+
{
|
|
4436
|
+
tagId: z4.string().describe("Tag ID to be merged (removed after merge)"),
|
|
4437
|
+
tagName: z4.string().describe("Name of the tag to be merged"),
|
|
4438
|
+
mergeIntoTagId: z4.string().describe("Tag ID to merge into (kept)"),
|
|
4439
|
+
mergeIntoTagName: z4.string(),
|
|
4440
|
+
reasoning: z4.string()
|
|
4441
|
+
},
|
|
4442
|
+
async (args) => {
|
|
4443
|
+
const result = collectRecommendation(
|
|
4444
|
+
"recommend_merge_tags",
|
|
4445
|
+
args,
|
|
4446
|
+
collector
|
|
4447
|
+
);
|
|
4448
|
+
return { content: [{ type: "text", text: result }] };
|
|
4449
|
+
}
|
|
4450
|
+
),
|
|
4451
|
+
tool4(
|
|
4452
|
+
"recommend_rename_tag",
|
|
4453
|
+
"Recommend renaming a tag",
|
|
4454
|
+
{
|
|
4455
|
+
tagId: z4.string(),
|
|
4456
|
+
tagName: z4.string().describe("Current tag name"),
|
|
4457
|
+
newName: z4.string().describe("Proposed new name"),
|
|
4458
|
+
reasoning: z4.string()
|
|
4459
|
+
},
|
|
4460
|
+
async (args) => {
|
|
4461
|
+
const result = collectRecommendation(
|
|
4462
|
+
"recommend_rename_tag",
|
|
4463
|
+
args,
|
|
4464
|
+
collector
|
|
4465
|
+
);
|
|
4466
|
+
return { content: [{ type: "text", text: result }] };
|
|
4467
|
+
}
|
|
4468
|
+
),
|
|
4469
|
+
tool4(
|
|
4470
|
+
"complete_audit",
|
|
4471
|
+
"Signal that the audit is complete with a summary of all findings",
|
|
4472
|
+
{ summary: z4.string().describe("Brief overview of all findings") },
|
|
4473
|
+
async (args) => {
|
|
4474
|
+
collector.complete = true;
|
|
4475
|
+
collector.summary = args.summary ?? "Audit completed.";
|
|
4476
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
|
|
4477
|
+
}
|
|
4478
|
+
)
|
|
4479
|
+
];
|
|
4480
|
+
return createSdkMcpServer2({
|
|
4481
|
+
name: "tag-audit",
|
|
4482
|
+
tools: auditTools
|
|
4483
|
+
});
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
// src/runner/project-audit-handler.ts
|
|
4487
|
+
var logger5 = createServiceLogger("ProjectAudit");
|
|
4488
|
+
var FALLBACK_MODEL2 = "claude-sonnet-4-20250514";
|
|
4489
|
+
function buildTagSection(tags) {
|
|
4490
|
+
if (tags.length === 0) return "No tags configured yet.";
|
|
4491
|
+
return tags.map((t) => {
|
|
4492
|
+
const paths = t.contextPaths ?? [];
|
|
4493
|
+
const pathStr = paths.length > 0 ? `
|
|
4494
|
+
Context links: ${paths.map((p) => `${p.type}:${p.path}`).join(", ")}` : "";
|
|
4495
|
+
return ` - ${t.name} (id: ${t.id})${t.description ? `: ${t.description}` : " [no description]"}${pathStr}
|
|
4496
|
+
Active tasks: ${t.activeTaskCount}`;
|
|
4497
|
+
}).join("\n");
|
|
4498
|
+
}
|
|
4499
|
+
function buildHeatmapSection(entries) {
|
|
4500
|
+
if (entries.length === 0) return "No file read analytics data available.";
|
|
4501
|
+
return entries.slice(0, 50).map((e) => {
|
|
4502
|
+
const tagBreakdown = Object.entries(e.byTag).sort(([, a], [, b]) => b - a).map(([tag, count]) => `${tag}:${count}`).join(", ");
|
|
4503
|
+
return ` ${e.filePath} \u2014 ${e.totalReads} reads${tagBreakdown ? ` (${tagBreakdown})` : ""}`;
|
|
4504
|
+
}).join("\n");
|
|
4505
|
+
}
|
|
4506
|
+
function buildAuditSystemPrompt(projectName, tags, heatmapData, projectDir) {
|
|
4507
|
+
return [
|
|
4508
|
+
"You are a project organization expert analyzing tag taxonomy for a software project.",
|
|
4509
|
+
"Tags are used to categorize tasks and link relevant documentation/rules/files to subsystems.",
|
|
4510
|
+
"",
|
|
4511
|
+
`PROJECT: ${projectName}`,
|
|
4512
|
+
"",
|
|
4513
|
+
`EXISTING TAGS (${tags.length}):`,
|
|
4514
|
+
buildTagSection(tags),
|
|
4515
|
+
"",
|
|
4516
|
+
"FILE READ ANALYTICS (what agents actually read, by tag):",
|
|
4517
|
+
buildHeatmapSection(heatmapData),
|
|
4518
|
+
"",
|
|
4519
|
+
`You have full access to the codebase at: ${projectDir}`,
|
|
4520
|
+
"Use your file reading and searching tools to understand the codebase structure,",
|
|
4521
|
+
"module boundaries, and architectural patterns before making recommendations.",
|
|
4522
|
+
"",
|
|
4523
|
+
"ANALYSIS TASKS:",
|
|
4524
|
+
"1. Read actual source files to understand code areas and module boundaries",
|
|
4525
|
+
"2. Search for imports, class definitions, and architectural patterns",
|
|
4526
|
+
"3. Coverage: Are all major subsystems/services represented by tags?",
|
|
4527
|
+
"4. Descriptions: Do all tags have clear, useful descriptions?",
|
|
4528
|
+
"5. Context Links: Are relevant rules/docs/folders linked to tags via contextPaths?",
|
|
4529
|
+
"6. Documentation Gaps: Which high-read files lack linked documentation?",
|
|
4530
|
+
"7. Cleanup: Any tags that should be merged, renamed, or removed?",
|
|
4531
|
+
"",
|
|
4532
|
+
"Use the tag-audit MCP tools to submit each recommendation.",
|
|
4533
|
+
"Call complete_audit when you are done with a thorough summary.",
|
|
4534
|
+
"Be comprehensive \u2014 recommend all improvements your analysis supports.",
|
|
4535
|
+
"Analyze actual file contents, not just file names."
|
|
4536
|
+
].join("\n");
|
|
4537
|
+
}
|
|
4538
|
+
async function runAuditQuery(request, connection, projectDir) {
|
|
4539
|
+
let agentCtx = null;
|
|
4540
|
+
try {
|
|
4541
|
+
agentCtx = await connection.fetchAgentContext();
|
|
4542
|
+
} catch {
|
|
4543
|
+
logger5.warn("Could not fetch agent context for audit, using defaults");
|
|
4544
|
+
}
|
|
4545
|
+
const model = agentCtx?.model || FALLBACK_MODEL2;
|
|
4546
|
+
const settings = agentCtx?.agentSettings ?? {};
|
|
4547
|
+
const collector = {
|
|
4548
|
+
recommendations: [],
|
|
4549
|
+
summary: "Audit completed.",
|
|
4550
|
+
complete: false
|
|
4551
|
+
};
|
|
4552
|
+
const systemPrompt = buildAuditSystemPrompt(
|
|
4553
|
+
request.projectName,
|
|
4554
|
+
request.tags,
|
|
4555
|
+
request.fileHeatmap,
|
|
4556
|
+
projectDir
|
|
4557
|
+
);
|
|
4558
|
+
const userPrompt = [
|
|
4559
|
+
"Analyze the project's tag taxonomy and submit recommendations using the tag-audit MCP tools.",
|
|
4560
|
+
`There are currently ${request.tags.length} tags configured.`,
|
|
4561
|
+
request.fileHeatmap.length > 0 ? `File analytics show ${request.fileHeatmap.length} files with read activity.` : "No file read analytics available.",
|
|
4562
|
+
"",
|
|
4563
|
+
"Start by exploring the codebase structure, then analyze each tag for accuracy and completeness.",
|
|
4564
|
+
"Call complete_audit when done."
|
|
4565
|
+
].join("\n");
|
|
4566
|
+
const events = query3({
|
|
4567
|
+
prompt: userPrompt,
|
|
4568
|
+
options: {
|
|
4569
|
+
model,
|
|
4570
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: systemPrompt },
|
|
4571
|
+
cwd: projectDir,
|
|
4572
|
+
permissionMode: "bypassPermissions",
|
|
4573
|
+
allowDangerouslySkipPermissions: true,
|
|
4574
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
4575
|
+
mcpServers: { "tag-audit": createAuditMcpServer(collector) },
|
|
4576
|
+
maxTurns: settings.maxTurns ?? 30,
|
|
4577
|
+
maxBudgetUsd: settings.maxBudgetUsd ?? 5,
|
|
4578
|
+
effort: settings.effort,
|
|
4579
|
+
thinking: settings.thinking
|
|
4580
|
+
}
|
|
4581
|
+
});
|
|
4582
|
+
for await (const event of events) {
|
|
4583
|
+
if (event.type === "result") break;
|
|
4584
|
+
}
|
|
4585
|
+
return collector;
|
|
4586
|
+
}
|
|
4587
|
+
async function handleProjectAuditRequest(request, connection, projectDir) {
|
|
4588
|
+
connection.emitAgentStatus("busy");
|
|
4589
|
+
try {
|
|
4590
|
+
const collector = await runAuditQuery(request, connection, projectDir);
|
|
4591
|
+
const result = {
|
|
4592
|
+
recommendations: collector.recommendations,
|
|
4593
|
+
summary: collector.summary,
|
|
4594
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4595
|
+
};
|
|
4596
|
+
logger5.info("Tag audit completed", {
|
|
4597
|
+
requestId: request.requestId,
|
|
4598
|
+
recommendationCount: result.recommendations.length
|
|
4599
|
+
});
|
|
4600
|
+
connection.emitAuditResult({ requestId: request.requestId, result });
|
|
4601
|
+
} catch (error) {
|
|
4602
|
+
logger5.error("Tag audit failed", {
|
|
4603
|
+
requestId: request.requestId,
|
|
4604
|
+
...errorMeta(error)
|
|
4605
|
+
});
|
|
4606
|
+
connection.emitAuditResult({
|
|
4607
|
+
requestId: request.requestId,
|
|
4608
|
+
error: error instanceof Error ? error.message : "Tag audit failed"
|
|
4609
|
+
});
|
|
4610
|
+
} finally {
|
|
4611
|
+
connection.emitAgentStatus("idle");
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
3840
4615
|
// src/runner/project-runner.ts
|
|
3841
|
-
var
|
|
4616
|
+
var logger6 = createServiceLogger("ProjectRunner");
|
|
3842
4617
|
var __filename = fileURLToPath(import.meta.url);
|
|
3843
4618
|
var __dirname = path.dirname(__filename);
|
|
3844
4619
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
3845
4620
|
var MAX_CONCURRENT = Math.max(1, parseInt(process.env.CONVEYOR_MAX_CONCURRENT ?? "10", 10) || 10);
|
|
3846
4621
|
var STOP_TIMEOUT_MS = 3e4;
|
|
4622
|
+
var START_CMD_KILL_TIMEOUT_MS = 5e3;
|
|
3847
4623
|
function setupWorkDir(projectDir, assignment) {
|
|
3848
4624
|
const { taskId, branch, devBranch, useWorktree } = assignment;
|
|
3849
4625
|
const shortId = taskId.slice(0, 8);
|
|
@@ -3856,12 +4632,12 @@ function setupWorkDir(projectDir, assignment) {
|
|
|
3856
4632
|
}
|
|
3857
4633
|
if (branch && branch !== devBranch) {
|
|
3858
4634
|
try {
|
|
3859
|
-
|
|
4635
|
+
execSync6(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
3860
4636
|
} catch {
|
|
3861
4637
|
try {
|
|
3862
|
-
|
|
4638
|
+
execSync6(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
3863
4639
|
} catch {
|
|
3864
|
-
|
|
4640
|
+
logger6.warn("Could not checkout branch", { taskId: shortId, branch });
|
|
3865
4641
|
}
|
|
3866
4642
|
}
|
|
3867
4643
|
}
|
|
@@ -3900,13 +4676,13 @@ function spawnChildAgent(assignment, workDir) {
|
|
|
3900
4676
|
child.stdout?.on("data", (data) => {
|
|
3901
4677
|
const lines = data.toString().trimEnd().split("\n");
|
|
3902
4678
|
for (const line of lines) {
|
|
3903
|
-
|
|
4679
|
+
logger6.info(line, { taskId: shortId });
|
|
3904
4680
|
}
|
|
3905
4681
|
});
|
|
3906
4682
|
child.stderr?.on("data", (data) => {
|
|
3907
4683
|
const lines = data.toString().trimEnd().split("\n");
|
|
3908
4684
|
for (const line of lines) {
|
|
3909
|
-
|
|
4685
|
+
logger6.error(line, { taskId: shortId });
|
|
3910
4686
|
}
|
|
3911
4687
|
});
|
|
3912
4688
|
return child;
|
|
@@ -3918,31 +4694,348 @@ var ProjectRunner = class {
|
|
|
3918
4694
|
heartbeatTimer = null;
|
|
3919
4695
|
stopping = false;
|
|
3920
4696
|
resolveLifecycle = null;
|
|
4697
|
+
// Start command process management
|
|
4698
|
+
startCommandChild = null;
|
|
4699
|
+
startCommandRunning = false;
|
|
4700
|
+
setupComplete = false;
|
|
4701
|
+
branchSwitchCommand;
|
|
4702
|
+
commitWatcher;
|
|
3921
4703
|
constructor(config) {
|
|
3922
4704
|
this.projectDir = config.projectDir;
|
|
3923
4705
|
this.connection = new ProjectConnection({
|
|
3924
4706
|
apiUrl: config.conveyorApiUrl,
|
|
3925
4707
|
projectToken: config.projectToken,
|
|
3926
|
-
projectId: config.projectId
|
|
4708
|
+
projectId: config.projectId,
|
|
4709
|
+
projectDir: config.projectDir
|
|
3927
4710
|
});
|
|
4711
|
+
this.commitWatcher = new CommitWatcher(
|
|
4712
|
+
{
|
|
4713
|
+
projectDir: this.projectDir,
|
|
4714
|
+
pollIntervalMs: Number(process.env.CONVEYOR_COMMIT_POLL_INTERVAL) || 1e4,
|
|
4715
|
+
debounceMs: 3e3
|
|
4716
|
+
},
|
|
4717
|
+
{
|
|
4718
|
+
onNewCommits: async (data) => {
|
|
4719
|
+
this.connection.emitNewCommitsDetected({
|
|
4720
|
+
branch: data.branch,
|
|
4721
|
+
commitCount: data.commitCount,
|
|
4722
|
+
latestCommit: {
|
|
4723
|
+
sha: data.newCommitSha,
|
|
4724
|
+
message: data.latestMessage,
|
|
4725
|
+
author: data.latestAuthor
|
|
4726
|
+
},
|
|
4727
|
+
autoSyncing: true
|
|
4728
|
+
});
|
|
4729
|
+
const startTime = Date.now();
|
|
4730
|
+
const stepsRun = await this.smartSync(data.previousSha, data.newCommitSha, data.branch);
|
|
4731
|
+
this.connection.emitEnvironmentReady({
|
|
4732
|
+
branch: data.branch,
|
|
4733
|
+
commitsSynced: data.commitCount,
|
|
4734
|
+
syncDurationMs: Date.now() - startTime,
|
|
4735
|
+
stepsRun
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
);
|
|
3928
4740
|
}
|
|
3929
4741
|
checkoutWorkspaceBranch() {
|
|
3930
4742
|
const workspaceBranch = process.env.CONVEYOR_WORKSPACE_BRANCH;
|
|
3931
4743
|
if (!workspaceBranch) return;
|
|
3932
4744
|
try {
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
4745
|
+
execSync6(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
4746
|
+
execSync6(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
4747
|
+
logger6.info("Checked out workspace branch", { workspaceBranch });
|
|
3936
4748
|
} catch (err) {
|
|
3937
|
-
|
|
4749
|
+
logger6.warn("Failed to checkout workspace branch, continuing on current branch", {
|
|
3938
4750
|
workspaceBranch,
|
|
3939
4751
|
...errorMeta(err)
|
|
3940
4752
|
});
|
|
3941
4753
|
}
|
|
3942
4754
|
}
|
|
4755
|
+
async executeSetupCommand() {
|
|
4756
|
+
const cmd = process.env.CONVEYOR_SETUP_COMMAND;
|
|
4757
|
+
if (!cmd) return;
|
|
4758
|
+
logger6.info("Running setup command", { command: cmd });
|
|
4759
|
+
try {
|
|
4760
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
4761
|
+
this.connection.emitEvent({ type: "setup_output", stream, data });
|
|
4762
|
+
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
4763
|
+
});
|
|
4764
|
+
logger6.info("Setup command completed");
|
|
4765
|
+
} catch (error) {
|
|
4766
|
+
logger6.error("Setup command failed", errorMeta(error));
|
|
4767
|
+
this.connection.emitEvent({
|
|
4768
|
+
type: "setup_error",
|
|
4769
|
+
message: error instanceof Error ? error.message : "Setup command failed"
|
|
4770
|
+
});
|
|
4771
|
+
throw error;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
executeStartCommand() {
|
|
4775
|
+
const cmd = process.env.CONVEYOR_START_COMMAND;
|
|
4776
|
+
if (!cmd) return;
|
|
4777
|
+
logger6.info("Running start command", { command: cmd });
|
|
4778
|
+
const child = runStartCommand(cmd, this.projectDir, (stream, data) => {
|
|
4779
|
+
this.connection.emitEvent({ type: "start_command_output", stream, data });
|
|
4780
|
+
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
4781
|
+
});
|
|
4782
|
+
this.startCommandChild = child;
|
|
4783
|
+
this.startCommandRunning = true;
|
|
4784
|
+
child.on("exit", (code, signal) => {
|
|
4785
|
+
this.startCommandRunning = false;
|
|
4786
|
+
this.startCommandChild = null;
|
|
4787
|
+
logger6.info("Start command exited", { code, signal });
|
|
4788
|
+
this.connection.emitEvent({
|
|
4789
|
+
type: "start_command_exited",
|
|
4790
|
+
code,
|
|
4791
|
+
signal,
|
|
4792
|
+
message: `Start command exited with code ${code}`
|
|
4793
|
+
});
|
|
4794
|
+
});
|
|
4795
|
+
child.on("error", (err) => {
|
|
4796
|
+
this.startCommandRunning = false;
|
|
4797
|
+
this.startCommandChild = null;
|
|
4798
|
+
logger6.error("Start command error", errorMeta(err));
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
async killStartCommand() {
|
|
4802
|
+
const child = this.startCommandChild;
|
|
4803
|
+
if (!child || !this.startCommandRunning) return;
|
|
4804
|
+
logger6.info("Killing start command");
|
|
4805
|
+
try {
|
|
4806
|
+
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
4807
|
+
} catch {
|
|
4808
|
+
child.kill("SIGTERM");
|
|
4809
|
+
}
|
|
4810
|
+
await new Promise((resolve2) => {
|
|
4811
|
+
const timer = setTimeout(() => {
|
|
4812
|
+
if (this.startCommandRunning && child.pid) {
|
|
4813
|
+
try {
|
|
4814
|
+
process.kill(-child.pid, "SIGKILL");
|
|
4815
|
+
} catch {
|
|
4816
|
+
child.kill("SIGKILL");
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
resolve2();
|
|
4820
|
+
}, START_CMD_KILL_TIMEOUT_MS);
|
|
4821
|
+
child.on("exit", () => {
|
|
4822
|
+
clearTimeout(timer);
|
|
4823
|
+
resolve2();
|
|
4824
|
+
});
|
|
4825
|
+
});
|
|
4826
|
+
this.startCommandChild = null;
|
|
4827
|
+
this.startCommandRunning = false;
|
|
4828
|
+
}
|
|
4829
|
+
async restartStartCommand() {
|
|
4830
|
+
await this.killStartCommand();
|
|
4831
|
+
this.executeStartCommand();
|
|
4832
|
+
}
|
|
4833
|
+
getEnvironmentStatus() {
|
|
4834
|
+
let currentBranch = "unknown";
|
|
4835
|
+
try {
|
|
4836
|
+
currentBranch = execSync6("git branch --show-current", {
|
|
4837
|
+
cwd: this.projectDir,
|
|
4838
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4839
|
+
}).toString().trim();
|
|
4840
|
+
} catch {
|
|
4841
|
+
}
|
|
4842
|
+
return {
|
|
4843
|
+
setupComplete: this.setupComplete,
|
|
4844
|
+
startCommandRunning: this.startCommandRunning,
|
|
4845
|
+
currentBranch,
|
|
4846
|
+
previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || null
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
getCurrentBranch() {
|
|
4850
|
+
try {
|
|
4851
|
+
return execSync6("git branch --show-current", {
|
|
4852
|
+
cwd: this.projectDir,
|
|
4853
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4854
|
+
}).toString().trim() || null;
|
|
4855
|
+
} catch {
|
|
4856
|
+
return null;
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
// oxlint-disable-next-line max-lines-per-function, complexity -- sequential sync steps with per-step error handling
|
|
4860
|
+
async smartSync(previousSha, newSha, branch) {
|
|
4861
|
+
const stepsRun = [];
|
|
4862
|
+
const status = execSync6("git status --porcelain", {
|
|
4863
|
+
cwd: this.projectDir,
|
|
4864
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4865
|
+
}).toString().trim();
|
|
4866
|
+
if (status) {
|
|
4867
|
+
this.connection.emitEvent({
|
|
4868
|
+
type: "commit_watch_warning",
|
|
4869
|
+
message: "Working tree has uncommitted changes. Auto-pull skipped."
|
|
4870
|
+
});
|
|
4871
|
+
return ["skipped:dirty_tree"];
|
|
4872
|
+
}
|
|
4873
|
+
await this.killStartCommand();
|
|
4874
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "running" });
|
|
4875
|
+
try {
|
|
4876
|
+
execSync6(`git pull origin ${branch}`, {
|
|
4877
|
+
cwd: this.projectDir,
|
|
4878
|
+
stdio: "pipe",
|
|
4879
|
+
timeout: 6e4
|
|
4880
|
+
});
|
|
4881
|
+
stepsRun.push("pull");
|
|
4882
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "success" });
|
|
4883
|
+
} catch (err) {
|
|
4884
|
+
const message = err instanceof Error ? err.message : "Pull failed";
|
|
4885
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "error", message });
|
|
4886
|
+
logger6.error("Git pull failed during commit sync", errorMeta(err));
|
|
4887
|
+
this.executeStartCommand();
|
|
4888
|
+
return ["error:pull"];
|
|
4889
|
+
}
|
|
4890
|
+
let changedFiles = [];
|
|
4891
|
+
try {
|
|
4892
|
+
changedFiles = execSync6(`git diff --name-only ${previousSha}..${newSha}`, {
|
|
4893
|
+
cwd: this.projectDir,
|
|
4894
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4895
|
+
}).toString().trim().split("\n").filter(Boolean);
|
|
4896
|
+
} catch {
|
|
4897
|
+
}
|
|
4898
|
+
const needsInstall = changedFiles.some(
|
|
4899
|
+
(f) => f === "package.json" || f === "bun.lockb" || f === "bunfig.toml" || f.endsWith("/package.json") || f.endsWith("/bun.lockb")
|
|
4900
|
+
);
|
|
4901
|
+
const needsPrisma = changedFiles.some(
|
|
4902
|
+
(f) => f.includes("prisma/schema.prisma") || f.includes("prisma/migrations/")
|
|
4903
|
+
);
|
|
4904
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
4905
|
+
if (cmd && (needsInstall || needsPrisma)) {
|
|
4906
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
4907
|
+
try {
|
|
4908
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
4909
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
4910
|
+
});
|
|
4911
|
+
stepsRun.push("branchSwitchCommand");
|
|
4912
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
4913
|
+
} catch (err) {
|
|
4914
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
4915
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
4916
|
+
logger6.error("Branch switch command failed during commit sync", errorMeta(err));
|
|
4917
|
+
}
|
|
4918
|
+
} else if (!cmd) {
|
|
4919
|
+
if (needsInstall) {
|
|
4920
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "running" });
|
|
4921
|
+
try {
|
|
4922
|
+
execSync6("bun install", { cwd: this.projectDir, timeout: 12e4, stdio: "pipe" });
|
|
4923
|
+
stepsRun.push("install");
|
|
4924
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "success" });
|
|
4925
|
+
} catch (err) {
|
|
4926
|
+
const message = err instanceof Error ? err.message : "Install failed";
|
|
4927
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "error", message });
|
|
4928
|
+
logger6.error("bun install failed during commit sync", errorMeta(err));
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
if (needsPrisma) {
|
|
4932
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "running" });
|
|
4933
|
+
try {
|
|
4934
|
+
execSync6("bunx prisma generate", {
|
|
4935
|
+
cwd: this.projectDir,
|
|
4936
|
+
timeout: 6e4,
|
|
4937
|
+
stdio: "pipe"
|
|
4938
|
+
});
|
|
4939
|
+
execSync6("bunx prisma db push --accept-data-loss", {
|
|
4940
|
+
cwd: this.projectDir,
|
|
4941
|
+
timeout: 6e4,
|
|
4942
|
+
stdio: "pipe"
|
|
4943
|
+
});
|
|
4944
|
+
stepsRun.push("prisma");
|
|
4945
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "success" });
|
|
4946
|
+
} catch (err) {
|
|
4947
|
+
const message = err instanceof Error ? err.message : "Prisma sync failed";
|
|
4948
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "error", message });
|
|
4949
|
+
logger6.error("Prisma sync failed during commit sync", errorMeta(err));
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
this.executeStartCommand();
|
|
4954
|
+
stepsRun.push("startCommand");
|
|
4955
|
+
return stepsRun;
|
|
4956
|
+
}
|
|
4957
|
+
async handleSwitchBranch(data, callback) {
|
|
4958
|
+
const { branch, syncAfter } = data;
|
|
4959
|
+
try {
|
|
4960
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "running" });
|
|
4961
|
+
try {
|
|
4962
|
+
execSync6("git fetch origin", { cwd: this.projectDir, stdio: "pipe" });
|
|
4963
|
+
} catch {
|
|
4964
|
+
logger6.warn("Git fetch failed during branch switch");
|
|
4965
|
+
}
|
|
4966
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "success" });
|
|
4967
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "running" });
|
|
4968
|
+
try {
|
|
4969
|
+
execSync6(`git checkout ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
4970
|
+
} catch (err) {
|
|
4971
|
+
const message = err instanceof Error ? err.message : "Checkout failed";
|
|
4972
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "error", message });
|
|
4973
|
+
callback({ ok: false, error: `Failed to checkout branch: ${message}` });
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
try {
|
|
4977
|
+
execSync6(`git pull origin ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
4978
|
+
} catch {
|
|
4979
|
+
logger6.warn("Git pull failed during branch switch", { branch });
|
|
4980
|
+
}
|
|
4981
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "success" });
|
|
4982
|
+
if (syncAfter !== false) {
|
|
4983
|
+
await this.handleSyncEnvironment();
|
|
4984
|
+
}
|
|
4985
|
+
this.commitWatcher.start(branch);
|
|
4986
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
4987
|
+
} catch (err) {
|
|
4988
|
+
const message = err instanceof Error ? err.message : "Branch switch failed";
|
|
4989
|
+
logger6.error("Branch switch failed", errorMeta(err));
|
|
4990
|
+
callback({ ok: false, error: message });
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async handleSyncEnvironment(callback) {
|
|
4994
|
+
try {
|
|
4995
|
+
await this.killStartCommand();
|
|
4996
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
4997
|
+
if (cmd) {
|
|
4998
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
4999
|
+
try {
|
|
5000
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
5001
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
5002
|
+
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
5003
|
+
});
|
|
5004
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
5005
|
+
} catch (err) {
|
|
5006
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
5007
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
5008
|
+
logger6.error("Branch switch sync command failed", errorMeta(err));
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
this.executeStartCommand();
|
|
5012
|
+
this.connection.emitEnvSwitchProgress({ step: "startCommand", status: "success" });
|
|
5013
|
+
callback?.({ ok: true, data: this.getEnvironmentStatus() });
|
|
5014
|
+
} catch (err) {
|
|
5015
|
+
const message = err instanceof Error ? err.message : "Sync failed";
|
|
5016
|
+
logger6.error("Environment sync failed", errorMeta(err));
|
|
5017
|
+
callback?.({ ok: false, error: message });
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
handleGetEnvStatus(callback) {
|
|
5021
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
5022
|
+
}
|
|
3943
5023
|
async start() {
|
|
3944
5024
|
this.checkoutWorkspaceBranch();
|
|
3945
5025
|
await this.connection.connect();
|
|
5026
|
+
try {
|
|
5027
|
+
await this.executeSetupCommand();
|
|
5028
|
+
this.executeStartCommand();
|
|
5029
|
+
this.setupComplete = true;
|
|
5030
|
+
this.connection.emitEvent({
|
|
5031
|
+
type: "setup_complete",
|
|
5032
|
+
previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || void 0,
|
|
5033
|
+
startCommandRunning: this.startCommandRunning
|
|
5034
|
+
});
|
|
5035
|
+
} catch (error) {
|
|
5036
|
+
logger6.error("Environment setup failed", errorMeta(error));
|
|
5037
|
+
this.setupComplete = false;
|
|
5038
|
+
}
|
|
3946
5039
|
this.connection.onTaskAssignment((assignment) => {
|
|
3947
5040
|
this.handleAssignment(assignment);
|
|
3948
5041
|
});
|
|
@@ -3950,17 +5043,40 @@ var ProjectRunner = class {
|
|
|
3950
5043
|
this.handleStopTask(data.taskId);
|
|
3951
5044
|
});
|
|
3952
5045
|
this.connection.onShutdown(() => {
|
|
3953
|
-
|
|
5046
|
+
logger6.info("Received shutdown signal from server");
|
|
3954
5047
|
void this.stop();
|
|
3955
5048
|
});
|
|
3956
5049
|
this.connection.onChatMessage((msg) => {
|
|
3957
|
-
|
|
5050
|
+
logger6.debug("Received project chat message");
|
|
3958
5051
|
void handleProjectChatMessage(msg, this.connection, this.projectDir);
|
|
3959
5052
|
});
|
|
5053
|
+
this.connection.onAuditRequest((request) => {
|
|
5054
|
+
logger6.debug("Received tag audit request", { requestId: request.requestId });
|
|
5055
|
+
void handleProjectAuditRequest(request, this.connection, this.projectDir);
|
|
5056
|
+
});
|
|
5057
|
+
this.connection.onSwitchBranch = (data, cb) => {
|
|
5058
|
+
void this.handleSwitchBranch(data, cb);
|
|
5059
|
+
};
|
|
5060
|
+
this.connection.onSyncEnvironment = (cb) => {
|
|
5061
|
+
void this.handleSyncEnvironment(cb);
|
|
5062
|
+
};
|
|
5063
|
+
this.connection.onGetEnvStatus = (cb) => {
|
|
5064
|
+
this.handleGetEnvStatus(cb);
|
|
5065
|
+
};
|
|
5066
|
+
try {
|
|
5067
|
+
const context = await this.connection.fetchAgentContext();
|
|
5068
|
+
this.branchSwitchCommand = context?.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5069
|
+
} catch {
|
|
5070
|
+
this.branchSwitchCommand = process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5071
|
+
}
|
|
3960
5072
|
this.heartbeatTimer = setInterval(() => {
|
|
3961
5073
|
this.connection.sendHeartbeat();
|
|
3962
5074
|
}, HEARTBEAT_INTERVAL_MS2);
|
|
3963
|
-
|
|
5075
|
+
const currentBranch = this.getCurrentBranch();
|
|
5076
|
+
if (currentBranch) {
|
|
5077
|
+
this.commitWatcher.start(currentBranch);
|
|
5078
|
+
}
|
|
5079
|
+
logger6.info("Connected, waiting for task assignments");
|
|
3964
5080
|
await new Promise((resolve2) => {
|
|
3965
5081
|
this.resolveLifecycle = resolve2;
|
|
3966
5082
|
process.on("SIGTERM", () => void this.stop());
|
|
@@ -3971,11 +5087,11 @@ var ProjectRunner = class {
|
|
|
3971
5087
|
const { taskId, mode } = assignment;
|
|
3972
5088
|
const shortId = taskId.slice(0, 8);
|
|
3973
5089
|
if (this.activeAgents.has(taskId)) {
|
|
3974
|
-
|
|
5090
|
+
logger6.info("Task already running, skipping", { taskId: shortId });
|
|
3975
5091
|
return;
|
|
3976
5092
|
}
|
|
3977
5093
|
if (this.activeAgents.size >= MAX_CONCURRENT) {
|
|
3978
|
-
|
|
5094
|
+
logger6.warn("Max concurrent agents reached, rejecting task", {
|
|
3979
5095
|
maxConcurrent: MAX_CONCURRENT,
|
|
3980
5096
|
taskId: shortId
|
|
3981
5097
|
});
|
|
@@ -3984,9 +5100,9 @@ var ProjectRunner = class {
|
|
|
3984
5100
|
}
|
|
3985
5101
|
try {
|
|
3986
5102
|
try {
|
|
3987
|
-
|
|
5103
|
+
execSync6("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
|
|
3988
5104
|
} catch {
|
|
3989
|
-
|
|
5105
|
+
logger6.warn("Git fetch failed", { taskId: shortId });
|
|
3990
5106
|
}
|
|
3991
5107
|
const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
|
|
3992
5108
|
const child = spawnChildAgent(assignment, workDir);
|
|
@@ -3997,12 +5113,12 @@ var ProjectRunner = class {
|
|
|
3997
5113
|
usesWorktree
|
|
3998
5114
|
});
|
|
3999
5115
|
this.connection.emitTaskStarted(taskId);
|
|
4000
|
-
|
|
5116
|
+
logger6.info("Started task", { taskId: shortId, mode, workDir });
|
|
4001
5117
|
child.on("exit", (code) => {
|
|
4002
5118
|
this.activeAgents.delete(taskId);
|
|
4003
5119
|
const reason = code === 0 ? "completed" : `exited with code ${code}`;
|
|
4004
5120
|
this.connection.emitTaskStopped(taskId, reason);
|
|
4005
|
-
|
|
5121
|
+
logger6.info("Task exited", { taskId: shortId, reason });
|
|
4006
5122
|
if (code === 0 && usesWorktree) {
|
|
4007
5123
|
try {
|
|
4008
5124
|
removeWorktree(this.projectDir, taskId);
|
|
@@ -4011,7 +5127,7 @@ var ProjectRunner = class {
|
|
|
4011
5127
|
}
|
|
4012
5128
|
});
|
|
4013
5129
|
} catch (error) {
|
|
4014
|
-
|
|
5130
|
+
logger6.error("Failed to start task", {
|
|
4015
5131
|
taskId: shortId,
|
|
4016
5132
|
...errorMeta(error)
|
|
4017
5133
|
});
|
|
@@ -4025,7 +5141,7 @@ var ProjectRunner = class {
|
|
|
4025
5141
|
const agent = this.activeAgents.get(taskId);
|
|
4026
5142
|
if (!agent) return;
|
|
4027
5143
|
const shortId = taskId.slice(0, 8);
|
|
4028
|
-
|
|
5144
|
+
logger6.info("Stopping task", { taskId: shortId });
|
|
4029
5145
|
agent.process.kill("SIGTERM");
|
|
4030
5146
|
const timer = setTimeout(() => {
|
|
4031
5147
|
if (this.activeAgents.has(taskId)) {
|
|
@@ -4045,7 +5161,9 @@ var ProjectRunner = class {
|
|
|
4045
5161
|
async stop() {
|
|
4046
5162
|
if (this.stopping) return;
|
|
4047
5163
|
this.stopping = true;
|
|
4048
|
-
|
|
5164
|
+
logger6.info("Shutting down");
|
|
5165
|
+
this.commitWatcher.stop();
|
|
5166
|
+
await this.killStartCommand();
|
|
4049
5167
|
if (this.heartbeatTimer) {
|
|
4050
5168
|
clearInterval(this.heartbeatTimer);
|
|
4051
5169
|
this.heartbeatTimer = null;
|
|
@@ -4070,7 +5188,7 @@ var ProjectRunner = class {
|
|
|
4070
5188
|
})
|
|
4071
5189
|
]);
|
|
4072
5190
|
this.connection.disconnect();
|
|
4073
|
-
|
|
5191
|
+
logger6.info("Shutdown complete");
|
|
4074
5192
|
if (this.resolveLifecycle) {
|
|
4075
5193
|
this.resolveLifecycle();
|
|
4076
5194
|
this.resolveLifecycle = null;
|
|
@@ -4156,4 +5274,4 @@ export {
|
|
|
4156
5274
|
ProjectRunner,
|
|
4157
5275
|
FileCache
|
|
4158
5276
|
};
|
|
4159
|
-
//# sourceMappingURL=chunk-
|
|
5277
|
+
//# sourceMappingURL=chunk-WPQXAPVA.js.map
|