@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.
@@ -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 execSync2 } from "child_process";
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
- execSync2(`git checkout --detach origin/${branch}`, {
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
- execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
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
- execSync2(`git worktree remove "${worktreePath}" --force`, {
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 execSync3 } from "child_process";
940
+ import { execSync as execSync4 } from "child_process";
694
941
  function unshallowRepo(workspaceDir) {
695
942
  try {
696
- execSync3("git fetch --unshallow", {
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 execSync4 } from "child_process";
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
- void host.callbacks.onEvent({ type: "error", message });
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
- void host.callbacks.onEvent({ type: "thinking", message });
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
- void host.callbacks.onEvent({
911
- type: "context_compacted",
912
- trigger: systemEvent.compact_metadata.trigger,
913
- preTokens: systemEvent.compact_metadata.pre_tokens
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
- void host.callbacks.onEvent({
917
- type: "subagent_started",
918
- sdkTaskId: systemEvent.task_id,
919
- description: systemEvent.description
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
- void host.callbacks.onEvent({
923
- type: "subagent_progress",
924
- sdkTaskId: systemEvent.task_id,
925
- description: systemEvent.description,
926
- toolUses: systemEvent.usage?.tool_uses ?? 0,
927
- durationMs: systemEvent.usage?.duration_ms ?? 0
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
- void host.callbacks.onEvent({
934
- type: "tool_progress",
935
- toolName: msg.tool_name ?? "",
936
- elapsedSeconds: msg.elapsed_time_seconds ?? 0
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
- `- Once you understand the scope, use set_story_points to assign a value`,
1367
- `- Use set_task_tags to categorize the work`,
1368
- `- For icons: FIRST call list_icons to check for existing matches. Use set_task_icon if one fits.`,
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
- `- \`set_story_points\` \u2014 assign story points`,
1412
- `- \`set_task_title\` \u2014 set an accurate title`,
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 set_story_points), **title** (via set_task_title)`,
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, set_story_points, set_task_tags, set_task_icon, set_task_title`,
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 set_story_points`,
1444
- `3. **Title** \u2014 Set an accurate title via set_task_title (if the current one is vague or "Untitled")`,
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 buildSystemPrompt(mode, context, config, setupLog, agentMode) {
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 (set_story_points), tags (set_task_tags), and title (set_task_title)`,
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 set_task_icon to check for existing matches.",
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
- "set_story_points",
2505
- "Set the story point estimate for this task. Use after understanding the scope of the work.",
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
- await Promise.resolve(connection.updateTaskProperties({ title }));
2544
- return textResult(`Task title updated to: ${title}`);
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 title: ${error instanceof Error ? error.message : "Unknown error"}`
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/index.ts
2557
- function textResult(text) {
2558
- return { content: [{ type: "text", text }] };
2559
- }
2560
- function imageBlock(data, mimeType) {
2561
- return { type: "image", data, mimeType };
2562
- }
2563
- function getTaskModeTools(agentMode, connection) {
2564
- if (agentMode === "discovery" || agentMode === "auto") {
2565
- return [buildUpdateTaskTool(connection)];
2566
- }
2567
- return [];
2568
- }
2569
- function getModeTools(agentMode, connection, config, context) {
2570
- if (config.mode === "task") return getTaskModeTools(agentMode, connection);
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 set_story_points)");
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 set_task_title)");
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
- return handleDiscoveryToolAccess(toolName, input);
4525
+ result = handleDiscoveryToolAccess(toolName, input);
4526
+ break;
2740
4527
  case "building":
2741
- return handleBuildingToolAccess(toolName, input);
4528
+ result = handleBuildingToolAccess(toolName, input);
4529
+ break;
2742
4530
  case "review":
2743
- return handleReviewToolAccess(toolName, input, host.isParentTask);
4531
+ result = handleReviewToolAccess(toolName, input, host.isParentTask);
4532
+ break;
2744
4533
  case "auto":
2745
- return handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
4534
+ result = handleAutoToolAccess(toolName, input, host.hasExitedPlanMode, host.isParentTask);
4535
+ break;
2746
4536
  default:
2747
- return { behavior: "allow", updatedInput: input };
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 logger2 = createServiceLogger("QueryExecutor");
2754
- var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
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 ? "acceptEdits" : "bypassPermissions",
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
- logger2.warn("Claude Code stderr", { data: data.trimEnd() });
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
- if (!(error instanceof Error)) return false;
3007
- return API_ERROR_PATTERN2.test(error.message) || IMAGE_ERROR_PATTERN2.test(error.message);
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 error instanceof Error && IMAGE_ERROR_PATTERN2.test(error.message);
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 { retriable, resultSummary, modeRestart, rateLimitResetsAt, staleSession } = await processEvents(agentQuery, context, host);
3047
- if (modeRestart || host.isStopped()) return;
3048
- if (rateLimitResetsAt) {
3049
- handleRateLimitPause(host, rateLimitResetsAt);
3050
- return;
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 logger3 = createServiceLogger("AgentRunner");
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
- const branch = this.taskContext.githubBranch;
3595
- execSync4(`git fetch origin ${branch} && git checkout ${branch}`, {
3596
- cwd: this.config.workspaceDir,
3597
- stdio: "ignore"
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
- logger3.info("Effective agent settings", {
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
- this.clearIdleTimers();
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.postChatMessage(
3773
- `Agent idle for ${IDLE_TIMEOUT_MS / 6e4} minutes with no new messages \u2014 shutting down.`
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
- if (this.inputResolver) {
3878
- this.inputResolver(null);
3879
- this.inputResolver = null;
3880
- }
3881
- }
3882
- };
3883
-
3884
- // src/runner/project-runner.ts
3885
- import { fork } from "child_process";
3886
- import { execSync as execSync5 } from "child_process";
3887
- import * as path from "path";
3888
- import { fileURLToPath } from "url";
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
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
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
- logger4.debug("Tool use", { tool: block.name });
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
- logger4.warn("Could not fetch agent context, using defaults");
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
- logger4.warn("Could not fetch chat history, proceeding without it");
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
- maxTurns: settings.maxTurns ?? 15,
3978
- maxBudgetUsd: settings.maxBudgetUsd ?? 5,
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
- const events = query2({ prompt, options });
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("busy");
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
- logger4.error("Failed to handle message", errorMeta(error));
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 logger5 = createServiceLogger("ProjectRunner");
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
- try {
4065
- execSync5(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
4066
- } catch {
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
- execSync5(`git checkout -b ${branch}`, { cwd: workDir, stdio: "ignore" });
6682
+ execSync7(`git checkout ${branch}`, { cwd: workDir, stdio: "ignore" });
4069
6683
  } catch {
4070
- logger5.warn("Could not checkout branch", { taskId: shortId, branch });
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
- logger5.info(line, { taskId: shortId });
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
- logger5.error(line, { taskId: shortId });
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
- execSync5(`git fetch origin ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
4144
- execSync5(`git checkout ${workspaceBranch}`, { cwd: this.projectDir, stdio: "pipe" });
4145
- logger5.info("Checked out workspace branch", { workspaceBranch });
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
- logger5.warn("Failed to checkout workspace branch, continuing on current branch", {
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
- logger5.info("Running setup command", { command: cmd });
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
- logger5.info("Setup command completed");
6824
+ logger8.info("Setup command completed");
4163
6825
  } catch (error) {
4164
- logger5.error("Setup command failed", errorMeta(error));
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
- logger5.info("Running start command", { command: cmd });
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
- logger5.info("Start command exited", { code, signal });
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
- logger5.error("Start command error", errorMeta(err));
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
- logger5.info("Killing start command");
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
- logger5.error("Environment setup failed", errorMeta(error));
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
- logger5.info("Received shutdown signal from server");
7088
+ logger8.info("Received shutdown signal from server");
4271
7089
  void this.stop();
4272
7090
  });
4273
7091
  this.connection.onChatMessage((msg) => {
4274
- logger5.debug("Received project chat message");
4275
- void handleProjectChatMessage(msg, this.connection, this.projectDir);
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
- logger5.info("Connected, waiting for task assignments");
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
- logger5.info("Task already running, skipping", { taskId: shortId });
7145
+ logger8.info("Task already running, skipping", { taskId: shortId });
4292
7146
  return;
4293
7147
  }
4294
7148
  if (this.activeAgents.size >= MAX_CONCURRENT) {
4295
- logger5.warn("Max concurrent agents reached, rejecting task", {
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
- execSync5("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
7158
+ execSync7("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
4305
7159
  } catch {
4306
- logger5.warn("Git fetch failed", { taskId: shortId });
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
- logger5.info("Started task", { taskId: shortId, mode, workDir });
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
- logger5.info("Task exited", { taskId: shortId, reason });
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
- logger5.error("Failed to start task", {
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
- logger5.info("Stopping task", { taskId: shortId });
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
- logger5.info("Shutting down");
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
- logger5.info("Shutdown complete");
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-HYWZJYPW.js.map
7333
+ //# sourceMappingURL=chunk-NKZSUGND.js.map