@tiflis-io/tiflis-code-workstation 0.3.29 → 0.3.31

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.
Files changed (2) hide show
  1. package/dist/main.js +3252 -759
  2. package/package.json +3 -1
package/dist/main.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-JSN52PLR.js";
6
6
 
7
7
  // src/main.ts
8
- import { randomUUID as randomUUID5 } from "crypto";
8
+ import { randomUUID as randomUUID6 } from "crypto";
9
9
 
10
10
  // src/app.ts
11
11
  import Fastify from "fastify";
@@ -274,6 +274,8 @@ var SESSION_CONFIG = {
274
274
  MAX_AGENT_SESSIONS: 10,
275
275
  /** Maximum number of concurrent terminal sessions */
276
276
  MAX_TERMINAL_SESSIONS: 5,
277
+ /** Maximum number of concurrent backlog sessions */
278
+ MAX_BACKLOG_SESSIONS: 10,
277
279
  /** Default terminal columns */
278
280
  DEFAULT_TERMINAL_COLS: 80,
279
281
  /** Default terminal rows */
@@ -1116,6 +1118,67 @@ var SubscriptionRepository = class {
1116
1118
  }
1117
1119
  };
1118
1120
 
1121
+ // src/infrastructure/persistence/repositories/session-repository.ts
1122
+ import { eq as eq3 } from "drizzle-orm";
1123
+ var SessionRepository = class {
1124
+ /**
1125
+ * Creates a new session record or updates if exists.
1126
+ */
1127
+ create(params) {
1128
+ const db2 = getDatabase();
1129
+ const newSession = {
1130
+ id: params.id,
1131
+ type: params.type,
1132
+ workspace: params.workspace,
1133
+ project: params.project,
1134
+ worktree: params.worktree,
1135
+ workingDir: params.workingDir,
1136
+ status: "active",
1137
+ createdAt: /* @__PURE__ */ new Date()
1138
+ };
1139
+ db2.insert(sessions).values(newSession).onConflictDoUpdate({
1140
+ target: sessions.id,
1141
+ set: {
1142
+ status: "active",
1143
+ terminatedAt: null
1144
+ }
1145
+ }).run();
1146
+ return { ...newSession, terminatedAt: null, createdAt: newSession.createdAt };
1147
+ }
1148
+ /**
1149
+ * Gets a session by ID.
1150
+ */
1151
+ getById(sessionId) {
1152
+ const db2 = getDatabase();
1153
+ return db2.select().from(sessions).where(eq3(sessions.id, sessionId)).get();
1154
+ }
1155
+ /**
1156
+ * Gets all active sessions.
1157
+ */
1158
+ getActive() {
1159
+ const db2 = getDatabase();
1160
+ return db2.select().from(sessions).where(eq3(sessions.status, "active")).all();
1161
+ }
1162
+ /**
1163
+ * Marks a session as terminated.
1164
+ */
1165
+ terminate(sessionId) {
1166
+ const db2 = getDatabase();
1167
+ db2.update(sessions).set({
1168
+ status: "terminated",
1169
+ terminatedAt: /* @__PURE__ */ new Date()
1170
+ }).where(eq3(sessions.id, sessionId)).run();
1171
+ }
1172
+ /**
1173
+ * Deletes old terminated sessions (for cleanup).
1174
+ */
1175
+ deleteOldTerminated(_olderThan) {
1176
+ const db2 = getDatabase();
1177
+ const result = db2.delete(sessions).where(eq3(sessions.status, "terminated")).run();
1178
+ return result.changes;
1179
+ }
1180
+ };
1181
+
1119
1182
  // src/infrastructure/websocket/tunnel-client.ts
1120
1183
  import WebSocket from "ws";
1121
1184
 
@@ -1171,7 +1234,7 @@ var ListSessionsSchema = z2.object({
1171
1234
  // Injected by tunnel for tunnel connections
1172
1235
  });
