@ebowwa/coder 0.7.64 → 0.7.66

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 (101) hide show
  1. package/dist/index.js +36233 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +377 -176
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -0,0 +1,750 @@
1
+ /**
2
+ * TeammateModeRunner - Manages teammate mode lifecycle
3
+ *
4
+ * Responsibilities:
5
+ * - Load team config from TeammateManager
6
+ * - Poll for incoming messages from file-based inbox
7
+ * - Inject messages into conversation via callback
8
+ * - Report idle status to team
9
+ * - Coordinate with task system
10
+ * - File locking and progress reporting
11
+ */
12
+
13
+ import { TeammateManager, generateTeammateId } from "./index.js";
14
+ import {
15
+ CoordinationManager,
16
+ type CoordinationConfig,
17
+ type CoordinationEvent,
18
+ type ProgressReport,
19
+ type FileClaim,
20
+ type CoordinationEventType,
21
+ } from "./coordination.js";
22
+ import type { Teammate, Team, TeammateMessage, TeammateStatus } from "../types/index.js";
23
+
24
+ // ============================================
25
+ // TYPES
26
+ // ============================================
27
+
28
+ export interface TeammateModeConfig {
29
+ /** Team name to join */
30
+ teamName: string;
31
+ /** Agent ID (if resuming) */
32
+ agentId?: string;
33
+ /** Agent display name */
34
+ agentName?: string;
35
+ /** Agent color for UI */
36
+ agentColor?: string;
37
+ /** Prompt/instructions for this teammate */
38
+ prompt?: string;
39
+ /** Working directory */
40
+ workingDirectory: string;
41
+ /** Message injection callback */
42
+ onMessage?: (message: TeammateMessage) => void;
43
+ /** Idle callback */
44
+ onIdle?: () => void;
45
+ /** Polling interval in ms (default: 2000) */
46
+ pollInterval?: number;
47
+ /** Coordination configuration */
48
+ coordination?: CoordinationConfig;
49
+ /** Callback when another teammate reports progress */
50
+ onTeammateProgress?: (teammateId: string, progress: ProgressReport) => void;
51
+ /** Callback when a file is claimed by another teammate */
52
+ onFileClaimed?: (claim: FileClaim) => void;
53
+ /** Callback when a file is released */
54
+ onFileReleased?: (filePath: string, teammateId: string) => void;
55
+ /** Callback for any coordination event */
56
+ onCoordinationEvent?: (event: CoordinationEvent) => void;
57
+ }
58
+
59
+ export interface TeammateModeState {
60
+ /** Whether teammate mode is active */
61
+ active: boolean;
62
+ /** Current teammate instance */
63
+ teammate: Teammate | null;
64
+ /** Team instance */
65
+ team: Team | null;
66
+ /** Message polling timer */
67
+ pollTimer: Timer | null;
68
+ /** Last activity timestamp */
69
+ lastActivity: number;
70
+ /** Current status */
71
+ status: TeammateStatus;
72
+ /** Pending messages waiting to be processed */
73
+ pendingMessages: TeammateMessage[];
74
+ /** Files currently claimed by this teammate */
75
+ claimedFiles: Set<string>;
76
+ /** Current progress report */
77
+ currentProgress?: ProgressReport;
78
+ }
79
+
80
+ // ============================================
81
+ // TEAMMATE MODE RUNNER
82
+ // ============================================
83
+
84
+ export class TeammateModeRunner {
85
+ private manager: TeammateManager;
86
+ private coordination: CoordinationManager;
87
+ private config: TeammateModeConfig;
88
+ private state: TeammateModeState;
89
+ private idleCheckTimer: Timer | null = null;
90
+ private readonly IDLE_TIMEOUT = 60000; // 60 seconds of no activity = idle
91
+
92
+ constructor(config: TeammateModeConfig) {
93
+ this.config = config;
94
+ this.manager = new TeammateManager();
95
+ this.coordination = new CoordinationManager(
96
+ this.manager,
97
+ config.coordination || {}
98
+ );
99
+ this.state = {
100
+ active: false,
101
+ teammate: null,
102
+ team: null,
103
+ pollTimer: null,
104
+ lastActivity: Date.now(),
105
+ status: "pending",
106
+ pendingMessages: [],
107
+ claimedFiles: new Set(),
108
+ };
109
+
110
+ // Set up coordination callbacks
111
+ this.setupCoordinationCallbacks();
112
+ }
113
+
114
+ /**
115
+ * Set up coordination event callbacks
116
+ */
117
+ private setupCoordinationCallbacks(): void {
118
+ const coordinationConfig = this.config.coordination || {};
119
+
120
+ // Merge user callbacks with internal handling
121
+ this.coordination = new CoordinationManager(this.manager, {
122
+ ...coordinationConfig,
123
+ onProgress: (teammateId, progress) => {
124
+ if (this.config.onTeammateProgress) {
125
+ this.config.onTeammateProgress(teammateId, progress);
126
+ }
127
+ if (coordinationConfig.onProgress) {
128
+ coordinationConfig.onProgress(teammateId, progress);
129
+ }
130
+ },
131
+ onFileClaimed: (claim) => {
132
+ if (this.config.onFileClaimed) {
133
+ this.config.onFileClaimed(claim);
134
+ }
135
+ if (coordinationConfig.onFileClaimed) {
136
+ coordinationConfig.onFileClaimed(claim);
137
+ }
138
+ },
139
+ onFileReleased: (filePath, teammateId) => {
140
+ if (this.config.onFileReleased) {
141
+ this.config.onFileReleased(filePath, teammateId);
142
+ }
143
+ if (coordinationConfig.onFileReleased) {
144
+ coordinationConfig.onFileReleased(filePath, teammateId);
145
+ }
146
+ },
147
+ onCoordinationEvent: (event) => {
148
+ if (this.config.onCoordinationEvent) {
149
+ this.config.onCoordinationEvent(event);
150
+ }
151
+ if (coordinationConfig.onCoordinationEvent) {
152
+ coordinationConfig.onCoordinationEvent(event);
153
+ }
154
+ },
155
+ });
156
+ }
157
+
158
+ // ============================================
159
+ // LIFECYCLE
160
+ // ============================================
161
+
162
+ /**
163
+ * Start teammate mode
164
+ * Creates or resumes teammate, joins team, starts polling
165
+ */
166
+ async start(): Promise<Teammate> {
167
+ if (this.state.active) {
168
+ throw new Error("Teammate mode already active");
169
+ }
170
+
171
+ // Load or create team
172
+ let team = this.manager.getTeam(this.config.teamName);
173
+ if (!team) {
174
+ // Create team if it doesn't exist
175
+ team = this.manager.createTeam({
176
+ name: this.config.teamName,
177
+ description: `Team ${this.config.teamName}`,
178
+ teammates: [],
179
+ taskListId: `${this.config.teamName}-tasks`,
180
+ coordination: {
181
+ dependencyOrder: [],
182
+ communicationProtocol: "broadcast",
183
+ taskAssignmentStrategy: "manual",
184
+ },
185
+ });
186
+ }
187
+ this.state.team = team;
188
+
189
+ // Create or resume teammate
190
+ let teammate: Teammate;
191
+ if (this.config.agentId) {
192
+ // Resume existing teammate
193
+ const existing = this.manager.getTeammate(this.config.agentId);
194
+ if (!existing) {
195
+ throw new Error(`Teammate not found: ${this.config.agentId}`);
196
+ }
197
+ teammate = existing;
198
+ } else {
199
+ // Create new teammate
200
+ teammate = {
201
+ teammateId: this.config.agentId || generateTeammateId(),
202
+ name: this.config.agentName || `agent-${Date.now().toString(36)}`,
203
+ teamName: this.config.teamName,
204
+ color: this.config.agentColor || "blue",
205
+ prompt: this.config.prompt || "",
206
+ planModeRequired: false,
207
+ insideTmux: !!process.env.TMUX,
208
+ status: "pending",
209
+ };
210
+
211
+ // Add to team
212
+ this.manager.addTeammate(this.config.teamName, teammate);
213
+ }
214
+
215
+ this.state.teammate = teammate;
216
+ this.state.active = true;
217
+ this.state.status = "in_progress";
218
+ this.state.lastActivity = Date.now();
219
+
220
+ // Update status
221
+ this.manager.updateTeammateStatus(teammate.teammateId, "in_progress");
222
+
223
+ // Initialize coordination
224
+ this.coordination.initialize(teammate.teammateId, this.config.teamName);
225
+
226
+ // Start message polling
227
+ this.startPolling();
228
+
229
+ // Start idle check
230
+ this.startIdleCheck();
231
+
232
+ // Broadcast join
233
+ this.broadcast(`Teammate ${teammate.name} joined the team`);
234
+
235
+ return teammate;
236
+ }
237
+
238
+ /**
239
+ * Stop teammate mode
240
+ * Stops polling, updates status, cleans up
241
+ */
242
+ async stop(): Promise<void> {
243
+ if (!this.state.active) return;
244
+
245
+ // Stop polling
246
+ this.stopPolling();
247
+ this.stopIdleCheck();
248
+
249
+ // Release all file claims
250
+ this.releaseAllFiles();
251
+
252
+ // Shutdown coordination
253
+ this.coordination.shutdown();
254
+
255
+ // Update status to idle
256
+ if (this.state.teammate) {
257
+ this.manager.updateTeammateStatus(this.state.teammate.teammateId, "idle");
258
+ }
259
+
260
+ // Persist team state
261
+ if (this.state.team) {
262
+ await this.manager.persistAllTeams();
263
+ }
264
+
265
+ this.state.active = false;
266
+ this.state.status = "idle";
267
+ }
268
+
269
+ // ============================================
270
+ // MESSAGE POLLING
271
+ // ============================================
272
+
273
+ private startPolling(): void {
274
+ if (this.state.pollTimer) return;
275
+
276
+ const interval = this.config.pollInterval || 2000;
277
+
278
+ this.state.pollTimer = setInterval(() => {
279
+ this.pollMessages();
280
+ }, interval);
281
+
282
+ // Also poll immediately
283
+ this.pollMessages();
284
+ }
285
+
286
+ private stopPolling(): void {
287
+ if (this.state.pollTimer) {
288
+ clearInterval(this.state.pollTimer);
289
+ this.state.pollTimer = null;
290
+ }
291
+ }
292
+
293
+ private pollMessages(): void {
294
+ if (!this.state.teammate) return;
295
+
296
+ // Check for new messages
297
+ const messages = this.manager.getMessages(this.state.teammate.teammateId);
298
+
299
+ for (const message of messages) {
300
+ // Track activity
301
+ this.state.lastActivity = Date.now();
302
+
303
+ // Add to pending
304
+ this.state.pendingMessages.push(message);
305
+
306
+ // Notify via callback
307
+ if (this.config.onMessage) {
308
+ this.config.onMessage(message);
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Get pending messages and clear queue
315
+ */
316
+ getPendingMessages(): TeammateMessage[] {
317
+ const messages = [...this.state.pendingMessages];
318
+ this.state.pendingMessages = [];
319
+ return messages;
320
+ }
321
+
322
+ /**
323
+ * Check if there are pending messages
324
+ */
325
+ hasPendingMessages(): boolean {
326
+ return this.state.pendingMessages.length > 0;
327
+ }
328
+
329
+ /**
330
+ * Peek at pending messages without clearing
331
+ */
332
+ peekPendingMessages(): TeammateMessage[] {
333
+ return [...this.state.pendingMessages];
334
+ }
335
+
336
+ // ============================================
337
+ // IDLE DETECTION
338
+ // ============================================
339
+
340
+ private startIdleCheck(): void {
341
+ if (this.idleCheckTimer) return;
342
+
343
+ this.idleCheckTimer = setInterval(() => {
344
+ this.checkIdle();
345
+ }, 10000); // Check every 10 seconds
346
+ }
347
+
348
+ private stopIdleCheck(): void {
349
+ if (this.idleCheckTimer) {
350
+ clearInterval(this.idleCheckTimer);
351
+ this.idleCheckTimer = null;
352
+ }
353
+ }
354
+
355
+ private checkIdle(): void {
356
+ if (!this.state.teammate) return;
357
+
358
+ const now = Date.now();
359
+ const timeSinceActivity = now - this.state.lastActivity;
360
+
361
+ if (timeSinceActivity > this.IDLE_TIMEOUT && this.state.status !== "idle") {
362
+ // Transition to idle
363
+ this.state.status = "idle";
364
+ this.manager.updateTeammateStatus(this.state.teammate.teammateId, "idle");
365
+
366
+ // Notify via callback
367
+ if (this.config.onIdle) {
368
+ this.config.onIdle();
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Report activity (resets idle timer)
375
+ */
376
+ reportActivity(): void {
377
+ this.state.lastActivity = Date.now();
378
+
379
+ if (this.state.status !== "in_progress" && this.state.teammate) {
380
+ this.state.status = "in_progress";
381
+ this.manager.updateTeammateStatus(this.state.teammate.teammateId, "in_progress");
382
+ }
383
+ }
384
+
385
+ // ============================================
386
+ // MESSAGING
387
+ // ============================================
388
+
389
+ /**
390
+ * Send a direct message to another teammate
391
+ */
392
+ sendDirectMessage(toId: string, content: string): void {
393
+ if (!this.state.teammate) {
394
+ throw new Error("Teammate mode not active");
395
+ }
396
+
397
+ this.manager.sendDirect(toId, this.state.teammate.teammateId, content);
398
+ this.reportActivity();
399
+ }
400
+
401
+ /**
402
+ * Broadcast a message to all teammates
403
+ */
404
+ broadcast(content: string): void {
405
+ if (!this.state.teammate) {
406
+ throw new Error("Teammate mode not active");
407
+ }
408
+
409
+ this.manager.broadcast(this.config.teamName, content, this.state.teammate.teammateId);
410
+ this.reportActivity();
411
+ }
412
+
413
+ /**
414
+ * Inject a user message (for external integration)
415
+ */
416
+ injectUserMessage(content: string): void {
417
+ if (!this.state.teammate) {
418
+ throw new Error("Teammate mode not active");
419
+ }
420
+
421
+ this.manager.injectUserMessageToTeammate(this.state.teammate.teammateId, content);
422
+ }
423
+
424
+ // ============================================
425
+ // STATUS & INFO
426
+ // ============================================
427
+
428
+ /**
429
+ * Get current teammate info
430
+ */
431
+ getTeammate(): Teammate | null {
432
+ return this.state.teammate;
433
+ }
434
+
435
+ /**
436
+ * Get team info
437
+ */
438
+ getTeam(): Team | null {
439
+ return this.state.team;
440
+ }
441
+
442
+ /**
443
+ * Get current status
444
+ */
445
+ getStatus(): TeammateStatus {
446
+ return this.state.status;
447
+ }
448
+
449
+ /**
450
+ * Update status
451
+ */
452
+ updateStatus(status: TeammateStatus): void {
453
+ this.state.status = status;
454
+ if (this.state.teammate) {
455
+ this.manager.updateTeammateStatus(this.state.teammate.teammateId, status);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Check if teammate mode is active
461
+ */
462
+ isActive(): boolean {
463
+ return this.state.active;
464
+ }
465
+
466
+ /**
467
+ * Get inbox statistics
468
+ */
469
+ getInboxStats(): { pending: number; processed: number } {
470
+ if (!this.state.teammate) {
471
+ return { pending: 0, processed: 0 };
472
+ }
473
+ return this.manager.getInboxStats(this.state.teammate.teammateId);
474
+ }
475
+
476
+ // ============================================
477
+ // TEAM OPERATIONS
478
+ // ============================================
479
+
480
+ /**
481
+ * Get all teammates in the team
482
+ */
483
+ getTeamMembers(): Teammate[] {
484
+ if (!this.state.team) return [];
485
+ return this.state.team.teammates;
486
+ }
487
+
488
+ /**
489
+ * Wait for all teammates to become idle
490
+ */
491
+ async waitForTeamIdle(options?: { timeout?: number; pollInterval?: number }): Promise<{
492
+ success: boolean;
493
+ timedOut: boolean;
494
+ statuses: Record<string, TeammateStatus>;
495
+ }> {
496
+ if (!this.state.team) {
497
+ return { success: false, timedOut: false, statuses: {} };
498
+ }
499
+
500
+ return this.manager.waitForTeammatesToBecomeIdle(this.config.teamName, options);
501
+ }
502
+
503
+ // ============================================
504
+ // COORDINATION - PROGRESS REPORTING
505
+ // ============================================
506
+
507
+ /**
508
+ * Report progress to teammates
509
+ * This broadcasts progress information to all team members
510
+ */
511
+ reportProgress(progress: ProgressReport): void {
512
+ if (!this.state.active) return;
513
+
514
+ this.state.currentProgress = progress;
515
+ this.coordination.reportProgress(progress);
516
+ this.reportActivity();
517
+ }
518
+
519
+ /**
520
+ * Report that you're blocked on something
521
+ */
522
+ reportBlocked(reason: string, blockedBy?: string): void {
523
+ if (!this.state.active) return;
524
+
525
+ this.updateStatus("pending");
526
+ this.coordination.reportBlocked(reason, blockedBy);
527
+ }
528
+
529
+ /**
530
+ * Report that you're unblocked
531
+ */
532
+ reportUnblocked(): void {
533
+ if (!this.state.active) return;
534
+
535
+ this.updateStatus("in_progress");
536
+ this.coordination.reportUnblocked();
537
+ }
538
+
539
+ /**
540
+ * Get progress reports from all teammates
541
+ */
542
+ getTeammateProgress(): Map<string, ProgressReport> {
543
+ // This would require storing progress in TeammateManager
544
+ // For now, return empty map
545
+ return new Map();
546
+ }
547
+
548
+ // ============================================
549
+ // COORDINATION - FILE LOCKING
550
+ // ============================================
551
+
552
+ /**
553
+ * Claim a file for exclusive editing
554
+ * Returns true if claim was successful
555
+ */
556
+ claimFile(filePath: string, reason?: string, expiresIn?: number): boolean {
557
+ if (!this.state.active) {
558
+ throw new Error("Teammate mode not active");
559
+ }
560
+
561
+ const claimed = this.coordination.claimFile(filePath, reason, expiresIn);
562
+ if (claimed) {
563
+ this.state.claimedFiles.add(filePath);
564
+ }
565
+ return claimed;
566
+ }
567
+
568
+ /**
569
+ * Release a file claim
570
+ */
571
+ releaseFile(filePath: string): void {
572
+ if (!this.state.active) return;
573
+
574
+ this.coordination.releaseFile(filePath);
575
+ this.state.claimedFiles.delete(filePath);
576
+ }
577
+
578
+ /**
579
+ * Release all file claims
580
+ */
581
+ releaseAllFiles(): void {
582
+ if (!this.state.active) return;
583
+
584
+ for (const file of this.state.claimedFiles) {
585
+ this.coordination.releaseFile(file);
586
+ }
587
+ this.state.claimedFiles.clear();
588
+ }
589
+
590
+ /**
591
+ * Check if a file is claimed by another teammate
592
+ */
593
+ isFileClaimed(filePath: string): boolean {
594
+ return this.coordination.isFileClaimed(filePath);
595
+ }
596
+
597
+ /**
598
+ * Get the claim on a file (if any)
599
+ */
600
+ getFileClaim(filePath: string): FileClaim | undefined {
601
+ return this.coordination.getFileClaim(filePath);
602
+ }
603
+
604
+ /**
605
+ * Get all files claimed by this teammate
606
+ */
607
+ getClaimedFiles(): string[] {
608
+ return Array.from(this.state.claimedFiles);
609
+ }
610
+
611
+ /**
612
+ * Get all claims across the team
613
+ */
614
+ getAllClaims(): FileClaim[] {
615
+ return this.coordination.getAllClaims();
616
+ }
617
+
618
+ /**
619
+ * Attempt to claim a file, wait if blocked
620
+ */
621
+ async waitForFileClaim(
622
+ filePath: string,
623
+ options?: {
624
+ timeout?: number;
625
+ pollInterval?: number;
626
+ reason?: string;
627
+ }
628
+ ): Promise<boolean> {
629
+ const { timeout = 30000, pollInterval = 1000, reason } = options || {};
630
+ const startTime = Date.now();
631
+
632
+ while (Date.now() - startTime < timeout) {
633
+ if (this.claimFile(filePath, reason)) {
634
+ return true;
635
+ }
636
+
637
+ // Check if current claimant has finished
638
+ const claim = this.getFileClaim(filePath);
639
+ if (claim) {
640
+ // Wait and retry
641
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
642
+ } else {
643
+ // Try again immediately
644
+ if (this.claimFile(filePath, reason)) {
645
+ return true;
646
+ }
647
+ }
648
+ }
649
+
650
+ return false;
651
+ }
652
+
653
+ // ============================================
654
+ // COORDINATION - UTILITIES
655
+ // ============================================
656
+
657
+ /**
658
+ * Check if we should work on a file (not claimed by others)
659
+ */
660
+ canWorkOnFile(filePath: string): boolean {
661
+ if (this.isFileClaimed(filePath)) {
662
+ const claim = this.getFileClaim(filePath);
663
+ // Can work if we're the claimant
664
+ return claim?.teammateId === this.state.teammate?.teammateId;
665
+ }
666
+ return true;
667
+ }
668
+
669
+ /**
670
+ * Claim multiple files atomically
671
+ * Returns true if all claims were successful
672
+ */
673
+ claimFiles(files: string[], reason?: string): boolean {
674
+ const claimed: string[] = [];
675
+
676
+ for (const file of files) {
677
+ if (this.claimFile(file, reason)) {
678
+ claimed.push(file);
679
+ } else {
680
+ // Rollback - release all claimed files
681
+ for (const f of claimed) {
682
+ this.releaseFile(f);
683
+ }
684
+ return false;
685
+ }
686
+ }
687
+
688
+ return true;
689
+ }
690
+
691
+ // ============================================
692
+ // TASK INTEGRATION
693
+ // ============================================
694
+
695
+ /**
696
+ * Report task completion
697
+ */
698
+ reportTaskComplete(taskId: string, taskSubject: string): void {
699
+ // Broadcast first, then update status (broadcast calls reportActivity which resets to in_progress)
700
+ this.broadcast(`Task completed: ${taskSubject} (${taskId})`);
701
+ this.updateStatus("completed");
702
+ }
703
+
704
+ /**
705
+ * Report task failure
706
+ */
707
+ reportTaskFailed(taskId: string, taskSubject: string, error: string): void {
708
+ // Broadcast first, then update status
709
+ this.broadcast(`Task failed: ${taskSubject} (${taskId}) - ${error}`);
710
+ this.updateStatus("failed");
711
+ }
712
+
713
+ /**
714
+ * Request task assignment
715
+ */
716
+ requestTask(): void {
717
+ // Update status first, then broadcast (broadcast would reset to in_progress)
718
+ this.updateStatus("idle");
719
+ this.broadcast("Ready for task assignment");
720
+ // Ensure status stays idle after broadcast
721
+ this.state.status = "idle";
722
+ }
723
+ }
724
+
725
+ // ============================================
726
+ // SINGLETON FOR GLOBAL ACCESS
727
+ // ============================================
728
+
729
+ let globalRunner: TeammateModeRunner | null = null;
730
+
731
+ /**
732
+ * Get the global teammate mode runner (if active)
733
+ */
734
+ export function getTeammateRunner(): TeammateModeRunner | null {
735
+ return globalRunner;
736
+ }
737
+
738
+ /**
739
+ * Set the global teammate mode runner
740
+ */
741
+ export function setTeammateRunner(runner: TeammateModeRunner | null): void {
742
+ globalRunner = runner;
743
+ }
744
+
745
+ /**
746
+ * Check if teammate mode is globally active
747
+ */
748
+ export function isTeammateModeActive(): boolean {
749
+ return globalRunner !== null && globalRunner.isActive();
750
+ }