@rallycry/conveyor-agent 6.0.1 → 6.0.3

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