@tiflis-io/tiflis-code-workstation 0.3.4 → 0.3.6

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 +1854 -133
  2. package/package.json +1 -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 randomUUID4 } from "crypto";
8
+ import { randomUUID as randomUUID5 } from "crypto";
9
9
 
10
10
  // src/app.ts
11
11
  import Fastify from "fastify";
@@ -129,7 +129,14 @@ var EnvSchema = z.object({
129
129
  /** Terminal output buffer size (number of messages, in-memory only, does not survive restarts) */
130
130
  TERMINAL_OUTPUT_BUFFER_SIZE: z.coerce.number().default(100),
131
131
  // Legacy (fallback for STT/TTS if specific keys not set)
132
- OPENAI_API_KEY: z.string().optional()
132
+ OPENAI_API_KEY: z.string().optional(),
133
+ // ─────────────────────────────────────────────────────────────
134
+ // Mock Mode Configuration (for screenshot automation)
135
+ // ─────────────────────────────────────────────────────────────
136
+ /** Enable mock mode for screenshot automation tests */
137
+ MOCK_MODE: z.string().transform((val) => val?.toLowerCase() === "true").default("false"),
138
+ /** Path to mock fixtures directory (defaults to built-in fixtures) */
139
+ MOCK_FIXTURES_PATH: z.string().optional()
133
140
  });
