@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,646 @@
1
+ /**
2
+ * Coordination Callbacks - Inter-agent communication and progress reporting
3
+ *
4
+ * Features:
5
+ * - Progress reporting callbacks
6
+ * - File locking/claiming
7
+ * - Status change notifications
8
+ * - Heartbeat system
9
+ */
10
+
11
+ import { TeammateManager } from "./index.js";
12
+ import type { Teammate, TeammateStatus, TeammateMessage } from "../types/index.js";
13
+ import { writeFileSync, existsSync, rmSync, readFileSync, mkdirSync, readdirSync } from "fs";
14
+ import { join } from "path";
15
+
16
+ // ============================================
17
+ // TYPES
18
+ // ============================================
19
+
20
+ export type CoordinationEventType =
21
+ | "progress"
22
+ | "status_change"
23
+ | "file_claim"
24
+ | "file_release"
25
+ | "heartbeat"
26
+ | "task_start"
27
+ | "task_progress"
28
+ | "task_complete"
29
+ | "task_failed"
30
+ | "blocked"
31
+ | "unblocked";
32
+
33
+ export interface CoordinationEvent {
34
+ type: CoordinationEventType;
35
+ teammateId: string;
36
+ teammateName: string;
37
+ teamName: string;
38
+ timestamp: number;
39
+ payload: Record<string, unknown>;
40
+ }
41
+
42
+ export interface ProgressReport {
43
+ /** Current step description */
44
+ step: string;
45
+ /** Current step number */
46
+ stepNumber: number;
47
+ /** Total steps (if known) */
48
+ totalSteps?: number;
49
+ /** Percentage complete (0-100) */
50
+ percentage?: number;
51
+ /** Files being worked on */
52
+ files?: string[];
53
+ /** Any blockers encountered */
54
+ blockers?: string[];
55
+ /** ETA in seconds */
56
+ eta?: number;
57
+ }
58
+
59
+ export interface FileClaim {
60
+ /** File path being claimed */
61
+ filePath: string;
62
+ /** Teammate ID claiming the file */
63
+ teammateId: string;
64
+ /** Teammate name */
65
+ teammateName: string;
66
+ /** When the claim was made */
67
+ claimedAt: number;
68
+ /** Optional expiration time */
69
+ expiresAt?: number;
70
+ /** Reason for claim */
71
+ reason?: string;
72
+ }
73
+
74
+ export type CoordinationCallback = (event: CoordinationEvent) => void;
75
+
76
+ export interface CoordinationConfig {
77
+ /** Enable progress reporting */
78
+ enableProgressReporting?: boolean;
79
+ /** Enable file locking */
80
+ enableFileLocking?: boolean;
81
+ /** Enable heartbeat */
82
+ enableHeartbeat?: boolean;
83
+ /** Heartbeat interval in ms (default: 30000) */
84
+ heartbeatInterval?: number;
85
+ /** Callback for coordination events */
86
+ onCoordinationEvent?: CoordinationCallback;
87
+ /** Callback for status changes from other teammates */
88
+ onStatusChange?: (teammateId: string, oldStatus: TeammateStatus, newStatus: TeammateStatus) => void;
89
+ /** Callback when a teammate reports progress */
90
+ onProgress?: (teammateId: string, progress: ProgressReport) => void;
91
+ /** Callback when a file is claimed */
92
+ onFileClaimed?: (claim: FileClaim) => void;
93
+ /** Callback when a file is released */
94
+ onFileReleased?: (filePath: string, teammateId: string) => void;
95
+ }
96
+
97
+ // ============================================
98
+ // COORDINATION MANAGER
99
+ // ============================================
100
+
101
+ export class CoordinationManager {
102
+ private manager: TeammateManager;
103
+ private storagePath: string;
104
+ private claimsPath: string;
105
+ private claims = new Map<string, FileClaim>();
106
+ private heartbeatTimer: Timer | null = null;
107
+ private config: CoordinationConfig;
108
+ private teammateId: string | null = null;
109
+ private teamName: string | null = null;
110
+
111
+ constructor(manager: TeammateManager, config: CoordinationConfig = {}) {
112
+ this.manager = manager;
113
+ this.config = config;
114
+ this.storagePath = join(process.env.HOME || "", ".claude", "teams");
115
+ this.claimsPath = join(this.storagePath, "_coordination", "claims");
116
+ this.ensureClaimsDirectory();
117
+ this.loadExistingClaims();
118
+ }
119
+
120
+ private ensureClaimsDirectory(): void {
121
+ if (!existsSync(this.claimsPath)) {
122
+ mkdirSync(this.claimsPath, { recursive: true });
123
+ }
124
+ }
125
+
126
+ private loadExistingClaims(): void {
127
+ this.reloadClaims();
128
+ }
129
+
130
+ /**
131
+ * Reload claims from disk - useful for synchronizing between instances
132
+ */
133
+ reloadClaims(): void {
134
+ if (!existsSync(this.claimsPath)) return;
135
+
136
+ // Clear current claims (we'll reload from disk)
137
+ this.claims.clear();
138
+
139
+ try {
140
+ const files = readdirSync(this.claimsPath).filter(f => f.endsWith(".json"));
141
+
142
+ for (const file of files) {
143
+ try {
144
+ const content = readFileSync(join(this.claimsPath, file), "utf-8");
145
+ const claim = JSON.parse(content) as FileClaim;
146
+
147
+ // Check if claim has expired
148
+ if (claim.expiresAt && claim.expiresAt < Date.now()) {
149
+ this.deleteClaimFile(claim.filePath);
150
+ continue;
151
+ }
152
+
153
+ this.claims.set(this.normalizePath(claim.filePath), claim);
154
+ } catch {
155
+ // Skip malformed claim files
156
+ }
157
+ }
158
+ } catch {
159
+ // Directory may not exist
160
+ }
161
+ }
162
+
163
+ private normalizePath(filePath: string): string {
164
+ return filePath.replace(/\\/g, "/").toLowerCase();
165
+ }
166
+
167
+ private getClaimFilePath(filePath: string): string {
168
+ const normalized = this.normalizePath(filePath);
169
+ const hash = this.hashPath(normalized);
170
+ return join(this.claimsPath, `${hash}.json`);
171
+ }
172
+
173
+ private hashPath(path: string): string {
174
+ // Simple hash for file path
175
+ let hash = 0;
176
+ for (let i = 0; i < path.length; i++) {
177
+ const char = path.charCodeAt(i);
178
+ hash = ((hash << 5) - hash) + char;
179
+ hash = hash & hash;
180
+ }
181
+ return Math.abs(hash).toString(36);
182
+ }
183
+
184
+ private deleteClaimFile(filePath: string): void {
185
+ const claimPath = this.getClaimFilePath(filePath);
186
+ if (existsSync(claimPath)) {
187
+ rmSync(claimPath);
188
+ }
189
+ }
190
+
191
+ // ============================================
192
+ // INITIALIZATION
193
+ // ============================================
194
+
195
+ /**
196
+ * Initialize coordination for a teammate
197
+ */
198
+ initialize(teammateId: string, teamName: string): void {
199
+ this.teammateId = teammateId;
200
+ this.teamName = teamName;
201
+
202
+ if (this.config.enableHeartbeat) {
203
+ this.startHeartbeat();
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Shutdown coordination
209
+ */
210
+ shutdown(): void {
211
+ if (this.heartbeatTimer) {
212
+ clearInterval(this.heartbeatTimer);
213
+ this.heartbeatTimer = null;
214
+ }
215
+
216
+ // Release all claims
217
+ if (this.teammateId) {
218
+ this.releaseAllClaims();
219
+ }
220
+ }
221
+
222
+ // ============================================
223
+ // HEARTBEAT
224
+ // ============================================
225
+
226
+ private startHeartbeat(): void {
227
+ const interval = this.config.heartbeatInterval || 30000;
228
+
229
+ this.heartbeatTimer = setInterval(() => {
230
+ this.sendHeartbeat();
231
+ }, interval);
232
+
233
+ // Send initial heartbeat
234
+ this.sendHeartbeat();
235
+ }
236
+
237
+ private sendHeartbeat(): void {
238
+ if (!this.teammateId || !this.teamName) return;
239
+
240
+ this.emitEvent({
241
+ type: "heartbeat",
242
+ teammateId: this.teammateId,
243
+ teammateName: this.getTeammateName(),
244
+ teamName: this.teamName,
245
+ timestamp: Date.now(),
246
+ payload: {},
247
+ });
248
+ }
249
+
250
+ // ============================================
251
+ // PROGRESS REPORTING
252
+ // ============================================
253
+
254
+ /**
255
+ * Report progress to the team
256
+ */
257
+ reportProgress(progress: ProgressReport): void {
258
+ if (!this.teammateId || !this.teamName) return;
259
+
260
+ this.emitEvent({
261
+ type: "task_progress",
262
+ teammateId: this.teammateId,
263
+ teammateName: this.getTeammateName(),
264
+ teamName: this.teamName,
265
+ timestamp: Date.now(),
266
+ payload: { progress },
267
+ });
268
+
269
+ // Broadcast progress message to teammates
270
+ this.manager.broadcast(
271
+ this.teamName,
272
+ `[PROGRESS] ${progress.step} (${progress.stepNumber}${progress.totalSteps ? `/${progress.totalSteps}` : ""})${progress.percentage ? ` - ${progress.percentage}%` : ""}`,
273
+ this.teammateId
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Report task start
279
+ */
280
+ reportTaskStart(taskId: string, taskDescription: string): void {
281
+ if (!this.teammateId || !this.teamName) return;
282
+
283
+ this.emitEvent({
284
+ type: "task_start",
285
+ teammateId: this.teammateId,
286
+ teammateName: this.getTeammateName(),
287
+ teamName: this.teamName,
288
+ timestamp: Date.now(),
289
+ payload: { taskId, taskDescription },
290
+ });
291
+
292
+ this.manager.broadcast(
293
+ this.teamName,
294
+ `[STARTED] ${taskDescription} (${taskId})`,
295
+ this.teammateId
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Report that you're blocked
301
+ */
302
+ reportBlocked(reason: string, blockedBy?: string): void {
303
+ if (!this.teammateId || !this.teamName) return;
304
+
305
+ this.emitEvent({
306
+ type: "blocked",
307
+ teammateId: this.teammateId,
308
+ teammateName: this.getTeammateName(),
309
+ teamName: this.teamName,
310
+ timestamp: Date.now(),
311
+ payload: { reason, blockedBy },
312
+ });
313
+
314
+ this.manager.broadcast(
315
+ this.teamName,
316
+ `[BLOCKED] ${reason}${blockedBy ? ` (waiting on: ${blockedBy})` : ""}`,
317
+ this.teammateId
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Report that you're unblocked
323
+ */
324
+ reportUnblocked(): void {
325
+ if (!this.teammateId || !this.teamName) return;
326
+
327
+ this.emitEvent({
328
+ type: "unblocked",
329
+ teammateId: this.teammateId,
330
+ teammateName: this.getTeammateName(),
331
+ teamName: this.teamName,
332
+ timestamp: Date.now(),
333
+ payload: {},
334
+ });
335
+
336
+ this.manager.broadcast(
337
+ this.teamName,
338
+ `[UNBLOCKED] Resuming work`,
339
+ this.teammateId
340
+ );
341
+ }
342
+
343
+ // ============================================
344
+ // FILE LOCKING
345
+ // ============================================
346
+
347
+ /**
348
+ * Check if a specific file is claimed on disk (by another teammate)
349
+ */
350
+ private checkClaimOnDisk(filePath: string): FileClaim | null {
351
+ const claimPath = this.getClaimFilePath(filePath);
352
+ if (!existsSync(claimPath)) return null;
353
+
354
+ try {
355
+ const content = readFileSync(claimPath, "utf-8");
356
+ const claim = JSON.parse(content) as FileClaim;
357
+
358
+ // Check if expired
359
+ if (claim.expiresAt && claim.expiresAt < Date.now()) {
360
+ this.deleteClaimFile(filePath);
361
+ return null;
362
+ }
363
+
364
+ // Only return if it's claimed by someone else
365
+ if (claim.teammateId !== this.teammateId) {
366
+ return claim;
367
+ }
368
+ return null;
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Claim exclusive access to a file
376
+ */
377
+ claimFile(filePath: string, reason?: string, expiresIn?: number): boolean {
378
+ if (!this.config.enableFileLocking) return true;
379
+ if (!this.teammateId) return false;
380
+
381
+ const normalized = this.normalizePath(filePath);
382
+
383
+ // Check in-memory claims first
384
+ const existing = this.claims.get(normalized);
385
+
386
+ // Check if already claimed by someone else in memory
387
+ if (existing && existing.teammateId !== this.teammateId) {
388
+ // Check if expired
389
+ if (existing.expiresAt && existing.expiresAt < Date.now()) {
390
+ this.claims.delete(normalized);
391
+ this.deleteClaimFile(filePath);
392
+ } else {
393
+ return false; // Claimed by another
394
+ }
395
+ }
396
+
397
+ // Also check on disk in case another process claimed it
398
+ const diskClaim = this.checkClaimOnDisk(filePath);
399
+ if (diskClaim) {
400
+ // Update our memory with the disk claim
401
+ this.claims.set(normalized, diskClaim);
402
+ return false; // Claimed by another on disk
403
+ }
404
+
405
+ const claim: FileClaim = {
406
+ filePath,
407
+ teammateId: this.teammateId,
408
+ teammateName: this.getTeammateName(),
409
+ claimedAt: Date.now(),
410
+ expiresAt: expiresIn ? Date.now() + expiresIn : undefined,
411
+ reason,
412
+ };
413
+
414
+ this.claims.set(normalized, claim);
415
+
416
+ // Persist claim
417
+ const claimPath = this.getClaimFilePath(filePath);
418
+ writeFileSync(claimPath, JSON.stringify(claim, null, 2));
419
+
420
+ this.emitEvent({
421
+ type: "file_claim",
422
+ teammateId: this.teammateId,
423
+ teammateName: this.getTeammateName(),
424
+ teamName: this.teamName || "",
425
+ timestamp: Date.now(),
426
+ payload: { claim },
427
+ });
428
+
429
+ return true;
430
+ }
431
+
432
+ /**
433
+ * Release a file claim
434
+ */
435
+ releaseFile(filePath: string): void {
436
+ if (!this.teammateId) return;
437
+
438
+ const normalized = this.normalizePath(filePath);
439
+ const claim = this.claims.get(normalized);
440
+
441
+ if (claim && claim.teammateId === this.teammateId) {
442
+ this.claims.delete(normalized);
443
+ this.deleteClaimFile(filePath);
444
+
445
+ this.emitEvent({
446
+ type: "file_release",
447
+ teammateId: this.teammateId,
448
+ teammateName: this.getTeammateName(),
449
+ teamName: this.teamName || "",
450
+ timestamp: Date.now(),
451
+ payload: { filePath },
452
+ });
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Release all file claims for this teammate
458
+ */
459
+ releaseAllClaims(): void {
460
+ if (!this.teammateId) return;
461
+
462
+ const myClaims = Array.from(this.claims.entries())
463
+ .filter(([_, claim]) => claim.teammateId === this.teammateId);
464
+
465
+ for (const [normalized, _] of myClaims) {
466
+ this.claims.delete(normalized);
467
+ }
468
+
469
+ // Clean up claim files on disk
470
+ try {
471
+ const files = readdirSync(this.claimsPath).filter(f => f.endsWith(".json"));
472
+
473
+ for (const file of files) {
474
+ try {
475
+ const content = readFileSync(join(this.claimsPath, file), "utf-8");
476
+ const claim = JSON.parse(content) as FileClaim;
477
+ if (claim.teammateId === this.teammateId) {
478
+ rmSync(join(this.claimsPath, file));
479
+ }
480
+ } catch {
481
+ // Skip
482
+ }
483
+ }
484
+ } catch {
485
+ // Directory may not exist
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Check if a file is claimed by another teammate
491
+ */
492
+ isFileClaimed(filePath: string): boolean {
493
+ const normalized = this.normalizePath(filePath);
494
+
495
+ // First check in-memory claims
496
+ const claim = this.claims.get(normalized);
497
+
498
+ if (claim) {
499
+ // Check if expired
500
+ if (claim.expiresAt && claim.expiresAt < Date.now()) {
501
+ this.claims.delete(normalized);
502
+ this.deleteClaimFile(filePath);
503
+ return false;
504
+ }
505
+
506
+ // Return true only if claimed by someone else
507
+ if (claim.teammateId !== this.teammateId) {
508
+ return true;
509
+ }
510
+ }
511
+
512
+ // Also check disk for claims by other processes
513
+ const diskClaim = this.checkClaimOnDisk(filePath);
514
+ if (diskClaim) {
515
+ // Update memory with disk claim
516
+ this.claims.set(normalized, diskClaim);
517
+ return true;
518
+ }
519
+
520
+ return false;
521
+ }
522
+
523
+ /**
524
+ * Get the claim on a file (if any)
525
+ */
526
+ getFileClaim(filePath: string): FileClaim | undefined {
527
+ const normalized = this.normalizePath(filePath);
528
+ const claim = this.claims.get(normalized);
529
+
530
+ if (!claim) return undefined;
531
+
532
+ // Check if expired
533
+ if (claim.expiresAt && claim.expiresAt < Date.now()) {
534
+ this.claims.delete(normalized);
535
+ this.deleteClaimFile(filePath);
536
+ return undefined;
537
+ }
538
+
539
+ return claim;
540
+ }
541
+
542
+ /**
543
+ * Get all files claimed by a specific teammate
544
+ */
545
+ getTeammateClaims(teammateId: string): FileClaim[] {
546
+ return Array.from(this.claims.values())
547
+ .filter(claim => claim.teammateId === teammateId);
548
+ }
549
+
550
+ /**
551
+ * Get all active claims
552
+ */
553
+ getAllClaims(): FileClaim[] {
554
+ return Array.from(this.claims.values());
555
+ }
556
+
557
+ // ============================================
558
+ // EVENT EMISSION
559
+ // ============================================
560
+
561
+ private emitEvent(event: CoordinationEvent): void {
562
+ if (this.config.onCoordinationEvent) {
563
+ this.config.onCoordinationEvent(event);
564
+ }
565
+
566
+ // Emit specific callbacks
567
+ switch (event.type) {
568
+ case "status_change":
569
+ if (this.config.onStatusChange) {
570
+ const { teammateId, oldStatus, newStatus } = event.payload as {
571
+ teammateId: string;
572
+ oldStatus: TeammateStatus;
573
+ newStatus: TeammateStatus;
574
+ };
575
+ this.config.onStatusChange(teammateId, oldStatus, newStatus);
576
+ }
577
+ break;
578
+
579
+ case "task_progress":
580
+ if (this.config.onProgress) {
581
+ const { progress } = event.payload as { progress: ProgressReport };
582
+ this.config.onProgress(event.teammateId, progress);
583
+ }
584
+ break;
585
+
586
+ case "file_claim":
587
+ if (this.config.onFileClaimed) {
588
+ const { claim } = event.payload as { claim: FileClaim };
589
+ this.config.onFileClaimed(claim);
590
+ }
591
+ break;
592
+
593
+ case "file_release":
594
+ if (this.config.onFileReleased) {
595
+ const { filePath } = event.payload as { filePath: string };
596
+ this.config.onFileReleased(filePath, event.teammateId);
597
+ }
598
+ break;
599
+ }
600
+ }
601
+
602
+ // ============================================
603
+ // HELPERS
604
+ // ============================================
605
+
606
+ private getTeammateName(): string {
607
+ if (!this.teammateId) return "unknown";
608
+ const teammate = this.manager.getTeammate(this.teammateId);
609
+ return teammate?.name || this.teammateId;
610
+ }
611
+ }
612
+
613
+ // ============================================
614
+ // COORDINATION HELPER FUNCTIONS
615
+ // ============================================
616
+
617
+ /**
618
+ * Create a coordination-aware message
619
+ */
620
+ export function createCoordinationMessage(
621
+ type: CoordinationEventType,
622
+ content: string
623
+ ): string {
624
+ return `[COORD:${type.toUpperCase()}] ${content}`;
625
+ }
626
+
627
+ /**
628
+ * Parse a coordination message
629
+ */
630
+ export function parseCoordinationMessage(message: string): {
631
+ isCoordination: boolean;
632
+ type?: CoordinationEventType;
633
+ content?: string;
634
+ } {
635
+ const match = message.match(/^\[COORD:([A-Z_]+)\]\s*(.*)$/);
636
+ if (!match) {
637
+ return { isCoordination: false };
638
+ }
639
+
640
+ const typeStr = match[1]!.toLowerCase() as CoordinationEventType;
641
+ return {
642
+ isCoordination: true,
643
+ type: typeStr,
644
+ content: match[2]!,
645
+ };
646
+ }