@rallycry/conveyor-agent 6.0.1 → 6.0.3
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-HYWZJYPW.js → chunk-SL5MRNSI.js} +1736 -148
- package/dist/chunk-SL5MRNSI.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +107 -2
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-HYWZJYPW.js.map +0 -1
|
@@ -220,16 +220,42 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
220
220
|
this.socket.on("agentRunner:runStartCommand", () => {
|
|
221
221
|
if (this.runStartCommandCallback) this.runStartCommandCallback();
|
|
222
222
|
});
|
|
223
|
+
this.socket.on("agentRunner:wake", () => {
|
|
224
|
+
if (this.chatMessageCallback) {
|
|
225
|
+
this.chatMessageCallback({ content: "", userId: "system" });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
this.socket.on("agentRunner:updateApiKey", (data) => {
|
|
229
|
+
process.env.ANTHROPIC_API_KEY = data.apiKey;
|
|
230
|
+
});
|
|
223
231
|
this.socket.on(
|
|
224
232
|
"agentRunner:runAuthTokenCommand",
|
|
225
233
|
(data, cb) => this.handleRunAuthTokenCommand(data.userEmail, cb)
|
|
226
234
|
);
|
|
227
235
|
this.socket.on("connect", () => {
|
|
236
|
+
process.stderr.write(`[conveyor] Socket connected
|
|
237
|
+
`);
|
|
228
238
|
if (!settled) {
|
|
229
239
|
settled = true;
|
|
230
240
|
resolve2();
|
|
231
241
|
}
|
|
232
242
|
});
|
|
243
|
+
this.socket.on("connect_error", (err) => {
|
|
244
|
+
process.stderr.write(`[conveyor] Socket connection error: ${err.message}
|
|
245
|
+
`);
|
|
246
|
+
});
|
|
247
|
+
this.socket.on("disconnect", (reason) => {
|
|
248
|
+
process.stderr.write(`[conveyor] Socket disconnected: ${reason}
|
|
249
|
+
`);
|
|
250
|
+
});
|
|
251
|
+
this.socket.io.on("reconnect", (attempt) => {
|
|
252
|
+
process.stderr.write(`[conveyor] Reconnected after ${attempt} attempt(s)
|
|
253
|
+
`);
|
|
254
|
+
});
|
|
255
|
+
this.socket.io.on("reconnect_error", (err) => {
|
|
256
|
+
process.stderr.write(`[conveyor] Reconnection error: ${err.message}
|
|
257
|
+
`);
|
|
258
|
+
});
|
|
233
259
|
this.socket.io.on("reconnect_attempt", () => {
|
|
234
260
|
attempts++;
|
|
235
261
|
if (!settled && attempts >= maxInitialAttempts) {
|
|
@@ -427,6 +453,23 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
427
453
|
triggerIdentification() {
|
|
428
454
|
return triggerIdentification(this.socket);
|
|
429
455
|
}
|
|
456
|
+
async refreshAuthToken() {
|
|
457
|
+
const codespaceName = process.env.CODESPACE_NAME;
|
|
458
|
+
const apiUrl = process.env.CONVEYOR_API_URL ?? this.config.conveyorApiUrl;
|
|
459
|
+
if (!codespaceName || !apiUrl) return false;
|
|
460
|
+
try {
|
|
461
|
+
const response = await fetch(`${apiUrl}/api/codespace/bootstrap/${codespaceName}`);
|
|
462
|
+
if (!response.ok) return false;
|
|
463
|
+
const config = await response.json();
|
|
464
|
+
if (config.envVars?.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
465
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = config.envVars.CLAUDE_CODE_OAUTH_TOKEN;
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
430
473
|
emitModeTransition(payload) {
|
|
431
474
|
if (!this.socket) return;
|
|
432
475
|
this.socket.emit("agentRunner:modeTransition", payload);
|
|
@@ -454,9 +497,16 @@ var ProjectConnection = class {
|
|
|
454
497
|
shutdownCallback = null;
|
|
455
498
|
chatMessageCallback = null;
|
|
456
499
|
earlyChatMessages = [];
|
|
500
|
+
auditRequestCallback = null;
|
|
501
|
+
// Branch switching callbacks
|
|
502
|
+
onSwitchBranch = null;
|
|
503
|
+
onSyncEnvironment = null;
|
|
504
|
+
onGetEnvStatus = null;
|
|
505
|
+
onRestartStartCommand = null;
|
|
457
506
|
constructor(config) {
|
|
458
507
|
this.config = config;
|
|
459
508
|
}
|
|
509
|
+
// oxlint-disable-next-line max-lines-per-function -- socket event registration requires co-located handlers
|
|
460
510
|
connect() {
|
|
461
511
|
return new Promise((resolve2, reject) => {
|
|
462
512
|
let settled = false;
|
|
@@ -496,12 +546,65 @@ var ProjectConnection = class {
|
|
|
496
546
|
this.earlyChatMessages.push(msg);
|
|
497
547
|
}
|
|
498
548
|
});
|
|
549
|
+
this.socket.on("projectRunner:auditTags", (data) => {
|
|
550
|
+
if (this.auditRequestCallback) {
|
|
551
|
+
this.auditRequestCallback(data);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
this.socket.on(
|
|
555
|
+
"projectRunner:switchBranch",
|
|
556
|
+
(data, cb) => {
|
|
557
|
+
if (this.onSwitchBranch) this.onSwitchBranch(data, cb);
|
|
558
|
+
else cb({ ok: false, error: "switchBranch handler not registered" });
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
this.socket.on("projectRunner:syncEnvironment", (cb) => {
|
|
562
|
+
if (this.onSyncEnvironment) this.onSyncEnvironment(cb);
|
|
563
|
+
else cb({ ok: false, error: "syncEnvironment handler not registered" });
|
|
564
|
+
});
|
|
565
|
+
this.socket.on("projectRunner:getEnvStatus", (cb) => {
|
|
566
|
+
if (this.onGetEnvStatus) this.onGetEnvStatus(cb);
|
|
567
|
+
else cb({ ok: false, data: void 0 });
|
|
568
|
+
});
|
|
569
|
+
this.socket.on(
|
|
570
|
+
"projectRunner:restartStartCommand",
|
|
571
|
+
(_data, cb) => {
|
|
572
|
+
if (this.onRestartStartCommand) this.onRestartStartCommand(cb);
|
|
573
|
+
else cb({ ok: false, error: "restartStartCommand handler not registered" });
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
this.socket.on(
|
|
577
|
+
"projectRunner:runAuthTokenCommand",
|
|
578
|
+
(data, cb) => this.handleRunAuthTokenCommand(data.userEmail, cb)
|
|
579
|
+
);
|
|
580
|
+
process.stderr.write(
|
|
581
|
+
`[conveyor] Connecting to ${this.config.apiUrl} (project: ${this.config.projectId})
|
|
582
|
+
`
|
|
583
|
+
);
|
|
499
584
|
this.socket.on("connect", () => {
|
|
585
|
+
process.stderr.write(`[conveyor] Project socket connected
|
|
586
|
+
`);
|
|
500
587
|
if (!settled) {
|
|
501
588
|
settled = true;
|
|
502
589
|
resolve2();
|
|
503
590
|
}
|
|
504
591
|
});
|
|
592
|
+
this.socket.on("connect_error", (err) => {
|
|
593
|
+
process.stderr.write(`[conveyor] Project socket connection error: ${err.message}
|
|
594
|
+
`);
|
|
595
|
+
});
|
|
596
|
+
this.socket.on("disconnect", (reason) => {
|
|
597
|
+
process.stderr.write(`[conveyor] Project socket disconnected: ${reason}
|
|
598
|
+
`);
|
|
599
|
+
});
|
|
600
|
+
this.socket.io.on("reconnect", (attempt) => {
|
|
601
|
+
process.stderr.write(`[conveyor] Project socket reconnected after ${attempt} attempt(s)
|
|
602
|
+
`);
|
|
603
|
+
});
|
|
604
|
+
this.socket.io.on("reconnect_error", (err) => {
|
|
605
|
+
process.stderr.write(`[conveyor] Project socket reconnection error: ${err.message}
|
|
606
|
+
`);
|
|
607
|
+
});
|
|
505
608
|
this.socket.io.on("reconnect_attempt", () => {
|
|
506
609
|
attempts++;
|
|
507
610
|
if (!settled && attempts >= maxInitialAttempts) {
|
|
@@ -527,6 +630,17 @@ var ProjectConnection = class {
|
|
|
527
630
|
}
|
|
528
631
|
this.earlyChatMessages = [];
|
|
529
632
|
}
|
|
633
|
+
onAuditRequest(callback) {
|
|
634
|
+
this.auditRequestCallback = callback;
|
|
635
|
+
}
|
|
636
|
+
emitAuditResult(data) {
|
|
637
|
+
if (!this.socket) return;
|
|
638
|
+
this.socket.emit("conveyor:tagAuditResult", data);
|
|
639
|
+
}
|
|
640
|
+
emitAuditProgress(data) {
|
|
641
|
+
if (!this.socket) return;
|
|
642
|
+
this.socket.emit("conveyor:tagAuditProgress", data);
|
|
643
|
+
}
|
|
530
644
|
sendHeartbeat() {
|
|
531
645
|
if (!this.socket) return;
|
|
532
646
|
this.socket.emit("projectRunner:heartbeat", {});
|
|
@@ -574,13 +688,13 @@ var ProjectConnection = class {
|
|
|
574
688
|
);
|
|
575
689
|
});
|
|
576
690
|
}
|
|
577
|
-
fetchChatHistory(limit) {
|
|
691
|
+
fetchChatHistory(limit, chatId) {
|
|
578
692
|
const socket = this.socket;
|
|
579
693
|
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
580
694
|
return new Promise((resolve2, reject) => {
|
|
581
695
|
socket.emit(
|
|
582
696
|
"projectRunner:getChatHistory",
|
|
583
|
-
{ limit },
|
|
697
|
+
{ limit, chatId },
|
|
584
698
|
(response) => {
|
|
585
699
|
if (response.success && response.data) resolve2(response.data);
|
|
586
700
|
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
@@ -588,6 +702,78 @@ var ProjectConnection = class {
|
|
|
588
702
|
);
|
|
589
703
|
});
|
|
590
704
|
}
|
|
705
|
+
// ── Project MCP tool request methods ──
|
|
706
|
+
requestListTasks(params) {
|
|
707
|
+
return this.requestWithCallback("projectRunner:listTasks", params);
|
|
708
|
+
}
|
|
709
|
+
requestGetTask(taskId) {
|
|
710
|
+
return this.requestWithCallback("projectRunner:getTask", { taskId });
|
|
711
|
+
}
|
|
712
|
+
requestCreateTask(params) {
|
|
713
|
+
return this.requestWithCallback("projectRunner:createTask", params);
|
|
714
|
+
}
|
|
715
|
+
requestUpdateTask(params) {
|
|
716
|
+
return this.requestWithCallback("projectRunner:updateTask", params);
|
|
717
|
+
}
|
|
718
|
+
requestSearchTasks(params) {
|
|
719
|
+
return this.requestWithCallback("projectRunner:searchTasks", params);
|
|
720
|
+
}
|
|
721
|
+
requestListTags() {
|
|
722
|
+
return this.requestWithCallback("projectRunner:listTags", {});
|
|
723
|
+
}
|
|
724
|
+
requestGetProjectSummary() {
|
|
725
|
+
return this.requestWithCallback("projectRunner:getProjectSummary", {});
|
|
726
|
+
}
|
|
727
|
+
requestWithCallback(event, data) {
|
|
728
|
+
const socket = this.socket;
|
|
729
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
730
|
+
return new Promise((resolve2, reject) => {
|
|
731
|
+
socket.emit(event, data, (response) => {
|
|
732
|
+
if (response.success) resolve2(response.data);
|
|
733
|
+
else reject(new Error(response.error ?? `${event} failed`));
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
emitNewCommitsDetected(data) {
|
|
738
|
+
if (!this.socket) return;
|
|
739
|
+
this.socket.emit("projectRunner:newCommitsDetected", data);
|
|
740
|
+
}
|
|
741
|
+
emitEnvironmentReady(data) {
|
|
742
|
+
if (!this.socket) return;
|
|
743
|
+
this.socket.emit("projectRunner:environmentReady", data);
|
|
744
|
+
}
|
|
745
|
+
emitEnvSwitchProgress(data) {
|
|
746
|
+
if (!this.socket) return;
|
|
747
|
+
this.socket.emit("projectRunner:envSwitchProgress", data);
|
|
748
|
+
}
|
|
749
|
+
handleRunAuthTokenCommand(userEmail, cb) {
|
|
750
|
+
try {
|
|
751
|
+
if (process.env.CODESPACES !== "true") {
|
|
752
|
+
cb({ ok: false, error: "Auth token command only available in codespace environments" });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const authCmd = process.env.CONVEYOR_AUTH_TOKEN_COMMAND;
|
|
756
|
+
if (!authCmd) {
|
|
757
|
+
cb({ ok: false, error: "CONVEYOR_AUTH_TOKEN_COMMAND not configured" });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const cwd = this.config.projectDir ?? process.cwd();
|
|
761
|
+
const token = runAuthTokenCommand(authCmd, userEmail, cwd);
|
|
762
|
+
if (!token) {
|
|
763
|
+
cb({
|
|
764
|
+
ok: false,
|
|
765
|
+
error: `Auth token command returned empty output. Command: ${authCmd}`
|
|
766
|
+
});
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
cb({ ok: true, token });
|
|
770
|
+
} catch (error) {
|
|
771
|
+
cb({
|
|
772
|
+
ok: false,
|
|
773
|
+
error: error instanceof Error ? error.message : "Auth token command failed"
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
591
777
|
disconnect() {
|
|
592
778
|
this.socket?.disconnect();
|
|
593
779
|
this.socket = null;
|
|
@@ -622,9 +808,32 @@ function errorMeta(error) {
|
|
|
622
808
|
}
|
|
623
809
|
|
|
624
810
|
// src/runner/worktree.ts
|
|
625
|
-
import { execSync as
|
|
811
|
+
import { execSync as execSync3 } from "child_process";
|
|
626
812
|
import { existsSync } from "fs";
|
|
627
813
|
import { join } from "path";
|
|
814
|
+
|
|
815
|
+
// src/runner/git-utils.ts
|
|
816
|
+
import { execSync as execSync2 } from "child_process";
|
|
817
|
+
function hasUncommittedChanges(cwd) {
|
|
818
|
+
const status = execSync2("git status --porcelain", {
|
|
819
|
+
cwd,
|
|
820
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
821
|
+
}).toString().trim();
|
|
822
|
+
return status.length > 0;
|
|
823
|
+
}
|
|
824
|
+
function getCurrentBranch(cwd) {
|
|
825
|
+
try {
|
|
826
|
+
const branch = execSync2("git branch --show-current", {
|
|
827
|
+
cwd,
|
|
828
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
829
|
+
}).toString().trim();
|
|
830
|
+
return branch || null;
|
|
831
|
+
} catch {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/runner/worktree.ts
|
|
628
837
|
var WORKTREE_DIR = ".worktrees";
|
|
629
838
|
function ensureWorktree(projectDir, taskId, branch) {
|
|
630
839
|
if (projectDir.includes(`/${WORKTREE_DIR}/`)) {
|
|
@@ -633,8 +842,11 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
633
842
|
const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
|
|
634
843
|
if (existsSync(worktreePath)) {
|
|
635
844
|
if (branch) {
|
|
845
|
+
if (hasUncommittedChanges(worktreePath)) {
|
|
846
|
+
return worktreePath;
|
|
847
|
+
}
|
|
636
848
|
try {
|
|
637
|
-
|
|
849
|
+
execSync3(`git checkout --detach origin/${branch}`, {
|
|
638
850
|
cwd: worktreePath,
|
|
639
851
|
stdio: "ignore"
|
|
640
852
|
});
|
|
@@ -644,17 +856,39 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
644
856
|
return worktreePath;
|
|
645
857
|
}
|
|
646
858
|
const ref = branch ? `origin/${branch}` : "HEAD";
|
|
647
|
-
|
|
859
|
+
execSync3(`git worktree add --detach "${worktreePath}" ${ref}`, {
|
|
648
860
|
cwd: projectDir,
|
|
649
861
|
stdio: "ignore"
|
|
650
862
|
});
|
|
651
863
|
return worktreePath;
|
|
652
864
|
}
|
|
865
|
+
function detachWorktreeBranch(projectDir, branch) {
|
|
866
|
+
try {
|
|
867
|
+
const output = execSync3("git worktree list --porcelain", {
|
|
868
|
+
cwd: projectDir,
|
|
869
|
+
encoding: "utf-8"
|
|
870
|
+
});
|
|
871
|
+
const entries = output.split("\n\n");
|
|
872
|
+
for (const entry of entries) {
|
|
873
|
+
const lines = entry.trim().split("\n");
|
|
874
|
+
const worktreeLine = lines.find((l) => l.startsWith("worktree "));
|
|
875
|
+
const branchLine = lines.find((l) => l.startsWith("branch "));
|
|
876
|
+
if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;
|
|
877
|
+
const worktreePath = worktreeLine.replace("worktree ", "");
|
|
878
|
+
if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;
|
|
879
|
+
try {
|
|
880
|
+
execSync3("git checkout --detach", { cwd: worktreePath, stdio: "ignore" });
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
}
|
|
653
887
|
function removeWorktree(projectDir, taskId) {
|
|
654
888
|
const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
|
|
655
889
|
if (!existsSync(worktreePath)) return;
|
|
656
890
|
try {
|
|
657
|
-
|
|
891
|
+
execSync3(`git worktree remove "${worktreePath}" --force`, {
|
|
658
892
|
cwd: projectDir,
|
|
659
893
|
stdio: "ignore"
|
|
660
894
|
});
|
|
@@ -690,10 +924,10 @@ function loadConveyorConfig(_workspaceDir) {
|
|
|
690
924
|
}
|
|
691
925
|
|
|
692
926
|
// src/setup/codespace.ts
|
|
693
|
-
import { execSync as
|
|
927
|
+
import { execSync as execSync4 } from "child_process";
|
|
694
928
|
function unshallowRepo(workspaceDir) {
|
|
695
929
|
try {
|
|
696
|
-
|
|
930
|
+
execSync4("git fetch --unshallow", {
|
|
697
931
|
cwd: workspaceDir,
|
|
698
932
|
stdio: "ignore",
|
|
699
933
|
timeout: 6e4
|
|
@@ -704,9 +938,245 @@ function unshallowRepo(workspaceDir) {
|
|
|
704
938
|
|
|
705
939
|
// src/runner/agent-runner.ts
|
|
706
940
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
707
|
-
import { execSync as
|
|
941
|
+
import { execSync as execSync5 } from "child_process";
|
|
942
|
+
|
|
943
|
+
// src/connection/tunnel-client.ts
|
|
944
|
+
import { request as httpRequest } from "http";
|
|
945
|
+
var logger2 = createServiceLogger("TunnelClient");
|
|
946
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
947
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
948
|
+
var TunnelClient = class {
|
|
949
|
+
apiUrl;
|
|
950
|
+
token;
|
|
951
|
+
localPort;
|
|
952
|
+
ws = null;
|
|
953
|
+
stopped = false;
|
|
954
|
+
reconnectAttempts = 0;
|
|
955
|
+
reconnectTimer = null;
|
|
956
|
+
constructor(apiUrl, token, localPort) {
|
|
957
|
+
this.apiUrl = apiUrl;
|
|
958
|
+
this.token = token;
|
|
959
|
+
this.localPort = localPort;
|
|
960
|
+
}
|
|
961
|
+
connect() {
|
|
962
|
+
if (this.stopped) return;
|
|
963
|
+
const wsUrl = this.apiUrl.replace(/^http/, "ws").replace(/\/$/, "");
|
|
964
|
+
const url = `${wsUrl}/api/tunnel?token=${encodeURIComponent(this.token)}`;
|
|
965
|
+
try {
|
|
966
|
+
this.ws = new WebSocket(url);
|
|
967
|
+
} catch (err) {
|
|
968
|
+
logger2.warn("Failed to create tunnel WebSocket", { error: String(err) });
|
|
969
|
+
this.scheduleReconnect();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this.ws.binaryType = "arraybuffer";
|
|
973
|
+
this.ws.addEventListener("open", () => {
|
|
974
|
+
this.reconnectAttempts = 0;
|
|
975
|
+
logger2.info("Tunnel connected", { port: this.localPort });
|
|
976
|
+
});
|
|
977
|
+
this.ws.addEventListener("close", () => {
|
|
978
|
+
logger2.info("Tunnel disconnected");
|
|
979
|
+
this.scheduleReconnect();
|
|
980
|
+
});
|
|
981
|
+
this.ws.addEventListener("error", (event) => {
|
|
982
|
+
logger2.warn("Tunnel error", { error: String(event) });
|
|
983
|
+
});
|
|
984
|
+
this.ws.addEventListener("message", (event) => {
|
|
985
|
+
this.handleMessage(event.data);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
disconnect() {
|
|
989
|
+
this.stopped = true;
|
|
990
|
+
if (this.reconnectTimer) {
|
|
991
|
+
clearTimeout(this.reconnectTimer);
|
|
992
|
+
this.reconnectTimer = null;
|
|
993
|
+
}
|
|
994
|
+
if (this.ws) {
|
|
995
|
+
this.ws.close(1e3, "shutdown");
|
|
996
|
+
this.ws = null;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
scheduleReconnect() {
|
|
1000
|
+
if (this.stopped) return;
|
|
1001
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempts, RECONNECT_MAX_MS);
|
|
1002
|
+
this.reconnectAttempts++;
|
|
1003
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
1004
|
+
}
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
// Message handling
|
|
1007
|
+
// ---------------------------------------------------------------------------
|
|
1008
|
+
handleMessage(data) {
|
|
1009
|
+
if (typeof data === "string") {
|
|
1010
|
+
this.handleJsonMessage(data);
|
|
1011
|
+
} else {
|
|
1012
|
+
this.handleBinaryFrame(data);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
handleJsonMessage(raw) {
|
|
1016
|
+
let msg;
|
|
1017
|
+
try {
|
|
1018
|
+
msg = JSON.parse(raw);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
switch (msg.type) {
|
|
1023
|
+
case "http-req":
|
|
1024
|
+
this.handleHttpRequest(msg.id, msg.method ?? "GET", msg.path ?? "/", msg.headers ?? {});
|
|
1025
|
+
break;
|
|
1026
|
+
case "http-req-end":
|
|
1027
|
+
this.endHttpRequest(msg.id);
|
|
1028
|
+
break;
|
|
1029
|
+
case "ws-upgrade":
|
|
1030
|
+
this.handleWsUpgrade(msg.id, msg.path ?? "/", msg.headers ?? {});
|
|
1031
|
+
break;
|
|
1032
|
+
case "ws-data":
|
|
1033
|
+
this.relayWsData(msg.id, msg.data ?? "");
|
|
1034
|
+
break;
|
|
1035
|
+
case "ws-close":
|
|
1036
|
+
this.closeWs(msg.id);
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
handleBinaryFrame(data) {
|
|
1041
|
+
const buf = new Uint8Array(data);
|
|
1042
|
+
if (buf.length <= 4) return;
|
|
1043
|
+
const id = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
|
|
1044
|
+
const body = buf.subarray(4);
|
|
1045
|
+
const pending = this.pendingHttpRequests.get(id);
|
|
1046
|
+
if (pending) pending.chunks.push(Buffer.from(body));
|
|
1047
|
+
}
|
|
1048
|
+
// ---------------------------------------------------------------------------
|
|
1049
|
+
// HTTP request proxying
|
|
1050
|
+
// ---------------------------------------------------------------------------
|
|
1051
|
+
pendingHttpRequests = /* @__PURE__ */ new Map();
|
|
1052
|
+
handleHttpRequest(id, method, path2, headers) {
|
|
1053
|
+
const state = { chunks: [], ended: false };
|
|
1054
|
+
this.pendingHttpRequests.set(id, state);
|
|
1055
|
+
const req = httpRequest(
|
|
1056
|
+
{
|
|
1057
|
+
hostname: "127.0.0.1",
|
|
1058
|
+
port: this.localPort,
|
|
1059
|
+
path: path2,
|
|
1060
|
+
method,
|
|
1061
|
+
headers: { ...headers, host: `127.0.0.1:${this.localPort}` }
|
|
1062
|
+
},
|
|
1063
|
+
(res) => {
|
|
1064
|
+
const resHeaders = {};
|
|
1065
|
+
for (const [key, val] of Object.entries(res.headers)) {
|
|
1066
|
+
if (typeof val === "string") resHeaders[key] = val;
|
|
1067
|
+
}
|
|
1068
|
+
this.send(
|
|
1069
|
+
JSON.stringify({ type: "http-res", id, statusCode: res.statusCode, headers: resHeaders })
|
|
1070
|
+
);
|
|
1071
|
+
res.on("data", (chunk) => {
|
|
1072
|
+
const frame = Buffer.alloc(4 + chunk.length);
|
|
1073
|
+
frame.writeUInt32BE(id, 0);
|
|
1074
|
+
chunk.copy(frame, 4);
|
|
1075
|
+
this.sendBinary(frame);
|
|
1076
|
+
});
|
|
1077
|
+
res.on("end", () => {
|
|
1078
|
+
this.send(JSON.stringify({ type: "http-end", id }));
|
|
1079
|
+
this.pendingHttpRequests.delete(id);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
req.on("error", (err) => {
|
|
1084
|
+
logger2.warn("Local HTTP request failed", { id, error: err.message });
|
|
1085
|
+
this.send(JSON.stringify({ type: "http-res", id, statusCode: 502, headers: {} }));
|
|
1086
|
+
this.send(JSON.stringify({ type: "http-end", id }));
|
|
1087
|
+
this.pendingHttpRequests.delete(id);
|
|
1088
|
+
});
|
|
1089
|
+
for (const chunk of state.chunks) {
|
|
1090
|
+
req.write(chunk);
|
|
1091
|
+
}
|
|
1092
|
+
state.chunks = [];
|
|
1093
|
+
if (state.ended) {
|
|
1094
|
+
req.end();
|
|
1095
|
+
} else {
|
|
1096
|
+
state.req = req;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
endHttpRequest(id) {
|
|
1100
|
+
const state = this.pendingHttpRequests.get(id);
|
|
1101
|
+
if (!state) return;
|
|
1102
|
+
state.ended = true;
|
|
1103
|
+
const req = state.req;
|
|
1104
|
+
if (req) {
|
|
1105
|
+
for (const chunk of state.chunks) {
|
|
1106
|
+
req.write(chunk);
|
|
1107
|
+
}
|
|
1108
|
+
req.end();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
// WebSocket proxying
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
localWebSockets = /* @__PURE__ */ new Map();
|
|
1115
|
+
handleWsUpgrade(id, path2, _headers) {
|
|
1116
|
+
const url = `ws://127.0.0.1:${this.localPort}${path2}`;
|
|
1117
|
+
try {
|
|
1118
|
+
const localWs = new WebSocket(url);
|
|
1119
|
+
localWs.addEventListener("open", () => {
|
|
1120
|
+
this.localWebSockets.set(id, localWs);
|
|
1121
|
+
this.send(JSON.stringify({ type: "ws-open", id }));
|
|
1122
|
+
});
|
|
1123
|
+
localWs.addEventListener("message", (event) => {
|
|
1124
|
+
const data = typeof event.data === "string" ? event.data : String(event.data);
|
|
1125
|
+
this.send(JSON.stringify({ type: "ws-data", id, data }));
|
|
1126
|
+
});
|
|
1127
|
+
localWs.addEventListener("close", () => {
|
|
1128
|
+
this.localWebSockets.delete(id);
|
|
1129
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1130
|
+
});
|
|
1131
|
+
localWs.addEventListener("error", () => {
|
|
1132
|
+
this.localWebSockets.delete(id);
|
|
1133
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1134
|
+
});
|
|
1135
|
+
} catch {
|
|
1136
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
relayWsData(id, data) {
|
|
1140
|
+
const localWs = this.localWebSockets.get(id);
|
|
1141
|
+
if (localWs && localWs.readyState === WebSocket.OPEN) {
|
|
1142
|
+
localWs.send(data);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
closeWs(id) {
|
|
1146
|
+
const localWs = this.localWebSockets.get(id);
|
|
1147
|
+
if (localWs) {
|
|
1148
|
+
localWs.close();
|
|
1149
|
+
this.localWebSockets.delete(id);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// ---------------------------------------------------------------------------
|
|
1153
|
+
// Send helpers
|
|
1154
|
+
// ---------------------------------------------------------------------------
|
|
1155
|
+
send(data) {
|
|
1156
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1157
|
+
this.ws.send(data);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
sendBinary(data) {
|
|
1161
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1162
|
+
const copy = new ArrayBuffer(data.byteLength);
|
|
1163
|
+
new Uint8Array(copy).set(
|
|
1164
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
1165
|
+
);
|
|
1166
|
+
this.ws.send(copy);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
708
1170
|
|
|
709
1171
|
// src/execution/event-handlers.ts
|
|
1172
|
+
function safeVoid(promise, context) {
|
|
1173
|
+
if (promise && typeof promise.catch === "function") {
|
|
1174
|
+
promise.catch((err) => {
|
|
1175
|
+
process.stderr.write(`[safeVoid] ${context}: ${err}
|
|
1176
|
+
`);
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
710
1180
|
function epochSecondsToISO(value) {
|
|
711
1181
|
if (typeof value === "string") return value;
|
|
712
1182
|
if (typeof value !== "number" || value <= 0) return void 0;
|
|
@@ -745,6 +1215,10 @@ async function processAssistantEvent(event, host, turnToolCalls) {
|
|
|
745
1215
|
}
|
|
746
1216
|
var API_ERROR_PATTERN = /API Error: [45]\d\d/;
|
|
747
1217
|
var IMAGE_ERROR_PATTERN = /Could not process image/i;
|
|
1218
|
+
var AUTH_ERROR_PATTERN = /Not logged in|Please run \/login|authentication failed|invalid.*token|unauthorized/i;
|
|
1219
|
+
function isAuthError(msg) {
|
|
1220
|
+
return AUTH_ERROR_PATTERN.test(msg);
|
|
1221
|
+
}
|
|
748
1222
|
function isRetriableMessage(msg) {
|
|
749
1223
|
if (IMAGE_ERROR_PATTERN.test(msg)) return true;
|
|
750
1224
|
if (API_ERROR_PATTERN.test(msg)) return true;
|
|
@@ -825,6 +1299,10 @@ function handleErrorResult(event, host) {
|
|
|
825
1299
|
if (isStaleSession) {
|
|
826
1300
|
return { retriable: false, staleSession: true };
|
|
827
1301
|
}
|
|
1302
|
+
if (isAuthError(errorMsg)) {
|
|
1303
|
+
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
1304
|
+
return { retriable: false, authError: true };
|
|
1305
|
+
}
|
|
828
1306
|
const retriable = isRetriableMessage(errorMsg);
|
|
829
1307
|
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
830
1308
|
return { retriable };
|
|
@@ -864,7 +1342,8 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
|
|
|
864
1342
|
return {
|
|
865
1343
|
retriable: result.retriable,
|
|
866
1344
|
resultSummary: result.resultSummary,
|
|
867
|
-
staleSession: result.staleSession
|
|
1345
|
+
staleSession: result.staleSession,
|
|
1346
|
+
authError: result.authError
|
|
868
1347
|
};
|
|
869
1348
|
}
|
|
870
1349
|
function handleRateLimitEvent(event, host) {
|
|
@@ -883,13 +1362,13 @@ function handleRateLimitEvent(event, host) {
|
|
|
883
1362
|
const resetsAtDisplay = resetsAt ?? "unknown";
|
|
884
1363
|
const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAtDisplay})`;
|
|
885
1364
|
host.connection.sendEvent({ type: "error", message });
|
|
886
|
-
|
|
1365
|
+
safeVoid(host.callbacks.onEvent({ type: "error", message }), "rateLimitRejected");
|
|
887
1366
|
return resetsAt;
|
|
888
1367
|
} else if (status === "allowed_warning") {
|
|
889
1368
|
const utilization = rate_limit_info.utilization ? `${Math.round(rate_limit_info.utilization * 100)}%` : "high";
|
|
890
1369
|
const message = `Rate limit warning: ${utilization} utilization (type: ${rate_limit_info.rateLimitType ?? "unknown"})`;
|
|
891
1370
|
host.connection.sendEvent({ type: "thinking", message });
|
|
892
|
-
|
|
1371
|
+
safeVoid(host.callbacks.onEvent({ type: "thinking", message }), "rateLimitWarning");
|
|
893
1372
|
}
|
|
894
1373
|
return void 0;
|
|
895
1374
|
}
|
|
@@ -907,34 +1386,46 @@ async function handleSystemEvent(event, host, context, sessionIdStored) {
|
|
|
907
1386
|
}
|
|
908
1387
|
function handleSystemSubevents(systemEvent, host) {
|
|
909
1388
|
if (systemEvent.subtype === "compact_boundary") {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1389
|
+
safeVoid(
|
|
1390
|
+
host.callbacks.onEvent({
|
|
1391
|
+
type: "context_compacted",
|
|
1392
|
+
trigger: systemEvent.compact_metadata.trigger,
|
|
1393
|
+
preTokens: systemEvent.compact_metadata.pre_tokens
|
|
1394
|
+
}),
|
|
1395
|
+
"compactBoundary"
|
|
1396
|
+
);
|
|
915
1397
|
} else if (systemEvent.subtype === "task_started") {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1398
|
+
safeVoid(
|
|
1399
|
+
host.callbacks.onEvent({
|
|
1400
|
+
type: "subagent_started",
|
|
1401
|
+
sdkTaskId: systemEvent.task_id,
|
|
1402
|
+
description: systemEvent.description
|
|
1403
|
+
}),
|
|
1404
|
+
"taskStarted"
|
|
1405
|
+
);
|
|
921
1406
|
} else if (systemEvent.subtype === "task_progress") {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1407
|
+
safeVoid(
|
|
1408
|
+
host.callbacks.onEvent({
|
|
1409
|
+
type: "subagent_progress",
|
|
1410
|
+
sdkTaskId: systemEvent.task_id,
|
|
1411
|
+
description: systemEvent.description,
|
|
1412
|
+
toolUses: systemEvent.usage?.tool_uses ?? 0,
|
|
1413
|
+
durationMs: systemEvent.usage?.duration_ms ?? 0
|
|
1414
|
+
}),
|
|
1415
|
+
"taskProgress"
|
|
1416
|
+
);
|
|
929
1417
|
}
|
|
930
1418
|
}
|
|
931
1419
|
function handleToolProgressEvent(event, host) {
|
|
932
1420
|
const msg = event;
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1421
|
+
safeVoid(
|
|
1422
|
+
host.callbacks.onEvent({
|
|
1423
|
+
type: "tool_progress",
|
|
1424
|
+
toolName: msg.tool_name ?? "",
|
|
1425
|
+
elapsedSeconds: msg.elapsed_time_seconds ?? 0
|
|
1426
|
+
}),
|
|
1427
|
+
"toolProgress"
|
|
1428
|
+
);
|
|
938
1429
|
}
|
|
939
1430
|
async function handleAssistantCase(event, host, turnToolCalls) {
|
|
940
1431
|
await processAssistantEvent(event, host, turnToolCalls);
|
|
@@ -952,11 +1443,13 @@ async function handleResultCase(event, host, context, startTime, isTyping, lastA
|
|
|
952
1443
|
retriable: resultInfo.retriable,
|
|
953
1444
|
resultSummary: resultInfo.resultSummary,
|
|
954
1445
|
staleSession: resultInfo.staleSession,
|
|
1446
|
+
authError: resultInfo.authError,
|
|
955
1447
|
stoppedTyping
|
|
956
1448
|
};
|
|
957
1449
|
}
|
|
958
1450
|
|
|
959
1451
|
// src/execution/event-processor.ts
|
|
1452
|
+
var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
|
|
960
1453
|
function stopTypingIfNeeded(host, isTyping) {
|
|
961
1454
|
if (isTyping) host.connection.sendTypingStop();
|
|
962
1455
|
}
|
|
@@ -978,6 +1471,12 @@ async function processAssistantCase(event, host, state) {
|
|
|
978
1471
|
}
|
|
979
1472
|
const usage = await handleAssistantCase(event, host, state.turnToolCalls);
|
|
980
1473
|
if (usage) state.lastAssistantUsage = usage;
|
|
1474
|
+
if (!state.sawApiError) {
|
|
1475
|
+
const fullText = event.message.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
1476
|
+
if (API_ERROR_PATTERN2.test(fullText)) {
|
|
1477
|
+
state.sawApiError = true;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
981
1480
|
}
|
|
982
1481
|
async function processResultCase(event, host, context, startTime, state) {
|
|
983
1482
|
const info = await handleResultCase(
|
|
@@ -992,6 +1491,7 @@ async function processResultCase(event, host, context, startTime, state) {
|
|
|
992
1491
|
state.retriable = info.retriable;
|
|
993
1492
|
state.resultSummary = info.resultSummary;
|
|
994
1493
|
if (info.staleSession) state.staleSession = true;
|
|
1494
|
+
if (info.authError) state.authError = true;
|
|
995
1495
|
}
|
|
996
1496
|
async function processEvents(events, context, host) {
|
|
997
1497
|
const startTime = Date.now();
|
|
@@ -1001,9 +1501,11 @@ async function processEvents(events, context, host) {
|
|
|
1001
1501
|
sessionIdStored: false,
|
|
1002
1502
|
isTyping: false,
|
|
1003
1503
|
retriable: false,
|
|
1504
|
+
sawApiError: false,
|
|
1004
1505
|
resultSummary: void 0,
|
|
1005
1506
|
rateLimitResetsAt: void 0,
|
|
1006
1507
|
staleSession: void 0,
|
|
1508
|
+
authError: void 0,
|
|
1007
1509
|
lastAssistantUsage: void 0,
|
|
1008
1510
|
turnToolCalls: []
|
|
1009
1511
|
};
|
|
@@ -1040,10 +1542,11 @@ async function processEvents(events, context, host) {
|
|
|
1040
1542
|
}
|
|
1041
1543
|
stopTypingIfNeeded(host, state.isTyping);
|
|
1042
1544
|
return {
|
|
1043
|
-
retriable: state.retriable,
|
|
1545
|
+
retriable: state.retriable || state.sawApiError,
|
|
1044
1546
|
resultSummary: state.resultSummary,
|
|
1045
1547
|
rateLimitResetsAt: state.rateLimitResetsAt,
|
|
1046
|
-
...state.staleSession && { staleSession: state.staleSession }
|
|
1548
|
+
...state.staleSession && { staleSession: state.staleSession },
|
|
1549
|
+
...state.authError && { authError: state.authError }
|
|
1047
1550
|
};
|
|
1048
1551
|
}
|
|
1049
1552
|
|
|
@@ -1401,6 +1904,8 @@ function buildDiscoveryPrompt(context) {
|
|
|
1401
1904
|
`You are in Discovery mode \u2014 helping plan and scope this task.`,
|
|
1402
1905
|
`- You have read-only codebase access (can read files, run git commands, search code)`,
|
|
1403
1906
|
`- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
|
|
1907
|
+
`- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
|
|
1908
|
+
`- If you identify code changes needed, describe them in the plan instead of implementing them`,
|
|
1404
1909
|
`- You can create and manage subtasks`,
|
|
1405
1910
|
`- Goal: collaborate with the user to create a clear plan`,
|
|
1406
1911
|
`- Proactively fill task properties (SP, tags, icon) as the plan takes shape`,
|
|
@@ -1535,6 +2040,14 @@ Project Agents:`);
|
|
|
1535
2040
|
parts.push(formatProjectAgentLine(pa));
|
|
1536
2041
|
}
|
|
1537
2042
|
}
|
|
2043
|
+
if (context.projectObjectives && context.projectObjectives.length > 0) {
|
|
2044
|
+
parts.push(`
|
|
2045
|
+
Project Objectives:`);
|
|
2046
|
+
for (const obj of context.projectObjectives) {
|
|
2047
|
+
const dates = `${obj.startDate.split("T")[0]} to ${obj.endDate.split("T")[0]}`;
|
|
2048
|
+
parts.push(`- **${obj.name}** (${dates})${obj.description ? ": " + obj.description : ""}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1538
2051
|
return parts;
|
|
1539
2052
|
}
|
|
1540
2053
|
function buildActivePreamble(context, workspaceDir) {
|
|
@@ -1657,7 +2170,7 @@ function detectRelaunchScenario(context, trustChatHistory = false) {
|
|
|
1657
2170
|
const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
|
|
1658
2171
|
return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
|
|
1659
2172
|
}
|
|
1660
|
-
function buildRelaunchWithSession(mode, context, agentMode) {
|
|
2173
|
+
function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
|
|
1661
2174
|
const scenario = detectRelaunchScenario(context);
|
|
1662
2175
|
if (!context.claudeSessionId || scenario === "fresh") return null;
|
|
1663
2176
|
const parts = [];
|
|
@@ -1705,7 +2218,7 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
|
|
|
1705
2218
|
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
1706
2219
|
`Review the current state of the codebase and verify everything is working correctly.`
|
|
1707
2220
|
);
|
|
1708
|
-
if (agentMode === "auto" || agentMode === "building") {
|
|
2221
|
+
if (agentMode === "auto" || agentMode === "building" && isAuto) {
|
|
1709
2222
|
parts.push(
|
|
1710
2223
|
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
1711
2224
|
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
@@ -1911,7 +2424,7 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
|
|
|
1911
2424
|
}
|
|
1912
2425
|
return parts;
|
|
1913
2426
|
}
|
|
1914
|
-
function buildInstructions(mode, context, scenario, agentMode) {
|
|
2427
|
+
function buildInstructions(mode, context, scenario, agentMode, isAuto) {
|
|
1915
2428
|
const parts = [`
|
|
1916
2429
|
## Instructions`];
|
|
1917
2430
|
const isPm = mode === "pm";
|
|
@@ -1943,7 +2456,7 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1943
2456
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
1944
2457
|
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`
|
|
1945
2458
|
);
|
|
1946
|
-
if (agentMode === "auto" || agentMode === "building") {
|
|
2459
|
+
if (agentMode === "auto" || agentMode === "building" && isAuto) {
|
|
1947
2460
|
parts.push(
|
|
1948
2461
|
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
1949
2462
|
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
@@ -1966,13 +2479,13 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1966
2479
|
async function buildInitialPrompt(mode, context, isAuto, agentMode) {
|
|
1967
2480
|
const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
|
|
1968
2481
|
if (!isPackRunner) {
|
|
1969
|
-
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode);
|
|
2482
|
+
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
|
|
1970
2483
|
if (sessionRelaunch) return sessionRelaunch;
|
|
1971
2484
|
}
|
|
1972
2485
|
const isPm = mode === "pm";
|
|
1973
2486
|
const scenario = detectRelaunchScenario(context, isPm);
|
|
1974
2487
|
const body = await buildTaskBody(context);
|
|
1975
|
-
const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
|
|
2488
|
+
const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode, isAuto);
|
|
1976
2489
|
return [...body, ...instructions].join("\n");
|
|
1977
2490
|
}
|
|
1978
2491
|
|
|
@@ -2234,11 +2747,14 @@ function buildCreatePullRequestTool(connection) {
|
|
|
2234
2747
|
"Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
|
|
2235
2748
|
{
|
|
2236
2749
|
title: z.string().describe("The PR title"),
|
|
2237
|
-
body: z.string().describe("The PR description/body in markdown")
|
|
2750
|
+
body: z.string().describe("The PR description/body in markdown"),
|
|
2751
|
+
branch: z.string().optional().describe(
|
|
2752
|
+
"The head branch name for the PR. If the task doesn't have a branch set, this will be used. Defaults to the task's existing branch."
|
|
2753
|
+
)
|
|
2238
2754
|
},
|
|
2239
|
-
async ({ title, body }) => {
|
|
2755
|
+
async ({ title, body, branch }) => {
|
|
2240
2756
|
try {
|
|
2241
|
-
const result = await connection.createPR({ title, body });
|
|
2757
|
+
const result = await connection.createPR({ title, body, branch });
|
|
2242
2758
|
connection.sendEvent({
|
|
2243
2759
|
type: "pr_created",
|
|
2244
2760
|
url: result.url,
|
|
@@ -2726,7 +3242,9 @@ async function handleAskUserQuestion(host, input) {
|
|
|
2726
3242
|
}
|
|
2727
3243
|
return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
|
|
2728
3244
|
}
|
|
3245
|
+
var DENIAL_WARNING_THRESHOLD = 3;
|
|
2729
3246
|
function buildCanUseTool(host) {
|
|
3247
|
+
let consecutiveDenials = 0;
|
|
2730
3248
|
return async (toolName, input) => {
|
|
2731
3249
|
if (toolName === "ExitPlanMode" && (host.agentMode === "auto" || host.agentMode === "discovery") && !host.hasExitedPlanMode) {
|
|
2732
3250
|
return await handleExitPlanMode(host, input);
|
|
@@ -2734,24 +3252,40 @@ function buildCanUseTool(host) {
|
|
|
2734
3252
|
if (toolName === "AskUserQuestion") {
|
|
2735
3253
|
return await handleAskUserQuestion(host, input);
|
|
2736
3254
|
}
|
|
3255
|
+
let result;
|
|
2737
3256
|
switch (host.agentMode) {
|
|
2738
3257
|
case "discovery":
|
|
2739
|
-
|
|
3258
|
+
result = handleDiscoveryToolAccess(toolName, input);
|
|
3259
|
+
break;
|
|
2740
3260
|
case "building":
|
|
2741
|
-
|
|
3261
|
+
result = handleBuildingToolAccess(toolName, input);
|
|
3262
|
+
break;
|
|
2742
3263
|
case "review":
|
|
2743
|
-
|
|
3264
|
+
result = handleReviewToolAccess(toolName, input, host.isParentTask);
|
|
3265
|
+
break;
|
|
2744
3266
|
case "auto":
|
|
2745
|
-
|
|
3267
|
+
result = handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
|
|
3268
|
+
break;
|
|
2746
3269
|
default:
|
|
2747
|
-
|
|
3270
|
+
result = { behavior: "allow", updatedInput: input };
|
|
3271
|
+
}
|
|
3272
|
+
if (result.behavior === "deny") {
|
|
3273
|
+
consecutiveDenials++;
|
|
3274
|
+
if (consecutiveDenials === DENIAL_WARNING_THRESHOLD) {
|
|
3275
|
+
host.connection.postChatMessage(
|
|
3276
|
+
`\u26A0\uFE0F Multiple tool denials detected. You are in ${host.agentMode} mode \u2014 file writes outside .claude/plans/ are not permitted. Focus on creating a plan instead of implementing code changes.`
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
} else {
|
|
3280
|
+
consecutiveDenials = 0;
|
|
2748
3281
|
}
|
|
3282
|
+
return result;
|
|
2749
3283
|
};
|
|
2750
3284
|
}
|
|
2751
3285
|
|
|
2752
3286
|
// src/execution/query-executor.ts
|
|
2753
|
-
var
|
|
2754
|
-
var
|
|
3287
|
+
var logger3 = createServiceLogger("QueryExecutor");
|
|
3288
|
+
var API_ERROR_PATTERN3 = /API Error: [45]\d\d/;
|
|
2755
3289
|
var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
|
|
2756
3290
|
var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
|
|
2757
3291
|
function buildHooks(host) {
|
|
@@ -2841,7 +3375,7 @@ function buildQueryOptions(host, context) {
|
|
|
2841
3375
|
disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
|
|
2842
3376
|
enableFileCheckpointing: settings.enableFileCheckpointing,
|
|
2843
3377
|
stderr: (data) => {
|
|
2844
|
-
|
|
3378
|
+
logger3.warn("Claude Code stderr", { data: data.trimEnd() });
|
|
2845
3379
|
}
|
|
2846
3380
|
};
|
|
2847
3381
|
if (isCloud && isReadOnly) {
|
|
@@ -2971,6 +3505,29 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
2971
3505
|
options: { ...options, resume: void 0 }
|
|
2972
3506
|
});
|
|
2973
3507
|
}
|
|
3508
|
+
async function handleAuthError(context, host, options) {
|
|
3509
|
+
host.connection.postChatMessage("Authentication expired. Re-bootstrapping credentials...");
|
|
3510
|
+
const refreshed = await host.connection.refreshAuthToken();
|
|
3511
|
+
if (!refreshed) {
|
|
3512
|
+
host.connection.postChatMessage("Failed to refresh authentication. Agent will restart.");
|
|
3513
|
+
host.connection.sendEvent({
|
|
3514
|
+
type: "error",
|
|
3515
|
+
message: "Auth re-bootstrap failed, exiting for restart"
|
|
3516
|
+
});
|
|
3517
|
+
process.exit(1);
|
|
3518
|
+
}
|
|
3519
|
+
context.claudeSessionId = null;
|
|
3520
|
+
host.connection.storeSessionId("");
|
|
3521
|
+
const freshPrompt = buildMultimodalPrompt(
|
|
3522
|
+
await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
3523
|
+
context
|
|
3524
|
+
);
|
|
3525
|
+
const freshQuery = query({
|
|
3526
|
+
prompt: host.createInputStream(freshPrompt),
|
|
3527
|
+
options: { ...options, resume: void 0 }
|
|
3528
|
+
});
|
|
3529
|
+
return runWithRetry(freshQuery, context, host, options);
|
|
3530
|
+
}
|
|
2974
3531
|
async function handleStaleSession(context, host, options) {
|
|
2975
3532
|
context.claudeSessionId = null;
|
|
2976
3533
|
host.connection.storeSessionId("");
|
|
@@ -3002,12 +3559,17 @@ function isStaleOrExitedSession(error, context) {
|
|
|
3002
3559
|
if (error.message.includes("No conversation found with session ID")) return true;
|
|
3003
3560
|
return !!context.claudeSessionId && error.message.includes("process exited");
|
|
3004
3561
|
}
|
|
3562
|
+
function getErrorMessage(error) {
|
|
3563
|
+
if (error instanceof Error) return error.message;
|
|
3564
|
+
if (typeof error === "string") return error;
|
|
3565
|
+
return String(error);
|
|
3566
|
+
}
|
|
3005
3567
|
function isRetriableError(error) {
|
|
3006
|
-
|
|
3007
|
-
return
|
|
3568
|
+
const message = getErrorMessage(error);
|
|
3569
|
+
return API_ERROR_PATTERN3.test(message) || IMAGE_ERROR_PATTERN2.test(message);
|
|
3008
3570
|
}
|
|
3009
3571
|
function classifyImageError(error) {
|
|
3010
|
-
return
|
|
3572
|
+
return IMAGE_ERROR_PATTERN2.test(getErrorMessage(error));
|
|
3011
3573
|
}
|
|
3012
3574
|
async function emitRetryStatus(host, attempt, delayMs) {
|
|
3013
3575
|
const delayMin = Math.round(delayMs / 6e4);
|
|
@@ -3034,26 +3596,41 @@ function handleRetryError(error, context, host, options, prevImageError) {
|
|
|
3034
3596
|
if (isStaleOrExitedSession(error, context) && context.claudeSessionId) {
|
|
3035
3597
|
return handleStaleSession(context, host, options);
|
|
3036
3598
|
}
|
|
3599
|
+
if (isAuthError(getErrorMessage(error))) {
|
|
3600
|
+
return handleAuthError(context, host, options);
|
|
3601
|
+
}
|
|
3037
3602
|
if (!isRetriableError(error)) throw error;
|
|
3038
3603
|
return { action: "continue", lastErrorWasImage: classifyImageError(error) || prevImageError };
|
|
3039
3604
|
}
|
|
3605
|
+
function handleProcessResult(result, context, host, options) {
|
|
3606
|
+
if (result.modeRestart || host.isStopped()) return { action: "return" };
|
|
3607
|
+
if (result.rateLimitResetsAt) {
|
|
3608
|
+
handleRateLimitPause(host, result.rateLimitResetsAt);
|
|
3609
|
+
return { action: "return" };
|
|
3610
|
+
}
|
|
3611
|
+
if (result.staleSession && context.claudeSessionId) {
|
|
3612
|
+
return { action: "return_promise", promise: handleStaleSession(context, host, options) };
|
|
3613
|
+
}
|
|
3614
|
+
if (result.authError) {
|
|
3615
|
+
return { action: "return_promise", promise: handleAuthError(context, host, options) };
|
|
3616
|
+
}
|
|
3617
|
+
if (!result.retriable) return { action: "return" };
|
|
3618
|
+
return {
|
|
3619
|
+
action: "continue",
|
|
3620
|
+
lastErrorWasImage: IMAGE_ERROR_PATTERN2.test(result.resultSummary ?? "")
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3040
3623
|
async function runWithRetry(initialQuery, context, host, options) {
|
|
3041
3624
|
let lastErrorWasImage = false;
|
|
3042
3625
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
3043
3626
|
if (host.isStopped()) return;
|
|
3044
3627
|
const agentQuery = attempt === 0 ? initialQuery : await buildRetryQuery(host, context, options, lastErrorWasImage);
|
|
3045
3628
|
try {
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3048
|
-
if (
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
}
|
|
3052
|
-
if (staleSession && context.claudeSessionId) {
|
|
3053
|
-
return handleStaleSession(context, host, options);
|
|
3054
|
-
}
|
|
3055
|
-
if (!retriable) return;
|
|
3056
|
-
lastErrorWasImage = IMAGE_ERROR_PATTERN2.test(resultSummary ?? "");
|
|
3629
|
+
const result = await processEvents(agentQuery, context, host);
|
|
3630
|
+
const outcome = handleProcessResult(result, context, host, options);
|
|
3631
|
+
if (outcome.action === "return") return;
|
|
3632
|
+
if (outcome.action === "return_promise") return outcome.promise;
|
|
3633
|
+
lastErrorWasImage = outcome.lastErrorWasImage;
|
|
3057
3634
|
} catch (error) {
|
|
3058
3635
|
const outcome = handleRetryError(error, context, host, options, lastErrorWasImage);
|
|
3059
3636
|
if (outcome instanceof Promise) return outcome;
|
|
@@ -3227,6 +3804,47 @@ async function executeSetupConfig(config, runnerConfig, connection, setupLog) {
|
|
|
3227
3804
|
async function checkoutTaskBranch(runnerConfig, connection, callbacks, setupLog) {
|
|
3228
3805
|
const taskBranch = process.env.CONVEYOR_TASK_BRANCH;
|
|
3229
3806
|
if (!taskBranch) return true;
|
|
3807
|
+
const currentBranch = getCurrentBranch(runnerConfig.workspaceDir);
|
|
3808
|
+
if (currentBranch === taskBranch) {
|
|
3809
|
+
pushSetupLog(setupLog, `[conveyor] Already on ${taskBranch}, skipping checkout`);
|
|
3810
|
+
connection.sendEvent({
|
|
3811
|
+
type: "setup_output",
|
|
3812
|
+
stream: "stdout",
|
|
3813
|
+
data: `Already on branch ${taskBranch}, skipping checkout
|
|
3814
|
+
`
|
|
3815
|
+
});
|
|
3816
|
+
try {
|
|
3817
|
+
await runSetupCommand(
|
|
3818
|
+
`git fetch origin ${taskBranch}`,
|
|
3819
|
+
runnerConfig.workspaceDir,
|
|
3820
|
+
(stream, data) => {
|
|
3821
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
3822
|
+
}
|
|
3823
|
+
);
|
|
3824
|
+
} catch {
|
|
3825
|
+
}
|
|
3826
|
+
return true;
|
|
3827
|
+
}
|
|
3828
|
+
let didStash = false;
|
|
3829
|
+
if (hasUncommittedChanges(runnerConfig.workspaceDir)) {
|
|
3830
|
+
pushSetupLog(setupLog, `[conveyor] Uncommitted changes detected, stashing before checkout`);
|
|
3831
|
+
connection.sendEvent({
|
|
3832
|
+
type: "setup_output",
|
|
3833
|
+
stream: "stdout",
|
|
3834
|
+
data: "Uncommitted changes detected \u2014 stashing before branch switch\n"
|
|
3835
|
+
});
|
|
3836
|
+
try {
|
|
3837
|
+
await runSetupCommand(
|
|
3838
|
+
`git stash push -m "conveyor-auto-stash"`,
|
|
3839
|
+
runnerConfig.workspaceDir,
|
|
3840
|
+
(stream, data) => {
|
|
3841
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
3842
|
+
}
|
|
3843
|
+
);
|
|
3844
|
+
didStash = true;
|
|
3845
|
+
} catch {
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3230
3848
|
pushSetupLog(setupLog, `[conveyor] Switching to task branch ${taskBranch}...`);
|
|
3231
3849
|
connection.sendEvent({
|
|
3232
3850
|
type: "setup_output",
|
|
@@ -3246,9 +3864,22 @@ async function checkoutTaskBranch(runnerConfig, connection, callbacks, setupLog)
|
|
|
3246
3864
|
}
|
|
3247
3865
|
);
|
|
3248
3866
|
pushSetupLog(setupLog, `[conveyor] Switched to ${taskBranch}`);
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3867
|
+
if (didStash) {
|
|
3868
|
+
try {
|
|
3869
|
+
await runSetupCommand("git stash pop", runnerConfig.workspaceDir, (stream, data) => {
|
|
3870
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
3871
|
+
});
|
|
3872
|
+
pushSetupLog(setupLog, `[conveyor] Restored stashed changes`);
|
|
3873
|
+
} catch {
|
|
3874
|
+
pushSetupLog(
|
|
3875
|
+
setupLog,
|
|
3876
|
+
`[conveyor] Warning: stash pop had conflicts \u2014 agent may need to resolve`
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
return true;
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
const message = `Failed to checkout ${taskBranch}: ${error instanceof Error ? error.message : "unknown error"}`;
|
|
3252
3883
|
connection.sendEvent({ type: "setup_error", message });
|
|
3253
3884
|
await callbacks.onEvent({ type: "setup_error", message });
|
|
3254
3885
|
connection.postChatMessage(`Failed to switch to task branch \`${taskBranch}\`.
|
|
@@ -3433,7 +4064,7 @@ function buildQueryHost(deps) {
|
|
|
3433
4064
|
}
|
|
3434
4065
|
|
|
3435
4066
|
// src/runner/agent-runner.ts
|
|
3436
|
-
var
|
|
4067
|
+
var logger4 = createServiceLogger("AgentRunner");
|
|
3437
4068
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3438
4069
|
var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
3439
4070
|
var AgentRunner = class {
|
|
@@ -3462,6 +4093,7 @@ var AgentRunner = class {
|
|
|
3462
4093
|
idleCheckInterval = null;
|
|
3463
4094
|
conveyorConfig = null;
|
|
3464
4095
|
_queryHost = null;
|
|
4096
|
+
tunnelClient = null;
|
|
3465
4097
|
constructor(config, callbacks) {
|
|
3466
4098
|
this.config = config;
|
|
3467
4099
|
this.connection = new ConveyorConnection(config);
|
|
@@ -3536,11 +4168,13 @@ var AgentRunner = class {
|
|
|
3536
4168
|
}
|
|
3537
4169
|
this.tryInitWorktree();
|
|
3538
4170
|
if (!await this.fetchAndInitContext()) return;
|
|
4171
|
+
this.startPreviewTunnel();
|
|
3539
4172
|
this.tryPostContextWorktree();
|
|
3540
4173
|
this.checkoutWorktreeBranch();
|
|
3541
4174
|
await this.executeInitialMode();
|
|
3542
4175
|
await this.runCoreLoop();
|
|
3543
4176
|
this.stopHeartbeat();
|
|
4177
|
+
this.tunnelClient?.disconnect();
|
|
3544
4178
|
await this.setState("finished");
|
|
3545
4179
|
this.connection.disconnect();
|
|
3546
4180
|
}
|
|
@@ -3573,6 +4207,11 @@ var AgentRunner = class {
|
|
|
3573
4207
|
this.activateWorktree("[conveyor] Using worktree (from task config):");
|
|
3574
4208
|
}
|
|
3575
4209
|
}
|
|
4210
|
+
startPreviewTunnel() {
|
|
4211
|
+
const port = this.conveyorConfig?.previewPort ?? (Number(process.env.CONVEYOR_PREVIEW_PORT) || 3050);
|
|
4212
|
+
this.tunnelClient = new TunnelClient(this.config.conveyorApiUrl, this.config.taskToken, port);
|
|
4213
|
+
this.tunnelClient.connect();
|
|
4214
|
+
}
|
|
3576
4215
|
activateWorktree(logPrefix) {
|
|
3577
4216
|
try {
|
|
3578
4217
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
@@ -3590,12 +4229,22 @@ var AgentRunner = class {
|
|
|
3590
4229
|
}
|
|
3591
4230
|
checkoutWorktreeBranch() {
|
|
3592
4231
|
if (!this.worktreeActive || !this.taskContext?.githubBranch) return;
|
|
4232
|
+
const branch = this.taskContext.githubBranch;
|
|
4233
|
+
const cwd = this.config.workspaceDir;
|
|
4234
|
+
if (getCurrentBranch(cwd) === branch) return;
|
|
3593
4235
|
try {
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
cwd:
|
|
3597
|
-
|
|
3598
|
-
}
|
|
4236
|
+
let didStash = false;
|
|
4237
|
+
if (hasUncommittedChanges(cwd)) {
|
|
4238
|
+
execSync5(`git stash push -m "conveyor-auto-stash"`, { cwd, stdio: "ignore" });
|
|
4239
|
+
didStash = true;
|
|
4240
|
+
}
|
|
4241
|
+
execSync5(`git fetch origin ${branch} && git checkout ${branch}`, { cwd, stdio: "ignore" });
|
|
4242
|
+
if (didStash) {
|
|
4243
|
+
try {
|
|
4244
|
+
execSync5("git stash pop", { cwd, stdio: "ignore" });
|
|
4245
|
+
} catch {
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
3599
4248
|
} catch {
|
|
3600
4249
|
}
|
|
3601
4250
|
}
|
|
@@ -3722,7 +4371,7 @@ var AgentRunner = class {
|
|
|
3722
4371
|
const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
|
|
3723
4372
|
const model = this.taskContext.model || this.config.model;
|
|
3724
4373
|
const thinking = formatThinkingSetting(s.thinking);
|
|
3725
|
-
|
|
4374
|
+
logger4.info("Effective agent settings", {
|
|
3726
4375
|
model,
|
|
3727
4376
|
mode: this.config.mode ?? "task",
|
|
3728
4377
|
effort: s.effort ?? "default",
|
|
@@ -3764,15 +4413,11 @@ var AgentRunner = class {
|
|
|
3764
4413
|
}
|
|
3765
4414
|
}, 1e3);
|
|
3766
4415
|
this.idleTimer = setTimeout(() => {
|
|
3767
|
-
|
|
3768
|
-
this.inputResolver = null;
|
|
3769
|
-
logger3.info("Idle timeout reached, shutting down", {
|
|
4416
|
+
logger4.info("Idle timeout reached, entering sleep mode", {
|
|
3770
4417
|
idleMinutes: IDLE_TIMEOUT_MS / 6e4
|
|
3771
4418
|
});
|
|
3772
|
-
this.connection.
|
|
3773
|
-
|
|
3774
|
-
);
|
|
3775
|
-
resolve2(null);
|
|
4419
|
+
this.connection.emitStatus("sleeping");
|
|
4420
|
+
this.connection.postChatMessage("Agent sleeping \u2014 send a message or click Resume to wake.");
|
|
3776
4421
|
}, IDLE_TIMEOUT_MS);
|
|
3777
4422
|
this.inputResolver = (msg) => {
|
|
3778
4423
|
this.clearIdleTimers();
|
|
@@ -3874,6 +4519,7 @@ var AgentRunner = class {
|
|
|
3874
4519
|
stop() {
|
|
3875
4520
|
this.stopped = true;
|
|
3876
4521
|
this.clearIdleTimers();
|
|
4522
|
+
this.tunnelClient?.disconnect();
|
|
3877
4523
|
if (this.inputResolver) {
|
|
3878
4524
|
this.inputResolver(null);
|
|
3879
4525
|
this.inputResolver = null;
|
|
@@ -3883,13 +4529,270 @@ var AgentRunner = class {
|
|
|
3883
4529
|
|
|
3884
4530
|
// src/runner/project-runner.ts
|
|
3885
4531
|
import { fork } from "child_process";
|
|
3886
|
-
import { execSync as
|
|
4532
|
+
import { execSync as execSync7 } from "child_process";
|
|
3887
4533
|
import * as path from "path";
|
|
3888
4534
|
import { fileURLToPath } from "url";
|
|
3889
4535
|
|
|
4536
|
+
// src/runner/commit-watcher.ts
|
|
4537
|
+
import { execSync as execSync6 } from "child_process";
|
|
4538
|
+
var logger5 = createServiceLogger("CommitWatcher");
|
|
4539
|
+
var CommitWatcher = class {
|
|
4540
|
+
constructor(config, callbacks) {
|
|
4541
|
+
this.config = config;
|
|
4542
|
+
this.callbacks = callbacks;
|
|
4543
|
+
}
|
|
4544
|
+
interval = null;
|
|
4545
|
+
lastKnownRemoteSha = null;
|
|
4546
|
+
branch = null;
|
|
4547
|
+
debounceTimer = null;
|
|
4548
|
+
isSyncing = false;
|
|
4549
|
+
start(branch) {
|
|
4550
|
+
this.stop();
|
|
4551
|
+
this.branch = branch;
|
|
4552
|
+
this.lastKnownRemoteSha = this.getLocalHeadSha();
|
|
4553
|
+
this.interval = setInterval(() => void this.poll(), this.config.pollIntervalMs);
|
|
4554
|
+
logger5.info("Commit watcher started", {
|
|
4555
|
+
branch,
|
|
4556
|
+
baseSha: this.lastKnownRemoteSha?.slice(0, 8)
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
stop() {
|
|
4560
|
+
if (this.interval) clearInterval(this.interval);
|
|
4561
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
4562
|
+
this.interval = null;
|
|
4563
|
+
this.debounceTimer = null;
|
|
4564
|
+
this.branch = null;
|
|
4565
|
+
this.lastKnownRemoteSha = null;
|
|
4566
|
+
this.isSyncing = false;
|
|
4567
|
+
}
|
|
4568
|
+
getLocalHeadSha() {
|
|
4569
|
+
return execSync6("git rev-parse HEAD", {
|
|
4570
|
+
cwd: this.config.projectDir,
|
|
4571
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4572
|
+
}).toString().trim();
|
|
4573
|
+
}
|
|
4574
|
+
poll() {
|
|
4575
|
+
if (!this.branch || this.isSyncing) return;
|
|
4576
|
+
try {
|
|
4577
|
+
execSync6(`git fetch origin ${this.branch} --quiet`, {
|
|
4578
|
+
cwd: this.config.projectDir,
|
|
4579
|
+
stdio: "ignore",
|
|
4580
|
+
timeout: 3e4
|
|
4581
|
+
});
|
|
4582
|
+
const remoteSha = execSync6(`git rev-parse origin/${this.branch}`, {
|
|
4583
|
+
cwd: this.config.projectDir,
|
|
4584
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4585
|
+
}).toString().trim();
|
|
4586
|
+
if (remoteSha !== this.lastKnownRemoteSha) {
|
|
4587
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
4588
|
+
this.debounceTimer = setTimeout(
|
|
4589
|
+
() => void this.handleNewCommits(remoteSha),
|
|
4590
|
+
this.config.debounceMs
|
|
4591
|
+
);
|
|
4592
|
+
}
|
|
4593
|
+
} catch {
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
async handleNewCommits(remoteSha) {
|
|
4597
|
+
if (!this.branch) return;
|
|
4598
|
+
const previousSha = this.lastKnownRemoteSha ?? "HEAD";
|
|
4599
|
+
let commitCount = 1;
|
|
4600
|
+
let latestMessage = "";
|
|
4601
|
+
let latestAuthor = "";
|
|
4602
|
+
try {
|
|
4603
|
+
const countOutput = execSync6(`git rev-list --count ${previousSha}..origin/${this.branch}`, {
|
|
4604
|
+
cwd: this.config.projectDir,
|
|
4605
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4606
|
+
}).toString().trim();
|
|
4607
|
+
commitCount = parseInt(countOutput, 10) || 1;
|
|
4608
|
+
const logOutput = execSync6(`git log -1 --format="%s|||%an" origin/${this.branch}`, {
|
|
4609
|
+
cwd: this.config.projectDir,
|
|
4610
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4611
|
+
}).toString().trim();
|
|
4612
|
+
const parts = logOutput.split("|||");
|
|
4613
|
+
latestMessage = parts[0] ?? "";
|
|
4614
|
+
latestAuthor = parts[1] ?? "";
|
|
4615
|
+
} catch {
|
|
4616
|
+
}
|
|
4617
|
+
this.lastKnownRemoteSha = remoteSha;
|
|
4618
|
+
this.isSyncing = true;
|
|
4619
|
+
logger5.info("New commits detected", {
|
|
4620
|
+
branch: this.branch,
|
|
4621
|
+
commitCount,
|
|
4622
|
+
sha: remoteSha.slice(0, 8)
|
|
4623
|
+
});
|
|
4624
|
+
try {
|
|
4625
|
+
await this.callbacks.onNewCommits({
|
|
4626
|
+
branch: this.branch,
|
|
4627
|
+
previousSha,
|
|
4628
|
+
newCommitSha: remoteSha,
|
|
4629
|
+
commitCount,
|
|
4630
|
+
latestMessage,
|
|
4631
|
+
latestAuthor
|
|
4632
|
+
});
|
|
4633
|
+
} catch (err) {
|
|
4634
|
+
logger5.error("Error handling new commits", errorMeta(err));
|
|
4635
|
+
} finally {
|
|
4636
|
+
this.isSyncing = false;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
};
|
|
4640
|
+
|
|
4641
|
+
// src/runner/project-chat-handler.ts
|
|
4642
|
+
import {
|
|
4643
|
+
query as query2,
|
|
4644
|
+
createSdkMcpServer as createSdkMcpServer2
|
|
4645
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
4646
|
+
|
|
4647
|
+
// src/tools/project-tools.ts
|
|
4648
|
+
import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
4649
|
+
import { z as z4 } from "zod";
|
|
4650
|
+
function buildReadTools(connection) {
|
|
4651
|
+
return [
|
|
4652
|
+
tool4(
|
|
4653
|
+
"list_tasks",
|
|
4654
|
+
"List tasks in the project. Optionally filter by status or assignee.",
|
|
4655
|
+
{
|
|
4656
|
+
status: z4.string().optional().describe("Filter by task status (e.g. Planning, Open, InProgress, ReviewPR, Complete)"),
|
|
4657
|
+
assigneeId: z4.string().optional().describe("Filter by assigned user ID"),
|
|
4658
|
+
limit: z4.number().optional().describe("Max number of tasks to return (default 50)")
|
|
4659
|
+
},
|
|
4660
|
+
async (params) => {
|
|
4661
|
+
try {
|
|
4662
|
+
const tasks = await connection.requestListTasks(params);
|
|
4663
|
+
return textResult(JSON.stringify(tasks, null, 2));
|
|
4664
|
+
} catch (error) {
|
|
4665
|
+
return textResult(
|
|
4666
|
+
`Failed to list tasks: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4667
|
+
);
|
|
4668
|
+
}
|
|
4669
|
+
},
|
|
4670
|
+
{ annotations: { readOnlyHint: true } }
|
|
4671
|
+
),
|
|
4672
|
+
tool4(
|
|
4673
|
+
"get_task",
|
|
4674
|
+
"Get detailed information about a task including its chat messages, child tasks, and codespace status.",
|
|
4675
|
+
{ task_id: z4.string().describe("The task ID to look up") },
|
|
4676
|
+
async ({ task_id }) => {
|
|
4677
|
+
try {
|
|
4678
|
+
const task = await connection.requestGetTask(task_id);
|
|
4679
|
+
return textResult(JSON.stringify(task, null, 2));
|
|
4680
|
+
} catch (error) {
|
|
4681
|
+
return textResult(
|
|
4682
|
+
`Failed to get task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4683
|
+
);
|
|
4684
|
+
}
|
|
4685
|
+
},
|
|
4686
|
+
{ annotations: { readOnlyHint: true } }
|
|
4687
|
+
),
|
|
4688
|
+
tool4(
|
|
4689
|
+
"search_tasks",
|
|
4690
|
+
"Search tasks by tags, text query, or status filters.",
|
|
4691
|
+
{
|
|
4692
|
+
tagNames: z4.array(z4.string()).optional().describe("Filter by tag names"),
|
|
4693
|
+
searchQuery: z4.string().optional().describe("Text search in title/description"),
|
|
4694
|
+
statusFilters: z4.array(z4.string()).optional().describe("Filter by statuses"),
|
|
4695
|
+
limit: z4.number().optional().describe("Max results (default 20)")
|
|
4696
|
+
},
|
|
4697
|
+
async (params) => {
|
|
4698
|
+
try {
|
|
4699
|
+
const tasks = await connection.requestSearchTasks(params);
|
|
4700
|
+
return textResult(JSON.stringify(tasks, null, 2));
|
|
4701
|
+
} catch (error) {
|
|
4702
|
+
return textResult(
|
|
4703
|
+
`Failed to search tasks: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
},
|
|
4707
|
+
{ annotations: { readOnlyHint: true } }
|
|
4708
|
+
),
|
|
4709
|
+
tool4(
|
|
4710
|
+
"list_tags",
|
|
4711
|
+
"List all tags available in the project.",
|
|
4712
|
+
{},
|
|
4713
|
+
async () => {
|
|
4714
|
+
try {
|
|
4715
|
+
const tags = await connection.requestListTags();
|
|
4716
|
+
return textResult(JSON.stringify(tags, null, 2));
|
|
4717
|
+
} catch (error) {
|
|
4718
|
+
return textResult(
|
|
4719
|
+
`Failed to list tags: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
},
|
|
4723
|
+
{ annotations: { readOnlyHint: true } }
|
|
4724
|
+
),
|
|
4725
|
+
tool4(
|
|
4726
|
+
"get_project_summary",
|
|
4727
|
+
"Get a summary of the project including task counts by status and active builds.",
|
|
4728
|
+
{},
|
|
4729
|
+
async () => {
|
|
4730
|
+
try {
|
|
4731
|
+
const summary = await connection.requestGetProjectSummary();
|
|
4732
|
+
return textResult(JSON.stringify(summary, null, 2));
|
|
4733
|
+
} catch (error) {
|
|
4734
|
+
return textResult(
|
|
4735
|
+
`Failed to get project summary: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
},
|
|
4739
|
+
{ annotations: { readOnlyHint: true } }
|
|
4740
|
+
)
|
|
4741
|
+
];
|
|
4742
|
+
}
|
|
4743
|
+
function buildMutationTools(connection) {
|
|
4744
|
+
return [
|
|
4745
|
+
tool4(
|
|
4746
|
+
"create_task",
|
|
4747
|
+
"Create a new task in the project.",
|
|
4748
|
+
{
|
|
4749
|
+
title: z4.string().describe("Task title"),
|
|
4750
|
+
description: z4.string().optional().describe("Task description"),
|
|
4751
|
+
plan: z4.string().optional().describe("Implementation plan in markdown"),
|
|
4752
|
+
status: z4.string().optional().describe("Initial status (default: Planning)"),
|
|
4753
|
+
isBug: z4.boolean().optional().describe("Whether this is a bug report")
|
|
4754
|
+
},
|
|
4755
|
+
async (params) => {
|
|
4756
|
+
try {
|
|
4757
|
+
const result = await connection.requestCreateTask(params);
|
|
4758
|
+
return textResult(`Task created: ${result.slug} (ID: ${result.id})`);
|
|
4759
|
+
} catch (error) {
|
|
4760
|
+
return textResult(
|
|
4761
|
+
`Failed to create task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
),
|
|
4766
|
+
tool4(
|
|
4767
|
+
"update_task",
|
|
4768
|
+
"Update an existing task's title, description, plan, status, or assignee.",
|
|
4769
|
+
{
|
|
4770
|
+
task_id: z4.string().describe("The task ID to update"),
|
|
4771
|
+
title: z4.string().optional().describe("New title"),
|
|
4772
|
+
description: z4.string().optional().describe("New description"),
|
|
4773
|
+
plan: z4.string().optional().describe("New plan in markdown"),
|
|
4774
|
+
status: z4.string().optional().describe("New status"),
|
|
4775
|
+
assignedUserId: z4.string().nullable().optional().describe("Assign to user ID, or null to unassign")
|
|
4776
|
+
},
|
|
4777
|
+
async ({ task_id, ...fields }) => {
|
|
4778
|
+
try {
|
|
4779
|
+
await connection.requestUpdateTask({ taskId: task_id, ...fields });
|
|
4780
|
+
return textResult("Task updated successfully.");
|
|
4781
|
+
} catch (error) {
|
|
4782
|
+
return textResult(
|
|
4783
|
+
`Failed to update task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
)
|
|
4788
|
+
];
|
|
4789
|
+
}
|
|
4790
|
+
function buildProjectTools(connection) {
|
|
4791
|
+
return [...buildReadTools(connection), ...buildMutationTools(connection)];
|
|
4792
|
+
}
|
|
4793
|
+
|
|
3890
4794
|
// src/runner/project-chat-handler.ts
|
|
3891
|
-
|
|
3892
|
-
var logger4 = createServiceLogger("ProjectChat");
|
|
4795
|
+
var logger6 = createServiceLogger("ProjectChat");
|
|
3893
4796
|
var FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
3894
4797
|
function buildSystemPrompt2(projectDir, agentCtx) {
|
|
3895
4798
|
const parts = [];
|
|
@@ -3942,27 +4845,31 @@ function processContentBlock(block, responseParts, turnToolCalls) {
|
|
|
3942
4845
|
input: inputStr.slice(0, 1e4),
|
|
3943
4846
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3944
4847
|
});
|
|
3945
|
-
|
|
4848
|
+
logger6.debug("Tool use", { tool: block.name });
|
|
3946
4849
|
}
|
|
3947
4850
|
}
|
|
3948
|
-
async function fetchContext(connection) {
|
|
4851
|
+
async function fetchContext(connection, chatId) {
|
|
3949
4852
|
let agentCtx = null;
|
|
3950
4853
|
try {
|
|
3951
4854
|
agentCtx = await connection.fetchAgentContext();
|
|
3952
4855
|
} catch {
|
|
3953
|
-
|
|
4856
|
+
logger6.warn("Could not fetch agent context, using defaults");
|
|
3954
4857
|
}
|
|
3955
4858
|
let chatHistory = [];
|
|
3956
4859
|
try {
|
|
3957
|
-
chatHistory = await connection.fetchChatHistory(30);
|
|
4860
|
+
chatHistory = await connection.fetchChatHistory(30, chatId);
|
|
3958
4861
|
} catch {
|
|
3959
|
-
|
|
4862
|
+
logger6.warn("Could not fetch chat history, proceeding without it");
|
|
3960
4863
|
}
|
|
3961
4864
|
return { agentCtx, chatHistory };
|
|
3962
4865
|
}
|
|
3963
|
-
function buildChatQueryOptions(agentCtx, projectDir) {
|
|
4866
|
+
function buildChatQueryOptions(agentCtx, projectDir, connection) {
|
|
3964
4867
|
const model = agentCtx?.model || FALLBACK_MODEL;
|
|
3965
4868
|
const settings = agentCtx?.agentSettings ?? {};
|
|
4869
|
+
const mcpServer = createSdkMcpServer2({
|
|
4870
|
+
name: "conveyor",
|
|
4871
|
+
tools: buildProjectTools(connection)
|
|
4872
|
+
});
|
|
3966
4873
|
return {
|
|
3967
4874
|
model,
|
|
3968
4875
|
systemPrompt: {
|
|
@@ -3974,12 +4881,48 @@ function buildChatQueryOptions(agentCtx, projectDir) {
|
|
|
3974
4881
|
permissionMode: "bypassPermissions",
|
|
3975
4882
|
allowDangerouslySkipPermissions: true,
|
|
3976
4883
|
tools: { type: "preset", preset: "claude_code" },
|
|
3977
|
-
|
|
3978
|
-
|
|
4884
|
+
mcpServers: { conveyor: mcpServer },
|
|
4885
|
+
maxTurns: settings.maxTurns ?? 30,
|
|
4886
|
+
maxBudgetUsd: settings.maxBudgetUsd ?? 50,
|
|
3979
4887
|
effort: settings.effort,
|
|
3980
4888
|
thinking: settings.thinking
|
|
3981
4889
|
};
|
|
3982
4890
|
}
|
|
4891
|
+
function emitResultCostAndContext(event, connection) {
|
|
4892
|
+
const resultEvent = event;
|
|
4893
|
+
if (resultEvent.total_cost_usd !== void 0 && resultEvent.total_cost_usd > 0) {
|
|
4894
|
+
connection.emitEvent({
|
|
4895
|
+
type: "cost_update",
|
|
4896
|
+
costUsd: resultEvent.total_cost_usd
|
|
4897
|
+
});
|
|
4898
|
+
}
|
|
4899
|
+
if (resultEvent.modelUsage && typeof resultEvent.modelUsage === "object") {
|
|
4900
|
+
const modelUsage = resultEvent.modelUsage;
|
|
4901
|
+
let contextWindow = 0;
|
|
4902
|
+
let totalInputTokens = 0;
|
|
4903
|
+
let totalCacheRead = 0;
|
|
4904
|
+
let totalCacheCreation = 0;
|
|
4905
|
+
for (const data of Object.values(modelUsage)) {
|
|
4906
|
+
const d = data;
|
|
4907
|
+
totalInputTokens += d.inputTokens ?? 0;
|
|
4908
|
+
totalCacheRead += d.cacheReadInputTokens ?? 0;
|
|
4909
|
+
totalCacheCreation += d.cacheCreationInputTokens ?? 0;
|
|
4910
|
+
const cw = d.contextWindow ?? 0;
|
|
4911
|
+
if (cw > contextWindow) contextWindow = cw;
|
|
4912
|
+
}
|
|
4913
|
+
if (contextWindow > 0) {
|
|
4914
|
+
const queryInputTokens = totalInputTokens + totalCacheRead + totalCacheCreation;
|
|
4915
|
+
connection.emitEvent({
|
|
4916
|
+
type: "context_update",
|
|
4917
|
+
contextTokens: queryInputTokens,
|
|
4918
|
+
contextWindow,
|
|
4919
|
+
inputTokens: totalInputTokens,
|
|
4920
|
+
cacheReadInputTokens: totalCacheRead,
|
|
4921
|
+
cacheCreationInputTokens: totalCacheCreation
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
3983
4926
|
function processEventStream(event, connection, responseParts, turnToolCalls, isTyping) {
|
|
3984
4927
|
if (event.type === "assistant") {
|
|
3985
4928
|
if (!isTyping.value) {
|
|
@@ -4001,19 +4944,30 @@ function processEventStream(event, connection, responseParts, turnToolCalls, isT
|
|
|
4001
4944
|
connection.emitEvent({ type: "agent_typing_stop" });
|
|
4002
4945
|
isTyping.value = false;
|
|
4003
4946
|
}
|
|
4947
|
+
emitResultCostAndContext(event, connection);
|
|
4004
4948
|
return true;
|
|
4005
4949
|
}
|
|
4006
4950
|
return false;
|
|
4007
4951
|
}
|
|
4008
|
-
async function runChatQuery(message, connection, projectDir) {
|
|
4009
|
-
const { agentCtx, chatHistory } = await fetchContext(connection);
|
|
4010
|
-
const options = buildChatQueryOptions(agentCtx, projectDir);
|
|
4952
|
+
async function runChatQuery(message, connection, projectDir, sessionId) {
|
|
4953
|
+
const { agentCtx, chatHistory } = await fetchContext(connection, message.chatId);
|
|
4954
|
+
const options = buildChatQueryOptions(agentCtx, projectDir, connection);
|
|
4011
4955
|
const prompt = buildPrompt(message, chatHistory);
|
|
4012
|
-
|
|
4956
|
+
connection.emitAgentStatus("running");
|
|
4957
|
+
const events = query2({
|
|
4958
|
+
prompt,
|
|
4959
|
+
options,
|
|
4960
|
+
...sessionId ? { resume: sessionId } : {}
|
|
4961
|
+
});
|
|
4013
4962
|
const responseParts = [];
|
|
4014
4963
|
const turnToolCalls = [];
|
|
4015
4964
|
const isTyping = { value: false };
|
|
4965
|
+
let resultSessionId;
|
|
4016
4966
|
for await (const event of events) {
|
|
4967
|
+
if (event.type === "result") {
|
|
4968
|
+
const resultEvent = event;
|
|
4969
|
+
resultSessionId = resultEvent.sessionId;
|
|
4970
|
+
}
|
|
4017
4971
|
const done = processEventStream(event, connection, responseParts, turnToolCalls, isTyping);
|
|
4018
4972
|
if (done) break;
|
|
4019
4973
|
}
|
|
@@ -4024,26 +4978,416 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
4024
4978
|
if (responseText) {
|
|
4025
4979
|
await connection.emitChatMessage(responseText);
|
|
4026
4980
|
}
|
|
4981
|
+
return resultSessionId;
|
|
4027
4982
|
}
|
|
4028
|
-
async function handleProjectChatMessage(message, connection, projectDir) {
|
|
4029
|
-
connection.emitAgentStatus("
|
|
4983
|
+
async function handleProjectChatMessage(message, connection, projectDir, sessionId) {
|
|
4984
|
+
connection.emitAgentStatus("fetching_context");
|
|
4030
4985
|
try {
|
|
4031
|
-
await runChatQuery(message, connection, projectDir);
|
|
4986
|
+
return await runChatQuery(message, connection, projectDir, sessionId);
|
|
4032
4987
|
} catch (error) {
|
|
4033
|
-
|
|
4988
|
+
logger6.error("Failed to handle message", errorMeta(error));
|
|
4989
|
+
connection.emitAgentStatus("error");
|
|
4034
4990
|
try {
|
|
4035
4991
|
await connection.emitChatMessage(
|
|
4036
4992
|
"I encountered an error processing your message. Please try again."
|
|
4037
4993
|
);
|
|
4038
4994
|
} catch {
|
|
4039
4995
|
}
|
|
4996
|
+
return void 0;
|
|
4997
|
+
} finally {
|
|
4998
|
+
connection.emitAgentStatus("idle");
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
// src/runner/project-audit-handler.ts
|
|
5003
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
5004
|
+
|
|
5005
|
+
// src/tools/audit-tools.ts
|
|
5006
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5007
|
+
import { tool as tool5, createSdkMcpServer as createSdkMcpServer3 } from "@anthropic-ai/claude-agent-sdk";
|
|
5008
|
+
import { z as z5 } from "zod";
|
|
5009
|
+
function mapCreateTag(input) {
|
|
5010
|
+
return {
|
|
5011
|
+
type: "create_tag",
|
|
5012
|
+
tagName: input.name,
|
|
5013
|
+
suggestion: `Create new tag "${input.name}"${input.description ? `: ${input.description}` : ""}`,
|
|
5014
|
+
reasoning: input.reasoning,
|
|
5015
|
+
payload: { name: input.name, color: input.color ?? "#6B7280", description: input.description }
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
function mapUpdateDescription(input) {
|
|
5019
|
+
return {
|
|
5020
|
+
type: "update_description",
|
|
5021
|
+
tagId: input.tagId,
|
|
5022
|
+
tagName: input.tagName,
|
|
5023
|
+
suggestion: `Update description for "${input.tagName}"`,
|
|
5024
|
+
reasoning: input.reasoning,
|
|
5025
|
+
payload: { description: input.description }
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
function mapContextLink(input) {
|
|
5029
|
+
return {
|
|
5030
|
+
type: "add_context_link",
|
|
5031
|
+
tagId: input.tagId,
|
|
5032
|
+
tagName: input.tagName,
|
|
5033
|
+
suggestion: `Link ${input.linkType}:${input.path} to "${input.tagName}"`,
|
|
5034
|
+
reasoning: input.reasoning,
|
|
5035
|
+
payload: { contextLink: { type: input.linkType, path: input.path, label: input.label } }
|
|
5036
|
+
};
|
|
5037
|
+
}
|
|
5038
|
+
function mapDocGap(input) {
|
|
5039
|
+
return {
|
|
5040
|
+
type: "documentation_gap",
|
|
5041
|
+
tagId: input.tagId,
|
|
5042
|
+
tagName: input.tagName,
|
|
5043
|
+
suggestion: `Documentation gap: ${input.filePath} (${input.readCount} reads)`,
|
|
5044
|
+
reasoning: input.reasoning,
|
|
5045
|
+
payload: {
|
|
5046
|
+
filePath: input.filePath,
|
|
5047
|
+
readCount: input.readCount,
|
|
5048
|
+
suggestedAction: input.suggestedAction
|
|
5049
|
+
}
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
function mapMergeTags(input) {
|
|
5053
|
+
return {
|
|
5054
|
+
type: "merge_tags",
|
|
5055
|
+
tagId: input.tagId,
|
|
5056
|
+
tagName: input.tagName,
|
|
5057
|
+
suggestion: `Merge "${input.tagName}" into "${input.mergeIntoTagName}"`,
|
|
5058
|
+
reasoning: input.reasoning,
|
|
5059
|
+
payload: { mergeIntoTagId: input.mergeIntoTagId }
|
|
5060
|
+
};
|
|
5061
|
+
}
|
|
5062
|
+
function mapRenameTag(input) {
|
|
5063
|
+
return {
|
|
5064
|
+
type: "rename_tag",
|
|
5065
|
+
tagId: input.tagId,
|
|
5066
|
+
tagName: input.tagName,
|
|
5067
|
+
suggestion: `Rename "${input.tagName}" to "${input.newName}"`,
|
|
5068
|
+
reasoning: input.reasoning,
|
|
5069
|
+
payload: { newName: input.newName }
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
var TOOL_MAPPERS = {
|
|
5073
|
+
recommend_create_tag: mapCreateTag,
|
|
5074
|
+
recommend_update_description: mapUpdateDescription,
|
|
5075
|
+
recommend_context_link: mapContextLink,
|
|
5076
|
+
flag_documentation_gap: mapDocGap,
|
|
5077
|
+
recommend_merge_tags: mapMergeTags,
|
|
5078
|
+
recommend_rename_tag: mapRenameTag
|
|
5079
|
+
};
|
|
5080
|
+
function collectRecommendation(toolName, input, collector, onRecommendation) {
|
|
5081
|
+
const mapper = TOOL_MAPPERS[toolName];
|
|
5082
|
+
if (!mapper) return JSON.stringify({ error: `Unknown tool: ${toolName}` });
|
|
5083
|
+
const rec = { id: randomUUID3(), ...mapper(input) };
|
|
5084
|
+
collector.recommendations.push(rec);
|
|
5085
|
+
onRecommendation?.({ tagName: rec.tagName ?? rec.type, type: rec.type });
|
|
5086
|
+
return JSON.stringify({ success: true, recommendationId: rec.id });
|
|
5087
|
+
}
|
|
5088
|
+
function createAuditMcpServer(collector, onRecommendation) {
|
|
5089
|
+
const auditTools = [
|
|
5090
|
+
tool5(
|
|
5091
|
+
"recommend_create_tag",
|
|
5092
|
+
"Recommend creating a new tag for an uncovered subsystem or area",
|
|
5093
|
+
{
|
|
5094
|
+
name: z5.string().describe("Proposed tag name (lowercase, hyphenated)"),
|
|
5095
|
+
color: z5.string().optional().describe("Hex color code"),
|
|
5096
|
+
description: z5.string().describe("What this tag covers"),
|
|
5097
|
+
reasoning: z5.string().describe("Why this tag should be created")
|
|
5098
|
+
},
|
|
5099
|
+
async (args) => {
|
|
5100
|
+
const result = collectRecommendation(
|
|
5101
|
+
"recommend_create_tag",
|
|
5102
|
+
args,
|
|
5103
|
+
collector,
|
|
5104
|
+
onRecommendation
|
|
5105
|
+
);
|
|
5106
|
+
return { content: [{ type: "text", text: result }] };
|
|
5107
|
+
}
|
|
5108
|
+
),
|
|
5109
|
+
tool5(
|
|
5110
|
+
"recommend_update_description",
|
|
5111
|
+
"Recommend updating a tag's description to better reflect its scope",
|
|
5112
|
+
{
|
|
5113
|
+
tagId: z5.string(),
|
|
5114
|
+
tagName: z5.string(),
|
|
5115
|
+
description: z5.string().describe("Proposed new description"),
|
|
5116
|
+
reasoning: z5.string()
|
|
5117
|
+
},
|
|
5118
|
+
async (args) => {
|
|
5119
|
+
const result = collectRecommendation(
|
|
5120
|
+
"recommend_update_description",
|
|
5121
|
+
args,
|
|
5122
|
+
collector,
|
|
5123
|
+
onRecommendation
|
|
5124
|
+
);
|
|
5125
|
+
return { content: [{ type: "text", text: result }] };
|
|
5126
|
+
}
|
|
5127
|
+
),
|
|
5128
|
+
tool5(
|
|
5129
|
+
"recommend_context_link",
|
|
5130
|
+
"Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
|
|
5131
|
+
{
|
|
5132
|
+
tagId: z5.string(),
|
|
5133
|
+
tagName: z5.string(),
|
|
5134
|
+
linkType: z5.enum(["rule", "doc", "file", "folder"]),
|
|
5135
|
+
path: z5.string(),
|
|
5136
|
+
label: z5.string().optional(),
|
|
5137
|
+
reasoning: z5.string()
|
|
5138
|
+
},
|
|
5139
|
+
async (args) => {
|
|
5140
|
+
const result = collectRecommendation(
|
|
5141
|
+
"recommend_context_link",
|
|
5142
|
+
args,
|
|
5143
|
+
collector,
|
|
5144
|
+
onRecommendation
|
|
5145
|
+
);
|
|
5146
|
+
return { content: [{ type: "text", text: result }] };
|
|
5147
|
+
}
|
|
5148
|
+
),
|
|
5149
|
+
tool5(
|
|
5150
|
+
"flag_documentation_gap",
|
|
5151
|
+
"Flag a file that agents read heavily but has no tag documentation linked",
|
|
5152
|
+
{
|
|
5153
|
+
tagName: z5.string().describe("Tag whose agents read this file"),
|
|
5154
|
+
tagId: z5.string().optional(),
|
|
5155
|
+
filePath: z5.string(),
|
|
5156
|
+
readCount: z5.number(),
|
|
5157
|
+
suggestedAction: z5.string().describe("What doc or rule should be created"),
|
|
5158
|
+
reasoning: z5.string()
|
|
5159
|
+
},
|
|
5160
|
+
async (args) => {
|
|
5161
|
+
const result = collectRecommendation(
|
|
5162
|
+
"flag_documentation_gap",
|
|
5163
|
+
args,
|
|
5164
|
+
collector,
|
|
5165
|
+
onRecommendation
|
|
5166
|
+
);
|
|
5167
|
+
return { content: [{ type: "text", text: result }] };
|
|
5168
|
+
}
|
|
5169
|
+
),
|
|
5170
|
+
tool5(
|
|
5171
|
+
"recommend_merge_tags",
|
|
5172
|
+
"Recommend merging one tag into another",
|
|
5173
|
+
{
|
|
5174
|
+
tagId: z5.string().describe("Tag ID to be merged (removed after merge)"),
|
|
5175
|
+
tagName: z5.string().describe("Name of the tag to be merged"),
|
|
5176
|
+
mergeIntoTagId: z5.string().describe("Tag ID to merge into (kept)"),
|
|
5177
|
+
mergeIntoTagName: z5.string(),
|
|
5178
|
+
reasoning: z5.string()
|
|
5179
|
+
},
|
|
5180
|
+
async (args) => {
|
|
5181
|
+
const result = collectRecommendation(
|
|
5182
|
+
"recommend_merge_tags",
|
|
5183
|
+
args,
|
|
5184
|
+
collector,
|
|
5185
|
+
onRecommendation
|
|
5186
|
+
);
|
|
5187
|
+
return { content: [{ type: "text", text: result }] };
|
|
5188
|
+
}
|
|
5189
|
+
),
|
|
5190
|
+
tool5(
|
|
5191
|
+
"recommend_rename_tag",
|
|
5192
|
+
"Recommend renaming a tag",
|
|
5193
|
+
{
|
|
5194
|
+
tagId: z5.string(),
|
|
5195
|
+
tagName: z5.string().describe("Current tag name"),
|
|
5196
|
+
newName: z5.string().describe("Proposed new name"),
|
|
5197
|
+
reasoning: z5.string()
|
|
5198
|
+
},
|
|
5199
|
+
async (args) => {
|
|
5200
|
+
const result = collectRecommendation(
|
|
5201
|
+
"recommend_rename_tag",
|
|
5202
|
+
args,
|
|
5203
|
+
collector,
|
|
5204
|
+
onRecommendation
|
|
5205
|
+
);
|
|
5206
|
+
return { content: [{ type: "text", text: result }] };
|
|
5207
|
+
}
|
|
5208
|
+
),
|
|
5209
|
+
tool5(
|
|
5210
|
+
"complete_audit",
|
|
5211
|
+
"Signal that the audit is complete with a summary of all findings",
|
|
5212
|
+
{ summary: z5.string().describe("Brief overview of all findings") },
|
|
5213
|
+
async (args) => {
|
|
5214
|
+
collector.complete = true;
|
|
5215
|
+
collector.summary = args.summary ?? "Audit completed.";
|
|
5216
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
|
|
5217
|
+
}
|
|
5218
|
+
)
|
|
5219
|
+
];
|
|
5220
|
+
return createSdkMcpServer3({
|
|
5221
|
+
name: "tag-audit",
|
|
5222
|
+
tools: auditTools
|
|
5223
|
+
});
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
// src/runner/project-audit-handler.ts
|
|
5227
|
+
var logger7 = createServiceLogger("ProjectAudit");
|
|
5228
|
+
var FALLBACK_MODEL2 = "claude-sonnet-4-20250514";
|
|
5229
|
+
function buildTagSection(tags) {
|
|
5230
|
+
if (tags.length === 0) return "No tags configured yet.";
|
|
5231
|
+
return tags.map((t) => {
|
|
5232
|
+
const paths = t.contextPaths ?? [];
|
|
5233
|
+
const pathStr = paths.length > 0 ? `
|
|
5234
|
+
Context links: ${paths.map((p) => `${p.type}:${p.path}`).join(", ")}` : "";
|
|
5235
|
+
return ` - ${t.name} (id: ${t.id})${t.description ? `: ${t.description}` : " [no description]"}${pathStr}
|
|
5236
|
+
Active tasks: ${t.activeTaskCount}`;
|
|
5237
|
+
}).join("\n");
|
|
5238
|
+
}
|
|
5239
|
+
function buildHeatmapSection(entries) {
|
|
5240
|
+
if (entries.length === 0) return "No file read analytics data available.";
|
|
5241
|
+
return entries.slice(0, 50).map((e) => {
|
|
5242
|
+
const tagBreakdown = Object.entries(e.byTag).sort(([, a], [, b]) => b - a).map(([tag, count]) => `${tag}:${count}`).join(", ");
|
|
5243
|
+
return ` ${e.filePath} \u2014 ${e.totalReads} reads${tagBreakdown ? ` (${tagBreakdown})` : ""}`;
|
|
5244
|
+
}).join("\n");
|
|
5245
|
+
}
|
|
5246
|
+
function buildAuditSystemPrompt(projectName, tags, heatmapData, projectDir) {
|
|
5247
|
+
return [
|
|
5248
|
+
"You are a project organization expert analyzing tag taxonomy for a software project.",
|
|
5249
|
+
"Tags are used to categorize tasks and link relevant documentation/rules/files to subsystems.",
|
|
5250
|
+
"",
|
|
5251
|
+
`PROJECT: ${projectName}`,
|
|
5252
|
+
"",
|
|
5253
|
+
`EXISTING TAGS (${tags.length}):`,
|
|
5254
|
+
buildTagSection(tags),
|
|
5255
|
+
"",
|
|
5256
|
+
"FILE READ ANALYTICS (what agents actually read, by tag):",
|
|
5257
|
+
buildHeatmapSection(heatmapData),
|
|
5258
|
+
"",
|
|
5259
|
+
`You have full access to the codebase at: ${projectDir}`,
|
|
5260
|
+
"Use your file reading and searching tools to understand the codebase structure,",
|
|
5261
|
+
"module boundaries, and architectural patterns before making recommendations.",
|
|
5262
|
+
"",
|
|
5263
|
+
"ANALYSIS TASKS:",
|
|
5264
|
+
"1. Read actual source files to understand code areas and module boundaries",
|
|
5265
|
+
"2. Search for imports, class definitions, and architectural patterns",
|
|
5266
|
+
"3. Coverage: Are all major subsystems/services represented by tags?",
|
|
5267
|
+
"4. Descriptions: Do all tags have clear, useful descriptions?",
|
|
5268
|
+
"5. Context Links: Are relevant rules/docs/folders linked to tags via contextPaths?",
|
|
5269
|
+
"6. Documentation Gaps: Which high-read files lack linked documentation?",
|
|
5270
|
+
"7. Cleanup: Any tags that should be merged, renamed, or removed?",
|
|
5271
|
+
"",
|
|
5272
|
+
"Use the tag-audit MCP tools to submit each recommendation.",
|
|
5273
|
+
"Call complete_audit when you are done with a thorough summary.",
|
|
5274
|
+
"Be comprehensive \u2014 recommend all improvements your analysis supports.",
|
|
5275
|
+
"Analyze actual file contents, not just file names."
|
|
5276
|
+
].join("\n");
|
|
5277
|
+
}
|
|
5278
|
+
function emitToolCallProgress(event, request, connection) {
|
|
5279
|
+
if (event.type !== "assistant") return;
|
|
5280
|
+
const assistantEvent = event;
|
|
5281
|
+
for (const block of assistantEvent.message.content) {
|
|
5282
|
+
if (block.type === "tool_use" && block.name) {
|
|
5283
|
+
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
5284
|
+
connection.emitAuditProgress({
|
|
5285
|
+
requestId: request.requestId,
|
|
5286
|
+
activity: {
|
|
5287
|
+
tool: block.name,
|
|
5288
|
+
input: inputStr.slice(0, 500),
|
|
5289
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5290
|
+
}
|
|
5291
|
+
});
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
async function runAuditQuery(request, connection, projectDir) {
|
|
5296
|
+
connection.emitAgentStatus("fetching_context");
|
|
5297
|
+
let agentCtx = null;
|
|
5298
|
+
try {
|
|
5299
|
+
agentCtx = await connection.fetchAgentContext();
|
|
5300
|
+
} catch {
|
|
5301
|
+
logger7.warn("Could not fetch agent context for audit, using defaults");
|
|
5302
|
+
}
|
|
5303
|
+
connection.emitAgentStatus("running");
|
|
5304
|
+
const model = agentCtx?.model || FALLBACK_MODEL2;
|
|
5305
|
+
const settings = agentCtx?.agentSettings ?? {};
|
|
5306
|
+
const collector = {
|
|
5307
|
+
recommendations: [],
|
|
5308
|
+
summary: "Audit completed.",
|
|
5309
|
+
complete: false
|
|
5310
|
+
};
|
|
5311
|
+
const onRecommendation = (rec) => {
|
|
5312
|
+
connection.emitEvent({
|
|
5313
|
+
type: "audit_recommendation",
|
|
5314
|
+
tagName: rec.tagName,
|
|
5315
|
+
recommendationType: rec.type
|
|
5316
|
+
});
|
|
5317
|
+
};
|
|
5318
|
+
const systemPrompt = buildAuditSystemPrompt(
|
|
5319
|
+
request.projectName,
|
|
5320
|
+
request.tags,
|
|
5321
|
+
request.fileHeatmap,
|
|
5322
|
+
projectDir
|
|
5323
|
+
);
|
|
5324
|
+
const userPrompt = [
|
|
5325
|
+
"Analyze the project's tag taxonomy and submit recommendations using the tag-audit MCP tools.",
|
|
5326
|
+
`There are currently ${request.tags.length} tags configured.`,
|
|
5327
|
+
request.fileHeatmap.length > 0 ? `File analytics show ${request.fileHeatmap.length} files with read activity.` : "No file read analytics available.",
|
|
5328
|
+
"",
|
|
5329
|
+
"Start by exploring the codebase structure, then analyze each tag for accuracy and completeness.",
|
|
5330
|
+
"Call complete_audit when done."
|
|
5331
|
+
].join("\n");
|
|
5332
|
+
const events = query3({
|
|
5333
|
+
prompt: userPrompt,
|
|
5334
|
+
options: {
|
|
5335
|
+
model,
|
|
5336
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: systemPrompt },
|
|
5337
|
+
cwd: projectDir,
|
|
5338
|
+
permissionMode: "bypassPermissions",
|
|
5339
|
+
allowDangerouslySkipPermissions: true,
|
|
5340
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
5341
|
+
mcpServers: { "tag-audit": createAuditMcpServer(collector, onRecommendation) },
|
|
5342
|
+
maxTurns: settings.maxTurns ?? 75,
|
|
5343
|
+
maxBudgetUsd: settings.maxBudgetUsd ?? 5,
|
|
5344
|
+
effort: settings.effort,
|
|
5345
|
+
thinking: settings.thinking
|
|
5346
|
+
}
|
|
5347
|
+
});
|
|
5348
|
+
const responseParts = [];
|
|
5349
|
+
const turnToolCalls = [];
|
|
5350
|
+
const isTyping = { value: false };
|
|
5351
|
+
for await (const event of events) {
|
|
5352
|
+
emitToolCallProgress(event, request, connection);
|
|
5353
|
+
const done = processEventStream(event, connection, responseParts, turnToolCalls, isTyping);
|
|
5354
|
+
if (done) break;
|
|
5355
|
+
}
|
|
5356
|
+
if (isTyping.value) {
|
|
5357
|
+
connection.emitEvent({ type: "agent_typing_stop" });
|
|
5358
|
+
}
|
|
5359
|
+
return collector;
|
|
5360
|
+
}
|
|
5361
|
+
async function handleProjectAuditRequest(request, connection, projectDir) {
|
|
5362
|
+
connection.emitAgentStatus("running");
|
|
5363
|
+
try {
|
|
5364
|
+
const collector = await runAuditQuery(request, connection, projectDir);
|
|
5365
|
+
const result = {
|
|
5366
|
+
recommendations: collector.recommendations,
|
|
5367
|
+
summary: collector.summary,
|
|
5368
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5369
|
+
};
|
|
5370
|
+
logger7.info("Tag audit completed", {
|
|
5371
|
+
requestId: request.requestId,
|
|
5372
|
+
recommendationCount: result.recommendations.length
|
|
5373
|
+
});
|
|
5374
|
+
connection.emitAuditResult({ requestId: request.requestId, result });
|
|
5375
|
+
} catch (error) {
|
|
5376
|
+
logger7.error("Tag audit failed", {
|
|
5377
|
+
requestId: request.requestId,
|
|
5378
|
+
...errorMeta(error)
|
|
5379
|
+
});
|
|
5380
|
+
connection.emitAuditResult({
|
|
5381
|
+
requestId: request.requestId,
|
|
5382
|
+
error: error instanceof Error ? error.message : "Tag audit failed"
|
|
5383
|
+
});
|
|
4040
5384
|
} finally {
|
|
4041
5385
|
connection.emitAgentStatus("idle");
|
|
4042
5386
|
}
|
|
4043
5387
|
}
|
|
4044
5388
|
|
|
4045
5389
|
// src/runner/project-runner.ts
|
|
4046
|
-
var
|
|
5390
|
+
var logger8 = createServiceLogger("ProjectRunner");
|
|
4047
5391
|
var __filename = fileURLToPath(import.meta.url);
|
|
4048
5392
|
var __dirname = path.dirname(__filename);
|
|
4049
5393
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
@@ -4061,13 +5405,20 @@ function setupWorkDir(projectDir, assignment) {
|
|
|
4061
5405
|
workDir = projectDir;
|
|
4062
5406
|
}
|
|
4063
5407
|
if (branch && branch !== devBranch) {
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
5408
|
+
if (hasUncommittedChanges(workDir)) {
|
|
5409
|
+
logger8.warn("Uncommitted changes in work dir, skipping checkout", {
|
|
5410
|
+
taskId: shortId,
|
|
5411
|
+
branch
|
|
5412
|
+
});
|
|
5413
|
+
} else {
|
|
4067
5414
|
try {
|
|
4068
|
-
|
|
5415
|
+
execSync7(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
4069
5416
|
} catch {
|
|
4070
|
-
|
|
5417
|
+
try {
|
|
5418
|
+
execSync7(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
5419
|
+
} catch {
|
|
5420
|
+
logger8.warn("Could not checkout branch", { taskId: shortId, branch });
|
|
5421
|
+
}
|
|
4071
5422
|
}
|
|
4072
5423
|
}
|
|
4073
5424
|
}
|
|
@@ -4106,13 +5457,13 @@ function spawnChildAgent(assignment, workDir) {
|
|
|
4106
5457
|
child.stdout?.on("data", (data) => {
|
|
4107
5458
|
const lines = data.toString().trimEnd().split("\n");
|
|
4108
5459
|
for (const line of lines) {
|
|
4109
|
-
|
|
5460
|
+
logger8.info(line, { taskId: shortId });
|
|
4110
5461
|
}
|
|
4111
5462
|
});
|
|
4112
5463
|
child.stderr?.on("data", (data) => {
|
|
4113
5464
|
const lines = data.toString().trimEnd().split("\n");
|
|
4114
5465
|
for (const line of lines) {
|
|
4115
|
-
|
|
5466
|
+
logger8.error(line, { taskId: shortId });
|
|
4116
5467
|
}
|
|
4117
5468
|
});
|
|
4118
5469
|
return child;
|
|
@@ -4124,27 +5475,71 @@ var ProjectRunner = class {
|
|
|
4124
5475
|
heartbeatTimer = null;
|
|
4125
5476
|
stopping = false;
|
|
4126
5477
|
resolveLifecycle = null;
|
|
5478
|
+
chatSessionIds = /* @__PURE__ */ new Map();
|
|
4127
5479
|
// Start command process management
|
|
4128
5480
|
startCommandChild = null;
|
|
4129
5481
|
startCommandRunning = false;
|
|
4130
5482
|
setupComplete = false;
|
|
5483
|
+
branchSwitchCommand;
|
|
5484
|
+
commitWatcher;
|
|
4131
5485
|
constructor(config) {
|
|
4132
5486
|
this.projectDir = config.projectDir;
|
|
4133
5487
|
this.connection = new ProjectConnection({
|
|
4134
5488
|
apiUrl: config.conveyorApiUrl,
|
|
4135
5489
|
projectToken: config.projectToken,
|
|
4136
|
-
projectId: config.projectId
|
|
5490
|
+
projectId: config.projectId,
|
|
5491
|
+
projectDir: config.projectDir
|
|
4137
5492
|
});
|
|
5493
|
+
this.commitWatcher = new CommitWatcher(
|
|
5494
|
+
{
|
|
5495
|
+
projectDir: this.projectDir,
|
|
5496
|
+
pollIntervalMs: Number(process.env.CONVEYOR_COMMIT_POLL_INTERVAL) || 1e4,
|
|
5497
|
+
debounceMs: 3e3
|
|
5498
|
+
},
|
|
5499
|
+
{
|
|
5500
|
+
onNewCommits: async (data) => {
|
|
5501
|
+
this.connection.emitNewCommitsDetected({
|
|
5502
|
+
branch: data.branch,
|
|
5503
|
+
commitCount: data.commitCount,
|
|
5504
|
+
latestCommit: {
|
|
5505
|
+
sha: data.newCommitSha,
|
|
5506
|
+
message: data.latestMessage,
|
|
5507
|
+
author: data.latestAuthor
|
|
5508
|
+
},
|
|
5509
|
+
autoSyncing: true
|
|
5510
|
+
});
|
|
5511
|
+
const startTime = Date.now();
|
|
5512
|
+
const stepsRun = await this.smartSync(data.previousSha, data.newCommitSha, data.branch);
|
|
5513
|
+
this.connection.emitEnvironmentReady({
|
|
5514
|
+
branch: data.branch,
|
|
5515
|
+
commitsSynced: data.commitCount,
|
|
5516
|
+
syncDurationMs: Date.now() - startTime,
|
|
5517
|
+
stepsRun
|
|
5518
|
+
});
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
);
|
|
4138
5522
|
}
|
|
4139
5523
|
checkoutWorkspaceBranch() {
|
|
4140
5524
|
const workspaceBranch = process.env.CONVEYOR_WORKSPACE_BRANCH;
|
|
4141
5525
|
if (!workspaceBranch) return;
|
|
5526
|
+
const currentBranch = this.getCurrentBranch();
|
|
5527
|
+
if (currentBranch === workspaceBranch) {
|
|
5528
|
+
logger8.info("Already on workspace branch", { workspaceBranch });
|
|
5529
|
+
return;
|
|
5530
|
+
}
|
|
5531
|
+
if (hasUncommittedChanges(this.projectDir)) {
|
|
5532
|
+
logger8.warn("Uncommitted changes detected, skipping workspace branch checkout", {
|
|
5533
|
+
workspaceBranch
|
|
5534
|
+
});
|
|
5535
|
+
return;
|
|
5536
|
+
}
|
|
4142
5537
|
try {
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5538
|
+
execSync7(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
5539
|
+
execSync7(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
5540
|
+
logger8.info("Checked out workspace branch", { workspaceBranch });
|
|
4146
5541
|
} catch (err) {
|
|
4147
|
-
|
|
5542
|
+
logger8.warn("Failed to checkout workspace branch, continuing on current branch", {
|
|
4148
5543
|
workspaceBranch,
|
|
4149
5544
|
...errorMeta(err)
|
|
4150
5545
|
});
|
|
@@ -4153,15 +5548,15 @@ var ProjectRunner = class {
|
|
|
4153
5548
|
async executeSetupCommand() {
|
|
4154
5549
|
const cmd = process.env.CONVEYOR_SETUP_COMMAND;
|
|
4155
5550
|
if (!cmd) return;
|
|
4156
|
-
|
|
5551
|
+
logger8.info("Running setup command", { command: cmd });
|
|
4157
5552
|
try {
|
|
4158
5553
|
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
4159
5554
|
this.connection.emitEvent({ type: "setup_output", stream, data });
|
|
4160
5555
|
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
4161
5556
|
});
|
|
4162
|
-
|
|
5557
|
+
logger8.info("Setup command completed");
|
|
4163
5558
|
} catch (error) {
|
|
4164
|
-
|
|
5559
|
+
logger8.error("Setup command failed", errorMeta(error));
|
|
4165
5560
|
this.connection.emitEvent({
|
|
4166
5561
|
type: "setup_error",
|
|
4167
5562
|
message: error instanceof Error ? error.message : "Setup command failed"
|
|
@@ -4172,7 +5567,7 @@ var ProjectRunner = class {
|
|
|
4172
5567
|
executeStartCommand() {
|
|
4173
5568
|
const cmd = process.env.CONVEYOR_START_COMMAND;
|
|
4174
5569
|
if (!cmd) return;
|
|
4175
|
-
|
|
5570
|
+
logger8.info("Running start command", { command: cmd });
|
|
4176
5571
|
const child = runStartCommand(cmd, this.projectDir, (stream, data) => {
|
|
4177
5572
|
this.connection.emitEvent({ type: "start_command_output", stream, data });
|
|
4178
5573
|
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
@@ -4182,7 +5577,7 @@ var ProjectRunner = class {
|
|
|
4182
5577
|
child.on("exit", (code, signal) => {
|
|
4183
5578
|
this.startCommandRunning = false;
|
|
4184
5579
|
this.startCommandChild = null;
|
|
4185
|
-
|
|
5580
|
+
logger8.info("Start command exited", { code, signal });
|
|
4186
5581
|
this.connection.emitEvent({
|
|
4187
5582
|
type: "start_command_exited",
|
|
4188
5583
|
code,
|
|
@@ -4193,13 +5588,13 @@ var ProjectRunner = class {
|
|
|
4193
5588
|
child.on("error", (err) => {
|
|
4194
5589
|
this.startCommandRunning = false;
|
|
4195
5590
|
this.startCommandChild = null;
|
|
4196
|
-
|
|
5591
|
+
logger8.error("Start command error", errorMeta(err));
|
|
4197
5592
|
});
|
|
4198
5593
|
}
|
|
4199
5594
|
async killStartCommand() {
|
|
4200
5595
|
const child = this.startCommandChild;
|
|
4201
5596
|
if (!child || !this.startCommandRunning) return;
|
|
4202
|
-
|
|
5597
|
+
logger8.info("Killing start command");
|
|
4203
5598
|
try {
|
|
4204
5599
|
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
4205
5600
|
} catch {
|
|
@@ -4229,21 +5624,177 @@ var ProjectRunner = class {
|
|
|
4229
5624
|
this.executeStartCommand();
|
|
4230
5625
|
}
|
|
4231
5626
|
getEnvironmentStatus() {
|
|
4232
|
-
let currentBranch = "unknown";
|
|
4233
|
-
try {
|
|
4234
|
-
currentBranch = execSync5("git branch --show-current", {
|
|
4235
|
-
cwd: this.projectDir,
|
|
4236
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4237
|
-
}).toString().trim();
|
|
4238
|
-
} catch {
|
|
4239
|
-
}
|
|
4240
5627
|
return {
|
|
4241
5628
|
setupComplete: this.setupComplete,
|
|
4242
5629
|
startCommandRunning: this.startCommandRunning,
|
|
4243
|
-
currentBranch,
|
|
5630
|
+
currentBranch: this.getCurrentBranch() ?? "unknown",
|
|
4244
5631
|
previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || null
|
|
4245
5632
|
};
|
|
4246
5633
|
}
|
|
5634
|
+
getCurrentBranch() {
|
|
5635
|
+
return getCurrentBranch(this.projectDir);
|
|
5636
|
+
}
|
|
5637
|
+
// oxlint-disable-next-line max-lines-per-function, complexity -- sequential sync steps with per-step error handling
|
|
5638
|
+
async smartSync(previousSha, newSha, branch) {
|
|
5639
|
+
const stepsRun = [];
|
|
5640
|
+
if (hasUncommittedChanges(this.projectDir)) {
|
|
5641
|
+
this.connection.emitEvent({
|
|
5642
|
+
type: "commit_watch_warning",
|
|
5643
|
+
message: "Working tree has uncommitted changes. Auto-pull skipped."
|
|
5644
|
+
});
|
|
5645
|
+
return ["skipped:dirty_tree"];
|
|
5646
|
+
}
|
|
5647
|
+
await this.killStartCommand();
|
|
5648
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "running" });
|
|
5649
|
+
try {
|
|
5650
|
+
execSync7(`git pull origin ${branch}`, {
|
|
5651
|
+
cwd: this.projectDir,
|
|
5652
|
+
stdio: "pipe",
|
|
5653
|
+
timeout: 6e4
|
|
5654
|
+
});
|
|
5655
|
+
stepsRun.push("pull");
|
|
5656
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "success" });
|
|
5657
|
+
} catch (err) {
|
|
5658
|
+
const message = err instanceof Error ? err.message : "Pull failed";
|
|
5659
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "error", message });
|
|
5660
|
+
logger8.error("Git pull failed during commit sync", errorMeta(err));
|
|
5661
|
+
this.executeStartCommand();
|
|
5662
|
+
return ["error:pull"];
|
|
5663
|
+
}
|
|
5664
|
+
let changedFiles = [];
|
|
5665
|
+
try {
|
|
5666
|
+
changedFiles = execSync7(`git diff --name-only ${previousSha}..${newSha}`, {
|
|
5667
|
+
cwd: this.projectDir,
|
|
5668
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5669
|
+
}).toString().trim().split("\n").filter(Boolean);
|
|
5670
|
+
} catch {
|
|
5671
|
+
}
|
|
5672
|
+
const needsInstall = changedFiles.some(
|
|
5673
|
+
(f) => f === "package.json" || f === "bun.lockb" || f === "bunfig.toml" || f.endsWith("/package.json") || f.endsWith("/bun.lockb")
|
|
5674
|
+
);
|
|
5675
|
+
const needsPrisma = changedFiles.some(
|
|
5676
|
+
(f) => f.includes("prisma/schema.prisma") || f.includes("prisma/migrations/")
|
|
5677
|
+
);
|
|
5678
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5679
|
+
if (cmd && (needsInstall || needsPrisma)) {
|
|
5680
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
5681
|
+
try {
|
|
5682
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
5683
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
5684
|
+
});
|
|
5685
|
+
stepsRun.push("branchSwitchCommand");
|
|
5686
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
5687
|
+
} catch (err) {
|
|
5688
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
5689
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
5690
|
+
logger8.error("Branch switch command failed during commit sync", errorMeta(err));
|
|
5691
|
+
}
|
|
5692
|
+
} else if (!cmd) {
|
|
5693
|
+
if (needsInstall) {
|
|
5694
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "running" });
|
|
5695
|
+
try {
|
|
5696
|
+
execSync7("bun install", { cwd: this.projectDir, timeout: 12e4, stdio: "pipe" });
|
|
5697
|
+
stepsRun.push("install");
|
|
5698
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "success" });
|
|
5699
|
+
} catch (err) {
|
|
5700
|
+
const message = err instanceof Error ? err.message : "Install failed";
|
|
5701
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "error", message });
|
|
5702
|
+
logger8.error("bun install failed during commit sync", errorMeta(err));
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
if (needsPrisma) {
|
|
5706
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "running" });
|
|
5707
|
+
try {
|
|
5708
|
+
execSync7("bunx prisma generate", {
|
|
5709
|
+
cwd: this.projectDir,
|
|
5710
|
+
timeout: 6e4,
|
|
5711
|
+
stdio: "pipe"
|
|
5712
|
+
});
|
|
5713
|
+
execSync7("bunx prisma db push --accept-data-loss", {
|
|
5714
|
+
cwd: this.projectDir,
|
|
5715
|
+
timeout: 6e4,
|
|
5716
|
+
stdio: "pipe"
|
|
5717
|
+
});
|
|
5718
|
+
stepsRun.push("prisma");
|
|
5719
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "success" });
|
|
5720
|
+
} catch (err) {
|
|
5721
|
+
const message = err instanceof Error ? err.message : "Prisma sync failed";
|
|
5722
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "error", message });
|
|
5723
|
+
logger8.error("Prisma sync failed during commit sync", errorMeta(err));
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
this.executeStartCommand();
|
|
5728
|
+
stepsRun.push("startCommand");
|
|
5729
|
+
return stepsRun;
|
|
5730
|
+
}
|
|
5731
|
+
async handleSwitchBranch(data, callback) {
|
|
5732
|
+
const { branch, syncAfter } = data;
|
|
5733
|
+
try {
|
|
5734
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "running" });
|
|
5735
|
+
try {
|
|
5736
|
+
execSync7("git fetch origin", { cwd: this.projectDir, stdio: "pipe" });
|
|
5737
|
+
} catch {
|
|
5738
|
+
logger8.warn("Git fetch failed during branch switch");
|
|
5739
|
+
}
|
|
5740
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "success" });
|
|
5741
|
+
detachWorktreeBranch(this.projectDir, branch);
|
|
5742
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "running" });
|
|
5743
|
+
try {
|
|
5744
|
+
execSync7(`git checkout ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
5745
|
+
} catch (err) {
|
|
5746
|
+
const message = err instanceof Error ? err.message : "Checkout failed";
|
|
5747
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "error", message });
|
|
5748
|
+
callback({ ok: false, error: `Failed to checkout branch: ${message}` });
|
|
5749
|
+
return;
|
|
5750
|
+
}
|
|
5751
|
+
try {
|
|
5752
|
+
execSync7(`git pull origin ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
5753
|
+
} catch {
|
|
5754
|
+
logger8.warn("Git pull failed during branch switch", { branch });
|
|
5755
|
+
}
|
|
5756
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "success" });
|
|
5757
|
+
if (syncAfter !== false) {
|
|
5758
|
+
await this.handleSyncEnvironment();
|
|
5759
|
+
}
|
|
5760
|
+
this.commitWatcher.start(branch);
|
|
5761
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
5762
|
+
} catch (err) {
|
|
5763
|
+
const message = err instanceof Error ? err.message : "Branch switch failed";
|
|
5764
|
+
logger8.error("Branch switch failed", errorMeta(err));
|
|
5765
|
+
callback({ ok: false, error: message });
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
async handleSyncEnvironment(callback) {
|
|
5769
|
+
try {
|
|
5770
|
+
await this.killStartCommand();
|
|
5771
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5772
|
+
if (cmd) {
|
|
5773
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
5774
|
+
try {
|
|
5775
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
5776
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
5777
|
+
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
5778
|
+
});
|
|
5779
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
5780
|
+
} catch (err) {
|
|
5781
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
5782
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
5783
|
+
logger8.error("Branch switch sync command failed", errorMeta(err));
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
this.executeStartCommand();
|
|
5787
|
+
this.connection.emitEnvSwitchProgress({ step: "startCommand", status: "success" });
|
|
5788
|
+
callback?.({ ok: true, data: this.getEnvironmentStatus() });
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
const message = err instanceof Error ? err.message : "Sync failed";
|
|
5791
|
+
logger8.error("Environment sync failed", errorMeta(err));
|
|
5792
|
+
callback?.({ ok: false, error: message });
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
handleGetEnvStatus(callback) {
|
|
5796
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
5797
|
+
}
|
|
4247
5798
|
async start() {
|
|
4248
5799
|
this.checkoutWorkspaceBranch();
|
|
4249
5800
|
await this.connection.connect();
|
|
@@ -4257,7 +5808,7 @@ var ProjectRunner = class {
|
|
|
4257
5808
|
startCommandRunning: this.startCommandRunning
|
|
4258
5809
|
});
|
|
4259
5810
|
} catch (error) {
|
|
4260
|
-
|
|
5811
|
+
logger8.error("Environment setup failed", errorMeta(error));
|
|
4261
5812
|
this.setupComplete = false;
|
|
4262
5813
|
}
|
|
4263
5814
|
this.connection.onTaskAssignment((assignment) => {
|
|
@@ -4267,17 +5818,53 @@ var ProjectRunner = class {
|
|
|
4267
5818
|
this.handleStopTask(data.taskId);
|
|
4268
5819
|
});
|
|
4269
5820
|
this.connection.onShutdown(() => {
|
|
4270
|
-
|
|
5821
|
+
logger8.info("Received shutdown signal from server");
|
|
4271
5822
|
void this.stop();
|
|
4272
5823
|
});
|
|
4273
5824
|
this.connection.onChatMessage((msg) => {
|
|
4274
|
-
|
|
4275
|
-
|
|
5825
|
+
logger8.debug("Received project chat message");
|
|
5826
|
+
const chatId = msg.chatId ?? "default";
|
|
5827
|
+
const existingSessionId = this.chatSessionIds.get(chatId);
|
|
5828
|
+
void handleProjectChatMessage(msg, this.connection, this.projectDir, existingSessionId).then(
|
|
5829
|
+
(newSessionId) => {
|
|
5830
|
+
if (newSessionId) {
|
|
5831
|
+
this.chatSessionIds.set(chatId, newSessionId);
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
);
|
|
4276
5835
|
});
|
|
5836
|
+
this.connection.onAuditRequest((request) => {
|
|
5837
|
+
logger8.debug("Received tag audit request", { requestId: request.requestId });
|
|
5838
|
+
void handleProjectAuditRequest(request, this.connection, this.projectDir);
|
|
5839
|
+
});
|
|
5840
|
+
this.connection.onSwitchBranch = (data, cb) => {
|
|
5841
|
+
void this.handleSwitchBranch(data, cb);
|
|
5842
|
+
};
|
|
5843
|
+
this.connection.onSyncEnvironment = (cb) => {
|
|
5844
|
+
void this.handleSyncEnvironment(cb);
|
|
5845
|
+
};
|
|
5846
|
+
this.connection.onGetEnvStatus = (cb) => {
|
|
5847
|
+
this.handleGetEnvStatus(cb);
|
|
5848
|
+
};
|
|
5849
|
+
this.connection.onRestartStartCommand = (cb) => {
|
|
5850
|
+
void this.restartStartCommand().then(() => cb({ ok: true })).catch(
|
|
5851
|
+
(err) => cb({ ok: false, error: err instanceof Error ? err.message : "Restart failed" })
|
|
5852
|
+
);
|
|
5853
|
+
};
|
|
5854
|
+
try {
|
|
5855
|
+
const context = await this.connection.fetchAgentContext();
|
|
5856
|
+
this.branchSwitchCommand = context?.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5857
|
+
} catch {
|
|
5858
|
+
this.branchSwitchCommand = process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
5859
|
+
}
|
|
4277
5860
|
this.heartbeatTimer = setInterval(() => {
|
|
4278
5861
|
this.connection.sendHeartbeat();
|
|
4279
5862
|
}, HEARTBEAT_INTERVAL_MS2);
|
|
4280
|
-
|
|
5863
|
+
const currentBranch = this.getCurrentBranch();
|
|
5864
|
+
if (currentBranch) {
|
|
5865
|
+
this.commitWatcher.start(currentBranch);
|
|
5866
|
+
}
|
|
5867
|
+
logger8.info("Connected, waiting for task assignments");
|
|
4281
5868
|
await new Promise((resolve2) => {
|
|
4282
5869
|
this.resolveLifecycle = resolve2;
|
|
4283
5870
|
process.on("SIGTERM", () => void this.stop());
|
|
@@ -4288,11 +5875,11 @@ var ProjectRunner = class {
|
|
|
4288
5875
|
const { taskId, mode } = assignment;
|
|
4289
5876
|
const shortId = taskId.slice(0, 8);
|
|
4290
5877
|
if (this.activeAgents.has(taskId)) {
|
|
4291
|
-
|
|
5878
|
+
logger8.info("Task already running, skipping", { taskId: shortId });
|
|
4292
5879
|
return;
|
|
4293
5880
|
}
|
|
4294
5881
|
if (this.activeAgents.size >= MAX_CONCURRENT) {
|
|
4295
|
-
|
|
5882
|
+
logger8.warn("Max concurrent agents reached, rejecting task", {
|
|
4296
5883
|
maxConcurrent: MAX_CONCURRENT,
|
|
4297
5884
|
taskId: shortId
|
|
4298
5885
|
});
|
|
@@ -4301,9 +5888,9 @@ var ProjectRunner = class {
|
|
|
4301
5888
|
}
|
|
4302
5889
|
try {
|
|
4303
5890
|
try {
|
|
4304
|
-
|
|
5891
|
+
execSync7("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
|
|
4305
5892
|
} catch {
|
|
4306
|
-
|
|
5893
|
+
logger8.warn("Git fetch failed", { taskId: shortId });
|
|
4307
5894
|
}
|
|
4308
5895
|
const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
|
|
4309
5896
|
const child = spawnChildAgent(assignment, workDir);
|
|
@@ -4314,12 +5901,12 @@ var ProjectRunner = class {
|
|
|
4314
5901
|
usesWorktree
|
|
4315
5902
|
});
|
|
4316
5903
|
this.connection.emitTaskStarted(taskId);
|
|
4317
|
-
|
|
5904
|
+
logger8.info("Started task", { taskId: shortId, mode, workDir });
|
|
4318
5905
|
child.on("exit", (code) => {
|
|
4319
5906
|
this.activeAgents.delete(taskId);
|
|
4320
5907
|
const reason = code === 0 ? "completed" : `exited with code ${code}`;
|
|
4321
5908
|
this.connection.emitTaskStopped(taskId, reason);
|
|
4322
|
-
|
|
5909
|
+
logger8.info("Task exited", { taskId: shortId, reason });
|
|
4323
5910
|
if (code === 0 && usesWorktree) {
|
|
4324
5911
|
try {
|
|
4325
5912
|
removeWorktree(this.projectDir, taskId);
|
|
@@ -4328,7 +5915,7 @@ var ProjectRunner = class {
|
|
|
4328
5915
|
}
|
|
4329
5916
|
});
|
|
4330
5917
|
} catch (error) {
|
|
4331
|
-
|
|
5918
|
+
logger8.error("Failed to start task", {
|
|
4332
5919
|
taskId: shortId,
|
|
4333
5920
|
...errorMeta(error)
|
|
4334
5921
|
});
|
|
@@ -4342,7 +5929,7 @@ var ProjectRunner = class {
|
|
|
4342
5929
|
const agent = this.activeAgents.get(taskId);
|
|
4343
5930
|
if (!agent) return;
|
|
4344
5931
|
const shortId = taskId.slice(0, 8);
|
|
4345
|
-
|
|
5932
|
+
logger8.info("Stopping task", { taskId: shortId });
|
|
4346
5933
|
agent.process.kill("SIGTERM");
|
|
4347
5934
|
const timer = setTimeout(() => {
|
|
4348
5935
|
if (this.activeAgents.has(taskId)) {
|
|
@@ -4362,7 +5949,8 @@ var ProjectRunner = class {
|
|
|
4362
5949
|
async stop() {
|
|
4363
5950
|
if (this.stopping) return;
|
|
4364
5951
|
this.stopping = true;
|
|
4365
|
-
|
|
5952
|
+
logger8.info("Shutting down");
|
|
5953
|
+
this.commitWatcher.stop();
|
|
4366
5954
|
await this.killStartCommand();
|
|
4367
5955
|
if (this.heartbeatTimer) {
|
|
4368
5956
|
clearInterval(this.heartbeatTimer);
|
|
@@ -4388,7 +5976,7 @@ var ProjectRunner = class {
|
|
|
4388
5976
|
})
|
|
4389
5977
|
]);
|
|
4390
5978
|
this.connection.disconnect();
|
|
4391
|
-
|
|
5979
|
+
logger8.info("Shutdown complete");
|
|
4392
5980
|
if (this.resolveLifecycle) {
|
|
4393
5981
|
this.resolveLifecycle();
|
|
4394
5982
|
this.resolveLifecycle = null;
|
|
@@ -4474,4 +6062,4 @@ export {
|
|
|
4474
6062
|
ProjectRunner,
|
|
4475
6063
|
FileCache
|
|
4476
6064
|
};
|
|
4477
|
-
//# sourceMappingURL=chunk-
|
|
6065
|
+
//# sourceMappingURL=chunk-SL5MRNSI.js.map
|