134
141
  function parseAgentAliases() {
135
142
  const aliases = /* @__PURE__ */ new Map();
@@ -433,14 +440,21 @@ function getAgentConfig(agentName) {
433
440
  }
434
441
  function getWorkstationVersion() {
435
442
  try {
436
- const __filename = fileURLToPath(import.meta.url);
437
- const __dirname = dirname(__filename);
438
- const packageJsonPath = join2(__dirname, "../../package.json");
439
- const packageJsonContent = readFileSync(packageJsonPath, "utf-8");
440
- const packageJson = JSON.parse(packageJsonContent);
441
- const version = packageJson.version;
442
- if (typeof version === "string" && version.length > 0) {
443
- return version;
443
+ const __filename2 = fileURLToPath(import.meta.url);
444
+ const __dirname2 = dirname(__filename2);
445
+ const possiblePaths = [
446
+ join2(__dirname2, "../package.json"),
447
+ join2(__dirname2, "../../package.json")
448
+ ];
449
+ for (const packageJsonPath of possiblePaths) {
450
+ try {
451
+ const packageJsonContent = readFileSync(packageJsonPath, "utf-8");
452
+ const packageJson = JSON.parse(packageJsonContent);
453
+ if (packageJson.name === "@tiflis-io/tiflis-code-workstation" && typeof packageJson.version === "string" && packageJson.version.length > 0) {
454
+ return packageJson.version;
455
+ }
456
+ } catch {
457
+ }
444
458
  }
445
459
  return "0.0.0";
446
460
  } catch {
@@ -1124,8 +1138,21 @@ var HeartbeatSchema = z2.object({
1124
1138
  var SyncMessageSchema = z2.object({
1125
1139
  type: z2.literal("sync"),
1126
1140
  id: z2.string(),
1127
- device_id: z2.string().optional()
1141
+ device_id: z2.string().optional(),
1142
+ // Injected by tunnel for tunnel connections
1143
+ lightweight: z2.boolean().optional()
1144
+ // If true, excludes message histories (for watchOS)
1145
+ });
1146
+ var HistoryRequestPayloadSchema = z2.object({
1147
+ session_id: z2.string().optional()
1148
+ // If omitted, returns supervisor history
1149
+ });
1150
+ var HistoryRequestSchema = z2.object({
1151
+ type: z2.literal("history.request"),
1152
+ id: z2.string(),
1153
+ device_id: z2.string().optional(),
1128
1154
  // Injected by tunnel for tunnel connections
1155
+ payload: HistoryRequestPayloadSchema.optional()
1129
1156
  });
1130
1157
  var ListSessionsSchema = z2.object({
1131
1158
  type: z2.literal("supervisor.list_sessions"),
@@ -1282,6 +1309,7 @@ var IncomingClientMessageSchema = z2.discriminatedUnion("type", [
1282
1309
  PingSchema,
1283
1310
  HeartbeatSchema,
1284
1311
  SyncMessageSchema,
1312
+ HistoryRequestSchema,
1285
1313
  ListSessionsSchema,
1286
1314
  CreateSessionSchema,
1287
1315
  TerminateSessionSchema,
@@ -2181,6 +2209,186 @@ var FileSystemWorkspaceDiscovery = class {
2181
2209
  worktrees: []
2182
2210
  };
2183
2211
  }
2212
+ /**
2213
+ * Gets current branch and uncommitted changes status for a project.
2214
+ */
2215
+ async getBranchStatus(workspace, project) {
2216
+ const projectPath = join4(this.workspacesRoot, workspace, project);
2217
+ if (!await this.isGitRepository(projectPath)) {
2218
+ throw new Error(`Project "${project}" is not a git repository`);
2219
+ }
2220
+ try {
2221
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
2222
+ cwd: projectPath,
2223
+ encoding: "utf-8"
2224
+ }).trim();
2225
+ const statusOutput = execSync("git status --porcelain", {
2226
+ cwd: projectPath,
2227
+ encoding: "utf-8"
2228
+ });
2229
+ const uncommittedChanges = statusOutput.trim().split("\n").filter((line) => line.length > 0);
2230
+ let aheadCommits = 0;
2231
+ const mainBranches = ["main", "master"];
2232
+ for (const mainBranch of mainBranches) {
2233
+ try {
2234
+ aheadCommits = parseInt(execSync(`git rev-list --count ${mainBranch}..${currentBranch}`, {
2235
+ cwd: projectPath,
2236
+ encoding: "utf-8"
2237
+ }).trim(), 10);
2238
+ break;
2239
+ } catch {
2240
+ continue;
2241
+ }
2242
+ }
2243
+ return {
2244
+ currentBranch,
2245
+ uncommittedChanges,
2246
+ aheadCommits,
2247
+ isClean: uncommittedChanges.length === 0
2248
+ };
2249
+ } catch {
2250
+ throw new Error("Failed to get branch status");
2251
+ }
2252
+ }
2253
+ /**
2254
+ * Merges source branch into target branch with safety checks.
2255
+ */
2256
+ async mergeBranch(workspace, project, sourceBranch, targetBranch = "main", options = {}) {
2257
+ const projectPath = join4(this.workspacesRoot, workspace, project);
2258
+ if (!await this.isGitRepository(projectPath)) {
2259
+ throw new Error(`Project "${project}" is not a git repository`);
2260
+ }
2261
+ try {
2262
+ if (!options.skipPreCheck) {
2263
+ const status = await this.getBranchStatus(workspace, project);
2264
+ if (!status.isClean) {
2265
+ return {
2266
+ success: false,
2267
+ message: `Cannot merge: uncommitted changes exist in ${status.currentBranch}`
2268
+ };
2269
+ }
2270
+ }
2271
+ execSync(`git checkout "${targetBranch}"`, { cwd: projectPath });
2272
+ try {
2273
+ execSync(`git pull origin "${targetBranch}"`, { cwd: projectPath });
2274
+ } catch {
2275
+ console.warn(`Failed to pull ${targetBranch} from remote, continuing with local merge`);
2276
+ }
2277
+ try {
2278
+ execSync(`git merge "${sourceBranch}"`, { cwd: projectPath });
2279
+ } catch (error) {
2280
+ const conflicts = execSync("git diff --name-only --diff-filter=U", {
2281
+ cwd: projectPath,
2282
+ encoding: "utf-8"
2283
+ }).trim().split("\n").filter((f) => f.length > 0);
2284
+ return {
2285
+ success: false,
2286
+ message: `Merge conflicts in files: ${conflicts.join(", ")}`,
2287
+ conflicts
2288
+ };
2289
+ }
2290
+ if (options.pushAfter) {
2291
+ try {
2292
+ execSync(`git push origin "${targetBranch}"`, { cwd: projectPath });
2293
+ } catch {
2294
+ console.warn(`Failed to push ${targetBranch} to remote`);
2295
+ }
2296
+ }
2297
+ return {
2298
+ success: true,
2299
+ message: `Successfully merged "${sourceBranch}" into "${targetBranch}"`
2300
+ };
2301
+ } catch (error) {
2302
+ return {
2303
+ success: false,
2304
+ message: `Merge failed: ${error instanceof Error ? error.message : String(error)}`
2305
+ };
2306
+ }
2307
+ }
2308
+ /**
2309
+ * Checks if branch is merged into target branch.
2310
+ */
2311
+ async isBranchMerged(workspace, project, branch, targetBranch) {
2312
+ const projectPath = join4(this.workspacesRoot, workspace, project);
2313
+ if (!await this.isGitRepository(projectPath)) {
2314
+ return false;
2315
+ }
2316
+ try {
2317
+ const mergeBase = execSync(`git merge-base "${targetBranch}" "${branch}"`, {
2318
+ cwd: projectPath,
2319
+ encoding: "utf-8"
2320
+ }).trim();
2321
+ const branchHead = execSync(`git rev-parse "${branch}"`, {
2322
+ cwd: projectPath,
2323
+ encoding: "utf-8"
2324
+ }).trim();
2325
+ return mergeBase === branchHead;
2326
+ } catch {
2327
+ return false;
2328
+ }
2329
+ }
2330
+ /**
2331
+ * Cleans up worktree and safely deletes the branch if merged.
2332
+ */
2333
+ async cleanupWorktreeAndBranch(workspace, project, branch) {
2334
+ const projectPath = join4(this.workspacesRoot, workspace, project);
2335
+ try {
2336
+ let branchDeleted = false;
2337
+ await this.removeWorktree(workspace, project, branch);
2338
+ const mainBranches = ["main", "master"];
2339
+ for (const mainBranch of mainBranches) {
2340
+ try {
2341
+ const isMerged = await this.isBranchMerged(workspace, project, branch, mainBranch);
2342
+ if (isMerged) {
2343
+ execSync(`git branch -d "${branch}"`, { cwd: projectPath });
2344
+ branchDeleted = true;
2345
+ break;
2346
+ }
2347
+ } catch {
2348
+ continue;
2349
+ }
2350
+ }
2351
+ return {
2352
+ success: true,
2353
+ message: `Cleaned up worktree for "${branch}"${branchDeleted ? " and deleted merged branch" : ""}`,
2354
+ branchDeleted
2355
+ };
2356
+ } catch (error) {
2357
+ return {
2358
+ success: false,
2359
+ message: `Cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
2360
+ branchDeleted: false
2361
+ };
2362
+ }
2363
+ }
2364
+ /**
2365
+ * Lists mergeable branches with their status.
2366
+ */
2367
+ async listMergeableBranches(workspace, project) {
2368
+ const worktrees = await this.listWorktrees(workspace, project);
2369
+ const mergeableBranches = [];
2370
+ for (const worktree of worktrees) {
2371
+ if (worktree.branch === "main" || worktree.branch === "master") continue;
2372
+ try {
2373
+ const [isMerged, hasChanges, aheadCommits] = await Promise.all([
2374
+ this.isBranchMerged(workspace, project, worktree.branch, "main"),
2375
+ this.getBranchStatus(workspace, project).then((status) => !status.isClean),
2376
+ this.getBranchStatus(workspace, project).then((status) => status.aheadCommits)
2377
+ ]);
2378
+ mergeableBranches.push({
2379
+ branch: worktree.branch,
2380
+ path: worktree.path,
2381
+ isMerged: await this.isBranchMerged(workspace, project, worktree.branch, "main"),
2382
+ hasUncommittedChanges: hasChanges,
2383
+ canCleanup: isMerged && !hasChanges,
2384
+ aheadCommits
2385
+ });
2386
+ } catch {
2387
+ continue;
2388
+ }
2389
+ }
2390
+ return mergeableBranches;
2391
+ }
2184
2392
  };
2185
2393
 
2186
2394
  // src/infrastructure/terminal/pty-manager.ts
@@ -3593,6 +3801,87 @@ var AgentSessionManager = class extends EventEmitter2 {
3593
3801
  this.terminateSession(id);
3594
3802
  }
3595
3803
  }
3804
+ /**
3805
+ * Terminates sessions that are running in a specific worktree.
3806
+ * Returns the list of terminated session IDs.
3807
+ */
3808
+ terminateWorktreeSessions(workspace, project, branch) {
3809
+ const worktreePath = `/${workspace}/${project}--${branch}`;
3810
+ const terminatedSessions = [];
3811
+ for (const [sessionId, state] of this.sessions) {
3812
+ const isInWorktree = state.workingDir.includes(worktreePath) || state.cliSessionId?.includes(`${project}--${branch}`) || state.workingDir.endsWith(`${project}--${branch}`);
3813
+ if (isInWorktree) {
3814
+ try {
3815
+ if (state.isExecuting) {
3816
+ this.cancelCommand(sessionId);
3817
+ }
3818
+ this.terminateSession(sessionId);
3819
+ terminatedSessions.push(sessionId);
3820
+ this.logger.info({ sessionId, workspace, project, branch }, "Terminated worktree session");
3821
+ } catch (error) {
3822
+ this.logger.error({ sessionId, error }, "Failed to terminate worktree session");
3823
+ }
3824
+ }
3825
+ }
3826
+ return terminatedSessions;
3827
+ }
3828
+ /**
3829
+ * Gets session summary for a specific worktree.
3830
+ */
3831
+ getWorktreeSessionSummary(workspace, project, branch) {
3832
+ const worktreePath = `/${workspace}/${project}--${branch}`;
3833
+ const activeSessions = Array.from(this.sessions.values()).filter(
3834
+ (session) => session.workingDir.includes(worktreePath) || session.cliSessionId?.includes(`${project}--${branch}`) || session.workingDir.endsWith(`${project}--${branch}`)
3835
+ );
3836
+ const sessionTypes = [...new Set(activeSessions.map((s) => s.agentType))];
3837
+ const executingCount = activeSessions.filter((s) => s.isExecuting).length;
3838
+ return {
3839
+ activeSessions,
3840
+ sessionCount: activeSessions.length,
3841
+ sessionTypes,
3842
+ executingCount
3843
+ };
3844
+ }
3845
+ /**
3846
+ * Lists all sessions with their worktree information.
3847
+ */
3848
+ listSessionsWithWorktreeInfo() {
3849
+ return Array.from(this.sessions.values()).map((session) => {
3850
+ const worktreeInfo = this.parseWorktreeInfo(session.workingDir);
3851
+ return {
3852
+ sessionId: session.sessionId,
3853
+ agentType: session.agentType,
3854
+ agentName: session.agentName,
3855
+ workingDir: session.workingDir,
3856
+ isExecuting: session.isExecuting,
3857
+ worktreeInfo
3858
+ };
3859
+ });
3860
+ }
3861
+ /**
3862
+ * Parse worktree information from a working directory path.
3863
+ */
3864
+ parseWorktreeInfo(workingDir) {
3865
+ const parts = workingDir.split("/");
3866
+ if (parts.length < 3) {
3867
+ return { isWorktree: false };
3868
+ }
3869
+ const workspacesIndex = parts.findIndex((part) => part === "workspaces" || part.includes("workspace"));
3870
+ if (workspacesIndex === -1 || workspacesIndex + 2 >= parts.length) {
3871
+ return { isWorktree: false };
3872
+ }
3873
+ const workspace = parts[workspacesIndex + 1];
3874
+ const projectPart = parts[workspacesIndex + 2];
3875
+ if (!projectPart) {
3876
+ return { isWorktree: false };
3877
+ }
3878
+ if (projectPart.includes("--")) {
3879
+ const [project, branch] = projectPart.split("--");
3880
+ return { workspace, project, branch, isWorktree: true };
3881
+ } else {
3882
+ return { workspace, project: projectPart, isWorktree: false };
3883
+ }
3884
+ }
3596
3885
  /**
3597
3886
  * Setup event handlers for an executor.
3598
3887
  */
@@ -4279,7 +4568,14 @@ var SubscriptionService = class {
4279
4568
  }
4280
4569
  const isNew = client.subscribe(session);
4281
4570
  if (isNew) {
4282
- this.deps.subscriptionRepository.subscribe(deviceId, sessionId);
4571
+ try {
4572
+ this.deps.subscriptionRepository.subscribe(deviceId, sessionId);
4573
+ } catch (error) {
4574
+ this.logger.debug(
4575
+ { deviceId, sessionId, error },
4576
+ "Failed to persist subscription (may be expected in mock mode)"
4577
+ );
4578
+ }
4283
4579
  }
4284
4580
  let isMaster = false;
4285
4581
  let cols;
@@ -5400,6 +5696,274 @@ var ChatHistoryService = class _ChatHistoryService {
5400
5696
  );
5401
5697
  return agentSessions;
5402
5698
  }
5699
+ // ============================================================================
5700
+ // Mock Data Seeding (for Screenshot Automation)
5701
+ // ============================================================================
5702
+ /**
5703
+ * Seeds mock chat history for screenshot automation.
5704
+ * Creates realistic conversation history with voice messages, code blocks, etc.
5705
+ *
5706
+ * @param agentSessions - Object with agent session IDs and their working directories
5707
+ */
5708
+ seedMockData(agentSessions) {
5709
+ this.logger.info("Seeding mock chat history for screenshots...");
5710
+ this.seedSupervisorHistory();
5711
+ if (agentSessions.claude) {
5712
+ this.ensureAgentSession(agentSessions.claude.id, "claude", agentSessions.claude.workingDir);
5713
+ this.seedClaudeAgentHistory(agentSessions.claude.id);
5714
+ }
5715
+ if (agentSessions.cursor) {
5716
+ this.ensureAgentSession(agentSessions.cursor.id, "cursor", agentSessions.cursor.workingDir);
5717
+ this.seedCursorAgentHistory(agentSessions.cursor.id);
5718
+ }
5719
+ if (agentSessions.opencode) {
5720
+ this.ensureAgentSession(agentSessions.opencode.id, "opencode", agentSessions.opencode.workingDir);
5721
+ this.seedOpenCodeAgentHistory(agentSessions.opencode.id);
5722
+ }
5723
+ this.logger.info("Mock chat history seeded successfully");
5724
+ }
5725
+ /**
5726
+ * Ensures an agent session exists in the database.
5727
+ * Creates it if it doesn't exist.
5728
+ */
5729
+ ensureAgentSession(sessionId, sessionType, workingDir) {
5730
+ try {
5731
+ const existing = this.sessionRepo.getById(sessionId);
5732
+ if (!existing) {
5733
+ this.sessionRepo.create({
5734
+ id: sessionId,
5735
+ type: sessionType,
5736
+ workingDir
5737
+ });
5738
+ this.logger.debug({ sessionId, sessionType }, "Created agent session in database for seeding");
5739
+ }
5740
+ } catch {
5741
+ }
5742
+ }
5743
+ /**
5744
+ * Seeds Supervisor chat with a realistic voice conversation.
5745
+ */
5746
+ seedSupervisorHistory() {
5747
+ this.ensureSupervisorSession();
5748
+ const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5749
+ this.messageRepo.deleteBySession(sessionId);
5750
+ const voiceInput1 = {
5751
+ id: "vi-1",
5752
+ block_type: "voice_input",
5753
+ content: "Show me the available workspaces",
5754
+ metadata: { duration: 2.1, has_audio: true }
5755
+ };
5756
+ this.messageRepo.create({
5757
+ sessionId,
5758
+ role: "user",
5759
+ contentType: "transcription",
5760
+ content: "Show me the available workspaces",
5761
+ contentBlocks: JSON.stringify([voiceInput1]),
5762
+ isComplete: true
5763
+ });
5764
+ const textBlock1 = {
5765
+ id: "tb-1",
5766
+ block_type: "text",
5767
+ content: "I found 2 workspaces with several projects. The **work** workspace contains my-app and api-service. The **personal** workspace has your blog project. Would you like to start an agent session in any of these?"
5768
+ };
5769
+ const voiceOutput1 = {
5770
+ id: "vo-1",
5771
+ block_type: "voice_output",
5772
+ content: "I found 2 workspaces with several projects.",
5773
+ metadata: { duration: 4.2, has_audio: true }
5774
+ };
5775
+ this.messageRepo.create({
5776
+ sessionId,
5777
+ role: "assistant",
5778
+ contentType: "text",
5779
+ content: "I found 2 workspaces with several projects. The work workspace contains my-app and api-service. The personal workspace has your blog project. Would you like to start an agent session in any of these?",
5780
+ contentBlocks: JSON.stringify([textBlock1, voiceOutput1]),
5781
+ isComplete: true
5782
+ });
5783
+ const voiceInput2 = {
5784
+ id: "vi-2",
5785
+ block_type: "voice_input",
5786
+ content: "Start Claude on my-app",
5787
+ metadata: { duration: 1.8, has_audio: true }
5788
+ };
5789
+ this.messageRepo.create({
5790
+ sessionId,
5791
+ role: "user",
5792
+ contentType: "transcription",
5793
+ content: "Start Claude on my-app",
5794
+ contentBlocks: JSON.stringify([voiceInput2]),
5795
+ isComplete: true
5796
+ });
5797
+ const textBlock2 = {
5798
+ id: "tb-2",
5799
+ block_type: "text",
5800
+ content: "I've started a new Claude Code session in **work/my-app**. You can find it in the sidebar under Agent Sessions. The session is ready for your commands!"
5801
+ };
5802
+ const voiceOutput2 = {
5803
+ id: "vo-2",
5804
+ block_type: "voice_output",
5805
+ content: "I've started a new Claude Code session in work/my-app.",
5806
+ metadata: { duration: 3.5, has_audio: true }
5807
+ };
5808
+ this.messageRepo.create({
5809
+ sessionId,
5810
+ role: "assistant",
5811
+ contentType: "text",
5812
+ content: "I've started a new Claude Code session in work/my-app. You can find it in the sidebar under Agent Sessions. The session is ready for your commands!",
5813
+ contentBlocks: JSON.stringify([textBlock2, voiceOutput2]),
5814
+ isComplete: true
5815
+ });
5816
+ this.logger.debug("Seeded Supervisor history with voice conversation");
5817
+ }
5818
+ /**
5819
+ * Seeds Claude agent chat with code examples and tool use.
5820
+ */
5821
+ seedClaudeAgentHistory(sessionId) {
5822
+ this.messageRepo.deleteBySession(sessionId);
5823
+ const userVoiceBlock = {
5824
+ id: "vi-claude-1",
5825
+ block_type: "voice_input",
5826
+ content: "Add a health check endpoint to the API",
5827
+ metadata: {
5828
+ duration: 2.3,
5829
+ has_audio: true
5830
+ }
5831
+ };
5832
+ this.messageRepo.create({
5833
+ sessionId,
5834
+ role: "user",
5835
+ contentType: "audio",
5836
+ content: "Add a health check endpoint to the API",
5837
+ contentBlocks: JSON.stringify([userVoiceBlock]),
5838
+ isComplete: true
5839
+ });
5840
+ const thinkingBlock = {
5841
+ id: "think-1",
5842
+ block_type: "thinking",
5843
+ content: "I'll add a simple health check endpoint that returns the server status and version information."
5844
+ };
5845
+ const toolBlock = {
5846
+ id: "tool-1",
5847
+ block_type: "tool",
5848
+ content: "Edit",
5849
+ metadata: {
5850
+ tool_name: "Edit",
5851
+ tool_status: "completed",
5852
+ tool_input: JSON.stringify({ file: "src/routes/health.ts" })
5853
+ }
5854
+ };
5855
+ const codeBlock = {
5856
+ id: "code-1",
5857
+ block_type: "code",
5858
+ content: `import { Router } from 'express';
5859
+
5860
+ const router = Router();
5861
+
5862
+ router.get('/health', (req, res) => {
5863
+ res.json({
5864
+ status: 'healthy',
5865
+ version: process.env.npm_package_version,
5866
+ uptime: process.uptime()
5867
+ });
5868
+ });
5869
+
5870
+ export default router;`,
5871
+ metadata: { language: "typescript" }
5872
+ };
5873
+ const textBlock = {
5874
+ id: "text-1",
5875
+ block_type: "text",
5876
+ content: "I've added a health check endpoint at `/health` that returns the server status, version, and uptime. You can test it with `curl http://localhost:3000/health`."
5877
+ };
5878
+ this.messageRepo.create({
5879
+ sessionId,
5880
+ role: "assistant",
5881
+ contentType: "text",
5882
+ content: "I've added a health check endpoint at /health that returns the server status, version, and uptime.",
5883
+ contentBlocks: JSON.stringify([thinkingBlock, toolBlock, codeBlock, textBlock]),
5884
+ isComplete: true
5885
+ });
5886
+ this.logger.debug({ sessionId }, "Seeded Claude agent history with code example");
5887
+ }
5888
+ /**
5889
+ * Seeds Cursor agent chat.
5890
+ */
5891
+ seedCursorAgentHistory(sessionId) {
5892
+ this.messageRepo.deleteBySession(sessionId);
5893
+ this.messageRepo.create({
5894
+ sessionId,
5895
+ role: "user",
5896
+ contentType: "text",
5897
+ content: "Explain the project structure",
5898
+ isComplete: true
5899
+ });
5900
+ const textBlock = {
5901
+ id: "text-cursor-1",
5902
+ block_type: "text",
5903
+ content: `This is a **Next.js portfolio site** with the following structure:
5904
+
5905
+ - \`/app\` - App router pages and layouts
5906
+ - \`/components\` - Reusable React components
5907
+ - \`/lib\` - Utility functions and helpers
5908
+ - \`/public\` - Static assets (images, fonts)
5909
+ - \`/styles\` - Global CSS and Tailwind config
5910
+
5911
+ The site uses **Tailwind CSS** for styling and **Framer Motion** for animations. Would you like me to explain any specific part in more detail?`
5912
+ };
5913
+ this.messageRepo.create({
5914
+ sessionId,
5915
+ role: "assistant",
5916
+ contentType: "text",
5917
+ content: "This is a Next.js portfolio site with app router, components, lib, public, and styles directories.",
5918
+ contentBlocks: JSON.stringify([textBlock]),
5919
+ isComplete: true
5920
+ });
5921
+ this.logger.debug({ sessionId }, "Seeded Cursor agent history");
5922
+ }
5923
+ /**
5924
+ * Seeds OpenCode agent chat.
5925
+ */
5926
+ seedOpenCodeAgentHistory(sessionId) {
5927
+ this.messageRepo.deleteBySession(sessionId);
5928
+ this.messageRepo.create({
5929
+ sessionId,
5930
+ role: "user",
5931
+ contentType: "text",
5932
+ content: "Run the tests",
5933
+ isComplete: true
5934
+ });
5935
+ const statusBlock = {
5936
+ id: "status-1",
5937
+ block_type: "status",
5938
+ content: "Running tests..."
5939
+ };
5940
+ const codeBlock = {
5941
+ id: "code-oc-1",
5942
+ block_type: "code",
5943
+ content: `\u2713 auth.test.ts (3 tests) 120ms
5944
+ \u2713 api.test.ts (8 tests) 340ms
5945
+ \u2713 utils.test.ts (5 tests) 45ms
5946
+
5947
+ Test Files 3 passed (3)
5948
+ Tests 16 passed (16)
5949
+ Time 0.51s`,
5950
+ metadata: { language: "shell" }
5951
+ };
5952
+ const textBlock = {
5953
+ id: "text-oc-1",
5954
+ block_type: "text",
5955
+ content: "All 16 tests passed across 3 test files. The test suite completed in 0.51 seconds."
5956
+ };
5957
+ this.messageRepo.create({
5958
+ sessionId,
5959
+ role: "assistant",
5960
+ contentType: "text",
5961
+ content: "All 16 tests passed across 3 test files.",
5962
+ contentBlocks: JSON.stringify([statusBlock, codeBlock, textBlock]),
5963
+ isComplete: true
5964
+ });
5965
+ this.logger.debug({ sessionId }, "Seeded OpenCode agent history");
5966
+ }
5403
5967
  };
5404
5968
 
5405
5969
  // src/infrastructure/persistence/in-memory-session-manager.ts
@@ -5457,6 +6021,21 @@ var InMemorySessionManager = class extends EventEmitter3 {
5457
6021
  * Sets up event listeners to sync agent session state.
5458
6022
  */
5459
6023
  setupAgentSessionSync() {
6024
+ this.agentSessionManager.on("sessionCreated", (state) => {
6025
+ if (!this.sessions.has(state.sessionId)) {
6026
+ const session = new AgentSession({
6027
+ id: new SessionId(state.sessionId),
6028
+ type: state.agentType,
6029
+ agentName: state.agentName,
6030
+ workingDir: state.workingDir
6031
+ });
6032
+ this.sessions.set(state.sessionId, session);
6033
+ this.logger.debug(
6034
+ { sessionId: state.sessionId, agentType: state.agentType },
6035
+ "Agent session registered from external creation"
6036
+ );
6037
+ }
6038
+ });
5460
6039
  this.agentSessionManager.on("sessionTerminated", (sessionId) => {
5461
6040
  const session = this.sessions.get(sessionId);
5462
6041
  if (session && isAgentType(session.type)) {
@@ -5763,7 +6342,7 @@ Path: ${project.path}`;
5763
6342
  // src/infrastructure/agents/supervisor/tools/worktree-tools.ts
5764
6343
  import { tool as tool2 } from "@langchain/core/tools";
5765
6344
  import { z as z4 } from "zod";
5766
- function createWorktreeTools(workspaceDiscovery) {
6345
+ function createWorktreeTools(workspaceDiscovery, _agentSessionManager) {
5767
6346
  const listWorktrees = tool2(
5768
6347
  async ({ workspace, project }) => {
5769
6348
  try {
@@ -5843,8 +6422,224 @@ ${worktrees.map((w) => `- ${w.name}: ${w.branch} (${w.path})`).join("\n")}`;
5843
6422
  })
5844
6423
  }
5845
6424
  );
5846
- return [listWorktrees, createWorktree, removeWorktree];
5847
- }
6425
+ const branchStatus = tool2(
6426
+ async ({ workspace, project }) => {
6427
+ try {
6428
+ const status = await workspaceDiscovery.getBranchStatus(workspace, project);
6429
+ return {
6430
+ currentBranch: status.currentBranch,
6431
+ uncommittedChanges: status.uncommittedChanges,
6432
+ aheadCommits: status.aheadCommits,
6433
+ isClean: status.isClean,
6434
+ summary: status.isClean ? `Branch "${status.currentBranch}" is clean with ${status.aheadCommits} commits ahead of main` : `Branch "${status.currentBranch}" has ${status.uncommittedChanges.length} uncommitted changes:
6435
+ ${status.uncommittedChanges.map((c) => ` ${c}`).join("\n")}`
6436
+ };
6437
+ } catch (error) {
6438
+ return { error: error instanceof Error ? error.message : String(error) };
6439
+ }
6440
+ },
6441
+ {
6442
+ name: "branch_status",
6443
+ description: "Get current branch status including uncommitted changes and commit count ahead of main",
6444
+ schema: z4.object({
6445
+ workspace: z4.string().describe("Workspace name containing the project"),
6446
+ project: z4.string().describe("Project name to get branch status for")
6447
+ })
6448
+ }
6449
+ );
6450
+ const mergeBranch = tool2(
6451
+ async ({ workspace, project, sourceBranch, targetBranch, pushAfter, skipPreCheck }) => {
6452
+ try {
6453
+ const result = await workspaceDiscovery.mergeBranch(
6454
+ workspace,
6455
+ project,
6456
+ sourceBranch,
6457
+ targetBranch ?? "main",
6458
+ { pushAfter, skipPreCheck }
6459
+ );
6460
+ return result;
6461
+ } catch (error) {
6462
+ return {
6463
+ success: false,
6464
+ message: `Merge operation failed: ${error instanceof Error ? error.message : String(error)}`
6465
+ };
6466
+ }
6467
+ },
6468
+ {
6469
+ name: "merge_branch",
6470
+ description: "Merge source branch into target branch with safety checks and optional push to remote",
6471
+ schema: z4.object({
6472
+ workspace: z4.string().describe("Workspace name"),
6473
+ project: z4.string().describe("Project name"),
6474
+ sourceBranch: z4.string().describe("Source branch to merge from"),
6475
+ targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
6476
+ pushAfter: z4.boolean().optional().describe("Push to remote after successful merge"),
6477
+ skipPreCheck: z4.boolean().optional().describe("Skip pre-merge safety checks (not recommended)")
6478
+ })
6479
+ }
6480
+ );
6481
+ const listMergeableBranches = tool2(
6482
+ async ({ workspace, project }) => {
6483
+ try {
6484
+ const branches = await workspaceDiscovery.listMergeableBranches(workspace, project);
6485
+ if (branches.length === 0) {
6486
+ return "No feature branches found.";
6487
+ }
6488
+ const statusLines = branches.map((b) => {
6489
+ const status = [
6490
+ `Branch: ${b.branch}`,
6491
+ `Path: ${b.path}`,
6492
+ `Merged: ${b.isMerged ? "\u2713" : "\u2717"}`,
6493
+ `Clean: ${b.hasUncommittedChanges ? "\u2717 (has changes)" : "\u2713"}`,
6494
+ `Cleanup ready: ${b.canCleanup ? "\u2713" : "\u2717"}`,
6495
+ `Ahead commits: ${b.aheadCommits}`
6496
+ ].join(" | ");
6497
+ return `- ${status}`;
6498
+ });
6499
+ const summary = `Found ${branches.length} branches. ${branches.filter((b) => b.canCleanup).length} are ready for cleanup.`;
6500
+ return `${summary}
6501
+
6502
+ ${statusLines.join("\n")}`;
6503
+ } catch (error) {
6504
+ return { error: error instanceof Error ? error.message : String(error) };
6505
+ }
6506
+ },
6507
+ {
6508
+ name: "list_mergeable_branches",
6509
+ description: "List all feature branches and their merge/cleanup status",
6510
+ schema: z4.object({
6511
+ workspace: z4.string().describe("Workspace name"),
6512
+ project: z4.string().describe("Project name")
6513
+ })
6514
+ }
6515
+ );
6516
+ const cleanupWorktree = tool2(
6517
+ async ({ workspace, project, branch, force }) => {
6518
+ try {
6519
+ if (!force) {
6520
+ const status = await workspaceDiscovery.getBranchStatus(workspace, project);
6521
+ if (!status.isClean && status.currentBranch === branch) {
6522
+ return {
6523
+ success: false,
6524
+ message: `Cannot cleanup: uncommitted changes exist in "${branch}". Use force=true to override.`,
6525
+ uncommittedChanges: status.uncommittedChanges
6526
+ };
6527
+ }
6528
+ }
6529
+ const result = await workspaceDiscovery.cleanupWorktreeAndBranch(
6530
+ workspace,
6531
+ project,
6532
+ branch
6533
+ );
6534
+ return result;
6535
+ } catch (error) {
6536
+ return {
6537
+ success: false,
6538
+ message: `Cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
6539
+ branchDeleted: false
6540
+ };
6541
+ }
6542
+ },
6543
+ {
6544
+ name: "cleanup_worktree",
6545
+ description: "Remove worktree and optionally delete the branch if merged",
6546
+ schema: z4.object({
6547
+ workspace: z4.string().describe("Workspace name"),
6548
+ project: z4.string().describe("Project name"),
6549
+ branch: z4.string().describe("Branch/worktree name to cleanup"),
6550
+ force: z4.boolean().optional().describe("Force cleanup even with uncommitted changes")
6551
+ })
6552
+ }
6553
+ );
6554
+ const completeFeature = tool2(
6555
+ async ({ workspace, project, featureBranch, targetBranch, skipConfirmation }) => {
6556
+ try {
6557
+ const results = [];
6558
+ const status = await workspaceDiscovery.getBranchStatus(workspace, project);
6559
+ results.push({
6560
+ step: "status_check",
6561
+ data: {
6562
+ currentBranch: status.currentBranch,
6563
+ isClean: status.isClean,
6564
+ uncommittedChanges: status.uncommittedChanges,
6565
+ aheadCommits: status.aheadCommits
6566
+ }
6567
+ });
6568
+ if (!status.isClean && status.currentBranch === featureBranch && !skipConfirmation) {
6569
+ return {
6570
+ success: false,
6571
+ message: `Cannot complete feature: uncommitted changes exist in "${featureBranch}". Commit or stash changes first.`,
6572
+ results
6573
+ };
6574
+ }
6575
+ const mergeResult = await workspaceDiscovery.mergeBranch(
6576
+ workspace,
6577
+ project,
6578
+ featureBranch,
6579
+ targetBranch ?? "main",
6580
+ { pushAfter: true }
6581
+ );
6582
+ results.push({
6583
+ step: "merge",
6584
+ data: mergeResult
6585
+ });
6586
+ if (!mergeResult.success) {
6587
+ return {
6588
+ success: false,
6589
+ message: `Merge failed: ${mergeResult.message}`,
6590
+ results
6591
+ };
6592
+ }
6593
+ const cleanupResult = await workspaceDiscovery.cleanupWorktreeAndBranch(
6594
+ workspace,
6595
+ project,
6596
+ featureBranch
6597
+ );
6598
+ results.push({
6599
+ step: "cleanup",
6600
+ data: cleanupResult
6601
+ });
6602
+ return {
6603
+ success: true,
6604
+ message: `\u2705 Feature "${featureBranch}" completed successfully!
6605
+
6606
+ Steps executed:
6607
+ 1. \u2705 Branch validated
6608
+ 2. \u2705 Merged into ${targetBranch ?? "main"} and pushed
6609
+ 3. \u2705 Worktree cleaned up${cleanupResult.branchDeleted ? " and branch deleted" : ""}`,
6610
+ results
6611
+ };
6612
+ } catch (error) {
6613
+ return {
6614
+ success: false,
6615
+ message: `Feature completion failed: ${error instanceof Error ? error.message : String(error)}`,
6616
+ results: []
6617
+ };
6618
+ }
6619
+ },
6620
+ {
6621
+ name: "complete_feature",
6622
+ description: "Complete workflow: merge feature branch into main and cleanup worktree/branch",
6623
+ schema: z4.object({
6624
+ workspace: z4.string().describe("Workspace name"),
6625
+ project: z4.string().describe("Project name"),
6626
+ featureBranch: z4.string().describe("Feature branch name to complete"),
6627
+ targetBranch: z4.string().optional().describe("Target branch (defaults to main)"),
6628
+ skipConfirmation: z4.boolean().optional().describe("Skip safety checks (not recommended)")
6629
+ })
6630
+ }
6631
+ );
6632
+ return [
6633
+ listWorktrees,
6634
+ createWorktree,
6635
+ removeWorktree,
6636
+ branchStatus,
6637
+ mergeBranch,
6638
+ listMergeableBranches,
6639
+ cleanupWorktree,
6640
+ completeFeature
6641
+ ];
6642
+ }
5848
6643
 
5849
6644
  // src/infrastructure/agents/supervisor/tools/session-tools.ts
5850
6645
  import { tool as tool3 } from "@langchain/core/tools";
@@ -6134,7 +6929,96 @@ Messages: ${agentState.messages.length}`;
6134
6929
  })
6135
6930
  }
6136
6931
  );
6137
- return [listSessions, listAvailableAgents, createAgentSession, createTerminalSession, terminateSession, terminateAllSessions, getSessionInfo];
6932
+ const listSessionsWithWorktrees = tool3(
6933
+ () => {
6934
+ try {
6935
+ const sessions2 = agentSessionManager.listSessionsWithWorktreeInfo();
6936
+ if (sessions2.length === 0) {
6937
+ return "No active sessions.";
6938
+ }
6939
+ const sessionLines = sessions2.map((s) => {
6940
+ const worktreeInfo = s.worktreeInfo ? s.worktreeInfo.isWorktree ? ` (worktree: ${s.worktreeInfo.workspace}/${s.worktreeInfo.project}--${s.worktreeInfo.branch})` : ` (main: ${s.worktreeInfo.workspace}/${s.worktreeInfo.project})` : "";
6941
+ const executing = s.isExecuting ? " [executing]" : "";
6942
+ return `- [${s.agentType}] ${s.sessionId}${executing}${worktreeInfo}
6943
+ Working dir: ${s.workingDir}`;
6944
+ });
6945
+ return `Active sessions:
6946
+ ${sessionLines.join("\n")}`;
6947
+ } catch (error) {
6948
+ return `Error listing sessions: ${error instanceof Error ? error.message : String(error)}`;
6949
+ }
6950
+ },
6951
+ {
6952
+ name: "list_sessions_with_worktrees",
6953
+ description: "Lists all active sessions with worktree information for branch management",
6954
+ schema: z5.object({})
6955
+ }
6956
+ );
6957
+ const getWorktreeSessionSummary = tool3(
6958
+ ({ workspace, project, branch }) => {
6959
+ try {
6960
+ const summary = agentSessionManager.getWorktreeSessionSummary(workspace, project, branch);
6961
+ if (summary.sessionCount === 0) {
6962
+ return `No active sessions in worktree "${workspace}/${project}--${branch}".`;
6963
+ }
6964
+ const sessionDetails = summary.activeSessions.map(
6965
+ (s) => `- [${s.agentType}] ${s.sessionId} (${s.isExecuting ? "executing" : "idle"})
6966
+ Created: ${new Date(s.createdAt).toISOString()}`
6967
+ ).join("\n");
6968
+ return `Worktree "${workspace}/${project}--${branch}" has ${summary.sessionCount} active session(s):
6969
+ ${sessionDetails}
6970
+
6971
+ Session types: ${summary.sessionTypes.join(", ")}
6972
+ Executing: ${summary.executingCount}`;
6973
+ } catch (error) {
6974
+ return `Error getting worktree session summary: ${error instanceof Error ? error.message : String(error)}`;
6975
+ }
6976
+ },
6977
+ {
6978
+ name: "get_worktree_session_summary",
6979
+ description: "Gets detailed session information for a specific worktree",
6980
+ schema: z5.object({
6981
+ workspace: z5.string().describe("Workspace name"),
6982
+ project: z5.string().describe("Project name"),
6983
+ branch: z5.string().describe("Branch/worktree name")
6984
+ })
6985
+ }
6986
+ );
6987
+ const terminateWorktreeSessions = tool3(
6988
+ ({ workspace, project, branch }) => {
6989
+ try {
6990
+ const terminatedSessions = agentSessionManager.terminateWorktreeSessions(workspace, project, branch);
6991
+ if (terminatedSessions.length === 0) {
6992
+ return `No active sessions to terminate in worktree "${workspace}/${project}--${branch}".`;
6993
+ }
6994
+ return `Terminated ${terminatedSessions.length} session(s) in worktree "${workspace}/${project}--${branch}":
6995
+ ${terminatedSessions.map((id) => ` - ${id}`).join("\n")}`;
6996
+ } catch (error) {
6997
+ return `Error terminating worktree sessions: ${error instanceof Error ? error.message : String(error)}`;
6998
+ }
6999
+ },
7000
+ {
7001
+ name: "terminate_worktree_sessions",
7002
+ description: "Terminates all active sessions in a specific worktree",
7003
+ schema: z5.object({
7004
+ workspace: z5.string().describe("Workspace name"),
7005
+ project: z5.string().describe("Project name"),
7006
+ branch: z5.string().describe("Branch/worktree name")
7007
+ })
7008
+ }
7009
+ );
7010
+ return [
7011
+ listSessions,
7012
+ listAvailableAgents,
7013
+ createAgentSession,
7014
+ createTerminalSession,
7015
+ terminateSession,
7016
+ terminateAllSessions,
7017
+ getSessionInfo,
7018
+ listSessionsWithWorktrees,
7019
+ getWorktreeSessionSummary,
7020
+ terminateWorktreeSessions
7021
+ ];
6138
7022
  }
6139
7023
 
6140
7024
  // src/infrastructure/agents/supervisor/tools/filesystem-tools.ts
@@ -6266,7 +7150,7 @@ var SupervisorAgent = class extends EventEmitter4 {
6266
7150
  const llm = this.createLLM(env);
6267
7151
  const tools = [
6268
7152
  ...createWorkspaceTools(config2.workspaceDiscovery),
6269
- ...createWorktreeTools(config2.workspaceDiscovery),
7153
+ ...createWorktreeTools(config2.workspaceDiscovery, config2.agentSessionManager),
6270
7154
  ...createSessionTools(
6271
7155
  config2.sessionManager,
6272
7156
  config2.agentSessionManager,
@@ -6518,142 +7402,682 @@ var SupervisorAgent = class extends EventEmitter4 {
6518
7402
  return this.isProcessingCommand || this.isExecuting;
6519
7403
  }
6520
7404
  /**
6521
- * Ends command processing (called after completion or error, not after cancel).
7405
+ * Ends command processing (called after completion or error, not after cancel).
7406
+ */
7407
+ endProcessing() {
7408
+ this.isProcessingCommand = false;
7409
+ this.logger.debug("Ended command processing");
7410
+ }
7411
+ /**
7412
+ * Clears global conversation history.
7413
+ * Also resets cancellation state to allow new commands.
7414
+ */
7415
+ clearHistory() {
7416
+ this.conversationHistory = [];
7417
+ this.isCancelled = false;
7418
+ this.logger.info("Global conversation history cleared");
7419
+ }
7420
+ /**
7421
+ * Resets the cancellation state.
7422
+ * Call this before starting a new command to ensure previous cancellation doesn't affect it.
7423
+ */
7424
+ resetCancellationState() {
7425
+ this.isCancelled = false;
7426
+ }
7427
+ /**
7428
+ * Restores global conversation history from persistent storage.
7429
+ * Called on startup to sync in-memory cache with database.
7430
+ */
7431
+ restoreHistory(history) {
7432
+ if (history.length === 0) return;
7433
+ this.conversationHistory = history.slice(-20);
7434
+ this.logger.debug({ messageCount: this.conversationHistory.length }, "Global conversation history restored");
7435
+ }
7436
+ /**
7437
+ * Builds the system message for the agent.
7438
+ */
7439
+ buildSystemMessage() {
7440
+ const systemPrompt = `You are the Supervisor Agent for Tiflis Code, a workstation management system.
7441
+
7442
+ Your role is to help users:
7443
+ 1. **Discover workspaces and projects** - List available workspaces and projects
7444
+ 2. **Manage git worktrees** - Create, list, and remove worktrees for parallel development
7445
+ 3. **Manage sessions** - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
7446
+ 4. **Navigate the file system** - List directories and read files
7447
+ 5. **Complete feature workflows** - Merge branches, clean up worktrees, and manage related sessions
7448
+
7449
+ ## CRITICAL: Always Use Tools - Never Be Lazy
7450
+
7451
+ **YOU MUST ALWAYS call tools to execute user requests. NEVER be lazy or skip actions based on memory or previous context.**
7452
+
7453
+ ### Mandatory Tool Usage Rules:
7454
+
7455
+ 1. **ALWAYS call tools for fresh data** - When user asks about workspaces, projects, sessions, or any system state:
7456
+ - ALWAYS call the appropriate tool (list_workspaces, list_projects, list_sessions, etc.)
7457
+ - NEVER respond from memory or previous conversation context
7458
+ - System state changes constantly - what was true before may not be true now
7459
+
7460
+ 2. **ALWAYS execute requested actions** - When user asks to create, terminate, or modify something:
7461
+ - ALWAYS call the tool to perform the action, even if you think it was done before
7462
+ - If user asks to create a session and one already exists, CREATE ANOTHER ONE (user knows what they want)
7463
+ - If user asks to list projects, LIST THEM NOW with a tool call (don't say "I already showed you")
7464
+ - NEVER refuse a direct request because "it was already done" or "nothing changed"
7465
+
7466
+ 3. **User intent is paramount** - When user explicitly requests an action:
7467
+ - Execute it immediately without questioning or suggesting alternatives
7468
+ - Don't assume user made a mistake - they know what they need
7469
+ - Multiple sessions in the same project is a valid use case
7470
+ - Refreshing information is always valid
7471
+
7472
+ 4. **No shortcuts** - You must:
7473
+ - Call list_workspaces/list_projects EVERY time user asks what workspaces/projects exist
7474
+ - Call list_sessions EVERY time user asks about active sessions
7475
+ - Call create_agent_session/create_terminal_session EVERY time user asks to create a session
7476
+ - Never say "based on our previous conversation" or "as I mentioned earlier" for factual data
7477
+
7478
+ ## Feature Completion & Merge Workflows
7479
+
7480
+ When users ask to "complete the feature", "finish the work", "merge and clean up", or similar requests:
7481
+
7482
+ ### Safety Checks First:
7483
+ 1. **Check branch status** with \`branch_status\` - Look for uncommitted changes
7484
+ 2. **List active sessions** with \`get_worktree_session_summary\` - Find sessions in the worktree
7485
+ 3. **Ask for confirmation** if there are uncommitted changes or active sessions
7486
+
7487
+ ### Complete Workflow with \`complete_feature\`:
7488
+ - Merges feature branch into main with automatic push
7489
+ - Cleans up the worktree and removes the branch if merged
7490
+ - One-command solution for feature completion
7491
+
7492
+ ### Step-by-Step Alternative:
7493
+ 1. **Handle uncommitted changes**: Commit, stash, or get user confirmation
7494
+ 2. **Terminate sessions**: Use \`terminate_worktree_sessions\` to clean up active sessions
7495
+ 3. **Merge branch**: Use \`merge_branch\` with pushAfter=true
7496
+ 4. **Cleanup worktree**: Use \`cleanup_worktree\` to remove worktree directory
7497
+
7498
+ ### Available Merge Tools:
7499
+ - **branch_status** - Check current branch state and uncommitted changes
7500
+ - **merge_branch** - Safe merge with conflict detection and push
7501
+ - **complete_feature** - Full workflow (merge + cleanup + push)
7502
+ - **cleanup_worktree** - Remove worktree and delete merged branch
7503
+ - **list_mergeable_branches** - Show all branches and their cleanup eligibility
7504
+ - **get_worktree_session_summary** - List sessions in a specific worktree
7505
+ - **terminate_worktree_sessions** - End all sessions in a worktree
7506
+
7507
+ ### Error Handling:
7508
+ - **Merge conflicts**: Report conflicting files and suggest manual resolution
7509
+ - **Uncommitted changes**: Offer to commit, stash, or force cleanup
7510
+ - **Active sessions**: List sessions and ask for termination confirmation
7511
+ - **Failed pushes**: Continue with local merge, warn about remote sync
7512
+
7513
+ ## Guidelines:
7514
+ - Be concise and helpful
7515
+ - Use tools to gather information before responding
7516
+ - When creating sessions, always confirm the workspace and project first
7517
+ - For ambiguous requests, ask clarifying questions
7518
+ - Format responses for terminal display (avoid markdown links)
7519
+ - ALWAYS prioritize safety - check before deleting/merging
7520
+
7521
+ ## Session Types:
7522
+ - **cursor** - Cursor AI agent for code assistance
7523
+ - **claude** - Claude Code CLI for AI coding
7524
+ - **opencode** - OpenCode AI agent
7525
+ - **terminal** - Shell terminal for direct commands
7526
+
7527
+ ## Creating Agent Sessions:
7528
+ When creating agent sessions, by default use the main project directory (main or master branch) unless the user explicitly requests a specific worktree or branch:
7529
+ - **Default behavior**: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
7530
+ - **Specific worktree**: Only specify \`worktree\` when the user explicitly asks for a feature branch worktree (NOT the main branch)
7531
+ - **IMPORTANT**: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - instead, omit the worktree parameter entirely to use the project root.
7532
+
7533
+ ## Worktree Management:
7534
+ Worktrees allow working on multiple branches simultaneously in separate directories.
7535
+ - **Branch naming**: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case. Types: \`feature\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`. Examples: \`feature/user-auth\`, \`fix/keyboard-layout\`, \`refactor/websocket-handler\`
7536
+ - **Directory pattern**: \`project--branch-name\` (slashes replaced with dashes, e.g., \`my-app--feature-user-auth\`)
7537
+ - **Creating worktrees**: Use \`create_worktree\` tool with:
7538
+ - \`createNewBranch: true\` \u2014 Creates a NEW branch and worktree (most common for new features)
7539
+ - \`createNewBranch: false\` \u2014 Checks out an EXISTING branch into a worktree
7540
+ - \`baseBranch\` \u2014 Optional starting point for new branches (defaults to HEAD, commonly "main")`;
7541
+ return [new HumanMessage(`[System Instructions]
7542
+ ${systemPrompt}
7543
+ [End Instructions]`)];
7544
+ }
7545
+ /**
7546
+ * Builds messages from conversation history.
7547
+ */
7548
+ buildHistoryMessages(history) {
7549
+ return history.map(
7550
+ (entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
7551
+ );
7552
+ }
7553
+ /**
7554
+ * Gets global conversation history.
7555
+ */
7556
+ getConversationHistory() {
7557
+ return this.conversationHistory;
7558
+ }
7559
+ /**
7560
+ * Adds an entry to global conversation history.
7561
+ */
7562
+ addToHistory(role, content) {
7563
+ this.conversationHistory.push({ role, content });
7564
+ if (this.conversationHistory.length > 20) {
7565
+ this.conversationHistory.splice(0, this.conversationHistory.length - 20);
7566
+ }
7567
+ }
7568
+ };
7569
+
7570
+ // src/infrastructure/mock/mock-supervisor-agent.ts
7571
+ import { EventEmitter as EventEmitter5 } from "events";
7572
+
7573
+ // src/infrastructure/mock/fixture-loader.ts
7574
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
7575
+ import { join as join7, dirname as dirname2 } from "path";
7576
+ import { fileURLToPath as fileURLToPath2 } from "url";
7577
+ var __filename = fileURLToPath2(import.meta.url);
7578
+ var __dirname = dirname2(__filename);
7579
+ var DEFAULT_FIXTURES_PATH = join7(__dirname, "fixtures");
7580
+ var fixtureCache = /* @__PURE__ */ new Map();
7581
+ function loadFixture(name, customPath) {
7582
+ const cacheKey = `${customPath ?? "default"}:${name}`;
7583
+ if (fixtureCache.has(cacheKey)) {
7584
+ return fixtureCache.get(cacheKey);
7585
+ }
7586
+ const fixturesDir = customPath ?? DEFAULT_FIXTURES_PATH;
7587
+ const filePath = join7(fixturesDir, `${name}.json`);
7588
+ if (!existsSync3(filePath)) {
7589
+ console.warn(`[MockMode] Fixture not found: ${filePath}`);
7590
+ return null;
7591
+ }
7592
+ try {
7593
+ const content = readFileSync2(filePath, "utf-8");
7594
+ const fixture = JSON.parse(content);
7595
+ fixtureCache.set(cacheKey, fixture);
7596
+ return fixture;
7597
+ } catch (error) {
7598
+ console.error(`[MockMode] Failed to load fixture ${filePath}:`, error);
7599
+ return null;
7600
+ }
7601
+ }
7602
+ function findMatchingResponse(fixture, input) {
7603
+ const normalizedInput = input.toLowerCase().trim();
7604
+ for (const scenario of Object.values(fixture.scenarios)) {
7605
+ for (const trigger of scenario.triggers) {
7606
+ if (normalizedInput.includes(trigger.toLowerCase())) {
7607
+ return scenario.response;
7608
+ }
7609
+ }
7610
+ }
7611
+ return fixture.default_response;
7612
+ }
7613
+
7614
+ // src/infrastructure/mock/streaming-simulator.ts
7615
+ var DEFAULT_TOKEN_DELAY_MS = 30;
7616
+ async function simulateStreaming(text2, delayMs = DEFAULT_TOKEN_DELAY_MS, onBlock, onComplete) {
7617
+ const tokens = tokenize(text2);
7618
+ let accumulated = "";
7619
+ for (let i = 0; i < tokens.length; i++) {
7620
+ accumulated += tokens[i];
7621
+ const block = {
7622
+ type: "text",
7623
+ text: accumulated
7624
+ };
7625
+ onBlock([block], false);
7626
+ if (i < tokens.length - 1) {
7627
+ await sleep(delayMs);
7628
+ }
7629
+ }
7630
+ const finalBlock = {
7631
+ type: "text",
7632
+ text: accumulated
7633
+ };
7634
+ onBlock([finalBlock], true);
7635
+ onComplete();
7636
+ }
7637
+ function tokenize(text2) {
7638
+ const tokens = [];
7639
+ let current = "";
7640
+ for (const char of text2) {
7641
+ if (char === " ") {
7642
+ if (current) {
7643
+ tokens.push(current);
7644
+ current = "";
7645
+ }
7646
+ tokens.push(" ");
7647
+ } else if (/[.,!?;:\n]/.test(char)) {
7648
+ if (current) {
7649
+ tokens.push(current);
7650
+ current = "";
7651
+ }
7652
+ tokens.push(char);
7653
+ } else {
7654
+ current += char;
7655
+ }
7656
+ }
7657
+ if (current) {
7658
+ tokens.push(current);
7659
+ }
7660
+ return tokens;
7661
+ }
7662
+ function sleep(ms) {
7663
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7664
+ }
7665
+
7666
+ // src/infrastructure/mock/mock-supervisor-agent.ts
7667
+ var MockSupervisorAgent = class extends EventEmitter5 {
7668
+ logger;
7669
+ fixturesPath;
7670
+ fixture;
7671
+ conversationHistory = [];
7672
+ isExecuting = false;
7673
+ isProcessingCommand = false;
7674
+ isCancelled = false;
7675
+ abortController = null;
7676
+ constructor(config2) {
7677
+ super();
7678
+ this.logger = config2.logger.child({ component: "MockSupervisorAgent" });
7679
+ this.fixturesPath = config2.fixturesPath;
7680
+ this.fixture = loadFixture("supervisor", this.fixturesPath);
7681
+ if (this.fixture) {
7682
+ this.logger.info(
7683
+ { scenarios: Object.keys(this.fixture.scenarios).length },
7684
+ "Mock Supervisor Agent initialized with fixtures"
7685
+ );
7686
+ } else {
7687
+ this.logger.warn(
7688
+ "Mock Supervisor Agent initialized without fixtures - will return default responses"
7689
+ );
7690
+ }
7691
+ }
7692
+ /**
7693
+ * Executes a command (non-streaming).
7694
+ */
7695
+ async execute(command, deviceId, _currentSessionId) {
7696
+ this.logger.info({ command, deviceId }, "Mock supervisor execute");
7697
+ const response = this.getResponse(command);
7698
+ this.conversationHistory.push({ role: "user", content: command });
7699
+ this.conversationHistory.push({ role: "assistant", content: response.text });
7700
+ return {
7701
+ output: response.text
7702
+ };
7703
+ }
7704
+ /**
7705
+ * Executes a command with simulated streaming.
7706
+ */
7707
+ async executeWithStream(command, deviceId) {
7708
+ this.logger.info({ command, deviceId }, "Mock supervisor executeWithStream");
7709
+ this.abortController = new AbortController();
7710
+ this.isExecuting = true;
7711
+ this.isCancelled = false;
7712
+ try {
7713
+ const response = this.getResponse(command);
7714
+ if (this.isExecuting && !this.isCancelled) {
7715
+ const statusBlock = createStatusBlock("Processing...");
7716
+ this.emit("blocks", deviceId, [statusBlock], false);
7717
+ }
7718
+ await this.sleep(100);
7719
+ const allBlocks = [];
7720
+ await simulateStreaming(
7721
+ response.text,
7722
+ response.delay_ms ?? 30,
7723
+ (blocks, isComplete) => {
7724
+ if (this.isExecuting && !this.isCancelled) {
7725
+ this.emit("blocks", deviceId, blocks, false);
7726
+ if (isComplete) {
7727
+ allBlocks.push(...blocks);
7728
+ }
7729
+ }
7730
+ },
7731
+ () => {
7732
+ }
7733
+ );
7734
+ if (this.isExecuting && !this.isCancelled) {
7735
+ this.conversationHistory.push({ role: "user", content: command });
7736
+ this.conversationHistory.push({
7737
+ role: "assistant",
7738
+ content: response.text
7739
+ });
7740
+ const completionBlock = createStatusBlock("Complete");
7741
+ this.emit(
7742
+ "blocks",
7743
+ deviceId,
7744
+ [completionBlock],
7745
+ true,
7746
+ response.text,
7747
+ allBlocks
7748
+ );
7749
+ }
7750
+ } catch (error) {
7751
+ if (this.isCancelled) {
7752
+ this.logger.info({ deviceId }, "Mock supervisor cancelled");
7753
+ return;
7754
+ }
7755
+ this.logger.error({ error, command }, "Mock supervisor error");
7756
+ const errorBlock = createTextBlock(
7757
+ error instanceof Error ? error.message : "An error occurred"
7758
+ );
7759
+ this.emit("blocks", deviceId, [errorBlock], true);
7760
+ } finally {
7761
+ this.isExecuting = false;
7762
+ this.abortController = null;
7763
+ }
7764
+ }
7765
+ /**
7766
+ * Gets the response for a command from fixtures.
7767
+ */
7768
+ getResponse(command) {
7769
+ if (!this.fixture) {
7770
+ return {
7771
+ text: "I'm the Supervisor agent. I can help you manage workspaces, sessions, and more. What would you like to do?",
7772
+ delay_ms: 30
7773
+ };
7774
+ }
7775
+ return findMatchingResponse(this.fixture, command);
7776
+ }
7777
+ /**
7778
+ * Cancels current execution.
7779
+ */
7780
+ cancel() {
7781
+ if (!this.isProcessingCommand && !this.isExecuting) {
7782
+ return false;
7783
+ }
7784
+ this.logger.info("Cancelling mock supervisor execution");
7785
+ this.isCancelled = true;
7786
+ this.isExecuting = false;
7787
+ this.isProcessingCommand = false;
7788
+ if (this.abortController) {
7789
+ this.abortController.abort();
7790
+ }
7791
+ return true;
7792
+ }
7793
+ /**
7794
+ * Check if execution was cancelled.
7795
+ */
7796
+ wasCancelled() {
7797
+ return this.isCancelled;
7798
+ }
7799
+ /**
7800
+ * Starts command processing.
7801
+ */
7802
+ startProcessing() {
7803
+ this.abortController = new AbortController();
7804
+ this.isProcessingCommand = true;
7805
+ this.isCancelled = false;
7806
+ return this.abortController;
7807
+ }
7808
+ /**
7809
+ * Checks if processing is active.
7810
+ */
7811
+ isProcessing() {
7812
+ return this.isProcessingCommand || this.isExecuting;
7813
+ }
7814
+ /**
7815
+ * Ends command processing.
7816
+ */
7817
+ endProcessing() {
7818
+ this.isProcessingCommand = false;
7819
+ }
7820
+ /**
7821
+ * Clears conversation history.
7822
+ */
7823
+ clearHistory() {
7824
+ this.conversationHistory = [];
7825
+ this.isCancelled = false;
7826
+ this.logger.info("Mock conversation history cleared");
7827
+ }
7828
+ /**
7829
+ * Resets cancellation state.
7830
+ */
7831
+ resetCancellationState() {
7832
+ this.isCancelled = false;
7833
+ }
7834
+ /**
7835
+ * Restores conversation history.
7836
+ */
7837
+ restoreHistory(history) {
7838
+ this.conversationHistory = history.slice(-20);
7839
+ }
7840
+ /**
7841
+ * Gets conversation history.
7842
+ */
7843
+ getConversationHistory() {
7844
+ return [...this.conversationHistory];
7845
+ }
7846
+ /**
7847
+ * Sleep utility.
7848
+ */
7849
+ sleep(ms) {
7850
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
7851
+ }
7852
+ };
7853
+
7854
+ // src/infrastructure/mock/mock-agent-session-manager.ts
7855
+ import { EventEmitter as EventEmitter6 } from "events";
7856
+ import { randomUUID as randomUUID3 } from "crypto";
7857
+ var MockAgentSessionManager = class extends EventEmitter6 {
7858
+ sessions = /* @__PURE__ */ new Map();
7859
+ fixtures = /* @__PURE__ */ new Map();
7860
+ logger;
7861
+ fixturesPath;
7862
+ constructor(config2) {
7863
+ super();
7864
+ this.logger = config2.logger.child({ component: "MockAgentSessionManager" });
7865
+ this.fixturesPath = config2.fixturesPath;
7866
+ this.loadAgentFixtures();
7867
+ this.logger.info("Mock Agent Session Manager initialized");
7868
+ }
7869
+ /**
7870
+ * Pre-load fixtures for all agent types.
7871
+ */
7872
+ loadAgentFixtures() {
7873
+ const agentTypes = ["cursor", "claude", "opencode"];
7874
+ for (const agentType of agentTypes) {
7875
+ const fixture = loadFixture(agentType, this.fixturesPath);
7876
+ this.fixtures.set(agentType, fixture);
7877
+ if (fixture) {
7878
+ this.logger.debug(
7879
+ { agentType, scenarios: Object.keys(fixture.scenarios).length },
7880
+ "Loaded fixture for agent type"
7881
+ );
7882
+ }
7883
+ }
7884
+ }
7885
+ /**
7886
+ * Create a new mock agent session.
7887
+ */
7888
+ createSession(agentType, workingDir, sessionId, agentName) {
7889
+ const id = sessionId ?? `agent-${randomUUID3()}`;
7890
+ const resolvedAgentName = agentName ?? agentType;
7891
+ const state = {
7892
+ sessionId: id,
7893
+ agentType,
7894
+ agentName: resolvedAgentName,
7895
+ workingDir,
7896
+ cliSessionId: `mock-cli-${randomUUID3().slice(0, 8)}`,
7897
+ isExecuting: false,
7898
+ isCancelled: false,
7899
+ messages: [],
7900
+ createdAt: Date.now(),
7901
+ lastActivityAt: Date.now()
7902
+ };
7903
+ this.sessions.set(id, state);
7904
+ this.logger.info(
7905
+ { sessionId: id, agentType, agentName: resolvedAgentName, workingDir },
7906
+ "Mock agent session created"
7907
+ );
7908
+ this.emit("sessionCreated", state);
7909
+ this.emit("cliSessionIdDiscovered", id, state.cliSessionId);
7910
+ return state;
7911
+ }
7912
+ /**
7913
+ * Execute a command in a mock agent session with simulated streaming.
7914
+ */
7915
+ async executeCommand(sessionId, prompt) {
7916
+ const state = this.sessions.get(sessionId);
7917
+ if (!state) {
7918
+ throw new Error(`Session not found: ${sessionId}`);
7919
+ }
7920
+ if (state.isExecuting) {
7921
+ this.cancelCommand(sessionId);
7922
+ }
7923
+ state.isExecuting = true;
7924
+ state.isCancelled = false;
7925
+ state.lastActivityAt = Date.now();
7926
+ const userMessage = {
7927
+ id: randomUUID3(),
7928
+ timestamp: Date.now(),
7929
+ role: "user",
7930
+ blocks: [createTextBlock(prompt)]
7931
+ };
7932
+ state.messages.push(userMessage);
7933
+ try {
7934
+ const fixture = this.fixtures.get(state.agentType);
7935
+ const response = fixture ? findMatchingResponse(fixture, prompt) : this.getDefaultResponse(state.agentType);
7936
+ await this.sleep(150);
7937
+ const allBlocks = [];
7938
+ await simulateStreaming(
7939
+ response.text,
7940
+ response.delay_ms ?? 30,
7941
+ (blocks, _isComplete) => {
7942
+ if (state.isExecuting && !state.isCancelled) {
7943
+ this.emit("blocks", sessionId, blocks, false);
7944
+ }
7945
+ },
7946
+ () => {
7947
+ }
7948
+ );
7949
+ if (state.isExecuting && !state.isCancelled) {
7950
+ const assistantMessage = {
7951
+ id: randomUUID3(),
7952
+ timestamp: Date.now(),
7953
+ role: "assistant",
7954
+ blocks: [createTextBlock(response.text)]
7955
+ };
7956
+ state.messages.push(assistantMessage);
7957
+ allBlocks.push(createTextBlock(response.text));
7958
+ const completionBlocks = [createStatusBlock("Command completed")];
7959
+ const completionMsg = {
7960
+ id: randomUUID3(),
7961
+ timestamp: Date.now(),
7962
+ role: "system",
7963
+ blocks: completionBlocks
7964
+ };
7965
+ state.messages.push(completionMsg);
7966
+ this.emit("blocks", sessionId, completionBlocks, true);
7967
+ }
7968
+ } catch (error) {
7969
+ if (!state.isCancelled) {
7970
+ this.logger.error({ sessionId, error }, "Mock command execution error");
7971
+ const errorBlocks = [
7972
+ createTextBlock(
7973
+ error instanceof Error ? error.message : "An error occurred"
7974
+ )
7975
+ ];
7976
+ this.emit("blocks", sessionId, errorBlocks, true);
7977
+ }
7978
+ } finally {
7979
+ state.isExecuting = false;
7980
+ state.lastActivityAt = Date.now();
7981
+ }
7982
+ }
7983
+ /**
7984
+ * Get default response when no fixture is available.
7985
+ */
7986
+ getDefaultResponse(agentType) {
7987
+ const responses = {
7988
+ claude: "I'm Claude, an AI assistant. I can help you with coding tasks, answer questions, and assist with various development workflows. What would you like me to help you with?",
7989
+ cursor: "I'm Cursor AI, ready to help you write and edit code. I can assist with code completion, refactoring, and explaining complex code. What can I help you with today?",
7990
+ opencode: "I'm OpenCode, an open-source AI coding assistant. I can help with code generation, debugging, and documentation. How can I assist you?"
7991
+ };
7992
+ return {
7993
+ text: responses[agentType],
7994
+ delay_ms: 30
7995
+ };
7996
+ }
7997
+ /**
7998
+ * Cancel current command execution.
7999
+ */
8000
+ cancelCommand(sessionId) {
8001
+ const state = this.sessions.get(sessionId);
8002
+ if (!state?.isExecuting) {
8003
+ return;
8004
+ }
8005
+ this.logger.info({ sessionId }, "Cancelling mock command execution");
8006
+ state.isExecuting = false;
8007
+ state.isCancelled = true;
8008
+ state.lastActivityAt = Date.now();
8009
+ }
8010
+ /**
8011
+ * Clear chat history for a session.
8012
+ */
8013
+ clearHistory(sessionId) {
8014
+ const state = this.sessions.get(sessionId);
8015
+ if (!state) {
8016
+ return;
8017
+ }
8018
+ state.messages = [];
8019
+ this.logger.info({ sessionId }, "Mock session history cleared");
8020
+ }
8021
+ /**
8022
+ * Terminate an agent session.
6522
8023
  */
6523
- endProcessing() {
6524
- this.isProcessingCommand = false;
6525
- this.logger.debug("Ended command processing");
8024
+ terminateSession(sessionId) {
8025
+ this.sessions.delete(sessionId);
8026
+ this.logger.info({ sessionId }, "Mock agent session terminated");
8027
+ this.emit("sessionTerminated", sessionId);
6526
8028
  }
6527
8029
  /**
6528
- * Clears global conversation history.
6529
- * Also resets cancellation state to allow new commands.
8030
+ * Get session state.
6530
8031
  */
6531
- clearHistory() {
6532
- this.conversationHistory = [];
6533
- this.isCancelled = false;
6534
- this.logger.info("Global conversation history cleared");
8032
+ getSession(sessionId) {
8033
+ return this.sessions.get(sessionId);
6535
8034
  }
6536
8035
  /**
6537
- * Resets the cancellation state.
6538
- * Call this before starting a new command to ensure previous cancellation doesn't affect it.
8036
+ * List all active sessions.
6539
8037
  */
6540
- resetCancellationState() {
6541
- this.isCancelled = false;
8038
+ listSessions() {
8039
+ return Array.from(this.sessions.values());
6542
8040
  }
6543
8041
  /**
6544
- * Restores global conversation history from persistent storage.
6545
- * Called on startup to sync in-memory cache with database.
8042
+ * Get chat history for a session.
6546
8043
  */
6547
- restoreHistory(history) {
6548
- if (history.length === 0) return;
6549
- this.conversationHistory = history.slice(-20);
6550
- this.logger.debug({ messageCount: this.conversationHistory.length }, "Global conversation history restored");
8044
+ getMessages(sessionId) {
8045
+ return this.sessions.get(sessionId)?.messages ?? [];
6551
8046
  }
6552
8047
  /**
6553
- * Builds the system message for the agent.
8048
+ * Check if a session is executing.
6554
8049
  */
6555
- buildSystemMessage() {
6556
- const systemPrompt = `You are the Supervisor Agent for Tiflis Code, a workstation management system.
6557
-
6558
- Your role is to help users:
6559
- 1. **Discover workspaces and projects** - List available workspaces and projects
6560
- 2. **Manage git worktrees** - Create, list, and remove worktrees for parallel development
6561
- 3. **Manage sessions** - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
6562
- 4. **Navigate the file system** - List directories and read files
6563
-
6564
- ## CRITICAL: Always Use Tools - Never Be Lazy
6565
-
6566
- **YOU MUST ALWAYS call tools to execute user requests. NEVER be lazy or skip actions based on memory or previous context.**
6567
-
6568
- ### Mandatory Tool Usage Rules:
6569
-
6570
- 1. **ALWAYS call tools for fresh data** - When user asks about workspaces, projects, sessions, or any system state:
6571
- - ALWAYS call the appropriate tool (list_workspaces, list_projects, list_sessions, etc.)
6572
- - NEVER respond from memory or previous conversation context
6573
- - System state changes constantly - what was true before may not be true now
6574
-
6575
- 2. **ALWAYS execute requested actions** - When user asks to create, terminate, or modify something:
6576
- - ALWAYS call the tool to perform the action, even if you think it was done before
6577
- - If user asks to create a session and one already exists, CREATE ANOTHER ONE (user knows what they want)
6578
- - If user asks to list projects, LIST THEM NOW with a tool call (don't say "I already showed you")
6579
- - NEVER refuse a direct request because "it was already done" or "nothing changed"
6580
-
6581
- 3. **User intent is paramount** - When user explicitly requests an action:
6582
- - Execute it immediately without questioning or suggesting alternatives
6583
- - Don't assume user made a mistake - they know what they need
6584
- - Multiple sessions in the same project is a valid use case
6585
- - Refreshing information is always valid
6586
-
6587
- 4. **No shortcuts** - You must:
6588
- - Call list_workspaces/list_projects EVERY time user asks what workspaces/projects exist
6589
- - Call list_sessions EVERY time user asks about active sessions
6590
- - Call create_agent_session/create_terminal_session EVERY time user asks to create a session
6591
- - Never say "based on our previous conversation" or "as I mentioned earlier" for factual data
6592
-
6593
- ## Guidelines:
6594
- - Be concise and helpful
6595
- - Use tools to gather information before responding
6596
- - When creating sessions, always confirm the workspace and project first
6597
- - For ambiguous requests, ask clarifying questions
6598
- - Format responses for terminal display (avoid markdown links)
6599
-
6600
- ## Session Types:
6601
- - **cursor** - Cursor AI agent for code assistance
6602
- - **claude** - Claude Code CLI for AI coding
6603
- - **opencode** - OpenCode AI agent
6604
- - **terminal** - Shell terminal for direct commands
6605
-
6606
- ## Creating Agent Sessions:
6607
- When creating agent sessions, by default use the main project directory (main or master branch) unless the user explicitly requests a specific worktree or branch:
6608
- - **Default behavior**: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
6609
- - **Specific worktree**: Only specify \`worktree\` when the user explicitly asks for a feature branch worktree (NOT the main branch)
6610
- - **IMPORTANT**: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - instead, omit the worktree parameter entirely to use the project root.
6611
- - **Example**: If user says "start claude on tiflis-code", create session WITHOUT worktree parameter (uses project root on main branch)
6612
- - **Example**: If user says "start claude on tiflis-code feature/auth branch", list worktrees, find the feature worktree name (e.g., "feature-auth"), and pass that as worktree
6613
-
6614
- ## Worktree Management:
6615
- Worktrees allow working on multiple branches simultaneously in separate directories.
6616
- - **Branch naming**: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case. Types: \`feature\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`. Examples: \`feature/user-auth\`, \`fix/keyboard-layout\`, \`refactor/websocket-handler\`
6617
- - **Directory pattern**: \`project--branch-name\` (slashes replaced with dashes, e.g., \`my-app--feature-user-auth\`)
6618
- - **Creating worktrees**: Use \`create_worktree\` tool with:
6619
- - \`createNewBranch: true\` \u2014 Creates a NEW branch and worktree (most common for new features)
6620
- - \`createNewBranch: false\` \u2014 Checks out an EXISTING branch into a worktree
6621
- - \`baseBranch\` \u2014 Optional starting point for new branches (defaults to HEAD, commonly "main")
6622
- - **Example**: To start work on a new feature, create worktree with \`createNewBranch: true\`, \`branch: "feature/new-keyboard"\`, \`baseBranch: "main"\``;
6623
- return [new HumanMessage(`[System Instructions]
6624
- ${systemPrompt}
6625
- [End Instructions]`)];
8050
+ isExecuting(sessionId) {
8051
+ return this.sessions.get(sessionId)?.isExecuting ?? false;
6626
8052
  }
6627
8053
  /**
6628
- * Builds messages from conversation history.
8054
+ * Check if a session was cancelled.
6629
8055
  */
6630
- buildHistoryMessages(history) {
6631
- return history.map(
6632
- (entry) => entry.role === "user" ? new HumanMessage(entry.content) : new AIMessage(entry.content)
6633
- );
8056
+ wasCancelled(sessionId) {
8057
+ return this.sessions.get(sessionId)?.isCancelled ?? false;
6634
8058
  }
6635
8059
  /**
6636
- * Gets global conversation history.
8060
+ * Cleanup all sessions.
6637
8061
  */
6638
- getConversationHistory() {
6639
- return this.conversationHistory;
8062
+ cleanup() {
8063
+ const sessionIds = Array.from(this.sessions.keys());
8064
+ for (const id of sessionIds) {
8065
+ this.terminateSession(id);
8066
+ }
6640
8067
  }
6641
8068
  /**
6642
- * Adds an entry to global conversation history.
8069
+ * Sleep utility.
6643
8070
  */
6644
- addToHistory(role, content) {
6645
- this.conversationHistory.push({ role, content });
6646
- if (this.conversationHistory.length > 20) {
6647
- this.conversationHistory.splice(0, this.conversationHistory.length - 20);
6648
- }
8071
+ sleep(ms) {
8072
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6649
8073
  }
6650
8074
  };
6651
8075
 
6652
8076
  // src/infrastructure/speech/stt-service.ts
6653
8077
  import { writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
6654
- import { join as join7 } from "path";
8078
+ import { join as join8 } from "path";
6655
8079
  import { tmpdir } from "os";
6656
- import { randomUUID as randomUUID3 } from "crypto";
8080
+ import { randomUUID as randomUUID4 } from "crypto";
6657
8081
  var STTService = class {
6658
8082
  config;
6659
8083
  logger;
@@ -6694,7 +8118,7 @@ var STTService = class {
6694
8118
  async transcribeOpenAI(audioBuffer, format, signal) {
6695
8119
  const baseUrl = this.config.baseUrl ?? "https://api.openai.com/v1";
6696
8120
  const endpoint = `${baseUrl}/audio/transcriptions`;
6697
- const tempPath = join7(tmpdir(), `stt-${randomUUID3()}.${format}`);
8121
+ const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
6698
8122
  try {
6699
8123
  if (signal?.aborted) {
6700
8124
  throw new Error("Transcription cancelled");
@@ -6742,7 +8166,7 @@ var STTService = class {
6742
8166
  async transcribeElevenLabs(audioBuffer, format, signal) {
6743
8167
  const baseUrl = this.config.baseUrl ?? "https://api.elevenlabs.io/v1";
6744
8168
  const endpoint = `${baseUrl}/speech-to-text`;
6745
- const tempPath = join7(tmpdir(), `stt-${randomUUID3()}.${format}`);
8169
+ const tempPath = join8(tmpdir(), `stt-${randomUUID4()}.${format}`);
6746
8170
  try {
6747
8171
  if (signal?.aborted) {
6748
8172
  throw new Error("Transcription cancelled");
@@ -7255,13 +8679,60 @@ async function bootstrap() {
7255
8679
  workspacesRoot: env.WORKSPACES_ROOT
7256
8680
  });
7257
8681
  const ptyManager = new PtyManager({ logger });
7258
- const agentSessionManager = new AgentSessionManager(logger);
8682
+ const agentSessionManager = env.MOCK_MODE ? new MockAgentSessionManager({ logger, fixturesPath: env.MOCK_FIXTURES_PATH }) : new AgentSessionManager(logger);
8683
+ if (env.MOCK_MODE) {
8684
+ logger.info(
8685
+ { fixturesPath: env.MOCK_FIXTURES_PATH ?? "built-in" },
8686
+ "Mock mode enabled - using mock agent session manager"
8687
+ );
8688
+ }
7259
8689
  const sessionManager = new InMemorySessionManager({
7260
8690
  ptyManager,
7261
8691
  agentSessionManager,
7262
8692
  workspacesRoot: env.WORKSPACES_ROOT,
7263
8693
  logger
7264
8694
  });
8695
+ if (env.MOCK_MODE) {
8696
+ const mockAgentManager = agentSessionManager;
8697
+ await sessionManager.createSession({
8698
+ sessionType: "supervisor",
8699
+ workingDir: env.WORKSPACES_ROOT
8700
+ });
8701
+ logger.info("Pre-created supervisor session for screenshots");
8702
+ mockAgentManager.createSession(
8703
+ "claude",
8704
+ `${env.WORKSPACES_ROOT}/work/my-app`,
8705
+ "claude-my-app",
8706
+ "claude"
8707
+ );
8708
+ mockAgentManager.createSession(
8709
+ "cursor",
8710
+ `${env.WORKSPACES_ROOT}/personal/blog`,
8711
+ "cursor-blog",
8712
+ "cursor"
8713
+ );
8714
+ mockAgentManager.createSession(
8715
+ "opencode",
8716
+ `${env.WORKSPACES_ROOT}/work/api-service`,
8717
+ "opencode-api",
8718
+ "opencode"
8719
+ );
8720
+ logger.info("Pre-created 3 mock agent sessions for screenshots");
8721
+ chatHistoryService.seedMockData({
8722
+ claude: {
8723
+ id: "claude-my-app",
8724
+ workingDir: `${env.WORKSPACES_ROOT}/work/my-app`
8725
+ },
8726
+ cursor: {
8727
+ id: "cursor-blog",
8728
+ workingDir: `${env.WORKSPACES_ROOT}/personal/blog`
8729
+ },
8730
+ opencode: {
8731
+ id: "opencode-api",
8732
+ workingDir: `${env.WORKSPACES_ROOT}/work/api-service`
8733
+ }
8734
+ });
8735
+ }
7265
8736
  const sttService = createSTTService(env, logger);
7266
8737
  if (sttService) {
7267
8738
  logger.info(
@@ -7295,7 +8766,10 @@ async function bootstrap() {
7295
8766
  const cancelledDuringTranscription = /* @__PURE__ */ new Set();
7296
8767
  const expectedAuthKey = new AuthKey(env.WORKSTATION_AUTH_KEY);
7297
8768
  let messageBroadcaster = null;
7298
- const supervisorAgent = new SupervisorAgent({
8769
+ const supervisorAgent = env.MOCK_MODE ? new MockSupervisorAgent({
8770
+ logger,
8771
+ fixturesPath: env.MOCK_FIXTURES_PATH
8772
+ }) : new SupervisorAgent({
7299
8773
  sessionManager,
7300
8774
  agentSessionManager,
7301
8775
  workspaceDiscovery,
@@ -7304,7 +8778,11 @@ async function bootstrap() {
7304
8778
  getMessageBroadcaster: () => messageBroadcaster,
7305
8779
  getChatHistoryService: () => chatHistoryService
7306
8780
  });
7307
- logger.info("Supervisor Agent initialized with LangGraph");
8781
+ if (env.MOCK_MODE) {
8782
+ logger.info("Mock Supervisor Agent initialized for screenshot automation");
8783
+ } else {
8784
+ logger.info("Supervisor Agent initialized with LangGraph");
8785
+ }
7308
8786
  const authenticateClient = new AuthenticateClientUseCase({
7309
8787
  clientRegistry,
7310
8788
  expectedAuthKey,
@@ -7377,10 +8855,11 @@ async function bootstrap() {
7377
8855
  const syncMessage = message;
7378
8856
  const client = clientRegistry.getBySocket(socket) ?? (syncMessage.device_id ? clientRegistry.getByDeviceId(new DeviceId(syncMessage.device_id)) : void 0);
7379
8857
  const subscriptions2 = client ? client.getSubscriptions() : [];
8858
+ const isLightweight = syncMessage.lightweight === true;
7380
8859
  const inMemorySessions = sessionManager.getSessionInfos();
7381
8860
  const persistedAgentSessions = chatHistoryService.getActiveAgentSessions();
7382
8861
  logger.debug(
7383
- { persistedAgentSessions, inMemoryCount: inMemorySessions.length },
8862
+ { persistedAgentSessions, inMemoryCount: inMemorySessions.length, isLightweight },
7384
8863
  "Sync: fetched sessions"
7385
8864
  );
7386
8865
  const inMemorySessionIds = new Set(
@@ -7405,6 +8884,67 @@ async function bootstrap() {
7405
8884
  };
7406
8885
  });
7407
8886
  const sessions2 = [...inMemorySessions, ...restoredAgentSessions];
8887
+ if (isLightweight) {
8888
+ const availableAgentsMap2 = getAvailableAgents();
8889
+ const availableAgents2 = Array.from(availableAgentsMap2.values()).map(
8890
+ (agent) => ({
8891
+ name: agent.name,
8892
+ base_type: agent.baseType,
8893
+ description: agent.description,
8894
+ is_alias: agent.isAlias
8895
+ })
8896
+ );
8897
+ const hiddenBaseTypes2 = getDisabledBaseAgents();
8898
+ const workspacesList2 = await workspaceDiscovery.listWorkspaces();
8899
+ const workspaces2 = await Promise.all(
8900
+ workspacesList2.map(async (ws) => {
8901
+ const projects = await workspaceDiscovery.listProjects(ws.name);
8902
+ return {
8903
+ name: ws.name,
8904
+ projects: projects.map((p) => ({
8905
+ name: p.name,
8906
+ is_git_repo: p.isGitRepo,
8907
+ default_branch: p.defaultBranch
8908
+ }))
8909
+ };
8910
+ })
8911
+ );
8912
+ const executingStates2 = {};
8913
+ for (const session of sessions2) {
8914
+ if (session.session_type === "cursor" || session.session_type === "claude" || session.session_type === "opencode") {
8915
+ executingStates2[session.session_id] = agentSessionManager.isExecuting(
8916
+ session.session_id
8917
+ );
8918
+ }
8919
+ }
8920
+ const supervisorIsExecuting2 = supervisorAgent.isProcessing();
8921
+ logger.info(
8922
+ {
8923
+ totalSessions: sessions2.length,
8924
+ isLightweight: true,
8925
+ availableAgentsCount: availableAgents2.length,
8926
+ workspacesCount: workspaces2.length,
8927
+ supervisorIsExecuting: supervisorIsExecuting2
8928
+ },
8929
+ "Sync: sending lightweight state to client (no histories)"
8930
+ );
8931
+ const syncStateMessage2 = JSON.stringify({
8932
+ type: "sync.state",
8933
+ id: syncMessage.id,
8934
+ payload: {
8935
+ sessions: sessions2,
8936
+ subscriptions: subscriptions2,
8937
+ availableAgents: availableAgents2,
8938
+ hiddenBaseTypes: hiddenBaseTypes2,
8939
+ workspaces: workspaces2,
8940
+ supervisorIsExecuting: supervisorIsExecuting2,
8941
+ executingStates: executingStates2
8942
+ // Omit: supervisorHistory, agentHistories, currentStreamingBlocks
8943
+ }
8944
+ });
8945
+ sendToDevice(socket, syncMessage.device_id, syncStateMessage2);
8946
+ return Promise.resolve();
8947
+ }
7408
8948
  const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
7409
8949
  const supervisorHistory = await Promise.all(
7410
8950
  supervisorHistoryRaw.map(async (msg) => ({
@@ -7775,7 +9315,7 @@ async function bootstrap() {
7775
9315
  logger.info({ wasCancelled }, "supervisorAgent.cancel() returned");
7776
9316
  if (messageBroadcaster) {
7777
9317
  const cancelBlock = {
7778
- id: randomUUID4(),
9318
+ id: randomUUID5(),
7779
9319
  block_type: "cancel",
7780
9320
  content: "Cancelled by user"
7781
9321
  };
@@ -8010,6 +9550,22 @@ async function bootstrap() {
8010
9550
  subscribeMessage.device_id,
8011
9551
  JSON.stringify(result)
8012
9552
  );
9553
+ if (env.MOCK_MODE) {
9554
+ const session = sessionManager.getSession(new SessionId(sessionId));
9555
+ if (session?.type === "terminal") {
9556
+ setTimeout(() => {
9557
+ const clearAndBanner = `clear && printf '\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\033[1;32m Tiflis Code - Remote Development Workstation\\033[0m\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n\\033[1;33m System Information:\\033[0m\\n \u251C\u2500 OS: macOS Sequoia 15.1\\n \u251C\u2500 Shell: zsh 5.9\\n \u251C\u2500 Node: v22.11.0\\n \u2514\u2500 Uptime: 2 days, 14 hours\\n\\n\\033[1;33m Active Sessions:\\033[0m\\n \u251C\u2500 Claude Code \u2500 tiflis/tiflis-code\\n \u251C\u2500 Cursor \u2500 personal/portfolio\\n \u2514\u2500 OpenCode \u2500 tiflis/tiflis-api\\n\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n'\r`;
9558
+ ptyManager.write(
9559
+ session,
9560
+ clearAndBanner
9561
+ );
9562
+ logger.info(
9563
+ { sessionId },
9564
+ "Mock mode: Generated terminal banner on subscribe"
9565
+ );
9566
+ }, 100);
9567
+ }
9568
+ }
8013
9569
  }
8014
9570
  }
8015
9571
  return Promise.resolve();
@@ -8307,7 +9863,7 @@ async function bootstrap() {
8307
9863
  }
8308
9864
  if (messageBroadcaster) {
8309
9865
  const cancelBlock = {
8310
- id: randomUUID4(),
9866
+ id: randomUUID5(),
8311
9867
  block_type: "cancel",
8312
9868
  content: "Cancelled by user"
8313
9869
  };
@@ -8511,6 +10067,107 @@ async function bootstrap() {
8511
10067
  }
8512
10068
  return Promise.resolve();
8513
10069
  },
10070
+ "history.request": async (socket, message) => {
10071
+ const historyRequest = message;
10072
+ const sessionId = historyRequest.payload?.session_id;
10073
+ const isSupervisor = !sessionId;
10074
+ logger.debug(
10075
+ { sessionId, isSupervisor, requestId: historyRequest.id },
10076
+ "History request received"
10077
+ );
10078
+ try {
10079
+ if (isSupervisor) {
10080
+ const supervisorHistoryRaw = chatHistoryService.getSupervisorHistory();
10081
+ const supervisorHistory = await Promise.all(
10082
+ supervisorHistoryRaw.map(async (msg) => ({
10083
+ message_id: msg.id,
10084
+ // Include message ID for audio.request
10085
+ sequence: msg.sequence,
10086
+ role: msg.role,
10087
+ content: msg.content,
10088
+ content_blocks: await chatHistoryService.enrichBlocksWithAudio(
10089
+ msg.contentBlocks,
10090
+ msg.audioOutputPath,
10091
+ msg.audioInputPath,
10092
+ false
10093
+ // Don't include audio in history response
10094
+ ),
10095
+ createdAt: msg.createdAt.toISOString()
10096
+ }))
10097
+ );
10098
+ const isExecuting = supervisorAgent.isProcessing();
10099
+ socket.send(
10100
+ JSON.stringify({
10101
+ type: "history.response",
10102
+ id: historyRequest.id,
10103
+ payload: {
10104
+ session_id: null,
10105
+ // Indicates supervisor
10106
+ history: supervisorHistory,
10107
+ is_executing: isExecuting
10108
+ }
10109
+ })
10110
+ );
10111
+ logger.debug(
10112
+ { messageCount: supervisorHistory.length, isExecuting },
10113
+ "Supervisor history sent"
10114
+ );
10115
+ } else {
10116
+ const history = chatHistoryService.getAgentHistory(sessionId);
10117
+ const enrichedHistory = await Promise.all(
10118
+ history.map(async (msg) => ({
10119
+ sequence: msg.sequence,
10120
+ role: msg.role,
10121
+ content: msg.content,
10122
+ content_blocks: await chatHistoryService.enrichBlocksWithAudio(
10123
+ msg.contentBlocks,
10124
+ msg.audioOutputPath,
10125
+ msg.audioInputPath,
10126
+ false
10127
+ // Don't include audio in history response
10128
+ ),
10129
+ createdAt: msg.createdAt.toISOString()
10130
+ }))
10131
+ );
10132
+ const isExecuting = agentSessionManager.isExecuting(sessionId);
10133
+ let currentStreamingBlocks;
10134
+ if (isExecuting) {
10135
+ const blocks = agentMessageAccumulator.get(sessionId);
10136
+ if (blocks && blocks.length > 0) {
10137
+ currentStreamingBlocks = blocks;
10138
+ }
10139
+ }
10140
+ socket.send(
10141
+ JSON.stringify({
10142
+ type: "history.response",
10143
+ id: historyRequest.id,
10144
+ payload: {
10145
+ session_id: sessionId,
10146
+ history: enrichedHistory,
10147
+ is_executing: isExecuting,
10148
+ current_streaming_blocks: currentStreamingBlocks
10149
+ }
10150
+ })
10151
+ );
10152
+ logger.debug(
10153
+ { sessionId, messageCount: enrichedHistory.length, isExecuting },
10154
+ "Agent session history sent"
10155
+ );
10156
+ }
10157
+ } catch (error) {
10158
+ logger.error({ error, sessionId }, "Failed to get history");
10159
+ socket.send(
10160
+ JSON.stringify({
10161
+ type: "history.response",
10162
+ id: historyRequest.id,
10163
+ payload: {
10164
+ session_id: sessionId ?? null,
10165
+ error: error instanceof Error ? error.message : "Failed to get history"
10166
+ }
10167
+ })
10168
+ );
10169
+ }
10170
+ },
8514
10171
  "audio.request": async (socket, message) => {
8515
10172
  const audioRequest = message;
8516
10173
  const { message_id, type = "output" } = audioRequest.payload;
@@ -9016,6 +10673,40 @@ async function bootstrap() {
9016
10673
  );
9017
10674
  });
9018
10675
  });
10676
+ if (env.MOCK_MODE) {
10677
+ const terminalSession = await sessionManager.createSession({
10678
+ sessionType: "terminal",
10679
+ workingDir: env.WORKSPACES_ROOT,
10680
+ terminalSize: { cols: 80, rows: 24 }
10681
+ });
10682
+ logger.info(
10683
+ { sessionId: terminalSession.id.value },
10684
+ "Pre-created terminal session for screenshots"
10685
+ );
10686
+ if (terminalSession.type === "terminal") {
10687
+ setTimeout(() => {
10688
+ try {
10689
+ const command = `printf $'\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\033[1;32m Tiflis Code - Remote Development Workstation\\033[0m\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n\\033[1;33m System Information:\\033[0m\\n \u251C\u2500 OS: macOS Sequoia 15.1\\n \u251C\u2500 Shell: zsh 5.9\\n \u251C\u2500 Node: v22.11.0\\n \u2514\u2500 Uptime: 2 days, 14 hours\\n\\n\\033[1;33m Active Sessions:\\033[0m\\n \u251C\u2500 Claude Code \u2500 tiflis/tiflis-code\\n \u251C\u2500 Cursor \u2500 personal/portfolio\\n \u2514\u2500 OpenCode \u2500 tiflis/tiflis-api\\n\\n\\033[1;36m\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\033[0m\\n\\n'`;
10690
+ ptyManager.write(terminalSession, command + "\r");
10691
+ logger.info("Ran terminal commands for screenshots");
10692
+ setTimeout(() => {
10693
+ const ts = terminalSession;
10694
+ const history = ts.getOutputHistory();
10695
+ logger.info(
10696
+ {
10697
+ sessionId: ts.id.value,
10698
+ bufferSize: history.length,
10699
+ currentSequence: ts.currentSequence
10700
+ },
10701
+ "Terminal output buffer status after command"
10702
+ );
10703
+ }, 500);
10704
+ } catch (error) {
10705
+ logger.warn({ error }, "Failed to run terminal commands");
10706
+ }
10707
+ }, 100);
10708
+ }
10709
+ }
9019
10710
  const app = createApp({ env, logger });
9020
10711
  registerHealthRoute(
9021
10712
  app,
@@ -9328,6 +11019,36 @@ bootstrap().catch((error) => {
9328
11019
  *
9329
11020
  * LangGraph-based Supervisor Agent for managing workstation resources.
9330
11021
  */
11022
+ /**
11023
+ * @file fixture-loader.ts
11024
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11025
+ * @license FSL-1.1-NC
11026
+ *
11027
+ * Loads and parses JSON fixture files for mock mode.
11028
+ */
11029
+ /**
11030
+ * @file streaming-simulator.ts
11031
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11032
+ * @license FSL-1.1-NC
11033
+ *
11034
+ * Simulates realistic streaming output for mock responses.
11035
+ */
11036
+ /**
11037
+ * @file mock-supervisor-agent.ts
11038
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11039
+ * @license FSL-1.1-NC
11040
+ *
11041
+ * Mock Supervisor Agent for screenshot automation tests.
11042
+ * Returns fixture-based responses with simulated streaming.
11043
+ */
11044
+ /**
11045
+ * @file mock-agent-session-manager.ts
11046
+ * @copyright 2025 Roman Barinov <rbarinov@gmail.com>
11047
+ * @license FSL-1.1-NC
11048
+ *
11049
+ * Mock Agent Session Manager for screenshot automation tests.
11050
+ * Simulates agent sessions with fixture-based responses and streaming.
11051
+ */
9331
11052
  /**
9332
11053
  * @file stt-service.ts
9333
11054
  * @copyright 2025 Roman Barinov <rbarinov@gmail.com>