@rallycry/conveyor-agent 6.0.2 → 6.0.4
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-NKZSUGND.js} +3113 -257
- package/dist/chunk-NKZSUGND.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +368 -4
- package/dist/index.js +1260 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-HYWZJYPW.js.map +0 -1
|
@@ -179,6 +179,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
179
179
|
constructor(config) {
|
|
180
180
|
this.config = config;
|
|
181
181
|
}
|
|
182
|
+
// oxlint-disable-next-line max-lines-per-function -- socket setup requires registering many event handlers
|
|
182
183
|
connect() {
|
|
183
184
|
return new Promise((resolve2, reject) => {
|
|
184
185
|
let settled = false;
|
|
@@ -220,16 +221,42 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
220
221
|
this.socket.on("agentRunner:runStartCommand", () => {
|
|
221
222
|
if (this.runStartCommandCallback) this.runStartCommandCallback();
|
|
222
223
|
});
|
|
224
|
+
this.socket.on("agentRunner:wake", () => {
|
|
225
|
+
if (this.chatMessageCallback) {
|
|
226
|
+
this.chatMessageCallback({ content: "", userId: "system" });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.socket.on("agentRunner:updateApiKey", (data) => {
|
|
230
|
+
process.env.ANTHROPIC_API_KEY = data.apiKey;
|
|
231
|
+
});
|
|
223
232
|
this.socket.on(
|
|
224
233
|
"agentRunner:runAuthTokenCommand",
|
|
225
234
|
(data, cb) => this.handleRunAuthTokenCommand(data.userEmail, cb)
|
|
226
235
|
);
|
|
227
236
|
this.socket.on("connect", () => {
|
|
237
|
+
process.stderr.write(`[conveyor] Socket connected
|
|
238
|
+
`);
|
|
228
239
|
if (!settled) {
|
|
229
240
|
settled = true;
|
|
230
241
|
resolve2();
|
|
231
242
|
}
|
|
232
243
|
});
|
|
244
|
+
this.socket.on("connect_error", (err) => {
|
|
245
|
+
process.stderr.write(`[conveyor] Socket connection error: ${err.message}
|
|
246
|
+
`);
|
|
247
|
+
});
|
|
248
|
+
this.socket.on("disconnect", (reason) => {
|
|
249
|
+
process.stderr.write(`[conveyor] Socket disconnected: ${reason}
|
|
250
|
+
`);
|
|
251
|
+
});
|
|
252
|
+
this.socket.io.on("reconnect", (attempt) => {
|
|
253
|
+
process.stderr.write(`[conveyor] Reconnected after ${attempt} attempt(s)
|
|
254
|
+
`);
|
|
255
|
+
});
|
|
256
|
+
this.socket.io.on("reconnect_error", (err) => {
|
|
257
|
+
process.stderr.write(`[conveyor] Reconnection error: ${err.message}
|
|
258
|
+
`);
|
|
259
|
+
});
|
|
233
260
|
this.socket.io.on("reconnect_attempt", () => {
|
|
234
261
|
attempts++;
|
|
235
262
|
if (!settled && attempts >= maxInitialAttempts) {
|
|
@@ -427,10 +454,39 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
427
454
|
triggerIdentification() {
|
|
428
455
|
return triggerIdentification(this.socket);
|
|
429
456
|
}
|
|
457
|
+
async refreshAuthToken() {
|
|
458
|
+
const codespaceName = process.env.CODESPACE_NAME;
|
|
459
|
+
const apiUrl = process.env.CONVEYOR_API_URL ?? this.config.conveyorApiUrl;
|
|
460
|
+
if (!codespaceName || !apiUrl) return false;
|
|
461
|
+
try {
|
|
462
|
+
const response = await fetch(`${apiUrl}/api/codespace/bootstrap/${codespaceName}`);
|
|
463
|
+
if (!response.ok) return false;
|
|
464
|
+
const config = await response.json();
|
|
465
|
+
if (config.envVars?.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
466
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = config.envVars.CLAUDE_CODE_OAUTH_TOKEN;
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
430
474
|
emitModeTransition(payload) {
|
|
431
475
|
if (!this.socket) return;
|
|
432
476
|
this.socket.emit("agentRunner:modeTransition", payload);
|
|
433
477
|
}
|
|
478
|
+
emitDebugStateChanged(state) {
|
|
479
|
+
if (!this.socket) return;
|
|
480
|
+
this.socket.emit("agentRunner:debugStateChanged", { state });
|
|
481
|
+
}
|
|
482
|
+
emitDebugSessionComplete(summary) {
|
|
483
|
+
if (!this.socket) return;
|
|
484
|
+
this.socket.emit("agentRunner:debugSessionComplete", { summary });
|
|
485
|
+
}
|
|
486
|
+
emitDebugReproduceRequested(hypothesis) {
|
|
487
|
+
if (!this.socket) return;
|
|
488
|
+
this.socket.emit("agentRunner:debugReproduceRequested", { hypothesis });
|
|
489
|
+
}
|
|
434
490
|
searchIncidents(status, source) {
|
|
435
491
|
return searchIncidents(this.socket, status, source);
|
|
436
492
|
}
|
|
@@ -454,9 +510,16 @@ var ProjectConnection = class {
|
|
|
454
510
|
shutdownCallback = null;
|
|
455
511
|
chatMessageCallback = null;
|
|
456
512
|
earlyChatMessages = [];
|
|
513
|
+
auditRequestCallback = null;
|
|
514
|
+
// Branch switching callbacks
|
|
515
|
+
onSwitchBranch = null;
|
|
516
|
+
onSyncEnvironment = null;
|
|
517
|
+
onGetEnvStatus = null;
|
|
518
|
+
onRestartStartCommand = null;
|
|
457
519
|
constructor(config) {
|
|
458
520
|
this.config = config;
|
|
459
521
|
}
|
|
522
|
+
// oxlint-disable-next-line max-lines-per-function -- socket event registration requires co-located handlers
|
|
460
523
|
connect() {
|
|
461
524
|
return new Promise((resolve2, reject) => {
|
|
462
525
|
let settled = false;
|
|
@@ -496,12 +559,65 @@ var ProjectConnection = class {
|
|
|
496
559
|
this.earlyChatMessages.push(msg);
|
|
497
560
|
}
|
|
498
561
|
});
|
|
562
|
+
this.socket.on("projectRunner:auditTags", (data) => {
|
|
563
|
+
if (this.auditRequestCallback) {
|
|
564
|
+
this.auditRequestCallback(data);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
this.socket.on(
|
|
568
|
+
"projectRunner:switchBranch",
|
|
569
|
+
(data, cb) => {
|
|
570
|
+
if (this.onSwitchBranch) this.onSwitchBranch(data, cb);
|
|
571
|
+
else cb({ ok: false, error: "switchBranch handler not registered" });
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
this.socket.on("projectRunner:syncEnvironment", (cb) => {
|
|
575
|
+
if (this.onSyncEnvironment) this.onSyncEnvironment(cb);
|
|
576
|
+
else cb({ ok: false, error: "syncEnvironment handler not registered" });
|
|
577
|
+
});
|
|
578
|
+
this.socket.on("projectRunner:getEnvStatus", (cb) => {
|
|
579
|
+
if (this.onGetEnvStatus) this.onGetEnvStatus(cb);
|
|
580
|
+
else cb({ ok: false, data: void 0 });
|
|
581
|
+
});
|
|
582
|
+
this.socket.on(
|
|
583
|
+
"projectRunner:restartStartCommand",
|
|
584
|
+
(_data, cb) => {
|
|
585
|
+
if (this.onRestartStartCommand) this.onRestartStartCommand(cb);
|
|
586
|
+
else cb({ ok: false, error: "restartStartCommand handler not registered" });
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
this.socket.on(
|
|
590
|
+
"projectRunner:runAuthTokenCommand",
|
|
591
|
+
(data, cb) => this.handleRunAuthTokenCommand(data.userEmail, cb)
|
|
592
|
+
);
|
|
593
|
+
process.stderr.write(
|
|
594
|
+
`[conveyor] Connecting to ${this.config.apiUrl} (project: ${this.config.projectId})
|
|
595
|
+
`
|
|
596
|
+
);
|
|
499
597
|
this.socket.on("connect", () => {
|
|
598
|
+
process.stderr.write(`[conveyor] Project socket connected
|
|
599
|
+
`);
|
|
500
600
|
if (!settled) {
|
|
501
601
|
settled = true;
|
|
502
602
|
resolve2();
|
|
503
603
|
}
|
|
504
604
|
});
|
|
605
|
+
this.socket.on("connect_error", (err) => {
|
|
606
|
+
process.stderr.write(`[conveyor] Project socket connection error: ${err.message}
|
|
607
|
+
`);
|
|
608
|
+
});
|
|
609
|
+
this.socket.on("disconnect", (reason) => {
|
|
610
|
+
process.stderr.write(`[conveyor] Project socket disconnected: ${reason}
|
|
611
|
+
`);
|
|
612
|
+
});
|
|
613
|
+
this.socket.io.on("reconnect", (attempt) => {
|
|
614
|
+
process.stderr.write(`[conveyor] Project socket reconnected after ${attempt} attempt(s)
|
|
615
|
+
`);
|
|
616
|
+
});
|
|
617
|
+
this.socket.io.on("reconnect_error", (err) => {
|
|
618
|
+
process.stderr.write(`[conveyor] Project socket reconnection error: ${err.message}
|
|
619
|
+
`);
|
|
620
|
+
});
|
|
505
621
|
this.socket.io.on("reconnect_attempt", () => {
|
|
506
622
|
attempts++;
|
|
507
623
|
if (!settled && attempts >= maxInitialAttempts) {
|
|
@@ -527,6 +643,17 @@ var ProjectConnection = class {
|
|
|
527
643
|
}
|
|
528
644
|
this.earlyChatMessages = [];
|
|
529
645
|
}
|
|
646
|
+
onAuditRequest(callback) {
|
|
647
|
+
this.auditRequestCallback = callback;
|
|
648
|
+
}
|
|
649
|
+
emitAuditResult(data) {
|
|
650
|
+
if (!this.socket) return;
|
|
651
|
+
this.socket.emit("conveyor:tagAuditResult", data);
|
|
652
|
+
}
|
|
653
|
+
emitAuditProgress(data) {
|
|
654
|
+
if (!this.socket) return;
|
|
655
|
+
this.socket.emit("conveyor:tagAuditProgress", data);
|
|
656
|
+
}
|
|
530
657
|
sendHeartbeat() {
|
|
531
658
|
if (!this.socket) return;
|
|
532
659
|
this.socket.emit("projectRunner:heartbeat", {});
|
|
@@ -574,13 +701,13 @@ var ProjectConnection = class {
|
|
|
574
701
|
);
|
|
575
702
|
});
|
|
576
703
|
}
|
|
577
|
-
fetchChatHistory(limit) {
|
|
704
|
+
fetchChatHistory(limit, chatId) {
|
|
578
705
|
const socket = this.socket;
|
|
579
706
|
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
580
707
|
return new Promise((resolve2, reject) => {
|
|
581
708
|
socket.emit(
|
|
582
709
|
"projectRunner:getChatHistory",
|
|
583
|
-
{ limit },
|
|
710
|
+
{ limit, chatId },
|
|
584
711
|
(response) => {
|
|
585
712
|
if (response.success && response.data) resolve2(response.data);
|
|
586
713
|
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
@@ -588,6 +715,78 @@ var ProjectConnection = class {
|
|
|
588
715
|
);
|
|
589
716
|
});
|
|
590
717
|
}
|
|
718
|
+
// ── Project MCP tool request methods ──
|
|
719
|
+
requestListTasks(params) {
|
|
720
|
+
return this.requestWithCallback("projectRunner:listTasks", params);
|
|
721
|
+
}
|
|
722
|
+
requestGetTask(taskId) {
|
|
723
|
+
return this.requestWithCallback("projectRunner:getTask", { taskId });
|
|
724
|
+
}
|
|
725
|
+
requestCreateTask(params) {
|
|
726
|
+
return this.requestWithCallback("projectRunner:createTask", params);
|
|
727
|
+
}
|
|
728
|
+
requestUpdateTask(params) {
|
|
729
|
+
return this.requestWithCallback("projectRunner:updateTask", params);
|
|
730
|
+
}
|
|
731
|
+
requestSearchTasks(params) {
|
|
732
|
+
return this.requestWithCallback("projectRunner:searchTasks", params);
|
|
733
|
+
}
|
|
734
|
+
requestListTags() {
|
|
735
|
+
return this.requestWithCallback("projectRunner:listTags", {});
|
|
736
|
+
}
|
|
737
|
+
requestGetProjectSummary() {
|
|
738
|
+
return this.requestWithCallback("projectRunner:getProjectSummary", {});
|
|
739
|
+
}
|
|
740
|
+
requestWithCallback(event, data) {
|
|
741
|
+
const socket = this.socket;
|
|
742
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
743
|
+
return new Promise((resolve2, reject) => {
|
|
744
|
+
socket.emit(event, data, (response) => {
|
|
745
|
+
if (response.success) resolve2(response.data);
|
|
746
|
+
else reject(new Error(response.error ?? `${event} failed`));
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
emitNewCommitsDetected(data) {
|
|
751
|
+
if (!this.socket) return;
|
|
752
|
+
this.socket.emit("projectRunner:newCommitsDetected", data);
|
|
753
|
+
}
|
|
754
|
+
emitEnvironmentReady(data) {
|
|
755
|
+
if (!this.socket) return;
|
|
756
|
+
this.socket.emit("projectRunner:environmentReady", data);
|
|
757
|
+
}
|
|
758
|
+
emitEnvSwitchProgress(data) {
|
|
759
|
+
if (!this.socket) return;
|
|
760
|
+
this.socket.emit("projectRunner:envSwitchProgress", data);
|
|
761
|
+
}
|
|
762
|
+
handleRunAuthTokenCommand(userEmail, cb) {
|
|
763
|
+
try {
|
|
764
|
+
if (process.env.CODESPACES !== "true") {
|
|
765
|
+
cb({ ok: false, error: "Auth token command only available in codespace environments" });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const authCmd = process.env.CONVEYOR_AUTH_TOKEN_COMMAND;
|
|
769
|
+
if (!authCmd) {
|
|
770
|
+
cb({ ok: false, error: "CONVEYOR_AUTH_TOKEN_COMMAND not configured" });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const cwd = this.config.projectDir ?? process.cwd();
|
|
774
|
+
const token = runAuthTokenCommand(authCmd, userEmail, cwd);
|
|
775
|
+
if (!token) {
|
|
776
|
+
cb({
|
|
777
|
+
ok: false,
|
|
778
|
+
error: `Auth token command returned empty output. Command: ${authCmd}`
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
cb({ ok: true, token });
|
|
783
|
+
} catch (error) {
|
|
784
|
+
cb({
|
|
785
|
+
ok: false,
|
|
786
|
+
error: error instanceof Error ? error.message : "Auth token command failed"
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
591
790
|
disconnect() {
|
|
592
791
|
this.socket?.disconnect();
|
|
593
792
|
this.socket = null;
|
|
@@ -622,9 +821,32 @@ function errorMeta(error) {
|
|
|
622
821
|
}
|
|
623
822
|
|
|
624
823
|
// src/runner/worktree.ts
|
|
625
|
-
import { execSync as
|
|
824
|
+
import { execSync as execSync3 } from "child_process";
|
|
626
825
|
import { existsSync } from "fs";
|
|
627
826
|
import { join } from "path";
|
|
827
|
+
|
|
828
|
+
// src/runner/git-utils.ts
|
|
829
|
+
import { execSync as execSync2 } from "child_process";
|
|
830
|
+
function hasUncommittedChanges(cwd) {
|
|
831
|
+
const status = execSync2("git status --porcelain", {
|
|
832
|
+
cwd,
|
|
833
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
834
|
+
}).toString().trim();
|
|
835
|
+
return status.length > 0;
|
|
836
|
+
}
|
|
837
|
+
function getCurrentBranch(cwd) {
|
|
838
|
+
try {
|
|
839
|
+
const branch = execSync2("git branch --show-current", {
|
|
840
|
+
cwd,
|
|
841
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
842
|
+
}).toString().trim();
|
|
843
|
+
return branch || null;
|
|
844
|
+
} catch {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/runner/worktree.ts
|
|
628
850
|
var WORKTREE_DIR = ".worktrees";
|
|
629
851
|
function ensureWorktree(projectDir, taskId, branch) {
|
|
630
852
|
if (projectDir.includes(`/${WORKTREE_DIR}/`)) {
|
|
@@ -633,8 +855,11 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
633
855
|
const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
|
|
634
856
|
if (existsSync(worktreePath)) {
|
|
635
857
|
if (branch) {
|
|
858
|
+
if (hasUncommittedChanges(worktreePath)) {
|
|
859
|
+
return worktreePath;
|
|
860
|
+
}
|
|
636
861
|
try {
|
|
637
|
-
|
|
862
|
+
execSync3(`git checkout --detach origin/${branch}`, {
|
|
638
863
|
cwd: worktreePath,
|
|
639
864
|
stdio: "ignore"
|
|
640
865
|
});
|
|
@@ -644,17 +869,39 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
644
869
|
return worktreePath;
|
|
645
870
|
}
|
|
646
871
|
const ref = branch ? `origin/${branch}` : "HEAD";
|
|
647
|
-
|
|
872
|
+
execSync3(`git worktree add --detach "${worktreePath}" ${ref}`, {
|
|
648
873
|
cwd: projectDir,
|
|
649
874
|
stdio: "ignore"
|
|
650
875
|
});
|
|
651
876
|
return worktreePath;
|
|
652
877
|
}
|
|
878
|
+
function detachWorktreeBranch(projectDir, branch) {
|
|
879
|
+
try {
|
|
880
|
+
const output = execSync3("git worktree list --porcelain", {
|
|
881
|
+
cwd: projectDir,
|
|
882
|
+
encoding: "utf-8"
|
|
883
|
+
});
|
|
884
|
+
const entries = output.split("\n\n");
|
|
885
|
+
for (const entry of entries) {
|
|
886
|
+
const lines = entry.trim().split("\n");
|
|
887
|
+
const worktreeLine = lines.find((l) => l.startsWith("worktree "));
|
|
888
|
+
const branchLine = lines.find((l) => l.startsWith("branch "));
|
|
889
|
+
if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;
|
|
890
|
+
const worktreePath = worktreeLine.replace("worktree ", "");
|
|
891
|
+
if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;
|
|
892
|
+
try {
|
|
893
|
+
execSync3("git checkout --detach", { cwd: worktreePath, stdio: "ignore" });
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
653
900
|
function removeWorktree(projectDir, taskId) {
|
|
654
901
|
const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
|
|
655
902
|
if (!existsSync(worktreePath)) return;
|
|
656
903
|
try {
|
|
657
|
-
|
|
904
|
+
execSync3(`git worktree remove "${worktreePath}" --force`, {
|
|
658
905
|
cwd: projectDir,
|
|
659
906
|
stdio: "ignore"
|
|
660
907
|
});
|
|
@@ -690,10 +937,10 @@ function loadConveyorConfig(_workspaceDir) {
|
|
|
690
937
|
}
|
|
691
938
|
|
|
692
939
|
// src/setup/codespace.ts
|
|
693
|
-
import { execSync as
|
|
940
|
+
import { execSync as execSync4 } from "child_process";
|
|
694
941
|
function unshallowRepo(workspaceDir) {
|
|
695
942
|
try {
|
|
696
|
-
|
|
943
|
+
execSync4("git fetch --unshallow", {
|
|
697
944
|
cwd: workspaceDir,
|
|
698
945
|
stdio: "ignore",
|
|
699
946
|
timeout: 6e4
|
|
@@ -704,9 +951,252 @@ function unshallowRepo(workspaceDir) {
|
|
|
704
951
|
|
|
705
952
|
// src/runner/agent-runner.ts
|
|
706
953
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
707
|
-
import { execSync as
|
|
954
|
+
import { execSync as execSync5 } from "child_process";
|
|
955
|
+
|
|
956
|
+
// src/connection/tunnel-client.ts
|
|
957
|
+
import { request as httpRequest } from "http";
|
|
958
|
+
var logger2 = createServiceLogger("TunnelClient");
|
|
959
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
960
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
961
|
+
var TunnelClient = class _TunnelClient {
|
|
962
|
+
static STABLE_MS = 3e4;
|
|
963
|
+
apiUrl;
|
|
964
|
+
token;
|
|
965
|
+
localPort;
|
|
966
|
+
ws = null;
|
|
967
|
+
stopped = false;
|
|
968
|
+
reconnectAttempts = 0;
|
|
969
|
+
reconnectTimer = null;
|
|
970
|
+
connectedAt = 0;
|
|
971
|
+
constructor(apiUrl, token, localPort) {
|
|
972
|
+
this.apiUrl = apiUrl;
|
|
973
|
+
this.token = token;
|
|
974
|
+
this.localPort = localPort;
|
|
975
|
+
}
|
|
976
|
+
connect() {
|
|
977
|
+
if (this.stopped) return;
|
|
978
|
+
const wsUrl = this.apiUrl.replace(/^http/, "ws").replace(/\/$/, "");
|
|
979
|
+
const url = `${wsUrl}/api/tunnel?token=${encodeURIComponent(this.token)}`;
|
|
980
|
+
try {
|
|
981
|
+
this.ws = new WebSocket(url);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
logger2.warn("Failed to create tunnel WebSocket", { error: String(err) });
|
|
984
|
+
this.scheduleReconnect();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.ws.binaryType = "arraybuffer";
|
|
988
|
+
this.ws.addEventListener("open", () => {
|
|
989
|
+
this.connectedAt = Date.now();
|
|
990
|
+
logger2.info("Tunnel connected", { port: this.localPort });
|
|
991
|
+
});
|
|
992
|
+
this.ws.addEventListener("close", (event) => {
|
|
993
|
+
logger2.info("Tunnel disconnected", { code: event.code, reason: event.reason });
|
|
994
|
+
this.scheduleReconnect();
|
|
995
|
+
});
|
|
996
|
+
this.ws.addEventListener("error", (event) => {
|
|
997
|
+
const msg = "message" in event ? event.message : "unknown";
|
|
998
|
+
logger2.warn("Tunnel error", { error: msg });
|
|
999
|
+
});
|
|
1000
|
+
this.ws.addEventListener("message", (event) => {
|
|
1001
|
+
this.handleMessage(event.data);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
disconnect() {
|
|
1005
|
+
this.stopped = true;
|
|
1006
|
+
if (this.reconnectTimer) {
|
|
1007
|
+
clearTimeout(this.reconnectTimer);
|
|
1008
|
+
this.reconnectTimer = null;
|
|
1009
|
+
}
|
|
1010
|
+
if (this.ws) {
|
|
1011
|
+
this.ws.close(1e3, "shutdown");
|
|
1012
|
+
this.ws = null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
scheduleReconnect() {
|
|
1016
|
+
if (this.stopped) return;
|
|
1017
|
+
if (Date.now() - this.connectedAt >= _TunnelClient.STABLE_MS) {
|
|
1018
|
+
this.reconnectAttempts = 0;
|
|
1019
|
+
}
|
|
1020
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempts, RECONNECT_MAX_MS);
|
|
1021
|
+
this.reconnectAttempts++;
|
|
1022
|
+
logger2.info("Tunnel reconnecting", { delay, attempt: this.reconnectAttempts });
|
|
1023
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
1024
|
+
}
|
|
1025
|
+
// ---------------------------------------------------------------------------
|
|
1026
|
+
// Message handling
|
|
1027
|
+
// ---------------------------------------------------------------------------
|
|
1028
|
+
handleMessage(data) {
|
|
1029
|
+
if (typeof data === "string") {
|
|
1030
|
+
this.handleJsonMessage(data);
|
|
1031
|
+
} else {
|
|
1032
|
+
this.handleBinaryFrame(data);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
handleJsonMessage(raw) {
|
|
1036
|
+
let msg;
|
|
1037
|
+
try {
|
|
1038
|
+
msg = JSON.parse(raw);
|
|
1039
|
+
} catch {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
switch (msg.type) {
|
|
1043
|
+
case "http-req":
|
|
1044
|
+
this.handleHttpRequest(msg.id, msg.method ?? "GET", msg.path ?? "/", msg.headers ?? {});
|
|
1045
|
+
break;
|
|
1046
|
+
case "http-req-end":
|
|
1047
|
+
this.endHttpRequest(msg.id);
|
|
1048
|
+
break;
|
|
1049
|
+
case "ws-upgrade":
|
|
1050
|
+
this.handleWsUpgrade(msg.id, msg.path ?? "/", msg.headers ?? {});
|
|
1051
|
+
break;
|
|
1052
|
+
case "ws-data":
|
|
1053
|
+
this.relayWsData(msg.id, msg.data ?? "");
|
|
1054
|
+
break;
|
|
1055
|
+
case "ws-close":
|
|
1056
|
+
this.closeWs(msg.id);
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
handleBinaryFrame(data) {
|
|
1061
|
+
const buf = new Uint8Array(data);
|
|
1062
|
+
if (buf.length <= 4) return;
|
|
1063
|
+
const id = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
|
|
1064
|
+
const body = buf.subarray(4);
|
|
1065
|
+
const pending = this.pendingHttpRequests.get(id);
|
|
1066
|
+
if (pending) pending.chunks.push(Buffer.from(body));
|
|
1067
|
+
}
|
|
1068
|
+
// ---------------------------------------------------------------------------
|
|
1069
|
+
// HTTP request proxying
|
|
1070
|
+
// ---------------------------------------------------------------------------
|
|
1071
|
+
pendingHttpRequests = /* @__PURE__ */ new Map();
|
|
1072
|
+
handleHttpRequest(id, method, path2, headers) {
|
|
1073
|
+
const state = { chunks: [], ended: false };
|
|
1074
|
+
this.pendingHttpRequests.set(id, state);
|
|
1075
|
+
const req = httpRequest(
|
|
1076
|
+
{
|
|
1077
|
+
hostname: "127.0.0.1",
|
|
1078
|
+
port: this.localPort,
|
|
1079
|
+
path: path2,
|
|
1080
|
+
method,
|
|
1081
|
+
headers: { ...headers, host: `127.0.0.1:${this.localPort}` }
|
|
1082
|
+
},
|
|
1083
|
+
(res) => {
|
|
1084
|
+
const resHeaders = {};
|
|
1085
|
+
for (const [key, val] of Object.entries(res.headers)) {
|
|
1086
|
+
if (typeof val === "string") resHeaders[key] = val;
|
|
1087
|
+
}
|
|
1088
|
+
this.send(
|
|
1089
|
+
JSON.stringify({ type: "http-res", id, statusCode: res.statusCode, headers: resHeaders })
|
|
1090
|
+
);
|
|
1091
|
+
res.on("data", (chunk) => {
|
|
1092
|
+
const frame = Buffer.alloc(4 + chunk.length);
|
|
1093
|
+
frame.writeUInt32BE(id, 0);
|
|
1094
|
+
chunk.copy(frame, 4);
|
|
1095
|
+
this.sendBinary(frame);
|
|
1096
|
+
});
|
|
1097
|
+
res.on("end", () => {
|
|
1098
|
+
this.send(JSON.stringify({ type: "http-end", id }));
|
|
1099
|
+
this.pendingHttpRequests.delete(id);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
);
|
|
1103
|
+
req.on("error", (err) => {
|
|
1104
|
+
logger2.warn("Local HTTP request failed", { id, error: err.message });
|
|
1105
|
+
this.send(JSON.stringify({ type: "http-res", id, statusCode: 502, headers: {} }));
|
|
1106
|
+
this.send(JSON.stringify({ type: "http-end", id }));
|
|
1107
|
+
this.pendingHttpRequests.delete(id);
|
|
1108
|
+
});
|
|
1109
|
+
for (const chunk of state.chunks) {
|
|
1110
|
+
req.write(chunk);
|
|
1111
|
+
}
|
|
1112
|
+
state.chunks = [];
|
|
1113
|
+
if (state.ended) {
|
|
1114
|
+
req.end();
|
|
1115
|
+
} else {
|
|
1116
|
+
state.req = req;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
endHttpRequest(id) {
|
|
1120
|
+
const state = this.pendingHttpRequests.get(id);
|
|
1121
|
+
if (!state) return;
|
|
1122
|
+
state.ended = true;
|
|
1123
|
+
const req = state.req;
|
|
1124
|
+
if (req) {
|
|
1125
|
+
for (const chunk of state.chunks) {
|
|
1126
|
+
req.write(chunk);
|
|
1127
|
+
}
|
|
1128
|
+
req.end();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// ---------------------------------------------------------------------------
|
|
1132
|
+
// WebSocket proxying
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
localWebSockets = /* @__PURE__ */ new Map();
|
|
1135
|
+
handleWsUpgrade(id, path2, _headers) {
|
|
1136
|
+
const url = `ws://127.0.0.1:${this.localPort}${path2}`;
|
|
1137
|
+
try {
|
|
1138
|
+
const localWs = new WebSocket(url);
|
|
1139
|
+
localWs.addEventListener("open", () => {
|
|
1140
|
+
this.localWebSockets.set(id, localWs);
|
|
1141
|
+
this.send(JSON.stringify({ type: "ws-open", id }));
|
|
1142
|
+
});
|
|
1143
|
+
localWs.addEventListener("message", (event) => {
|
|
1144
|
+
const data = typeof event.data === "string" ? event.data : String(event.data);
|
|
1145
|
+
this.send(JSON.stringify({ type: "ws-data", id, data }));
|
|
1146
|
+
});
|
|
1147
|
+
localWs.addEventListener("close", () => {
|
|
1148
|
+
this.localWebSockets.delete(id);
|
|
1149
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1150
|
+
});
|
|
1151
|
+
localWs.addEventListener("error", () => {
|
|
1152
|
+
this.localWebSockets.delete(id);
|
|
1153
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1154
|
+
});
|
|
1155
|
+
} catch {
|
|
1156
|
+
this.send(JSON.stringify({ type: "ws-close", id }));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
relayWsData(id, data) {
|
|
1160
|
+
const localWs = this.localWebSockets.get(id);
|
|
1161
|
+
if (localWs && localWs.readyState === WebSocket.OPEN) {
|
|
1162
|
+
localWs.send(data);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
closeWs(id) {
|
|
1166
|
+
const localWs = this.localWebSockets.get(id);
|
|
1167
|
+
if (localWs) {
|
|
1168
|
+
localWs.close();
|
|
1169
|
+
this.localWebSockets.delete(id);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
// ---------------------------------------------------------------------------
|
|
1173
|
+
// Send helpers
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
send(data) {
|
|
1176
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1177
|
+
this.ws.send(data);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
sendBinary(data) {
|
|
1181
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1182
|
+
const copy = new ArrayBuffer(data.byteLength);
|
|
1183
|
+
new Uint8Array(copy).set(
|
|
1184
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
1185
|
+
);
|
|
1186
|
+
this.ws.send(copy);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
708
1190
|
|
|
709
1191
|
// src/execution/event-handlers.ts
|
|
1192
|
+
function safeVoid(promise, context) {
|
|
1193
|
+
if (promise && typeof promise.catch === "function") {
|
|
1194
|
+
promise.catch((err) => {
|
|
1195
|
+
process.stderr.write(`[safeVoid] ${context}: ${err}
|
|
1196
|
+
`);
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
710
1200
|
function epochSecondsToISO(value) {
|
|
711
1201
|
if (typeof value === "string") return value;
|
|
712
1202
|
if (typeof value !== "number" || value <= 0) return void 0;
|
|
@@ -745,6 +1235,10 @@ async function processAssistantEvent(event, host, turnToolCalls) {
|
|
|
745
1235
|
}
|
|
746
1236
|
var API_ERROR_PATTERN = /API Error: [45]\d\d/;
|
|
747
1237
|
var IMAGE_ERROR_PATTERN = /Could not process image/i;
|
|
1238
|
+
var AUTH_ERROR_PATTERN = /Not logged in|Please run \/login|authentication failed|invalid.*token|unauthorized/i;
|
|
1239
|
+
function isAuthError(msg) {
|
|
1240
|
+
return AUTH_ERROR_PATTERN.test(msg);
|
|
1241
|
+
}
|
|
748
1242
|
function isRetriableMessage(msg) {
|
|
749
1243
|
if (IMAGE_ERROR_PATTERN.test(msg)) return true;
|
|
750
1244
|
if (API_ERROR_PATTERN.test(msg)) return true;
|
|
@@ -825,6 +1319,10 @@ function handleErrorResult(event, host) {
|
|
|
825
1319
|
if (isStaleSession) {
|
|
826
1320
|
return { retriable: false, staleSession: true };
|
|
827
1321
|
}
|
|
1322
|
+
if (isAuthError(errorMsg)) {
|
|
1323
|
+
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
1324
|
+
return { retriable: false, authError: true };
|
|
1325
|
+
}
|
|
828
1326
|
const retriable = isRetriableMessage(errorMsg);
|
|
829
1327
|
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
830
1328
|
return { retriable };
|
|
@@ -864,7 +1362,8 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
|
|
|
864
1362
|
return {
|
|
865
1363
|
retriable: result.retriable,
|
|
866
1364
|
resultSummary: result.resultSummary,
|
|
867
|
-
staleSession: result.staleSession
|
|
1365
|
+
staleSession: result.staleSession,
|
|
1366
|
+
authError: result.authError
|
|
868
1367
|
};
|
|
869
1368
|
}
|
|
870
1369
|
function handleRateLimitEvent(event, host) {
|
|
@@ -883,13 +1382,13 @@ function handleRateLimitEvent(event, host) {
|
|
|
883
1382
|
const resetsAtDisplay = resetsAt ?? "unknown";
|
|
884
1383
|
const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAtDisplay})`;
|
|
885
1384
|
host.connection.sendEvent({ type: "error", message });
|
|
886
|
-
|
|
1385
|
+
safeVoid(host.callbacks.onEvent({ type: "error", message }), "rateLimitRejected");
|
|
887
1386
|
return resetsAt;
|
|
888
1387
|
} else if (status === "allowed_warning") {
|
|
889
1388
|
const utilization = rate_limit_info.utilization ? `${Math.round(rate_limit_info.utilization * 100)}%` : "high";
|
|
890
1389
|
const message = `Rate limit warning: ${utilization} utilization (type: ${rate_limit_info.rateLimitType ?? "unknown"})`;
|
|
891
1390
|
host.connection.sendEvent({ type: "thinking", message });
|
|
892
|
-
|
|
1391
|
+
safeVoid(host.callbacks.onEvent({ type: "thinking", message }), "rateLimitWarning");
|
|
893
1392
|
}
|
|
894
1393
|
return void 0;
|
|
895
1394
|
}
|
|
@@ -907,34 +1406,46 @@ async function handleSystemEvent(event, host, context, sessionIdStored) {
|
|
|
907
1406
|
}
|
|
908
1407
|
function handleSystemSubevents(systemEvent, host) {
|
|
909
1408
|
if (systemEvent.subtype === "compact_boundary") {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1409
|
+
safeVoid(
|
|
1410
|
+
host.callbacks.onEvent({
|
|
1411
|
+
type: "context_compacted",
|
|
1412
|
+
trigger: systemEvent.compact_metadata.trigger,
|
|
1413
|
+
preTokens: systemEvent.compact_metadata.pre_tokens
|
|
1414
|
+
}),
|
|
1415
|
+
"compactBoundary"
|
|
1416
|
+
);
|
|
915
1417
|
} else if (systemEvent.subtype === "task_started") {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1418
|
+
safeVoid(
|
|
1419
|
+
host.callbacks.onEvent({
|
|
1420
|
+
type: "subagent_started",
|
|
1421
|
+
sdkTaskId: systemEvent.task_id,
|
|
1422
|
+
description: systemEvent.description
|
|
1423
|
+
}),
|
|
1424
|
+
"taskStarted"
|
|
1425
|
+
);
|
|
921
1426
|
} else if (systemEvent.subtype === "task_progress") {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1427
|
+
safeVoid(
|
|
1428
|
+
host.callbacks.onEvent({
|
|
1429
|
+
type: "subagent_progress",
|
|
1430
|
+
sdkTaskId: systemEvent.task_id,
|
|
1431
|
+
description: systemEvent.description,
|
|
1432
|
+
toolUses: systemEvent.usage?.tool_uses ?? 0,
|
|
1433
|
+
durationMs: systemEvent.usage?.duration_ms ?? 0
|
|
1434
|
+
}),
|
|
1435
|
+
"taskProgress"
|
|
1436
|
+
);
|
|
929
1437
|
}
|
|
930
1438
|
}
|
|
931
1439
|
function handleToolProgressEvent(event, host) {
|
|
932
1440
|
const msg = event;
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1441
|
+
safeVoid(
|
|
1442
|
+
host.callbacks.onEvent({
|
|
1443
|
+
type: "tool_progress",
|
|
1444
|
+
toolName: msg.tool_name ?? "",
|
|
1445
|
+
elapsedSeconds: msg.elapsed_time_seconds ?? 0
|
|
1446
|
+
}),
|
|
1447
|
+
"toolProgress"
|
|
1448
|
+
);
|
|
938
1449
|
}
|
|
939
1450
|
async function handleAssistantCase(event, host, turnToolCalls) {
|
|
940
1451
|
await processAssistantEvent(event, host, turnToolCalls);
|
|
@@ -952,11 +1463,13 @@ async function handleResultCase(event, host, context, startTime, isTyping, lastA
|
|
|
952
1463
|
retriable: resultInfo.retriable,
|
|
953
1464
|
resultSummary: resultInfo.resultSummary,
|
|
954
1465
|
staleSession: resultInfo.staleSession,
|
|
1466
|
+
authError: resultInfo.authError,
|
|
955
1467
|
stoppedTyping
|
|
956
1468
|
};
|
|
957
1469
|
}
|
|
958
1470
|
|
|
959
1471
|
// src/execution/event-processor.ts
|
|
1472
|
+
var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
|
|
960
1473
|
function stopTypingIfNeeded(host, isTyping) {
|
|
961
1474
|
if (isTyping) host.connection.sendTypingStop();
|
|
962
1475
|
}
|
|
@@ -978,6 +1491,12 @@ async function processAssistantCase(event, host, state) {
|
|
|
978
1491
|
}
|
|
979
1492
|
const usage = await handleAssistantCase(event, host, state.turnToolCalls);
|
|
980
1493
|
if (usage) state.lastAssistantUsage = usage;
|
|
1494
|
+
if (!state.sawApiError) {
|
|
1495
|
+
const fullText = event.message.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
1496
|
+
if (API_ERROR_PATTERN2.test(fullText)) {
|
|
1497
|
+
state.sawApiError = true;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
981
1500
|
}
|
|
982
1501
|
async function processResultCase(event, host, context, startTime, state) {
|
|
983
1502
|
const info = await handleResultCase(
|
|
@@ -992,6 +1511,7 @@ async function processResultCase(event, host, context, startTime, state) {
|
|
|
992
1511
|
state.retriable = info.retriable;
|
|
993
1512
|
state.resultSummary = info.resultSummary;
|
|
994
1513
|
if (info.staleSession) state.staleSession = true;
|
|
1514
|
+
if (info.authError) state.authError = true;
|
|
995
1515
|
}
|
|
996
1516
|
async function processEvents(events, context, host) {
|
|
997
1517
|
const startTime = Date.now();
|
|
@@ -1001,9 +1521,11 @@ async function processEvents(events, context, host) {
|
|
|
1001
1521
|
sessionIdStored: false,
|
|
1002
1522
|
isTyping: false,
|
|
1003
1523
|
retriable: false,
|
|
1524
|
+
sawApiError: false,
|
|
1004
1525
|
resultSummary: void 0,
|
|
1005
1526
|
rateLimitResetsAt: void 0,
|
|
1006
1527
|
staleSession: void 0,
|
|
1528
|
+
authError: void 0,
|
|
1007
1529
|
lastAssistantUsage: void 0,
|
|
1008
1530
|
turnToolCalls: []
|
|
1009
1531
|
};
|
|
@@ -1040,10 +1562,11 @@ async function processEvents(events, context, host) {
|
|
|
1040
1562
|
}
|
|
1041
1563
|
stopTypingIfNeeded(host, state.isTyping);
|
|
1042
1564
|
return {
|
|
1043
|
-
retriable: state.retriable,
|
|
1565
|
+
retriable: state.retriable || state.sawApiError,
|
|
1044
1566
|
resultSummary: state.resultSummary,
|
|
1045
1567
|
rateLimitResetsAt: state.rateLimitResetsAt,
|
|
1046
|
-
...state.staleSession && { staleSession: state.staleSession }
|
|
1568
|
+
...state.staleSession && { staleSession: state.staleSession },
|
|
1569
|
+
...state.authError && { authError: state.authError }
|
|
1047
1570
|
};
|
|
1048
1571
|
}
|
|
1049
1572
|
|
|
@@ -1363,11 +1886,10 @@ function buildPropertyInstructions(context) {
|
|
|
1363
1886
|
``,
|
|
1364
1887
|
`### Proactive Property Management`,
|
|
1365
1888
|
`As you plan this task, proactively fill in task properties when you have enough context:`,
|
|
1366
|
-
`-
|
|
1367
|
-
`-
|
|
1368
|
-
`- For icons: FIRST call list_icons to check for existing matches
|
|
1889
|
+
`- Use update_task_properties to set any combination of: title, story points, tags, and icon`,
|
|
1890
|
+
`- You can update all properties at once or just one at a time as needed`,
|
|
1891
|
+
`- For icons: FIRST call list_icons to check for existing matches, then use update_task_properties with iconId.`,
|
|
1369
1892
|
` Only call generate_task_icon if no existing icon is a good fit.`,
|
|
1370
|
-
`- Use set_task_title if the current title doesn't accurately reflect the plan`,
|
|
1371
1893
|
``,
|
|
1372
1894
|
`Don't wait for the user to ask \u2014 fill these in naturally as the plan takes shape.`,
|
|
1373
1895
|
`If the user adjusts the plan significantly, update the properties to match.`
|
|
@@ -1401,6 +1923,8 @@ function buildDiscoveryPrompt(context) {
|
|
|
1401
1923
|
`You are in Discovery mode \u2014 helping plan and scope this task.`,
|
|
1402
1924
|
`- You have read-only codebase access (can read files, run git commands, search code)`,
|
|
1403
1925
|
`- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
|
|
1926
|
+
`- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
|
|
1927
|
+
`- If you identify code changes needed, describe them in the plan instead of implementing them`,
|
|
1404
1928
|
`- You can create and manage subtasks`,
|
|
1405
1929
|
`- Goal: collaborate with the user to create a clear plan`,
|
|
1406
1930
|
`- Proactively fill task properties (SP, tags, icon) as the plan takes shape`,
|
|
@@ -1408,10 +1932,8 @@ function buildDiscoveryPrompt(context) {
|
|
|
1408
1932
|
`### Self-Identification Tools`,
|
|
1409
1933
|
`Use these MCP tools to set your own task properties:`,
|
|
1410
1934
|
`- \`update_task\` \u2014 save your plan and description`,
|
|
1411
|
-
`- \`
|
|
1412
|
-
`- \`
|
|
1413
|
-
`- \`set_task_tags\` \u2014 categorize the work`,
|
|
1414
|
-
`- \`set_task_icon\` / \`generate_task_icon\` \u2014 set a task icon (call \`list_icons\` first)`,
|
|
1935
|
+
`- \`update_task_properties\` \u2014 set title, story points, tags, and icon (any combination)`,
|
|
1936
|
+
`- \`generate_task_icon\` \u2014 generate a new icon if needed (call \`list_icons\` first)`,
|
|
1415
1937
|
``,
|
|
1416
1938
|
`### Self-Update vs Subtasks`,
|
|
1417
1939
|
`- If the work fits in a single task (1-3 SP), update YOUR OWN plan and properties \u2014 do not create subtasks`,
|
|
@@ -1419,7 +1941,7 @@ function buildDiscoveryPrompt(context) {
|
|
|
1419
1941
|
``,
|
|
1420
1942
|
`### Finishing Planning`,
|
|
1421
1943
|
`Once your plan is complete and all required properties are set, call the **ExitPlanMode** tool.`,
|
|
1422
|
-
`- Required before ExitPlanMode will succeed: **plan** (via update_task), **story points** (via
|
|
1944
|
+
`- Required before ExitPlanMode will succeed: **plan** (via update_task), **story points** (via update_task_properties), **title** (via update_task_properties)`,
|
|
1423
1945
|
`- ExitPlanMode validates these properties and moves the task to Open status`,
|
|
1424
1946
|
`- It does NOT start building \u2014 the team controls when to switch to Build mode`
|
|
1425
1947
|
];
|
|
@@ -1435,13 +1957,13 @@ function buildAutoPrompt(context) {
|
|
|
1435
1957
|
`### Phase 1: Discovery & Planning (current)`,
|
|
1436
1958
|
`- You have read-only codebase access (can read files, run git commands, search code)`,
|
|
1437
1959
|
`- You can write plan files in .claude/plans/ only \u2014 no other file writes`,
|
|
1438
|
-
`- You have MCP tools for task properties: update_task,
|
|
1960
|
+
`- You have MCP tools for task properties: update_task, update_task_properties`,
|
|
1439
1961
|
``,
|
|
1440
1962
|
`### Required before transitioning:`,
|
|
1441
1963
|
`Before calling ExitPlanMode, you MUST fill in ALL of these:`,
|
|
1442
1964
|
`1. **Plan** \u2014 Save a clear implementation plan using update_task`,
|
|
1443
|
-
`2. **Story Points** \u2014 Assign via
|
|
1444
|
-
`3. **Title** \u2014 Set an accurate title via
|
|
1965
|
+
`2. **Story Points** \u2014 Assign via update_task_properties`,
|
|
1966
|
+
`3. **Title** \u2014 Set an accurate title via update_task_properties (if the current one is vague or "Untitled")`,
|
|
1445
1967
|
``,
|
|
1446
1968
|
`### Transitioning to Building:`,
|
|
1447
1969
|
`When your plan is complete and all required properties are set, call the **ExitPlanMode** tool.`,
|
|
@@ -1535,6 +2057,14 @@ Project Agents:`);
|
|
|
1535
2057
|
parts.push(formatProjectAgentLine(pa));
|
|
1536
2058
|
}
|
|
1537
2059
|
}
|
|
2060
|
+
if (context.projectObjectives && context.projectObjectives.length > 0) {
|
|
2061
|
+
parts.push(`
|
|
2062
|
+
Project Objectives:`);
|
|
2063
|
+
for (const obj of context.projectObjectives) {
|
|
2064
|
+
const dates = `${obj.startDate.split("T")[0]} to ${obj.endDate.split("T")[0]}`;
|
|
2065
|
+
parts.push(`- **${obj.name}** (${dates})${obj.description ? ": " + obj.description : ""}`);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1538
2068
|
return parts;
|
|
1539
2069
|
}
|
|
1540
2070
|
function buildActivePreamble(context, workspaceDir) {
|
|
@@ -1589,7 +2119,37 @@ Git safety \u2014 STRICT rules:`,
|
|
|
1589
2119
|
`- If \`git push\` fails with "non-fast-forward", run \`git push --force-with-lease origin ${context.githubBranch}\`. This branch is exclusively yours \u2014 force-with-lease is safe.`
|
|
1590
2120
|
];
|
|
1591
2121
|
}
|
|
1592
|
-
function
|
|
2122
|
+
function buildDebugModeSection(hypothesisTracker) {
|
|
2123
|
+
const lines = [
|
|
2124
|
+
`
|
|
2125
|
+
## Debug Mode`,
|
|
2126
|
+
`You have access to debug tools that let you set breakpoints, inspect live state,`,
|
|
2127
|
+
`and capture client-side behavior. Use these when:`,
|
|
2128
|
+
`- You can't find the bug from code reading alone`,
|
|
2129
|
+
`- The bug is state-dependent or timing-dependent`,
|
|
2130
|
+
`- You need to verify a hypothesis about runtime behavior`,
|
|
2131
|
+
``,
|
|
2132
|
+
`Debugging workflow:`,
|
|
2133
|
+
`1. Form a hypothesis about the root cause`,
|
|
2134
|
+
`2. Identify 2-5 strategic locations to observe (don't over-instrument)`,
|
|
2135
|
+
`3. Set breakpoints or probes at those locations`,
|
|
2136
|
+
`4. Ask the user to reproduce on the preview link`,
|
|
2137
|
+
`5. Analyze the captured data \u2014 confirm or refute your hypothesis`,
|
|
2138
|
+
`6. If refuted, refine hypothesis and repeat (max 3 iterations)`,
|
|
2139
|
+
`7. Exit debug mode and implement the fix`,
|
|
2140
|
+
``,
|
|
2141
|
+
`Keep debug sessions focused. You don't need to see everything \u2014`,
|
|
2142
|
+
`just the data that tests your hypothesis.`
|
|
2143
|
+
];
|
|
2144
|
+
if (hypothesisTracker?.isActive()) {
|
|
2145
|
+
const debugContext = hypothesisTracker.getDebugContext();
|
|
2146
|
+
if (debugContext) {
|
|
2147
|
+
lines.push(``, debugContext);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return lines.join("\n");
|
|
2151
|
+
}
|
|
2152
|
+
function buildSystemPrompt(mode, context, config, setupLog, agentMode, options) {
|
|
1593
2153
|
const isPm = mode === "pm";
|
|
1594
2154
|
const isPmActive = isPm && agentMode === "building";
|
|
1595
2155
|
const isPackRunner = isPm && !!config.isAuto && !!context.isParentTask;
|
|
@@ -1628,6 +2188,9 @@ Your responses are sent directly to the task chat \u2014 the team sees everythin
|
|
|
1628
2188
|
`Use the mcp__conveyor__create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
|
|
1629
2189
|
);
|
|
1630
2190
|
}
|
|
2191
|
+
if (options?.hasDebugTools) {
|
|
2192
|
+
parts.push(buildDebugModeSection(options.hypothesisTracker));
|
|
2193
|
+
}
|
|
1631
2194
|
const modePrompt = buildModePrompt(agentMode, context);
|
|
1632
2195
|
if (modePrompt) {
|
|
1633
2196
|
parts.push(modePrompt);
|
|
@@ -1657,7 +2220,7 @@ function detectRelaunchScenario(context, trustChatHistory = false) {
|
|
|
1657
2220
|
const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
|
|
1658
2221
|
return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
|
|
1659
2222
|
}
|
|
1660
|
-
function buildRelaunchWithSession(mode, context, agentMode) {
|
|
2223
|
+
function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
|
|
1661
2224
|
const scenario = detectRelaunchScenario(context);
|
|
1662
2225
|
if (!context.claudeSessionId || scenario === "fresh") return null;
|
|
1663
2226
|
const parts = [];
|
|
@@ -1705,7 +2268,7 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
|
|
|
1705
2268
|
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
1706
2269
|
`Review the current state of the codebase and verify everything is working correctly.`
|
|
1707
2270
|
);
|
|
1708
|
-
if (agentMode === "auto" || agentMode === "building") {
|
|
2271
|
+
if (agentMode === "auto" || agentMode === "building" && isAuto) {
|
|
1709
2272
|
parts.push(
|
|
1710
2273
|
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
1711
2274
|
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
@@ -1837,7 +2400,7 @@ CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or g
|
|
|
1837
2400
|
`You are operating autonomously. Begin planning immediately.`,
|
|
1838
2401
|
`1. Explore the codebase to understand the architecture and relevant files`,
|
|
1839
2402
|
`2. Draft a clear implementation plan and save it with update_task`,
|
|
1840
|
-
`3. Set story points
|
|
2403
|
+
`3. Set story points, tags, and title (update_task_properties)`,
|
|
1841
2404
|
`4. When the plan and all required properties are set, call ExitPlanMode to transition to building`,
|
|
1842
2405
|
`Do NOT wait for team input \u2014 proceed autonomously.`
|
|
1843
2406
|
];
|
|
@@ -1911,7 +2474,7 @@ Address the requested changes directly. Do NOT re-investigate the codebase from
|
|
|
1911
2474
|
}
|
|
1912
2475
|
return parts;
|
|
1913
2476
|
}
|
|
1914
|
-
function buildInstructions(mode, context, scenario, agentMode) {
|
|
2477
|
+
function buildInstructions(mode, context, scenario, agentMode, isAuto) {
|
|
1915
2478
|
const parts = [`
|
|
1916
2479
|
## Instructions`];
|
|
1917
2480
|
const isPm = mode === "pm";
|
|
@@ -1943,7 +2506,7 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1943
2506
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
1944
2507
|
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`
|
|
1945
2508
|
);
|
|
1946
|
-
if (agentMode === "auto" || agentMode === "building") {
|
|
2509
|
+
if (agentMode === "auto" || agentMode === "building" && isAuto) {
|
|
1947
2510
|
parts.push(
|
|
1948
2511
|
`If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
|
|
1949
2512
|
`Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
|
|
@@ -1966,13 +2529,13 @@ function buildInstructions(mode, context, scenario, agentMode) {
|
|
|
1966
2529
|
async function buildInitialPrompt(mode, context, isAuto, agentMode) {
|
|
1967
2530
|
const isPackRunner = mode === "pm" && !!isAuto && !!context.isParentTask;
|
|
1968
2531
|
if (!isPackRunner) {
|
|
1969
|
-
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode);
|
|
2532
|
+
const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
|
|
1970
2533
|
if (sessionRelaunch) return sessionRelaunch;
|
|
1971
2534
|
}
|
|
1972
2535
|
const isPm = mode === "pm";
|
|
1973
2536
|
const scenario = detectRelaunchScenario(context, isPm);
|
|
1974
2537
|
const body = await buildTaskBody(context);
|
|
1975
|
-
const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode);
|
|
2538
|
+
const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode, isAuto);
|
|
1976
2539
|
return [...body, ...instructions].join("\n");
|
|
1977
2540
|
}
|
|
1978
2541
|
|
|
@@ -2234,11 +2797,14 @@ function buildCreatePullRequestTool(connection) {
|
|
|
2234
2797
|
"Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
|
|
2235
2798
|
{
|
|
2236
2799
|
title: z.string().describe("The PR title"),
|
|
2237
|
-
body: z.string().describe("The PR description/body in markdown")
|
|
2800
|
+
body: z.string().describe("The PR description/body in markdown"),
|
|
2801
|
+
branch: z.string().optional().describe(
|
|
2802
|
+
"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."
|
|
2803
|
+
)
|
|
2238
2804
|
},
|
|
2239
|
-
async ({ title, body }) => {
|
|
2805
|
+
async ({ title, body, branch }) => {
|
|
2240
2806
|
try {
|
|
2241
|
-
const result = await connection.createPR({ title, body });
|
|
2807
|
+
const result = await connection.createPR({ title, body, branch });
|
|
2242
2808
|
connection.sendEvent({
|
|
2243
2809
|
type: "pr_created",
|
|
2244
2810
|
url: result.url,
|
|
@@ -2444,7 +3010,7 @@ function buildIconTools(connection) {
|
|
|
2444
3010
|
return [
|
|
2445
3011
|
tool3(
|
|
2446
3012
|
"list_icons",
|
|
2447
|
-
"List available icons (default library + user-created). Returns icon IDs, names, and whether they're defaults. Call this FIRST before
|
|
3013
|
+
"List available icons (default library + user-created). Returns icon IDs, names, and whether they're defaults. Call this FIRST before update_task_properties to check for existing matches.",
|
|
2448
3014
|
{},
|
|
2449
3015
|
async () => {
|
|
2450
3016
|
try {
|
|
@@ -2458,23 +3024,6 @@ function buildIconTools(connection) {
|
|
|
2458
3024
|
},
|
|
2459
3025
|
{ annotations: { readOnlyHint: true } }
|
|
2460
3026
|
),
|
|
2461
|
-
tool3(
|
|
2462
|
-
"set_task_icon",
|
|
2463
|
-
"Assign an existing icon to this task by its ID. Use list_icons first to find a matching icon.",
|
|
2464
|
-
{
|
|
2465
|
-
iconId: z3.string().describe("The icon ID to assign")
|
|
2466
|
-
},
|
|
2467
|
-
async ({ iconId }) => {
|
|
2468
|
-
try {
|
|
2469
|
-
await Promise.resolve(connection.updateTaskProperties({ iconId }));
|
|
2470
|
-
return textResult("Icon assigned to task.");
|
|
2471
|
-
} catch (error) {
|
|
2472
|
-
return textResult(
|
|
2473
|
-
`Failed to set icon: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2474
|
-
);
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
),
|
|
2478
3027
|
tool3(
|
|
2479
3028
|
"generate_task_icon",
|
|
2480
3029
|
"Generate a new SVG icon using AI and assign it to this task. Only use if no existing icon from list_icons is a good fit. Provide a concise visual description.",
|
|
@@ -2501,50 +3050,32 @@ function buildDiscoveryTools(connection, context) {
|
|
|
2501
3050
|
const spDescription = buildStoryPointDescription(context?.storyPoints);
|
|
2502
3051
|
return [
|
|
2503
3052
|
tool3(
|
|
2504
|
-
"
|
|
2505
|
-
"Set
|
|
2506
|
-
{ value: z3.number().describe(spDescription) },
|
|
2507
|
-
async ({ value }) => {
|
|
2508
|
-
try {
|
|
2509
|
-
await Promise.resolve(connection.updateTaskProperties({ storyPointValue: value }));
|
|
2510
|
-
return textResult(`Story points set to ${value}`);
|
|
2511
|
-
} catch (error) {
|
|
2512
|
-
return textResult(
|
|
2513
|
-
`Failed to set story points: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2514
|
-
);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
),
|
|
2518
|
-
tool3(
|
|
2519
|
-
"set_task_tags",
|
|
2520
|
-
"Assign tags to this task from the project's available tags. Use the tag IDs from the project tags list.",
|
|
2521
|
-
{
|
|
2522
|
-
tagIds: z3.array(z3.string()).describe("Array of tag IDs to assign")
|
|
2523
|
-
},
|
|
2524
|
-
async ({ tagIds }) => {
|
|
2525
|
-
try {
|
|
2526
|
-
await Promise.resolve(connection.updateTaskProperties({ tagIds }));
|
|
2527
|
-
return textResult(`Tags assigned: ${tagIds.length} tag(s)`);
|
|
2528
|
-
} catch (error) {
|
|
2529
|
-
return textResult(
|
|
2530
|
-
`Failed to set tags: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2531
|
-
);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
),
|
|
2535
|
-
tool3(
|
|
2536
|
-
"set_task_title",
|
|
2537
|
-
"Update the task title to better reflect the planned work.",
|
|
3053
|
+
"update_task_properties",
|
|
3054
|
+
"Set one or more task properties in a single call. All fields are optional \u2014 only include the fields you want to update.",
|
|
2538
3055
|
{
|
|
2539
|
-
title: z3.string().describe("The new task title")
|
|
3056
|
+
title: z3.string().optional().describe("The new task title"),
|
|
3057
|
+
storyPointValue: z3.number().optional().describe(spDescription),
|
|
3058
|
+
tagIds: z3.array(z3.string()).optional().describe("Array of tag IDs to assign"),
|
|
3059
|
+
iconId: z3.string().optional().describe("Icon ID to assign (use list_icons first)")
|
|
2540
3060
|
},
|
|
2541
|
-
async ({ title }) => {
|
|
3061
|
+
async ({ title, storyPointValue, tagIds, iconId }) => {
|
|
2542
3062
|
try {
|
|
2543
|
-
|
|
2544
|
-
|
|
3063
|
+
const updateFields = {};
|
|
3064
|
+
if (title !== void 0) updateFields.title = title;
|
|
3065
|
+
if (storyPointValue !== void 0) updateFields.storyPointValue = storyPointValue;
|
|
3066
|
+
if (tagIds !== void 0) updateFields.tagIds = tagIds;
|
|
3067
|
+
if (iconId !== void 0) updateFields.iconId = iconId;
|
|
3068
|
+
await Promise.resolve(connection.updateTaskProperties(updateFields));
|
|
3069
|
+
const updatedFields = [];
|
|
3070
|
+
if (title !== void 0) updatedFields.push(`title to "${title}"`);
|
|
3071
|
+
if (storyPointValue !== void 0)
|
|
3072
|
+
updatedFields.push(`story points to ${storyPointValue}`);
|
|
3073
|
+
if (tagIds !== void 0) updatedFields.push(`tags (${tagIds.length} tag(s))`);
|
|
3074
|
+
if (iconId !== void 0) updatedFields.push(`icon`);
|
|
3075
|
+
return textResult(`Task properties updated: ${updatedFields.join(", ")}`);
|
|
2545
3076
|
} catch (error) {
|
|
2546
3077
|
return textResult(
|
|
2547
|
-
`Failed to update
|
|
3078
|
+
`Failed to update task properties: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2548
3079
|
);
|
|
2549
3080
|
}
|
|
2550
3081
|
}
|
|
@@ -2553,21 +3084,1272 @@ function buildDiscoveryTools(connection, context) {
|
|
|
2553
3084
|
];
|
|
2554
3085
|
}
|
|
2555
3086
|
|
|
2556
|
-
// src/tools/
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
3087
|
+
// src/tools/debug-tools.ts
|
|
3088
|
+
import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
|
|
3089
|
+
import { z as z6 } from "zod";
|
|
3090
|
+
|
|
3091
|
+
// src/tools/telemetry-tools.ts
|
|
3092
|
+
import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
3093
|
+
import { z as z4 } from "zod";
|
|
3094
|
+
|
|
3095
|
+
// src/debug/telemetry-injector.ts
|
|
3096
|
+
var BUFFER_SIZE = 200;
|
|
3097
|
+
var BODY_MAX_BYTES = 1024;
|
|
3098
|
+
var MAX_DEPTH = 3;
|
|
3099
|
+
var EXCLUDED_EXTENSIONS = [
|
|
3100
|
+
".js",
|
|
3101
|
+
".css",
|
|
3102
|
+
".ico",
|
|
3103
|
+
".png",
|
|
3104
|
+
".jpg",
|
|
3105
|
+
".jpeg",
|
|
3106
|
+
".gif",
|
|
3107
|
+
".svg",
|
|
3108
|
+
".woff",
|
|
3109
|
+
".woff2",
|
|
3110
|
+
".ttf",
|
|
3111
|
+
".eot",
|
|
3112
|
+
".map",
|
|
3113
|
+
".webp"
|
|
3114
|
+
];
|
|
3115
|
+
var EXCLUDED_PATHS = ["/_next/", "/__nextjs", "/favicon", "/_healthz", "/healthz", "/health"];
|
|
3116
|
+
var EXCLUDED_SOCKET_EVENTS = ["ping", "pong", "connection", "disconnect", "websocket", "upgrade"];
|
|
3117
|
+
function scriptPreamble() {
|
|
3118
|
+
return `(function() {
|
|
3119
|
+
if (globalThis.__conveyorTelemetry) {
|
|
3120
|
+
return JSON.stringify({ success: true, alreadyInjected: true, patches: globalThis.__conveyorTelemetry._patches });
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
var BUFFER_SIZE = ${BUFFER_SIZE};
|
|
3124
|
+
var BODY_MAX_BYTES = ${BODY_MAX_BYTES};
|
|
3125
|
+
var MAX_DEPTH = ${MAX_DEPTH};
|
|
3126
|
+
var excludedExtensions = ${JSON.stringify(EXCLUDED_EXTENSIONS)};
|
|
3127
|
+
var excludedPaths = ${JSON.stringify(EXCLUDED_PATHS)};
|
|
3128
|
+
var excludedSocketEvents = ${JSON.stringify(EXCLUDED_SOCKET_EVENTS)};`;
|
|
3129
|
+
}
|
|
3130
|
+
function scriptHelpers() {
|
|
3131
|
+
return `
|
|
3132
|
+
function truncate(str, max) {
|
|
3133
|
+
if (typeof str !== 'string') return '';
|
|
3134
|
+
if (str.length <= max) return str;
|
|
3135
|
+
return str.slice(0, max) + '...[truncated]';
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
function safeStringify(obj, depth) {
|
|
3139
|
+
if (depth === undefined) depth = MAX_DEPTH;
|
|
3140
|
+
try {
|
|
3141
|
+
if (obj === null || obj === undefined) return String(obj);
|
|
3142
|
+
if (typeof obj === 'string') return obj;
|
|
3143
|
+
if (typeof obj !== 'object') return String(obj);
|
|
3144
|
+
if (depth <= 0) return '[Object]';
|
|
3145
|
+
var seen = new Set();
|
|
3146
|
+
return JSON.stringify(obj, function(key, value) {
|
|
3147
|
+
if (typeof value === 'object' && value !== null) {
|
|
3148
|
+
if (seen.has(value)) return '[Circular]';
|
|
3149
|
+
seen.add(value);
|
|
3150
|
+
}
|
|
3151
|
+
return value;
|
|
3152
|
+
}, 0).slice(0, BODY_MAX_BYTES);
|
|
3153
|
+
} catch (e) {
|
|
3154
|
+
return '[unserializable]';
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
function isExcludedUrl(url) {
|
|
3159
|
+
if (!url) return false;
|
|
3160
|
+
var lower = url.toLowerCase();
|
|
3161
|
+
for (var i = 0; i < excludedExtensions.length; i++) {
|
|
3162
|
+
if (lower.endsWith(excludedExtensions[i])) return true;
|
|
3163
|
+
}
|
|
3164
|
+
for (var j = 0; j < excludedPaths.length; j++) {
|
|
3165
|
+
if (lower.indexOf(excludedPaths[j]) !== -1) return true;
|
|
3166
|
+
}
|
|
3167
|
+
return false;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function isExcludedSocketEvent(event) {
|
|
3171
|
+
return excludedSocketEvents.indexOf(event) !== -1;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
function nowMs() { return Date.now(); }`;
|
|
3175
|
+
}
|
|
3176
|
+
function scriptBuffer() {
|
|
3177
|
+
return `
|
|
3178
|
+
var buffer = [];
|
|
3179
|
+
|
|
3180
|
+
function pushEvent(event) {
|
|
3181
|
+
buffer.push(event);
|
|
3182
|
+
if (buffer.length > BUFFER_SIZE) { buffer.shift(); }
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function getEvents(filter, limit) {
|
|
3186
|
+
var result = buffer;
|
|
3187
|
+
if (filter) {
|
|
3188
|
+
if (filter.type) {
|
|
3189
|
+
result = result.filter(function(e) { return e.type === filter.type; });
|
|
3190
|
+
}
|
|
3191
|
+
if (filter.urlPattern) {
|
|
3192
|
+
var re;
|
|
3193
|
+
try { re = new RegExp(filter.urlPattern, 'i'); } catch(e) { re = null; }
|
|
3194
|
+
if (re) {
|
|
3195
|
+
result = result.filter(function(e) { return e.type === 'http' && re.test(e.url); });
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (filter.minDuration) {
|
|
3199
|
+
var minD = filter.minDuration;
|
|
3200
|
+
result = result.filter(function(e) { return ('duration' in e) && e.duration >= minD; });
|
|
3201
|
+
}
|
|
3202
|
+
if (filter.errorOnly) {
|
|
3203
|
+
result = result.filter(function(e) {
|
|
3204
|
+
return e.type === 'error' || (e.type === 'http' && e.status >= 400);
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
if (filter.since) {
|
|
3208
|
+
var since = filter.since;
|
|
3209
|
+
result = result.filter(function(e) { return e.timestamp >= since; });
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (limit && limit > 0) { result = result.slice(-limit); }
|
|
3213
|
+
return result;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function clear() { buffer = []; }
|
|
3217
|
+
|
|
3218
|
+
var patches = { express: false, prisma: false, socketIo: false, errorHandler: false };`;
|
|
3219
|
+
}
|
|
3220
|
+
function scriptPatchExpress() {
|
|
3221
|
+
return `
|
|
3222
|
+
function patchExpress() {
|
|
3223
|
+
try {
|
|
3224
|
+
var http = require('http');
|
|
3225
|
+
var originalEmit = http.Server.prototype.emit;
|
|
3226
|
+
http.Server.prototype.emit = function(event) {
|
|
3227
|
+
if (event === 'request') {
|
|
3228
|
+
var req = arguments[1];
|
|
3229
|
+
var res = arguments[2];
|
|
3230
|
+
if (req && res && !isExcludedUrl(req.url)) {
|
|
3231
|
+
var start = nowMs();
|
|
3232
|
+
var reqBody = '';
|
|
3233
|
+
if (req.readable) {
|
|
3234
|
+
var chunks = [];
|
|
3235
|
+
var origPush = req.push;
|
|
3236
|
+
req.push = function(chunk) {
|
|
3237
|
+
if (chunk) { chunks.push(typeof chunk === 'string' ? chunk : chunk.toString()); }
|
|
3238
|
+
return origPush.apply(this, arguments);
|
|
3239
|
+
};
|
|
3240
|
+
var origReqEmit = req.emit;
|
|
3241
|
+
req.emit = function(evt) {
|
|
3242
|
+
if (evt === 'end') { reqBody = truncate(chunks.join(''), BODY_MAX_BYTES); }
|
|
3243
|
+
return origReqEmit.apply(this, arguments);
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
var origEnd = res.end;
|
|
3247
|
+
res.end = function(chunk) {
|
|
3248
|
+
var resBody = '';
|
|
3249
|
+
if (chunk) { resBody = truncate(typeof chunk === 'string' ? chunk : chunk.toString(), BODY_MAX_BYTES); }
|
|
3250
|
+
var duration = nowMs() - start;
|
|
3251
|
+
pushEvent({
|
|
3252
|
+
type: 'http', timestamp: start, method: req.method || 'UNKNOWN',
|
|
3253
|
+
url: req.url || '/', status: res.statusCode || 0, duration: duration,
|
|
3254
|
+
requestBody: reqBody || undefined, responseBody: resBody || undefined,
|
|
3255
|
+
error: res.statusCode >= 400 ? ('HTTP ' + res.statusCode) : undefined
|
|
3256
|
+
});
|
|
3257
|
+
return origEnd.apply(this, arguments);
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return originalEmit.apply(this, arguments);
|
|
3262
|
+
};
|
|
3263
|
+
patches.express = true;
|
|
3264
|
+
} catch (e) {}
|
|
3265
|
+
}`;
|
|
3266
|
+
}
|
|
3267
|
+
function scriptPatchPrisma() {
|
|
3268
|
+
return `
|
|
3269
|
+
function patchPrisma() {
|
|
3270
|
+
try {
|
|
3271
|
+
var prismaClient = globalThis.prisma || globalThis.__prisma;
|
|
3272
|
+
if (!prismaClient) {
|
|
3273
|
+
var cacheKeys = Object.keys(require.cache || {});
|
|
3274
|
+
for (var i = 0; i < cacheKeys.length; i++) {
|
|
3275
|
+
var mod = require.cache[cacheKeys[i]];
|
|
3276
|
+
if (mod && mod.exports && typeof mod.exports.$on === 'function') {
|
|
3277
|
+
prismaClient = mod.exports;
|
|
3278
|
+
break;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (prismaClient && typeof prismaClient.$on === 'function') {
|
|
3283
|
+
prismaClient.$on('query', function(e) {
|
|
3284
|
+
pushEvent({
|
|
3285
|
+
type: 'db', timestamp: nowMs(),
|
|
3286
|
+
query: truncate(e.query || '', BODY_MAX_BYTES),
|
|
3287
|
+
params: truncate(safeStringify(e.params), BODY_MAX_BYTES),
|
|
3288
|
+
duration: e.duration || 0
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
patches.prisma = true;
|
|
3292
|
+
}
|
|
3293
|
+
} catch (e) {}
|
|
3294
|
+
}`;
|
|
3295
|
+
}
|
|
3296
|
+
function scriptPatchSocketIo() {
|
|
3297
|
+
return `
|
|
3298
|
+
function patchSocketIo() {
|
|
3299
|
+
try {
|
|
3300
|
+
var io = globalThis.io || globalThis.__io;
|
|
3301
|
+
if (io && io.sockets) {
|
|
3302
|
+
var origOn = io.sockets.constructor.prototype.on;
|
|
3303
|
+
if (origOn) {
|
|
3304
|
+
io.sockets.constructor.prototype.on = function(event, handler) {
|
|
3305
|
+
if (!isExcludedSocketEvent(event)) {
|
|
3306
|
+
var wrappedHandler = function() {
|
|
3307
|
+
var args = Array.prototype.slice.call(arguments);
|
|
3308
|
+
pushEvent({
|
|
3309
|
+
type: 'socket', timestamp: nowMs(), event: event, direction: 'in',
|
|
3310
|
+
data: truncate(safeStringify(args[0]), BODY_MAX_BYTES),
|
|
3311
|
+
namespace: (this.nsp && this.nsp.name) || '/'
|
|
3312
|
+
});
|
|
3313
|
+
return handler.apply(this, arguments);
|
|
3314
|
+
};
|
|
3315
|
+
return origOn.call(this, event, wrappedHandler);
|
|
3316
|
+
}
|
|
3317
|
+
return origOn.call(this, event, handler);
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
var origEmit = io.sockets.constructor.prototype.emit;
|
|
3321
|
+
if (origEmit) {
|
|
3322
|
+
io.sockets.constructor.prototype.emit = function(event) {
|
|
3323
|
+
if (!isExcludedSocketEvent(event)) {
|
|
3324
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
3325
|
+
pushEvent({
|
|
3326
|
+
type: 'socket', timestamp: nowMs(), event: event, direction: 'out',
|
|
3327
|
+
data: truncate(safeStringify(args[0]), BODY_MAX_BYTES),
|
|
3328
|
+
namespace: (this.nsp && this.nsp.name) || '/'
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
return origEmit.apply(this, arguments);
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
patches.socketIo = true;
|
|
3335
|
+
}
|
|
3336
|
+
} catch (e) {}
|
|
3337
|
+
}`;
|
|
3338
|
+
}
|
|
3339
|
+
function scriptPatchErrors() {
|
|
3340
|
+
return `
|
|
3341
|
+
function patchErrorHandler() {
|
|
3342
|
+
try {
|
|
3343
|
+
process.on('uncaughtException', function(err) {
|
|
3344
|
+
pushEvent({
|
|
3345
|
+
type: 'error', timestamp: nowMs(),
|
|
3346
|
+
message: err.message || String(err),
|
|
3347
|
+
stack: truncate(err.stack || '', BODY_MAX_BYTES),
|
|
3348
|
+
context: 'uncaughtException'
|
|
3349
|
+
});
|
|
3350
|
+
});
|
|
3351
|
+
process.on('unhandledRejection', function(reason) {
|
|
3352
|
+
var msg = (reason && reason.message) ? reason.message : String(reason);
|
|
3353
|
+
var stack = (reason && reason.stack) ? reason.stack : '';
|
|
3354
|
+
pushEvent({
|
|
3355
|
+
type: 'error', timestamp: nowMs(), message: msg,
|
|
3356
|
+
stack: truncate(stack, BODY_MAX_BYTES), context: 'unhandledRejection'
|
|
3357
|
+
});
|
|
3358
|
+
});
|
|
3359
|
+
patches.errorHandler = true;
|
|
3360
|
+
} catch (e) {}
|
|
3361
|
+
}`;
|
|
3362
|
+
}
|
|
3363
|
+
function scriptInit() {
|
|
3364
|
+
return `
|
|
3365
|
+
patchExpress();
|
|
3366
|
+
patchPrisma();
|
|
3367
|
+
patchSocketIo();
|
|
3368
|
+
patchErrorHandler();
|
|
3369
|
+
|
|
3370
|
+
globalThis.__conveyorTelemetry = {
|
|
3371
|
+
getEvents: getEvents,
|
|
3372
|
+
clear: clear,
|
|
3373
|
+
getStatus: function() {
|
|
3374
|
+
return { active: true, eventCount: buffer.length, patches: patches };
|
|
3375
|
+
},
|
|
3376
|
+
_patches: patches,
|
|
3377
|
+
_buffer: buffer
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
return JSON.stringify({ success: true, alreadyInjected: false, patches: patches });
|
|
3381
|
+
})();`;
|
|
3382
|
+
}
|
|
3383
|
+
function generateTelemetryScript() {
|
|
3384
|
+
return [
|
|
3385
|
+
scriptPreamble(),
|
|
3386
|
+
scriptHelpers(),
|
|
3387
|
+
scriptBuffer(),
|
|
3388
|
+
scriptPatchExpress(),
|
|
3389
|
+
scriptPatchPrisma(),
|
|
3390
|
+
scriptPatchSocketIo(),
|
|
3391
|
+
scriptPatchErrors(),
|
|
3392
|
+
scriptInit()
|
|
3393
|
+
].join("\n");
|
|
3394
|
+
}
|
|
3395
|
+
async function injectTelemetry(cdpClient) {
|
|
3396
|
+
try {
|
|
3397
|
+
const script = generateTelemetryScript();
|
|
3398
|
+
const result = await cdpClient.evaluate(script);
|
|
3399
|
+
if (result.type === "error") {
|
|
3400
|
+
return { success: false, error: result.value };
|
|
3401
|
+
}
|
|
3402
|
+
try {
|
|
3403
|
+
const parsed = JSON.parse(result.value);
|
|
3404
|
+
return {
|
|
3405
|
+
success: parsed.success === true,
|
|
3406
|
+
patches: parsed.patches
|
|
3407
|
+
};
|
|
3408
|
+
} catch {
|
|
3409
|
+
return { success: true };
|
|
3410
|
+
}
|
|
3411
|
+
} catch (error) {
|
|
3412
|
+
return {
|
|
3413
|
+
success: false,
|
|
3414
|
+
error: error instanceof Error ? error.message : "Unknown injection error"
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
async function queryTelemetry(cdpClient, filter, limit) {
|
|
3419
|
+
const filterJson = JSON.stringify(filter ?? {});
|
|
3420
|
+
const limitVal = limit ?? 20;
|
|
3421
|
+
const result = await cdpClient.evaluate(
|
|
3422
|
+
`JSON.stringify(globalThis.__conveyorTelemetry ? globalThis.__conveyorTelemetry.getEvents(${filterJson}, ${limitVal}) : [])`
|
|
3423
|
+
);
|
|
3424
|
+
if (result.type === "error") {
|
|
3425
|
+
throw new Error(`Telemetry query failed: ${result.value}`);
|
|
3426
|
+
}
|
|
3427
|
+
try {
|
|
3428
|
+
let val = result.value;
|
|
3429
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
3430
|
+
val = JSON.parse(val);
|
|
3431
|
+
}
|
|
3432
|
+
return JSON.parse(val);
|
|
3433
|
+
} catch {
|
|
3434
|
+
return [];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
async function clearTelemetry(cdpClient) {
|
|
3438
|
+
await cdpClient.evaluate(
|
|
3439
|
+
`globalThis.__conveyorTelemetry ? (globalThis.__conveyorTelemetry.clear(), 'cleared') : 'not active'`
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
async function getTelemetryStatus(cdpClient) {
|
|
3443
|
+
const result = await cdpClient.evaluate(
|
|
3444
|
+
`JSON.stringify(globalThis.__conveyorTelemetry ? globalThis.__conveyorTelemetry.getStatus() : { active: false, eventCount: 0, patches: { express: false, prisma: false, socketIo: false, errorHandler: false } })`
|
|
3445
|
+
);
|
|
3446
|
+
if (result.type === "error") {
|
|
3447
|
+
return {
|
|
3448
|
+
active: false,
|
|
3449
|
+
eventCount: 0,
|
|
3450
|
+
patches: { express: false, prisma: false, socketIo: false, errorHandler: false }
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
try {
|
|
3454
|
+
let val = result.value;
|
|
3455
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
3456
|
+
val = JSON.parse(val);
|
|
3457
|
+
}
|
|
3458
|
+
return JSON.parse(val);
|
|
3459
|
+
} catch {
|
|
3460
|
+
return {
|
|
3461
|
+
active: false,
|
|
3462
|
+
eventCount: 0,
|
|
3463
|
+
patches: { express: false, prisma: false, socketIo: false, errorHandler: false }
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// src/tools/telemetry-tools.ts
|
|
3469
|
+
function requireDebugClient(manager) {
|
|
3470
|
+
if (!manager.isDebugMode()) {
|
|
3471
|
+
return "Debug mode is not active. Use debug_enter_mode first.";
|
|
3472
|
+
}
|
|
3473
|
+
const client = manager.getClient();
|
|
3474
|
+
if (!client?.isConnected()) {
|
|
3475
|
+
return "CDP client is not connected. Try exiting and re-entering debug mode.";
|
|
3476
|
+
}
|
|
3477
|
+
return client;
|
|
3478
|
+
}
|
|
3479
|
+
function formatError(error) {
|
|
3480
|
+
return error instanceof Error ? error.message : "Unknown error";
|
|
3481
|
+
}
|
|
3482
|
+
function buildGetTelemetryTool(manager) {
|
|
3483
|
+
return tool4(
|
|
3484
|
+
"debug_get_telemetry",
|
|
3485
|
+
"Query structured telemetry events (HTTP requests, database queries, Socket.IO events, errors) captured from the running dev server. Returns filtered, structured data instead of raw logs.",
|
|
3486
|
+
{
|
|
3487
|
+
type: z4.enum(["http", "db", "socket", "error"]).optional().describe("Filter by event type"),
|
|
3488
|
+
urlPattern: z4.string().optional().describe("Regex pattern to filter HTTP events by URL"),
|
|
3489
|
+
minDuration: z4.number().optional().describe("Minimum duration in ms \u2014 only return events slower than this"),
|
|
3490
|
+
errorOnly: z4.boolean().optional().describe("Only return error events and HTTP 4xx/5xx responses"),
|
|
3491
|
+
since: z4.number().optional().describe("Only return events after this timestamp (ms since epoch)"),
|
|
3492
|
+
limit: z4.number().optional().describe("Max events to return (default: 20, from most recent)")
|
|
3493
|
+
},
|
|
3494
|
+
async ({ type, urlPattern, minDuration, errorOnly, since, limit }) => {
|
|
3495
|
+
const clientOrErr = requireDebugClient(manager);
|
|
3496
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3497
|
+
try {
|
|
3498
|
+
const filter = {
|
|
3499
|
+
...type && { type },
|
|
3500
|
+
...urlPattern && { urlPattern },
|
|
3501
|
+
...minDuration && { minDuration },
|
|
3502
|
+
...errorOnly && { errorOnly },
|
|
3503
|
+
...since && { since }
|
|
3504
|
+
};
|
|
3505
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
3506
|
+
const events = await queryTelemetry(clientOrErr, hasFilter ? filter : void 0, limit);
|
|
3507
|
+
if (events.length === 0) {
|
|
3508
|
+
return textResult("No telemetry events found matching the filter.");
|
|
3509
|
+
}
|
|
3510
|
+
return textResult(JSON.stringify(events, null, 2));
|
|
3511
|
+
} catch (error) {
|
|
3512
|
+
return textResult(`Failed to query telemetry: ${formatError(error)}`);
|
|
3513
|
+
}
|
|
3514
|
+
},
|
|
3515
|
+
{ annotations: { readOnlyHint: true } }
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
function buildClearTelemetryTool(manager) {
|
|
3519
|
+
return tool4(
|
|
3520
|
+
"debug_clear_telemetry",
|
|
3521
|
+
"Clear all captured telemetry events from the buffer. Useful to reset before reproducing a specific issue.",
|
|
3522
|
+
{},
|
|
3523
|
+
async () => {
|
|
3524
|
+
const clientOrErr = requireDebugClient(manager);
|
|
3525
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3526
|
+
try {
|
|
3527
|
+
await clearTelemetry(clientOrErr);
|
|
3528
|
+
return textResult("Telemetry buffer cleared.");
|
|
3529
|
+
} catch (error) {
|
|
3530
|
+
return textResult(`Failed to clear telemetry: ${formatError(error)}`);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
function buildTelemetryStatusTool(manager) {
|
|
3536
|
+
return tool4(
|
|
3537
|
+
"debug_telemetry_status",
|
|
3538
|
+
"Check if telemetry is active, how many events have been captured, and which framework patches (Express, Prisma, Socket.IO) were successfully applied.",
|
|
3539
|
+
{},
|
|
3540
|
+
async () => {
|
|
3541
|
+
const clientOrErr = requireDebugClient(manager);
|
|
3542
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3543
|
+
try {
|
|
3544
|
+
const status = await getTelemetryStatus(clientOrErr);
|
|
3545
|
+
return textResult(JSON.stringify(status, null, 2));
|
|
3546
|
+
} catch (error) {
|
|
3547
|
+
return textResult(`Failed to get telemetry status: ${formatError(error)}`);
|
|
3548
|
+
}
|
|
3549
|
+
},
|
|
3550
|
+
{ annotations: { readOnlyHint: true } }
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
function buildTelemetryTools(manager) {
|
|
3554
|
+
return [
|
|
3555
|
+
buildGetTelemetryTool(manager),
|
|
3556
|
+
buildClearTelemetryTool(manager),
|
|
3557
|
+
buildTelemetryStatusTool(manager)
|
|
3558
|
+
];
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// src/tools/client-debug-tools.ts
|
|
3562
|
+
import { tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
|
|
3563
|
+
import { z as z5 } from "zod";
|
|
3564
|
+
function requirePlaywrightClient(manager) {
|
|
3565
|
+
if (!manager.isClientDebugMode()) {
|
|
3566
|
+
return "Client debug mode is not active. Use debug_enter_mode with clientSide: true first.";
|
|
3567
|
+
}
|
|
3568
|
+
const client = manager.getPlaywrightClient();
|
|
3569
|
+
if (!client?.isConnected()) {
|
|
3570
|
+
return "Playwright client is not connected. Try exiting and re-entering debug mode.";
|
|
3571
|
+
}
|
|
3572
|
+
return client;
|
|
3573
|
+
}
|
|
3574
|
+
function formatError2(error) {
|
|
3575
|
+
return error instanceof Error ? error.message : "Unknown error";
|
|
3576
|
+
}
|
|
3577
|
+
function buildClientBreakpointTools(manager) {
|
|
3578
|
+
return [
|
|
3579
|
+
tool5(
|
|
3580
|
+
"debug_set_client_breakpoint",
|
|
3581
|
+
"Set a breakpoint in client-side code running in the headless Chromium browser. V8 resolves source maps automatically, so original .tsx/.ts file paths work. Use this for React components, client utilities, and browser-side code.",
|
|
3582
|
+
{
|
|
3583
|
+
file: z5.string().describe(
|
|
3584
|
+
"Original source file path (e.g., src/components/App.tsx) \u2014 source maps resolve automatically"
|
|
3585
|
+
),
|
|
3586
|
+
line: z5.number().describe("Line number (1-based) in the original source file"),
|
|
3587
|
+
condition: z5.string().optional().describe("JavaScript condition expression \u2014 breakpoint only triggers when truthy")
|
|
3588
|
+
},
|
|
3589
|
+
async ({ file, line, condition }) => {
|
|
3590
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3591
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3592
|
+
try {
|
|
3593
|
+
const breakpointId = await clientOrErr.setBreakpoint(file, line, condition);
|
|
3594
|
+
const condStr = condition ? ` (condition: ${condition})` : "";
|
|
3595
|
+
const sourceMapNote = clientOrErr.hasSourceMaps() === false ? "\n\u26A0\uFE0F Source maps not detected \u2014 breakpoints will reference bundled code." : "";
|
|
3596
|
+
return textResult(
|
|
3597
|
+
`Client breakpoint set: ${file}:${line}${condStr}
|
|
3598
|
+
Breakpoint ID: ${breakpointId}${sourceMapNote}`
|
|
3599
|
+
);
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
return textResult(`Failed to set client breakpoint: ${formatError2(error)}`);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
),
|
|
3605
|
+
tool5(
|
|
3606
|
+
"debug_remove_client_breakpoint",
|
|
3607
|
+
"Remove a previously set client-side breakpoint by its ID.",
|
|
3608
|
+
{
|
|
3609
|
+
breakpointId: z5.string().describe("The breakpoint ID returned by debug_set_client_breakpoint")
|
|
3610
|
+
},
|
|
3611
|
+
async ({ breakpointId }) => {
|
|
3612
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3613
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3614
|
+
try {
|
|
3615
|
+
await clientOrErr.removeBreakpoint(breakpointId);
|
|
3616
|
+
return textResult(`Client breakpoint ${breakpointId} removed.`);
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
return textResult(`Failed to remove client breakpoint: ${formatError2(error)}`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
),
|
|
3622
|
+
tool5(
|
|
3623
|
+
"debug_list_client_breakpoints",
|
|
3624
|
+
"List all active client-side breakpoints with their file, line, and condition.",
|
|
3625
|
+
{},
|
|
3626
|
+
// oxlint-disable-next-line require-await
|
|
3627
|
+
async () => {
|
|
3628
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3629
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3630
|
+
const breakpoints = clientOrErr.listBreakpoints();
|
|
3631
|
+
if (breakpoints.length === 0) {
|
|
3632
|
+
return textResult("No client breakpoints set.");
|
|
3633
|
+
}
|
|
3634
|
+
return textResult(JSON.stringify(breakpoints, null, 2));
|
|
3635
|
+
},
|
|
3636
|
+
{ annotations: { readOnlyHint: true } }
|
|
3637
|
+
)
|
|
3638
|
+
];
|
|
3639
|
+
}
|
|
3640
|
+
function buildClientInspectionTools(manager) {
|
|
3641
|
+
return [
|
|
3642
|
+
tool5(
|
|
3643
|
+
"debug_inspect_client_paused",
|
|
3644
|
+
"When the client-side debugger is paused at a breakpoint, returns the call stack and local variables. Includes React component state, props, and hooks when paused inside a component.",
|
|
3645
|
+
{},
|
|
3646
|
+
async () => {
|
|
3647
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3648
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3649
|
+
const queuedHits = manager.drainClientBreakpointHitQueue();
|
|
3650
|
+
if (!clientOrErr.isPaused()) {
|
|
3651
|
+
if (queuedHits.length > 0) {
|
|
3652
|
+
return textResult(
|
|
3653
|
+
`Client debugger was paused but has since resumed. Recent breakpoint hits:
|
|
3654
|
+
${JSON.stringify(queuedHits, null, 2)}`
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
return textResult(
|
|
3658
|
+
"Client debugger is not currently paused. Set client breakpoints and trigger the code path in the browser to pause execution."
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
try {
|
|
3662
|
+
const callStack = clientOrErr.getCallStack();
|
|
3663
|
+
const topFrame = callStack[0];
|
|
3664
|
+
let variables = [];
|
|
3665
|
+
if (topFrame) {
|
|
3666
|
+
try {
|
|
3667
|
+
variables = await clientOrErr.getScopeVariables(topFrame.callFrameId);
|
|
3668
|
+
} catch {
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
const result = {
|
|
3672
|
+
side: "client",
|
|
3673
|
+
reason: clientOrErr.getPausedState()?.reason,
|
|
3674
|
+
hitBreakpoints: clientOrErr.getPausedState()?.hitBreakpoints,
|
|
3675
|
+
callStack,
|
|
3676
|
+
localVariables: variables
|
|
3677
|
+
};
|
|
3678
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
3679
|
+
} catch (error) {
|
|
3680
|
+
return textResult(`Failed to inspect client paused state: ${formatError2(error)}`);
|
|
3681
|
+
}
|
|
3682
|
+
},
|
|
3683
|
+
{ annotations: { readOnlyHint: true } }
|
|
3684
|
+
),
|
|
3685
|
+
tool5(
|
|
3686
|
+
"debug_evaluate_client",
|
|
3687
|
+
"Evaluate a JavaScript expression in the client-side browser context. When paused at a client breakpoint, evaluates in the paused scope. Can access DOM, window, React internals, etc.",
|
|
3688
|
+
{
|
|
3689
|
+
expression: z5.string().describe("JavaScript expression to evaluate in the browser context"),
|
|
3690
|
+
frameIndex: z5.number().optional().describe("Call stack frame index (0 = top frame). Defaults to the top frame.")
|
|
3691
|
+
},
|
|
3692
|
+
async ({ expression, frameIndex }) => {
|
|
3693
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3694
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3695
|
+
try {
|
|
3696
|
+
let callFrameId;
|
|
3697
|
+
if (clientOrErr.isPaused()) {
|
|
3698
|
+
const callStack = clientOrErr.getCallStack();
|
|
3699
|
+
const frame = callStack[frameIndex ?? 0];
|
|
3700
|
+
callFrameId = frame?.callFrameId;
|
|
3701
|
+
}
|
|
3702
|
+
const result = await clientOrErr.evaluate(expression, callFrameId);
|
|
3703
|
+
return textResult(`(${result.type}) ${result.value}`);
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
return textResult(`Client evaluation failed: ${formatError2(error)}`);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
)
|
|
3709
|
+
];
|
|
3710
|
+
}
|
|
3711
|
+
function buildClientExecutionTools(manager) {
|
|
3712
|
+
return [
|
|
3713
|
+
tool5(
|
|
3714
|
+
"debug_continue_client",
|
|
3715
|
+
"Resume client-side execution after the browser debugger has paused at a breakpoint.",
|
|
3716
|
+
{},
|
|
3717
|
+
async () => {
|
|
3718
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3719
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3720
|
+
if (!clientOrErr.isPaused()) {
|
|
3721
|
+
return textResult("Client debugger is not paused.");
|
|
3722
|
+
}
|
|
3723
|
+
try {
|
|
3724
|
+
await clientOrErr.resume();
|
|
3725
|
+
return textResult("Client execution resumed.");
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
return textResult(`Failed to resume client: ${formatError2(error)}`);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
)
|
|
3731
|
+
];
|
|
3732
|
+
}
|
|
3733
|
+
function buildClientInteractionTools(manager) {
|
|
3734
|
+
return [
|
|
3735
|
+
tool5(
|
|
3736
|
+
"debug_client_screenshot",
|
|
3737
|
+
"Take a screenshot of the current page state in the headless browser. Returns the image as base64-encoded PNG.",
|
|
3738
|
+
{},
|
|
3739
|
+
async () => {
|
|
3740
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3741
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3742
|
+
try {
|
|
3743
|
+
const base64 = await clientOrErr.screenshot();
|
|
3744
|
+
return {
|
|
3745
|
+
content: [
|
|
3746
|
+
imageBlock(base64, "image/png"),
|
|
3747
|
+
{
|
|
3748
|
+
type: "text",
|
|
3749
|
+
text: `Screenshot captured (${clientOrErr.getCurrentUrl()})`
|
|
3750
|
+
}
|
|
3751
|
+
]
|
|
3752
|
+
};
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
return textResult(`Failed to capture screenshot: ${formatError2(error)}`);
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
{ annotations: { readOnlyHint: true } }
|
|
3758
|
+
),
|
|
3759
|
+
tool5(
|
|
3760
|
+
"debug_navigate_client",
|
|
3761
|
+
"Navigate the headless browser to a specific URL. Use this to reproduce specific flows or visit different pages.",
|
|
3762
|
+
{
|
|
3763
|
+
url: z5.string().describe("URL to navigate to (e.g., http://localhost:3000/dashboard)")
|
|
3764
|
+
},
|
|
3765
|
+
async ({ url }) => {
|
|
3766
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3767
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3768
|
+
try {
|
|
3769
|
+
await clientOrErr.navigate(url);
|
|
3770
|
+
return textResult(`Navigated to: ${clientOrErr.getCurrentUrl()}`);
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
return textResult(`Failed to navigate: ${formatError2(error)}`);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
),
|
|
3776
|
+
tool5(
|
|
3777
|
+
"debug_click_client",
|
|
3778
|
+
"Click an element on the page in the headless browser. Use CSS selectors to target elements. Useful for reproducing bugs by interacting with the UI programmatically.",
|
|
3779
|
+
{
|
|
3780
|
+
selector: z5.string().describe(
|
|
3781
|
+
"CSS selector of the element to click (e.g., 'button.submit', '#login-form input[type=submit]')"
|
|
3782
|
+
)
|
|
3783
|
+
},
|
|
3784
|
+
async ({ selector }) => {
|
|
3785
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3786
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3787
|
+
try {
|
|
3788
|
+
await clientOrErr.click(selector);
|
|
3789
|
+
return textResult(`Clicked: ${selector}`);
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
return textResult(`Failed to click "${selector}": ${formatError2(error)}`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
)
|
|
3795
|
+
];
|
|
3796
|
+
}
|
|
3797
|
+
function buildClientConsoleTool(manager) {
|
|
3798
|
+
return tool5(
|
|
3799
|
+
"debug_get_client_console",
|
|
3800
|
+
"Get console messages captured from the headless browser. Includes console.log, warn, error, etc.",
|
|
3801
|
+
{
|
|
3802
|
+
level: z5.string().optional().describe("Filter by console level: log, warn, error, info, debug"),
|
|
3803
|
+
limit: z5.number().optional().describe("Maximum number of recent messages to return (default: all)")
|
|
3804
|
+
},
|
|
3805
|
+
// oxlint-disable-next-line require-await
|
|
3806
|
+
async ({ level, limit }) => {
|
|
3807
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3808
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3809
|
+
const messages = clientOrErr.getConsoleMessages(level, limit);
|
|
3810
|
+
if (messages.length === 0) {
|
|
3811
|
+
const filterDesc = level ? ` (level: ${level})` : "";
|
|
3812
|
+
return textResult(`No console messages captured${filterDesc}.`);
|
|
3813
|
+
}
|
|
3814
|
+
const formatted = messages.map((m) => {
|
|
3815
|
+
const time = new Date(m.timestamp).toLocaleTimeString("en-US", { hour12: false });
|
|
3816
|
+
const loc = m.url ? ` [${m.url}${m.line ? `:${m.line}` : ""}]` : "";
|
|
3817
|
+
return `[${time}] ${m.level.toUpperCase()}: ${m.text}${loc}`;
|
|
3818
|
+
}).join("\n");
|
|
3819
|
+
return textResult(`${messages.length} console message(s):
|
|
3820
|
+
${formatted}`);
|
|
3821
|
+
},
|
|
3822
|
+
{ annotations: { readOnlyHint: true } }
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
function buildClientNetworkTool(manager) {
|
|
3826
|
+
return tool5(
|
|
3827
|
+
"debug_get_client_network",
|
|
3828
|
+
"Get network requests captured from the headless browser. Shows URLs, methods, status codes, and timing.",
|
|
3829
|
+
{
|
|
3830
|
+
filter: z5.string().optional().describe("Regex pattern to filter requests by URL"),
|
|
3831
|
+
limit: z5.number().optional().describe("Maximum number of recent requests to return (default: all)")
|
|
3832
|
+
},
|
|
3833
|
+
// oxlint-disable-next-line require-await
|
|
3834
|
+
async ({ filter, limit }) => {
|
|
3835
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3836
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3837
|
+
const requests = clientOrErr.getNetworkRequests(filter, limit);
|
|
3838
|
+
if (requests.length === 0) {
|
|
3839
|
+
const filterDesc = filter ? ` matching "${filter}"` : "";
|
|
3840
|
+
return textResult(`No network requests captured${filterDesc}.`);
|
|
3841
|
+
}
|
|
3842
|
+
const formatted = requests.map((r) => {
|
|
3843
|
+
const time = new Date(r.timestamp).toLocaleTimeString("en-US", { hour12: false });
|
|
3844
|
+
const status = r.status ? ` \u2192 ${r.status}` : " \u2192 (pending)";
|
|
3845
|
+
const dur = r.duration ? ` (${r.duration}ms)` : "";
|
|
3846
|
+
return `[${time}] ${r.method} ${r.url}${status}${dur}`;
|
|
3847
|
+
}).join("\n");
|
|
3848
|
+
return textResult(`${requests.length} network request(s):
|
|
3849
|
+
${formatted}`);
|
|
3850
|
+
},
|
|
3851
|
+
{ annotations: { readOnlyHint: true } }
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
function buildClientErrorsTool(manager) {
|
|
3855
|
+
return tool5(
|
|
3856
|
+
"debug_get_client_errors",
|
|
3857
|
+
"Get uncaught errors captured from the headless browser. Includes error messages and source-mapped stack traces.",
|
|
3858
|
+
{
|
|
3859
|
+
limit: z5.number().optional().describe("Maximum number of recent errors to return (default: all)")
|
|
3860
|
+
},
|
|
3861
|
+
// oxlint-disable-next-line require-await
|
|
3862
|
+
async ({ limit }) => {
|
|
3863
|
+
const clientOrErr = requirePlaywrightClient(manager);
|
|
3864
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
3865
|
+
const errors = clientOrErr.getPageErrors(limit);
|
|
3866
|
+
if (errors.length === 0) {
|
|
3867
|
+
return textResult("No uncaught page errors captured.");
|
|
3868
|
+
}
|
|
3869
|
+
const formatted = errors.map((e) => {
|
|
3870
|
+
const time = new Date(e.timestamp).toLocaleTimeString("en-US", { hour12: false });
|
|
3871
|
+
const stack = e.stack ? `
|
|
3872
|
+
${e.stack.split("\n").slice(0, 5).join("\n ")}` : "";
|
|
3873
|
+
return `[${time}] ${e.message}${stack}`;
|
|
3874
|
+
}).join("\n\n");
|
|
3875
|
+
return textResult(`${errors.length} page error(s):
|
|
3876
|
+
${formatted}`);
|
|
3877
|
+
},
|
|
3878
|
+
{ annotations: { readOnlyHint: true } }
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
function buildClientCaptureTools(manager) {
|
|
3882
|
+
return [
|
|
3883
|
+
buildClientConsoleTool(manager),
|
|
3884
|
+
buildClientNetworkTool(manager),
|
|
3885
|
+
buildClientErrorsTool(manager)
|
|
3886
|
+
];
|
|
3887
|
+
}
|
|
3888
|
+
function buildClientDebugTools(manager) {
|
|
3889
|
+
return [
|
|
3890
|
+
...buildClientBreakpointTools(manager),
|
|
3891
|
+
...buildClientInspectionTools(manager),
|
|
3892
|
+
...buildClientExecutionTools(manager),
|
|
3893
|
+
...buildClientInteractionTools(manager),
|
|
3894
|
+
...buildClientCaptureTools(manager)
|
|
3895
|
+
];
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
// src/tools/debug-tools.ts
|
|
3899
|
+
function requireDebugClient2(manager) {
|
|
3900
|
+
if (!manager.isDebugMode()) {
|
|
3901
|
+
return "Debug mode is not active. Use debug_enter_mode first.";
|
|
3902
|
+
}
|
|
3903
|
+
const client = manager.getClient();
|
|
3904
|
+
if (!client?.isConnected()) {
|
|
3905
|
+
return "CDP client is not connected. Try exiting and re-entering debug mode.";
|
|
3906
|
+
}
|
|
3907
|
+
return client;
|
|
3908
|
+
}
|
|
3909
|
+
function formatError3(error) {
|
|
3910
|
+
return error instanceof Error ? error.message : "Unknown error";
|
|
3911
|
+
}
|
|
3912
|
+
async function handleEnterDebugMode(manager, {
|
|
3913
|
+
hypothesis,
|
|
3914
|
+
serverSide,
|
|
3915
|
+
clientSide,
|
|
3916
|
+
previewUrl
|
|
3917
|
+
}) {
|
|
3918
|
+
const wantServer = serverSide ?? !clientSide;
|
|
3919
|
+
const wantClient = clientSide ?? false;
|
|
3920
|
+
const alreadyMsg = checkAlreadyActive(manager, wantServer, wantClient);
|
|
3921
|
+
if (alreadyMsg) return textResult(alreadyMsg);
|
|
3922
|
+
await manager.enterDebugMode(void 0, {
|
|
3923
|
+
serverSide: wantServer && !manager.isDebugMode(),
|
|
3924
|
+
clientSide: wantClient && !manager.isClientDebugMode(),
|
|
3925
|
+
previewUrl
|
|
3926
|
+
});
|
|
3927
|
+
return textResult(buildActivationMessage(manager, hypothesis));
|
|
3928
|
+
}
|
|
3929
|
+
function checkAlreadyActive(manager, wantServer, wantClient) {
|
|
3930
|
+
if (wantServer && manager.isDebugMode() && !wantClient) return "Already in server debug mode.";
|
|
3931
|
+
if (wantClient && manager.isClientDebugMode() && !wantServer)
|
|
3932
|
+
return "Already in client debug mode.";
|
|
3933
|
+
if (wantServer && manager.isDebugMode() && wantClient && manager.isClientDebugMode()) {
|
|
3934
|
+
return "Already in both server and client debug mode.";
|
|
3935
|
+
}
|
|
3936
|
+
return null;
|
|
3937
|
+
}
|
|
3938
|
+
function buildActivationMessage(manager, hypothesis) {
|
|
3939
|
+
const modes = [];
|
|
3940
|
+
if (manager.isDebugMode()) modes.push("server");
|
|
3941
|
+
if (manager.isClientDebugMode()) modes.push("client");
|
|
3942
|
+
const modeStr = modes.join(" + ");
|
|
3943
|
+
const sourceMapWarning = manager.getPlaywrightClient()?.hasSourceMaps() === false ? "\n\u26A0\uFE0F Source maps not detected in the client \u2014 client breakpoints will reference bundled code." : "";
|
|
3944
|
+
return hypothesis ? `Debug mode activated (${modeStr}). Hypothesis: ${hypothesis}
|
|
3945
|
+
Set breakpoints to test your hypothesis.${sourceMapWarning}` : `Debug mode activated (${modeStr}). Set breakpoints to begin debugging.${sourceMapWarning}`;
|
|
3946
|
+
}
|
|
3947
|
+
function buildDebugLifecycleTools(manager) {
|
|
3948
|
+
return [
|
|
3949
|
+
tool6(
|
|
3950
|
+
"debug_enter_mode",
|
|
3951
|
+
"Activate debug mode: restarts the dev server with Node.js --inspect flag and connects the CDP debugger. Optionally launch a headless Chromium browser for client-side debugging. Use serverSide for backend breakpoints, clientSide for frontend breakpoints, or both for full-stack.",
|
|
3952
|
+
{
|
|
3953
|
+
hypothesis: z6.string().optional().describe("Your hypothesis about the bug \u2014 helps track debugging intent"),
|
|
3954
|
+
serverSide: z6.boolean().optional().describe(
|
|
3955
|
+
"Enable server-side Node.js debugging (default: true if clientSide is not set)"
|
|
3956
|
+
),
|
|
3957
|
+
clientSide: z6.boolean().optional().describe("Enable client-side browser debugging via headless Chromium + Playwright"),
|
|
3958
|
+
previewUrl: z6.string().optional().describe(
|
|
3959
|
+
"Preview URL for client-side debugging (e.g., http://localhost:3000). Required when clientSide is true."
|
|
3960
|
+
)
|
|
3961
|
+
},
|
|
3962
|
+
async (params) => {
|
|
3963
|
+
try {
|
|
3964
|
+
return await handleEnterDebugMode(manager, params);
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
return textResult(`Failed to enter debug mode: ${formatError3(error)}`);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
),
|
|
3970
|
+
tool6(
|
|
3971
|
+
"debug_exit_mode",
|
|
3972
|
+
"Exit debug mode: removes all breakpoints, disconnects the debugger, and restarts the dev server normally.",
|
|
3973
|
+
{},
|
|
3974
|
+
async () => {
|
|
3975
|
+
try {
|
|
3976
|
+
if (!manager.isDebugMode()) {
|
|
3977
|
+
return textResult("Not in debug mode.");
|
|
3978
|
+
}
|
|
3979
|
+
await manager.exitDebugMode();
|
|
3980
|
+
return textResult("Debug mode deactivated. Dev server restarted normally.");
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
return textResult(`Failed to exit debug mode: ${formatError3(error)}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
)
|
|
3986
|
+
];
|
|
3987
|
+
}
|
|
3988
|
+
function buildBreakpointTools(manager) {
|
|
3989
|
+
return [
|
|
3990
|
+
tool6(
|
|
3991
|
+
"debug_set_breakpoint",
|
|
3992
|
+
"Set a breakpoint at the specified file and line number. Optionally provide a condition expression that must evaluate to true for the breakpoint to pause execution.",
|
|
3993
|
+
{
|
|
3994
|
+
file: z6.string().describe("Absolute or relative file path to set the breakpoint in"),
|
|
3995
|
+
line: z6.number().describe("Line number (1-based) to set the breakpoint on"),
|
|
3996
|
+
condition: z6.string().optional().describe("JavaScript condition expression \u2014 breakpoint only triggers when truthy")
|
|
3997
|
+
},
|
|
3998
|
+
async ({ file, line, condition }) => {
|
|
3999
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4000
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4001
|
+
try {
|
|
4002
|
+
const client = clientOrErr;
|
|
4003
|
+
const breakpointId = await client.setBreakpoint(file, line, condition);
|
|
4004
|
+
const condStr = condition ? ` (condition: ${condition})` : "";
|
|
4005
|
+
return textResult(
|
|
4006
|
+
`Breakpoint set: ${file}:${line}${condStr}
|
|
4007
|
+
Breakpoint ID: ${breakpointId}`
|
|
4008
|
+
);
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
return textResult(`Failed to set breakpoint: ${formatError3(error)}`);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
),
|
|
4014
|
+
tool6(
|
|
4015
|
+
"debug_remove_breakpoint",
|
|
4016
|
+
"Remove a previously set breakpoint by its ID.",
|
|
4017
|
+
{
|
|
4018
|
+
breakpointId: z6.string().describe("The breakpoint ID returned by debug_set_breakpoint")
|
|
4019
|
+
},
|
|
4020
|
+
async ({ breakpointId }) => {
|
|
4021
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4022
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4023
|
+
try {
|
|
4024
|
+
const client = clientOrErr;
|
|
4025
|
+
await client.removeBreakpoint(breakpointId);
|
|
4026
|
+
return textResult(`Breakpoint ${breakpointId} removed.`);
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
return textResult(`Failed to remove breakpoint: ${formatError3(error)}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
),
|
|
4032
|
+
tool6(
|
|
4033
|
+
"debug_list_breakpoints",
|
|
4034
|
+
"List all currently active breakpoints with their file, line, and condition.",
|
|
4035
|
+
{},
|
|
4036
|
+
// oxlint-disable-next-line require-await
|
|
4037
|
+
async () => {
|
|
4038
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4039
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4040
|
+
const breakpoints = clientOrErr.listBreakpoints();
|
|
4041
|
+
if (breakpoints.length === 0) {
|
|
4042
|
+
return textResult("No breakpoints set.");
|
|
4043
|
+
}
|
|
4044
|
+
return textResult(JSON.stringify(breakpoints, null, 2));
|
|
4045
|
+
},
|
|
4046
|
+
{ annotations: { readOnlyHint: true } }
|
|
4047
|
+
)
|
|
4048
|
+
];
|
|
4049
|
+
}
|
|
4050
|
+
function buildInspectionTools(manager) {
|
|
4051
|
+
return [
|
|
4052
|
+
tool6(
|
|
4053
|
+
"debug_inspect_paused",
|
|
4054
|
+
"When the debugger is paused at a breakpoint, returns the call stack and local variables. Check this after a breakpoint is hit to understand the current execution state.",
|
|
4055
|
+
{},
|
|
4056
|
+
async () => {
|
|
4057
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4058
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4059
|
+
const client = clientOrErr;
|
|
4060
|
+
const queuedHits = manager.drainBreakpointHitQueue();
|
|
4061
|
+
if (!client.isPaused()) {
|
|
4062
|
+
if (queuedHits.length > 0) {
|
|
4063
|
+
return textResult(
|
|
4064
|
+
`Debugger was paused but has since resumed. Recent breakpoint hits:
|
|
4065
|
+
${JSON.stringify(queuedHits, null, 2)}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
return textResult(
|
|
4069
|
+
"Debugger is not currently paused. Set breakpoints and trigger the code path to pause execution."
|
|
4070
|
+
);
|
|
4071
|
+
}
|
|
4072
|
+
try {
|
|
4073
|
+
const callStack = client.getCallStack();
|
|
4074
|
+
const topFrame = callStack[0];
|
|
4075
|
+
let variables = [];
|
|
4076
|
+
if (topFrame) {
|
|
4077
|
+
try {
|
|
4078
|
+
variables = await client.getScopeVariables(topFrame.callFrameId);
|
|
4079
|
+
} catch {
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
const result = {
|
|
4083
|
+
reason: client.getPausedState()?.reason,
|
|
4084
|
+
hitBreakpoints: client.getPausedState()?.hitBreakpoints,
|
|
4085
|
+
callStack,
|
|
4086
|
+
localVariables: variables
|
|
4087
|
+
};
|
|
4088
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
4089
|
+
} catch (error) {
|
|
4090
|
+
return textResult(`Failed to inspect paused state: ${formatError3(error)}`);
|
|
4091
|
+
}
|
|
4092
|
+
},
|
|
4093
|
+
{ annotations: { readOnlyHint: true } }
|
|
4094
|
+
),
|
|
4095
|
+
tool6(
|
|
4096
|
+
"debug_evaluate",
|
|
4097
|
+
"Evaluate a JavaScript expression in the current paused scope (or globally if not paused). When paused, use frameIndex to evaluate in a specific call frame.",
|
|
4098
|
+
{
|
|
4099
|
+
expression: z6.string().describe("The JavaScript expression to evaluate"),
|
|
4100
|
+
frameIndex: z6.number().optional().describe("Call stack frame index (0 = top frame). Defaults to the top frame.")
|
|
4101
|
+
},
|
|
4102
|
+
async ({ expression, frameIndex }) => {
|
|
4103
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4104
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4105
|
+
try {
|
|
4106
|
+
const client = clientOrErr;
|
|
4107
|
+
let callFrameId;
|
|
4108
|
+
if (client.isPaused()) {
|
|
4109
|
+
const callStack = client.getCallStack();
|
|
4110
|
+
const frame = callStack[frameIndex ?? 0];
|
|
4111
|
+
callFrameId = frame?.callFrameId;
|
|
4112
|
+
}
|
|
4113
|
+
const result = await client.evaluate(expression, callFrameId);
|
|
4114
|
+
return textResult(`(${result.type}) ${result.value}`);
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
return textResult(`Evaluation failed: ${formatError3(error)}`);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
)
|
|
4120
|
+
];
|
|
4121
|
+
}
|
|
4122
|
+
function buildProbeManagementTools(manager) {
|
|
4123
|
+
return [
|
|
4124
|
+
tool6(
|
|
4125
|
+
"debug_add_probe",
|
|
4126
|
+
"Add a debug probe at a specific code location. Captures expression values each time the line executes \u2014 without pausing or modifying source files. Like console.log but better: structured, no diff pollution, auto-cleaned on debug exit.",
|
|
4127
|
+
{
|
|
4128
|
+
file: z6.string().describe("File path to probe"),
|
|
4129
|
+
line: z6.number().describe("Line number (1-based) to probe"),
|
|
4130
|
+
expressions: z6.array(z6.string()).describe(
|
|
4131
|
+
'JavaScript expressions to capture when the line executes (e.g., ["req.params.id", "user.role"])'
|
|
4132
|
+
),
|
|
4133
|
+
label: z6.string().optional().describe("Optional label for this probe (defaults to file:line)")
|
|
4134
|
+
},
|
|
4135
|
+
async ({ file, line, expressions, label }) => {
|
|
4136
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4137
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4138
|
+
try {
|
|
4139
|
+
const probeId = await clientOrErr.setLogpoint(file, line, expressions, label);
|
|
4140
|
+
const probeLabel = label ?? `${file}:${line}`;
|
|
4141
|
+
const exprList = expressions.join(", ");
|
|
4142
|
+
return textResult(
|
|
4143
|
+
`Probe "${probeLabel}" set at ${file}:${line}
|
|
4144
|
+
Capturing: ${exprList}
|
|
4145
|
+
Probe ID: ${probeId}
|
|
4146
|
+
|
|
4147
|
+
Trigger the code path, then use debug_get_probe_results to see captured values.`
|
|
4148
|
+
);
|
|
4149
|
+
} catch (error) {
|
|
4150
|
+
return textResult(`Failed to add probe: ${formatError3(error)}`);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
),
|
|
4154
|
+
tool6(
|
|
4155
|
+
"debug_remove_probe",
|
|
4156
|
+
"Remove a previously set debug probe by its ID.",
|
|
4157
|
+
{
|
|
4158
|
+
probeId: z6.string().describe("The probe ID returned by debug_add_probe")
|
|
4159
|
+
},
|
|
4160
|
+
async ({ probeId }) => {
|
|
4161
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4162
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4163
|
+
try {
|
|
4164
|
+
await clientOrErr.removeProbe(probeId);
|
|
4165
|
+
return textResult(`Probe ${probeId} removed.`);
|
|
4166
|
+
} catch (error) {
|
|
4167
|
+
return textResult(`Failed to remove probe: ${formatError3(error)}`);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
),
|
|
4171
|
+
tool6(
|
|
4172
|
+
"debug_list_probes",
|
|
4173
|
+
"List all active debug probes with their file, line, expressions, and labels.",
|
|
4174
|
+
{},
|
|
4175
|
+
// oxlint-disable-next-line require-await
|
|
4176
|
+
async () => {
|
|
4177
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4178
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4179
|
+
const probes = clientOrErr.listProbes();
|
|
4180
|
+
if (probes.length === 0) {
|
|
4181
|
+
return textResult("No probes set.");
|
|
4182
|
+
}
|
|
4183
|
+
const lines = probes.map(
|
|
4184
|
+
(p) => `${p.probeId}: "${p.label}" at ${p.file}:${p.line} \u2014 [${p.expressions.join(", ")}]`
|
|
4185
|
+
);
|
|
4186
|
+
return textResult(lines.join("\n"));
|
|
4187
|
+
},
|
|
4188
|
+
{ annotations: { readOnlyHint: true } }
|
|
4189
|
+
)
|
|
4190
|
+
];
|
|
4191
|
+
}
|
|
4192
|
+
function buildProbeResultTools(manager) {
|
|
4193
|
+
return [
|
|
4194
|
+
tool6(
|
|
4195
|
+
"debug_get_probe_results",
|
|
4196
|
+
"Fetch captured probe hit data. Returns expression values from each time a probed line executed.",
|
|
4197
|
+
{
|
|
4198
|
+
probeId: z6.string().optional().describe("Filter results by probe ID (resolves to its label)"),
|
|
4199
|
+
label: z6.string().optional().describe("Filter results by probe label"),
|
|
4200
|
+
limit: z6.number().optional().describe("Maximum number of recent hits to return (default: all)")
|
|
4201
|
+
},
|
|
4202
|
+
async ({ probeId, label, limit }) => {
|
|
4203
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4204
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4205
|
+
try {
|
|
4206
|
+
let filterLabel = label;
|
|
4207
|
+
if (probeId && !filterLabel) {
|
|
4208
|
+
const probe = clientOrErr.listProbes().find((p) => p.probeId === probeId);
|
|
4209
|
+
if (probe) filterLabel = probe.label;
|
|
4210
|
+
}
|
|
4211
|
+
const hits = await clientOrErr.getProbeResults(filterLabel, limit);
|
|
4212
|
+
if (hits.length === 0) {
|
|
4213
|
+
const filterDesc = filterLabel ? ` for "${filterLabel}"` : "";
|
|
4214
|
+
return textResult(
|
|
4215
|
+
`No probe hits${filterDesc}. Make sure the probed code path has been triggered.`
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
const formatted = formatProbeHits(hits);
|
|
4219
|
+
return textResult(formatted);
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
return textResult(`Failed to get probe results: ${formatError3(error)}`);
|
|
4222
|
+
}
|
|
4223
|
+
},
|
|
4224
|
+
{ annotations: { readOnlyHint: true } }
|
|
4225
|
+
)
|
|
4226
|
+
];
|
|
4227
|
+
}
|
|
4228
|
+
function formatProbeHits(hits) {
|
|
4229
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4230
|
+
for (const hit of hits) {
|
|
4231
|
+
const group = grouped.get(hit.label) ?? [];
|
|
4232
|
+
group.push(hit);
|
|
4233
|
+
grouped.set(hit.label, group);
|
|
4234
|
+
}
|
|
4235
|
+
const sections = [];
|
|
4236
|
+
for (const [label, labelHits] of grouped) {
|
|
4237
|
+
const header = `Probe "${label}" \u2014 hit ${labelHits.length} time${labelHits.length === 1 ? "" : "s"}:`;
|
|
4238
|
+
const lines = labelHits.map((hit) => {
|
|
4239
|
+
const time = new Date(hit.timestamp).toLocaleTimeString("en-US", { hour12: false });
|
|
4240
|
+
const entries = Object.entries(hit.data).map(([key, val]) => `${key}=${formatProbeValue(val)}`).join(", ");
|
|
4241
|
+
return ` [${time}] ${entries}`;
|
|
4242
|
+
});
|
|
4243
|
+
sections.push([header, ...lines].join("\n"));
|
|
4244
|
+
}
|
|
4245
|
+
return sections.join("\n\n");
|
|
4246
|
+
}
|
|
4247
|
+
function formatProbeValue(value) {
|
|
4248
|
+
if (value === null) return "null";
|
|
4249
|
+
if (value === void 0) return "undefined";
|
|
4250
|
+
if (typeof value === "string") {
|
|
4251
|
+
return value.length > 100 ? `"${value.slice(0, 97)}..."` : `"${value}"`;
|
|
4252
|
+
}
|
|
4253
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4254
|
+
if (Array.isArray(value)) {
|
|
4255
|
+
return `Array(${value.length})`;
|
|
4256
|
+
}
|
|
4257
|
+
if (typeof value === "object") {
|
|
4258
|
+
const keys = Object.keys(value);
|
|
4259
|
+
if (keys.length <= 3) {
|
|
4260
|
+
const preview = keys.map((k) => `${k}: ${formatProbeValue(value[k])}`).join(", ");
|
|
4261
|
+
return `{${preview}}`;
|
|
4262
|
+
}
|
|
4263
|
+
return `Object(${keys.length} keys)`;
|
|
4264
|
+
}
|
|
4265
|
+
return String(value);
|
|
4266
|
+
}
|
|
4267
|
+
function buildExecutionControlTools(manager) {
|
|
4268
|
+
return [
|
|
4269
|
+
tool6(
|
|
4270
|
+
"debug_continue",
|
|
4271
|
+
"Resume execution after the debugger has paused at a breakpoint.",
|
|
4272
|
+
{},
|
|
4273
|
+
async () => {
|
|
4274
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4275
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4276
|
+
if (!clientOrErr.isPaused()) {
|
|
4277
|
+
return textResult("Debugger is not paused.");
|
|
4278
|
+
}
|
|
4279
|
+
try {
|
|
4280
|
+
await clientOrErr.resume();
|
|
4281
|
+
return textResult("Execution resumed.");
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
return textResult(`Failed to resume: ${formatError3(error)}`);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
),
|
|
4287
|
+
tool6(
|
|
4288
|
+
"debug_step_over",
|
|
4289
|
+
"Step over the current line while paused at a breakpoint. Executes the current line and pauses at the next line in the same function.",
|
|
4290
|
+
{},
|
|
4291
|
+
async () => {
|
|
4292
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4293
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4294
|
+
if (!clientOrErr.isPaused()) {
|
|
4295
|
+
return textResult("Debugger is not paused.");
|
|
4296
|
+
}
|
|
4297
|
+
try {
|
|
4298
|
+
await clientOrErr.stepOver();
|
|
4299
|
+
return textResult("Stepped over. Use debug_inspect_paused to see current state.");
|
|
4300
|
+
} catch (error) {
|
|
4301
|
+
return textResult(`Failed to step over: ${formatError3(error)}`);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
),
|
|
4305
|
+
tool6(
|
|
4306
|
+
"debug_step_into",
|
|
4307
|
+
"Step into the function call on the current line while paused at a breakpoint. Pauses at the first line inside the called function.",
|
|
4308
|
+
{},
|
|
4309
|
+
async () => {
|
|
4310
|
+
const clientOrErr = requireDebugClient2(manager);
|
|
4311
|
+
if (typeof clientOrErr === "string") return textResult(clientOrErr);
|
|
4312
|
+
if (!clientOrErr.isPaused()) {
|
|
4313
|
+
return textResult("Debugger is not paused.");
|
|
4314
|
+
}
|
|
4315
|
+
try {
|
|
4316
|
+
await clientOrErr.stepInto();
|
|
4317
|
+
return textResult("Stepped into. Use debug_inspect_paused to see current state.");
|
|
4318
|
+
} catch (error) {
|
|
4319
|
+
return textResult(`Failed to step into: ${formatError3(error)}`);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
)
|
|
4323
|
+
];
|
|
4324
|
+
}
|
|
4325
|
+
function buildDebugTools(manager) {
|
|
4326
|
+
return [
|
|
4327
|
+
...buildDebugLifecycleTools(manager),
|
|
4328
|
+
...buildBreakpointTools(manager),
|
|
4329
|
+
...buildProbeManagementTools(manager),
|
|
4330
|
+
...buildProbeResultTools(manager),
|
|
4331
|
+
...buildInspectionTools(manager),
|
|
4332
|
+
...buildExecutionControlTools(manager),
|
|
4333
|
+
...buildTelemetryTools(manager),
|
|
4334
|
+
...buildClientDebugTools(manager)
|
|
4335
|
+
];
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
// src/tools/index.ts
|
|
4339
|
+
function textResult(text) {
|
|
4340
|
+
return { content: [{ type: "text", text }] };
|
|
4341
|
+
}
|
|
4342
|
+
function imageBlock(data, mimeType) {
|
|
4343
|
+
return { type: "image", data, mimeType };
|
|
4344
|
+
}
|
|
4345
|
+
function getTaskModeTools(agentMode, connection) {
|
|
4346
|
+
if (agentMode === "discovery" || agentMode === "auto") {
|
|
4347
|
+
return [buildUpdateTaskTool(connection)];
|
|
4348
|
+
}
|
|
4349
|
+
return [];
|
|
4350
|
+
}
|
|
4351
|
+
function getModeTools(agentMode, connection, config, context) {
|
|
4352
|
+
if (config.mode === "task") return getTaskModeTools(agentMode, connection);
|
|
2571
4353
|
switch (agentMode) {
|
|
2572
4354
|
case "building":
|
|
2573
4355
|
return context?.isParentTask ? buildPmTools(connection, context?.storyPoints, { includePackTools: true }) : [];
|
|
@@ -2582,14 +4364,15 @@ function getModeTools(agentMode, connection, config, context) {
|
|
|
2582
4364
|
return config.mode === "pm" ? buildPmTools(connection, context?.storyPoints, { includePackTools: false }) : [];
|
|
2583
4365
|
}
|
|
2584
4366
|
}
|
|
2585
|
-
function createConveyorMcpServer(connection, config, context, agentMode) {
|
|
4367
|
+
function createConveyorMcpServer(connection, config, context, agentMode, debugManager) {
|
|
2586
4368
|
const commonTools = buildCommonTools(connection, config);
|
|
2587
4369
|
const effectiveMode = agentMode ?? context?.agentMode ?? void 0;
|
|
2588
4370
|
const modeTools = getModeTools(effectiveMode, connection, config, context);
|
|
2589
4371
|
const discoveryTools = effectiveMode === "discovery" || effectiveMode === "auto" ? buildDiscoveryTools(connection, context) : [];
|
|
4372
|
+
const debugTools = debugManager && effectiveMode === "building" ? buildDebugTools(debugManager) : [];
|
|
2590
4373
|
return createSdkMcpServer({
|
|
2591
4374
|
name: "conveyor",
|
|
2592
|
-
tools: [...commonTools, ...modeTools, ...discoveryTools]
|
|
4375
|
+
tools: [...commonTools, ...modeTools, ...discoveryTools, ...debugTools]
|
|
2593
4376
|
});
|
|
2594
4377
|
}
|
|
2595
4378
|
|
|
@@ -2667,9 +4450,9 @@ async function handleExitPlanMode(host, input) {
|
|
|
2667
4450
|
const taskProps = await host.connection.getTaskProperties();
|
|
2668
4451
|
const missingProps = [];
|
|
2669
4452
|
if (!taskProps.plan?.trim()) missingProps.push("plan (save via update_task)");
|
|
2670
|
-
if (!taskProps.storyPointId) missingProps.push("story points (use
|
|
4453
|
+
if (!taskProps.storyPointId) missingProps.push("story points (use update_task_properties)");
|
|
2671
4454
|
if (!taskProps.title || taskProps.title === "Untitled")
|
|
2672
|
-
missingProps.push("title (use
|
|
4455
|
+
missingProps.push("title (use update_task_properties)");
|
|
2673
4456
|
if (missingProps.length > 0) {
|
|
2674
4457
|
return {
|
|
2675
4458
|
behavior: "deny",
|
|
@@ -2726,7 +4509,9 @@ async function handleAskUserQuestion(host, input) {
|
|
|
2726
4509
|
}
|
|
2727
4510
|
return { behavior: "allow", updatedInput: { questions: input.questions, answers } };
|
|
2728
4511
|
}
|
|
4512
|
+
var DENIAL_WARNING_THRESHOLD = 3;
|
|
2729
4513
|
function buildCanUseTool(host) {
|
|
4514
|
+
let consecutiveDenials = 0;
|
|
2730
4515
|
return async (toolName, input) => {
|
|
2731
4516
|
if (toolName === "ExitPlanMode" && (host.agentMode === "auto" || host.agentMode === "discovery") && !host.hasExitedPlanMode) {
|
|
2732
4517
|
return await handleExitPlanMode(host, input);
|
|
@@ -2734,24 +4519,40 @@ function buildCanUseTool(host) {
|
|
|
2734
4519
|
if (toolName === "AskUserQuestion") {
|
|
2735
4520
|
return await handleAskUserQuestion(host, input);
|
|
2736
4521
|
}
|
|
4522
|
+
let result;
|
|
2737
4523
|
switch (host.agentMode) {
|
|
2738
4524
|
case "discovery":
|
|
2739
|
-
|
|
4525
|
+
result = handleDiscoveryToolAccess(toolName, input);
|
|
4526
|
+
break;
|
|
2740
4527
|
case "building":
|
|
2741
|
-
|
|
4528
|
+
result = handleBuildingToolAccess(toolName, input);
|
|
4529
|
+
break;
|
|
2742
4530
|
case "review":
|
|
2743
|
-
|
|
4531
|
+
result = handleReviewToolAccess(toolName, input, host.isParentTask);
|
|
4532
|
+
break;
|
|
2744
4533
|
case "auto":
|
|
2745
|
-
|
|
4534
|
+
result = handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
|
|
4535
|
+
break;
|
|
2746
4536
|
default:
|
|
2747
|
-
|
|
4537
|
+
result = { behavior: "allow", updatedInput: input };
|
|
4538
|
+
}
|
|
4539
|
+
if (result.behavior === "deny") {
|
|
4540
|
+
consecutiveDenials++;
|
|
4541
|
+
if (consecutiveDenials === DENIAL_WARNING_THRESHOLD) {
|
|
4542
|
+
host.connection.postChatMessage(
|
|
4543
|
+
`\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.`
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
} else {
|
|
4547
|
+
consecutiveDenials = 0;
|
|
2748
4548
|
}
|
|
4549
|
+
return result;
|
|
2749
4550
|
};
|
|
2750
4551
|
}
|
|
2751
4552
|
|
|
2752
4553
|
// src/execution/query-executor.ts
|
|
2753
|
-
var
|
|
2754
|
-
var
|
|
4554
|
+
var logger3 = createServiceLogger("QueryExecutor");
|
|
4555
|
+
var API_ERROR_PATTERN3 = /API Error: [45]\d\d/;
|
|
2755
4556
|
var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
|
|
2756
4557
|
var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
|
|
2757
4558
|
function buildHooks(host) {
|
|
@@ -2827,7 +4628,7 @@ function buildQueryOptions(host, context) {
|
|
|
2827
4628
|
},
|
|
2828
4629
|
settingSources,
|
|
2829
4630
|
cwd: host.config.workspaceDir,
|
|
2830
|
-
permissionMode: needsCanUseTool ? "
|
|
4631
|
+
permissionMode: needsCanUseTool ? "plan" : "bypassPermissions",
|
|
2831
4632
|
allowDangerouslySkipPermissions: !needsCanUseTool,
|
|
2832
4633
|
canUseTool: buildCanUseTool(host),
|
|
2833
4634
|
tools: { type: "preset", preset: "claude_code" },
|
|
@@ -2841,7 +4642,7 @@ function buildQueryOptions(host, context) {
|
|
|
2841
4642
|
disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
|
|
2842
4643
|
enableFileCheckpointing: settings.enableFileCheckpointing,
|
|
2843
4644
|
stderr: (data) => {
|
|
2844
|
-
|
|
4645
|
+
logger3.warn("Claude Code stderr", { data: data.trimEnd() });
|
|
2845
4646
|
}
|
|
2846
4647
|
};
|
|
2847
4648
|
if (isCloud && isReadOnly) {
|
|
@@ -2971,6 +4772,29 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
|
|
|
2971
4772
|
options: { ...options, resume: void 0 }
|
|
2972
4773
|
});
|
|
2973
4774
|
}
|
|
4775
|
+
async function handleAuthError(context, host, options) {
|
|
4776
|
+
host.connection.postChatMessage("Authentication expired. Re-bootstrapping credentials...");
|
|
4777
|
+
const refreshed = await host.connection.refreshAuthToken();
|
|
4778
|
+
if (!refreshed) {
|
|
4779
|
+
host.connection.postChatMessage("Failed to refresh authentication. Agent will restart.");
|
|
4780
|
+
host.connection.sendEvent({
|
|
4781
|
+
type: "error",
|
|
4782
|
+
message: "Auth re-bootstrap failed, exiting for restart"
|
|
4783
|
+
});
|
|
4784
|
+
process.exit(1);
|
|
4785
|
+
}
|
|
4786
|
+
context.claudeSessionId = null;
|
|
4787
|
+
host.connection.storeSessionId("");
|
|
4788
|
+
const freshPrompt = buildMultimodalPrompt(
|
|
4789
|
+
await buildInitialPrompt(host.config.mode, context, host.config.isAuto, host.agentMode),
|
|
4790
|
+
context
|
|
4791
|
+
);
|
|
4792
|
+
const freshQuery = query({
|
|
4793
|
+
prompt: host.createInputStream(freshPrompt),
|
|
4794
|
+
options: { ...options, resume: void 0 }
|
|
4795
|
+
});
|
|
4796
|
+
return runWithRetry(freshQuery, context, host, options);
|
|
4797
|
+
}
|
|
2974
4798
|
async function handleStaleSession(context, host, options) {
|
|
2975
4799
|
context.claudeSessionId = null;
|
|
2976
4800
|
host.connection.storeSessionId("");
|
|
@@ -3002,12 +4826,17 @@ function isStaleOrExitedSession(error, context) {
|
|
|
3002
4826
|
if (error.message.includes("No conversation found with session ID")) return true;
|
|
3003
4827
|
return !!context.claudeSessionId && error.message.includes("process exited");
|
|
3004
4828
|
}
|
|
4829
|
+
function getErrorMessage(error) {
|
|
4830
|
+
if (error instanceof Error) return error.message;
|
|
4831
|
+
if (typeof error === "string") return error;
|
|
4832
|
+
return String(error);
|
|
4833
|
+
}
|
|
3005
4834
|
function isRetriableError(error) {
|
|
3006
|
-
|
|
3007
|
-
return
|
|
4835
|
+
const message = getErrorMessage(error);
|
|
4836
|
+
return API_ERROR_PATTERN3.test(message) || IMAGE_ERROR_PATTERN2.test(message);
|
|
3008
4837
|
}
|
|
3009
4838
|
function classifyImageError(error) {
|
|
3010
|
-
return
|
|
4839
|
+
return IMAGE_ERROR_PATTERN2.test(getErrorMessage(error));
|
|
3011
4840
|
}
|
|
3012
4841
|
async function emitRetryStatus(host, attempt, delayMs) {
|
|
3013
4842
|
const delayMin = Math.round(delayMs / 6e4);
|
|
@@ -3034,26 +4863,41 @@ function handleRetryError(error, context, host, options, prevImageError) {
|
|
|
3034
4863
|
if (isStaleOrExitedSession(error, context) && context.claudeSessionId) {
|
|
3035
4864
|
return handleStaleSession(context, host, options);
|
|
3036
4865
|
}
|
|
4866
|
+
if (isAuthError(getErrorMessage(error))) {
|
|
4867
|
+
return handleAuthError(context, host, options);
|
|
4868
|
+
}
|
|
3037
4869
|
if (!isRetriableError(error)) throw error;
|
|
3038
4870
|
return { action: "continue", lastErrorWasImage: classifyImageError(error) || prevImageError };
|
|
3039
4871
|
}
|
|
4872
|
+
function handleProcessResult(result, context, host, options) {
|
|
4873
|
+
if (result.modeRestart || host.isStopped()) return { action: "return" };
|
|
4874
|
+
if (result.rateLimitResetsAt) {
|
|
4875
|
+
handleRateLimitPause(host, result.rateLimitResetsAt);
|
|
4876
|
+
return { action: "return" };
|
|
4877
|
+
}
|
|
4878
|
+
if (result.staleSession && context.claudeSessionId) {
|
|
4879
|
+
return { action: "return_promise", promise: handleStaleSession(context, host, options) };
|
|
4880
|
+
}
|
|
4881
|
+
if (result.authError) {
|
|
4882
|
+
return { action: "return_promise", promise: handleAuthError(context, host, options) };
|
|
4883
|
+
}
|
|
4884
|
+
if (!result.retriable) return { action: "return" };
|
|
4885
|
+
return {
|
|
4886
|
+
action: "continue",
|
|
4887
|
+
lastErrorWasImage: IMAGE_ERROR_PATTERN2.test(result.resultSummary ?? "")
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
3040
4890
|
async function runWithRetry(initialQuery, context, host, options) {
|
|
3041
4891
|
let lastErrorWasImage = false;
|
|
3042
4892
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
3043
4893
|
if (host.isStopped()) return;
|
|
3044
4894
|
const agentQuery = attempt === 0 ? initialQuery : await buildRetryQuery(host, context, options, lastErrorWasImage);
|
|
3045
4895
|
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 ?? "");
|
|
4896
|
+
const result = await processEvents(agentQuery, context, host);
|
|
4897
|
+
const outcome = handleProcessResult(result, context, host, options);
|
|
4898
|
+
if (outcome.action === "return") return;
|
|
4899
|
+
if (outcome.action === "return_promise") return outcome.promise;
|
|
4900
|
+
lastErrorWasImage = outcome.lastErrorWasImage;
|
|
3057
4901
|
} catch (error) {
|
|
3058
4902
|
const outcome = handleRetryError(error, context, host, options, lastErrorWasImage);
|
|
3059
4903
|
if (outcome instanceof Promise) return outcome;
|
|
@@ -3227,6 +5071,47 @@ async function executeSetupConfig(config, runnerConfig, connection, setupLog) {
|
|
|
3227
5071
|
async function checkoutTaskBranch(runnerConfig, connection, callbacks, setupLog) {
|
|
3228
5072
|
const taskBranch = process.env.CONVEYOR_TASK_BRANCH;
|
|
3229
5073
|
if (!taskBranch) return true;
|
|
5074
|
+
const currentBranch = getCurrentBranch(runnerConfig.workspaceDir);
|
|
5075
|
+
if (currentBranch === taskBranch) {
|
|
5076
|
+
pushSetupLog(setupLog, `[conveyor] Already on ${taskBranch}, skipping checkout`);
|
|
5077
|
+
connection.sendEvent({
|
|
5078
|
+
type: "setup_output",
|
|
5079
|
+
stream: "stdout",
|
|
5080
|
+
data: `Already on branch ${taskBranch}, skipping checkout
|
|
5081
|
+
`
|
|
5082
|
+
});
|
|
5083
|
+
try {
|
|
5084
|
+
await runSetupCommand(
|
|
5085
|
+
`git fetch origin ${taskBranch}`,
|
|
5086
|
+
runnerConfig.workspaceDir,
|
|
5087
|
+
(stream, data) => {
|
|
5088
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
5089
|
+
}
|
|
5090
|
+
);
|
|
5091
|
+
} catch {
|
|
5092
|
+
}
|
|
5093
|
+
return true;
|
|
5094
|
+
}
|
|
5095
|
+
let didStash = false;
|
|
5096
|
+
if (hasUncommittedChanges(runnerConfig.workspaceDir)) {
|
|
5097
|
+
pushSetupLog(setupLog, `[conveyor] Uncommitted changes detected, stashing before checkout`);
|
|
5098
|
+
connection.sendEvent({
|
|
5099
|
+
type: "setup_output",
|
|
5100
|
+
stream: "stdout",
|
|
5101
|
+
data: "Uncommitted changes detected \u2014 stashing before branch switch\n"
|
|
5102
|
+
});
|
|
5103
|
+
try {
|
|
5104
|
+
await runSetupCommand(
|
|
5105
|
+
`git stash push -m "conveyor-auto-stash"`,
|
|
5106
|
+
runnerConfig.workspaceDir,
|
|
5107
|
+
(stream, data) => {
|
|
5108
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
5109
|
+
}
|
|
5110
|
+
);
|
|
5111
|
+
didStash = true;
|
|
5112
|
+
} catch {
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
3230
5115
|
pushSetupLog(setupLog, `[conveyor] Switching to task branch ${taskBranch}...`);
|
|
3231
5116
|
connection.sendEvent({
|
|
3232
5117
|
type: "setup_output",
|
|
@@ -3246,6 +5131,19 @@ async function checkoutTaskBranch(runnerConfig, connection, callbacks, setupLog)
|
|
|
3246
5131
|
}
|
|
3247
5132
|
);
|
|
3248
5133
|
pushSetupLog(setupLog, `[conveyor] Switched to ${taskBranch}`);
|
|
5134
|
+
if (didStash) {
|
|
5135
|
+
try {
|
|
5136
|
+
await runSetupCommand("git stash pop", runnerConfig.workspaceDir, (stream, data) => {
|
|
5137
|
+
connection.sendEvent({ type: "setup_output", stream, data });
|
|
5138
|
+
});
|
|
5139
|
+
pushSetupLog(setupLog, `[conveyor] Restored stashed changes`);
|
|
5140
|
+
} catch {
|
|
5141
|
+
pushSetupLog(
|
|
5142
|
+
setupLog,
|
|
5143
|
+
`[conveyor] Warning: stash pop had conflicts \u2014 agent may need to resolve`
|
|
5144
|
+
);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
3249
5147
|
return true;
|
|
3250
5148
|
} catch (error) {
|
|
3251
5149
|
const message = `Failed to checkout ${taskBranch}: ${error instanceof Error ? error.message : "unknown error"}`;
|
|
@@ -3433,7 +5331,7 @@ function buildQueryHost(deps) {
|
|
|
3433
5331
|
}
|
|
3434
5332
|
|
|
3435
5333
|
// src/runner/agent-runner.ts
|
|
3436
|
-
var
|
|
5334
|
+
var logger4 = createServiceLogger("AgentRunner");
|
|
3437
5335
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3438
5336
|
var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
3439
5337
|
var AgentRunner = class {
|
|
@@ -3462,6 +5360,7 @@ var AgentRunner = class {
|
|
|
3462
5360
|
idleCheckInterval = null;
|
|
3463
5361
|
conveyorConfig = null;
|
|
3464
5362
|
_queryHost = null;
|
|
5363
|
+
tunnelClient = null;
|
|
3465
5364
|
constructor(config, callbacks) {
|
|
3466
5365
|
this.config = config;
|
|
3467
5366
|
this.connection = new ConveyorConnection(config);
|
|
@@ -3536,11 +5435,13 @@ var AgentRunner = class {
|
|
|
3536
5435
|
}
|
|
3537
5436
|
this.tryInitWorktree();
|
|
3538
5437
|
if (!await this.fetchAndInitContext()) return;
|
|
5438
|
+
this.startPreviewTunnel();
|
|
3539
5439
|
this.tryPostContextWorktree();
|
|
3540
5440
|
this.checkoutWorktreeBranch();
|
|
3541
5441
|
await this.executeInitialMode();
|
|
3542
5442
|
await this.runCoreLoop();
|
|
3543
5443
|
this.stopHeartbeat();
|
|
5444
|
+
this.tunnelClient?.disconnect();
|
|
3544
5445
|
await this.setState("finished");
|
|
3545
5446
|
this.connection.disconnect();
|
|
3546
5447
|
}
|
|
@@ -3573,6 +5474,11 @@ var AgentRunner = class {
|
|
|
3573
5474
|
this.activateWorktree("[conveyor] Using worktree (from task config):");
|
|
3574
5475
|
}
|
|
3575
5476
|
}
|
|
5477
|
+
startPreviewTunnel() {
|
|
5478
|
+
const port = this.conveyorConfig?.previewPort ?? (Number(process.env.CONVEYOR_PREVIEW_PORT) || 3050);
|
|
5479
|
+
this.tunnelClient = new TunnelClient(this.config.conveyorApiUrl, this.config.taskToken, port);
|
|
5480
|
+
this.tunnelClient.connect();
|
|
5481
|
+
}
|
|
3576
5482
|
activateWorktree(logPrefix) {
|
|
3577
5483
|
try {
|
|
3578
5484
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
@@ -3590,12 +5496,22 @@ var AgentRunner = class {
|
|
|
3590
5496
|
}
|
|
3591
5497
|
checkoutWorktreeBranch() {
|
|
3592
5498
|
if (!this.worktreeActive || !this.taskContext?.githubBranch) return;
|
|
5499
|
+
const branch = this.taskContext.githubBranch;
|
|
5500
|
+
const cwd = this.config.workspaceDir;
|
|
5501
|
+
if (getCurrentBranch(cwd) === branch) return;
|
|
3593
5502
|
try {
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
cwd:
|
|
3597
|
-
|
|
3598
|
-
}
|
|
5503
|
+
let didStash = false;
|
|
5504
|
+
if (hasUncommittedChanges(cwd)) {
|
|
5505
|
+
execSync5(`git stash push -m "conveyor-auto-stash"`, { cwd, stdio: "ignore" });
|
|
5506
|
+
didStash = true;
|
|
5507
|
+
}
|
|
5508
|
+
execSync5(`git fetch origin ${branch} && git checkout ${branch}`, { cwd, stdio: "ignore" });
|
|
5509
|
+
if (didStash) {
|
|
5510
|
+
try {
|
|
5511
|
+
execSync5("git stash pop", { cwd, stdio: "ignore" });
|
|
5512
|
+
} catch {
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
3599
5515
|
} catch {
|
|
3600
5516
|
}
|
|
3601
5517
|
}
|
|
@@ -3722,7 +5638,7 @@ var AgentRunner = class {
|
|
|
3722
5638
|
const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
|
|
3723
5639
|
const model = this.taskContext.model || this.config.model;
|
|
3724
5640
|
const thinking = formatThinkingSetting(s.thinking);
|
|
3725
|
-
|
|
5641
|
+
logger4.info("Effective agent settings", {
|
|
3726
5642
|
model,
|
|
3727
5643
|
mode: this.config.mode ?? "task",
|
|
3728
5644
|
effort: s.effort ?? "default",
|
|
@@ -3764,15 +5680,11 @@ var AgentRunner = class {
|
|
|
3764
5680
|
}
|
|
3765
5681
|
}, 1e3);
|
|
3766
5682
|
this.idleTimer = setTimeout(() => {
|
|
3767
|
-
|
|
3768
|
-
this.inputResolver = null;
|
|
3769
|
-
logger3.info("Idle timeout reached, shutting down", {
|
|
5683
|
+
logger4.info("Idle timeout reached, entering sleep mode", {
|
|
3770
5684
|
idleMinutes: IDLE_TIMEOUT_MS / 6e4
|
|
3771
5685
|
});
|
|
3772
|
-
this.connection.
|
|
3773
|
-
|
|
3774
|
-
);
|
|
3775
|
-
resolve2(null);
|
|
5686
|
+
this.connection.emitStatus("sleeping");
|
|
5687
|
+
this.connection.postChatMessage("Agent sleeping \u2014 send a message or click Resume to wake.");
|
|
3776
5688
|
}, IDLE_TIMEOUT_MS);
|
|
3777
5689
|
this.inputResolver = (msg) => {
|
|
3778
5690
|
this.clearIdleTimers();
|
|
@@ -3864,32 +5776,290 @@ var AgentRunner = class {
|
|
|
3864
5776
|
if (typeof q.interrupt === "function") {
|
|
3865
5777
|
void q.interrupt();
|
|
3866
5778
|
}
|
|
3867
|
-
host.activeQuery = null;
|
|
3868
|
-
}
|
|
3869
|
-
if (this.inputResolver) {
|
|
3870
|
-
this.inputResolver(null);
|
|
3871
|
-
this.inputResolver = null;
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
stop() {
|
|
3875
|
-
this.stopped = true;
|
|
3876
|
-
this.clearIdleTimers();
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
this.inputResolver
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
}
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
import {
|
|
3887
|
-
import
|
|
3888
|
-
import
|
|
5779
|
+
host.activeQuery = null;
|
|
5780
|
+
}
|
|
5781
|
+
if (this.inputResolver) {
|
|
5782
|
+
this.inputResolver(null);
|
|
5783
|
+
this.inputResolver = null;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
stop() {
|
|
5787
|
+
this.stopped = true;
|
|
5788
|
+
this.clearIdleTimers();
|
|
5789
|
+
this.tunnelClient?.disconnect();
|
|
5790
|
+
if (this.inputResolver) {
|
|
5791
|
+
this.inputResolver(null);
|
|
5792
|
+
this.inputResolver = null;
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
};
|
|
5796
|
+
|
|
5797
|
+
// src/runner/project-runner.ts
|
|
5798
|
+
import { fork } from "child_process";
|
|
5799
|
+
import { execSync as execSync7 } from "child_process";
|
|
5800
|
+
import * as path from "path";
|
|
5801
|
+
import { fileURLToPath } from "url";
|
|
5802
|
+
|
|
5803
|
+
// src/runner/commit-watcher.ts
|
|
5804
|
+
import { execSync as execSync6 } from "child_process";
|
|
5805
|
+
var logger5 = createServiceLogger("CommitWatcher");
|
|
5806
|
+
var CommitWatcher = class {
|
|
5807
|
+
constructor(config, callbacks) {
|
|
5808
|
+
this.config = config;
|
|
5809
|
+
this.callbacks = callbacks;
|
|
5810
|
+
}
|
|
5811
|
+
interval = null;
|
|
5812
|
+
lastKnownRemoteSha = null;
|
|
5813
|
+
branch = null;
|
|
5814
|
+
debounceTimer = null;
|
|
5815
|
+
isSyncing = false;
|
|
5816
|
+
start(branch) {
|
|
5817
|
+
this.stop();
|
|
5818
|
+
this.branch = branch;
|
|
5819
|
+
this.lastKnownRemoteSha = this.getLocalHeadSha();
|
|
5820
|
+
this.interval = setInterval(() => void this.poll(), this.config.pollIntervalMs);
|
|
5821
|
+
logger5.info("Commit watcher started", {
|
|
5822
|
+
branch,
|
|
5823
|
+
baseSha: this.lastKnownRemoteSha?.slice(0, 8)
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
stop() {
|
|
5827
|
+
if (this.interval) clearInterval(this.interval);
|
|
5828
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
5829
|
+
this.interval = null;
|
|
5830
|
+
this.debounceTimer = null;
|
|
5831
|
+
this.branch = null;
|
|
5832
|
+
this.lastKnownRemoteSha = null;
|
|
5833
|
+
this.isSyncing = false;
|
|
5834
|
+
}
|
|
5835
|
+
getLocalHeadSha() {
|
|
5836
|
+
return execSync6("git rev-parse HEAD", {
|
|
5837
|
+
cwd: this.config.projectDir,
|
|
5838
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5839
|
+
}).toString().trim();
|
|
5840
|
+
}
|
|
5841
|
+
poll() {
|
|
5842
|
+
if (!this.branch || this.isSyncing) return;
|
|
5843
|
+
try {
|
|
5844
|
+
execSync6(`git fetch origin ${this.branch} --quiet`, {
|
|
5845
|
+
cwd: this.config.projectDir,
|
|
5846
|
+
stdio: "ignore",
|
|
5847
|
+
timeout: 3e4
|
|
5848
|
+
});
|
|
5849
|
+
const remoteSha = execSync6(`git rev-parse origin/${this.branch}`, {
|
|
5850
|
+
cwd: this.config.projectDir,
|
|
5851
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5852
|
+
}).toString().trim();
|
|
5853
|
+
if (remoteSha !== this.lastKnownRemoteSha) {
|
|
5854
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
5855
|
+
this.debounceTimer = setTimeout(
|
|
5856
|
+
() => void this.handleNewCommits(remoteSha),
|
|
5857
|
+
this.config.debounceMs
|
|
5858
|
+
);
|
|
5859
|
+
}
|
|
5860
|
+
} catch {
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
async handleNewCommits(remoteSha) {
|
|
5864
|
+
if (!this.branch) return;
|
|
5865
|
+
const previousSha = this.lastKnownRemoteSha ?? "HEAD";
|
|
5866
|
+
let commitCount = 1;
|
|
5867
|
+
let latestMessage = "";
|
|
5868
|
+
let latestAuthor = "";
|
|
5869
|
+
try {
|
|
5870
|
+
const countOutput = execSync6(`git rev-list --count ${previousSha}..origin/${this.branch}`, {
|
|
5871
|
+
cwd: this.config.projectDir,
|
|
5872
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5873
|
+
}).toString().trim();
|
|
5874
|
+
commitCount = parseInt(countOutput, 10) || 1;
|
|
5875
|
+
const logOutput = execSync6(`git log -1 --format="%s|||%an" origin/${this.branch}`, {
|
|
5876
|
+
cwd: this.config.projectDir,
|
|
5877
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5878
|
+
}).toString().trim();
|
|
5879
|
+
const parts = logOutput.split("|||");
|
|
5880
|
+
latestMessage = parts[0] ?? "";
|
|
5881
|
+
latestAuthor = parts[1] ?? "";
|
|
5882
|
+
} catch {
|
|
5883
|
+
}
|
|
5884
|
+
this.lastKnownRemoteSha = remoteSha;
|
|
5885
|
+
this.isSyncing = true;
|
|
5886
|
+
logger5.info("New commits detected", {
|
|
5887
|
+
branch: this.branch,
|
|
5888
|
+
commitCount,
|
|
5889
|
+
sha: remoteSha.slice(0, 8)
|
|
5890
|
+
});
|
|
5891
|
+
try {
|
|
5892
|
+
await this.callbacks.onNewCommits({
|
|
5893
|
+
branch: this.branch,
|
|
5894
|
+
previousSha,
|
|
5895
|
+
newCommitSha: remoteSha,
|
|
5896
|
+
commitCount,
|
|
5897
|
+
latestMessage,
|
|
5898
|
+
latestAuthor
|
|
5899
|
+
});
|
|
5900
|
+
} catch (err) {
|
|
5901
|
+
logger5.error("Error handling new commits", errorMeta(err));
|
|
5902
|
+
} finally {
|
|
5903
|
+
this.isSyncing = false;
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
};
|
|
5907
|
+
|
|
5908
|
+
// src/runner/project-chat-handler.ts
|
|
5909
|
+
import {
|
|
5910
|
+
query as query2,
|
|
5911
|
+
createSdkMcpServer as createSdkMcpServer2
|
|
5912
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
5913
|
+
|
|
5914
|
+
// src/tools/project-tools.ts
|
|
5915
|
+
import { tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
|
|
5916
|
+
import { z as z7 } from "zod";
|
|
5917
|
+
function buildReadTools(connection) {
|
|
5918
|
+
return [
|
|
5919
|
+
tool7(
|
|
5920
|
+
"list_tasks",
|
|
5921
|
+
"List tasks in the project. Optionally filter by status or assignee.",
|
|
5922
|
+
{
|
|
5923
|
+
status: z7.string().optional().describe("Filter by task status (e.g. Planning, Open, InProgress, ReviewPR, Complete)"),
|
|
5924
|
+
assigneeId: z7.string().optional().describe("Filter by assigned user ID"),
|
|
5925
|
+
limit: z7.number().optional().describe("Max number of tasks to return (default 50)")
|
|
5926
|
+
},
|
|
5927
|
+
async (params) => {
|
|
5928
|
+
try {
|
|
5929
|
+
const tasks = await connection.requestListTasks(params);
|
|
5930
|
+
return textResult(JSON.stringify(tasks, null, 2));
|
|
5931
|
+
} catch (error) {
|
|
5932
|
+
return textResult(
|
|
5933
|
+
`Failed to list tasks: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
5934
|
+
);
|
|
5935
|
+
}
|
|
5936
|
+
},
|
|
5937
|
+
{ annotations: { readOnlyHint: true } }
|
|
5938
|
+
),
|
|
5939
|
+
tool7(
|
|
5940
|
+
"get_task",
|
|
5941
|
+
"Get detailed information about a task including its chat messages, child tasks, and codespace status.",
|
|
5942
|
+
{ task_id: z7.string().describe("The task ID to look up") },
|
|
5943
|
+
async ({ task_id }) => {
|
|
5944
|
+
try {
|
|
5945
|
+
const task = await connection.requestGetTask(task_id);
|
|
5946
|
+
return textResult(JSON.stringify(task, null, 2));
|
|
5947
|
+
} catch (error) {
|
|
5948
|
+
return textResult(
|
|
5949
|
+
`Failed to get task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5952
|
+
},
|
|
5953
|
+
{ annotations: { readOnlyHint: true } }
|
|
5954
|
+
),
|
|
5955
|
+
tool7(
|
|
5956
|
+
"search_tasks",
|
|
5957
|
+
"Search tasks by tags, text query, or status filters.",
|
|
5958
|
+
{
|
|
5959
|
+
tagNames: z7.array(z7.string()).optional().describe("Filter by tag names"),
|
|
5960
|
+
searchQuery: z7.string().optional().describe("Text search in title/description"),
|
|
5961
|
+
statusFilters: z7.array(z7.string()).optional().describe("Filter by statuses"),
|
|
5962
|
+
limit: z7.number().optional().describe("Max results (default 20)")
|
|
5963
|
+
},
|
|
5964
|
+
async (params) => {
|
|
5965
|
+
try {
|
|
5966
|
+
const tasks = await connection.requestSearchTasks(params);
|
|
5967
|
+
return textResult(JSON.stringify(tasks, null, 2));
|
|
5968
|
+
} catch (error) {
|
|
5969
|
+
return textResult(
|
|
5970
|
+
`Failed to search tasks: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
5971
|
+
);
|
|
5972
|
+
}
|
|
5973
|
+
},
|
|
5974
|
+
{ annotations: { readOnlyHint: true } }
|
|
5975
|
+
),
|
|
5976
|
+
tool7(
|
|
5977
|
+
"list_tags",
|
|
5978
|
+
"List all tags available in the project.",
|
|
5979
|
+
{},
|
|
5980
|
+
async () => {
|
|
5981
|
+
try {
|
|
5982
|
+
const tags = await connection.requestListTags();
|
|
5983
|
+
return textResult(JSON.stringify(tags, null, 2));
|
|
5984
|
+
} catch (error) {
|
|
5985
|
+
return textResult(
|
|
5986
|
+
`Failed to list tags: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
5987
|
+
);
|
|
5988
|
+
}
|
|
5989
|
+
},
|
|
5990
|
+
{ annotations: { readOnlyHint: true } }
|
|
5991
|
+
),
|
|
5992
|
+
tool7(
|
|
5993
|
+
"get_project_summary",
|
|
5994
|
+
"Get a summary of the project including task counts by status and active builds.",
|
|
5995
|
+
{},
|
|
5996
|
+
async () => {
|
|
5997
|
+
try {
|
|
5998
|
+
const summary = await connection.requestGetProjectSummary();
|
|
5999
|
+
return textResult(JSON.stringify(summary, null, 2));
|
|
6000
|
+
} catch (error) {
|
|
6001
|
+
return textResult(
|
|
6002
|
+
`Failed to get project summary: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
6003
|
+
);
|
|
6004
|
+
}
|
|
6005
|
+
},
|
|
6006
|
+
{ annotations: { readOnlyHint: true } }
|
|
6007
|
+
)
|
|
6008
|
+
];
|
|
6009
|
+
}
|
|
6010
|
+
function buildMutationTools(connection) {
|
|
6011
|
+
return [
|
|
6012
|
+
tool7(
|
|
6013
|
+
"create_task",
|
|
6014
|
+
"Create a new task in the project.",
|
|
6015
|
+
{
|
|
6016
|
+
title: z7.string().describe("Task title"),
|
|
6017
|
+
description: z7.string().optional().describe("Task description"),
|
|
6018
|
+
plan: z7.string().optional().describe("Implementation plan in markdown"),
|
|
6019
|
+
status: z7.string().optional().describe("Initial status (default: Planning)"),
|
|
6020
|
+
isBug: z7.boolean().optional().describe("Whether this is a bug report")
|
|
6021
|
+
},
|
|
6022
|
+
async (params) => {
|
|
6023
|
+
try {
|
|
6024
|
+
const result = await connection.requestCreateTask(params);
|
|
6025
|
+
return textResult(`Task created: ${result.slug} (ID: ${result.id})`);
|
|
6026
|
+
} catch (error) {
|
|
6027
|
+
return textResult(
|
|
6028
|
+
`Failed to create task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
6029
|
+
);
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
),
|
|
6033
|
+
tool7(
|
|
6034
|
+
"update_task",
|
|
6035
|
+
"Update an existing task's title, description, plan, status, or assignee.",
|
|
6036
|
+
{
|
|
6037
|
+
task_id: z7.string().describe("The task ID to update"),
|
|
6038
|
+
title: z7.string().optional().describe("New title"),
|
|
6039
|
+
description: z7.string().optional().describe("New description"),
|
|
6040
|
+
plan: z7.string().optional().describe("New plan in markdown"),
|
|
6041
|
+
status: z7.string().optional().describe("New status"),
|
|
6042
|
+
assignedUserId: z7.string().nullable().optional().describe("Assign to user ID, or null to unassign")
|
|
6043
|
+
},
|
|
6044
|
+
async ({ task_id, ...fields }) => {
|
|
6045
|
+
try {
|
|
6046
|
+
await connection.requestUpdateTask({ taskId: task_id, ...fields });
|
|
6047
|
+
return textResult("Task updated successfully.");
|
|
6048
|
+
} catch (error) {
|
|
6049
|
+
return textResult(
|
|
6050
|
+
`Failed to update task: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
6051
|
+
);
|
|
6052
|
+
}
|
|
6053
|
+
}
|
|
6054
|
+
)
|
|
6055
|
+
];
|
|
6056
|
+
}
|
|
6057
|
+
function buildProjectTools(connection) {
|
|
6058
|
+
return [...buildReadTools(connection), ...buildMutationTools(connection)];
|
|
6059
|
+
}
|
|
3889
6060
|
|
|
3890
6061
|
// src/runner/project-chat-handler.ts
|
|
3891
|
-
|
|
3892
|
-
var logger4 = createServiceLogger("ProjectChat");
|
|
6062
|
+
var logger6 = createServiceLogger("ProjectChat");
|
|
3893
6063
|
var FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
3894
6064
|
function buildSystemPrompt2(projectDir, agentCtx) {
|
|
3895
6065
|
const parts = [];
|
|
@@ -3942,27 +6112,31 @@ function processContentBlock(block, responseParts, turnToolCalls) {
|
|
|
3942
6112
|
input: inputStr.slice(0, 1e4),
|
|
3943
6113
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3944
6114
|
});
|
|
3945
|
-
|
|
6115
|
+
logger6.debug("Tool use", { tool: block.name });
|
|
3946
6116
|
}
|
|
3947
6117
|
}
|
|
3948
|
-
async function fetchContext(connection) {
|
|
6118
|
+
async function fetchContext(connection, chatId) {
|
|
3949
6119
|
let agentCtx = null;
|
|
3950
6120
|
try {
|
|
3951
6121
|
agentCtx = await connection.fetchAgentContext();
|
|
3952
6122
|
} catch {
|
|
3953
|
-
|
|
6123
|
+
logger6.warn("Could not fetch agent context, using defaults");
|
|
3954
6124
|
}
|
|
3955
6125
|
let chatHistory = [];
|
|
3956
6126
|
try {
|
|
3957
|
-
chatHistory = await connection.fetchChatHistory(30);
|
|
6127
|
+
chatHistory = await connection.fetchChatHistory(30, chatId);
|
|
3958
6128
|
} catch {
|
|
3959
|
-
|
|
6129
|
+
logger6.warn("Could not fetch chat history, proceeding without it");
|
|
3960
6130
|
}
|
|
3961
6131
|
return { agentCtx, chatHistory };
|
|
3962
6132
|
}
|
|
3963
|
-
function buildChatQueryOptions(agentCtx, projectDir) {
|
|
6133
|
+
function buildChatQueryOptions(agentCtx, projectDir, connection) {
|
|
3964
6134
|
const model = agentCtx?.model || FALLBACK_MODEL;
|
|
3965
6135
|
const settings = agentCtx?.agentSettings ?? {};
|
|
6136
|
+
const mcpServer = createSdkMcpServer2({
|
|
6137
|
+
name: "conveyor",
|
|
6138
|
+
tools: buildProjectTools(connection)
|
|
6139
|
+
});
|
|
3966
6140
|
return {
|
|
3967
6141
|
model,
|
|
3968
6142
|
systemPrompt: {
|
|
@@ -3974,12 +6148,48 @@ function buildChatQueryOptions(agentCtx, projectDir) {
|
|
|
3974
6148
|
permissionMode: "bypassPermissions",
|
|
3975
6149
|
allowDangerouslySkipPermissions: true,
|
|
3976
6150
|
tools: { type: "preset", preset: "claude_code" },
|
|
3977
|
-
|
|
3978
|
-
|
|
6151
|
+
mcpServers: { conveyor: mcpServer },
|
|
6152
|
+
maxTurns: settings.maxTurns ?? 30,
|
|
6153
|
+
maxBudgetUsd: settings.maxBudgetUsd ?? 50,
|
|
3979
6154
|
effort: settings.effort,
|
|
3980
6155
|
thinking: settings.thinking
|
|
3981
6156
|
};
|
|
3982
6157
|
}
|
|
6158
|
+
function emitResultCostAndContext(event, connection) {
|
|
6159
|
+
const resultEvent = event;
|
|
6160
|
+
if (resultEvent.total_cost_usd !== void 0 && resultEvent.total_cost_usd > 0) {
|
|
6161
|
+
connection.emitEvent({
|
|
6162
|
+
type: "cost_update",
|
|
6163
|
+
costUsd: resultEvent.total_cost_usd
|
|
6164
|
+
});
|
|
6165
|
+
}
|
|
6166
|
+
if (resultEvent.modelUsage && typeof resultEvent.modelUsage === "object") {
|
|
6167
|
+
const modelUsage = resultEvent.modelUsage;
|
|
6168
|
+
let contextWindow = 0;
|
|
6169
|
+
let totalInputTokens = 0;
|
|
6170
|
+
let totalCacheRead = 0;
|
|
6171
|
+
let totalCacheCreation = 0;
|
|
6172
|
+
for (const data of Object.values(modelUsage)) {
|
|
6173
|
+
const d = data;
|
|
6174
|
+
totalInputTokens += d.inputTokens ?? 0;
|
|
6175
|
+
totalCacheRead += d.cacheReadInputTokens ?? 0;
|
|
6176
|
+
totalCacheCreation += d.cacheCreationInputTokens ?? 0;
|
|
6177
|
+
const cw = d.contextWindow ?? 0;
|
|
6178
|
+
if (cw > contextWindow) contextWindow = cw;
|
|
6179
|
+
}
|
|
6180
|
+
if (contextWindow > 0) {
|
|
6181
|
+
const queryInputTokens = totalInputTokens + totalCacheRead + totalCacheCreation;
|
|
6182
|
+
connection.emitEvent({
|
|
6183
|
+
type: "context_update",
|
|
6184
|
+
contextTokens: queryInputTokens,
|
|
6185
|
+
contextWindow,
|
|
6186
|
+
inputTokens: totalInputTokens,
|
|
6187
|
+
cacheReadInputTokens: totalCacheRead,
|
|
6188
|
+
cacheCreationInputTokens: totalCacheCreation
|
|
6189
|
+
});
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
3983
6193
|
function processEventStream(event, connection, responseParts, turnToolCalls, isTyping) {
|
|
3984
6194
|
if (event.type === "assistant") {
|
|
3985
6195
|
if (!isTyping.value) {
|
|
@@ -4001,19 +6211,30 @@ function processEventStream(event, connection, responseParts, turnToolCalls, isT
|
|
|
4001
6211
|
connection.emitEvent({ type: "agent_typing_stop" });
|
|
4002
6212
|
isTyping.value = false;
|
|
4003
6213
|
}
|
|
6214
|
+
emitResultCostAndContext(event, connection);
|
|
4004
6215
|
return true;
|
|
4005
6216
|
}
|
|
4006
6217
|
return false;
|
|
4007
6218
|
}
|
|
4008
|
-
async function runChatQuery(message, connection, projectDir) {
|
|
4009
|
-
const { agentCtx, chatHistory } = await fetchContext(connection);
|
|
4010
|
-
const options = buildChatQueryOptions(agentCtx, projectDir);
|
|
6219
|
+
async function runChatQuery(message, connection, projectDir, sessionId) {
|
|
6220
|
+
const { agentCtx, chatHistory } = await fetchContext(connection, message.chatId);
|
|
6221
|
+
const options = buildChatQueryOptions(agentCtx, projectDir, connection);
|
|
4011
6222
|
const prompt = buildPrompt(message, chatHistory);
|
|
4012
|
-
|
|
6223
|
+
connection.emitAgentStatus("running");
|
|
6224
|
+
const events = query2({
|
|
6225
|
+
prompt,
|
|
6226
|
+
options,
|
|
6227
|
+
...sessionId ? { resume: sessionId } : {}
|
|
6228
|
+
});
|
|
4013
6229
|
const responseParts = [];
|
|
4014
6230
|
const turnToolCalls = [];
|
|
4015
6231
|
const isTyping = { value: false };
|
|
6232
|
+
let resultSessionId;
|
|
4016
6233
|
for await (const event of events) {
|
|
6234
|
+
if (event.type === "result") {
|
|
6235
|
+
const resultEvent = event;
|
|
6236
|
+
resultSessionId = resultEvent.sessionId;
|
|
6237
|
+
}
|
|
4017
6238
|
const done = processEventStream(event, connection, responseParts, turnToolCalls, isTyping);
|
|
4018
6239
|
if (done) break;
|
|
4019
6240
|
}
|
|
@@ -4024,26 +6245,416 @@ async function runChatQuery(message, connection, projectDir) {
|
|
|
4024
6245
|
if (responseText) {
|
|
4025
6246
|
await connection.emitChatMessage(responseText);
|
|
4026
6247
|
}
|
|
6248
|
+
return resultSessionId;
|
|
4027
6249
|
}
|
|
4028
|
-
async function handleProjectChatMessage(message, connection, projectDir) {
|
|
4029
|
-
connection.emitAgentStatus("
|
|
6250
|
+
async function handleProjectChatMessage(message, connection, projectDir, sessionId) {
|
|
6251
|
+
connection.emitAgentStatus("fetching_context");
|
|
4030
6252
|
try {
|
|
4031
|
-
await runChatQuery(message, connection, projectDir);
|
|
6253
|
+
return await runChatQuery(message, connection, projectDir, sessionId);
|
|
4032
6254
|
} catch (error) {
|
|
4033
|
-
|
|
6255
|
+
logger6.error("Failed to handle message", errorMeta(error));
|
|
6256
|
+
connection.emitAgentStatus("error");
|
|
4034
6257
|
try {
|
|
4035
6258
|
await connection.emitChatMessage(
|
|
4036
6259
|
"I encountered an error processing your message. Please try again."
|
|
4037
6260
|
);
|
|
4038
6261
|
} catch {
|
|
4039
6262
|
}
|
|
6263
|
+
return void 0;
|
|
6264
|
+
} finally {
|
|
6265
|
+
connection.emitAgentStatus("idle");
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
|
|
6269
|
+
// src/runner/project-audit-handler.ts
|
|
6270
|
+
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
6271
|
+
|
|
6272
|
+
// src/tools/audit-tools.ts
|
|
6273
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6274
|
+
import { tool as tool8, createSdkMcpServer as createSdkMcpServer3 } from "@anthropic-ai/claude-agent-sdk";
|
|
6275
|
+
import { z as z8 } from "zod";
|
|
6276
|
+
function mapCreateTag(input) {
|
|
6277
|
+
return {
|
|
6278
|
+
type: "create_tag",
|
|
6279
|
+
tagName: input.name,
|
|
6280
|
+
suggestion: `Create new tag "${input.name}"${input.description ? `: ${input.description}` : ""}`,
|
|
6281
|
+
reasoning: input.reasoning,
|
|
6282
|
+
payload: { name: input.name, color: input.color ?? "#6B7280", description: input.description }
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
function mapUpdateDescription(input) {
|
|
6286
|
+
return {
|
|
6287
|
+
type: "update_description",
|
|
6288
|
+
tagId: input.tagId,
|
|
6289
|
+
tagName: input.tagName,
|
|
6290
|
+
suggestion: `Update description for "${input.tagName}"`,
|
|
6291
|
+
reasoning: input.reasoning,
|
|
6292
|
+
payload: { description: input.description }
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
function mapContextLink(input) {
|
|
6296
|
+
return {
|
|
6297
|
+
type: "add_context_link",
|
|
6298
|
+
tagId: input.tagId,
|
|
6299
|
+
tagName: input.tagName,
|
|
6300
|
+
suggestion: `Link ${input.linkType}:${input.path} to "${input.tagName}"`,
|
|
6301
|
+
reasoning: input.reasoning,
|
|
6302
|
+
payload: { contextLink: { type: input.linkType, path: input.path, label: input.label } }
|
|
6303
|
+
};
|
|
6304
|
+
}
|
|
6305
|
+
function mapDocGap(input) {
|
|
6306
|
+
return {
|
|
6307
|
+
type: "documentation_gap",
|
|
6308
|
+
tagId: input.tagId,
|
|
6309
|
+
tagName: input.tagName,
|
|
6310
|
+
suggestion: `Documentation gap: ${input.filePath} (${input.readCount} reads)`,
|
|
6311
|
+
reasoning: input.reasoning,
|
|
6312
|
+
payload: {
|
|
6313
|
+
filePath: input.filePath,
|
|
6314
|
+
readCount: input.readCount,
|
|
6315
|
+
suggestedAction: input.suggestedAction
|
|
6316
|
+
}
|
|
6317
|
+
};
|
|
6318
|
+
}
|
|
6319
|
+
function mapMergeTags(input) {
|
|
6320
|
+
return {
|
|
6321
|
+
type: "merge_tags",
|
|
6322
|
+
tagId: input.tagId,
|
|
6323
|
+
tagName: input.tagName,
|
|
6324
|
+
suggestion: `Merge "${input.tagName}" into "${input.mergeIntoTagName}"`,
|
|
6325
|
+
reasoning: input.reasoning,
|
|
6326
|
+
payload: { mergeIntoTagId: input.mergeIntoTagId }
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
function mapRenameTag(input) {
|
|
6330
|
+
return {
|
|
6331
|
+
type: "rename_tag",
|
|
6332
|
+
tagId: input.tagId,
|
|
6333
|
+
tagName: input.tagName,
|
|
6334
|
+
suggestion: `Rename "${input.tagName}" to "${input.newName}"`,
|
|
6335
|
+
reasoning: input.reasoning,
|
|
6336
|
+
payload: { newName: input.newName }
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
6339
|
+
var TOOL_MAPPERS = {
|
|
6340
|
+
recommend_create_tag: mapCreateTag,
|
|
6341
|
+
recommend_update_description: mapUpdateDescription,
|
|
6342
|
+
recommend_context_link: mapContextLink,
|
|
6343
|
+
flag_documentation_gap: mapDocGap,
|
|
6344
|
+
recommend_merge_tags: mapMergeTags,
|
|
6345
|
+
recommend_rename_tag: mapRenameTag
|
|
6346
|
+
};
|
|
6347
|
+
function collectRecommendation(toolName, input, collector, onRecommendation) {
|
|
6348
|
+
const mapper = TOOL_MAPPERS[toolName];
|
|
6349
|
+
if (!mapper) return JSON.stringify({ error: `Unknown tool: ${toolName}` });
|
|
6350
|
+
const rec = { id: randomUUID3(), ...mapper(input) };
|
|
6351
|
+
collector.recommendations.push(rec);
|
|
6352
|
+
onRecommendation?.({ tagName: rec.tagName ?? rec.type, type: rec.type });
|
|
6353
|
+
return JSON.stringify({ success: true, recommendationId: rec.id });
|
|
6354
|
+
}
|
|
6355
|
+
function createAuditMcpServer(collector, onRecommendation) {
|
|
6356
|
+
const auditTools = [
|
|
6357
|
+
tool8(
|
|
6358
|
+
"recommend_create_tag",
|
|
6359
|
+
"Recommend creating a new tag for an uncovered subsystem or area",
|
|
6360
|
+
{
|
|
6361
|
+
name: z8.string().describe("Proposed tag name (lowercase, hyphenated)"),
|
|
6362
|
+
color: z8.string().optional().describe("Hex color code"),
|
|
6363
|
+
description: z8.string().describe("What this tag covers"),
|
|
6364
|
+
reasoning: z8.string().describe("Why this tag should be created")
|
|
6365
|
+
},
|
|
6366
|
+
async (args) => {
|
|
6367
|
+
const result = collectRecommendation(
|
|
6368
|
+
"recommend_create_tag",
|
|
6369
|
+
args,
|
|
6370
|
+
collector,
|
|
6371
|
+
onRecommendation
|
|
6372
|
+
);
|
|
6373
|
+
return { content: [{ type: "text", text: result }] };
|
|
6374
|
+
}
|
|
6375
|
+
),
|
|
6376
|
+
tool8(
|
|
6377
|
+
"recommend_update_description",
|
|
6378
|
+
"Recommend updating a tag's description to better reflect its scope",
|
|
6379
|
+
{
|
|
6380
|
+
tagId: z8.string(),
|
|
6381
|
+
tagName: z8.string(),
|
|
6382
|
+
description: z8.string().describe("Proposed new description"),
|
|
6383
|
+
reasoning: z8.string()
|
|
6384
|
+
},
|
|
6385
|
+
async (args) => {
|
|
6386
|
+
const result = collectRecommendation(
|
|
6387
|
+
"recommend_update_description",
|
|
6388
|
+
args,
|
|
6389
|
+
collector,
|
|
6390
|
+
onRecommendation
|
|
6391
|
+
);
|
|
6392
|
+
return { content: [{ type: "text", text: result }] };
|
|
6393
|
+
}
|
|
6394
|
+
),
|
|
6395
|
+
tool8(
|
|
6396
|
+
"recommend_context_link",
|
|
6397
|
+
"Recommend linking a doc, rule, file, or folder to a tag's contextPaths",
|
|
6398
|
+
{
|
|
6399
|
+
tagId: z8.string(),
|
|
6400
|
+
tagName: z8.string(),
|
|
6401
|
+
linkType: z8.enum(["rule", "doc", "file", "folder"]),
|
|
6402
|
+
path: z8.string(),
|
|
6403
|
+
label: z8.string().optional(),
|
|
6404
|
+
reasoning: z8.string()
|
|
6405
|
+
},
|
|
6406
|
+
async (args) => {
|
|
6407
|
+
const result = collectRecommendation(
|
|
6408
|
+
"recommend_context_link",
|
|
6409
|
+
args,
|
|
6410
|
+
collector,
|
|
6411
|
+
onRecommendation
|
|
6412
|
+
);
|
|
6413
|
+
return { content: [{ type: "text", text: result }] };
|
|
6414
|
+
}
|
|
6415
|
+
),
|
|
6416
|
+
tool8(
|
|
6417
|
+
"flag_documentation_gap",
|
|
6418
|
+
"Flag a file that agents read heavily but has no tag documentation linked",
|
|
6419
|
+
{
|
|
6420
|
+
tagName: z8.string().describe("Tag whose agents read this file"),
|
|
6421
|
+
tagId: z8.string().optional(),
|
|
6422
|
+
filePath: z8.string(),
|
|
6423
|
+
readCount: z8.number(),
|
|
6424
|
+
suggestedAction: z8.string().describe("What doc or rule should be created"),
|
|
6425
|
+
reasoning: z8.string()
|
|
6426
|
+
},
|
|
6427
|
+
async (args) => {
|
|
6428
|
+
const result = collectRecommendation(
|
|
6429
|
+
"flag_documentation_gap",
|
|
6430
|
+
args,
|
|
6431
|
+
collector,
|
|
6432
|
+
onRecommendation
|
|
6433
|
+
);
|
|
6434
|
+
return { content: [{ type: "text", text: result }] };
|
|
6435
|
+
}
|
|
6436
|
+
),
|
|
6437
|
+
tool8(
|
|
6438
|
+
"recommend_merge_tags",
|
|
6439
|
+
"Recommend merging one tag into another",
|
|
6440
|
+
{
|
|
6441
|
+
tagId: z8.string().describe("Tag ID to be merged (removed after merge)"),
|
|
6442
|
+
tagName: z8.string().describe("Name of the tag to be merged"),
|
|
6443
|
+
mergeIntoTagId: z8.string().describe("Tag ID to merge into (kept)"),
|
|
6444
|
+
mergeIntoTagName: z8.string(),
|
|
6445
|
+
reasoning: z8.string()
|
|
6446
|
+
},
|
|
6447
|
+
async (args) => {
|
|
6448
|
+
const result = collectRecommendation(
|
|
6449
|
+
"recommend_merge_tags",
|
|
6450
|
+
args,
|
|
6451
|
+
collector,
|
|
6452
|
+
onRecommendation
|
|
6453
|
+
);
|
|
6454
|
+
return { content: [{ type: "text", text: result }] };
|
|
6455
|
+
}
|
|
6456
|
+
),
|
|
6457
|
+
tool8(
|
|
6458
|
+
"recommend_rename_tag",
|
|
6459
|
+
"Recommend renaming a tag",
|
|
6460
|
+
{
|
|
6461
|
+
tagId: z8.string(),
|
|
6462
|
+
tagName: z8.string().describe("Current tag name"),
|
|
6463
|
+
newName: z8.string().describe("Proposed new name"),
|
|
6464
|
+
reasoning: z8.string()
|
|
6465
|
+
},
|
|
6466
|
+
async (args) => {
|
|
6467
|
+
const result = collectRecommendation(
|
|
6468
|
+
"recommend_rename_tag",
|
|
6469
|
+
args,
|
|
6470
|
+
collector,
|
|
6471
|
+
onRecommendation
|
|
6472
|
+
);
|
|
6473
|
+
return { content: [{ type: "text", text: result }] };
|
|
6474
|
+
}
|
|
6475
|
+
),
|
|
6476
|
+
tool8(
|
|
6477
|
+
"complete_audit",
|
|
6478
|
+
"Signal that the audit is complete with a summary of all findings",
|
|
6479
|
+
{ summary: z8.string().describe("Brief overview of all findings") },
|
|
6480
|
+
async (args) => {
|
|
6481
|
+
collector.complete = true;
|
|
6482
|
+
collector.summary = args.summary ?? "Audit completed.";
|
|
6483
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
|
|
6484
|
+
}
|
|
6485
|
+
)
|
|
6486
|
+
];
|
|
6487
|
+
return createSdkMcpServer3({
|
|
6488
|
+
name: "tag-audit",
|
|
6489
|
+
tools: auditTools
|
|
6490
|
+
});
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
// src/runner/project-audit-handler.ts
|
|
6494
|
+
var logger7 = createServiceLogger("ProjectAudit");
|
|
6495
|
+
var FALLBACK_MODEL2 = "claude-sonnet-4-20250514";
|
|
6496
|
+
function buildTagSection(tags) {
|
|
6497
|
+
if (tags.length === 0) return "No tags configured yet.";
|
|
6498
|
+
return tags.map((t) => {
|
|
6499
|
+
const paths = t.contextPaths ?? [];
|
|
6500
|
+
const pathStr = paths.length > 0 ? `
|
|
6501
|
+
Context links: ${paths.map((p) => `${p.type}:${p.path}`).join(", ")}` : "";
|
|
6502
|
+
return ` - ${t.name} (id: ${t.id})${t.description ? `: ${t.description}` : " [no description]"}${pathStr}
|
|
6503
|
+
Active tasks: ${t.activeTaskCount}`;
|
|
6504
|
+
}).join("\n");
|
|
6505
|
+
}
|
|
6506
|
+
function buildHeatmapSection(entries) {
|
|
6507
|
+
if (entries.length === 0) return "No file read analytics data available.";
|
|
6508
|
+
return entries.slice(0, 50).map((e) => {
|
|
6509
|
+
const tagBreakdown = Object.entries(e.byTag).sort(([, a], [, b]) => b - a).map(([tag, count]) => `${tag}:${count}`).join(", ");
|
|
6510
|
+
return ` ${e.filePath} \u2014 ${e.totalReads} reads${tagBreakdown ? ` (${tagBreakdown})` : ""}`;
|
|
6511
|
+
}).join("\n");
|
|
6512
|
+
}
|
|
6513
|
+
function buildAuditSystemPrompt(projectName, tags, heatmapData, projectDir) {
|
|
6514
|
+
return [
|
|
6515
|
+
"You are a project organization expert analyzing tag taxonomy for a software project.",
|
|
6516
|
+
"Tags are used to categorize tasks and link relevant documentation/rules/files to subsystems.",
|
|
6517
|
+
"",
|
|
6518
|
+
`PROJECT: ${projectName}`,
|
|
6519
|
+
"",
|
|
6520
|
+
`EXISTING TAGS (${tags.length}):`,
|
|
6521
|
+
buildTagSection(tags),
|
|
6522
|
+
"",
|
|
6523
|
+
"FILE READ ANALYTICS (what agents actually read, by tag):",
|
|
6524
|
+
buildHeatmapSection(heatmapData),
|
|
6525
|
+
"",
|
|
6526
|
+
`You have full access to the codebase at: ${projectDir}`,
|
|
6527
|
+
"Use your file reading and searching tools to understand the codebase structure,",
|
|
6528
|
+
"module boundaries, and architectural patterns before making recommendations.",
|
|
6529
|
+
"",
|
|
6530
|
+
"ANALYSIS TASKS:",
|
|
6531
|
+
"1. Read actual source files to understand code areas and module boundaries",
|
|
6532
|
+
"2. Search for imports, class definitions, and architectural patterns",
|
|
6533
|
+
"3. Coverage: Are all major subsystems/services represented by tags?",
|
|
6534
|
+
"4. Descriptions: Do all tags have clear, useful descriptions?",
|
|
6535
|
+
"5. Context Links: Are relevant rules/docs/folders linked to tags via contextPaths?",
|
|
6536
|
+
"6. Documentation Gaps: Which high-read files lack linked documentation?",
|
|
6537
|
+
"7. Cleanup: Any tags that should be merged, renamed, or removed?",
|
|
6538
|
+
"",
|
|
6539
|
+
"Use the tag-audit MCP tools to submit each recommendation.",
|
|
6540
|
+
"Call complete_audit when you are done with a thorough summary.",
|
|
6541
|
+
"Be comprehensive \u2014 recommend all improvements your analysis supports.",
|
|
6542
|
+
"Analyze actual file contents, not just file names."
|
|
6543
|
+
].join("\n");
|
|
6544
|
+
}
|
|
6545
|
+
function emitToolCallProgress(event, request, connection) {
|
|
6546
|
+
if (event.type !== "assistant") return;
|
|
6547
|
+
const assistantEvent = event;
|
|
6548
|
+
for (const block of assistantEvent.message.content) {
|
|
6549
|
+
if (block.type === "tool_use" && block.name) {
|
|
6550
|
+
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
6551
|
+
connection.emitAuditProgress({
|
|
6552
|
+
requestId: request.requestId,
|
|
6553
|
+
activity: {
|
|
6554
|
+
tool: block.name,
|
|
6555
|
+
input: inputStr.slice(0, 500),
|
|
6556
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6557
|
+
}
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
async function runAuditQuery(request, connection, projectDir) {
|
|
6563
|
+
connection.emitAgentStatus("fetching_context");
|
|
6564
|
+
let agentCtx = null;
|
|
6565
|
+
try {
|
|
6566
|
+
agentCtx = await connection.fetchAgentContext();
|
|
6567
|
+
} catch {
|
|
6568
|
+
logger7.warn("Could not fetch agent context for audit, using defaults");
|
|
6569
|
+
}
|
|
6570
|
+
connection.emitAgentStatus("running");
|
|
6571
|
+
const model = agentCtx?.model || FALLBACK_MODEL2;
|
|
6572
|
+
const settings = agentCtx?.agentSettings ?? {};
|
|
6573
|
+
const collector = {
|
|
6574
|
+
recommendations: [],
|
|
6575
|
+
summary: "Audit completed.",
|
|
6576
|
+
complete: false
|
|
6577
|
+
};
|
|
6578
|
+
const onRecommendation = (rec) => {
|
|
6579
|
+
connection.emitEvent({
|
|
6580
|
+
type: "audit_recommendation",
|
|
6581
|
+
tagName: rec.tagName,
|
|
6582
|
+
recommendationType: rec.type
|
|
6583
|
+
});
|
|
6584
|
+
};
|
|
6585
|
+
const systemPrompt = buildAuditSystemPrompt(
|
|
6586
|
+
request.projectName,
|
|
6587
|
+
request.tags,
|
|
6588
|
+
request.fileHeatmap,
|
|
6589
|
+
projectDir
|
|
6590
|
+
);
|
|
6591
|
+
const userPrompt = [
|
|
6592
|
+
"Analyze the project's tag taxonomy and submit recommendations using the tag-audit MCP tools.",
|
|
6593
|
+
`There are currently ${request.tags.length} tags configured.`,
|
|
6594
|
+
request.fileHeatmap.length > 0 ? `File analytics show ${request.fileHeatmap.length} files with read activity.` : "No file read analytics available.",
|
|
6595
|
+
"",
|
|
6596
|
+
"Start by exploring the codebase structure, then analyze each tag for accuracy and completeness.",
|
|
6597
|
+
"Call complete_audit when done."
|
|
6598
|
+
].join("\n");
|
|
6599
|
+
const events = query3({
|
|
6600
|
+
prompt: userPrompt,
|
|
6601
|
+
options: {
|
|
6602
|
+
model,
|
|
6603
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: systemPrompt },
|
|
6604
|
+
cwd: projectDir,
|
|
6605
|
+
permissionMode: "bypassPermissions",
|
|
6606
|
+
allowDangerouslySkipPermissions: true,
|
|
6607
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
6608
|
+
mcpServers: { "tag-audit": createAuditMcpServer(collector, onRecommendation) },
|
|
6609
|
+
maxTurns: settings.maxTurns ?? 75,
|
|
6610
|
+
maxBudgetUsd: settings.maxBudgetUsd ?? 5,
|
|
6611
|
+
effort: settings.effort,
|
|
6612
|
+
thinking: settings.thinking
|
|
6613
|
+
}
|
|
6614
|
+
});
|
|
6615
|
+
const responseParts = [];
|
|
6616
|
+
const turnToolCalls = [];
|
|
6617
|
+
const isTyping = { value: false };
|
|
6618
|
+
for await (const event of events) {
|
|
6619
|
+
emitToolCallProgress(event, request, connection);
|
|
6620
|
+
const done = processEventStream(event, connection, responseParts, turnToolCalls, isTyping);
|
|
6621
|
+
if (done) break;
|
|
6622
|
+
}
|
|
6623
|
+
if (isTyping.value) {
|
|
6624
|
+
connection.emitEvent({ type: "agent_typing_stop" });
|
|
6625
|
+
}
|
|
6626
|
+
return collector;
|
|
6627
|
+
}
|
|
6628
|
+
async function handleProjectAuditRequest(request, connection, projectDir) {
|
|
6629
|
+
connection.emitAgentStatus("running");
|
|
6630
|
+
try {
|
|
6631
|
+
const collector = await runAuditQuery(request, connection, projectDir);
|
|
6632
|
+
const result = {
|
|
6633
|
+
recommendations: collector.recommendations,
|
|
6634
|
+
summary: collector.summary,
|
|
6635
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6636
|
+
};
|
|
6637
|
+
logger7.info("Tag audit completed", {
|
|
6638
|
+
requestId: request.requestId,
|
|
6639
|
+
recommendationCount: result.recommendations.length
|
|
6640
|
+
});
|
|
6641
|
+
connection.emitAuditResult({ requestId: request.requestId, result });
|
|
6642
|
+
} catch (error) {
|
|
6643
|
+
logger7.error("Tag audit failed", {
|
|
6644
|
+
requestId: request.requestId,
|
|
6645
|
+
...errorMeta(error)
|
|
6646
|
+
});
|
|
6647
|
+
connection.emitAuditResult({
|
|
6648
|
+
requestId: request.requestId,
|
|
6649
|
+
error: error instanceof Error ? error.message : "Tag audit failed"
|
|
6650
|
+
});
|
|
4040
6651
|
} finally {
|
|
4041
6652
|
connection.emitAgentStatus("idle");
|
|
4042
6653
|
}
|
|
4043
6654
|
}
|
|
4044
6655
|
|
|
4045
6656
|
// src/runner/project-runner.ts
|
|
4046
|
-
var
|
|
6657
|
+
var logger8 = createServiceLogger("ProjectRunner");
|
|
4047
6658
|
var __filename = fileURLToPath(import.meta.url);
|
|
4048
6659
|
var __dirname = path.dirname(__filename);
|
|
4049
6660
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
@@ -4061,13 +6672,20 @@ function setupWorkDir(projectDir, assignment) {
|
|
|
4061
6672
|
workDir = projectDir;
|
|
4062
6673
|
}
|
|
4063
6674
|
if (branch && branch !== devBranch) {
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
6675
|
+
if (hasUncommittedChanges(workDir)) {
|
|
6676
|
+
logger8.warn("Uncommitted changes in work dir, skipping checkout", {
|
|
6677
|
+
taskId: shortId,
|
|
6678
|
+
branch
|
|
6679
|
+
});
|
|
6680
|
+
} else {
|
|
4067
6681
|
try {
|
|
4068
|
-
|
|
6682
|
+
execSync7(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
4069
6683
|
} catch {
|
|
4070
|
-
|
|
6684
|
+
try {
|
|
6685
|
+
execSync7(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
|
|
6686
|
+
} catch {
|
|
6687
|
+
logger8.warn("Could not checkout branch", { taskId: shortId, branch });
|
|
6688
|
+
}
|
|
4071
6689
|
}
|
|
4072
6690
|
}
|
|
4073
6691
|
}
|
|
@@ -4106,13 +6724,13 @@ function spawnChildAgent(assignment, workDir) {
|
|
|
4106
6724
|
child.stdout?.on("data", (data) => {
|
|
4107
6725
|
const lines = data.toString().trimEnd().split("\n");
|
|
4108
6726
|
for (const line of lines) {
|
|
4109
|
-
|
|
6727
|
+
logger8.info(line, { taskId: shortId });
|
|
4110
6728
|
}
|
|
4111
6729
|
});
|
|
4112
6730
|
child.stderr?.on("data", (data) => {
|
|
4113
6731
|
const lines = data.toString().trimEnd().split("\n");
|
|
4114
6732
|
for (const line of lines) {
|
|
4115
|
-
|
|
6733
|
+
logger8.error(line, { taskId: shortId });
|
|
4116
6734
|
}
|
|
4117
6735
|
});
|
|
4118
6736
|
return child;
|
|
@@ -4124,27 +6742,71 @@ var ProjectRunner = class {
|
|
|
4124
6742
|
heartbeatTimer = null;
|
|
4125
6743
|
stopping = false;
|
|
4126
6744
|
resolveLifecycle = null;
|
|
6745
|
+
chatSessionIds = /* @__PURE__ */ new Map();
|
|
4127
6746
|
// Start command process management
|
|
4128
6747
|
startCommandChild = null;
|
|
4129
6748
|
startCommandRunning = false;
|
|
4130
6749
|
setupComplete = false;
|
|
6750
|
+
branchSwitchCommand;
|
|
6751
|
+
commitWatcher;
|
|
4131
6752
|
constructor(config) {
|
|
4132
6753
|
this.projectDir = config.projectDir;
|
|
4133
6754
|
this.connection = new ProjectConnection({
|
|
4134
6755
|
apiUrl: config.conveyorApiUrl,
|
|
4135
6756
|
projectToken: config.projectToken,
|
|
4136
|
-
projectId: config.projectId
|
|
6757
|
+
projectId: config.projectId,
|
|
6758
|
+
projectDir: config.projectDir
|
|
4137
6759
|
});
|
|
6760
|
+
this.commitWatcher = new CommitWatcher(
|
|
6761
|
+
{
|
|
6762
|
+
projectDir: this.projectDir,
|
|
6763
|
+
pollIntervalMs: Number(process.env.CONVEYOR_COMMIT_POLL_INTERVAL) || 1e4,
|
|
6764
|
+
debounceMs: 3e3
|
|
6765
|
+
},
|
|
6766
|
+
{
|
|
6767
|
+
onNewCommits: async (data) => {
|
|
6768
|
+
this.connection.emitNewCommitsDetected({
|
|
6769
|
+
branch: data.branch,
|
|
6770
|
+
commitCount: data.commitCount,
|
|
6771
|
+
latestCommit: {
|
|
6772
|
+
sha: data.newCommitSha,
|
|
6773
|
+
message: data.latestMessage,
|
|
6774
|
+
author: data.latestAuthor
|
|
6775
|
+
},
|
|
6776
|
+
autoSyncing: true
|
|
6777
|
+
});
|
|
6778
|
+
const startTime = Date.now();
|
|
6779
|
+
const stepsRun = await this.smartSync(data.previousSha, data.newCommitSha, data.branch);
|
|
6780
|
+
this.connection.emitEnvironmentReady({
|
|
6781
|
+
branch: data.branch,
|
|
6782
|
+
commitsSynced: data.commitCount,
|
|
6783
|
+
syncDurationMs: Date.now() - startTime,
|
|
6784
|
+
stepsRun
|
|
6785
|
+
});
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
);
|
|
4138
6789
|
}
|
|
4139
6790
|
checkoutWorkspaceBranch() {
|
|
4140
6791
|
const workspaceBranch = process.env.CONVEYOR_WORKSPACE_BRANCH;
|
|
4141
6792
|
if (!workspaceBranch) return;
|
|
6793
|
+
const currentBranch = this.getCurrentBranch();
|
|
6794
|
+
if (currentBranch === workspaceBranch) {
|
|
6795
|
+
logger8.info("Already on workspace branch", { workspaceBranch });
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
if (hasUncommittedChanges(this.projectDir)) {
|
|
6799
|
+
logger8.warn("Uncommitted changes detected, skipping workspace branch checkout", {
|
|
6800
|
+
workspaceBranch
|
|
6801
|
+
});
|
|
6802
|
+
return;
|
|
6803
|
+
}
|
|
4142
6804
|
try {
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
6805
|
+
execSync7(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
6806
|
+
execSync7(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
6807
|
+
logger8.info("Checked out workspace branch", { workspaceBranch });
|
|
4146
6808
|
} catch (err) {
|
|
4147
|
-
|
|
6809
|
+
logger8.warn("Failed to checkout workspace branch, continuing on current branch", {
|
|
4148
6810
|
workspaceBranch,
|
|
4149
6811
|
...errorMeta(err)
|
|
4150
6812
|
});
|
|
@@ -4153,15 +6815,15 @@ var ProjectRunner = class {
|
|
|
4153
6815
|
async executeSetupCommand() {
|
|
4154
6816
|
const cmd = process.env.CONVEYOR_SETUP_COMMAND;
|
|
4155
6817
|
if (!cmd) return;
|
|
4156
|
-
|
|
6818
|
+
logger8.info("Running setup command", { command: cmd });
|
|
4157
6819
|
try {
|
|
4158
6820
|
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
4159
6821
|
this.connection.emitEvent({ type: "setup_output", stream, data });
|
|
4160
6822
|
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
4161
6823
|
});
|
|
4162
|
-
|
|
6824
|
+
logger8.info("Setup command completed");
|
|
4163
6825
|
} catch (error) {
|
|
4164
|
-
|
|
6826
|
+
logger8.error("Setup command failed", errorMeta(error));
|
|
4165
6827
|
this.connection.emitEvent({
|
|
4166
6828
|
type: "setup_error",
|
|
4167
6829
|
message: error instanceof Error ? error.message : "Setup command failed"
|
|
@@ -4172,7 +6834,7 @@ var ProjectRunner = class {
|
|
|
4172
6834
|
executeStartCommand() {
|
|
4173
6835
|
const cmd = process.env.CONVEYOR_START_COMMAND;
|
|
4174
6836
|
if (!cmd) return;
|
|
4175
|
-
|
|
6837
|
+
logger8.info("Running start command", { command: cmd });
|
|
4176
6838
|
const child = runStartCommand(cmd, this.projectDir, (stream, data) => {
|
|
4177
6839
|
this.connection.emitEvent({ type: "start_command_output", stream, data });
|
|
4178
6840
|
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
@@ -4182,7 +6844,7 @@ var ProjectRunner = class {
|
|
|
4182
6844
|
child.on("exit", (code, signal) => {
|
|
4183
6845
|
this.startCommandRunning = false;
|
|
4184
6846
|
this.startCommandChild = null;
|
|
4185
|
-
|
|
6847
|
+
logger8.info("Start command exited", { code, signal });
|
|
4186
6848
|
this.connection.emitEvent({
|
|
4187
6849
|
type: "start_command_exited",
|
|
4188
6850
|
code,
|
|
@@ -4193,13 +6855,13 @@ var ProjectRunner = class {
|
|
|
4193
6855
|
child.on("error", (err) => {
|
|
4194
6856
|
this.startCommandRunning = false;
|
|
4195
6857
|
this.startCommandChild = null;
|
|
4196
|
-
|
|
6858
|
+
logger8.error("Start command error", errorMeta(err));
|
|
4197
6859
|
});
|
|
4198
6860
|
}
|
|
4199
6861
|
async killStartCommand() {
|
|
4200
6862
|
const child = this.startCommandChild;
|
|
4201
6863
|
if (!child || !this.startCommandRunning) return;
|
|
4202
|
-
|
|
6864
|
+
logger8.info("Killing start command");
|
|
4203
6865
|
try {
|
|
4204
6866
|
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
4205
6867
|
} catch {
|
|
@@ -4229,21 +6891,177 @@ var ProjectRunner = class {
|
|
|
4229
6891
|
this.executeStartCommand();
|
|
4230
6892
|
}
|
|
4231
6893
|
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
6894
|
return {
|
|
4241
6895
|
setupComplete: this.setupComplete,
|
|
4242
6896
|
startCommandRunning: this.startCommandRunning,
|
|
4243
|
-
currentBranch,
|
|
6897
|
+
currentBranch: this.getCurrentBranch() ?? "unknown",
|
|
4244
6898
|
previewPort: Number(process.env.CONVEYOR_PREVIEW_PORT) || null
|
|
4245
6899
|
};
|
|
4246
6900
|
}
|
|
6901
|
+
getCurrentBranch() {
|
|
6902
|
+
return getCurrentBranch(this.projectDir);
|
|
6903
|
+
}
|
|
6904
|
+
// oxlint-disable-next-line max-lines-per-function, complexity -- sequential sync steps with per-step error handling
|
|
6905
|
+
async smartSync(previousSha, newSha, branch) {
|
|
6906
|
+
const stepsRun = [];
|
|
6907
|
+
if (hasUncommittedChanges(this.projectDir)) {
|
|
6908
|
+
this.connection.emitEvent({
|
|
6909
|
+
type: "commit_watch_warning",
|
|
6910
|
+
message: "Working tree has uncommitted changes. Auto-pull skipped."
|
|
6911
|
+
});
|
|
6912
|
+
return ["skipped:dirty_tree"];
|
|
6913
|
+
}
|
|
6914
|
+
await this.killStartCommand();
|
|
6915
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "running" });
|
|
6916
|
+
try {
|
|
6917
|
+
execSync7(`git pull origin ${branch}`, {
|
|
6918
|
+
cwd: this.projectDir,
|
|
6919
|
+
stdio: "pipe",
|
|
6920
|
+
timeout: 6e4
|
|
6921
|
+
});
|
|
6922
|
+
stepsRun.push("pull");
|
|
6923
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "success" });
|
|
6924
|
+
} catch (err) {
|
|
6925
|
+
const message = err instanceof Error ? err.message : "Pull failed";
|
|
6926
|
+
this.connection.emitEnvSwitchProgress({ step: "pull", status: "error", message });
|
|
6927
|
+
logger8.error("Git pull failed during commit sync", errorMeta(err));
|
|
6928
|
+
this.executeStartCommand();
|
|
6929
|
+
return ["error:pull"];
|
|
6930
|
+
}
|
|
6931
|
+
let changedFiles = [];
|
|
6932
|
+
try {
|
|
6933
|
+
changedFiles = execSync7(`git diff --name-only ${previousSha}..${newSha}`, {
|
|
6934
|
+
cwd: this.projectDir,
|
|
6935
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6936
|
+
}).toString().trim().split("\n").filter(Boolean);
|
|
6937
|
+
} catch {
|
|
6938
|
+
}
|
|
6939
|
+
const needsInstall = changedFiles.some(
|
|
6940
|
+
(f) => f === "package.json" || f === "bun.lockb" || f === "bunfig.toml" || f.endsWith("/package.json") || f.endsWith("/bun.lockb")
|
|
6941
|
+
);
|
|
6942
|
+
const needsPrisma = changedFiles.some(
|
|
6943
|
+
(f) => f.includes("prisma/schema.prisma") || f.includes("prisma/migrations/")
|
|
6944
|
+
);
|
|
6945
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
6946
|
+
if (cmd && (needsInstall || needsPrisma)) {
|
|
6947
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
6948
|
+
try {
|
|
6949
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
6950
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
6951
|
+
});
|
|
6952
|
+
stepsRun.push("branchSwitchCommand");
|
|
6953
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
6954
|
+
} catch (err) {
|
|
6955
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
6956
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
6957
|
+
logger8.error("Branch switch command failed during commit sync", errorMeta(err));
|
|
6958
|
+
}
|
|
6959
|
+
} else if (!cmd) {
|
|
6960
|
+
if (needsInstall) {
|
|
6961
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "running" });
|
|
6962
|
+
try {
|
|
6963
|
+
execSync7("bun install", { cwd: this.projectDir, timeout: 12e4, stdio: "pipe" });
|
|
6964
|
+
stepsRun.push("install");
|
|
6965
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "success" });
|
|
6966
|
+
} catch (err) {
|
|
6967
|
+
const message = err instanceof Error ? err.message : "Install failed";
|
|
6968
|
+
this.connection.emitEnvSwitchProgress({ step: "install", status: "error", message });
|
|
6969
|
+
logger8.error("bun install failed during commit sync", errorMeta(err));
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
if (needsPrisma) {
|
|
6973
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "running" });
|
|
6974
|
+
try {
|
|
6975
|
+
execSync7("bunx prisma generate", {
|
|
6976
|
+
cwd: this.projectDir,
|
|
6977
|
+
timeout: 6e4,
|
|
6978
|
+
stdio: "pipe"
|
|
6979
|
+
});
|
|
6980
|
+
execSync7("bunx prisma db push --accept-data-loss", {
|
|
6981
|
+
cwd: this.projectDir,
|
|
6982
|
+
timeout: 6e4,
|
|
6983
|
+
stdio: "pipe"
|
|
6984
|
+
});
|
|
6985
|
+
stepsRun.push("prisma");
|
|
6986
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "success" });
|
|
6987
|
+
} catch (err) {
|
|
6988
|
+
const message = err instanceof Error ? err.message : "Prisma sync failed";
|
|
6989
|
+
this.connection.emitEnvSwitchProgress({ step: "prisma", status: "error", message });
|
|
6990
|
+
logger8.error("Prisma sync failed during commit sync", errorMeta(err));
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
this.executeStartCommand();
|
|
6995
|
+
stepsRun.push("startCommand");
|
|
6996
|
+
return stepsRun;
|
|
6997
|
+
}
|
|
6998
|
+
async handleSwitchBranch(data, callback) {
|
|
6999
|
+
const { branch, syncAfter } = data;
|
|
7000
|
+
try {
|
|
7001
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "running" });
|
|
7002
|
+
try {
|
|
7003
|
+
execSync7("git fetch origin", { cwd: this.projectDir, stdio: "pipe" });
|
|
7004
|
+
} catch {
|
|
7005
|
+
logger8.warn("Git fetch failed during branch switch");
|
|
7006
|
+
}
|
|
7007
|
+
this.connection.emitEnvSwitchProgress({ step: "fetch", status: "success" });
|
|
7008
|
+
detachWorktreeBranch(this.projectDir, branch);
|
|
7009
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "running" });
|
|
7010
|
+
try {
|
|
7011
|
+
execSync7(`git checkout ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
7012
|
+
} catch (err) {
|
|
7013
|
+
const message = err instanceof Error ? err.message : "Checkout failed";
|
|
7014
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "error", message });
|
|
7015
|
+
callback({ ok: false, error: `Failed to checkout branch: ${message}` });
|
|
7016
|
+
return;
|
|
7017
|
+
}
|
|
7018
|
+
try {
|
|
7019
|
+
execSync7(`git pull origin ${branch}`, { cwd: this.projectDir, stdio: "pipe" });
|
|
7020
|
+
} catch {
|
|
7021
|
+
logger8.warn("Git pull failed during branch switch", { branch });
|
|
7022
|
+
}
|
|
7023
|
+
this.connection.emitEnvSwitchProgress({ step: "checkout", status: "success" });
|
|
7024
|
+
if (syncAfter !== false) {
|
|
7025
|
+
await this.handleSyncEnvironment();
|
|
7026
|
+
}
|
|
7027
|
+
this.commitWatcher.start(branch);
|
|
7028
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
7029
|
+
} catch (err) {
|
|
7030
|
+
const message = err instanceof Error ? err.message : "Branch switch failed";
|
|
7031
|
+
logger8.error("Branch switch failed", errorMeta(err));
|
|
7032
|
+
callback({ ok: false, error: message });
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
async handleSyncEnvironment(callback) {
|
|
7036
|
+
try {
|
|
7037
|
+
await this.killStartCommand();
|
|
7038
|
+
const cmd = this.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
7039
|
+
if (cmd) {
|
|
7040
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "running" });
|
|
7041
|
+
try {
|
|
7042
|
+
await runSetupCommand(cmd, this.projectDir, (stream, data) => {
|
|
7043
|
+
this.connection.emitEvent({ type: "sync_output", stream, data });
|
|
7044
|
+
(stream === "stderr" ? process.stderr : process.stdout).write(data);
|
|
7045
|
+
});
|
|
7046
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "success" });
|
|
7047
|
+
} catch (err) {
|
|
7048
|
+
const message = err instanceof Error ? err.message : "Sync command failed";
|
|
7049
|
+
this.connection.emitEnvSwitchProgress({ step: "sync", status: "error", message });
|
|
7050
|
+
logger8.error("Branch switch sync command failed", errorMeta(err));
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
this.executeStartCommand();
|
|
7054
|
+
this.connection.emitEnvSwitchProgress({ step: "startCommand", status: "success" });
|
|
7055
|
+
callback?.({ ok: true, data: this.getEnvironmentStatus() });
|
|
7056
|
+
} catch (err) {
|
|
7057
|
+
const message = err instanceof Error ? err.message : "Sync failed";
|
|
7058
|
+
logger8.error("Environment sync failed", errorMeta(err));
|
|
7059
|
+
callback?.({ ok: false, error: message });
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
handleGetEnvStatus(callback) {
|
|
7063
|
+
callback({ ok: true, data: this.getEnvironmentStatus() });
|
|
7064
|
+
}
|
|
4247
7065
|
async start() {
|
|
4248
7066
|
this.checkoutWorkspaceBranch();
|
|
4249
7067
|
await this.connection.connect();
|
|
@@ -4257,7 +7075,7 @@ var ProjectRunner = class {
|
|
|
4257
7075
|
startCommandRunning: this.startCommandRunning
|
|
4258
7076
|
});
|
|
4259
7077
|
} catch (error) {
|
|
4260
|
-
|
|
7078
|
+
logger8.error("Environment setup failed", errorMeta(error));
|
|
4261
7079
|
this.setupComplete = false;
|
|
4262
7080
|
}
|
|
4263
7081
|
this.connection.onTaskAssignment((assignment) => {
|
|
@@ -4267,17 +7085,53 @@ var ProjectRunner = class {
|
|
|
4267
7085
|
this.handleStopTask(data.taskId);
|
|
4268
7086
|
});
|
|
4269
7087
|
this.connection.onShutdown(() => {
|
|
4270
|
-
|
|
7088
|
+
logger8.info("Received shutdown signal from server");
|
|
4271
7089
|
void this.stop();
|
|
4272
7090
|
});
|
|
4273
7091
|
this.connection.onChatMessage((msg) => {
|
|
4274
|
-
|
|
4275
|
-
|
|
7092
|
+
logger8.debug("Received project chat message");
|
|
7093
|
+
const chatId = msg.chatId ?? "default";
|
|
7094
|
+
const existingSessionId = this.chatSessionIds.get(chatId);
|
|
7095
|
+
void handleProjectChatMessage(msg, this.connection, this.projectDir, existingSessionId).then(
|
|
7096
|
+
(newSessionId) => {
|
|
7097
|
+
if (newSessionId) {
|
|
7098
|
+
this.chatSessionIds.set(chatId, newSessionId);
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
);
|
|
4276
7102
|
});
|
|
7103
|
+
this.connection.onAuditRequest((request) => {
|
|
7104
|
+
logger8.debug("Received tag audit request", { requestId: request.requestId });
|
|
7105
|
+
void handleProjectAuditRequest(request, this.connection, this.projectDir);
|
|
7106
|
+
});
|
|
7107
|
+
this.connection.onSwitchBranch = (data, cb) => {
|
|
7108
|
+
void this.handleSwitchBranch(data, cb);
|
|
7109
|
+
};
|
|
7110
|
+
this.connection.onSyncEnvironment = (cb) => {
|
|
7111
|
+
void this.handleSyncEnvironment(cb);
|
|
7112
|
+
};
|
|
7113
|
+
this.connection.onGetEnvStatus = (cb) => {
|
|
7114
|
+
this.handleGetEnvStatus(cb);
|
|
7115
|
+
};
|
|
7116
|
+
this.connection.onRestartStartCommand = (cb) => {
|
|
7117
|
+
void this.restartStartCommand().then(() => cb({ ok: true })).catch(
|
|
7118
|
+
(err) => cb({ ok: false, error: err instanceof Error ? err.message : "Restart failed" })
|
|
7119
|
+
);
|
|
7120
|
+
};
|
|
7121
|
+
try {
|
|
7122
|
+
const context = await this.connection.fetchAgentContext();
|
|
7123
|
+
this.branchSwitchCommand = context?.branchSwitchCommand ?? process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
7124
|
+
} catch {
|
|
7125
|
+
this.branchSwitchCommand = process.env.CONVEYOR_BRANCH_SWITCH_COMMAND;
|
|
7126
|
+
}
|
|
4277
7127
|
this.heartbeatTimer = setInterval(() => {
|
|
4278
7128
|
this.connection.sendHeartbeat();
|
|
4279
7129
|
}, HEARTBEAT_INTERVAL_MS2);
|
|
4280
|
-
|
|
7130
|
+
const currentBranch = this.getCurrentBranch();
|
|
7131
|
+
if (currentBranch) {
|
|
7132
|
+
this.commitWatcher.start(currentBranch);
|
|
7133
|
+
}
|
|
7134
|
+
logger8.info("Connected, waiting for task assignments");
|
|
4281
7135
|
await new Promise((resolve2) => {
|
|
4282
7136
|
this.resolveLifecycle = resolve2;
|
|
4283
7137
|
process.on("SIGTERM", () => void this.stop());
|
|
@@ -4288,11 +7142,11 @@ var ProjectRunner = class {
|
|
|
4288
7142
|
const { taskId, mode } = assignment;
|
|
4289
7143
|
const shortId = taskId.slice(0, 8);
|
|
4290
7144
|
if (this.activeAgents.has(taskId)) {
|
|
4291
|
-
|
|
7145
|
+
logger8.info("Task already running, skipping", { taskId: shortId });
|
|
4292
7146
|
return;
|
|
4293
7147
|
}
|
|
4294
7148
|
if (this.activeAgents.size >= MAX_CONCURRENT) {
|
|
4295
|
-
|
|
7149
|
+
logger8.warn("Max concurrent agents reached, rejecting task", {
|
|
4296
7150
|
maxConcurrent: MAX_CONCURRENT,
|
|
4297
7151
|
taskId: shortId
|
|
4298
7152
|
});
|
|
@@ -4301,9 +7155,9 @@ var ProjectRunner = class {
|
|
|
4301
7155
|
}
|
|
4302
7156
|
try {
|
|
4303
7157
|
try {
|
|
4304
|
-
|
|
7158
|
+
execSync7("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
|
|
4305
7159
|
} catch {
|
|
4306
|
-
|
|
7160
|
+
logger8.warn("Git fetch failed", { taskId: shortId });
|
|
4307
7161
|
}
|
|
4308
7162
|
const { workDir, usesWorktree } = setupWorkDir(this.projectDir, assignment);
|
|
4309
7163
|
const child = spawnChildAgent(assignment, workDir);
|
|
@@ -4314,12 +7168,12 @@ var ProjectRunner = class {
|
|
|
4314
7168
|
usesWorktree
|
|
4315
7169
|
});
|
|
4316
7170
|
this.connection.emitTaskStarted(taskId);
|
|
4317
|
-
|
|
7171
|
+
logger8.info("Started task", { taskId: shortId, mode, workDir });
|
|
4318
7172
|
child.on("exit", (code) => {
|
|
4319
7173
|
this.activeAgents.delete(taskId);
|
|
4320
7174
|
const reason = code === 0 ? "completed" : `exited with code ${code}`;
|
|
4321
7175
|
this.connection.emitTaskStopped(taskId, reason);
|
|
4322
|
-
|
|
7176
|
+
logger8.info("Task exited", { taskId: shortId, reason });
|
|
4323
7177
|
if (code === 0 && usesWorktree) {
|
|
4324
7178
|
try {
|
|
4325
7179
|
removeWorktree(this.projectDir, taskId);
|
|
@@ -4328,7 +7182,7 @@ var ProjectRunner = class {
|
|
|
4328
7182
|
}
|
|
4329
7183
|
});
|
|
4330
7184
|
} catch (error) {
|
|
4331
|
-
|
|
7185
|
+
logger8.error("Failed to start task", {
|
|
4332
7186
|
taskId: shortId,
|
|
4333
7187
|
...errorMeta(error)
|
|
4334
7188
|
});
|
|
@@ -4342,7 +7196,7 @@ var ProjectRunner = class {
|
|
|
4342
7196
|
const agent = this.activeAgents.get(taskId);
|
|
4343
7197
|
if (!agent) return;
|
|
4344
7198
|
const shortId = taskId.slice(0, 8);
|
|
4345
|
-
|
|
7199
|
+
logger8.info("Stopping task", { taskId: shortId });
|
|
4346
7200
|
agent.process.kill("SIGTERM");
|
|
4347
7201
|
const timer = setTimeout(() => {
|
|
4348
7202
|
if (this.activeAgents.has(taskId)) {
|
|
@@ -4362,7 +7216,8 @@ var ProjectRunner = class {
|
|
|
4362
7216
|
async stop() {
|
|
4363
7217
|
if (this.stopping) return;
|
|
4364
7218
|
this.stopping = true;
|
|
4365
|
-
|
|
7219
|
+
logger8.info("Shutting down");
|
|
7220
|
+
this.commitWatcher.stop();
|
|
4366
7221
|
await this.killStartCommand();
|
|
4367
7222
|
if (this.heartbeatTimer) {
|
|
4368
7223
|
clearInterval(this.heartbeatTimer);
|
|
@@ -4388,7 +7243,7 @@ var ProjectRunner = class {
|
|
|
4388
7243
|
})
|
|
4389
7244
|
]);
|
|
4390
7245
|
this.connection.disconnect();
|
|
4391
|
-
|
|
7246
|
+
logger8.info("Shutdown complete");
|
|
4392
7247
|
if (this.resolveLifecycle) {
|
|
4393
7248
|
this.resolveLifecycle();
|
|
4394
7249
|
this.resolveLifecycle = null;
|
|
@@ -4467,6 +7322,7 @@ export {
|
|
|
4467
7322
|
ProjectConnection,
|
|
4468
7323
|
createServiceLogger,
|
|
4469
7324
|
errorMeta,
|
|
7325
|
+
injectTelemetry,
|
|
4470
7326
|
ensureWorktree,
|
|
4471
7327
|
removeWorktree,
|
|
4472
7328
|
loadConveyorConfig,
|
|
@@ -4474,4 +7330,4 @@ export {
|
|
|
4474
7330
|
ProjectRunner,
|
|
4475
7331
|
FileCache
|
|
4476
7332
|
};
|
|
4477
|
-
//# sourceMappingURL=chunk-
|
|
7333
|
+
//# sourceMappingURL=chunk-NKZSUGND.js.map
|