@rallycry/conveyor-agent 5.9.3 → 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.
- package/dist/{chunk-MTIMRYLD.js → chunk-MRTSBPY7.js} +1176 -57
- package/dist/chunk-MRTSBPY7.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-MTIMRYLD.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;
|
|
@@ -995,6 +1072,8 @@ async function processResultCase(event, host, context, startTime, state) {
|
|
|
995
1072
|
}
|
|
996
1073
|
async function processEvents(events, context, host) {
|
|
997
1074
|
const startTime = Date.now();
|
|
1075
|
+
let lastStatusEmit = Date.now();
|
|
1076
|
+
const STATUS_REEMIT_INTERVAL_MS = 5e3;
|
|
998
1077
|
const state = {
|
|
999
1078
|
sessionIdStored: false,
|
|
1000
1079
|
isTyping: false,
|
|
@@ -1007,6 +1086,11 @@ async function processEvents(events, context, host) {
|
|
|
1007
1086
|
};
|
|
1008
1087
|
for await (const event of events) {
|
|
1009
1088
|
if (host.isStopped()) break;
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
if (now - lastStatusEmit >= STATUS_REEMIT_INTERVAL_MS) {
|
|
1091
|
+
host.connection.emitStatus("running");
|
|
1092
|
+
lastStatusEmit = now;
|
|
1093
|
+
}
|
|
1010
1094
|
if (host.pendingModeRestart) {
|
|
1011
1095
|
stopTypingIfNeeded(host, state.isTyping);
|
|
1012
1096
|
return { retriable: false, modeRestart: true };
|
|
@@ -1171,6 +1255,184 @@ After addressing the feedback, resume your autonomous loop: call list_subtasks a
|
|
|
1171
1255
|
return parts;
|
|
1172
1256
|
}
|
|
1173
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
|
+
|
|
1174
1436
|
// src/execution/mode-prompt.ts
|
|
1175
1437
|
function buildPropertyInstructions(context) {
|
|
1176
1438
|
const parts = [];
|
|
@@ -1197,7 +1459,14 @@ function buildPropertyInstructions(context) {
|
|
|
1197
1459
|
if (context.projectTags && context.projectTags.length > 0) {
|
|
1198
1460
|
parts.push(``, `Available project tags:`);
|
|
1199
1461
|
for (const tag of context.projectTags) {
|
|
1200
|
-
|
|
1462
|
+
const desc = tag.description ? ` \u2014 ${tag.description}` : "";
|
|
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
|
+
}
|
|
1201
1470
|
}
|
|
1202
1471
|
}
|
|
1203
1472
|
return parts;
|
|
@@ -1343,6 +1612,14 @@ Project Agents:`);
|
|
|
1343
1612
|
parts.push(formatProjectAgentLine(pa));
|
|
1344
1613
|
}
|
|
1345
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
|
+
}
|
|
1346
1623
|
return parts;
|
|
1347
1624
|
}
|
|
1348
1625
|
function buildActivePreamble(context, workspaceDir) {
|
|
@@ -1444,7 +1721,6 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
|
|
|
1444
1721
|
}
|
|
1445
1722
|
|
|
1446
1723
|
// src/execution/prompt-builder.ts
|
|
1447
|
-
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
|
|
1448
1724
|
function formatFileSize(bytes) {
|
|
1449
1725
|
if (bytes === void 0) return "";
|
|
1450
1726
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -1457,16 +1733,16 @@ function findLastAgentMessageIndex2(history) {
|
|
|
1457
1733
|
}
|
|
1458
1734
|
return -1;
|
|
1459
1735
|
}
|
|
1460
|
-
function detectRelaunchScenario(context) {
|
|
1736
|
+
function detectRelaunchScenario(context, trustChatHistory = false) {
|
|
1461
1737
|
const lastAgentIdx = findLastAgentMessageIndex2(context.chatHistory);
|
|
1462
1738
|
if (lastAgentIdx === -1) return "fresh";
|
|
1463
|
-
const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId ||
|
|
1739
|
+
const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || trustChatHistory;
|
|
1464
1740
|
if (!hasPriorWork) return "fresh";
|
|
1465
1741
|
const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
|
|
1466
1742
|
const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
|
|
1467
1743
|
return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
|
|
1468
1744
|
}
|
|
1469
|
-
function buildRelaunchWithSession(mode, context) {
|
|
1745
|
+
function buildRelaunchWithSession(mode, context, agentMode) {
|
|
1470
1746
|
const scenario = detectRelaunchScenario(context);
|
|
1471
1747
|
if (!context.claudeSessionId || scenario === "fresh") return null;
|
|
1472
1748
|
const parts = [];
|
|
@@ -1512,9 +1788,18 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
|
|
|
1512
1788
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
1513
1789
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
1514
1790
|
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
1515
|
-
`Review the current state of the codebase and verify everything is working correctly
|
|
1516
|
-
`Reply with a brief status update (visible in chat), then wait for further instructions.`
|
|
1791
|
+
`Review the current state of the codebase and verify everything is working correctly.`
|
|
1517
1792
|
);
|
|
1793
|
+
if (agentMode === "auto" || agentMode === "building") {
|
|
1794
|
+
parts.push(
|
|
1795
|
+
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
1796
|
+
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
1797
|
+
);
|
|
1798
|
+
} else {
|
|
1799
|
+
parts.push(
|
|
1800
|
+
`Reply with a brief status update (visible in chat), then wait for further instructions.`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1518
1803
|
if (context.githubPRUrl) {
|
|
1519
1804
|
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
1520
1805
|
}
|
|
@@ -1572,7 +1857,17 @@ function formatChatHistory(chatHistory) {
|
|
|
1572
1857
|
}
|
|
1573
1858
|
return parts;
|
|
1574
1859
|
}
|
|
1575
|
-
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) {
|
|
1576
1871
|
const parts = [];
|
|
1577
1872
|
parts.push(`# Task: ${context.title}`);
|
|
1578
1873
|
if (context.description) {
|
|
@@ -1600,6 +1895,8 @@ ${context.plan}`);
|
|
|
1600
1895
|
parts.push(`- [${icon}] \`${ref.path}\``);
|
|
1601
1896
|
}
|
|
1602
1897
|
}
|
|
1898
|
+
const tagSection = await resolveTaskTagContext(context);
|
|
1899
|
+
if (tagSection) parts.push(tagSection);
|
|
1603
1900
|
if (context.chatHistory.length > 0) {
|
|
1604
1901
|
parts.push(...formatChatHistory(context.chatHistory));
|
|
1605
1902
|
}
|
|
@@ -1729,10 +2026,19 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1729
2026
|
parts.push(
|
|
1730
2027
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
1731
2028
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
1732
|
-
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct
|
|
1733
|
-
`Reply with a brief status update summarizing where things stand (visible in chat).`,
|
|
1734
|
-
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
2029
|
+
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`
|
|
1735
2030
|
);
|
|
2031
|
+
if (agentMode === "auto" || agentMode === "building") {
|
|
2032
|
+
parts.push(
|
|
2033
|
+
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
2034
|
+
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
2035
|
+
);
|
|
2036
|
+
} else {
|
|
2037
|
+
parts.push(
|
|
2038
|
+
`Reply with a brief status update summarizing where things stand (visible in chat).`,
|
|
2039
|
+
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
1736
2042
|
if (context.githubPRUrl) {
|
|
1737
2043
|
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
1738
2044
|
}
|
|
@@ -1742,14 +2048,15 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1742
2048
|
parts.push(...buildFeedbackInstructions(context, isPm));
|
|
1743
2049
|
return parts;
|
|
1744
2050
|
}
|
|
1745
|
-
function buildInitialPrompt(mode, context, isAuto, agentMode) {
|
|
2051
|
+
async function buildInitialPrompt(mode, context, isAuto, agentMode) {
|
|
1746
2052
|
const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
|
|
1747
2053
|
if (!isPackRunner) {
|
|
1748
|
-
const sessionRelaunch = buildRelaunchWithSession(mode, context);
|
|
2054
|
+
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode);
|
|
1749
2055
|
if (sessionRelaunch) return sessionRelaunch;
|
|
1750
2056
|
}
|
|
1751
|
-
const
|
|
1752
|
-
const
|
|
2057
|
+
const isPm = mode === "pm";
|
|
2058
|
+
const scenario = detectRelaunchScenario(context, isPm);
|
|
2059
|
+
const body = await buildTaskBody(context);
|
|
1753
2060
|
const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
|
|
1754
2061
|
return [...body, ...instructions].join("\n");
|
|
1755
2062
|
}
|
|
@@ -2658,13 +2965,13 @@ function buildMultimodalPrompt(textPrompt, context, skipImages = false) {
|
|
|
2658
2965
|
}
|
|
2659
2966
|
return blocks;
|
|
2660
2967
|
}
|
|
2661
|
-
function buildFollowUpPrompt(host, context, followUpContent) {
|
|
2968
|
+
async function buildFollowUpPrompt(host, context, followUpContent) {
|
|
2662
2969
|
const isPmMode = host.config.mode === "pm";
|
|
2663
2970
|
const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
2664
2971
|
const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
|
|
2665
2972
|
(b) => b.type === "image"
|
|
2666
2973
|
);
|
|
2667
|
-
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)}
|
|
2668
2975
|
|
|
2669
2976
|
---
|
|
2670
2977
|
|
|
@@ -2693,7 +3000,7 @@ async function runSdkQuery(host, context, followUpContent) {
|
|
|
2693
3000
|
const options = buildQueryOptions(host, context);
|
|
2694
3001
|
const resume = context.claudeSessionId ?? void 0;
|
|
2695
3002
|
if (followUpContent) {
|
|
2696
|
-
const prompt = buildFollowUpPrompt(host, context, followUpContent);
|
|
3003
|
+
const prompt = await buildFollowUpPrompt(host, context, followUpContent);
|
|
2697
3004
|
const agentQuery = query({
|
|
2698
3005
|
prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
|
|
2699
3006
|
options: { ...options, resume }
|
|
@@ -2707,7 +3014,12 @@ async function runSdkQuery(host, context, followUpContent) {
|
|
|
2707
3014
|
} else if (isDiscoveryLike) {
|
|
2708
3015
|
return;
|
|
2709
3016
|
} else {
|
|
2710
|
-
const initialPrompt = buildInitialPrompt(
|
|
3017
|
+
const initialPrompt = await buildInitialPrompt(
|
|
3018
|
+
host.config.mode,
|
|
3019
|
+
context,
|
|
3020
|
+
host.config.isAuto,
|
|
3021
|
+
mode
|
|
3022
|
+
);
|
|
2711
3023
|
const prompt = buildMultimodalPrompt(initialPrompt, context);
|
|
2712
3024
|
const agentQuery = query({
|
|
2713
3025
|
prompt: host.createInputStream(prompt),
|
|
@@ -2724,14 +3036,14 @@ async function runSdkQuery(host, context, followUpContent) {
|
|
|
2724
3036
|
host.syncPlanFile();
|
|
2725
3037
|
}
|
|
2726
3038
|
}
|
|
2727
|
-
function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
3039
|
+
async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
2728
3040
|
if (lastErrorWasImage) {
|
|
2729
3041
|
host.connection.postChatMessage(
|
|
2730
3042
|
"An attached image could not be processed. Retrying without images..."
|
|
2731
3043
|
);
|
|
2732
3044
|
}
|
|
2733
3045
|
const retryPrompt = buildMultimodalPrompt(
|
|
2734
|
-
buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
3046
|
+
await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
2735
3047
|
context,
|
|
2736
3048
|
lastErrorWasImage
|
|
2737
3049
|
);
|
|
@@ -2740,11 +3052,11 @@ function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
2740
3052
|
options: { ...options, resume: void 0 }
|
|
2741
3053
|
});
|
|
2742
3054
|
}
|
|
2743
|
-
function handleStaleSession(context, host, options) {
|
|
3055
|
+
async function handleStaleSession(context, host, options) {
|
|
2744
3056
|
context.claudeSessionId = null;
|
|
2745
3057
|
host.connection.storeSessionId("");
|
|
2746
3058
|
const freshPrompt = buildMultimodalPrompt(
|
|
2747
|
-
buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
3059
|
+
await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
2748
3060
|
context
|
|
2749
3061
|
);
|
|
2750
3062
|
const freshQuery = query({
|
|
@@ -2810,7 +3122,7 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
2810
3122
|
let lastErrorWasImage = false;
|
|
2811
3123
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
2812
3124
|
if (host.isStopped()) return;
|
|
2813
|
-
const agentQuery = attempt === 0 ? initialQuery : buildRetryQuery(host, context, options, lastErrorWasImage);
|
|
3125
|
+
const agentQuery = attempt === 0 ? initialQuery : await buildRetryQuery(host, context, options, lastErrorWasImage);
|
|
2814
3126
|
try {
|
|
2815
3127
|
const { retriable, resultSummary, modeRestart, rateLimitResetsAt, staleSession } = await processEvents(agentQuery, context, host);
|
|
2816
3128
|
if (modeRestart || host.isStopped()) return;
|
|
@@ -3028,6 +3340,16 @@ ${message}`);
|
|
|
3028
3340
|
}
|
|
3029
3341
|
async function runSetupSafe(runnerConfig, connection, callbacks, setupLog, _effectiveAgentMode, setState) {
|
|
3030
3342
|
await setState("setup");
|
|
3343
|
+
if (process.env.CONVEYOR_FROM_PROJECT_RUNNER === "true") {
|
|
3344
|
+
const config2 = await loadConveyorConfig(runnerConfig.workspaceDir);
|
|
3345
|
+
const setupEvent = {
|
|
3346
|
+
type: "setup_complete",
|
|
3347
|
+
previewPort: config2?.previewPort ?? void 0
|
|
3348
|
+
};
|
|
3349
|
+
connection.sendEvent(setupEvent);
|
|
3350
|
+
await callbacks.onEvent(setupEvent);
|
|
3351
|
+
return { ok: true, conveyorConfig: config2 };
|
|
3352
|
+
}
|
|
3031
3353
|
const ports = await loadForwardPorts(runnerConfig.workspaceDir);
|
|
3032
3354
|
if (ports.length > 0 && process.env.CODESPACE_NAME) {
|
|
3033
3355
|
const visibility = ports.map((p) => `${p}:public`).join(" ");
|
|
@@ -3642,13 +3964,118 @@ var AgentRunner = class {
|
|
|
3642
3964
|
|
|
3643
3965
|
// src/runner/project-runner.ts
|
|
3644
3966
|
import { fork } from "child_process";
|
|
3645
|
-
import { execSync as
|
|
3967
|
+
import { execSync as execSync6 } from "child_process";
|
|
3646
3968
|
import * as path from "path";
|
|
3647
3969
|
import { fileURLToPath } from "url";
|
|
3648
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
|
+
|
|
3649
4076
|
// src/runner/project-chat-handler.ts
|
|
3650
4077
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
3651
|
-
var
|
|
4078
|
+
var logger4 = createServiceLogger("ProjectChat");
|
|
3652
4079
|
var FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
3653
4080
|
function buildSystemPrompt2(projectDir, agentCtx) {
|
|
3654
4081
|
const parts = [];
|
|
@@ -3701,7 +4128,7 @@ function processContentBlock(block, responseParts, turnToolCalls) {
|
|
|
3701
4128
|
input: inputStr.slice(0, 1e4),
|
|
3702
4129
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3703
4130
|
});
|
|
3704
|
-
|
|
4131
|
+
logger4.debug("Tool use", { tool: block.name });
|
|
3705
4132
|
}
|
|
3706
4133
|
}
|
|
3707
4134
|
async function fetchContext(connection) {
|
|
@@ -3709,13 +4136,13 @@ async function fetchContext(connection) {
|
|
|
3709
4136
|
try {
|
|
3710
4137
|
agentCtx = await connection.fetchAgentContext();
|
|
3711
4138
|
} catch {
|
|
3712
|
-
|
|
4139
|
+
logger4.warn("Could not fetch agent context, using defaults");
|
|
3713
4140
|
}
|
|
3714
4141
|
let chatHistory = [];
|
|
3715
4142
|
try {
|
|
3716
4143
|
chatHistory = await connection.fetchChatHistory(30);
|
|
3717
4144
|
} catch {
|
|
3718
|
-
|
|
4145
|
+
logger4.warn("Could not fetch chat history, proceeding without it");
|
|
3719
4146
|
}
|
|
3720
4147
|
return { agentCtx, chatHistory };
|
|
3721
4148
|
}
|
|
@@ -3768,6 +4195,7 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
3768
4195
|
const { agentCtx, chatHistory } = await fetchContext(connection);
|
|
3769
4196
|
const options = buildChatQueryOptions(agentCtx, projectDir);
|
|
3770
4197
|
const prompt = buildPrompt(message, chatHistory);
|
|
4198
|
+
connection.emitAgentStatus("running");
|
|
3771
4199
|
const events = query2({ prompt, options });
|
|
3772
4200
|
const responseParts = [];
|
|
3773
4201
|
const turnToolCalls = [];
|
|
@@ -3785,11 +4213,12 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
3785
4213
|
}
|
|
3786
4214
|
}
|
|
3787
4215
|
async function handleProjectChatMessage(message, connection, projectDir) {
|
|
3788
|
-
connection.emitAgentStatus("
|
|
4216
|
+
connection.emitAgentStatus("fetching_context");
|
|
3789
4217
|
try {
|
|
3790
4218
|
await runChatQuery(message, connection, projectDir);
|
|
3791
4219
|
} catch (error) {
|
|
3792
|
-
|
|
4220
|
+
logger4.error("Failed to handle message", errorMeta(error));
|
|
4221
|
+
connection.emitAgentStatus("error");
|
|
3793
4222
|
try {
|
|
3794
4223
|
await connection.emitChatMessage(
|
|
3795
4224
|
"I encountered an error processing your message. Please try again."
|
|
@@ -3801,13 +4230,360 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
3801
4230
|
}
|
|
3802
4231
|
}
|
|
3803
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
|
+
|
|
3804
4579
|
// src/runner/project-runner.ts
|
|
3805
|
-
var
|
|
4580
|
+
var logger6 = createServiceLogger("ProjectRunner");
|
|
3806
4581
|
var __filename = fileURLToPath(import.meta.url);
|
|
3807
4582
|
var __dirname = path.dirname(__filename);
|
|
3808
4583
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
3809
4584
|
var MAX_CONCURRENT = Math.max(1, parseInt(process.env.CONVEYOR_MAX_CONCURRENT ?? "10", 10) || 10);
|
|
3810
4585
|
var STOP_TIMEOUT_MS = 3e4;
|
|
4586
|
+
var START_CMD_KILL_TIMEOUT_MS = 5e3;
|
|
3811
4587
|
function setupWorkDir(projectDir, assignment) {
|
|
3812
4588
|
const { taskId, branch, devBranch, useWorktree } = assignment;
|
|
3813
4589
|
const shortId = taskId.slice(0, 8);
|
|
@@ -3820,12 +4596,12 @@ function setupWorkDir(projectDir, assignment) {
|
|
|
3820
4596
|
}
|
|
3821
4597
|
if (branch && branch !== devBranch) {
|
|
3822
4598
|
try {
|
|
3823
|
-
|
|
4599
|
+
execSync6(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
3824
4600
|
} catch {
|
|
3825
4601
|
try {
|
|
3826
|
-
|
|
4602
|
+
execSync6(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
3827
4603
|
} catch {
|
|
3828
|
-
|
|
4604
|
+
logger6.warn("Could not checkout branch", { taskId: shortId, branch });
|
|
3829
4605
|
}
|
|
3830
4606
|
}
|
|
3831
4607
|
}
|
|
@@ -3848,7 +4624,8 @@ function spawnChildAgent(assignment, workDir) {
|
|
|
3848
4624
|
CONVEYOR_USE_WORKTREE: "false",
|
|
3849
4625
|
CONVEYOR_AGENT_MODE: isAuto ? "auto" : "",
|
|
3850
4626
|
CONVEYOR_IS_AUTO: isAuto ? "true" : "false",
|
|
3851
|
-
CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false"
|
|
4627
|
+
CONVEYOR_USE_SANDBOX: useSandbox === true ? "true" : "false",
|
|
4628
|
+
CONVEYOR_FROM_PROJECT_RUNNER: "true"
|
|
3852
4629
|
},
|
|
3853
4630
|
cwd: workDir,
|
|
3854
4631
|
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
@@ -3863,13 +4640,13 @@ function spawnChildAgent(assignment, workDir) {
|
|
|
3863
4640
|
child.stdout?.on("data", (data) => {
|
|
3864
4641
|
const lines = data.toString().trimEnd().split("\n");
|
|
3865
4642
|
for (const line of lines) {
|
|
3866
|
-
|
|
4643
|
+
logger6.info(line, { taskId: shortId });
|
|
3867
4644
|
}
|
|
3868
4645
|
});
|
|
3869
4646
|
child.stderr?.on("data", (data) => {
|
|
3870
4647
|
const lines = data.toString().trimEnd().split("\n");
|
|
3871
4648
|
for (const line of lines) {
|
|
3872
|
-
|
|
4649
|
+
logger6.error(line, { taskId: shortId });
|
|
3873
4650
|
}
|
|
3874
4651
|
});
|
|
3875
4652
|
return child;
|
|
@@ -3881,31 +4658,348 @@ var ProjectRunner = class {
|
|
|
3881
4658
|
heartbeatTimer = null;
|
|
3882
4659
|
stopping = false;
|
|
3883
4660
|
resolveLifecycle = null;
|
|
4661
|
+
// Start command process management
|
|
4662
|
+
startCommandChild = null;
|
|
4663
|
+
startCommandRunning = false;
|
|
4664
|
+
setupComplete = false;
|
|
4665
|
+
branchSwitchCommand;
|
|
4666
|
+
commitWatcher;
|
|
3884
4667
|
constructor(config) {
|
|
3885
4668
|
this.projectDir = config.projectDir;
|
|
3886
4669
|
this.connection = new ProjectConnection({
|
|
3887
4670
|
apiUrl: config.conveyorApiUrl,
|
|
3888
4671
|
projectToken: config.projectToken,
|
|
3889
|
-
projectId: config.projectId
|
|
4672
|
+
projectId: config.projectId,
|
|
4673
|
+
projectDir: config.projectDir
|
|
3890
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
|
+
);
|
|
3891
4704
|
}
|
|
3892
4705
|
checkoutWorkspaceBranch() {
|
|
3893
4706
|
const workspaceBranch = process.env.CONVEYOR_WORKSPACE_BRANCH;
|
|
3894
4707
|
if (!workspaceBranch) return;
|
|
3895
4708
|
try {
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
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 });
|
|
3899
4712
|
} catch (err) {
|
|
3900
|
-
|
|
4713
|
+
logger6.warn("Failed to checkout workspace branch, continuing on current branch", {
|
|
3901
4714
|
workspaceBranch,
|
|
3902
4715
|
...errorMeta(err)
|
|
3903
4716
|
});
|
|
3904
4717
|
}
|
|
3905
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
|
+
}
|
|
3906
4987
|
async start() {
|
|
3907
4988
|
this.checkoutWorkspaceBranch();
|
|
3908
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
|
+
}
|
|
3909
5003
|
this.connection.onTaskAssignment((assignment) => {
|
|
3910
5004
|
this.handleAssignment(assignment);
|
|
3911
5005
|
});
|
|
@@ -3913,17 +5007,40 @@ var ProjectRunner = class {
|
|
|
3913
5007
|
this.handleStopTask(data.taskId);
|
|
3914
5008
|
});
|
|
3915
5009
|
this.connection.onShutdown(() => {
|
|
3916
|
-
|
|
5010
|
+
logger6.info("Received shutdown signal from server");
|
|
3917
5011
|
void this.stop();
|
|
3918
5012
|
});
|
|
3919
5013
|
this.connection.onChatMessage((msg) => {
|
|
3920
|
-
|
|
5014
|
+
logger6.debug("Received project chat message");
|
|
3921
5015
|
void handleProjectChatMessage(msg, this.connection, this.projectDir);
|
|
3922
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
|
+
}
|
|
3923
5036
|
this.heartbeatTimer = setInterval(() => {
|
|
3924
5037
|
this.connection.sendHeartbeat();
|
|
3925
5038
|
}, HEARTBEAT_INTERVAL_MS2);
|
|
3926
|
-
|
|
5039
|
+
const currentBranch = this.getCurrentBranch();
|
|
5040
|
+
if (currentBranch) {
|
|
5041
|
+
this.commitWatcher.start(currentBranch);
|
|
5042
|
+
}
|
|
5043
|
+
logger6.info("Connected, waiting for task assignments");
|
|
3927
5044
|
await new Promise((resolve2) => {
|
|
3928
5045
|
this.resolveLifecycle = resolve2;
|
|
3929
5046
|
process.on("SIGTERM", () => void this.stop());
|
|
@@ -3934,11 +5051,11 @@ var ProjectRunner = class {
|
|
|
3934
5051
|
const { taskId, mode } = assignment;
|
|
3935
5052
|
const shortId = taskId.slice(0, 8);
|
|
3936
5053
|
if (this.activeAgents.has(taskId)) {
|
|
3937
|
-
|
|
5054
|
+
logger6.info("Task already running, skipping", { taskId: shortId });
|
|
3938
5055
|
return;
|
|
3939
5056
|
}
|
|
3940
5057
|
if (this.activeAgents.size >= MAX_CONCURRENT) {
|
|
3941
|
-
|
|
5058
|
+
logger6.warn("Max concurrent agents reached, rejecting task", {
|
|
3942
5059
|
maxConcurrent: MAX_CONCURRENT,
|
|
3943
5060
|
taskId: shortId
|
|
3944
5061
|
});
|
|
@@ -3947,9 +5064,9 @@ var ProjectRunner = class {
|
|
|
3947
5064
|
}
|
|
3948
5065
|
try {
|
|
3949
5066
|
try {
|
|
3950
|
-
|
|
5067
|
+
execSync6("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
|
|
3951
5068
|
} catch {
|
|
3952
|
-
|
|
5069
|
+
logger6.warn("Git fetch failed", { taskId: shortId });
|
|
3953
5070
|
}
|
|
3954
5071
|
const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
|
|
3955
5072
|
const child = spawnChildAgent(assignment, workDir);
|
|
@@ -3960,12 +5077,12 @@ var ProjectRunner = class {
|
|
|
3960
5077
|
usesWorktree
|
|
3961
5078
|
});
|
|
3962
5079
|
this.connection.emitTaskStarted(taskId);
|
|
3963
|
-
|
|
5080
|
+
logger6.info("Started task", { taskId: shortId, mode, workDir });
|
|
3964
5081
|
child.on("exit", (code) => {
|
|
3965
5082
|
this.activeAgents.delete(taskId);
|
|
3966
5083
|
const reason = code === 0 ? "completed" : `exited with code ${code}`;
|
|
3967
5084
|
this.connection.emitTaskStopped(taskId, reason);
|
|
3968
|
-
|
|
5085
|
+
logger6.info("Task exited", { taskId: shortId, reason });
|
|
3969
5086
|
if (code === 0 && usesWorktree) {
|
|
3970
5087
|
try {
|
|
3971
5088
|
removeWorktree(this.projectDir, taskId);
|
|
@@ -3974,7 +5091,7 @@ var ProjectRunner = class {
|
|
|
3974
5091
|
}
|
|
3975
5092
|
});
|
|
3976
5093
|
} catch (error) {
|
|
3977
|
-
|
|
5094
|
+
logger6.error("Failed to start task", {
|
|
3978
5095
|
taskId: shortId,
|
|
3979
5096
|
...errorMeta(error)
|
|
3980
5097
|
});
|
|
@@ -3988,7 +5105,7 @@ var ProjectRunner = class {
|
|
|
3988
5105
|
const agent = this.activeAgents.get(taskId);
|
|
3989
5106
|
if (!agent) return;
|
|
3990
5107
|
const shortId = taskId.slice(0, 8);
|
|
3991
|
-
|
|
5108
|
+
logger6.info("Stopping task", { taskId: shortId });
|
|
3992
5109
|
agent.process.kill("SIGTERM");
|
|
3993
5110
|
const timer = setTimeout(() => {
|
|
3994
5111
|
if (this.activeAgents.has(taskId)) {
|
|
@@ -4008,7 +5125,9 @@ var ProjectRunner = class {
|
|
|
4008
5125
|
async stop() {
|
|
4009
5126
|
if (this.stopping) return;
|
|
4010
5127
|
this.stopping = true;
|
|
4011
|
-
|
|
5128
|
+
logger6.info("Shutting down");
|
|
5129
|
+
this.commitWatcher.stop();
|
|
5130
|
+
await this.killStartCommand();
|
|
4012
5131
|
if (this.heartbeatTimer) {
|
|
4013
5132
|
clearInterval(this.heartbeatTimer);
|
|
4014
5133
|
this.heartbeatTimer = null;
|
|
@@ -4033,7 +5152,7 @@ var ProjectRunner = class {
|
|
|
4033
5152
|
})
|
|
4034
5153
|
]);
|
|
4035
5154
|
this.connection.disconnect();
|
|
4036
|
-
|
|
5155
|
+
logger6.info("Shutdown complete");
|
|
4037
5156
|
if (this.resolveLifecycle) {
|
|
4038
5157
|
this.resolveLifecycle();
|
|
4039
5158
|
this.resolveLifecycle = null;
|
|
@@ -4119,4 +5238,4 @@ export {
|
|
|
4119
5238
|
ProjectRunner,
|
|
4120
5239
|
FileCache
|
|
4121
5240
|
};
|
|
4122
|
-
//# sourceMappingURL=chunk-
|
|
5241
|
+
//# sourceMappingURL=chunk-MRTSBPY7.js.map
|