1173
1236
  var CreateSessionPayloadSchema = z2.object({
1174
- session_type: z2.enum(["cursor", "claude", "opencode", "terminal"]),
1237
+ session_type: z2.enum(["cursor", "claude", "opencode", "terminal", "backlog-agent"]),
1175
1238
  agent_name: z2.string().optional(),
1176
1239
  // Custom alias name (e.g., 'zai' for claude with custom config)
1177
1240
  workspace: z2.string().min(1, "Workspace is required"),
@@ -1398,7 +1461,7 @@ function getMessageType(data) {
1398
1461
  }
1399
1462
 
1400
1463
  // src/infrastructure/websocket/tunnel-client.ts
1401
- var TunnelClient = class {
1464
+ var TunnelClient = class _TunnelClient {
1402
1465
  config;
1403
1466
  callbacks;
1404
1467
  logger;
@@ -1412,6 +1475,8 @@ var TunnelClient = class {
1412
1475
  reconnectTimeout = null;
1413
1476
  registrationTimeout = null;
1414
1477
  messageBuffer = [];
1478
+ static MAX_SEND_RETRIES = 3;
1479
+ static SEND_RETRY_DELAY_MS = 100;
1415
1480
  constructor(config2, callbacks) {
1416
1481
  this.config = config2;
1417
1482
  this.callbacks = callbacks;
@@ -1514,8 +1579,9 @@ var TunnelClient = class {
1514
1579
  this.messageBuffer = [];
1515
1580
  }
1516
1581
  /**
1517
- * Sends a message to the tunnel (for forwarding to clients).
1518
- * Detects send failures and triggers reconnection.
1582
+ * Sends a message to the tunnel (for forwarding to clients) with retry logic (FIX #3).
1583
+ * Buffers during reconnection, retries on transient failures with exponential backoff.
1584
+ * Returns true if send was successful or queued for retry.
1519
1585
  */
1520
1586
  send(message) {
1521
1587
  if (this.state !== "registered" || !this.ws) {
@@ -1528,18 +1594,55 @@ var TunnelClient = class {
1528
1594
  if (this.ws.readyState !== WebSocket.OPEN) {
1529
1595
  this.logger.warn(
1530
1596
  { readyState: this.ws.readyState },
1531
- "Socket not open, triggering reconnection"
1597
+ "Socket not open, buffering message and triggering reconnection"
1532
1598
  );
1599
+ this.messageBuffer.push(message);
1533
1600
  this.handleSendFailure();
1601
+ return true;
1602
+ }
1603
+ return this.sendWithRetry(message, 0);
1604
+ }
1605
+ /**
1606
+ * FIX #3: Send with exponential backoff retry.
1607
+ * On failure, schedules retry with exponential delay (100ms, 200ms, 400ms).
1608
+ */
1609
+ sendWithRetry(message, attempt) {
1610
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1611
+ if (attempt === 0) {
1612
+ this.messageBuffer.push(message);
1613
+ }
1534
1614
  return false;
1535
1615
  }
1536
- this.ws.send(message, (error) => {
1537
- if (error) {
1538
- this.logger.error({ error }, "Send failed, triggering reconnection");
1539
- this.handleSendFailure();
1616
+ try {
1617
+ this.ws.send(message, (error) => {
1618
+ if (error) {
1619
+ if (attempt < _TunnelClient.MAX_SEND_RETRIES) {
1620
+ const delay = _TunnelClient.SEND_RETRY_DELAY_MS * Math.pow(2, attempt);
1621
+ this.logger.debug(
1622
+ { attempt: attempt + 1, delay, errorType: error.name },
1623
+ "Send failed, scheduling retry with exponential backoff"
1624
+ );
1625
+ setTimeout(() => {
1626
+ this.sendWithRetry(message, attempt + 1);
1627
+ }, delay);
1628
+ } else {
1629
+ this.logger.error(
1630
+ { error: error.message, attempts: attempt + 1 },
1631
+ "Send failed after all retries, buffering message"
1632
+ );
1633
+ this.messageBuffer.push(message);
1634
+ this.handleSendFailure();
1635
+ }
1636
+ }
1637
+ });
1638
+ return true;
1639
+ } catch (error) {
1640
+ this.logger.error({ error: error.message }, "Unexpected error in send");
1641
+ if (attempt === 0) {
1642
+ this.messageBuffer.push(message);
1540
1643
  }
1541
- });
1542
- return true;
1644
+ return false;
1645
+ }
1543
1646
  }
1544
1647
  /**
1545
1648
  * Handles send failure by disconnecting and scheduling reconnection.
@@ -2457,6 +2560,8 @@ var FileSystemWorkspaceDiscovery = class {
2457
2560
  // src/infrastructure/terminal/pty-manager.ts
2458
2561
  import * as pty from "node-pty";
2459
2562
  import { nanoid } from "nanoid";
2563
+ import { existsSync as existsSync3 } from "fs";
2564
+ import { execSync as execSync3 } from "child_process";
2460
2565
 
2461
2566
  // src/domain/entities/session.ts
2462
2567
  var Session = class {
@@ -2779,15 +2884,26 @@ function isTerminalSession(session) {
2779
2884
 
2780
2885
  // src/infrastructure/shell/shell-env.ts
2781
2886
  import { execSync as execSync2 } from "child_process";
2887
+ import { existsSync as existsSync2 } from "fs";
2782
2888
  var cachedShellEnv = null;
2783
- function getDefaultShell() {
2784
- return process.env.SHELL ?? "/bin/bash";
2889
+ function resolveShell() {
2890
+ const primaryShell = process.env.SHELL;
2891
+ if (primaryShell && existsSync2(primaryShell)) {
2892
+ return primaryShell;
2893
+ }
2894
+ const fallbackShells = ["/bin/zsh", "/bin/bash", "/bin/sh"];
2895
+ for (const shell of fallbackShells) {
2896
+ if (existsSync2(shell)) {
2897
+ return shell;
2898
+ }
2899
+ }
2900
+ return "/bin/bash";
2785
2901
  }
2786
2902
  function getShellEnv() {
2787
2903
  if (cachedShellEnv) {
2788
2904
  return cachedShellEnv;
2789
2905
  }
2790
- const shell = getDefaultShell();
2906
+ const shell = resolveShell();
2791
2907
  const isZsh = shell.includes("zsh");
2792
2908
  const isBash = shell.includes("bash");
2793
2909
  let envOutput;
@@ -2838,17 +2954,49 @@ function getShellEnv() {
2838
2954
  return cachedShellEnv;
2839
2955
  } catch (error) {
2840
2956
  console.warn(
2841
- "Failed to retrieve shell environment, using process.env:",
2842
- error instanceof Error ? error.message : String(error)
2957
+ "Failed to retrieve shell environment from shell",
2958
+ {
2959
+ shell,
2960
+ error: error instanceof Error ? error.message : String(error)
2961
+ }
2843
2962
  );
2963
+ console.warn("Using process.env as fallback");
2844
2964
  cachedShellEnv = { ...process.env };
2845
2965
  return cachedShellEnv;
2846
2966
  }
2847
2967
  }
2848
2968
 
2849
2969
  // src/infrastructure/terminal/pty-manager.ts
2850
- function getDefaultShell2() {
2851
- return process.env.SHELL ?? "/bin/bash";
2970
+ function resolveShell2(logger) {
2971
+ const primaryShell = process.env.SHELL;
2972
+ if (primaryShell && existsSync3(primaryShell)) {
2973
+ logger.debug({ shell: primaryShell }, "Using primary shell from SHELL environment");
2974
+ return primaryShell;
2975
+ }
2976
+ if (primaryShell) {
2977
+ logger.warn(
2978
+ { primaryShell, exists: false },
2979
+ "Primary shell from SHELL env does not exist, trying alternatives"
2980
+ );
2981
+ }
2982
+ const fallbackShells = ["/bin/zsh", "/bin/bash", "/bin/sh"];
2983
+ for (const shell of fallbackShells) {
2984
+ if (existsSync3(shell)) {
2985
+ logger.debug({ shell }, "Using fallback shell");
2986
+ return shell;
2987
+ }
2988
+ }
2989
+ try {
2990
+ const whichSh = execSync3("which sh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2991
+ if (whichSh && existsSync3(whichSh)) {
2992
+ logger.debug({ shell: whichSh }, "Found sh in PATH as last resort");
2993
+ return whichSh;
2994
+ }
2995
+ } catch {
2996
+ logger.warn("Failed to find sh in PATH");
2997
+ }
2998
+ logger.error("No usable shell found, defaulting to /bin/bash");
2999
+ return "/bin/bash";
2852
3000
  }
2853
3001
  var PtyManager = class {
2854
3002
  logger;
@@ -2860,40 +3008,59 @@ var PtyManager = class {
2860
3008
  }
2861
3009
  /**
2862
3010
  * Creates a new terminal session.
3011
+ * Resolves shell with fallbacks and includes detailed error logging for debugging M1 issues.
2863
3012
  */
2864
3013
  create(workingDir, cols, rows) {
2865
3014
  const sessionId = new SessionId(nanoid(12));
2866
- const shell = getDefaultShell2();
3015
+ const shell = resolveShell2(this.logger);
2867
3016
  this.logger.debug(
2868
3017
  { sessionId: sessionId.value, workingDir, shell, cols, rows },
2869
3018
  "Creating terminal session"
2870
3019
  );
2871
- const shellEnv = getShellEnv();
2872
- const ptyProcess = pty.spawn(shell, [], {
2873
- name: "xterm-256color",
2874
- cols,
2875
- rows,
2876
- cwd: workingDir,
2877
- env: {
2878
- ...shellEnv,
2879
- TERM: "xterm-256color",
2880
- // Disable zsh partial line marker (inverse % sign on startup)
2881
- PROMPT_EOL_MARK: ""
2882
- }
2883
- });
2884
- const session = new TerminalSession({
2885
- id: sessionId,
2886
- pty: ptyProcess,
2887
- cols,
2888
- rows,
2889
- workingDir,
2890
- bufferSize: this.bufferSize
2891
- });
2892
- this.logger.info(
2893
- { sessionId: sessionId.value, pid: ptyProcess.pid },
2894
- "Terminal session created"
2895
- );
2896
- return Promise.resolve(session);
3020
+ try {
3021
+ const shellEnv = getShellEnv();
3022
+ const ptyProcess = pty.spawn(shell, [], {
3023
+ name: "xterm-256color",
3024
+ cols,
3025
+ rows,
3026
+ cwd: workingDir,
3027
+ env: {
3028
+ ...shellEnv,
3029
+ TERM: "xterm-256color",
3030
+ // Disable zsh partial line marker (inverse % sign on startup)
3031
+ PROMPT_EOL_MARK: ""
3032
+ }
3033
+ });
3034
+ const session = new TerminalSession({
3035
+ id: sessionId,
3036
+ pty: ptyProcess,
3037
+ cols,
3038
+ rows,
3039
+ workingDir,
3040
+ bufferSize: this.bufferSize
3041
+ });
3042
+ this.logger.info(
3043
+ { sessionId: sessionId.value, pid: ptyProcess.pid, shell },
3044
+ "Terminal session created"
3045
+ );
3046
+ return Promise.resolve(session);
3047
+ } catch (error) {
3048
+ const errorMsg = error instanceof Error ? error.message : String(error);
3049
+ this.logger.error(
3050
+ {
3051
+ sessionId: sessionId.value,
3052
+ shell,
3053
+ workingDir,
3054
+ cols,
3055
+ rows,
3056
+ error: errorMsg,
3057
+ stack: error instanceof Error ? error.stack : void 0
3058
+ },
3059
+ "Failed to create terminal session"
3060
+ );
3061
+ const message = `Terminal creation failed: ${errorMsg}. Shell: ${shell}, Working dir: ${workingDir}`;
3062
+ throw new Error(message);
3063
+ }
2897
3064
  }
2898
3065
  /**
2899
3066
  * Writes data to a terminal session.
@@ -3055,39 +3222,32 @@ var HeadlessAgentExecutor = class extends EventEmitter {
3055
3222
  cwd: this.workingDir,
3056
3223
  env: {
3057
3224
  ...shellEnv,
3058
- // Apply alias env vars (e.g., CLAUDE_CONFIG_DIR)
3059
3225
  ...aliasEnvVars,
3060
- // Ensure proper terminal environment
3061
3226
  TERM: "xterm-256color",
3062
- // Disable interactive prompts
3063
3227
  CI: "true"
3064
3228
  },
3065
3229
  stdio: ["ignore", "pipe", "pipe"],
3066
- // stdin ignored, stdout/stderr piped
3067
- detached: true,
3068
- // Create new process group for clean termination
3069
- // Ensure child doesn't interfere with parent's signal handling
3070
- // @ts-expect-error - Node.js 16+ option
3071
- ignoreParentSignals: true
3230
+ detached: true
3072
3231
  });
3073
- this.subprocess.stdout?.on("data", (data) => {
3232
+ const proc = this.subprocess;
3233
+ proc.stdout?.on("data", (data) => {
3074
3234
  if (this.isKilled) return;
3075
3235
  const text2 = data.toString();
3076
3236
  this.emit("stdout", text2);
3077
3237
  });
3078
- this.subprocess.stderr?.on("data", (data) => {
3238
+ proc.stderr?.on("data", (data) => {
3079
3239
  if (this.isKilled) return;
3080
3240
  const text2 = data.toString();
3081
3241
  this.emit("stderr", text2);
3082
3242
  });
3083
- this.subprocess.on("exit", (code) => {
3243
+ proc.on("exit", (code) => {
3084
3244
  this.clearExecutionTimeout();
3085
3245
  if (!this.isKilled) {
3086
3246
  this.emit("exit", code);
3087
3247
  }
3088
3248
  this.subprocess = null;
3089
3249
  });
3090
- this.subprocess.on("error", (error) => {
3250
+ proc.on("error", (error) => {
3091
3251
  this.clearExecutionTimeout();
3092
3252
  if (!this.isKilled) {
3093
3253
  this.emit("error", error);
@@ -4638,11 +4798,15 @@ var CreateSessionUseCase = class {
4638
4798
  * Creates a new session.
4639
4799
  */
4640
4800
  async execute(params) {
4641
- const { requestId, sessionType, agentName, workspace, project, worktree } = params;
4642
- this.logger.info({ requestId, sessionType, agentName, workspace, project, worktree }, "CreateSession execute called");
4801
+ const { requestId, sessionType, agentName, workspace, project, worktree: rawWorktree, backlogAgent, backlogId } = params;
4802
+ let worktree = rawWorktree;
4803
+ if (worktree && (worktree.toLowerCase() === "main" || worktree.toLowerCase() === "master")) {
4804
+ worktree = void 0;
4805
+ }
4806
+ this.logger.info({ requestId, sessionType, agentName, workspace, project, worktree, backlogAgent, backlogId }, "CreateSession execute called");
4643
4807
  let workingDir;
4644
4808
  let workspacePath = null;
4645
- if (sessionType === "terminal") {
4809
+ if (sessionType === "terminal" || sessionType === "backlog-agent") {
4646
4810
  const hasRealWorkspace = workspace && workspace !== "home";
4647
4811
  const hasRealProject = project && project !== "default";
4648
4812
  this.logger.info({ hasRealWorkspace, hasRealProject, workspace, project }, "Terminal session path logic");
@@ -4754,6 +4918,11 @@ var CreateSessionUseCase = class {
4754
4918
  if (count2 >= SESSION_CONFIG.MAX_TERMINAL_SESSIONS) {
4755
4919
  throw new SessionLimitReachedError("terminal", SESSION_CONFIG.MAX_TERMINAL_SESSIONS);
4756
4920
  }
4921
+ } else if (sessionType === "backlog-agent") {
4922
+ const count2 = this.deps.sessionManager.countByType("backlog-agent");
4923
+ if (count2 >= SESSION_CONFIG.MAX_BACKLOG_SESSIONS) {
4924
+ throw new SessionLimitReachedError("backlog", SESSION_CONFIG.MAX_BACKLOG_SESSIONS);
4925
+ }
4757
4926
  } else if (sessionType !== "supervisor") {
4758
4927
  const agentCount = this.deps.sessionManager.countByType("cursor") + this.deps.sessionManager.countByType("claude") + this.deps.sessionManager.countByType("opencode");
4759
4928
  if (agentCount >= SESSION_CONFIG.MAX_AGENT_SESSIONS) {
@@ -5067,13 +5236,58 @@ var SubscriptionService = class {
5067
5236
  };
5068
5237
 
5069
5238
  // src/application/services/message-broadcaster-impl.ts
5070
- var MessageBroadcasterImpl = class {
5239
+ var MessageBroadcasterImpl = class _MessageBroadcasterImpl {
5071
5240
  deps;
5072
5241
  logger;
5242
+ /** FIX #4: Buffer messages for devices during auth flow */
5243
+ authBuffers = /* @__PURE__ */ new Map();
5244
+ static AUTH_BUFFER_TTL_MS = 5e3;
5245
+ // 5 second buffer TTL
5073
5246
  constructor(deps) {
5074
5247
  this.deps = deps;
5075
5248
  this.logger = deps.logger.child({ service: "broadcaster" });
5076
5249
  }
5250
+ /**
5251
+ * FIX #4: Buffers a message for a device during auth flow.
5252
+ * Called when subscribing clients are not yet authenticated.
5253
+ * Messages are buffered with TTL and flushed when auth completes.
5254
+ */
5255
+ bufferMessageForAuth(deviceId, sessionId, message) {
5256
+ if (!this.authBuffers.has(deviceId)) {
5257
+ this.authBuffers.set(deviceId, []);
5258
+ }
5259
+ const buffer = this.authBuffers.get(deviceId) ?? [];
5260
+ buffer.push({ sessionId, message, timestamp: Date.now() });
5261
+ const now = Date.now();
5262
+ const filtered = buffer.filter(
5263
+ (item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
5264
+ );
5265
+ this.authBuffers.set(deviceId, filtered);
5266
+ this.logger.debug(
5267
+ { deviceId, bufferSize: filtered.length },
5268
+ "Buffered message for authenticating device"
5269
+ );
5270
+ }
5271
+ /**
5272
+ * FIX #4: Flushes buffered messages for a device after authentication.
5273
+ * Called from main.ts after subscription restore completes.
5274
+ * Returns buffered messages so they can be sent to subscribed sessions.
5275
+ */
5276
+ flushAuthBuffer(deviceId) {
5277
+ const buffer = this.authBuffers.get(deviceId) ?? [];
5278
+ this.authBuffers.delete(deviceId);
5279
+ const now = Date.now();
5280
+ const validMessages = buffer.filter(
5281
+ (item) => now - item.timestamp < _MessageBroadcasterImpl.AUTH_BUFFER_TTL_MS
5282
+ );
5283
+ if (validMessages.length > 0) {
5284
+ this.logger.info(
5285
+ { deviceId, messageCount: validMessages.length },
5286
+ "Flushing auth buffer - delivering buffered messages"
5287
+ );
5288
+ }
5289
+ return validMessages;
5290
+ }
5077
5291
  /**
5078
5292
  * Broadcasts a message to all connected clients via tunnel.
5079
5293
  * The tunnel handles client filtering and delivery.
@@ -5115,6 +5329,8 @@ var MessageBroadcasterImpl = class {
5115
5329
  * Broadcasts a message to all clients subscribed to a session (by session ID string).
5116
5330
  * Sends targeted messages to each subscriber in parallel with timeout.
5117
5331
  * Prevents slow clients from blocking others.
5332
+ *
5333
+ * FIX #4: Also buffers messages for unauthenticated subscribers during auth flow.
5118
5334
  */
5119
5335
  async broadcastToSubscribers(sessionId, message) {
5120
5336
  const session = new SessionId(sessionId);
@@ -5123,9 +5339,36 @@ var MessageBroadcasterImpl = class {
5123
5339
  (c) => c.isAuthenticated
5124
5340
  );
5125
5341
  if (authenticatedSubscribers.length === 0) {
5342
+ const unauthenticatedCount = subscribers.length - authenticatedSubscribers.length;
5343
+ if (unauthenticatedCount > 0) {
5344
+ this.logger.debug(
5345
+ { sessionId, unauthenticatedCount },
5346
+ "Subscribers are authenticating - buffering message for delivery after auth"
5347
+ );
5348
+ for (const client of subscribers) {
5349
+ if (!client.isAuthenticated) {
5350
+ this.bufferMessageForAuth(client.deviceId.value, sessionId, message);
5351
+ }
5352
+ }
5353
+ } else {
5354
+ let messageType = "unknown";
5355
+ try {
5356
+ const parsed = JSON.parse(message);
5357
+ messageType = parsed.type ?? "unknown";
5358
+ } catch {
5359
+ }
5360
+ this.logger.debug(
5361
+ { sessionId, messageType },
5362
+ "No subscribers found for session"
5363
+ );
5364
+ }
5126
5365
  return;
5127
5366
  }
5128
5367
  const SEND_TIMEOUT_MS = 2e3;
5368
+ this.logger.debug(
5369
+ { sessionId, subscriberCount: authenticatedSubscribers.length },
5370
+ "Broadcasting message to subscribers"
5371
+ );
5129
5372
  const sendPromises = authenticatedSubscribers.map(async (client) => {
5130
5373
  try {
5131
5374
  await Promise.race([
@@ -5162,12 +5405,23 @@ var MessageBroadcasterImpl = class {
5162
5405
  );
5163
5406
  }
5164
5407
  });
5165
- await Promise.allSettled(sendPromises);
5408
+ const results = await Promise.allSettled(sendPromises);
5409
+ const failedCount = results.filter((r) => r.status === "rejected").length;
5410
+ if (failedCount > 0) {
5411
+ this.logger.warn(
5412
+ { sessionId, totalSubscribers: authenticatedSubscribers.length, failedCount },
5413
+ "Some subscribers failed to receive message"
5414
+ );
5415
+ }
5416
+ }
5417
+ getAuthenticatedDeviceIds() {
5418
+ const clients = this.deps.clientRegistry.getAll();
5419
+ return clients.filter((c) => c.isAuthenticated).map((c) => c.deviceId.value);
5166
5420
  }
5167
5421
  };
5168
5422
 
5169
5423
  // src/infrastructure/persistence/repositories/message-repository.ts
5170
- import { eq as eq3, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
5424
+ import { eq as eq4, desc, gt, lt, and as and2, max, count } from "drizzle-orm";
5171
5425
  import { nanoid as nanoid2 } from "nanoid";
5172
5426
  var MessageRepository = class {
5173
5427
  /**
@@ -5175,7 +5429,7 @@ var MessageRepository = class {
5175
5429
  */
5176
5430
  create(params) {
5177
5431
  const db2 = getDatabase();
5178
- const result = db2.select({ maxSeq: max(messages.sequence) }).from(messages).where(eq3(messages.sessionId, params.sessionId)).get();
5432
+ const result = db2.select({ maxSeq: max(messages.sequence) }).from(messages).where(eq4(messages.sessionId, params.sessionId)).get();
5179
5433
  const nextSequence = (result?.maxSeq ?? 0) + 1;
5180
5434
  const newMessage = {
5181
5435
  id: params.messageId ?? nanoid2(16),
@@ -5196,17 +5450,17 @@ var MessageRepository = class {
5196
5450
  }
5197
5451
  getBySession(sessionId, limit = 100) {
5198
5452
  const db2 = getDatabase();
5199
- return db2.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
5453
+ return db2.select().from(messages).where(eq4(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all();
5200
5454
  }
5201
5455
  getBySessionPaginated(sessionId, options = {}) {
5202
5456
  const db2 = getDatabase();
5203
5457
  const limit = Math.min(options.limit ?? 20, 50);
5204
- const totalResult = db2.select({ count: count() }).from(messages).where(eq3(messages.sessionId, sessionId)).get();
5458
+ const totalResult = db2.select({ count: count() }).from(messages).where(eq4(messages.sessionId, sessionId)).get();
5205
5459
  const totalCount = totalResult?.count ?? 0;
5206
5460
  const whereClause = options.beforeSequence ? and2(
5207
- eq3(messages.sessionId, sessionId),
5461
+ eq4(messages.sessionId, sessionId),
5208
5462
  lt(messages.sequence, options.beforeSequence)
5209
- ) : eq3(messages.sessionId, sessionId);
5463
+ ) : eq4(messages.sessionId, sessionId);
5210
5464
  const rows = db2.select().from(messages).where(whereClause).orderBy(desc(messages.sequence)).limit(limit + 1).all();
5211
5465
  const hasMore = rows.length > limit;
5212
5466
  const resultMessages = hasMore ? rows.slice(0, limit) : rows;
@@ -5214,117 +5468,56 @@ var MessageRepository = class {
5214
5468
  }
5215
5469
  getAfterTimestamp(sessionId, timestamp, limit = 100) {
5216
5470
  const db2 = getDatabase();
5217
- return db2.select().from(messages).where(and2(eq3(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
5471
+ return db2.select().from(messages).where(and2(eq4(messages.sessionId, sessionId), gt(messages.createdAt, timestamp))).orderBy(messages.createdAt).limit(limit).all();
5218
5472
  }
5219
5473
  /**
5220
5474
  * Updates message completion status.
5221
5475
  */
5222
5476
  markComplete(messageId) {
5223
5477
  const db2 = getDatabase();
5224
- db2.update(messages).set({ isComplete: true }).where(eq3(messages.id, messageId)).run();
5478
+ db2.update(messages).set({ isComplete: true }).where(eq4(messages.id, messageId)).run();
5225
5479
  }
5226
5480
  /**
5227
5481
  * Updates message audio output path.
5228
5482
  */
5229
5483
  setAudioOutput(messageId, audioPath) {
5230
5484
  const db2 = getDatabase();
5231
- db2.update(messages).set({ audioOutputPath: audioPath }).where(eq3(messages.id, messageId)).run();
5485
+ db2.update(messages).set({ audioOutputPath: audioPath }).where(eq4(messages.id, messageId)).run();
5232
5486
  }
5233
5487
  /**
5234
5488
  * Updates message content blocks (JSON string).
5235
5489
  */
5236
5490
  updateContentBlocks(messageId, contentBlocks) {
5237
5491
  const db2 = getDatabase();
5238
- db2.update(messages).set({ contentBlocks }).where(eq3(messages.id, messageId)).run();
5492
+ db2.update(messages).set({ contentBlocks }).where(eq4(messages.id, messageId)).run();
5239
5493
  }
5240
5494
  /**
5241
5495
  * Gets the last message for a session.
5242
5496
  */
5243
5497
  getLastBySession(sessionId) {
5244
5498
  const db2 = getDatabase();
5245
- return db2.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(1).get();
5499
+ return db2.select().from(messages).where(eq4(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(1).get();
5246
5500
  }
5247
5501
  /**
5248
5502
  * Gets a message by ID.
5249
5503
  */
5250
5504
  getById(messageId) {
5251
5505
  const db2 = getDatabase();
5252
- return db2.select().from(messages).where(eq3(messages.id, messageId)).get();
5506
+ return db2.select().from(messages).where(eq4(messages.id, messageId)).get();
5253
5507
  }
5254
5508
  /**
5255
5509
  * Deletes all messages for a session.
5256
5510
  */
5257
5511
  deleteBySession(sessionId) {
5258
5512
  const db2 = getDatabase();
5259
- db2.delete(messages).where(eq3(messages.sessionId, sessionId)).run();
5260
- }
5261
- };
5262
-
5263
- // src/infrastructure/persistence/repositories/session-repository.ts
5264
- import { eq as eq4 } from "drizzle-orm";
5265
- var SessionRepository = class {
5266
- /**
5267
- * Creates a new session record or updates if exists.
5268
- */
5269
- create(params) {
5270
- const db2 = getDatabase();
5271
- const newSession = {
5272
- id: params.id,
5273
- type: params.type,
5274
- workspace: params.workspace,
5275
- project: params.project,
5276
- worktree: params.worktree,
5277
- workingDir: params.workingDir,
5278
- status: "active",
5279
- createdAt: /* @__PURE__ */ new Date()
5280
- };
5281
- db2.insert(sessions).values(newSession).onConflictDoUpdate({
5282
- target: sessions.id,
5283
- set: {
5284
- status: "active",
5285
- terminatedAt: null
5286
- }
5287
- }).run();
5288
- return { ...newSession, terminatedAt: null, createdAt: newSession.createdAt };
5289
- }
5290
- /**
5291
- * Gets a session by ID.
5292
- */
5293
- getById(sessionId) {
5294
- const db2 = getDatabase();
5295
- return db2.select().from(sessions).where(eq4(sessions.id, sessionId)).get();
5296
- }
5297
- /**
5298
- * Gets all active sessions.
5299
- */
5300
- getActive() {
5301
- const db2 = getDatabase();
5302
- return db2.select().from(sessions).where(eq4(sessions.status, "active")).all();
5303
- }
5304
- /**
5305
- * Marks a session as terminated.
5306
- */
5307
- terminate(sessionId) {
5308
- const db2 = getDatabase();
5309
- db2.update(sessions).set({
5310
- status: "terminated",
5311
- terminatedAt: /* @__PURE__ */ new Date()
5312
- }).where(eq4(sessions.id, sessionId)).run();
5313
- }
5314
- /**
5315
- * Deletes old terminated sessions (for cleanup).
5316
- */
5317
- deleteOldTerminated(_olderThan) {
5318
- const db2 = getDatabase();
5319
- const result = db2.delete(sessions).where(eq4(sessions.status, "terminated")).run();
5320
- return result.changes;
5513
+ db2.delete(messages).where(eq4(messages.sessionId, sessionId)).run();
5321
5514
  }
5322
5515
  };
5323
5516
 
5324
5517
  // src/infrastructure/persistence/storage/audio-storage.ts
5325
5518
  import { mkdir as mkdir2, writeFile, readFile, unlink, rm, readdir as readdir2 } from "fs/promises";
5326
5519
  import { join as join5 } from "path";
5327
- import { existsSync as existsSync2 } from "fs";
5520
+ import { existsSync as existsSync4 } from "fs";
5328
5521
  var AudioStorage = class {
5329
5522
  baseDir;
5330
5523
  constructor(dataDir) {
@@ -5367,7 +5560,7 @@ var AudioStorage = class {
5367
5560
  * Deletes a specific audio file.
5368
5561
  */
5369
5562
  async deleteAudio(path) {
5370
- if (existsSync2(path)) {
5563
+ if (existsSync4(path)) {
5371
5564
  await unlink(path);
5372
5565
  }
5373
5566
  }
@@ -5377,10 +5570,10 @@ var AudioStorage = class {
5377
5570
  async deleteSessionAudio(sessionId) {
5378
5571
  const inputDir = join5(this.baseDir, "input", sessionId);
5379
5572
  const outputDir = join5(this.baseDir, "output", sessionId);
5380
- if (existsSync2(inputDir)) {
5573
+ if (existsSync4(inputDir)) {
5381
5574
  await rm(inputDir, { recursive: true, force: true });
5382
5575
  }
5383
- if (existsSync2(outputDir)) {
5576
+ if (existsSync4(outputDir)) {
5384
5577
  await rm(outputDir, { recursive: true, force: true });
5385
5578
  }
5386
5579
  }
@@ -5388,7 +5581,7 @@ var AudioStorage = class {
5388
5581
  * Checks if an audio file exists.
5389
5582
  */
5390
5583
  exists(path) {
5391
- return existsSync2(path);
5584
+ return existsSync4(path);
5392
5585
  }
5393
5586
  /**
5394
5587
  * Finds audio file by message ID across all sessions.
@@ -5400,7 +5593,7 @@ var AudioStorage = class {
5400
5593
  */
5401
5594
  async findAudioByMessageId(messageId, type) {
5402
5595
  const typeDir = join5(this.baseDir, type);
5403
- if (!existsSync2(typeDir)) {
5596
+ if (!existsSync4(typeDir)) {
5404
5597
  return null;
5405
5598
  }
5406
5599
  try {
@@ -6385,7 +6578,9 @@ Test Files 3 passed (3)
6385
6578
  };
6386
6579
 
6387
6580
  // src/infrastructure/persistence/in-memory-session-manager.ts
6388
- import { EventEmitter as EventEmitter3 } from "events";
6581
+ import { EventEmitter as EventEmitter6 } from "events";
6582
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
6583
+ import { join as join9 } from "path";
6389
6584
  import { nanoid as nanoid3 } from "nanoid";
6390
6585
 
6391
6586
  // src/domain/entities/supervisor-session.ts
@@ -6421,35 +6616,1830 @@ var SupervisorSession = class extends Session {
6421
6616
  }
6422
6617
  };
6423
6618
 
6424
- // src/infrastructure/persistence/in-memory-session-manager.ts
6425
- var InMemorySessionManager = class extends EventEmitter3 {
6426
- sessions = /* @__PURE__ */ new Map();
6427
- ptyManager;
6428
- agentSessionManager;
6429
- logger;
6430
- supervisorSession = null;
6431
- constructor(config2) {
6432
- super();
6433
- this.ptyManager = config2.ptyManager;
6434
- this.agentSessionManager = config2.agentSessionManager;
6435
- this.logger = config2.logger.child({ component: "session-manager" });
6436
- this.setupAgentSessionSync();
6619
+ // src/domain/entities/backlog-agent-session.ts
6620
+ var BacklogAgentSession = class extends Session {
6621
+ _agentName;
6622
+ _backlogId;
6623
+ _harnessRunning = false;
6624
+ constructor(props) {
6625
+ super({
6626
+ ...props,
6627
+ type: "backlog-agent"
6628
+ });
6629
+ this._agentName = props.agentName;
6630
+ this._backlogId = props.backlogId;
6437
6631
  }
6438
- /**
6439
- * Sets up event listeners to sync agent session state.
6440
- */
6441
- setupAgentSessionSync() {
6442
- this.agentSessionManager.on("sessionCreated", (state) => {
6443
- if (!this.sessions.has(state.sessionId)) {
6444
- const session = new AgentSession({
6445
- id: new SessionId(state.sessionId),
6446
- type: state.agentType,
6447
- agentName: state.agentName,
6448
- workingDir: state.workingDir
6449
- });
6450
- this.sessions.set(state.sessionId, session);
6451
- this.logger.debug(
6452
- { sessionId: state.sessionId, agentType: state.agentType },
6632
+ get agentName() {
6633
+ return this._agentName;
6634
+ }
6635
+ get backlogId() {
6636
+ return this._backlogId;
6637
+ }
6638
+ get harnessRunning() {
6639
+ return this._harnessRunning;
6640
+ }
6641
+ setHarnessRunning(running) {
6642
+ this._harnessRunning = running;
6643
+ if (running) {
6644
+ this.markBusy();
6645
+ } else {
6646
+ this.markIdle();
6647
+ }
6648
+ }
6649
+ terminate() {
6650
+ this._harnessRunning = false;
6651
+ this.markTerminated();
6652
+ return Promise.resolve();
6653
+ }
6654
+ toInfo() {
6655
+ const info = super.toInfo();
6656
+ return {
6657
+ ...info,
6658
+ agent_name: this._agentName,
6659
+ backlog_id: this._backlogId,
6660
+ harness_running: this._harnessRunning
6661
+ // backlog_summary will be populated by BacklogAgentManager if needed
6662
+ };
6663
+ }
6664
+ };
6665
+
6666
+ // src/infrastructure/agents/backlog-agent-manager.ts
6667
+ import { EventEmitter as EventEmitter5 } from "events";
6668
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6669
+ import { join as join8 } from "path";
6670
+
6671
+ // src/domain/value-objects/backlog.ts
6672
+ import { z as z3 } from "zod";
6673
+ var TaskSchema = z3.object({
6674
+ id: z3.number().describe("Internal task ID (sequential)"),
6675
+ external_id: z3.string().optional().describe("External system ID (e.g., AUTH-123)"),
6676
+ external_url: z3.string().optional().describe("Link to external issue"),
6677
+ title: z3.string().describe("Task title"),
6678
+ description: z3.string().describe("Detailed description"),
6679
+ acceptance_criteria: z3.array(z3.string()).describe("Acceptance criteria"),
6680
+ dependencies: z3.array(z3.number()).describe("Task IDs this task depends on"),
6681
+ priority: z3.enum(["low", "medium", "high"]).describe("Priority level"),
6682
+ complexity: z3.enum(["simple", "moderate", "complex"]).describe("Estimated complexity"),
6683
+ status: z3.enum(["pending", "in_progress", "completed", "failed", "skipped"]).describe("Current status"),
6684
+ started_at: z3.string().datetime().optional().describe("When task execution started"),
6685
+ completed_at: z3.string().datetime().optional().describe("When task was completed"),
6686
+ error: z3.string().optional().describe("Error message if task failed"),
6687
+ commit_hash: z3.string().optional().describe("Git commit hash if code was written"),
6688
+ retry_count: z3.number().default(0).describe("Number of times task was retried")
6689
+ });
6690
+ var TaskSourceSchema = z3.object({
6691
+ type: z3.enum(["jira", "github", "gitlab", "linear", "notion", "manual"]).describe("Source system"),
6692
+ system: z3.string().optional().describe("Human-friendly system name"),
6693
+ issue_id: z3.string().optional().describe("Single issue/epic ID"),
6694
+ sprint_id: z3.number().optional().describe("Jira sprint ID"),
6695
+ project_key: z3.string().optional().describe("Jira project key"),
6696
+ repository: z3.string().optional().describe("GitHub repo (owner/repo)"),
6697
+ labels: z3.array(z3.string()).optional().describe("GitHub/GitLab labels"),
6698
+ url: z3.string().optional().describe("URL for custom sources")
6699
+ });
6700
+ var BacklogSchema = z3.object({
6701
+ id: z3.string().describe("Unique backlog identifier"),
6702
+ project: z3.string().describe("Project name"),
6703
+ worktree: z3.string().optional().describe("Git worktree/branch name (omitted for main/master)"),
6704
+ agent: z3.enum(["claude", "cursor", "opencode"]).describe("Agent to use for execution"),
6705
+ source: TaskSourceSchema.describe("Where tasks came from"),
6706
+ created_at: z3.string().datetime().describe("When backlog was created"),
6707
+ updated_at: z3.string().datetime().optional().describe("Last update time"),
6708
+ tasks: z3.array(TaskSchema).describe("List of tasks"),
6709
+ completed_at: z3.string().datetime().optional().describe("When all tasks completed"),
6710
+ summary: z3.object({
6711
+ total: z3.number(),
6712
+ completed: z3.number(),
6713
+ failed: z3.number(),
6714
+ in_progress: z3.number(),
6715
+ pending: z3.number()
6716
+ }).optional().describe("Task count summary")
6717
+ });
6718
+ function recalculateSummary(backlog) {
6719
+ const summary = {
6720
+ total: backlog.tasks.length,
6721
+ completed: 0,
6722
+ failed: 0,
6723
+ in_progress: 0,
6724
+ pending: 0
6725
+ };
6726
+ for (const task of backlog.tasks) {
6727
+ if (task.status === "completed") summary.completed++;
6728
+ else if (task.status === "failed") summary.failed++;
6729
+ else if (task.status === "in_progress") summary.in_progress++;
6730
+ else if (task.status === "pending") summary.pending++;
6731
+ }
6732
+ return {
6733
+ ...backlog,
6734
+ summary,
6735
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
6736
+ };
6737
+ }
6738
+
6739
+ // src/infrastructure/agents/backlog-harness.ts
6740
+ import { EventEmitter as EventEmitter3 } from "events";
6741
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
6742
+ import { join as join6 } from "path";
6743
+ var BacklogHarness = class _BacklogHarness extends EventEmitter3 {
6744
+ backlog;
6745
+ isRunning = false;
6746
+ isPaused = false;
6747
+ workingDir;
6748
+ agentSessionManager;
6749
+ logger;
6750
+ startTime = 0;
6751
+ selectedAgent;
6752
+ constructor(backlog, workingDir, selectedAgent, agentSessionManager, logger) {
6753
+ super();
6754
+ this.backlog = backlog;
6755
+ this.workingDir = workingDir;
6756
+ this.selectedAgent = selectedAgent;
6757
+ this.agentSessionManager = agentSessionManager;
6758
+ this.logger = logger;
6759
+ }
6760
+ /**
6761
+ * Start autonomous execution of tasks.
6762
+ */
6763
+ async start() {
6764
+ if (this.isRunning) {
6765
+ this.logger.warn("Harness already running");
6766
+ return;
6767
+ }
6768
+ this.isRunning = true;
6769
+ this.isPaused = false;
6770
+ this.startTime = Date.now();
6771
+ this.emit("harness-started", {
6772
+ totalTasks: this.backlog.tasks.length
6773
+ });
6774
+ this.broadcastOutput({
6775
+ id: "harness-start",
6776
+ block_type: "status",
6777
+ content: `\u{1F680} Starting Harness. Total tasks: ${this.backlog.tasks.length}`
6778
+ });
6779
+ try {
6780
+ await this.executeLoop();
6781
+ } catch (error) {
6782
+ this.logger.error({ error }, "Harness error");
6783
+ this.broadcastOutput({
6784
+ id: "harness-error",
6785
+ block_type: "error",
6786
+ content: `Harness error: ${error instanceof Error ? error.message : "Unknown error"}`
6787
+ });
6788
+ } finally {
6789
+ this.isRunning = false;
6790
+ }
6791
+ }
6792
+ /**
6793
+ * Pause execution (current task finishes, then pauses).
6794
+ */
6795
+ pause() {
6796
+ if (!this.isRunning) {
6797
+ this.logger.warn("Harness not running");
6798
+ return;
6799
+ }
6800
+ this.isPaused = true;
6801
+ this.emit("harness-paused", void 0);
6802
+ this.broadcastOutput({
6803
+ id: "harness-pause",
6804
+ block_type: "status",
6805
+ content: "\u23F8\uFE0F Harness paused"
6806
+ });
6807
+ }
6808
+ /**
6809
+ * Resume paused execution.
6810
+ */
6811
+ resume() {
6812
+ if (!this.isRunning || !this.isPaused) {
6813
+ this.logger.warn("Cannot resume harness");
6814
+ return;
6815
+ }
6816
+ this.isPaused = false;
6817
+ this.emit("harness-resumed", void 0);
6818
+ this.broadcastOutput({
6819
+ id: "harness-resume",
6820
+ block_type: "status",
6821
+ content: "\u25B6\uFE0F Harness resumed"
6822
+ });
6823
+ }
6824
+ /**
6825
+ * Stop execution immediately.
6826
+ */
6827
+ stop() {
6828
+ this.isRunning = false;
6829
+ this.isPaused = false;
6830
+ this.emit("harness-stopped", void 0);
6831
+ this.broadcastOutput({
6832
+ id: "harness-stop",
6833
+ block_type: "status",
6834
+ content: "\u23F9\uFE0F Harness stopped"
6835
+ });
6836
+ }
6837
+ /**
6838
+ * Main execution loop.
6839
+ */
6840
+ async executeLoop() {
6841
+ let completedCount = 0;
6842
+ let failedCount = 0;
6843
+ while (this.isRunning) {
6844
+ while (this.isPaused && this.isRunning) {
6845
+ await this.sleep(1e3);
6846
+ }
6847
+ if (!this.isRunning) break;
6848
+ const task = this.findNextPendingTask();
6849
+ if (!task) {
6850
+ break;
6851
+ }
6852
+ if (!this.canExecuteTask(task)) {
6853
+ this.logger.debug(`Task ${task.id} dependencies not ready, skipping`);
6854
+ continue;
6855
+ }
6856
+ try {
6857
+ await this.executeTask(task);
6858
+ completedCount++;
6859
+ } catch (error) {
6860
+ this.logger.error({ error, taskId: task.id }, `Task ${task.id} failed`);
6861
+ failedCount++;
6862
+ this.emit("task-failed", {
6863
+ taskId: task.id,
6864
+ title: task.title,
6865
+ error: error instanceof Error ? error.message : "Unknown error"
6866
+ });
6867
+ this.updateTaskStatus(task.id, "failed", {
6868
+ error: error instanceof Error ? error.message : "Unknown error"
6869
+ });
6870
+ }
6871
+ this.saveBacklog();
6872
+ }
6873
+ const duration = Date.now() - this.startTime;
6874
+ const summary = this.backlog.summary ?? {
6875
+ total: this.backlog.tasks.length,
6876
+ completed: completedCount,
6877
+ failed: failedCount,
6878
+ in_progress: 0,
6879
+ pending: this.backlog.tasks.filter((t) => t.status === "pending").length
6880
+ };
6881
+ this.emit("harness-completed", {
6882
+ completed: summary.completed,
6883
+ failed: summary.failed,
6884
+ total: summary.total,
6885
+ duration
6886
+ });
6887
+ this.broadcastOutput({
6888
+ id: "harness-complete",
6889
+ block_type: "status",
6890
+ content: `\u2705 Harness completed. Completed: ${summary.completed}/${summary.total}, Failed: ${summary.failed}`
6891
+ });
6892
+ }
6893
+ /**
6894
+ * Execute a single task.
6895
+ */
6896
+ async executeTask(task) {
6897
+ const startTime = Date.now();
6898
+ this.emit("task-started", {
6899
+ taskId: task.id,
6900
+ title: task.title,
6901
+ externalId: task.external_id
6902
+ });
6903
+ this.updateTaskStatus(task.id, "in_progress", { started_at: (/* @__PURE__ */ new Date()).toISOString() });
6904
+ this.broadcastOutput({
6905
+ id: `task-${task.id}-start`,
6906
+ block_type: "status",
6907
+ content: `\u{1F4CC} Task ${task.id}: ${task.title}`
6908
+ });
6909
+ const prompt = this.buildTaskPrompt(task);
6910
+ const taskIndex = this.backlog.tasks.findIndex((t) => t.id === task.id) + 1;
6911
+ const totalTasks = this.backlog.tasks.length;
6912
+ const taskSessionId = `backlog-${this.backlog.id}-task-${task.id}`;
6913
+ const taskAgentName = `Task ${taskIndex}/${totalTasks}: ${task.title}`;
6914
+ this.logger.info(
6915
+ { sessionId: taskSessionId, agent: this.selectedAgent, taskIndex, totalTasks },
6916
+ "Creating new agent session for task iteration"
6917
+ );
6918
+ const agentType = this.selectedAgent;
6919
+ const newSession = this.agentSessionManager.createSession(
6920
+ agentType,
6921
+ this.workingDir,
6922
+ taskSessionId,
6923
+ taskAgentName
6924
+ );
6925
+ const sessionId = newSession.sessionId;
6926
+ this.logger.info(
6927
+ { sessionId, agent: this.selectedAgent, taskIndex, taskTitle: task.title },
6928
+ "Created new agent session for task"
6929
+ );
6930
+ await new Promise((resolve2, reject) => {
6931
+ let isResolved = false;
6932
+ let executionTimeoutHandle;
6933
+ const onBlocks = (emittedSessionId, blocks, isComplete) => {
6934
+ if (emittedSessionId !== sessionId) {
6935
+ return;
6936
+ }
6937
+ this.broadcastOutput(...blocks);
6938
+ if (isResolved) {
6939
+ return;
6940
+ }
6941
+ let commitHash;
6942
+ let hasCommitDetected = false;
6943
+ for (const block of blocks) {
6944
+ if (block.block_type === "text") {
6945
+ const content = block.content;
6946
+ const commitRegex = /commit\s+([a-f0-9]{7,40})/i;
6947
+ const commitMatch = commitRegex.exec(content);
6948
+ if (commitMatch) {
6949
+ commitHash = commitMatch[1];
6950
+ hasCommitDetected = true;
6951
+ break;
6952
+ }
6953
+ const headRegex = /HEAD\s+is\s+now\s+at\s+([a-f0-9]{7,40})/i;
6954
+ const headMatch = headRegex.exec(content);
6955
+ if (headMatch) {
6956
+ commitHash = headMatch[1];
6957
+ hasCommitDetected = true;
6958
+ break;
6959
+ }
6960
+ }
6961
+ }
6962
+ if (hasCommitDetected && commitHash) {
6963
+ isResolved = true;
6964
+ if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
6965
+ this.agentSessionManager.removeListener("blocks", onBlocks);
6966
+ const duration = Date.now() - startTime;
6967
+ this.emit("task-completed", {
6968
+ taskId: task.id,
6969
+ title: task.title,
6970
+ success: true,
6971
+ commitHash,
6972
+ duration
6973
+ });
6974
+ this.updateTaskStatus(task.id, "completed", {
6975
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6976
+ commit_hash: commitHash
6977
+ });
6978
+ this.logger.info(
6979
+ { taskId: task.id, title: task.title, commitHash, duration },
6980
+ "Task completed with commit"
6981
+ );
6982
+ resolve2();
6983
+ return;
6984
+ }
6985
+ if (isComplete && !isResolved) {
6986
+ isResolved = true;
6987
+ if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
6988
+ this.agentSessionManager.removeListener("blocks", onBlocks);
6989
+ const duration = Date.now() - startTime;
6990
+ const errorMsg = "Task execution completed but no commit was detected. The agent may not have completed the implementation.";
6991
+ this.emit("task-completed", {
6992
+ taskId: task.id,
6993
+ title: task.title,
6994
+ success: false,
6995
+ duration
6996
+ });
6997
+ this.updateTaskStatus(task.id, "failed", {
6998
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
6999
+ error: errorMsg
7000
+ });
7001
+ this.logger.warn(
7002
+ { taskId: task.id, title: task.title, duration },
7003
+ errorMsg
7004
+ );
7005
+ reject(new Error(errorMsg));
7006
+ }
7007
+ };
7008
+ this.agentSessionManager.on("blocks", onBlocks);
7009
+ executionTimeoutHandle = setTimeout(() => {
7010
+ if (!isResolved) {
7011
+ isResolved = true;
7012
+ this.agentSessionManager.removeListener("blocks", onBlocks);
7013
+ const duration = Date.now() - startTime;
7014
+ this.emit("task-completed", {
7015
+ taskId: task.id,
7016
+ title: task.title,
7017
+ success: false,
7018
+ duration
7019
+ });
7020
+ this.updateTaskStatus(task.id, "failed", {
7021
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
7022
+ error: "Task execution timeout (30 minutes exceeded)"
7023
+ });
7024
+ reject(new Error("Task execution timeout"));
7025
+ }
7026
+ }, 30 * 60 * 1e3);
7027
+ this.agentSessionManager.executeCommand(sessionId, prompt).catch((error) => {
7028
+ if (!isResolved) {
7029
+ isResolved = true;
7030
+ if (executionTimeoutHandle) clearTimeout(executionTimeoutHandle);
7031
+ this.agentSessionManager.removeListener("blocks", onBlocks);
7032
+ reject(error instanceof Error ? error : new Error(String(error)));
7033
+ }
7034
+ });
7035
+ });
7036
+ }
7037
+ /**
7038
+ * Build a prompt for the coding agent based on task.
7039
+ */
7040
+ buildTaskPrompt(task) {
7041
+ return `
7042
+ You are a senior developer. Complete this task:
7043
+
7044
+ ## Task: ${task.title}
7045
+
7046
+ ${task.description}
7047
+
7048
+ ## Acceptance Criteria:
7049
+ ${task.acceptance_criteria.map((c) => `- ${c}`).join("\n")}
7050
+
7051
+ ## Instructions:
7052
+ 1. Implement the feature/fix according to the acceptance criteria
7053
+ 2. Write/update tests to cover your changes
7054
+ 3. Run all tests and make sure they pass
7055
+ 4. Commit your changes with a descriptive message
7056
+ 5. Ensure code follows project conventions
7057
+
7058
+ ## IMPORTANT - Completion Signal:
7059
+ When you have COMPLETED the task and committed your changes:
7060
+ - Make sure your last action is a git commit
7061
+ - Output the full commit hash in your final summary
7062
+ - Write a clear summary confirming what was implemented and that acceptance criteria are met
7063
+
7064
+ Do NOT write "Task complete" or similar - just commit your code and include the hash in your output.
7065
+ `.trim();
7066
+ }
7067
+ /**
7068
+ * Find next pending task.
7069
+ */
7070
+ findNextPendingTask() {
7071
+ return this.backlog.tasks.find((t) => t.status === "pending");
7072
+ }
7073
+ /**
7074
+ * Check if task dependencies are satisfied.
7075
+ */
7076
+ canExecuteTask(task) {
7077
+ if (task.dependencies.length === 0) {
7078
+ return true;
7079
+ }
7080
+ return task.dependencies.every((depId) => {
7081
+ const depTask = this.backlog.tasks.find((t) => t.id === depId);
7082
+ return depTask?.status === "completed";
7083
+ });
7084
+ }
7085
+ /**
7086
+ * Update task status.
7087
+ */
7088
+ updateTaskStatus(taskId, status, updates = {}) {
7089
+ const task = this.backlog.tasks.find((t) => t.id === taskId);
7090
+ if (task) {
7091
+ task.status = status;
7092
+ Object.assign(task, updates);
7093
+ this.backlog = recalculateSummary(this.backlog);
7094
+ }
7095
+ }
7096
+ /**
7097
+ * Save backlog to file.
7098
+ */
7099
+ saveBacklog() {
7100
+ const backlogPath = join6(this.workingDir, "backlog.json");
7101
+ try {
7102
+ writeFileSync(backlogPath, JSON.stringify(this.backlog, null, 2));
7103
+ this.logger.debug(`Saved backlog to ${backlogPath}`);
7104
+ } catch (error) {
7105
+ this.logger.error({ error }, "Failed to save backlog");
7106
+ }
7107
+ }
7108
+ /**
7109
+ * Broadcast output blocks to UI.
7110
+ */
7111
+ broadcastOutput(...blocks) {
7112
+ this.emit("output", blocks);
7113
+ }
7114
+ /**
7115
+ * Sleep helper.
7116
+ */
7117
+ sleep(ms) {
7118
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7119
+ }
7120
+ /**
7121
+ * Load backlog from file.
7122
+ * @param selectedAgent - The agent type to use for harness execution
7123
+ */
7124
+ static loadFromFile(path, selectedAgent, agentSessionManager, logger) {
7125
+ const content = readFileSync2(path, "utf-8");
7126
+ const backlog = JSON.parse(content);
7127
+ const workingDir = path.substring(0, path.lastIndexOf("/"));
7128
+ return new _BacklogHarness(backlog, workingDir, selectedAgent, agentSessionManager, logger);
7129
+ }
7130
+ };
7131
+
7132
+ // src/infrastructure/agents/base/lang-graph-agent.ts
7133
+ import { EventEmitter as EventEmitter4 } from "events";
7134
+ import { ChatOpenAI } from "@langchain/openai";
7135
+ import { createReactAgent } from "@langchain/langgraph/prebuilt";
7136
+ import { HumanMessage, AIMessage, isAIMessage } from "@langchain/core/messages";
7137
+ var LangGraphAgent = class extends EventEmitter4 {
7138
+ logger;
7139
+ agent = null;
7140
+ conversationHistory = [];
7141
+ abortController = null;
7142
+ isExecuting = false;
7143
+ isCancelled = false;
7144
+ /** Tracks if we're processing a command (including STT, before LLM execution) */
7145
+ isProcessingCommand = false;
7146
+ /** Timestamp when current execution started (for race condition protection) */
7147
+ executionStartedAt = 0;
7148
+ constructor(logger) {
7149
+ super();
7150
+ this.logger = logger.child({ component: this.constructor.name });
7151
+ }
7152
+ /**
7153
+ * Called after successful execution completion.
7154
+ * Subclasses can override to perform agent-specific post-execution logic.
7155
+ */
7156
+ async onExecutionComplete(_blocks, _finalOutput) {
7157
+ }
7158
+ /**
7159
+ * Initializes the LangGraph agent and state manager.
7160
+ * Called by subclasses during construction.
7161
+ */
7162
+ initializeAgent() {
7163
+ if (this.agent) {
7164
+ return;
7165
+ }
7166
+ try {
7167
+ const env = getEnv();
7168
+ const llm = this.createLLM(env);
7169
+ const tools = this.createTools();
7170
+ this.logger.info({ toolCount: tools.length }, `Creating ${this.constructor.name} with tools`);
7171
+ this.agent = createReactAgent({
7172
+ llm,
7173
+ tools
7174
+ });
7175
+ } catch (error) {
7176
+ this.logger.error({ error }, `Failed to initialize ${this.constructor.name}`);
7177
+ throw error;
7178
+ }
7179
+ }
7180
+ /**
7181
+ * Creates the LLM instance based on environment configuration.
7182
+ */
7183
+ createLLM(env) {
7184
+ const provider = env.AGENT_PROVIDER;
7185
+ const apiKey = env.AGENT_API_KEY;
7186
+ const modelName = env.AGENT_MODEL_NAME;
7187
+ const baseUrl = env.AGENT_BASE_URL;
7188
+ const temperature = env.AGENT_TEMPERATURE;
7189
+ if (!apiKey) {
7190
+ throw new Error(`AGENT_API_KEY is required for ${this.constructor.name}`);
7191
+ }
7192
+ this.logger.info({ provider, model: modelName }, "Initializing LLM");
7193
+ return new ChatOpenAI({
7194
+ openAIApiKey: apiKey,
7195
+ modelName,
7196
+ temperature,
7197
+ configuration: baseUrl ? {
7198
+ baseURL: baseUrl
7199
+ } : void 0
7200
+ });
7201
+ }
7202
+ /**
7203
+ * Executes a command with streaming output.
7204
+ * Emits 'blocks' events as content is generated.
7205
+ *
7206
+ * This is the unified streaming method used by all agents.
7207
+ * Subclasses should NOT override this unless they have very specific requirements.
7208
+ */
7209
+ async executeWithStream(command, deviceId) {
7210
+ this.logger.info(
7211
+ { command, deviceId, wasCancelledBefore: this.isCancelled },
7212
+ `Executing ${this.constructor.name} with streaming`
7213
+ );
7214
+ this.abortController = new AbortController();
7215
+ this.isExecuting = true;
7216
+ this.isCancelled = false;
7217
+ this.executionStartedAt = Date.now();
7218
+ this.logger.debug({ isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Flags reset for new execution");
7219
+ try {
7220
+ const stateManager = this.createStateManager();
7221
+ const history = await stateManager.loadHistory();
7222
+ this.conversationHistory = history;
7223
+ const messages2 = [
7224
+ ...this.buildSystemMessage(),
7225
+ ...this.buildHistoryMessages(history),
7226
+ new HumanMessage(command)
7227
+ ];
7228
+ if (this.isExecuting && !this.isCancelled) {
7229
+ const statusBlock = createStatusBlock("Processing...");
7230
+ this.logger.debug({ deviceId, blockType: "status" }, "Emitting status block");
7231
+ this.emit("blocks", deviceId, [statusBlock], false);
7232
+ }
7233
+ this.logger.info({ deviceId }, `Starting LangGraph agent stream for ${this.constructor.name}`);
7234
+ if (!this.agent) {
7235
+ throw new Error("Agent not initialized");
7236
+ }
7237
+ const stream = await this.agent.stream(
7238
+ { messages: messages2 },
7239
+ {
7240
+ streamMode: "values",
7241
+ signal: this.abortController.signal
7242
+ }
7243
+ );
7244
+ let finalOutput = "";
7245
+ const allBlocks = [];
7246
+ for await (const chunk of stream) {
7247
+ if (this.isCancelled || !this.isExecuting) {
7248
+ this.logger.info(
7249
+ { deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting },
7250
+ `${this.constructor.name} execution cancelled, stopping stream processing`
7251
+ );
7252
+ return;
7253
+ }
7254
+ const chunkData = chunk;
7255
+ const chunkMessages = chunkData.messages;
7256
+ if (!chunkMessages || chunkMessages.length === 0) continue;
7257
+ const lastMessage = chunkMessages[chunkMessages.length - 1];
7258
+ if (!lastMessage) continue;
7259
+ if (isAIMessage(lastMessage)) {
7260
+ const content = lastMessage.content;
7261
+ if (typeof content === "string" && content.length > 0) {
7262
+ finalOutput = content;
7263
+ if (this.isExecuting && !this.isCancelled) {
7264
+ const textBlock = createTextBlock(content);
7265
+ this.emit("blocks", deviceId, [textBlock], false);
7266
+ accumulateBlocks(allBlocks, [textBlock]);
7267
+ }
7268
+ } else if (Array.isArray(content)) {
7269
+ for (const item of content) {
7270
+ if (typeof item === "object" && this.isExecuting && !this.isCancelled) {
7271
+ const block = this.parseContentItem(item);
7272
+ if (block) {
7273
+ this.emit("blocks", deviceId, [block], false);
7274
+ accumulateBlocks(allBlocks, [block]);
7275
+ }
7276
+ }
7277
+ }
7278
+ }
7279
+ }
7280
+ if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
7281
+ const toolContent = lastMessage.content;
7282
+ const toolName = lastMessage.name ?? "tool";
7283
+ const toolCallId = lastMessage.tool_call_id;
7284
+ const toolBlock = createToolBlock(
7285
+ toolName,
7286
+ "completed",
7287
+ void 0,
7288
+ typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
7289
+ toolCallId
7290
+ );
7291
+ this.emit("blocks", deviceId, [toolBlock], false);
7292
+ accumulateBlocks(allBlocks, [toolBlock]);
7293
+ }
7294
+ }
7295
+ if (this.isExecuting && !this.isCancelled) {
7296
+ this.addToHistory("user", command);
7297
+ this.addToHistory("assistant", finalOutput);
7298
+ const finalBlocks = mergeToolBlocks(allBlocks);
7299
+ const stateManager2 = this.createStateManager();
7300
+ await stateManager2.saveHistory(this.conversationHistory);
7301
+ await this.onExecutionComplete(finalBlocks, finalOutput);
7302
+ const completionBlock = createStatusBlock("Complete");
7303
+ this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
7304
+ this.logger.debug({ output: finalOutput.slice(0, 200) }, `${this.constructor.name} streaming completed`);
7305
+ } else {
7306
+ this.logger.info(
7307
+ { deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting },
7308
+ `${this.constructor.name} streaming ended due to cancellation`
7309
+ );
7310
+ }
7311
+ } catch (error) {
7312
+ if (this.isCancelled || !this.isExecuting) {
7313
+ this.logger.info({ deviceId }, `${this.constructor.name} execution cancelled (caught in error handler)`);
7314
+ return;
7315
+ }
7316
+ this.logger.error({ error, command }, `${this.constructor.name} streaming failed`);
7317
+ const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
7318
+ const errorBlock = createErrorBlock(errorMessage);
7319
+ this.emit("blocks", deviceId, [errorBlock], true);
7320
+ } finally {
7321
+ this.isExecuting = false;
7322
+ this.abortController = null;
7323
+ }
7324
+ }
7325
+ /**
7326
+ * Parses a content item from LangGraph into a ContentBlock.
7327
+ */
7328
+ parseContentItem(item) {
7329
+ const type = item.type;
7330
+ if (type === "text" && typeof item.text === "string" && item.text.trim()) {
7331
+ return createTextBlock(item.text);
7332
+ }
7333
+ if (type === "tool_use") {
7334
+ const name = typeof item.name === "string" ? item.name : "tool";
7335
+ const input = item.input;
7336
+ const toolUseId = typeof item.id === "string" ? item.id : void 0;
7337
+ return createToolBlock(name, "running", input, void 0, toolUseId);
7338
+ }
7339
+ return null;
7340
+ }
7341
+ /**
7342
+ * Cancels the current execution if running.
7343
+ * Returns true if cancellation was initiated.
7344
+ */
7345
+ cancel() {
7346
+ if (!this.isProcessingCommand && !this.isExecuting) {
7347
+ this.logger.debug(
7348
+ { isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting },
7349
+ "No active execution to cancel"
7350
+ );
7351
+ return false;
7352
+ }
7353
+ const timeSinceStart = Date.now() - this.executionStartedAt;
7354
+ if (this.isExecuting && timeSinceStart < 500) {
7355
+ this.logger.info(
7356
+ { timeSinceStart, isProcessingCommand: this.isProcessingCommand },
7357
+ "Ignoring cancel - execution just started (race condition protection)"
7358
+ );
7359
+ return false;
7360
+ }
7361
+ this.logger.info(
7362
+ { isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting, timeSinceStart },
7363
+ `Cancelling ${this.constructor.name} execution`
7364
+ );
7365
+ this.isCancelled = true;
7366
+ this.isExecuting = false;
7367
+ this.isProcessingCommand = false;
7368
+ if (this.abortController) {
7369
+ this.abortController.abort();
7370
+ }
7371
+ return true;
7372
+ }
7373
+ /**
7374
+ * Check if execution was cancelled.
7375
+ * Used by clients to filter out any late-arriving blocks.
7376
+ */
7377
+ wasCancelled() {
7378
+ return this.isCancelled;
7379
+ }
7380
+ /**
7381
+ * Starts command processing (before STT/LLM execution).
7382
+ * Returns an AbortController that can be used to cancel STT and other operations.
7383
+ */
7384
+ startProcessing() {
7385
+ this.abortController = new AbortController();
7386
+ this.isProcessingCommand = true;
7387
+ this.isCancelled = false;
7388
+ this.logger.debug("Started command processing");
7389
+ return this.abortController;
7390
+ }
7391
+ /**
7392
+ * Checks if command processing is active (STT or LLM execution).
7393
+ */
7394
+ isProcessing() {
7395
+ return this.isProcessingCommand || this.isExecuting;
7396
+ }
7397
+ /**
7398
+ * Ends command processing (called after completion or error, not after cancel).
7399
+ */
7400
+ endProcessing() {
7401
+ this.isProcessingCommand = false;
7402
+ this.logger.debug("Ended command processing");
7403
+ }
7404
+ /**
7405
+ * Gets conversation history.
7406
+ */
7407
+ getConversationHistory() {
7408
+ return this.conversationHistory;
7409
+ }
7410
+ /**
7411
+ * Resets the cancellation state.
7412
+ * Call this before starting a new command to ensure previous cancellation doesn't affect it.
7413
+ */
7414
+ resetCancellationState() {
7415
+ this.isCancelled = false;
7416
+ }
7417
+ /**
7418
+ * Restores conversation history from persistent storage.
7419
+ * Called on startup to sync in-memory cache with database.
7420
+ */
7421
+ restoreHistory(history) {
7422
+ if (history.length === 0) return;
7423
+ this.conversationHistory = history.slice(-20);
7424
+ this.logger.debug({ messageCount: this.conversationHistory.length }, "Conversation history restored");
7425
+ }
7426
+ /**
7427
+ * Builds the system message for the agent.
7428
+ */
7429
+ buildSystemMessage() {
7430
+ const systemPrompt = this.buildSystemPrompt();
7431
+ return [new HumanMessage(`[System Instructions]
7432
+ ${systemPrompt}
7433
+ [End Instructions]`)];
7434
+ }
7435
+ /**
7436
+ * Builds messages from conversation history.
7437
+ */
7438
+ buildHistoryMessages(history) {
7439
+ return history.map(
7440
+ (entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
7441
+ );
7442
+ }
7443
+ /**
7444
+ * Adds an entry to conversation history.
7445
+ */
7446
+ addToHistory(role, content) {
7447
+ this.conversationHistory.push({ role, content });
7448
+ if (this.conversationHistory.length > 20) {
7449
+ this.conversationHistory.splice(0, this.conversationHistory.length - 20);
7450
+ }
7451
+ }
7452
+ };
7453
+
7454
+ // src/infrastructure/agents/base/backlog-state-manager.ts
7455
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
7456
+ import { join as join7 } from "path";
7457
+ var BacklogStateManager = class {
7458
+ constructor(workingDir, logger) {
7459
+ this.workingDir = workingDir;
7460
+ this.logger = logger;
7461
+ }
7462
+ getHistoryPath() {
7463
+ return join7(this.workingDir, "conversation-history.json");
7464
+ }
7465
+ loadHistory() {
7466
+ const historyPath = this.getHistoryPath();
7467
+ if (!existsSync5(historyPath)) {
7468
+ return Promise.resolve([]);
7469
+ }
7470
+ try {
7471
+ const content = readFileSync3(historyPath, "utf-8");
7472
+ const data = JSON.parse(content);
7473
+ this.logger.debug({ messageCount: data.length }, "Loaded conversation history from file");
7474
+ return Promise.resolve(data);
7475
+ } catch (error) {
7476
+ this.logger.error({ error }, "Failed to load conversation history from file");
7477
+ return Promise.resolve([]);
7478
+ }
7479
+ }
7480
+ saveHistory(history) {
7481
+ const historyPath = this.getHistoryPath();
7482
+ try {
7483
+ writeFileSync2(historyPath, JSON.stringify(history, null, 2), "utf-8");
7484
+ this.logger.debug({ messageCount: history.length }, "Saved conversation history to file");
7485
+ } catch (error) {
7486
+ this.logger.error({ error }, "Failed to save conversation history to file");
7487
+ }
7488
+ return Promise.resolve();
7489
+ }
7490
+ clearHistory() {
7491
+ const historyPath = this.getHistoryPath();
7492
+ try {
7493
+ if (existsSync5(historyPath)) {
7494
+ writeFileSync2(historyPath, JSON.stringify([], null, 2), "utf-8");
7495
+ }
7496
+ this.logger.info("Cleared conversation history");
7497
+ } catch (error) {
7498
+ this.logger.error({ error }, "Failed to clear conversation history");
7499
+ }
7500
+ return Promise.resolve();
7501
+ }
7502
+ loadAdditionalState(key) {
7503
+ if (key !== "backlog") {
7504
+ return Promise.resolve(null);
7505
+ }
7506
+ const backlogPath = join7(this.workingDir, "backlog.json");
7507
+ if (!existsSync5(backlogPath)) {
7508
+ return Promise.resolve(null);
7509
+ }
7510
+ try {
7511
+ const content = readFileSync3(backlogPath, "utf-8");
7512
+ const data = JSON.parse(content);
7513
+ this.logger.debug("Loaded backlog state from file");
7514
+ return Promise.resolve(data);
7515
+ } catch (error) {
7516
+ this.logger.error({ error }, "Failed to load backlog state from file");
7517
+ return Promise.resolve(null);
7518
+ }
7519
+ }
7520
+ saveAdditionalState(key, state) {
7521
+ if (key !== "backlog") {
7522
+ return Promise.resolve();
7523
+ }
7524
+ const backlogPath = join7(this.workingDir, "backlog.json");
7525
+ try {
7526
+ writeFileSync2(backlogPath, JSON.stringify(state, null, 2), "utf-8");
7527
+ this.logger.debug("Saved backlog state to file");
7528
+ } catch (error) {
7529
+ this.logger.error({ error }, "Failed to save backlog state to file");
7530
+ }
7531
+ return Promise.resolve();
7532
+ }
7533
+ close() {
7534
+ return Promise.resolve();
7535
+ }
7536
+ };
7537
+
7538
+ // src/infrastructure/agents/backlog-agent-tools.ts
7539
+ import { tool } from "@langchain/core/tools";
7540
+ import { z as z4 } from "zod";
7541
+ function createBacklogAgentTools(context) {
7542
+ const getStatus = tool(
7543
+ () => {
7544
+ const blocks = context.getStatus();
7545
+ return blocks.map((b) => b.content).join("\n");
7546
+ },
7547
+ {
7548
+ name: "get_backlog_status",
7549
+ description: "Get the current status of the backlog including task counts and progress",
7550
+ schema: z4.object({})
7551
+ }
7552
+ );
7553
+ const startHarness = tool(
7554
+ () => {
7555
+ const blocks = context.startHarness();
7556
+ return blocks.map((b) => b.content).join("\n");
7557
+ },
7558
+ {
7559
+ name: "start_backlog_harness",
7560
+ description: "Start the backlog harness to begin executing tasks",
7561
+ schema: z4.object({})
7562
+ }
7563
+ );
7564
+ const stopHarness = tool(
7565
+ () => {
7566
+ const blocks = context.stopHarness();
7567
+ return blocks.map((b) => b.content).join("\n");
7568
+ },
7569
+ {
7570
+ name: "stop_backlog_harness",
7571
+ description: "Stop the backlog harness execution",
7572
+ schema: z4.object({})
7573
+ }
7574
+ );
7575
+ const pauseHarness = tool(
7576
+ () => {
7577
+ const blocks = context.pauseHarness();
7578
+ return blocks.map((b) => b.content).join("\n");
7579
+ },
7580
+ {
7581
+ name: "pause_backlog_harness",
7582
+ description: "Pause the backlog harness (current task will complete)",
7583
+ schema: z4.object({})
7584
+ }
7585
+ );
7586
+ const resumeHarness = tool(
7587
+ () => {
7588
+ const blocks = context.resumeHarness();
7589
+ return blocks.map((b) => b.content).join("\n");
7590
+ },
7591
+ {
7592
+ name: "resume_backlog_harness",
7593
+ description: "Resume a paused backlog harness",
7594
+ schema: z4.object({})
7595
+ }
7596
+ );
7597
+ const listTasks = tool(
7598
+ () => {
7599
+ const blocks = context.listTasks();
7600
+ return blocks.map((b) => b.content).join("\n");
7601
+ },
7602
+ {
7603
+ name: "list_backlog_tasks",
7604
+ description: "List all tasks in the backlog with their status",
7605
+ schema: z4.object({})
7606
+ }
7607
+ );
7608
+ const addTask = tool(
7609
+ ({ title, description }) => {
7610
+ const blocks = context.addTask(title, description);
7611
+ return blocks.map((b) => b.content).join("\n");
7612
+ },
7613
+ {
7614
+ name: "add_backlog_task",
7615
+ description: "Add a new task to the backlog",
7616
+ schema: z4.object({
7617
+ title: z4.string().describe("Task title"),
7618
+ description: z4.string().optional().describe("Task description")
7619
+ })
7620
+ }
7621
+ );
7622
+ const getAvailableAgents2 = tool(
7623
+ async () => {
7624
+ const agents = await context.getAvailableAgents();
7625
+ const agentsList = agents.map((a) => `- ${a.name}${a.isAlias ? " (alias)" : ""}: ${a.description}`).join("\n");
7626
+ return `Available agents:
7627
+ ${agentsList}`;
7628
+ },
7629
+ {
7630
+ name: "get_available_agents",
7631
+ description: "Get list of all available coding agents (base agents and custom aliases)",
7632
+ schema: z4.object({})
7633
+ }
7634
+ );
7635
+ const parseAgentSelection = tool(
7636
+ async ({ userResponse }) => {
7637
+ const result = await context.parseAgentSelection(userResponse);
7638
+ return `Agent selection result: ${result.valid ? `Selected agent: ${result.agentName}` : result.message}`;
7639
+ },
7640
+ {
7641
+ name: "parse_agent_selection",
7642
+ description: "Parse user response to extract selected agent name and validate it against available agents",
7643
+ schema: z4.object({
7644
+ userResponse: z4.string().describe("The user response containing agent selection")
7645
+ })
7646
+ }
7647
+ );
7648
+ return [getStatus, startHarness, stopHarness, pauseHarness, resumeHarness, listTasks, addTask, getAvailableAgents2, parseAgentSelection];
7649
+ }
7650
+
7651
+ // src/infrastructure/agents/backlog-agent.ts
7652
+ var BACKLOG_AGENT_SYSTEM_PROMPT = `You are a Backlog Agent responsible for managing development tasks in a backlog.
7653
+
7654
+ You have the following capabilities:
7655
+ - View backlog status and task progress
7656
+ - Start/stop/pause/resume harness execution
7657
+ - List tasks in the backlog
7658
+ - Add new tasks
7659
+ - Show available coding agents
7660
+ - Parse agent selection from user responses
7661
+
7662
+ When the user sends a message, interpret their intent and use the appropriate tool(s) to fulfill their request.
7663
+
7664
+ Guidelines:
7665
+ - If the user asks about progress, status, or the current state \u2192 use get_backlog_status
7666
+ - If the user wants to start execution \u2192 use start_backlog_harness
7667
+ - If the user wants to stop, pause, or resume \u2192 use the appropriate harness tool
7668
+ - If the user wants to see tasks \u2192 use list_backlog_tasks
7669
+ - If the user describes something to do or wants to add a task \u2192 use add_backlog_task
7670
+
7671
+ SPECIAL CASE - Agent Selection for Harness Execution:
7672
+ When start_backlog_harness tool returns a message asking which agent to use:
7673
+ 1. Acknowledge that we're waiting for agent selection
7674
+ 2. DO NOT call any other tools until the user provides an agent selection
7675
+ 3. When the user responds with an agent name (e.g., "I want to use claude" or "use cursor"):
7676
+ - Call parse_agent_selection with their response to validate and extract the agent name
7677
+ - Report the results to the user
7678
+ - The agent will then proceed with harness creation
7679
+
7680
+ You can call multiple tools in sequence if needed to fully satisfy the user's request (e.g., get status AND list tasks). After all tools have returned, provide a concise summary of what was accomplished.
7681
+
7682
+ Be helpful and informative. Always confirm actions and provide status updates.`;
7683
+ var BacklogAgent = class extends LangGraphAgent {
7684
+ toolsContext;
7685
+ workingDir;
7686
+ constructor(toolsContext, workingDir, logger) {
7687
+ super(logger);
7688
+ this.toolsContext = toolsContext;
7689
+ this.workingDir = workingDir;
7690
+ this.initializeAgent();
7691
+ }
7692
+ /**
7693
+ * Implements abstract method: build system prompt for Backlog Agent.
7694
+ */
7695
+ buildSystemPrompt() {
7696
+ return BACKLOG_AGENT_SYSTEM_PROMPT;
7697
+ }
7698
+ /**
7699
+ * Implements abstract method: create tools for Backlog Agent.
7700
+ */
7701
+ createTools() {
7702
+ return createBacklogAgentTools(this.toolsContext);
7703
+ }
7704
+ /**
7705
+ * Implements abstract method: create state manager for Backlog Agent.
7706
+ * Uses file-backed persistence for backlog state and conversation history.
7707
+ */
7708
+ createStateManager() {
7709
+ return new BacklogStateManager(this.workingDir, this.logger);
7710
+ }
7711
+ };
7712
+
7713
+ // src/infrastructure/agents/backlog-agent-manager.ts
7714
+ var BacklogAgentManager = class _BacklogAgentManager extends EventEmitter5 {
7715
+ session;
7716
+ backlog;
7717
+ harness = null;
7718
+ conversationHistory = [];
7719
+ workingDir;
7720
+ agentSessionManager;
7721
+ logger;
7722
+ llmAgent;
7723
+ selectedAgent = null;
7724
+ agentSelectionInProgress = false;
7725
+ constructor(session, backlog, workingDir, agentSessionManager, logger) {
7726
+ super();
7727
+ this.session = session;
7728
+ this.backlog = backlog;
7729
+ this.workingDir = workingDir;
7730
+ this.agentSessionManager = agentSessionManager;
7731
+ this.logger = logger;
7732
+ this.llmAgent = new BacklogAgent(
7733
+ {
7734
+ getStatus: () => this.getStatusBlocks(),
7735
+ startHarness: () => this.startHarnessCommand(),
7736
+ stopHarness: () => this.stopHarnessCommand(),
7737
+ pauseHarness: () => this.pauseHarnessCommand(),
7738
+ resumeHarness: () => this.resumeHarnessCommand(),
7739
+ listTasks: () => this.listTasksBlocks(),
7740
+ addTask: (title, description) => this.addTaskCommand({ title, description }),
7741
+ getAvailableAgents: () => this.getAvailableAgentsData(),
7742
+ parseAgentSelection: (userResponse) => this.parseAgentSelectionData(userResponse)
7743
+ },
7744
+ workingDir,
7745
+ logger
7746
+ );
7747
+ }
7748
+ /**
7749
+ * Get session info.
7750
+ */
7751
+ getSession() {
7752
+ return this.session;
7753
+ }
7754
+ /**
7755
+ * Get current backlog.
7756
+ */
7757
+ getBacklog() {
7758
+ return this.backlog;
7759
+ }
7760
+ /**
7761
+ * Update backlog from file.
7762
+ */
7763
+ updateBacklogFromFile() {
7764
+ const backlogPath = join8(this.workingDir, "backlog.json");
7765
+ if (existsSync6(backlogPath)) {
7766
+ try {
7767
+ const content = readFileSync4(backlogPath, "utf-8");
7768
+ this.backlog = BacklogSchema.parse(JSON.parse(content));
7769
+ this.logger.debug("Updated backlog from file");
7770
+ } catch (error) {
7771
+ this.logger.error({ error }, "Failed to load backlog from file");
7772
+ }
7773
+ }
7774
+ }
7775
+ /**
7776
+ * Process user command using streaming LLM execution.
7777
+ *
7778
+ * This method now uses the unified streaming mode from LangGraphAgent
7779
+ * which emits blocks in real-time to all connected clients.
7780
+ *
7781
+ * For MVP, the command processor:
7782
+ * - Parses user intent (start_harness, add_task, get_status, etc.)
7783
+ * - Returns appropriate response through LLM streaming
7784
+ * - Handles agent selection if in progress
7785
+ */
7786
+ async executeCommand(userMessage) {
7787
+ this.conversationHistory.push({ role: "user", content: userMessage });
7788
+ if (this.agentSelectionInProgress && !this.selectedAgent) {
7789
+ const selectionBlocks = this.handleAgentSelection(userMessage);
7790
+ const responseText = selectionBlocks.map((b) => b.content).join("\n");
7791
+ this.conversationHistory.push({ role: "assistant", content: responseText });
7792
+ this.saveBacklog();
7793
+ return selectionBlocks;
7794
+ }
7795
+ const streamedBlocks = [];
7796
+ const blockHandler = (_deviceId, blocks, isComplete, finalOutput) => {
7797
+ streamedBlocks.push(...blocks);
7798
+ if (isComplete && finalOutput) {
7799
+ this.conversationHistory.push({ role: "assistant", content: finalOutput });
7800
+ this.saveBacklog();
7801
+ }
7802
+ };
7803
+ this.llmAgent.on("blocks", blockHandler);
7804
+ try {
7805
+ await this.llmAgent.executeWithStream(userMessage, "backlog-manager");
7806
+ } finally {
7807
+ this.llmAgent.removeListener("blocks", blockHandler);
7808
+ }
7809
+ return streamedBlocks;
7810
+ }
7811
+ /**
7812
+ * Get status blocks for current backlog.
7813
+ */
7814
+ getStatusBlocks() {
7815
+ const summary = this.backlog.summary ?? {
7816
+ total: this.backlog.tasks.length,
7817
+ completed: 0,
7818
+ failed: 0,
7819
+ in_progress: 0,
7820
+ pending: 0
7821
+ };
7822
+ const percentage = summary.total > 0 ? Math.round(summary.completed / summary.total * 100) : 0;
7823
+ const worktreeDisplay = this.backlog.worktree ?? "main";
7824
+ const statusText = `
7825
+ \u{1F4CA} **Backlog Status**: ${this.backlog.id}
7826
+
7827
+ **Progress**: ${summary.completed}/${summary.total} tasks (${percentage}%)
7828
+
7829
+ **Breakdown**:
7830
+ - \u2705 Completed: ${summary.completed}
7831
+ - \u23F3 In Progress: ${summary.in_progress}
7832
+ - \u23F8\uFE0F Pending: ${summary.pending}
7833
+ - \u274C Failed: ${summary.failed}
7834
+
7835
+ **Agent**: ${this.backlog.agent}
7836
+ **Worktree**: ${worktreeDisplay}
7837
+
7838
+ **Harness Status**: ${this.harness ? this.session.harnessRunning ? "\u{1F7E2} Running" : "\u{1F534} Stopped" : "\u274C Not started"}
7839
+ `.trim();
7840
+ return [
7841
+ {
7842
+ id: "status",
7843
+ block_type: "text",
7844
+ content: statusText
7845
+ }
7846
+ ];
7847
+ }
7848
+ startHarnessCommand() {
7849
+ if (this.harness) {
7850
+ return [
7851
+ {
7852
+ id: "harness-already-running",
7853
+ block_type: "text",
7854
+ content: '\u26A0\uFE0F Harness is already running! Use "stop" to stop it first.'
7855
+ }
7856
+ ];
7857
+ }
7858
+ const pendingCount = this.backlog.tasks.filter((t) => t.status === "pending").length;
7859
+ const inProgressCount = this.backlog.tasks.filter((t) => t.status === "in_progress").length;
7860
+ if (pendingCount === 0 && inProgressCount === 0) {
7861
+ return [
7862
+ {
7863
+ id: "no-tasks-to-run",
7864
+ block_type: "text",
7865
+ content: '\u274C No pending tasks to execute. Add tasks first using "add task" command.'
7866
+ }
7867
+ ];
7868
+ }
7869
+ if (!this.selectedAgent && !this.agentSelectionInProgress) {
7870
+ return this.askForAgentSelection();
7871
+ }
7872
+ if (this.agentSelectionInProgress) {
7873
+ return [
7874
+ {
7875
+ id: "agent-selection-pending",
7876
+ block_type: "status",
7877
+ content: "\u23F3 Waiting for you to select an agent..."
7878
+ }
7879
+ ];
7880
+ }
7881
+ return this.createAndStartHarness();
7882
+ }
7883
+ /**
7884
+ * Ask user which agent to use for harness execution.
7885
+ */
7886
+ askForAgentSelection() {
7887
+ this.agentSelectionInProgress = true;
7888
+ const agents = Array.from(getAvailableAgents().values());
7889
+ const agentList = agents.map((a) => `\u2022 **${a.name}**${a.isAlias ? " (alias)" : ""}: ${a.description}`).join("\n");
7890
+ const question = `
7891
+ \u{1F916} **Select a coding agent** to execute the harness tasks:
7892
+
7893
+ ${agentList}
7894
+
7895
+ Please respond with the agent name you'd like to use (e.g., "claude", "cursor", or your alias name).
7896
+ `.trim();
7897
+ return [
7898
+ {
7899
+ id: "agent-selection-question",
7900
+ block_type: "text",
7901
+ content: question
7902
+ }
7903
+ ];
7904
+ }
7905
+ /**
7906
+ * Handle user's agent selection response.
7907
+ */
7908
+ handleAgentSelection(userMessage) {
7909
+ const availableAgents = getAvailableAgents();
7910
+ const agentNames = Array.from(availableAgents.keys());
7911
+ const selectedAgent = this.findBestAgentMatch(userMessage, agentNames);
7912
+ if (!selectedAgent) {
7913
+ const agentList = agentNames.map((name) => {
7914
+ const config2 = availableAgents.get(name);
7915
+ return `\u2022 **${name}**${config2?.isAlias ? " (alias)" : ""}: ${config2?.description ?? ""}`;
7916
+ }).join("\n");
7917
+ return [
7918
+ {
7919
+ id: "agent-selection-invalid",
7920
+ block_type: "text",
7921
+ content: `\u274C I didn't recognize that agent name. Here are the available options:
7922
+
7923
+ ${agentList}
7924
+
7925
+ Please try again.`
7926
+ }
7927
+ ];
7928
+ }
7929
+ this.selectedAgent = selectedAgent;
7930
+ this.agentSelectionInProgress = false;
7931
+ const selectedConfig = availableAgents.get(selectedAgent);
7932
+ const confirmation = `\u2705 Great! I'll use **${selectedAgent}**${selectedConfig?.isAlias ? " (alias)" : ""} to execute the tasks.
7933
+
7934
+ Now starting the harness...`;
7935
+ const confirmationBlocks = [
7936
+ {
7937
+ id: "agent-selection-confirmed",
7938
+ block_type: "status",
7939
+ content: confirmation
7940
+ }
7941
+ ];
7942
+ const harnessBlocks = this.createAndStartHarness();
7943
+ return [...confirmationBlocks, ...harnessBlocks];
7944
+ }
7945
+ /**
7946
+ * Find best agent match from user response using improved fuzzy matching.
7947
+ * Handles variations like "claude code", "use cursor", "my zai alias", etc.
7948
+ */
7949
+ findBestAgentMatch(userMessage, agentNames) {
7950
+ const lowerMessage = userMessage.toLowerCase();
7951
+ for (const agentName of agentNames) {
7952
+ if (lowerMessage === agentName.toLowerCase()) {
7953
+ return agentName;
7954
+ }
7955
+ }
7956
+ for (const agentName of agentNames) {
7957
+ const lowerAgent = agentName.toLowerCase();
7958
+ if (lowerMessage.includes(lowerAgent)) {
7959
+ return agentName;
7960
+ }
7961
+ }
7962
+ const commonPatterns = {
7963
+ claude: ["claude", "claude code", "claudecode", "claude-code", "claude agent"],
7964
+ cursor: ["cursor", "cursor agent", "cursoragent", "cursor-agent"],
7965
+ opencode: ["opencode", "open code", "opencode agent", "open-code"]
7966
+ };
7967
+ for (const [baseName, patterns] of Object.entries(commonPatterns)) {
7968
+ const agentName = agentNames.find((n) => n.toLowerCase() === baseName);
7969
+ if (agentName) {
7970
+ for (const pattern of patterns) {
7971
+ if (lowerMessage.includes(pattern.toLowerCase())) {
7972
+ return agentName;
7973
+ }
7974
+ }
7975
+ }
7976
+ }
7977
+ let bestMatch = null;
7978
+ const threshold = 0.6;
7979
+ for (const agentName of agentNames) {
7980
+ const similarity = this.calculateStringSimilarity(lowerMessage, agentName.toLowerCase());
7981
+ if (similarity > threshold && (!bestMatch || similarity > bestMatch.score)) {
7982
+ bestMatch = { name: agentName, score: similarity };
7983
+ }
7984
+ }
7985
+ return bestMatch ? bestMatch.name : null;
7986
+ }
7987
+ /**
7988
+ * Calculate string similarity score (0-1) using Levenshtein distance.
7989
+ */
7990
+ calculateStringSimilarity(str1, str2) {
7991
+ const maxLen = Math.max(str1.length, str2.length);
7992
+ if (maxLen === 0) return 1;
7993
+ const distance = this.levenshteinDistance(str1, str2);
7994
+ return 1 - distance / maxLen;
7995
+ }
7996
+ /**
7997
+ * Calculate Levenshtein distance between two strings.
7998
+ */
7999
+ levenshteinDistance(str1, str2) {
8000
+ const len1 = str1.length;
8001
+ const len2 = str2.length;
8002
+ const matrix = Array.from(
8003
+ { length: len1 + 1 },
8004
+ () => Array.from({ length: len2 + 1 }, () => 0)
8005
+ );
8006
+ for (let i = 0; i <= len1; i++) {
8007
+ const row = matrix[i];
8008
+ if (row) row[0] = i;
8009
+ }
8010
+ for (let j = 0; j <= len2; j++) {
8011
+ const firstRow = matrix[0];
8012
+ if (firstRow) firstRow[j] = j;
8013
+ }
8014
+ for (let i = 1; i <= len1; i++) {
8015
+ for (let j = 1; j <= len2; j++) {
8016
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
8017
+ const row = matrix[i];
8018
+ const prevRow = matrix[i - 1];
8019
+ if (row && prevRow) {
8020
+ row[j] = Math.min(
8021
+ (prevRow[j] ?? 0) + 1,
8022
+ (row[j - 1] ?? 0) + 1,
8023
+ (prevRow[j - 1] ?? 0) + cost
8024
+ );
8025
+ }
8026
+ }
8027
+ }
8028
+ return matrix[len1]?.[len2] ?? 0;
8029
+ }
8030
+ /**
8031
+ * Create and start the harness with selected agent.
8032
+ */
8033
+ createAndStartHarness() {
8034
+ if (!this.selectedAgent) {
8035
+ return [
8036
+ {
8037
+ id: "no-agent-selected",
8038
+ block_type: "error",
8039
+ content: "\u274C No agent selected. Please select an agent first."
8040
+ }
8041
+ ];
8042
+ }
8043
+ this.harness = new BacklogHarness(
8044
+ this.backlog,
8045
+ this.workingDir,
8046
+ this.selectedAgent,
8047
+ this.agentSessionManager,
8048
+ this.logger
8049
+ );
8050
+ this.harness.on("output", (blocks) => {
8051
+ this.emit("output", blocks);
8052
+ });
8053
+ this.harness.on("harness-completed", () => {
8054
+ this.selectedAgent = null;
8055
+ this.agentSelectionInProgress = false;
8056
+ });
8057
+ this.session.setHarnessRunning(true);
8058
+ void this.harness.start().catch((error) => {
8059
+ this.logger.error({ error }, "Harness error");
8060
+ this.session.setHarnessRunning(false);
8061
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
8062
+ this.emit("output", [
8063
+ {
8064
+ id: "harness-error",
8065
+ block_type: "error",
8066
+ content: `\u{1F534} Harness error: ${errorMsg}`
8067
+ }
8068
+ ]);
8069
+ });
8070
+ const worktreeDisplay = this.backlog.worktree ?? "main";
8071
+ const pendingCount = this.backlog.tasks.filter((t) => t.status === "pending").length;
8072
+ const inProgressCount = this.backlog.tasks.filter((t) => t.status === "in_progress").length;
8073
+ const tasksInfo = inProgressCount > 0 ? `${inProgressCount} task(s) in progress, ${pendingCount} pending` : `${pendingCount} task(s) to execute`;
8074
+ return [
8075
+ {
8076
+ id: "harness-started",
8077
+ block_type: "status",
8078
+ content: `\u{1F680} Harness started for ${worktreeDisplay}. ${tasksInfo}.`
8079
+ },
8080
+ {
8081
+ id: "harness-notice",
8082
+ block_type: "text",
8083
+ content: 'The harness will execute tasks sequentially. Use "status" to check progress, "pause" to pause, or "stop" to stop.'
8084
+ }
8085
+ ];
8086
+ }
8087
+ /**
8088
+ * Stop harness execution and reset agent selection.
8089
+ */
8090
+ stopHarnessCommand() {
8091
+ if (!this.harness || !this.session.harnessRunning) {
8092
+ return [
8093
+ {
8094
+ id: "harness-not-running",
8095
+ block_type: "text",
8096
+ content: "\u26A0\uFE0F Harness is not running."
8097
+ }
8098
+ ];
8099
+ }
8100
+ this.harness.stop();
8101
+ this.session.setHarnessRunning(false);
8102
+ this.selectedAgent = null;
8103
+ this.agentSelectionInProgress = false;
8104
+ return [
8105
+ {
8106
+ id: "harness-stopped",
8107
+ block_type: "status",
8108
+ content: "\u23F9\uFE0F Harness stopped. Agent selection reset."
8109
+ }
8110
+ ];
8111
+ }
8112
+ pauseHarnessCommand() {
8113
+ if (!this.harness || !this.session.harnessRunning) {
8114
+ return [
8115
+ {
8116
+ id: "harness-not-running",
8117
+ block_type: "text",
8118
+ content: "\u26A0\uFE0F Harness is not running."
8119
+ }
8120
+ ];
8121
+ }
8122
+ this.harness.pause();
8123
+ return [
8124
+ {
8125
+ id: "harness-paused",
8126
+ block_type: "status",
8127
+ content: "\u23F8\uFE0F Harness paused (current task will complete)."
8128
+ }
8129
+ ];
8130
+ }
8131
+ resumeHarnessCommand() {
8132
+ if (!this.harness) {
8133
+ return [
8134
+ {
8135
+ id: "harness-not-running",
8136
+ block_type: "text",
8137
+ content: "\u26A0\uFE0F Harness is not running."
8138
+ }
8139
+ ];
8140
+ }
8141
+ this.harness.resume();
8142
+ return [
8143
+ {
8144
+ id: "harness-resumed",
8145
+ block_type: "status",
8146
+ content: "\u25B6\uFE0F Harness resumed."
8147
+ }
8148
+ ];
8149
+ }
8150
+ /**
8151
+ * Add a new task to backlog.
8152
+ */
8153
+ addTaskCommand(params) {
8154
+ const priorityValue = params.priority;
8155
+ const complexityValue = params.complexity;
8156
+ const newTask = {
8157
+ id: Math.max(0, ...this.backlog.tasks.map((t) => t.id)) + 1,
8158
+ title: params.title ?? "Untitled",
8159
+ description: params.description ?? "",
8160
+ acceptance_criteria: params.criteria ? [params.criteria] : [],
8161
+ dependencies: [],
8162
+ priority: priorityValue ?? "medium",
8163
+ complexity: complexityValue ?? "moderate",
8164
+ status: "pending",
8165
+ retry_count: 0
8166
+ };
8167
+ this.backlog.tasks.push(newTask);
8168
+ return [
8169
+ {
8170
+ id: "task-added",
8171
+ block_type: "text",
8172
+ content: `\u2705 Task ${newTask.id} added: "${newTask.title}"`
8173
+ }
8174
+ ];
8175
+ }
8176
+ /**
8177
+ * Reorder tasks by priority/dependency.
8178
+ */
8179
+ listTasksBlocks() {
8180
+ const statusEmoji = {
8181
+ pending: "\u2B1C",
8182
+ in_progress: "\u{1F7E8}",
8183
+ completed: "\u2705",
8184
+ failed: "\u274C",
8185
+ skipped: "\u23ED\uFE0F"
8186
+ };
8187
+ const tasksList = this.backlog.tasks.map((t) => {
8188
+ const emoji = statusEmoji[t.status] ?? "\u2753";
8189
+ const status = t.status.replace("_", " ").toUpperCase();
8190
+ let line = `${emoji} **${t.id}. ${t.title}** [${status}]`;
8191
+ if (t.description) {
8192
+ line += `
8193
+ ${t.description.split("\n")[0]}`;
8194
+ }
8195
+ if (t.error) {
8196
+ line += `
8197
+ \u274C Error: ${t.error}`;
8198
+ }
8199
+ return line;
8200
+ }).join("\n\n");
8201
+ const summary = this.backlog.summary ?? {
8202
+ total: this.backlog.tasks.length,
8203
+ completed: 0,
8204
+ failed: 0,
8205
+ in_progress: 0,
8206
+ pending: this.backlog.tasks.length
8207
+ };
8208
+ const content = `
8209
+ \u{1F4CB} **Tasks in ${this.backlog.id}**
8210
+
8211
+ **Progress**: ${summary.completed}/${summary.total} completed
8212
+
8213
+ **Breakdown**:
8214
+ \u2705 Completed: ${summary.completed}
8215
+ \u{1F7E8} In Progress: ${summary.in_progress}
8216
+ \u2B1C Pending: ${summary.pending}
8217
+ \u274C Failed: ${summary.failed}
8218
+
8219
+ **Task List**:
8220
+ ${tasksList}
8221
+ `.trim();
8222
+ return [
8223
+ {
8224
+ id: "tasks-list",
8225
+ block_type: "text",
8226
+ content
8227
+ }
8228
+ ];
8229
+ }
8230
+ getAvailableAgentsData() {
8231
+ const availableAgents = getAvailableAgents();
8232
+ const agents = Array.from(availableAgents.values()).map((config2) => ({
8233
+ name: config2.name,
8234
+ description: config2.description,
8235
+ isAlias: config2.isAlias
8236
+ }));
8237
+ return Promise.resolve(agents);
8238
+ }
8239
+ parseAgentSelectionData(userResponse) {
8240
+ const availableAgents = getAvailableAgents();
8241
+ const agentNames = Array.from(availableAgents.keys());
8242
+ const selectedAgent = this.findBestAgentMatch(userResponse, agentNames);
8243
+ if (!selectedAgent) {
8244
+ const availableList = agentNames.join(", ");
8245
+ return Promise.resolve({
8246
+ agentName: null,
8247
+ valid: false,
8248
+ message: `Invalid agent. Available agents: ${availableList}`
8249
+ });
8250
+ }
8251
+ return Promise.resolve({
8252
+ agentName: selectedAgent,
8253
+ valid: true,
8254
+ message: `Selected agent: ${selectedAgent}`
8255
+ });
8256
+ }
8257
+ saveBacklog() {
8258
+ const backlogPath = join8(this.workingDir, "backlog.json");
8259
+ try {
8260
+ writeFileSync3(backlogPath, JSON.stringify(this.backlog, null, 2));
8261
+ this.logger.debug({ backlogPath }, "Saved backlog");
8262
+ } catch (error) {
8263
+ this.logger.error({ error }, "Failed to save backlog");
8264
+ }
8265
+ }
8266
+ /**
8267
+ * Create a new BacklogAgentManager for manual input.
8268
+ */
8269
+ static createEmpty(session, workingDir, agentSessionManager, logger) {
8270
+ const agentType = session.agentName;
8271
+ const backlog = {
8272
+ id: session.backlogId,
8273
+ project: workingDir.split("/").pop() ?? "project",
8274
+ worktree: session.workspacePath?.worktree,
8275
+ agent: agentType,
8276
+ source: { type: "manual" },
8277
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
8278
+ tasks: [],
8279
+ summary: { total: 0, completed: 0, failed: 0, in_progress: 0, pending: 0 }
8280
+ };
8281
+ return new _BacklogAgentManager(session, backlog, workingDir, agentSessionManager, logger);
8282
+ }
8283
+ /**
8284
+ * Creates a manager and attempts to load backlog from file.
8285
+ * If backlog.json doesn't exist, starts with empty backlog.
8286
+ * Used during session restoration to load persisted backlog state.
8287
+ */
8288
+ static createAndLoadFromFile(session, workingDir, agentSessionManager, logger) {
8289
+ const manager = _BacklogAgentManager.createEmpty(session, workingDir, agentSessionManager, logger);
8290
+ manager.updateBacklogFromFile();
8291
+ return manager;
8292
+ }
8293
+ };
8294
+
8295
+ // src/infrastructure/persistence/in-memory-session-manager.ts
8296
+ var InMemorySessionManager = class extends EventEmitter6 {
8297
+ sessions = /* @__PURE__ */ new Map();
8298
+ ptyManager;
8299
+ agentSessionManager;
8300
+ logger;
8301
+ supervisorSession = null;
8302
+ backlogManagers;
8303
+ sessionRepository;
8304
+ constructor(config2) {
8305
+ super();
8306
+ this.ptyManager = config2.ptyManager;
8307
+ this.agentSessionManager = config2.agentSessionManager;
8308
+ this.logger = config2.logger.child({ component: "session-manager" });
8309
+ this.backlogManagers = config2.backlogManagers ?? /* @__PURE__ */ new Map();
8310
+ this.sessionRepository = config2.sessionRepository;
8311
+ this.setupAgentSessionSync();
8312
+ }
8313
+ /**
8314
+ * Restores persisted sessions from the database on startup.
8315
+ */
8316
+ restoreSessions() {
8317
+ this.logger.info("restoreSessions() called");
8318
+ if (!this.sessionRepository) {
8319
+ this.logger.warn("No session repository - skipping session restoration");
8320
+ return Promise.resolve();
8321
+ }
8322
+ let persistedSessions = [];
8323
+ try {
8324
+ persistedSessions = this.sessionRepository.getActive();
8325
+ this.logger.info(
8326
+ { count: persistedSessions.length, sessions: persistedSessions.map((s) => ({ id: s.id, type: s.type })) },
8327
+ "Restoring persisted sessions from database"
8328
+ );
8329
+ if (persistedSessions.length === 0) {
8330
+ this.logger.info("No persisted sessions found in database");
8331
+ return Promise.resolve();
8332
+ }
8333
+ } catch (dbError) {
8334
+ this.logger.error(
8335
+ { error: dbError instanceof Error ? dbError.message : String(dbError) },
8336
+ "Error fetching sessions from database"
8337
+ );
8338
+ return Promise.resolve();
8339
+ }
8340
+ let backlogRestoreCount = 0;
8341
+ for (const persistedSession of persistedSessions) {
8342
+ try {
8343
+ if (persistedSession.type === "backlog-agent") {
8344
+ this.logger.info(
8345
+ { sessionId: persistedSession.id, project: persistedSession.project, workingDir: persistedSession.workingDir },
8346
+ "Starting restoration of backlog-agent session"
8347
+ );
8348
+ const backlogPath = join9(persistedSession.workingDir, "backlog.json");
8349
+ let agentName = "claude";
8350
+ if (existsSync7(backlogPath)) {
8351
+ try {
8352
+ const backlogContent = readFileSync5(backlogPath, "utf-8");
8353
+ const backlogData = JSON.parse(backlogContent);
8354
+ if (backlogData.agent && ["claude", "cursor", "opencode"].includes(backlogData.agent)) {
8355
+ agentName = backlogData.agent;
8356
+ }
8357
+ this.logger.debug(
8358
+ { sessionId: persistedSession.id, agentName },
8359
+ "Loaded agent type from backlog.json"
8360
+ );
8361
+ } catch (parseError) {
8362
+ this.logger.warn(
8363
+ { sessionId: persistedSession.id, error: parseError },
8364
+ "Failed to parse backlog.json, using default agent"
8365
+ );
8366
+ }
8367
+ }
8368
+ const backlogSession = new BacklogAgentSession({
8369
+ id: new SessionId(persistedSession.id),
8370
+ type: "backlog-agent",
8371
+ workspacePath: persistedSession.workspace ? new WorkspacePath(
8372
+ persistedSession.workspace,
8373
+ persistedSession.project ?? void 0,
8374
+ persistedSession.worktree ?? void 0
8375
+ ) : void 0,
8376
+ workingDir: persistedSession.workingDir,
8377
+ agentName,
8378
+ backlogId: `${persistedSession.project ?? "unknown"}-${Date.now()}`
8379
+ });
8380
+ this.sessions.set(persistedSession.id, backlogSession);
8381
+ try {
8382
+ const manager = BacklogAgentManager.createAndLoadFromFile(
8383
+ backlogSession,
8384
+ persistedSession.workingDir,
8385
+ this.agentSessionManager,
8386
+ this.logger
8387
+ );
8388
+ this.backlogManagers.set(persistedSession.id, manager);
8389
+ backlogRestoreCount++;
8390
+ this.logger.info(
8391
+ {
8392
+ sessionId: persistedSession.id,
8393
+ project: persistedSession.project,
8394
+ backlogManagersCount: this.backlogManagers.size,
8395
+ isInMap: this.backlogManagers.has(persistedSession.id)
8396
+ },
8397
+ "Successfully restored backlog-agent session from database"
8398
+ );
8399
+ } catch (managerError) {
8400
+ this.logger.error(
8401
+ { sessionId: persistedSession.id, error: managerError instanceof Error ? managerError.message : String(managerError), stack: managerError instanceof Error ? managerError.stack : void 0 },
8402
+ "Failed to create backlog manager during restoration"
8403
+ );
8404
+ throw managerError;
8405
+ }
8406
+ } else if (persistedSession.type === "supervisor") {
8407
+ this.logger.debug({ sessionId: persistedSession.id }, "Skipping supervisor session restoration (singleton)");
8408
+ } else if (persistedSession.type === "terminal") {
8409
+ this.logger.debug({ sessionId: persistedSession.id }, "Skipping terminal session restoration (requires PTY)");
8410
+ }
8411
+ } catch (error) {
8412
+ this.logger.error(
8413
+ { sessionId: persistedSession.id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 },
8414
+ "Error restoring session from database"
8415
+ );
8416
+ }
8417
+ }
8418
+ this.logger.info(
8419
+ {
8420
+ backlogManagersRestored: backlogRestoreCount,
8421
+ totalBacklogManagers: this.backlogManagers.size,
8422
+ backlogManagerIds: Array.from(this.backlogManagers.keys())
8423
+ },
8424
+ "Session restoration complete"
8425
+ );
8426
+ return Promise.resolve();
8427
+ }
8428
+ /**
8429
+ * Sets up event listeners to sync agent session state.
8430
+ */
8431
+ setupAgentSessionSync() {
8432
+ this.agentSessionManager.on("sessionCreated", (state) => {
8433
+ if (!this.sessions.has(state.sessionId)) {
8434
+ const session = new AgentSession({
8435
+ id: new SessionId(state.sessionId),
8436
+ type: state.agentType,
8437
+ agentName: state.agentName,
8438
+ workingDir: state.workingDir
8439
+ });
8440
+ this.sessions.set(state.sessionId, session);
8441
+ this.logger.debug(
8442
+ { sessionId: state.sessionId, agentType: state.agentType },
6453
8443
  "Agent session registered from external creation"
6454
8444
  );
6455
8445
  }
@@ -6473,7 +8463,7 @@ var InMemorySessionManager = class extends EventEmitter3 {
6473
8463
  * Creates a new session.
6474
8464
  */
6475
8465
  async createSession(params) {
6476
- const { sessionType, workingDir, terminalSize, agentName } = params;
8466
+ const { sessionType, workingDir, terminalSize, agentName, backlogAgent, backlogId } = params;
6477
8467
  if (sessionType === "supervisor") {
6478
8468
  return this.getOrCreateSupervisor(workingDir);
6479
8469
  }
@@ -6482,6 +8472,16 @@ var InMemorySessionManager = class extends EventEmitter3 {
6482
8472
  const rows = terminalSize?.rows ?? SESSION_CONFIG.DEFAULT_TERMINAL_ROWS;
6483
8473
  const session = await this.ptyManager.create(workingDir, cols, rows);
6484
8474
  this.sessions.set(session.id.value, session);
8475
+ if (this.sessionRepository) {
8476
+ this.sessionRepository.create({
8477
+ id: session.id.value,
8478
+ type: "terminal",
8479
+ workspace: params.workspacePath?.workspace,
8480
+ project: params.workspacePath?.project,
8481
+ worktree: params.workspacePath?.worktree,
8482
+ workingDir
8483
+ });
8484
+ }
6485
8485
  this.logger.info(
6486
8486
  { sessionId: session.id.value, sessionType, workingDir },
6487
8487
  "Terminal session created"
@@ -6489,6 +8489,9 @@ var InMemorySessionManager = class extends EventEmitter3 {
6489
8489
  this.emit("terminalSessionCreated", session);
6490
8490
  return session;
6491
8491
  }
8492
+ if (sessionType === "backlog-agent") {
8493
+ return this.createBacklogSession(workingDir, backlogAgent ?? "claude", backlogId, params.workspacePath);
8494
+ }
6492
8495
  if (isAgentType(sessionType)) {
6493
8496
  return this.createAgentSession(sessionType, workingDir, agentName, params.workspacePath);
6494
8497
  }
@@ -6521,6 +8524,16 @@ var InMemorySessionManager = class extends EventEmitter3 {
6521
8524
  cliSessionId: agentState.cliSessionId
6522
8525
  });
6523
8526
  this.sessions.set(sessionId.value, session);
8527
+ if (this.sessionRepository) {
8528
+ this.sessionRepository.create({
8529
+ id: sessionId.value,
8530
+ type: agentName ?? agentType,
8531
+ workspace: workspacePath?.workspace,
8532
+ project: workspacePath?.project,
8533
+ worktree: workspacePath?.worktree,
8534
+ workingDir
8535
+ });
8536
+ }
6524
8537
  this.logger.info(
6525
8538
  { sessionId: sessionId.value, agentType, agentName: resolvedAgentName, workingDir, workspacePath },
6526
8539
  "Agent session created"
@@ -6541,6 +8554,13 @@ var InMemorySessionManager = class extends EventEmitter3 {
6541
8554
  workingDir
6542
8555
  });
6543
8556
  this.sessions.set(sessionId.value, this.supervisorSession);
8557
+ if (this.sessionRepository) {
8558
+ this.sessionRepository.create({
8559
+ id: sessionId.value,
8560
+ type: "supervisor",
8561
+ workingDir
8562
+ });
8563
+ }
6544
8564
  this.logger.info(
6545
8565
  { sessionId: sessionId.value },
6546
8566
  "Supervisor session created"
@@ -6648,19 +8668,111 @@ var InMemorySessionManager = class extends EventEmitter3 {
6648
8668
  getAgentSessionManager() {
6649
8669
  return this.agentSessionManager;
6650
8670
  }
8671
+ /**
8672
+ * Creates a backlog agent session.
8673
+ */
8674
+ createBacklogSession(workingDir, backlogAgent, backlogId, workspacePath) {
8675
+ const sessionId = new SessionId(`backlog-${nanoid3(8)}`);
8676
+ const finalBacklogId = backlogId ?? `backlog-${nanoid3(8)}`;
8677
+ const session = new BacklogAgentSession({
8678
+ id: sessionId,
8679
+ type: "backlog-agent",
8680
+ workspacePath,
8681
+ workingDir,
8682
+ agentName: backlogAgent,
8683
+ backlogId: finalBacklogId
8684
+ });
8685
+ this.sessions.set(sessionId.value, session);
8686
+ const manager = BacklogAgentManager.createEmpty(
8687
+ session,
8688
+ workingDir,
8689
+ this.agentSessionManager,
8690
+ this.logger
8691
+ );
8692
+ this.backlogManagers.set(sessionId.value, manager);
8693
+ this.logger.debug(
8694
+ {
8695
+ sessionId: sessionId.value,
8696
+ backlogManagersCount: this.backlogManagers.size
8697
+ },
8698
+ "BacklogAgentManager registered"
8699
+ );
8700
+ if (this.sessionRepository) {
8701
+ this.sessionRepository.create({
8702
+ id: sessionId.value,
8703
+ type: "backlog-agent",
8704
+ workspace: workspacePath?.workspace,
8705
+ project: workspacePath?.project,
8706
+ worktree: workspacePath?.worktree,
8707
+ workingDir
8708
+ });
8709
+ }
8710
+ this.logger.info(
8711
+ {
8712
+ sessionId: sessionId.value,
8713
+ backlogId: finalBacklogId,
8714
+ backlogAgent,
8715
+ workingDir,
8716
+ workspacePath
8717
+ },
8718
+ "Backlog agent session created"
8719
+ );
8720
+ return session;
8721
+ }
8722
+ /**
8723
+ * Gets the backlog managers registry.
8724
+ */
8725
+ getBacklogManagers() {
8726
+ return this.backlogManagers;
8727
+ }
6651
8728
  };
6652
8729
 
6653
- // src/infrastructure/agents/supervisor/supervisor-agent.ts
6654
- import { EventEmitter as EventEmitter4 } from "events";
6655
- import { ChatOpenAI } from "@langchain/openai";
6656
- import { createReactAgent } from "@langchain/langgraph/prebuilt";
6657
- import { HumanMessage, AIMessage, isAIMessage } from "@langchain/core/messages";
8730
+ // src/infrastructure/agents/supervisor/supervisor-state-manager.ts
8731
+ var SupervisorStateManager = class {
8732
+ constructor(chatHistoryService) {
8733
+ this.chatHistoryService = chatHistoryService;
8734
+ }
8735
+ loadHistory() {
8736
+ const history = this.chatHistoryService.getSupervisorHistory();
8737
+ const entries = history.map((entry) => ({
8738
+ role: entry.role,
8739
+ content: entry.content,
8740
+ timestamp: new Date(entry.createdAt).getTime()
8741
+ }));
8742
+ return Promise.resolve(entries);
8743
+ }
8744
+ saveHistory(history) {
8745
+ if (history.length === 0) return Promise.resolve();
8746
+ const lastEntry = history[history.length - 1];
8747
+ if (!lastEntry) return Promise.resolve();
8748
+ if (lastEntry.role === "system") return Promise.resolve();
8749
+ const storedHistory = this.chatHistoryService.getSupervisorHistory(1);
8750
+ const mostRecentStored = storedHistory[0];
8751
+ if (!mostRecentStored || mostRecentStored.content !== lastEntry.content) {
8752
+ this.chatHistoryService.saveSupervisorMessage(lastEntry.role, lastEntry.content);
8753
+ }
8754
+ return Promise.resolve();
8755
+ }
8756
+ clearHistory() {
8757
+ this.chatHistoryService.clearSupervisorHistory();
8758
+ return Promise.resolve();
8759
+ }
8760
+ loadAdditionalState(_key) {
8761
+ return Promise.resolve(null);
8762
+ }
8763
+ saveAdditionalState(_key, _state) {
8764
+ return Promise.resolve();
8765
+ }
8766
+ close() {
8767
+ return Promise.resolve();
8768
+ }
8769
+ };
6658
8770
 
6659
- // src/infrastructure/agents/supervisor/tools/workspace-tools.ts
6660
- import { tool } from "@langchain/core/tools";
6661
- import { z as z3 } from "zod";
8771
+ // src/infrastructure/agents/supervisor/tools/workspace-tools.ts
8772
+ import { tool as tool2 } from "@langchain/core/tools";
8773
+ import { z as z5 } from "zod";
6662
8774
  function createWorkspaceTools(workspaceDiscovery) {
6663
- const listWorkspaces = tool(
8775
+ const listWorkspaces = tool2(
6664
8776
  async () => {
6665
8777
  const workspaces = await workspaceDiscovery.listWorkspaces();
6666
8778
  if (workspaces.length === 0) {
@@ -6672,10 +8784,10 @@ ${workspaces.map((w) => `- ${w.name} (${w.projectCount} projects)`).join("\n")}`
6672
8784
  {
6673
8785
  name: "list_workspaces",
6674
8786
  description: "Lists all available workspaces (top-level directories in the workspaces root). Use this to discover what workspaces exist.",
6675
- schema: z3.object({})
8787
+ schema: z5.object({})
6676
8788
  }
6677
8789
  );
6678
- const listProjects = tool(
8790
+ const listProjects = tool2(
6679
8791
  async ({ workspace }) => {
6680
8792
  try {
6681
8793
  const projects = await workspaceDiscovery.listProjects(workspace);
@@ -6691,12 +8803,12 @@ ${projects.map((p) => `- ${p.name}${p.isGitRepo ? " (git)" : ""}`).join("\n")}`;
6691
8803
  {
6692
8804
  name: "list_projects",
6693
8805
  description: "Lists all projects in a specific workspace. Projects are git repositories or directories.",
6694
- schema: z3.object({
6695
- workspace: z3.string().describe("Name of the workspace to list projects from")
8806
+ schema: z5.object({
8807
+ workspace: z5.string().describe("Name of the workspace to list projects from")
6696
8808
  })
6697
8809
  }
6698
8810
  );
6699
- const getProjectInfo = tool(
8811
+ const getProjectInfo = tool2(
6700
8812
  async ({ workspace, project }) => {
6701
8813
  try {
6702
8814
  const projectInfo = await workspaceDiscovery.getProject(workspace, project);
@@ -6717,13 +8829,13 @@ Default Branch: ${projectInfo.defaultBranch}` : ""}${worktreeInfo}`;
6717
8829
  {
6718
8830
  name: "get_project_info",
6719
8831
  description: "Gets detailed information about a project including its path and any git worktrees.",
6720
- schema: z3.object({
6721
- workspace: z3.string().describe("Name of the workspace"),
6722
- project: z3.string().describe("Name of the project")
8832
+ schema: z5.object({
8833
+ workspace: z5.string().describe("Name of the workspace"),
8834
+ project: z5.string().describe("Name of the project")
6723
8835
  })
6724
8836
  }
6725
8837
  );
6726
- const createWorkspace = tool(
8838
+ const createWorkspace = tool2(
6727
8839
  async ({ name }) => {
6728
8840
  try {
6729
8841
  const workspace = await workspaceDiscovery.createWorkspace(name);
@@ -6735,15 +8847,15 @@ Default Branch: ${projectInfo.defaultBranch}` : ""}${worktreeInfo}`;
6735
8847
  {
6736
8848
  name: "create_workspace",
6737
8849
  description: 'Creates a new workspace directory. The name must be in lower-kebab-case (e.g., "my-company", "personal-projects").',
6738
- schema: z3.object({
6739
- name: z3.string().regex(
8850
+ schema: z5.object({
8851
+ name: z5.string().regex(
6740
8852
  /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
6741
8853
  'Name must be in lower-kebab-case (e.g., "my-workspace")'
6742
8854
  ).describe("Name for the new workspace in lower-kebab-case")
6743
8855
  })
6744
8856
  }
6745
8857
  );
6746
- const createProject = tool(
8858
+ const createProject = tool2(
6747
8859
  async ({
6748
8860
  workspace,
6749
8861
  name,
@@ -6761,13 +8873,13 @@ Path: ${project.path}`;
6761
8873
  {
6762
8874
  name: "create_project",
6763
8875
  description: "Creates a new project directory within a workspace. The name must be in lower-kebab-case. By default, initializes a git repository.",
6764
- schema: z3.object({
6765
- workspace: z3.string().describe("Name of the workspace to create the project in"),
6766
- name: z3.string().regex(
8876
+ schema: z5.object({
8877
+ workspace: z5.string().describe("Name of the workspace to create the project in"),
8878
+ name: z5.string().regex(
6767
8879
  /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
6768
8880
  'Name must be in lower-kebab-case (e.g., "my-project")'
6769
8881
  ).describe("Name for the new project in lower-kebab-case"),
6770
- init_git: z3.boolean().optional().default(true).describe("Whether to initialize a git repository (default: true)")
8882
+ init_git: z5.boolean().optional().default(true).describe("Whether to initialize a git repository (default: true)")
6771
8883
  })
6772
8884
  }
6773
8885
  );
@@ -6775,10 +8887,10 @@ Path: ${project.path}`;
6775
8887
  }
6776
8888
 
6777
8889
  // src/infrastructure/agents/supervisor/tools/worktree-tools.ts
6778
- import { tool as tool2 } from "@langchain/core/tools";
6779
- import { z as z4 } from "zod";
8890
+ import { tool as tool3 } from "@langchain/core/tools";
8891
+ import { z as z6 } from "zod";
6780
8892
  function createWorktreeTools(workspaceDiscovery, _agentSessionManager) {
6781
- const listWorktrees = tool2(
8893
+ const listWorktrees = tool3(
6782
8894
  async ({ workspace, project }) => {
6783
8895
  try {
6784
8896
  const worktrees = await workspaceDiscovery.listWorktrees(workspace, project);
@@ -6794,13 +8906,13 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
6794
8906
  {
6795
8907
  name: "list_worktrees",
6796
8908
  description: "Lists all git worktrees for a specific project. Worktrees allow working on multiple branches simultaneously.",
6797
- schema: z4.object({
6798
- workspace: z4.string().describe("Name of the workspace"),
6799
- project: z4.string().describe("Name of the project (git repository)")
8909
+ schema: z6.object({
8910
+ workspace: z6.string().describe("Name of the workspace"),
8911
+ project: z6.string().describe("Name of the project (git repository)")
6800
8912
  })
6801
8913
  }
6802
8914
  );
6803
- const createWorktree = tool2(
8915
+ const createWorktree = tool3(
6804
8916
  async ({
6805
8917
  workspace,
6806
8918
  project,
@@ -6825,16 +8937,16 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
6825
8937
  {
6826
8938
  name: "create_worktree",
6827
8939
  description: "Creates a new git worktree for a project. Use this to work on a different branch without switching. Can either checkout an existing branch or create a new branch.",
6828
- schema: z4.object({
6829
- workspace: z4.string().describe("Name of the workspace"),
6830
- project: z4.string().describe("Name of the project (git repository)"),
6831
- branch: z4.string().describe("Branch name to use in the worktree"),
6832
- createNewBranch: z4.boolean().optional().describe("If true, creates a new branch with the given name. If false or omitted, checks out an existing branch."),
6833
- baseBranch: z4.string().optional().describe('When creating a new branch, specifies the starting point (e.g., "main", "develop"). Defaults to current HEAD if not specified.')
8940
+ schema: z6.object({
8941
+ workspace: z6.string().describe("Name of the workspace"),
8942
+ project: z6.string().describe("Name of the project (git repository)"),
8943
+ branch: z6.string().describe("Branch name to use in the worktree"),
8944
+ createNewBranch: z6.boolean().optional().describe("If true, creates a new branch with the given name. If false or omitted, checks out an existing branch."),
8945
+ baseBranch: z6.string().optional().describe('When creating a new branch, specifies the starting point (e.g., "main", "develop"). Defaults to current HEAD if not specified.')
6834
8946
  })
6835
8947
  }
6836
8948
  );
6837
- const removeWorktree = tool2(
8949
+ const removeWorktree = tool3(
6838
8950
  async ({
6839
8951
  workspace,
6840
8952
  project,
@@ -6850,14 +8962,14 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
6850
8962
  {
6851
8963
  name: "remove_worktree",
6852
8964
  description: "Removes a git worktree from a project. This deletes the worktree directory.",
6853
- schema: z4.object({
6854
- workspace: z4.string().describe("Name of the workspace"),
6855
- project: z4.string().describe("Name of the project"),
6856
- worktreeName: z4.string().describe("Name of the worktree to remove")
8965
+ schema: z6.object({
8966
+ workspace: z6.string().describe("Name of the workspace"),
8967
+ project: z6.string().describe("Name of the project"),
8968
+ worktreeName: z6.string().describe("Name of the worktree to remove")
6857
8969
  })
6858
8970
  }
6859
8971
  );
6860
- const branchStatus = tool2(
8972
+ const branchStatus = tool3(
6861
8973
  async ({ workspace, project }) => {
6862
8974
  try {
6863
8975
  const status = await workspaceDiscovery.getBranchStatus(workspace, project);
@@ -6876,13 +8988,13 @@ ${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
6876
8988
  {
6877
8989
  name: "branch_status",
6878
8990
  description: "Get current branch status including uncommitted changes and commit count ahead of main",
6879
- schema: z4.object({
6880
- workspace: z4.string().describe("Workspace name containing the project"),
6881
- project: z4.string().describe("Project name to get branch status for")
8991
+ schema: z6.object({
8992
+ workspace: z6.string().describe("Workspace name containing the project"),
8993
+ project: z6.string().describe("Project name to get branch status for")
6882
8994
  })
6883
8995
  }
6884
8996
  );
6885
- const mergeBranch = tool2(
8997
+ const mergeBranch = tool3(
6886
8998
  async ({ workspace, project, sourceBranch, targetBranch, pushAfter, skipPreCheck }) => {
6887
8999
  try {
6888
9000
  const result = await workspaceDiscovery.mergeBranch(
@@ -6903,17 +9015,17 @@ ${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
6903
9015
  {
6904
9016
  name: "merge_branch",
6905
9017
  description: "Merge source branch into target branch with safety checks and optional push to remote",
6906
- schema: z4.object({
6907
- workspace: z4.string().describe("Workspace name"),
6908
- project: z4.string().describe("Project name"),
6909
- sourceBranch: z4.string().describe("Source branch to merge from"),
6910
- targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
6911
- pushAfter: z4.boolean().optional().describe("Push to remote after successful merge"),
6912
- skipPreCheck: z4.boolean().optional().describe("Skip pre-merge safety checks (not recommended)")
9018
+ schema: z6.object({
9019
+ workspace: z6.string().describe("Workspace name"),
9020
+ project: z6.string().describe("Project name"),
9021
+ sourceBranch: z6.string().describe("Source branch to merge from"),
9022
+ targetBranch: z6.string().optional().describe("Target branch (defaults to main)"),
9023
+ pushAfter: z6.boolean().optional().describe("Push to remote after successful merge"),
9024
+ skipPreCheck: z6.boolean().optional().describe("Skip pre-merge safety checks (not recommended)")
6913
9025
  })
6914
9026
  }
6915
9027
  );
6916
- const listMergeableBranches = tool2(
9028
+ const listMergeableBranches = tool3(
6917
9029
  async ({ workspace, project }) => {
6918
9030
  try {
6919
9031
  const branches = await workspaceDiscovery.listMergeableBranches(workspace, project);
@@ -6942,13 +9054,13 @@ ${statusLines.join("\n")}`;
6942
9054
  {
6943
9055
  name: "list_mergeable_branches",
6944
9056
  description: "List all feature branches and their merge/cleanup status",
6945
- schema: z4.object({
6946
- workspace: z4.string().describe("Workspace name"),
6947
- project: z4.string().describe("Project name")
9057
+ schema: z6.object({
9058
+ workspace: z6.string().describe("Workspace name"),
9059
+ project: z6.string().describe("Project name")
6948
9060
  })
6949
9061
  }
6950
9062
  );
6951
- const cleanupWorktree = tool2(
9063
+ const cleanupWorktree = tool3(
6952
9064
  async ({ workspace, project, branch, force }) => {
6953
9065
  try {
6954
9066
  if (!force) {
@@ -6978,15 +9090,15 @@ ${statusLines.join("\n")}`;
6978
9090
  {
6979
9091
  name: "cleanup_worktree",
6980
9092
  description: "Remove worktree and optionally delete the branch if merged",
6981
- schema: z4.object({
6982
- workspace: z4.string().describe("Workspace name"),
6983
- project: z4.string().describe("Project name"),
6984
- branch: z4.string().describe("Branch/worktree name to cleanup"),
6985
- force: z4.boolean().optional().describe("Force cleanup even with uncommitted changes")
9093
+ schema: z6.object({
9094
+ workspace: z6.string().describe("Workspace name"),
9095
+ project: z6.string().describe("Project name"),
9096
+ branch: z6.string().describe("Branch/worktree name to cleanup"),
9097
+ force: z6.boolean().optional().describe("Force cleanup even with uncommitted changes")
6986
9098
  })
6987
9099
  }
6988
9100
  );
6989
- const completeFeature = tool2(
9101
+ const completeFeature = tool3(
6990
9102
  async ({ workspace, project, featureBranch, targetBranch, skipConfirmation }) => {
6991
9103
  try {
6992
9104
  const results = [];
@@ -7055,12 +9167,12 @@ Steps executed:
7055
9167
  {
7056
9168
  name: "complete_feature",
7057
9169
  description: "Complete workflow: merge feature branch into main and cleanup worktree/branch",
7058
- schema: z4.object({
7059
- workspace: z4.string().describe("Workspace name"),
7060
- project: z4.string().describe("Project name"),
7061
- featureBranch: z4.string().describe("Feature branch name to complete"),
7062
- targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
7063
- skipConfirmation: z4.boolean().optional().describe("Skip safety checks (not recommended)")
9170
+ schema: z6.object({
9171
+ workspace: z6.string().describe("Workspace name"),
9172
+ project: z6.string().describe("Project name"),
9173
+ featureBranch: z6.string().describe("Feature branch name to complete"),
9174
+ targetBranch: z6.string().optional().describe("Target branch (defaults to main)"),
9175
+ skipConfirmation: z6.boolean().optional().describe("Skip safety checks (not recommended)")
7064
9176
  })
7065
9177
  }
7066
9178
  );
@@ -7077,10 +9189,10 @@ Steps executed:
7077
9189
  }
7078
9190
 
7079
9191
  // src/infrastructure/agents/supervisor/tools/session-tools.ts
7080
- import { tool as tool3 } from "@langchain/core/tools";
7081
- import { z as z5 } from "zod";
9192
+ import { tool as tool4 } from "@langchain/core/tools";
9193
+ import { z as z7 } from "zod";
7082
9194
  function createSessionTools(sessionManager, agentSessionManager, workspaceDiscovery, workspacesRoot, _getMessageBroadcaster, getChatHistoryService, clearSupervisorContext, terminateSessionCallback) {
7083
- const listSessions = tool3(
9195
+ const listSessions = tool4(
7084
9196
  () => {
7085
9197
  const chatHistoryService = getChatHistoryService?.();
7086
9198
  const inMemorySessions = sessionManager.getAllSessions();
@@ -7108,10 +9220,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
7108
9220
  {
7109
9221
  name: "list_sessions",
7110
9222
  description: "Lists all active sessions (terminals, agents). Use this to see what sessions are running.",
7111
- schema: z5.object({})
9223
+ schema: z7.object({})
7112
9224
  }
7113
9225
  );
7114
- const listAvailableAgents = tool3(
9226
+ const listAvailableAgents = tool4(
7115
9227
  () => {
7116
9228
  const agents = getAvailableAgents();
7117
9229
  if (agents.size === 0) {
@@ -7127,10 +9239,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
7127
9239
  {
7128
9240
  name: "list_available_agents",
7129
9241
  description: "Lists all available AI agent types, including base agents (cursor, claude, opencode) and custom aliases configured via AGENT_ALIAS_* environment variables.",
7130
- schema: z5.object({})
9242
+ schema: z7.object({})
7131
9243
  }
7132
9244
  );
7133
- const createAgentSession = tool3(
9245
+ const createAgentSession = tool4(
7134
9246
  async ({
7135
9247
  agentName,
7136
9248
  workspace,
@@ -7143,7 +9255,10 @@ ${allSessions.map((s) => `- [${s.type}] ${s.id} (${s.status}) - ${s.workingDir}`
7143
9255
  const available = Array.from(getAvailableAgents().keys()).join(", ");
7144
9256
  return `Error: Unknown agent "${agentName}". Available agents: ${available}`;
7145
9257
  }
7146
- const normalizedWorktree = worktree === "main" ? void 0 : worktree;
9258
+ let normalizedWorktree = worktree;
9259
+ if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
9260
+ normalizedWorktree = void 0;
9261
+ }
7147
9262
  const workingDir = workspaceDiscovery.resolvePath(workspace, project, normalizedWorktree);
7148
9263
  const exists = await workspaceDiscovery.pathExists(workingDir);
7149
9264
  if (!exists) {
@@ -7164,22 +9279,25 @@ Working directory: ${session.workingDir}`;
7164
9279
  {
7165
9280
  name: "create_agent_session",
7166
9281
  description: "Creates a new AI agent session in a specific project. Supports base agents (cursor, claude, opencode) and custom aliases configured via AGENT_ALIAS_* environment variables. Use list_available_agents to see all available options.",
7167
- schema: z5.object({
7168
- agentName: z5.string().describe('Name of the agent to start (e.g., "claude", "cursor", "opencode", or a custom alias like "zai")'),
7169
- workspace: z5.string().describe("Name of the workspace"),
7170
- project: z5.string().describe("Name of the project"),
7171
- worktree: z5.string().optional().describe("Optional worktree name")
9282
+ schema: z7.object({
9283
+ agentName: z7.string().describe('Name of the agent to start (e.g., "claude", "cursor", "opencode", or a custom alias like "zai")'),
9284
+ workspace: z7.string().describe("Name of the workspace"),
9285
+ project: z7.string().describe("Name of the project"),
9286
+ worktree: z7.string().optional().describe("Optional worktree name")
7172
9287
  })
7173
9288
  }
7174
9289
  );
7175
- const createTerminalSession = tool3(
9290
+ const createTerminalSession = tool4(
7176
9291
  async ({
7177
9292
  workspace,
7178
9293
  project,
7179
9294
  worktree
7180
9295
  }) => {
7181
9296
  try {
7182
- const normalizedWorktree = worktree === "main" ? void 0 : worktree;
9297
+ let normalizedWorktree = worktree;
9298
+ if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
9299
+ normalizedWorktree = void 0;
9300
+ }
7183
9301
  let workingDir;
7184
9302
  if (workspace && project) {
7185
9303
  workingDir = workspaceDiscovery.resolvePath(workspace, project, normalizedWorktree);
@@ -7211,14 +9329,14 @@ Working directory: ${session.workingDir}`;
7211
9329
  - workspace + project + worktree: opens terminal in that worktree directory
7212
9330
 
7213
9331
  Use this tool when user asks to "open terminal", "create terminal", or similar requests.`,
7214
- schema: z5.object({
7215
- workspace: z5.string().optional().describe("Name of the workspace. Omit to open in workspaces root."),
7216
- project: z5.string().optional().describe("Name of the project within the workspace. Requires workspace."),
7217
- worktree: z5.string().optional().describe("Worktree name. Requires workspace and project.")
9332
+ schema: z7.object({
9333
+ workspace: z7.string().optional().describe("Name of the workspace. Omit to open in workspaces root."),
9334
+ project: z7.string().optional().describe("Name of the project within the workspace. Requires workspace."),
9335
+ worktree: z7.string().optional().describe("Worktree name. Requires workspace and project.")
7218
9336
  })
7219
9337
  }
7220
9338
  );
7221
- const terminateSession = tool3(
9339
+ const terminateSession = tool4(
7222
9340
  async ({ sessionId }) => {
7223
9341
  try {
7224
9342
  if (!terminateSessionCallback) {
@@ -7236,12 +9354,12 @@ Use this tool when user asks to "open terminal", "create terminal", or similar r
7236
9354
  {
7237
9355
  name: "terminate_session",
7238
9356
  description: "Terminates an active session by its ID.",
7239
- schema: z5.object({
7240
- sessionId: z5.string().describe("ID of the session to terminate")
9357
+ schema: z7.object({
9358
+ sessionId: z7.string().describe("ID of the session to terminate")
7241
9359
  })
7242
9360
  }
7243
9361
  );
7244
- const terminateAllSessions = tool3(
9362
+ const terminateAllSessions = tool4(
7245
9363
  async ({ sessionType }) => {
7246
9364
  try {
7247
9365
  if (!terminateSessionCallback) {
@@ -7294,12 +9412,12 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`;
7294
9412
  {
7295
9413
  name: "terminate_all_sessions",
7296
9414
  description: "Terminates all active sessions, or all sessions of a specific type (terminal, cursor, claude, opencode).",
7297
- schema: z5.object({
7298
- sessionType: z5.enum(["terminal", "cursor", "claude", "opencode", "all"]).optional().describe('Type of sessions to terminate. Omit or use "all" to terminate all sessions.')
9415
+ schema: z7.object({
9416
+ sessionType: z7.enum(["terminal", "cursor", "claude", "opencode", "all"]).optional().describe('Type of sessions to terminate. Omit or use "all" to terminate all sessions.')
7299
9417
  })
7300
9418
  }
7301
9419
  );
7302
- const getSessionInfo = tool3(
9420
+ const getSessionInfo = tool4(
7303
9421
  async ({ sessionId }) => {
7304
9422
  try {
7305
9423
  const { SessionId: SessionId2 } = await import("./session-id-VKOYWZAK.js");
@@ -7334,12 +9452,12 @@ Messages: ${agentState.messages.length}`;
7334
9452
  {
7335
9453
  name: "get_session_info",
7336
9454
  description: "Gets detailed information about a specific session.",
7337
- schema: z5.object({
7338
- sessionId: z5.string().describe("ID of the session")
9455
+ schema: z7.object({
9456
+ sessionId: z7.string().describe("ID of the session")
7339
9457
  })
7340
9458
  }
7341
9459
  );
7342
- const listSessionsWithWorktrees = tool3(
9460
+ const listSessionsWithWorktrees = tool4(
7343
9461
  () => {
7344
9462
  try {
7345
9463
  const sessions2 = agentSessionManager.listSessionsWithWorktreeInfo();
@@ -7361,10 +9479,10 @@ ${sessionLines.join("\n")}`;
7361
9479
  {
7362
9480
  name: "list_sessions_with_worktrees",
7363
9481
  description: "Lists all active sessions with worktree information for branch management",
7364
- schema: z5.object({})
9482
+ schema: z7.object({})
7365
9483
  }
7366
9484
  );
7367
- const getWorktreeSessionSummary = tool3(
9485
+ const getWorktreeSessionSummary = tool4(
7368
9486
  ({ workspace, project, branch }) => {
7369
9487
  try {
7370
9488
  const summary = agentSessionManager.getWorktreeSessionSummary(workspace, project, branch);
@@ -7387,14 +9505,14 @@ Executing: ${summary.executingCount}`;
7387
9505
  {
7388
9506
  name: "get_worktree_session_summary",
7389
9507
  description: "Gets detailed session information for a specific worktree",
7390
- schema: z5.object({
7391
- workspace: z5.string().describe("Workspace name"),
7392
- project: z5.string().describe("Project name"),
7393
- branch: z5.string().describe("Branch/worktree name")
9508
+ schema: z7.object({
9509
+ workspace: z7.string().describe("Workspace name"),
9510
+ project: z7.string().describe("Project name"),
9511
+ branch: z7.string().describe("Branch/worktree name")
7394
9512
  })
7395
9513
  }
7396
9514
  );
7397
- const terminateWorktreeSessions = tool3(
9515
+ const terminateWorktreeSessions = tool4(
7398
9516
  ({ workspace, project, branch }) => {
7399
9517
  try {
7400
9518
  const terminatedSessions = agentSessionManager.terminateWorktreeSessions(workspace, project, branch);
@@ -7410,14 +9528,14 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
7410
9528
  {
7411
9529
  name: "terminate_worktree_sessions",
7412
9530
  description: "Terminates all active sessions in a specific worktree",
7413
- schema: z5.object({
7414
- workspace: z5.string().describe("Workspace name"),
7415
- project: z5.string().describe("Project name"),
7416
- branch: z5.string().describe("Branch/worktree name")
9531
+ schema: z7.object({
9532
+ workspace: z7.string().describe("Workspace name"),
9533
+ project: z7.string().describe("Project name"),
9534
+ branch: z7.string().describe("Branch/worktree name")
7417
9535
  })
7418
9536
  }
7419
9537
  );
7420
- const clearContext = tool3(
9538
+ const clearContext = tool4(
7421
9539
  () => {
7422
9540
  try {
7423
9541
  if (clearSupervisorContext) {
@@ -7431,7 +9549,7 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
7431
9549
  {
7432
9550
  name: "clear_supervisor_context",
7433
9551
  description: 'Clears the supervisor agent conversation context and history. Use when user asks to "clear context", "reset conversation", "start fresh", or similar requests.',
7434
- schema: z5.object({})
9552
+ schema: z7.object({})
7435
9553
  }
7436
9554
  );
7437
9555
  return [
@@ -7450,12 +9568,12 @@ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
7450
9568
  }
7451
9569
 
7452
9570
  // src/infrastructure/agents/supervisor/tools/filesystem-tools.ts
7453
- import { tool as tool4 } from "@langchain/core/tools";
7454
- import { z as z6 } from "zod";
9571
+ import { tool as tool5 } from "@langchain/core/tools";
9572
+ import { z as z8 } from "zod";
7455
9573
  import { readdir as readdir3, readFile as readFile2, stat as stat2 } from "fs/promises";
7456
- import { join as join6, resolve } from "path";
9574
+ import { join as join10, resolve } from "path";
7457
9575
  function createFilesystemTools(workspacesRoot) {
7458
- const listDirectory = tool4(
9576
+ const listDirectory = tool5(
7459
9577
  async ({ path, showHidden }) => {
7460
9578
  try {
7461
9579
  const resolvedPath = resolveSafePath(path, workspacesRoot);
@@ -7477,432 +9595,324 @@ ${formatted.join("\n")}`;
7477
9595
  {
7478
9596
  name: "list_directory",
7479
9597
  description: "Lists the contents of a directory. Shows files and subdirectories.",
7480
- schema: z6.object({
7481
- path: z6.string().describe("Path to the directory (relative to workspaces root or absolute)"),
7482
- showHidden: z6.boolean().optional().describe("Whether to show hidden files (starting with .)")
9598
+ schema: z8.object({
9599
+ path: z8.string().describe("Path to the directory (relative to workspaces root or absolute)"),
9600
+ showHidden: z8.boolean().optional().describe("Whether to show hidden files (starting with .)")
7483
9601
  })
7484
9602
  }
7485
9603
  );
7486
- const readFileContent = tool4(
9604
+ const readFileContent = tool5(
7487
9605
  async ({ path, maxLines }) => {
7488
9606
  try {
7489
9607
  const resolvedPath = resolveSafePath(path, workspacesRoot);
7490
9608
  const fileStat = await stat2(resolvedPath);
7491
- if (fileStat.size > 100 * 1024) {
7492
- return `File is too large (${Math.round(fileStat.size / 1024)}KB). Maximum is 100KB.`;
7493
- }
7494
- const content = await readFile2(resolvedPath, "utf-8");
7495
- const lines = content.split("\n");
7496
- if (maxLines && lines.length > maxLines) {
7497
- return `File: ${path} (showing first ${maxLines} of ${lines.length} lines)
7498
-
7499
- ${lines.slice(0, maxLines).join("\n")}
7500
-
7501
- ... (${lines.length - maxLines} more lines)`;
7502
- }
7503
- return `File: ${path}
7504
-
7505
- ${content}`;
7506
- } catch (error) {
7507
- return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
7508
- }
7509
- },
7510
- {
7511
- name: "read_file",
7512
- description: "Reads the contents of a file. Limited to 100KB files for safety.",
7513
- schema: z6.object({
7514
- path: z6.string().describe("Path to the file"),
7515
- maxLines: z6.number().optional().describe("Maximum number of lines to return")
7516
- })
7517
- }
7518
- );
7519
- const getFileInfo = tool4(
7520
- async ({ path }) => {
7521
- try {
7522
- const resolvedPath = resolveSafePath(path, workspacesRoot);
7523
- const fileStat = await stat2(resolvedPath);
7524
- const info = [
7525
- `Path: ${path}`,
7526
- `Type: ${fileStat.isDirectory() ? "Directory" : "File"}`,
7527
- `Size: ${formatSize(fileStat.size)}`,
7528
- `Modified: ${fileStat.mtime.toISOString()}`,
7529
- `Created: ${fileStat.birthtime.toISOString()}`
7530
- ];
7531
- return info.join("\n");
7532
- } catch (error) {
7533
- return `Error getting file info: ${error instanceof Error ? error.message : String(error)}`;
7534
- }
7535
- },
7536
- {
7537
- name: "get_file_info",
7538
- description: "Gets information about a file or directory (size, modified date, etc.).",
7539
- schema: z6.object({
7540
- path: z6.string().describe("Path to the file or directory")
7541
- })
7542
- }
7543
- );
7544
- return [listDirectory, readFileContent, getFileInfo];
7545
- }
7546
- function resolveSafePath(path, workspacesRoot) {
7547
- const resolved = path.startsWith("/") ? path : join6(workspacesRoot, path);
7548
- const normalized = resolve(resolved);
7549
- const homeDir = process.env.HOME ?? "/";
7550
- if (!normalized.startsWith(workspacesRoot) && !normalized.startsWith(homeDir)) {
7551
- throw new Error(`Access denied: Path must be within ${workspacesRoot} or home directory`);
7552
- }
7553
- return normalized;
7554
- }
7555
- function formatSize(bytes) {
7556
- if (bytes < 1024) return `${bytes} B`;
7557
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
7558
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
7559
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
7560
- }
7561
-
7562
- // src/infrastructure/agents/supervisor/supervisor-agent.ts
7563
- var SupervisorAgent = class extends EventEmitter4 {
7564
- logger;
7565
- agent;
7566
- getMessageBroadcaster;
7567
- getChatHistoryService;
7568
- conversationHistory = [];
7569
- abortController = null;
7570
- isExecuting = false;
7571
- isCancelled = false;
7572
- /** Tracks if we're processing a command (including STT, before LLM execution) */
7573
- isProcessingCommand = false;
7574
- /** Timestamp when current execution started (for race condition protection) */
7575
- executionStartedAt = 0;
7576
- constructor(config2) {
7577
- super();
7578
- this.logger = config2.logger.child({ component: "SupervisorAgent" });
7579
- this.getMessageBroadcaster = config2.getMessageBroadcaster;
7580
- this.getChatHistoryService = config2.getChatHistoryService;
7581
- const env = getEnv();
7582
- const llm = this.createLLM(env);
7583
- const terminateSessionCallback = async (sessionId) => {
7584
- const terminate = config2.getTerminateSession?.();
7585
- if (!terminate) {
7586
- this.logger.warn("Terminate session callback not available");
7587
- return false;
7588
- }
7589
- return terminate(sessionId);
7590
- };
7591
- const tools = [
7592
- ...createWorkspaceTools(config2.workspaceDiscovery),
7593
- ...createWorktreeTools(config2.workspaceDiscovery, config2.agentSessionManager),
7594
- ...createSessionTools(
7595
- config2.sessionManager,
7596
- config2.agentSessionManager,
7597
- config2.workspaceDiscovery,
7598
- config2.workspacesRoot,
7599
- config2.getMessageBroadcaster,
7600
- config2.getChatHistoryService,
7601
- () => this.clearContext(),
7602
- terminateSessionCallback
7603
- ),
7604
- ...createFilesystemTools(config2.workspacesRoot)
7605
- ];
7606
- this.logger.info({ toolCount: tools.length }, "Creating Supervisor Agent with tools");
7607
- this.agent = createReactAgent({
7608
- llm,
7609
- tools
7610
- });
7611
- }
7612
- /**
7613
- * Creates the LLM instance based on configuration.
7614
- */
7615
- createLLM(env) {
7616
- const provider = env.AGENT_PROVIDER;
7617
- const apiKey = env.AGENT_API_KEY;
7618
- const modelName = env.AGENT_MODEL_NAME;
7619
- const baseUrl = env.AGENT_BASE_URL;
7620
- const temperature = env.AGENT_TEMPERATURE;
7621
- if (!apiKey) {
7622
- throw new Error("AGENT_API_KEY is required for Supervisor Agent");
7623
- }
7624
- this.logger.info({ provider, model: modelName }, "Initializing LLM");
7625
- return new ChatOpenAI({
7626
- openAIApiKey: apiKey,
7627
- modelName,
7628
- temperature,
7629
- configuration: baseUrl ? {
7630
- baseURL: baseUrl
7631
- } : void 0
7632
- });
7633
- }
7634
- /**
7635
- * Executes a command through the supervisor agent.
7636
- * Note: deviceId is used for routing responses, not for history (history is global).
7637
- */
7638
- async execute(command, deviceId, currentSessionId) {
7639
- this.logger.info({ command, deviceId, currentSessionId }, "Executing supervisor command");
7640
- try {
7641
- const history = this.getConversationHistory();
7642
- const messages2 = [
7643
- ...this.buildSystemMessage(),
7644
- ...this.buildHistoryMessages(history),
7645
- new HumanMessage(command)
7646
- ];
7647
- const result = await this.agent.invoke({
7648
- messages: messages2
7649
- });
7650
- const agentMessages = result.messages;
7651
- const lastMessage = agentMessages[agentMessages.length - 1];
7652
- const content = lastMessage?.content;
7653
- const output = typeof content === "string" ? content : JSON.stringify(content);
7654
- this.addToHistory("user", command);
7655
- this.addToHistory("assistant", output);
7656
- this.logger.debug({ output: output.slice(0, 200) }, "Supervisor command completed");
7657
- return {
7658
- output,
7659
- sessionId: currentSessionId
7660
- };
7661
- } catch (error) {
7662
- this.logger.error({ error, command }, "Supervisor command failed");
7663
- const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
7664
- return {
7665
- output: `Error: ${errorMessage}`
7666
- };
7667
- }
7668
- }
7669
- /**
7670
- * Executes a command with streaming output.
7671
- * Emits 'blocks' events as content is generated.
7672
- * Note: deviceId is used for routing responses, history is global.
7673
- */
7674
- async executeWithStream(command, deviceId) {
7675
- this.logger.info({ command, deviceId, wasCancelledBefore: this.isCancelled }, "Executing supervisor command with streaming");
7676
- this.abortController = new AbortController();
7677
- this.isExecuting = true;
7678
- this.isCancelled = false;
7679
- this.executionStartedAt = Date.now();
7680
- this.logger.debug({ isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Flags reset for new execution");
7681
- try {
7682
- const history = this.getConversationHistory();
7683
- const messages2 = [
7684
- ...this.buildSystemMessage(),
7685
- ...this.buildHistoryMessages(history),
7686
- new HumanMessage(command)
7687
- ];
7688
- if (this.isExecuting && !this.isCancelled) {
7689
- const statusBlock = createStatusBlock("Processing...");
7690
- this.logger.debug({ deviceId, blockType: "status" }, "Emitting status block");
7691
- this.emit("blocks", deviceId, [statusBlock], false);
7692
- }
7693
- this.logger.info({ deviceId }, "Starting LangGraph agent stream");
7694
- const stream = await this.agent.stream(
7695
- { messages: messages2 },
7696
- {
7697
- streamMode: "values",
7698
- signal: this.abortController.signal
7699
- }
7700
- );
7701
- let finalOutput = "";
7702
- const allBlocks = [];
7703
- for await (const chunk of stream) {
7704
- if (this.isCancelled || !this.isExecuting) {
7705
- this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor execution cancelled, stopping stream processing");
7706
- return;
7707
- }
7708
- const chunkData = chunk;
7709
- const chunkMessages = chunkData.messages;
7710
- if (!chunkMessages || chunkMessages.length === 0) continue;
7711
- const lastMessage = chunkMessages[chunkMessages.length - 1];
7712
- if (!lastMessage) continue;
7713
- if (isAIMessage(lastMessage)) {
7714
- const content = lastMessage.content;
7715
- if (typeof content === "string" && content.length > 0) {
7716
- finalOutput = content;
7717
- if (this.isExecuting && !this.isCancelled) {
7718
- const textBlock = createTextBlock(content);
7719
- this.emit("blocks", deviceId, [textBlock], false);
7720
- accumulateBlocks(allBlocks, [textBlock]);
7721
- }
7722
- } else if (Array.isArray(content)) {
7723
- for (const item of content) {
7724
- if (typeof item === "object" && this.isExecuting && !this.isCancelled) {
7725
- const block = this.parseContentItem(item);
7726
- if (block) {
7727
- this.emit("blocks", deviceId, [block], false);
7728
- accumulateBlocks(allBlocks, [block]);
7729
- }
7730
- }
7731
- }
7732
- }
7733
- }
7734
- if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
7735
- const toolContent = lastMessage.content;
7736
- const toolName = lastMessage.name ?? "tool";
7737
- const toolCallId = lastMessage.tool_call_id;
7738
- const toolBlock = createToolBlock(
7739
- toolName,
7740
- "completed",
7741
- void 0,
7742
- typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
7743
- toolCallId
7744
- );
7745
- this.emit("blocks", deviceId, [toolBlock], false);
7746
- accumulateBlocks(allBlocks, [toolBlock]);
9609
+ if (fileStat.size > 100 * 1024) {
9610
+ return `File is too large (${Math.round(fileStat.size / 1024)}KB). Maximum is 100KB.`;
7747
9611
  }
9612
+ const content = await readFile2(resolvedPath, "utf-8");
9613
+ const lines = content.split("\n");
9614
+ if (maxLines && lines.length > maxLines) {
9615
+ return `File: ${path} (showing first ${maxLines} of ${lines.length} lines)
9616
+
9617
+ ${lines.slice(0, maxLines).join("\n")}
9618
+
9619
+ ... (${lines.length - maxLines} more lines)`;
9620
+ }
9621
+ return `File: ${path}
9622
+
9623
+ ${content}`;
9624
+ } catch (error) {
9625
+ return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
7748
9626
  }
7749
- if (this.isExecuting && !this.isCancelled) {
7750
- this.addToHistory("user", command);
7751
- this.addToHistory("assistant", finalOutput);
7752
- const finalBlocks = mergeToolBlocks(allBlocks);
7753
- const completionBlock = createStatusBlock("Complete");
7754
- this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
7755
- this.logger.debug({ output: finalOutput.slice(0, 200) }, "Supervisor streaming completed");
7756
- } else {
7757
- this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor streaming ended due to cancellation");
7758
- }
7759
- } catch (error) {
7760
- if (this.isCancelled || !this.isExecuting) {
7761
- this.logger.info({ deviceId }, "Supervisor execution cancelled (caught in error handler)");
7762
- return;
9627
+ },
9628
+ {
9629
+ name: "read_file",
9630
+ description: "Reads the contents of a file. Limited to 100KB files for safety.",
9631
+ schema: z8.object({
9632
+ path: z8.string().describe("Path to the file"),
9633
+ maxLines: z8.number().optional().describe("Maximum number of lines to return")
9634
+ })
9635
+ }
9636
+ );
9637
+ const getFileInfo = tool5(
9638
+ async ({ path }) => {
9639
+ try {
9640
+ const resolvedPath = resolveSafePath(path, workspacesRoot);
9641
+ const fileStat = await stat2(resolvedPath);
9642
+ const info = [
9643
+ `Path: ${path}`,
9644
+ `Type: ${fileStat.isDirectory() ? "Directory" : "File"}`,
9645
+ `Size: ${formatSize(fileStat.size)}`,
9646
+ `Modified: ${fileStat.mtime.toISOString()}`,
9647
+ `Created: ${fileStat.birthtime.toISOString()}`
9648
+ ];
9649
+ return info.join("\n");
9650
+ } catch (error) {
9651
+ return `Error getting file info: ${error instanceof Error ? error.message : String(error)}`;
7763
9652
  }
7764
- this.logger.error({ error, command }, "Supervisor streaming failed");
7765
- const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
7766
- const errorBlock = createErrorBlock(errorMessage);
7767
- this.emit("blocks", deviceId, [errorBlock], true);
7768
- } finally {
7769
- this.isExecuting = false;
7770
- this.abortController = null;
9653
+ },
9654
+ {
9655
+ name: "get_file_info",
9656
+ description: "Gets information about a file or directory (size, modified date, etc.).",
9657
+ schema: z8.object({
9658
+ path: z8.string().describe("Path to the file or directory")
9659
+ })
7771
9660
  }
9661
+ );
9662
+ return [listDirectory, readFileContent, getFileInfo];
9663
+ }
9664
+ function resolveSafePath(path, workspacesRoot) {
9665
+ const resolved = path.startsWith("/") ? path : join10(workspacesRoot, path);
9666
+ const normalized = resolve(resolved);
9667
+ const homeDir = process.env.HOME ?? "/";
9668
+ if (!normalized.startsWith(workspacesRoot) && !normalized.startsWith(homeDir)) {
9669
+ throw new Error(`Access denied: Path must be within ${workspacesRoot} or home directory`);
7772
9670
  }
7773
- /**
7774
- * Parses a content item from LangGraph into a ContentBlock.
7775
- */
7776
- parseContentItem(item) {
7777
- const type = item.type;
7778
- if (type === "text" && typeof item.text === "string" && item.text.trim()) {
7779
- return createTextBlock(item.text);
9671
+ return normalized;
9672
+ }
9673
+ function formatSize(bytes) {
9674
+ if (bytes < 1024) return `${bytes} B`;
9675
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
9676
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
9677
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
9678
+ }
9679
+
9680
+ // src/infrastructure/agents/supervisor/tools/backlog-tools.ts
9681
+ import { join as join11 } from "path";
9682
+ import { existsSync as existsSync8 } from "fs";
9683
+ import { tool as tool6 } from "@langchain/core/tools";
9684
+ import { z as z9 } from "zod";
9685
+ function createBacklogTools(sessionManager, agentSessionManager, backlogManagers, workspacesRoot, getMessageBroadcaster, logger) {
9686
+ const createBacklogSession = tool6(
9687
+ async (args) => {
9688
+ const finalBacklogId = args.backlogId ?? `${args.project}-${Date.now()}`;
9689
+ let normalizedWorktree = args.worktree;
9690
+ if (normalizedWorktree && (normalizedWorktree.toLowerCase() === "main" || normalizedWorktree.toLowerCase() === "master")) {
9691
+ normalizedWorktree = void 0;
9692
+ }
9693
+ const root = workspacesRoot ?? "/workspaces";
9694
+ let workingDir;
9695
+ if (!normalizedWorktree) {
9696
+ workingDir = join11(root, args.workspace, args.project);
9697
+ } else {
9698
+ workingDir = join11(root, args.workspace, `${args.project}--${normalizedWorktree}`);
9699
+ }
9700
+ const workspacePath = new WorkspacePath(
9701
+ args.workspace,
9702
+ args.project,
9703
+ normalizedWorktree
9704
+ );
9705
+ if (!existsSync8(workingDir)) {
9706
+ const details = normalizedWorktree ? `The worktree/branch "${normalizedWorktree}" is checked out` : `The main/master branch is checked out`;
9707
+ return `\u274C ERROR: Project path does not exist: ${workingDir}
9708
+
9709
+ Please make sure:
9710
+ 1. The workspace "${args.workspace}" exists
9711
+ 2. The project "${args.project}" exists
9712
+ 3. ${details}
9713
+
9714
+ Use list_worktrees to see available branches for the project.`;
9715
+ }
9716
+ const session = await sessionManager.createSession({
9717
+ sessionType: "backlog-agent",
9718
+ workspacePath,
9719
+ workingDir,
9720
+ backlogId: finalBacklogId
9721
+ });
9722
+ if (session.type !== "backlog-agent") {
9723
+ return `Failed to create backlog session`;
9724
+ }
9725
+ const backlogSession = session;
9726
+ const noopFn = () => {
9727
+ };
9728
+ const noopLogger = {
9729
+ level: "silent",
9730
+ debug: noopFn,
9731
+ info: noopFn,
9732
+ warn: noopFn,
9733
+ error: noopFn,
9734
+ fatal: noopFn,
9735
+ trace: noopFn,
9736
+ silent: noopFn,
9737
+ child: () => noopLogger
9738
+ };
9739
+ const backlogLogger = logger ?? noopLogger;
9740
+ const manager = BacklogAgentManager.createEmpty(
9741
+ backlogSession,
9742
+ workingDir,
9743
+ agentSessionManager,
9744
+ backlogLogger
9745
+ );
9746
+ backlogManagers.set(session.id.value, manager);
9747
+ const broadcaster = getMessageBroadcaster?.();
9748
+ if (broadcaster) {
9749
+ broadcaster.broadcastToAll(JSON.stringify({
9750
+ type: "session.created",
9751
+ session_id: session.id.value,
9752
+ payload: {
9753
+ session_type: "backlog-agent",
9754
+ workspace: args.workspace,
9755
+ project: args.project,
9756
+ worktree: normalizedWorktree,
9757
+ working_dir: workingDir
9758
+ }
9759
+ }));
9760
+ }
9761
+ return `\u2705 Created backlog session "${finalBacklogId}" at ${workingDir}
9762
+ Session ID: ${session.id.value}`;
9763
+ },
9764
+ {
9765
+ name: "create_backlog_session",
9766
+ description: "Create a new Backlog Agent session for autonomous development (uses system default model)",
9767
+ schema: z9.object({
9768
+ workspace: z9.string().describe("Workspace name"),
9769
+ project: z9.string().describe("Project name"),
9770
+ worktree: z9.string().optional().describe("Git worktree/branch name. Omit for main/master branch"),
9771
+ backlogId: z9.string().optional().describe("Custom backlog identifier")
9772
+ })
7780
9773
  }
7781
- if (type === "tool_use") {
7782
- const name = typeof item.name === "string" ? item.name : "tool";
7783
- const input = item.input;
7784
- const toolUseId = typeof item.id === "string" ? item.id : void 0;
7785
- return createToolBlock(name, "running", input, void 0, toolUseId);
9774
+ );
9775
+ const listBacklogSessions = tool6(
9776
+ () => {
9777
+ const sessions2 = Array.from(backlogManagers.entries()).map(
9778
+ ([sessionId, manager]) => `- ${sessionId}: ${manager.getBacklog().id} (${manager.getSession().agentName})`
9779
+ ).join("\n");
9780
+ if (sessions2.length === 0) {
9781
+ return "No active backlog sessions";
9782
+ }
9783
+ return `Active Backlog Sessions:
9784
+ ${sessions2}`;
9785
+ },
9786
+ {
9787
+ name: "list_backlog_sessions",
9788
+ description: "List all active Backlog Agent sessions",
9789
+ schema: z9.object({})
7786
9790
  }
7787
- return null;
7788
- }
7789
- /**
7790
- * Cancels the current execution if running.
7791
- * Returns true if cancellation was initiated.
7792
- */
7793
- cancel() {
7794
- if (!this.isProcessingCommand && !this.isExecuting) {
7795
- this.logger.debug(
7796
- { isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting },
7797
- "No active execution to cancel"
7798
- );
7799
- return false;
9791
+ );
9792
+ const getBacklogStatus = tool6(
9793
+ (args) => {
9794
+ const manager = backlogManagers.get(args.sessionId);
9795
+ if (!manager) {
9796
+ return `Backlog session "${args.sessionId}" not found`;
9797
+ }
9798
+ const backlog = manager.getBacklog();
9799
+ const summary = backlog.summary ?? {
9800
+ total: backlog.tasks.length,
9801
+ completed: 0,
9802
+ failed: 0,
9803
+ in_progress: 0,
9804
+ pending: backlog.tasks.length
9805
+ };
9806
+ const percentage = summary.total > 0 ? summary.completed / summary.total * 100 : 0;
9807
+ const worktreeDisplay = backlog.worktree ?? "main";
9808
+ return `
9809
+ \u{1F4CA} Backlog: ${backlog.id}
9810
+ Progress: ${summary.completed}/${summary.total} (${percentage.toFixed(0)}%)
9811
+ - Completed: ${summary.completed}
9812
+ - In Progress: ${summary.in_progress}
9813
+ - Pending: ${summary.pending}
9814
+ - Failed: ${summary.failed}
9815
+
9816
+ Agent: ${manager.getSession().agentName}
9817
+ Worktree: ${worktreeDisplay}
9818
+ `.trim();
9819
+ },
9820
+ {
9821
+ name: "get_backlog_status",
9822
+ description: "Get status of a backlog session",
9823
+ schema: z9.object({
9824
+ sessionId: z9.string().describe("Backlog session ID")
9825
+ })
7800
9826
  }
7801
- const timeSinceStart = Date.now() - this.executionStartedAt;
7802
- if (this.isExecuting && timeSinceStart < 500) {
7803
- this.logger.info(
7804
- { timeSinceStart, isProcessingCommand: this.isProcessingCommand },
7805
- "Ignoring cancel - execution just started (race condition protection)"
9827
+ );
9828
+ const addTaskToBacklog = tool6(
9829
+ async (args) => {
9830
+ const manager = backlogManagers.get(args.sessionId);
9831
+ if (!manager) {
9832
+ return `Backlog session "${args.sessionId}" not found`;
9833
+ }
9834
+ const blocks = await manager.executeCommand(
9835
+ `add task: "${args.title}" - ${args.description}${args.criteria ? ` (criteria: ${args.criteria})` : ""}`
7806
9836
  );
7807
- return false;
7808
- }
7809
- this.logger.info(
7810
- { isProcessingCommand: this.isProcessingCommand, isExecuting: this.isExecuting, timeSinceStart },
7811
- "Cancelling supervisor execution"
7812
- );
7813
- this.isCancelled = true;
7814
- this.isExecuting = false;
7815
- this.isProcessingCommand = false;
7816
- if (this.abortController) {
7817
- this.abortController.abort();
9837
+ return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
9838
+ },
9839
+ {
9840
+ name: "add_task_to_backlog",
9841
+ description: "Add a task to a backlog",
9842
+ schema: z9.object({
9843
+ sessionId: z9.string().describe("Backlog session ID"),
9844
+ title: z9.string().describe("Task title"),
9845
+ description: z9.string().describe("Task description"),
9846
+ criteria: z9.string().optional().describe("Acceptance criteria")
9847
+ })
7818
9848
  }
7819
- return true;
7820
- }
7821
- /**
7822
- * Check if execution was cancelled.
7823
- * Used by main.ts to filter out any late-arriving blocks.
7824
- */
7825
- wasCancelled() {
7826
- return this.isCancelled;
7827
- }
7828
- /**
7829
- * Starts command processing (before STT/LLM execution).
7830
- * Returns an AbortController that can be used to cancel STT and other operations.
7831
- */
7832
- startProcessing() {
7833
- this.abortController = new AbortController();
7834
- this.isProcessingCommand = true;
7835
- this.isCancelled = false;
7836
- this.logger.debug("Started command processing");
7837
- return this.abortController;
7838
- }
7839
- /**
7840
- * Checks if command processing is active (STT or LLM execution).
7841
- */
7842
- isProcessing() {
7843
- return this.isProcessingCommand || this.isExecuting;
7844
- }
7845
- /**
7846
- * Ends command processing (called after completion or error, not after cancel).
7847
- */
7848
- endProcessing() {
7849
- this.isProcessingCommand = false;
7850
- this.logger.debug("Ended command processing");
7851
- }
7852
- /**
7853
- * Clears global conversation history (in-memory only).
7854
- * Also resets cancellation state to allow new commands.
7855
- * @deprecated Use clearContext() for full context clearing with persistence and broadcast.
7856
- */
7857
- clearHistory() {
7858
- this.conversationHistory = [];
7859
- this.isCancelled = false;
7860
- this.logger.info("Global conversation history cleared");
7861
- }
7862
- /**
7863
- * Clears supervisor context completely:
7864
- * - In-memory conversation history
7865
- * - Persistent history in database
7866
- * - Notifies all connected clients
7867
- */
7868
- clearContext() {
7869
- this.conversationHistory = [];
7870
- this.isCancelled = false;
7871
- const chatHistoryService = this.getChatHistoryService?.();
7872
- if (chatHistoryService) {
7873
- chatHistoryService.clearSupervisorHistory();
9849
+ );
9850
+ const startBacklogHarness = tool6(
9851
+ async (args) => {
9852
+ const manager = backlogManagers.get(args.sessionId);
9853
+ if (!manager) {
9854
+ return `Backlog session "${args.sessionId}" not found`;
9855
+ }
9856
+ const blocks = await manager.executeCommand("start");
9857
+ return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
9858
+ },
9859
+ {
9860
+ name: "start_backlog_harness",
9861
+ description: "Start autonomous execution of a backlog",
9862
+ schema: z9.object({
9863
+ sessionId: z9.string().describe("Backlog session ID")
9864
+ })
7874
9865
  }
7875
- const broadcaster = this.getMessageBroadcaster?.();
7876
- if (broadcaster) {
7877
- const clearNotification = JSON.stringify({
7878
- type: "supervisor.context_cleared",
7879
- payload: { timestamp: Date.now() }
7880
- });
7881
- broadcaster.broadcastToAll(clearNotification);
9866
+ );
9867
+ const stopBacklogHarness = tool6(
9868
+ async (args) => {
9869
+ const manager = backlogManagers.get(args.sessionId);
9870
+ if (!manager) {
9871
+ return `Backlog session "${args.sessionId}" not found`;
9872
+ }
9873
+ const blocks = await manager.executeCommand("stop");
9874
+ return blocks.map((b) => b.block_type === "text" ? b.content : "").join("\n");
9875
+ },
9876
+ {
9877
+ name: "stop_backlog_harness",
9878
+ description: "Stop autonomous execution of a backlog",
9879
+ schema: z9.object({
9880
+ sessionId: z9.string().describe("Backlog session ID")
9881
+ })
7882
9882
  }
7883
- this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
7884
- }
7885
- /**
7886
- * Resets the cancellation state.
7887
- * Call this before starting a new command to ensure previous cancellation doesn't affect it.
7888
- */
7889
- resetCancellationState() {
7890
- this.isCancelled = false;
7891
- }
7892
- /**
7893
- * Restores global conversation history from persistent storage.
7894
- * Called on startup to sync in-memory cache with database.
7895
- */
7896
- restoreHistory(history) {
7897
- if (history.length === 0) return;
7898
- this.conversationHistory = history.slice(-20);
7899
- this.logger.debug({ messageCount: this.conversationHistory.length }, "Global conversation history restored");
9883
+ );
9884
+ return {
9885
+ createBacklogSession,
9886
+ listBacklogSessions,
9887
+ getBacklogStatus,
9888
+ addTaskToBacklog,
9889
+ startBacklogHarness,
9890
+ stopBacklogHarness
9891
+ };
9892
+ }
9893
+
9894
+ // src/infrastructure/agents/supervisor/supervisor-agent.ts
9895
+ var SupervisorAgent = class extends LangGraphAgent {
9896
+ getMessageBroadcaster;
9897
+ getChatHistoryService;
9898
+ sessionManager;
9899
+ agentSessionManager;
9900
+ workspaceDiscovery;
9901
+ workspacesRoot;
9902
+ getTerminateSession;
9903
+ constructor(config2) {
9904
+ super(config2.logger);
9905
+ this.getMessageBroadcaster = config2.getMessageBroadcaster;
9906
+ this.getChatHistoryService = config2.getChatHistoryService;
9907
+ this.sessionManager = config2.sessionManager;
9908
+ this.agentSessionManager = config2.agentSessionManager;
9909
+ this.workspaceDiscovery = config2.workspaceDiscovery;
9910
+ this.workspacesRoot = config2.workspacesRoot;
9911
+ this.getTerminateSession = config2.getTerminateSession;
9912
+ this.initializeAgent();
7900
9913
  }
7901
- /**
7902
- * Builds the system message for the agent.
7903
- */
7904
- buildSystemMessage() {
7905
- const systemPrompt = `## MANDATORY RULES (STRICTLY ENFORCED)
9914
+ buildSystemPrompt() {
9915
+ return `## MANDATORY RULES (STRICTLY ENFORCED)
7906
9916
 
7907
9917
  You MUST always respond in English.
7908
9918
 
@@ -8047,6 +10057,59 @@ IMPORTANT: When \`list_worktrees\` shows a worktree named "main" with \`isMain:
8047
10057
 
8048
10058
  ---
8049
10059
 
10060
+ ## BACKLOG SESSIONS (Autonomous Development)
10061
+
10062
+ Backlog sessions are special autonomous coding sessions that use the default system LLM model to execute a series of tasks.
10063
+
10064
+ ### Creating Backlog Sessions (CRITICAL PATH VALIDATION):
10065
+
10066
+ IMPORTANT: You MUST validate the project path EXISTS before creating a backlog session!
10067
+
10068
+ Step 1: Call \`list_worktrees\` for the project to see all available branches/worktrees
10069
+ Step 2: PARSE THE OUTPUT CAREFULLY:
10070
+ - Output format: "Worktrees for 'workspace/project':
10071
+ - worktree-name: branch-name (/path/to/directory)"
10072
+ - Example: "- main: main (/Users/roman/tiflis-code-work/roman/eva)"
10073
+ - This means the main branch is at /Users/roman/tiflis-code-work/roman/eva (NO --main suffix!)
10074
+ Step 3: Determine the correct worktree parameter:
10075
+ - If worktree is "main" or "master" (the default/primary branch):
10076
+ * Check the path shown: /Users/roman/tiflis-code-work/roman/eva
10077
+ * The path has NO worktree suffix (no --main, no --master)
10078
+ * DO NOT PASS WORKTREE PARAMETER - omit it entirely
10079
+ - If worktree is a feature branch (e.g., "feature-auth"):
10080
+ * The path would be: /Users/roman/tiflis-code-work/roman/eva--feature-auth
10081
+ * PASS worktree="feature-auth" parameter
10082
+ Step 4: Call \`create_backlog_session\` with workspace, project, and worktree (only if non-main)
10083
+ Step 5: Confirm the session was created successfully
10084
+
10085
+ ### Path Construction Rules (CRITICAL):
10086
+ - Worktree parameter controls the directory name pattern:
10087
+ * Omit worktree \u2192 uses: /workspaces/{workspace}/{project}
10088
+ * Pass worktree="feature-x" \u2192 uses: /workspaces/{workspace}/{project}--feature-x
10089
+ - NEVER pass worktree="main" or worktree="master" - just omit the parameter instead
10090
+ - ALWAYS check list_worktrees output to see actual paths
10091
+ - Only pass worktree when the branch name appears in the path AFTER the project name with -- separator
10092
+
10093
+ ### Example Flows:
10094
+
10095
+ User: "Create a backlog for eva on the main branch"
10096
+ Step 1: Call list_worktrees(roman, eva)
10097
+ Step 2: Output shows: "- main: main (/Users/roman/tiflis-code-work/roman/eva)"
10098
+ \u2192 Path is /roman/eva (no --main suffix)
10099
+ \u2192 This is the main branch, omit worktree parameter
10100
+ Step 3: Call create_backlog_session(workspace="roman", project="eva") [NO worktree parameter!]
10101
+ Step 4: \u2705 Created at /Users/roman/tiflis-code-work/roman/eva
10102
+
10103
+ User: "Create a backlog for tiflis-code on the feature-auth branch"
10104
+ Step 1: Call list_worktrees(tiflis, tiflis-code)
10105
+ Step 2: Output shows: "- feature-auth: feature/auth (/Users/roman/tiflis-code-work/tiflis/tiflis-code--feature-auth)"
10106
+ \u2192 Path has --feature-auth suffix
10107
+ \u2192 This is a feature branch, pass worktree parameter
10108
+ Step 3: Call create_backlog_session(workspace="tiflis", project="tiflis-code", worktree="feature-auth")
10109
+ Step 4: \u2705 Created at /Users/roman/tiflis-code-work/tiflis/tiflis-code--feature-auth
10110
+
10111
+ ---
10112
+
8050
10113
  ## WORKTREE MANAGEMENT
8051
10114
 
8052
10115
  Worktrees allow working on multiple branches simultaneously in separate directories.
@@ -8075,45 +10138,117 @@ Creating worktrees with \`create_worktree\`:
8075
10138
  - ALWAYS use bullet lists or numbered lists instead of tables
8076
10139
  - Keep list items short and scannable for mobile reading
8077
10140
  - ALWAYS prioritize safety - check before deleting/merging`;
8078
- return [new HumanMessage(`[System Instructions]
8079
- ${systemPrompt}
8080
- [End Instructions]`)];
8081
10141
  }
8082
10142
  /**
8083
- * Builds messages from conversation history.
10143
+ * Implements abstract method: create tools for Supervisor.
10144
+ */
10145
+ createTools() {
10146
+ const terminateSessionCallback = async (sessionId) => {
10147
+ const terminate = this.getTerminateSession?.();
10148
+ if (!terminate) {
10149
+ this.logger.warn("Terminate session callback not available");
10150
+ return false;
10151
+ }
10152
+ return terminate(sessionId);
10153
+ };
10154
+ return [
10155
+ ...createWorkspaceTools(this.workspaceDiscovery),
10156
+ ...createWorktreeTools(this.workspaceDiscovery, this.agentSessionManager),
10157
+ ...createSessionTools(
10158
+ this.sessionManager,
10159
+ this.agentSessionManager,
10160
+ this.workspaceDiscovery,
10161
+ this.workspacesRoot,
10162
+ this.getMessageBroadcaster,
10163
+ this.getChatHistoryService,
10164
+ () => this.clearContext(),
10165
+ terminateSessionCallback
10166
+ ),
10167
+ ...createFilesystemTools(this.workspacesRoot),
10168
+ ...Object.values(createBacklogTools(
10169
+ this.sessionManager,
10170
+ this.agentSessionManager,
10171
+ this.sessionManager.getBacklogManagers?.() ?? /* @__PURE__ */ new Map(),
10172
+ this.workspacesRoot,
10173
+ this.getMessageBroadcaster,
10174
+ this.logger
10175
+ ))
10176
+ ];
10177
+ }
10178
+ /**
10179
+ * Implements abstract method: create state manager for Supervisor.
8084
10180
  */
8085
- buildHistoryMessages(history) {
8086
- return history.map(
8087
- (entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
8088
- );
10181
+ createStateManager() {
10182
+ const chatHistoryService = this.getChatHistoryService?.();
10183
+ if (!chatHistoryService) {
10184
+ throw new Error("ChatHistoryService is required for SupervisorAgent");
10185
+ }
10186
+ return new SupervisorStateManager(chatHistoryService);
8089
10187
  }
8090
10188
  /**
8091
- * Gets global conversation history.
10189
+ * Clears supervisor context completely:
10190
+ * - In-memory conversation history
10191
+ * - Persistent history in database
10192
+ * - Notifies all connected clients
8092
10193
  */
8093
- getConversationHistory() {
8094
- return this.conversationHistory;
10194
+ clearContext() {
10195
+ this.conversationHistory = [];
10196
+ this.isCancelled = false;
10197
+ const chatHistoryService = this.getChatHistoryService?.();
10198
+ if (chatHistoryService) {
10199
+ chatHistoryService.clearSupervisorHistory();
10200
+ }
10201
+ const broadcaster = this.getMessageBroadcaster?.();
10202
+ if (broadcaster) {
10203
+ const clearNotification = JSON.stringify({
10204
+ type: "supervisor.context_cleared",
10205
+ payload: { timestamp: Date.now() }
10206
+ });
10207
+ broadcaster.broadcastToAll(clearNotification);
10208
+ }
10209
+ this.logger.info("Supervisor context cleared (in-memory, persistent, and clients notified)");
8095
10210
  }
8096
10211
  /**
8097
- * Adds an entry to global conversation history.
10212
+ * Synchronous execute method for ISupervisorAgent interface compatibility.
10213
+ * Internally calls executeWithStream and returns a result.
8098
10214
  */
8099
- addToHistory(role, content) {
8100
- this.conversationHistory.push({ role, content });
8101
- if (this.conversationHistory.length > 20) {
8102
- this.conversationHistory.splice(0, this.conversationHistory.length - 20);
10215
+ async execute(command, deviceId, _currentSessionId) {
10216
+ let output = "";
10217
+ const blockHandler = (_deviceId, _blocks, isComplete, finalOutput) => {
10218
+ if (isComplete && finalOutput) {
10219
+ output = finalOutput;
10220
+ }
10221
+ };
10222
+ this.on("blocks", blockHandler);
10223
+ try {
10224
+ await this.executeWithStream(command, deviceId);
10225
+ } finally {
10226
+ this.removeListener("blocks", blockHandler);
8103
10227
  }
10228
+ return { output };
10229
+ }
10230
+ clearHistory() {
10231
+ this.clearContext();
10232
+ }
10233
+ /**
10234
+ * Records acknowledgment of context clear from a device.
10235
+ * Used for multi-device synchronization.
10236
+ */
10237
+ recordClearAck(_broadcastId, _deviceId) {
10238
+ this.logger.debug({ _broadcastId, _deviceId }, "Context clear acknowledgment received");
8104
10239
  }
8105
10240
  };
8106
10241
 
8107
10242
  // src/infrastructure/mock/mock-supervisor-agent.ts
8108
- import { EventEmitter as EventEmitter5 } from "events";
10243
+ import { EventEmitter as EventEmitter7 } from "events";
8109
10244
 
8110
10245
  // src/infrastructure/mock/fixture-loader.ts
8111
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
8112
- import { join as join7, dirname as dirname2 } from "path";
10246
+ import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
10247
+ import { join as join12, dirname as dirname2 } from "path";
8113
10248
  import { fileURLToPath as fileURLToPath2 } from "url";
8114
10249
  var __filename = fileURLToPath2(import.meta.url);
8115
10250
  var __dirname = dirname2(__filename);
8116
- var DEFAULT_FIXTURES_PATH = join7(__dirname, "fixtures");
10251
+ var DEFAULT_FIXTURES_PATH = join12(__dirname, "fixtures");
8117
10252
  var fixtureCache = /* @__PURE__ */ new Map();
8118
10253
  function loadFixture(name, customPath) {
8119
10254
  const cacheKey = `${customPath ?? "default"}:${name}`;
@@ -8122,13 +10257,13 @@ function loadFixture(name, customPath) {
8122
10257
  return cached;
8123
10258
  }
8124
10259
  const fixturesDir = customPath ?? DEFAULT_FIXTURES_PATH;
8125
- const filePath = join7(fixturesDir, `${name}.json`);
8126
- if (!existsSync3(filePath)) {
10260
+ const filePath = join12(fixturesDir, `${name}.json`);
10261
+ if (!existsSync9(filePath)) {
8127
10262
  console.warn(`[MockMode] Fixture not found: ${filePath}`);
8128
10263
  return null;
8129
10264
  }
8130
10265
  try {
8131
- const content = readFileSync2(filePath, "utf-8");
10266
+ const content = readFileSync6(filePath, "utf-8");
8132
10267
  const fixture = JSON.parse(content);
8133
10268
  fixtureCache.set(cacheKey, fixture);
8134
10269
  return fixture;
@@ -8196,7 +10331,7 @@ function sleep(ms) {
8196
10331
  }
8197
10332
 
8198
10333
  // src/infrastructure/mock/mock-supervisor-agent.ts
8199
- var MockSupervisorAgent = class extends EventEmitter5 {
10334
+ var MockSupervisorAgent = class extends EventEmitter7 {
8200
10335
  logger;
8201
10336
  fixturesPath;
8202
10337
  fixture;
@@ -8375,18 +10510,23 @@ var MockSupervisorAgent = class extends EventEmitter5 {
8375
10510
  getConversationHistory() {
8376
10511
  return [...this.conversationHistory];
8377
10512
  }
8378
- /**
8379
- * Sleep utility.
8380
- */
10513
+ clearContext() {
10514
+ this.conversationHistory = [];
10515
+ this.isCancelled = false;
10516
+ this.logger.info("Mock context cleared");
10517
+ }
10518
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
10519
+ recordClearAck(_broadcastId, _deviceId) {
10520
+ }
8381
10521
  sleep(ms) {
8382
10522
  return new Promise((resolve2) => setTimeout(resolve2, ms));
8383
10523
  }
8384
10524
  };
8385
10525
 
8386
10526
  // src/infrastructure/mock/mock-agent-session-manager.ts
8387
- import { EventEmitter as EventEmitter6 } from "events";
10527
+ import { EventEmitter as EventEmitter8 } from "events";
8388
10528
  import { randomUUID as randomUUID3 } from "crypto";
8389
- var MockAgentSessionManager = class extends EventEmitter6 {
10529
+ var MockAgentSessionManager = class extends EventEmitter8 {
8390
10530
  sessions = /* @__PURE__ */ new Map();
8391
10531
  fixtures = /* @__PURE__ */ new Map();
8392
10532
  logger;
@@ -8607,7 +10747,7 @@ var MockAgentSessionManager = class extends EventEmitter6 {
8607
10747
 
8608
10748
  // src/infrastructure/speech/stt-service.ts
8609
10749
  import { writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
8610
- import { join as join8 } from "path";
10750
+ import { join as join13 } from "path";
8611
10751
  import { tmpdir } from "os";
8612
10752
  import { randomUUID as randomUUID4 } from "crypto";
8613
10753
  var STTService = class {
@@ -8661,7 +10801,7 @@ var STTService = class {
8661
10801
  async transcribeOpenAI(audioBuffer, format, signal) {
8662
10802
  const baseUrl = this.config.baseUrl ?? "https://api.openai.com/v1";
8663
10803
  const endpoint = `${baseUrl}/audio/transcriptions`;
8664
- const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
10804
+ const tempPath = join13(tmpdir(), `stt-${randomUUID4()}.${format}`);
8665
10805
  try {
8666
10806
  if (signal?.aborted) {
8667
10807
  throw new Error("Transcription cancelled");
@@ -8709,7 +10849,7 @@ var STTService = class {
8709
10849
  async transcribeElevenLabs(audioBuffer, format, signal) {
8710
10850
  const baseUrl = this.config.baseUrl ?? "https://api.elevenlabs.io/v1";
8711
10851
  const endpoint = `${baseUrl}/speech-to-text`;
8712
- const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
10852
+ const tempPath = join13(tmpdir(), `stt-${randomUUID4()}.${format}`);
8713
10853
  try {
8714
10854
  if (signal?.aborted) {
8715
10855
  throw new Error("Transcription cancelled");
@@ -9088,6 +11228,27 @@ function createSummarizationService(env, logger) {
9088
11228
  return new SummarizationService(config2, logger);
9089
11229
  }
9090
11230
 
11231
+ // src/domain/value-objects/chat-message.ts
11232
+ import { randomUUID as randomUUID5 } from "crypto";
11233
+ function createChatMessage(type, content, metadata) {
11234
+ return {
11235
+ id: randomUUID5(),
11236
+ timestamp: Date.now(),
11237
+ type,
11238
+ content,
11239
+ metadata
11240
+ };
11241
+ }
11242
+ function createUserMessage(content) {
11243
+ return createChatMessage("user", content);
11244
+ }
11245
+ function createAssistantMessage(content, metadata) {
11246
+ return createChatMessage("assistant", content, metadata);
11247
+ }
11248
+ function createErrorMessage(content, metadata) {
11249
+ return createChatMessage("error", content, metadata);
11250
+ }
11251
+
9091
11252
  // src/main.ts
9092
11253
  function printBanner(version) {
9093
11254
  const dim = "\x1B[2m";
@@ -9181,7 +11342,7 @@ function handleTunnelMessage(rawMessage, tunnelClient, messageBroadcaster, creat
9181
11342
  );
9182
11343
  }
9183
11344
  }
9184
- function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService) {
11345
+ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logger, subscriptionService, messageBroadcaster) {
9185
11346
  const authResult = AuthMessageSchema.safeParse(data);
9186
11347
  if (!authResult.success) {
9187
11348
  logger.warn(
@@ -9211,6 +11372,32 @@ function handleAuthMessageViaTunnel(data, tunnelClient, authenticateClient, logg
9211
11372
  );
9212
11373
  }
9213
11374
  }
11375
+ if (messageBroadcaster) {
11376
+ const bufferedMessages = messageBroadcaster.flushAuthBuffer(deviceId);
11377
+ if (bufferedMessages.length > 0) {
11378
+ logger.info(
11379
+ { deviceId, messageCount: bufferedMessages.length },
11380
+ "Flushing buffered messages after subscription restore"
11381
+ );
11382
+ (async () => {
11383
+ for (const bufferedMsg of bufferedMessages) {
11384
+ try {
11385
+ await messageBroadcaster.broadcastToSubscribers(
11386
+ bufferedMsg.sessionId,
11387
+ bufferedMsg.message
11388
+ );
11389
+ } catch (error) {
11390
+ logger.error(
11391
+ { error, deviceId, sessionId: bufferedMsg.sessionId },
11392
+ "Failed to send buffered message"
11393
+ );
11394
+ }
11395
+ }
11396
+ })().catch((error) => {
11397
+ logger.error({ error, deviceId }, "Error flushing buffered messages");
11398
+ });
11399
+ }
11400
+ }
9214
11401
  const responseJson = JSON.stringify(result);
9215
11402
  if (tunnelClient.send(responseJson)) {
9216
11403
  logger.info(
@@ -9292,6 +11479,7 @@ async function bootstrap() {
9292
11479
  chatHistoryService.ensureSupervisorSession();
9293
11480
  const workstationMetadataRepository = new WorkstationMetadataRepository();
9294
11481
  const subscriptionRepository = new SubscriptionRepository();
11482
+ const sessionRepository = new SessionRepository();
9295
11483
  const clientRegistry = new InMemoryClientRegistry(logger);
9296
11484
  const workspaceDiscovery = new FileSystemWorkspaceDiscovery({
9297
11485
  workspacesRoot: env.WORKSPACES_ROOT
@@ -9308,8 +11496,19 @@ async function bootstrap() {
9308
11496
  ptyManager,
9309
11497
  agentSessionManager,
9310
11498
  workspacesRoot: env.WORKSPACES_ROOT,
9311
- logger
11499
+ logger,
11500
+ sessionRepository
9312
11501
  });
11502
+ try {
11503
+ logger.info("About to call sessionManager.restoreSessions()");
11504
+ await sessionManager.restoreSessions();
11505
+ logger.info("sessionManager.restoreSessions() completed successfully");
11506
+ } catch (error) {
11507
+ logger.error(
11508
+ { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 },
11509
+ "Failed to restore sessions from database"
11510
+ );
11511
+ }
9313
11512
  if (env.MOCK_MODE) {
9314
11513
  const mockAgentManager = agentSessionManager;
9315
11514
  await sessionManager.createSession({
@@ -9400,7 +11599,7 @@ async function bootstrap() {
9400
11599
  },
9401
11600
  accumulate(newBlocks) {
9402
11601
  if (this.blocks.length === 0 && newBlocks.length > 0) {
9403
- this.streamingMessageId = randomUUID5();
11602
+ this.streamingMessageId = randomUUID6();
9404
11603
  }
9405
11604
  accumulateBlocks(this.blocks, newBlocks);
9406
11605
  }
@@ -9449,7 +11648,7 @@ async function bootstrap() {
9449
11648
  }
9450
11649
  };
9451
11650
  const createMessageHandlers = () => ({
9452
- auth: (socket, message) => {
11651
+ auth: async (socket, message) => {
9453
11652
  const authMessage = message;
9454
11653
  const deviceId = authMessage.payload.device_id;
9455
11654
  const result = authenticateClient.execute({
@@ -9467,6 +11666,21 @@ async function bootstrap() {
9467
11666
  );
9468
11667
  }
9469
11668
  }
11669
+ if (broadcaster) {
11670
+ const bufferedMessages = broadcaster.flushAuthBuffer(deviceId);
11671
+ if (bufferedMessages.length > 0) {
11672
+ logger.info(
11673
+ { deviceId, messageCount: bufferedMessages.length },
11674
+ "Flushing buffered messages after subscription restore"
11675
+ );
11676
+ for (const bufferedMsg of bufferedMessages) {
11677
+ await broadcaster.broadcastToSubscribers(
11678
+ bufferedMsg.sessionId,
11679
+ bufferedMsg.message
11680
+ );
11681
+ }
11682
+ }
11683
+ }
9470
11684
  sendToDevice(socket, deviceId, JSON.stringify(result));
9471
11685
  return Promise.resolve();
9472
11686
  },
@@ -9500,7 +11714,14 @@ async function bootstrap() {
9500
11714
  const inMemorySessions = sessionManager.getSessionInfos();
9501
11715
  const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
9502
11716
  logger.debug(
9503
- { persistedAgentSessions, inMemoryCount: inMemorySessions.length },
11717
+ {
11718
+ inMemoryCount: inMemorySessions.length,
11719
+ inMemorySessionTypes: inMemorySessions.map((s) => ({
11720
+ id: s.session_id,
11721
+ type: s.session_type
11722
+ })),
11723
+ persistedAgentCount: persistedAgentSessions.length
11724
+ },
9504
11725
  "Sync: fetched sessions"
9505
11726
  );
9506
11727
  const inMemorySessionIds = new Set(
@@ -9866,7 +12087,7 @@ async function bootstrap() {
9866
12087
  logger.info({ wasCancelled }, "supervisorAgent.cancel() returned");
9867
12088
  if (messageBroadcaster) {
9868
12089
  const cancelBlock = {
9869
- id: randomUUID5(),
12090
+ id: randomUUID6(),
9870
12091
  block_type: "cancel",
9871
12092
  content: "Cancelled by user"
9872
12093
  };
@@ -9911,7 +12132,7 @@ async function bootstrap() {
9911
12132
  return Promise.resolve();
9912
12133
  },
9913
12134
  // Clear supervisor conversation history (global)
9914
- "supervisor.clear_context": (socket, message) => {
12135
+ "supervisor.clear_context": async (socket, message) => {
9915
12136
  const clearMessage = message;
9916
12137
  const directClient = clientRegistry.getBySocket(socket);
9917
12138
  const tunnelClient2 = clearMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(clearMessage.device_id)) : void 0;
@@ -10034,18 +12255,21 @@ async function bootstrap() {
10034
12255
  const sessionId = subscribeMessage.session_id;
10035
12256
  const agentSession = agentSessionManager.getSession(sessionId);
10036
12257
  const isPersistedAgent = !agentSession && chatHistoryService.getActiveAgentSessions().some((s) => s.sessionId === sessionId);
10037
- if (agentSession || isPersistedAgent) {
12258
+ const backlogManagers = sessionManager.getBacklogManagers();
12259
+ const isBacklogSession = backlogManagers.has(sessionId);
12260
+ if (agentSession || isPersistedAgent || isBacklogSession) {
10038
12261
  client.subscribe(new SessionId(sessionId));
10039
12262
  logger.info(
10040
12263
  {
10041
12264
  deviceId: client.deviceId.value,
10042
12265
  sessionId,
12266
+ sessionType: isBacklogSession ? "backlog-agent" : "agent",
10043
12267
  allSubscriptions: client.getSubscriptions(),
10044
12268
  clientStatus: client.status
10045
12269
  },
10046
- "Client subscribed to agent session"
12270
+ "Client subscribed to session"
10047
12271
  );
10048
- const isExecuting = agentSessionManager.isExecuting(sessionId);
12272
+ const isExecuting = isBacklogSession ? false : agentSessionManager.isExecuting(sessionId);
10049
12273
  const currentStreamingBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10050
12274
  const streamingMessageId = currentStreamingBlocks.length > 0 ? agentStreamingMessageIds.get(sessionId) : void 0;
10051
12275
  sendToDevice(
@@ -10063,11 +12287,12 @@ async function bootstrap() {
10063
12287
  {
10064
12288
  deviceId: client.deviceId.value,
10065
12289
  sessionId,
12290
+ sessionType: isBacklogSession ? "backlog-agent" : "agent",
10066
12291
  isExecuting,
10067
12292
  streamingBlocksCount: currentStreamingBlocks.length,
10068
12293
  streamingMessageId
10069
12294
  },
10070
- "Agent session subscribed (v1.13 - use history.request for messages)"
12295
+ "Session subscribed (use history.request for messages)"
10071
12296
  );
10072
12297
  } else {
10073
12298
  const result = subscriptionService.subscribe(
@@ -10100,21 +12325,42 @@ async function bootstrap() {
10100
12325
  return Promise.resolve();
10101
12326
  },
10102
12327
  "session.unsubscribe": (socket, message) => {
10103
- if (!subscriptionService) return Promise.resolve();
10104
12328
  const unsubscribeMessage = message;
10105
12329
  const client = clientRegistry.getBySocket(socket) ?? (unsubscribeMessage.device_id ? clientRegistry.getByDeviceId(
10106
12330
  new DeviceId(unsubscribeMessage.device_id)
10107
12331
  ) : void 0);
10108
12332
  if (client?.isAuthenticated) {
10109
- const result = subscriptionService.unsubscribe(
10110
- client.deviceId.value,
10111
- unsubscribeMessage.session_id
10112
- );
10113
- sendToDevice(
10114
- socket,
10115
- unsubscribeMessage.device_id,
10116
- JSON.stringify(result)
10117
- );
12333
+ const sessionId = unsubscribeMessage.session_id;
12334
+ const backlogManagers = sessionManager.getBacklogManagers();
12335
+ const isBacklogSession = backlogManagers.has(sessionId);
12336
+ if (isBacklogSession) {
12337
+ client.unsubscribe(new SessionId(sessionId));
12338
+ logger.info(
12339
+ {
12340
+ deviceId: client.deviceId.value,
12341
+ sessionId
12342
+ },
12343
+ "Client unsubscribed from backlog-agent session"
12344
+ );
12345
+ sendToDevice(
12346
+ socket,
12347
+ unsubscribeMessage.device_id,
12348
+ JSON.stringify({
12349
+ type: "session.unsubscribed",
12350
+ session_id: sessionId
12351
+ })
12352
+ );
12353
+ } else if (subscriptionService) {
12354
+ const result = subscriptionService.unsubscribe(
12355
+ client.deviceId.value,
12356
+ sessionId
12357
+ );
12358
+ sendToDevice(
12359
+ socket,
12360
+ unsubscribeMessage.device_id,
12361
+ JSON.stringify(result)
12362
+ );
12363
+ }
10118
12364
  }
10119
12365
  return Promise.resolve();
10120
12366
  },
@@ -10140,6 +12386,153 @@ async function bootstrap() {
10140
12386
  );
10141
12387
  }
10142
12388
  cancelledDuringTranscription.delete(sessionId);
12389
+ const backlogManagers = sessionManager.getBacklogManagers();
12390
+ const isBacklogSession = backlogManagers.has(sessionId);
12391
+ if (isBacklogSession) {
12392
+ const manager = backlogManagers.get(sessionId);
12393
+ if (!manager) {
12394
+ logger.error(
12395
+ { sessionId },
12396
+ "Backlog manager found in map but is undefined - this should not happen"
12397
+ );
12398
+ const errorMessage = {
12399
+ type: "session.output",
12400
+ session_id: sessionId,
12401
+ payload: {
12402
+ content_blocks: [
12403
+ {
12404
+ id: "error",
12405
+ blockType: "error",
12406
+ content: "Internal error: backlog manager not properly initialized"
12407
+ }
12408
+ ],
12409
+ content: "Internal error: backlog manager not properly initialized",
12410
+ content_type: "agent",
12411
+ is_complete: true,
12412
+ timestamp: Date.now(),
12413
+ message_id: messageId
12414
+ }
12415
+ };
12416
+ if (messageBroadcaster) {
12417
+ void messageBroadcaster.broadcastToSubscribers(
12418
+ sessionId,
12419
+ JSON.stringify(errorMessage)
12420
+ );
12421
+ }
12422
+ return;
12423
+ }
12424
+ const prompt = execMessage.payload.content ?? execMessage.payload.text ?? execMessage.payload.prompt ?? "";
12425
+ if (prompt) {
12426
+ chatHistoryService.saveMessage(sessionId, createUserMessage(prompt), true);
12427
+ }
12428
+ try {
12429
+ const blocks = await manager.executeCommand(prompt);
12430
+ if (blocks.length > 0) {
12431
+ const assistantContent = blocks.map((b) => b.content).join("\n");
12432
+ chatHistoryService.saveMessage(
12433
+ sessionId,
12434
+ createAssistantMessage(assistantContent),
12435
+ true
12436
+ );
12437
+ }
12438
+ if (messageBroadcaster) {
12439
+ const streamingMessageId = getOrCreateBacklogStreamingMessageId(sessionId);
12440
+ const protocolBlocks = blocks.map((block) => ({
12441
+ id: block.id,
12442
+ blockType: block.block_type,
12443
+ content: block.content,
12444
+ metadata: "metadata" in block ? block.metadata : void 0
12445
+ }));
12446
+ const contentText = protocolBlocks.map((b) => b.content).join("\n");
12447
+ const streamingMessage = {
12448
+ type: "session.output",
12449
+ session_id: sessionId,
12450
+ streaming_message_id: streamingMessageId,
12451
+ payload: {
12452
+ content_type: "agent",
12453
+ content: contentText,
12454
+ content_blocks: protocolBlocks,
12455
+ timestamp: Date.now(),
12456
+ is_complete: false
12457
+ }
12458
+ };
12459
+ void messageBroadcaster.broadcastToSubscribers(
12460
+ sessionId,
12461
+ JSON.stringify(streamingMessage)
12462
+ );
12463
+ const completionMessage = {
12464
+ type: "session.output",
12465
+ session_id: sessionId,
12466
+ streaming_message_id: streamingMessageId,
12467
+ payload: {
12468
+ content_type: "agent",
12469
+ content: contentText,
12470
+ content_blocks: protocolBlocks,
12471
+ timestamp: Date.now(),
12472
+ is_complete: true
12473
+ }
12474
+ };
12475
+ void messageBroadcaster.broadcastToSubscribers(
12476
+ sessionId,
12477
+ JSON.stringify(completionMessage)
12478
+ );
12479
+ clearBacklogStreamingMessageId(sessionId);
12480
+ }
12481
+ } catch (error) {
12482
+ const errorContent = error instanceof Error ? error.message : "Unknown error";
12483
+ logger.error(
12484
+ { error, sessionId, errorContent, stack: error instanceof Error ? error.stack : void 0 },
12485
+ "Failed to execute backlog command"
12486
+ );
12487
+ chatHistoryService.saveMessage(
12488
+ sessionId,
12489
+ createErrorMessage(errorContent),
12490
+ true
12491
+ );
12492
+ if (messageBroadcaster) {
12493
+ const streamingMessageId = getOrCreateBacklogStreamingMessageId(sessionId);
12494
+ const errorBlock = {
12495
+ id: "error",
12496
+ blockType: "error",
12497
+ content: errorContent
12498
+ };
12499
+ const errorStreamingMessage = {
12500
+ type: "session.output",
12501
+ session_id: sessionId,
12502
+ streaming_message_id: streamingMessageId,
12503
+ payload: {
12504
+ content_type: "agent",
12505
+ content: errorContent,
12506
+ content_blocks: [errorBlock],
12507
+ timestamp: Date.now(),
12508
+ is_complete: false
12509
+ }
12510
+ };
12511
+ void messageBroadcaster.broadcastToSubscribers(
12512
+ sessionId,
12513
+ JSON.stringify(errorStreamingMessage)
12514
+ );
12515
+ const errorCompletionMessage = {
12516
+ type: "session.output",
12517
+ session_id: sessionId,
12518
+ streaming_message_id: streamingMessageId,
12519
+ payload: {
12520
+ content_type: "agent",
12521
+ content: errorContent,
12522
+ content_blocks: [errorBlock],
12523
+ timestamp: Date.now(),
12524
+ is_complete: true
12525
+ }
12526
+ };
12527
+ void messageBroadcaster.broadcastToSubscribers(
12528
+ sessionId,
12529
+ JSON.stringify(errorCompletionMessage)
12530
+ );
12531
+ clearBacklogStreamingMessageId(sessionId);
12532
+ }
12533
+ }
12534
+ return;
12535
+ }
10143
12536
  if (execMessage.payload.audio) {
10144
12537
  logger.info(
10145
12538
  { sessionId, hasAudio: true, messageId },
@@ -10408,7 +12801,7 @@ async function bootstrap() {
10408
12801
  }
10409
12802
  if (messageBroadcaster) {
10410
12803
  const cancelBlock = {
10411
- id: randomUUID5(),
12804
+ id: randomUUID6(),
10412
12805
  block_type: "cancel",
10413
12806
  content: "Cancelled by user"
10414
12807
  };
@@ -10869,8 +13262,27 @@ async function bootstrap() {
10869
13262
  tunnelClient,
10870
13263
  authenticateClient,
10871
13264
  logger,
10872
- subscriptionService
13265
+ subscriptionService,
13266
+ broadcaster
10873
13267
  );
13268
+ } else if (messageType === "supervisor.context_cleared.ack") {
13269
+ try {
13270
+ const ackData = data;
13271
+ const broadcastId = ackData.payload?.broadcast_id;
13272
+ const deviceId = ackData.payload?.device_id;
13273
+ if (broadcastId && deviceId && supervisorAgent) {
13274
+ supervisorAgent.recordClearAck(broadcastId, deviceId);
13275
+ logger.debug(
13276
+ { broadcastId, deviceId },
13277
+ "Recorded context clear acknowledgment"
13278
+ );
13279
+ }
13280
+ } catch (ackError) {
13281
+ logger.warn(
13282
+ { error: ackError, message: message.slice(0, 100) },
13283
+ "Failed to process context clear acknowledgment"
13284
+ );
13285
+ }
10874
13286
  } else {
10875
13287
  handleTunnelMessage(
10876
13288
  message,
@@ -10932,7 +13344,7 @@ async function bootstrap() {
10932
13344
  const getOrCreateAgentStreamingMessageId = (sessionId) => {
10933
13345
  let messageId = agentStreamingMessageIds.get(sessionId);
10934
13346
  if (!messageId) {
10935
- messageId = randomUUID5();
13347
+ messageId = randomUUID6();
10936
13348
  agentStreamingMessageIds.set(sessionId, messageId);
10937
13349
  }
10938
13350
  return messageId;
@@ -10949,6 +13361,18 @@ async function bootstrap() {
10949
13361
  }, STREAMING_STATE_GRACE_PERIOD_MS);
10950
13362
  agentCleanupTimeouts.set(sessionId, timeout);
10951
13363
  };
13364
+ const backlogStreamingMessageIds = /* @__PURE__ */ new Map();
13365
+ const getOrCreateBacklogStreamingMessageId = (sessionId) => {
13366
+ let messageId = backlogStreamingMessageIds.get(sessionId);
13367
+ if (!messageId) {
13368
+ messageId = randomUUID6();
13369
+ backlogStreamingMessageIds.set(sessionId, messageId);
13370
+ }
13371
+ return messageId;
13372
+ };
13373
+ const clearBacklogStreamingMessageId = (sessionId) => {
13374
+ backlogStreamingMessageIds.delete(sessionId);
13375
+ };
10952
13376
  agentSessionManager.on(
10953
13377
  "blocks",
10954
13378
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -11516,6 +13940,11 @@ bootstrap().catch((error) => {
11516
13940
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11517
13941
  * @license FSL-1.1-NC
11518
13942
  */
13943
+ /**
13944
+ * @file session-repository.ts
13945
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
13946
+ * @license FSL-1.1-NC
13947
+ */
11519
13948
  /**
11520
13949
  * @file schemas.ts
11521
13950
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
@@ -11658,11 +14087,6 @@ bootstrap().catch((error) => {
11658
14087
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11659
14088
  * @license FSL-1.1-NC
11660
14089
  */
11661
- /**
11662
- * @file session-repository.ts
11663
- * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11664
- * @license FSL-1.1-NC
11665
- */
11666
14090
  /**
11667
14091
  * @file audio-storage.ts
11668
14092
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
@@ -11680,11 +14104,69 @@ bootstrap().catch((error) => {
11680
14104
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11681
14105
  * @license FSL-1.1-NC
11682
14106
  */
14107
+ /**
14108
+ * @file backlog-agent-session.ts
14109
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14110
+ * @license FSL-1.1-NC
14111
+ */
14112
+ /**
14113
+ * @file backlog.ts
14114
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14115
+ * @license FSL-1.1-NC
14116
+ */
14117
+ /**
14118
+ * @file backlog-harness.ts
14119
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14120
+ * @license FSL-1.1-NC
14121
+ */
14122
+ /**
14123
+ * @file lang-graph-agent.ts
14124
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14125
+ * @license FSL-1.1-NC
14126
+ *
14127
+ * Abstract base class for all LangGraph-based agents.
14128
+ * Provides unified streaming, state management, and event emission patterns.
14129
+ */
14130
+ /**
14131
+ * @file backlog-state-manager.ts
14132
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14133
+ * @license FSL-1.1-NC
14134
+ *
14135
+ * AgentStateManager implementation for backlog persistence.
14136
+ * Persists both conversation history and backlog state to files and database.
14137
+ */
14138
+ /**
14139
+ * @file backlog-agent-tools.ts
14140
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14141
+ * @license FSL-1.1-NC
14142
+ *
14143
+ * LangGraph tools for backlog agent operations.
14144
+ */
14145
+ /**
14146
+ * @file backlog-agent.ts
14147
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14148
+ * @license FSL-1.1-NC
14149
+ *
14150
+ * LangGraph-based Backlog Agent for executing backlog commands.
14151
+ * Extends LangGraphAgent base class for unified streaming and state management.
14152
+ */
14153
+ /**
14154
+ * @file backlog-agent-manager.ts
14155
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14156
+ * @license FSL-1.1-NC
14157
+ */
11683
14158
  /**
11684
14159
  * @file in-memory-session-manager.ts
11685
14160
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11686
14161
  * @license FSL-1.1-NC
11687
14162
  */
14163
+ /**
14164
+ * @file supervisor-state-manager.ts
14165
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14166
+ * @license FSL-1.1-NC
14167
+ *
14168
+ * AgentStateManager implementation for SupervisorAgent database persistence.
14169
+ */
11688
14170
  /**
11689
14171
  * @file workspace-tools.ts
11690
14172
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
@@ -11713,12 +14195,18 @@ bootstrap().catch((error) => {
11713
14195
  *
11714
14196
  * LangGraph tools for file system operations.
11715
14197
  */
14198
+ /**
14199
+ * @file backlog-tools.ts
14200
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14201
+ * @license FSL-1.1-NC
14202
+ */
11716
14203
  /**
11717
14204
  * @file supervisor-agent.ts
11718
14205
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11719
14206
  * @license FSL-1.1-NC
11720
14207
  *
11721
14208
  * LangGraph-based Supervisor Agent for managing workstation resources.
14209
+ * Extends LangGraphAgent base class for unified streaming and state management.
11722
14210
  */
11723
14211
  /**
11724
14212
  * @file fixture-loader.ts
@@ -11772,6 +14260,11 @@ bootstrap().catch((error) => {
11772
14260
  * Service for summarizing long responses before TTS synthesis.
11773
14261
  * Uses the same LLM provider as the supervisor agent.
11774
14262
  */
14263
+ /**
14264
+ * @file chat-message.ts
14265
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
14266
+ * @license FSL-1.1-NC
14267
+ */
11775
14268
  /**
11776
14269
  * @file main.ts
11777
14270
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>