claude-comms 1.0.46 → 2.0.1

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 (54) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +12 -13
  3. package/package.json +6 -10
  4. package/.agents/AGENTS.md +0 -61
  5. package/.agents/repo.md +0 -18
  6. package/.agents/tools/README.md +0 -45
  7. package/.agents/tools/agent-job/README.md +0 -208
  8. package/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
  9. package/.agents/tools/agent-job/agent_job.py +0 -936
  10. package/.agents/tools/agent-job/agent_monitor.py +0 -575
  11. package/.agents/tools/agent-job/consultant_template.txt +0 -73
  12. package/.agents/tools/chrome-devtools/README.md +0 -101
  13. package/.agents/tools/chrome-devtools/browsertools.py +0 -839
  14. package/.agents/tools/chrome-devtools/config.json +0 -24
  15. package/.agents/tools/workflow/CONSUMER_SETUP.md +0 -114
  16. package/.agents/tools/workflow/ONBOARD.md +0 -167
  17. package/.agents/tools/workflow/README.md +0 -169
  18. package/.agents/tools/workflow/cli.ts +0 -726
  19. package/.agents/tools/workflow/config.example.json +0 -16
  20. package/.agents/tools/workflow/init.ts +0 -76
  21. package/.agents/tools/workflow/lib/file-tracker.test.ts +0 -81
  22. package/.agents/tools/workflow/lib/file-tracker.ts +0 -387
  23. package/.agents/tools/workflow/lib/harness-executor.ts +0 -649
  24. package/.agents/tools/workflow/lib/prompts.scenario.test.ts +0 -262
  25. package/.agents/tools/workflow/lib/prompts.ts +0 -548
  26. package/.agents/tools/workflow/lib/streams.test.ts +0 -140
  27. package/.agents/tools/workflow/lib/streams.ts +0 -254
  28. package/.agents/tools/workflow/runner.ts +0 -994
  29. package/.agents/tools/workflow/templates/document.md +0 -42
  30. package/.agents/tools/workflow/templates/implement.md +0 -117
  31. package/.agents/tools/workflow/templates/plan.md +0 -54
  32. package/.agents/tools/workflow/templates/pm.md +0 -84
  33. package/.agents/tools/workflow/templates/product-owner.md +0 -211
  34. package/.agents/tools/workflow/templates/review.md +0 -56
  35. package/.agents/tools/workflow/templates/uat.md +0 -71
  36. package/.claude/agents/core/engineer.md +0 -146
  37. package/.claude/commands/action-feedback-item.md +0 -27
  38. package/.claude/commands/install-feedback-widget.md +0 -599
  39. package/.claude/commands/learn.md +0 -32
  40. package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
  41. package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
  42. package/.claude/hooks/comms/get_unread_messages.py +0 -71
  43. package/.claude/hooks/comms/register_subagent.py +0 -74
  44. package/.claude/hooks/comms/send_message.py +0 -87
  45. package/.claude/hooks/comms/update_subagent_completion.py +0 -126
  46. package/.claude/hooks/context/session_id.py +0 -35
  47. package/.claude/hooks/observability/send_event.py +0 -110
  48. package/.claude/hooks/session-data/get_session_data.py +0 -261
  49. package/.claude/hooks/session-data/intercept_session_id.py +0 -69
  50. package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
  51. package/.claude/hooks/utils/server_config.py +0 -17
  52. package/.claude/settings.json +0 -108
  53. package/.claude/settings.local.json +0 -46
  54. package/CLAUDE.md +0 -240
@@ -1,649 +0,0 @@
1
- /**
2
- * Harness Executor
3
- *
4
- * Encapsulates the execution of AI harness processes (Claude, Codex, Gemini)
5
- * with file-based event streaming for crash resilience.
6
- *
7
- * Key features:
8
- * - Process stdout writes directly to log file (independent of runner)
9
- * - LogTailer watches file for real-time event processing
10
- * - Orphan detection and reconciliation on startup
11
- * - Clean separation from runner orchestration logic
12
- */
13
-
14
- import { spawn, ChildProcess } from "child_process";
15
- import {
16
- watch,
17
- FSWatcher,
18
- openSync,
19
- closeSync,
20
- createReadStream,
21
- statSync,
22
- existsSync,
23
- writeFileSync,
24
- } from "fs";
25
- import { createInterface } from "readline";
26
- import { EventEmitter } from "events";
27
-
28
- import {
29
- JobTracker,
30
- FileJobStatus,
31
- JobPaths,
32
- ensureJobDir,
33
- findOrphanedJobs,
34
- isPidAlive,
35
- utcNowIso,
36
- writeJobStatus,
37
- } from "./file-tracker.js";
38
-
39
- import {
40
- StreamHandler,
41
- createStreamHandler,
42
- buildCommand,
43
- CommandOptions,
44
- } from "./streams.js";
45
-
46
- // ============================================================================
47
- // Types
48
- // ============================================================================
49
-
50
- export type Harness = "claude" | "codex" | "gemini";
51
-
52
- export interface ExecutionCallbacks {
53
- /** Called when job completes successfully */
54
- onComplete: (result: string, sessionId?: string) => void;
55
- /** Called when job fails */
56
- onFail: (reason: string, partialResult?: string) => void;
57
- /** Called when job times out */
58
- onTimeout: (partialResult: string) => void;
59
- /** Optional: called for each event (for custom handling) */
60
- onEvent?: (event: Record<string, unknown>) => void;
61
- }
62
-
63
- export interface ExecutionHandle {
64
- jobId: string;
65
- pid: number;
66
- /** Kill the process */
67
- kill: () => void;
68
- /** Get current tracker for status inspection */
69
- getTracker: () => JobTracker;
70
- }
71
-
72
- export interface ExecutorConfig {
73
- /** Timeout in milliseconds before killing the process */
74
- timeoutMs: number;
75
- /** Working directory for spawned processes (defaults to process.cwd()) */
76
- cwd?: string;
77
- /** Polling interval for file watcher fallback (ms) */
78
- pollIntervalMs?: number;
79
- /** Debounce time for file change events (ms) */
80
- debounceMs?: number;
81
- }
82
-
83
- export interface ExecuteOptions {
84
- jobId: string;
85
- harness: Harness;
86
- prompt: string;
87
- /** Session ID for Claude resume */
88
- sessionId?: string;
89
- /** Additional environment variables */
90
- env?: Record<string, string>;
91
- }
92
-
93
- export interface ReconciliationResult {
94
- jobId: string;
95
- finalStatus: "complete" | "error" | "timeout";
96
- result?: string;
97
- sessionId?: string;
98
- }
99
-
100
- // ============================================================================
101
- // LogTailer - Watches log file and processes new lines
102
- // ============================================================================
103
-
104
- /**
105
- * Tails a log file and emits events for each new JSON line.
106
- * Handles file watching with fallback polling for reliability.
107
- */
108
- export class LogTailer extends EventEmitter {
109
- private logPath: string;
110
- private position: number;
111
- private watcher: FSWatcher | null = null;
112
- private pollTimer: NodeJS.Timeout | null = null;
113
- private debounceTimer: NodeJS.Timeout | null = null;
114
- private processing = false;
115
- private stopped = false;
116
- private lineBuffer = "";
117
-
118
- private pollIntervalMs: number;
119
- private debounceMs: number;
120
-
121
- constructor(
122
- logPath: string,
123
- startPosition: number = 0,
124
- options: { pollIntervalMs?: number; debounceMs?: number } = {}
125
- ) {
126
- super();
127
- this.logPath = logPath;
128
- this.position = startPosition;
129
- this.pollIntervalMs = options.pollIntervalMs ?? 500;
130
- this.debounceMs = options.debounceMs ?? 50;
131
- }
132
-
133
- /**
134
- * Start watching the log file
135
- */
136
- start(): void {
137
- if (this.stopped) return;
138
-
139
- // Process any existing content first
140
- this.processNewContent();
141
-
142
- // Set up file watcher
143
- try {
144
- this.watcher = watch(this.logPath, (eventType) => {
145
- if (eventType === "change") {
146
- this.scheduleProcess();
147
- }
148
- });
149
-
150
- this.watcher.on("error", (err) => {
151
- console.error(`[LogTailer] Watch error for ${this.logPath}:`, err);
152
- // Fall back to polling
153
- this.startPolling();
154
- });
155
- } catch (err) {
156
- console.error(`[LogTailer] Failed to watch ${this.logPath}:`, err);
157
- this.startPolling();
158
- }
159
-
160
- // Also poll as a fallback (some systems have flaky fs.watch)
161
- this.startPolling();
162
- }
163
-
164
- /**
165
- * Stop watching
166
- */
167
- stop(): void {
168
- this.stopped = true;
169
-
170
- if (this.watcher) {
171
- this.watcher.close();
172
- this.watcher = null;
173
- }
174
-
175
- if (this.pollTimer) {
176
- clearInterval(this.pollTimer);
177
- this.pollTimer = null;
178
- }
179
-
180
- if (this.debounceTimer) {
181
- clearTimeout(this.debounceTimer);
182
- this.debounceTimer = null;
183
- }
184
- }
185
-
186
- /**
187
- * Get current read position
188
- */
189
- getPosition(): number {
190
- return this.position;
191
- }
192
-
193
- /**
194
- * Process all remaining content (call before stopping for final read)
195
- */
196
- async flush(): Promise<void> {
197
- await this.processNewContent();
198
- }
199
-
200
- private startPolling(): void {
201
- if (this.pollTimer || this.stopped) return;
202
-
203
- this.pollTimer = setInterval(() => {
204
- this.scheduleProcess();
205
- }, this.pollIntervalMs);
206
- }
207
-
208
- private scheduleProcess(): void {
209
- if (this.stopped) return;
210
-
211
- // Debounce rapid changes
212
- if (this.debounceTimer) {
213
- clearTimeout(this.debounceTimer);
214
- }
215
-
216
- this.debounceTimer = setTimeout(() => {
217
- this.processNewContent();
218
- }, this.debounceMs);
219
- }
220
-
221
- private async processNewContent(): Promise<void> {
222
- if (this.processing || this.stopped) return;
223
- this.processing = true;
224
-
225
- try {
226
- if (!existsSync(this.logPath)) {
227
- this.processing = false;
228
- return;
229
- }
230
-
231
- const stats = statSync(this.logPath);
232
- const fileSize = stats.size;
233
-
234
- // Handle file truncation (e.g., log rotation)
235
- if (fileSize < this.position) {
236
- console.log(`[LogTailer] File truncated, resetting position from ${this.position} to 0`);
237
- this.position = 0;
238
- this.lineBuffer = "";
239
- }
240
-
241
- if (fileSize <= this.position) {
242
- this.processing = false;
243
- return;
244
- }
245
-
246
- // Read new content from current position
247
- const stream = createReadStream(this.logPath, {
248
- start: this.position,
249
- end: fileSize - 1, // Read up to current size (exclusive end)
250
- encoding: "utf-8",
251
- });
252
-
253
- // Collect all new content
254
- let newContent = this.lineBuffer;
255
- for await (const chunk of stream) {
256
- newContent += chunk;
257
- }
258
-
259
- // Split into lines, keeping incomplete last line in buffer
260
- const lines = newContent.split("\n");
261
-
262
- // If content doesn't end with newline, last element is incomplete
263
- if (!newContent.endsWith("\n")) {
264
- this.lineBuffer = lines.pop() || "";
265
- } else {
266
- this.lineBuffer = "";
267
- // Remove empty string from trailing newline
268
- if (lines.length > 0 && lines[lines.length - 1] === "") {
269
- lines.pop();
270
- }
271
- }
272
-
273
- // Process complete lines
274
- for (const line of lines) {
275
- if (!line.trim()) continue;
276
-
277
- try {
278
- const event = JSON.parse(line);
279
- this.emit("event", event, line);
280
- } catch {
281
- // Not valid JSON, emit as raw line
282
- this.emit("line", line);
283
- }
284
- }
285
-
286
- // Update position to file size (we've read everything up to this point)
287
- // Subtract buffered incomplete line bytes
288
- const bufferedBytes = Buffer.byteLength(this.lineBuffer, "utf-8");
289
- this.position = fileSize - bufferedBytes;
290
- this.emit("position", this.position);
291
- } catch (err) {
292
- console.error(`[LogTailer] Error processing ${this.logPath}:`, err);
293
- } finally {
294
- this.processing = false;
295
- }
296
- }
297
- }
298
-
299
- // ============================================================================
300
- // HarnessExecutor - Main executor class
301
- // ============================================================================
302
-
303
- /**
304
- * Executes harness processes with file-based event streaming.
305
- */
306
- export class HarnessExecutor {
307
- private config: ExecutorConfig;
308
- private activeJobs = new Map<
309
- string,
310
- {
311
- child: ChildProcess;
312
- tailer: LogTailer;
313
- tracker: JobTracker;
314
- handler: StreamHandler;
315
- timeout: NodeJS.Timeout;
316
- }
317
- >();
318
-
319
- constructor(config: ExecutorConfig) {
320
- this.config = {
321
- pollIntervalMs: 500,
322
- debounceMs: 50,
323
- ...config,
324
- };
325
- }
326
-
327
- /**
328
- * Execute a harness job with file-based event streaming
329
- */
330
- execute(options: ExecuteOptions, callbacks: ExecutionCallbacks): ExecutionHandle {
331
- const { jobId, harness, prompt, sessionId, env } = options;
332
-
333
- // 1. Create job directory and ensure log file exists
334
- const paths = ensureJobDir(jobId);
335
- writeFileSync(paths.logPath, ""); // Ensure empty file exists
336
-
337
- // 2. Open file descriptor for child stdout
338
- const logFd = openSync(paths.logPath, "a");
339
-
340
- // 3. Build command
341
- const commandOptions: CommandOptions = {};
342
- if (sessionId && harness === "claude") {
343
- commandOptions.sessionId = sessionId;
344
- }
345
- const { cmd, args } = buildCommand(harness, prompt, commandOptions);
346
-
347
- // 4. Spawn child with stdout going directly to file
348
- const child = spawn(cmd, args, {
349
- stdio: ["ignore", logFd, "pipe"], // stdout to file, stderr to pipe
350
- env: { ...process.env, ...env },
351
- cwd: this.config.cwd,
352
- });
353
-
354
- // Close our copy of the fd (child inherited it)
355
- closeSync(logFd);
356
-
357
- const pid = child.pid || 0;
358
-
359
- // 5. Create tracker (this also writes initial status.json)
360
- const tracker = new JobTracker(jobId, harness, pid);
361
-
362
- // 6. Create stream handler
363
- const handler = createStreamHandler(harness);
364
-
365
- // 7. Create and start log tailer
366
- const tailer = new LogTailer(paths.logPath, 0, {
367
- pollIntervalMs: this.config.pollIntervalMs,
368
- debounceMs: this.config.debounceMs,
369
- });
370
-
371
- // Wire up event handling
372
- tailer.on("event", (event: Record<string, unknown>, _rawLine: string) => {
373
- handler.onEvent(event);
374
- const eventType = (event.type as string) || "event";
375
- tracker.recordEvent(eventType);
376
-
377
- // Call optional user callback
378
- callbacks.onEvent?.(event);
379
- });
380
-
381
- tailer.on("position", (pos: number) => {
382
- tracker.setReadPosition(pos);
383
- });
384
-
385
- tailer.start();
386
-
387
- // 8. Set up timeout
388
- let timedOut = false;
389
- let spawnFailed = false;
390
- const timeout = setTimeout(() => {
391
- timedOut = true;
392
- console.log(`[${jobId}] Timeout after ${this.config.timeoutMs}ms`);
393
- tracker.timeout();
394
- child.kill("SIGTERM");
395
- setTimeout(() => child.kill("SIGKILL"), 5000);
396
- }, this.config.timeoutMs);
397
-
398
- // 9. Handle spawn errors (command not found, permission denied, etc.)
399
- child.on("error", (err) => {
400
- spawnFailed = true;
401
- clearTimeout(timeout);
402
- tailer.stop();
403
- this.activeJobs.delete(jobId);
404
-
405
- const reason = `spawn_error: ${err.message}`;
406
- console.error(`[${jobId}] Spawn failed: ${err.message}`);
407
- tracker.fail(reason);
408
- callbacks.onFail(reason, undefined);
409
- });
410
-
411
- // 10. Handle stderr
412
- child.stderr?.on("data", (data: Buffer) => {
413
- console.error(`[${jobId}] stderr: ${data.toString()}`);
414
- });
415
-
416
- // 12. Handle process exit
417
- child.on("close", async (code) => {
418
- // If spawn failed, error handler already cleaned up
419
- if (spawnFailed) return;
420
-
421
- clearTimeout(timeout);
422
-
423
- // Final flush to capture any remaining events
424
- await tailer.flush();
425
- tailer.stop();
426
-
427
- this.activeJobs.delete(jobId);
428
-
429
- const result = handler.getResult();
430
- console.log(`[${jobId}] Exited with code ${code}`);
431
-
432
- if (timedOut) {
433
- callbacks.onTimeout(result || "(no output)");
434
- } else if (code === 0 && handler.isComplete()) {
435
- tracker.complete(result);
436
- callbacks.onComplete(result, handler.getSessionId() || undefined);
437
- } else {
438
- const failureReason = handler.getFailureReason();
439
- const reason = failureReason
440
- ? `process_exit_${code} (${failureReason})`
441
- : `process_exit_${code}`;
442
- tracker.fail(reason);
443
- callbacks.onFail(reason, result);
444
- }
445
- });
446
-
447
- // Track active job
448
- this.activeJobs.set(jobId, { child, tailer, tracker, handler, timeout });
449
-
450
- return {
451
- jobId,
452
- pid,
453
- kill: () => {
454
- child.kill("SIGTERM");
455
- setTimeout(() => {
456
- if (!child.killed) child.kill("SIGKILL");
457
- }, 5000);
458
- },
459
- getTracker: () => tracker,
460
- };
461
- }
462
-
463
- /**
464
- * Reconcile orphaned jobs on startup.
465
- * Reads unprocessed events from log files and determines final status.
466
- */
467
- async reconcileOrphans(
468
- onReconciled: (result: ReconciliationResult) => Promise<void>
469
- ): Promise<void> {
470
- const orphans = findOrphanedJobs();
471
-
472
- if (orphans.length === 0) {
473
- console.log("[Reconcile] No orphaned jobs found");
474
- return;
475
- }
476
-
477
- console.log(`[Reconcile] Found ${orphans.length} orphaned jobs`);
478
-
479
- for (const { jobId, status, paths } of orphans) {
480
- console.log(`[Reconcile] Processing orphan: ${jobId}`);
481
-
482
- try {
483
- const result = await this.reconcileOrphan(jobId, status, paths);
484
- await onReconciled(result);
485
- } catch (err) {
486
- console.error(`[Reconcile] Error processing ${jobId}:`, err);
487
-
488
- // Mark as error
489
- status.status = "error";
490
- status.status_reason = `reconciliation_error: ${err}`;
491
- status.end_time = utcNowIso();
492
- writeJobStatus(paths.statusPath, status);
493
-
494
- await onReconciled({
495
- jobId,
496
- finalStatus: "error",
497
- result: `Reconciliation failed: ${err}`,
498
- });
499
- }
500
- }
501
- }
502
-
503
- /**
504
- * Process a single orphaned job
505
- */
506
- private async reconcileOrphan(
507
- jobId: string,
508
- status: FileJobStatus,
509
- paths: JobPaths
510
- ): Promise<ReconciliationResult> {
511
- // Check if process is somehow still alive (shouldn't be, but check anyway)
512
- if (isPidAlive(status.pid)) {
513
- console.log(`[Reconcile] ${jobId} still alive (PID ${status.pid}), killing`);
514
- try {
515
- process.kill(status.pid!, "SIGTERM");
516
- await new Promise((r) => setTimeout(r, 500));
517
- if (isPidAlive(status.pid)) {
518
- process.kill(status.pid!, "SIGKILL");
519
- }
520
- } catch {
521
- // Ignore kill errors
522
- }
523
- }
524
-
525
- // Create handler to process events
526
- const handler = createStreamHandler(status.harness);
527
-
528
- // Read log from last known position (or 0 if not set)
529
- const startPosition = status.read_position ?? 0;
530
-
531
- if (!existsSync(paths.logPath)) {
532
- // No log file - mark as error
533
- status.status = "error";
534
- status.status_reason = "orphaned_no_log";
535
- status.end_time = utcNowIso();
536
- writeJobStatus(paths.statusPath, status);
537
-
538
- return {
539
- jobId,
540
- finalStatus: "error",
541
- result: "Job orphaned with no log file",
542
- };
543
- }
544
-
545
- // Process remaining events in log
546
- const stream = createReadStream(paths.logPath, {
547
- start: startPosition,
548
- encoding: "utf-8",
549
- });
550
-
551
- const rl = createInterface({
552
- input: stream,
553
- crlfDelay: Infinity,
554
- });
555
-
556
- let eventsProcessed = 0;
557
-
558
- for await (const line of rl) {
559
- if (!line.trim()) continue;
560
-
561
- try {
562
- const event = JSON.parse(line);
563
- handler.onEvent(event);
564
- eventsProcessed++;
565
- } catch {
566
- // Not JSON, skip
567
- }
568
- }
569
-
570
- console.log(
571
- `[Reconcile] ${jobId}: processed ${eventsProcessed} events from position ${startPosition}`
572
- );
573
-
574
- // Determine final status
575
- const result = handler.getResult();
576
- const sessionId = handler.getSessionId();
577
-
578
- if (handler.isComplete()) {
579
- // Job actually completed successfully
580
- status.status = "complete";
581
- status.status_reason = "reconciled_complete";
582
- status.end_time = utcNowIso();
583
- status.completion.final_message = result;
584
- writeJobStatus(paths.statusPath, status);
585
-
586
- return {
587
- jobId,
588
- finalStatus: "complete",
589
- result,
590
- sessionId: sessionId || undefined,
591
- };
592
- } else {
593
- // Job was interrupted
594
- status.status = "error";
595
- status.status_reason = "orphaned_interrupted";
596
- status.end_time = utcNowIso();
597
- if (result) {
598
- status.completion.final_message = result;
599
- }
600
- writeJobStatus(paths.statusPath, status);
601
-
602
- return {
603
- jobId,
604
- finalStatus: "error",
605
- result: result || "Job orphaned without completion",
606
- };
607
- }
608
- }
609
-
610
- /**
611
- * Check if a job is currently being tracked
612
- */
613
- isTracking(jobId: string): boolean {
614
- return this.activeJobs.has(jobId);
615
- }
616
-
617
- /**
618
- * Get handle for an active job
619
- */
620
- getHandle(jobId: string): ExecutionHandle | null {
621
- const job = this.activeJobs.get(jobId);
622
- if (!job) return null;
623
-
624
- return {
625
- jobId,
626
- pid: job.child.pid || 0,
627
- kill: () => {
628
- job.child.kill("SIGTERM");
629
- setTimeout(() => {
630
- if (!job.child.killed) job.child.kill("SIGKILL");
631
- }, 5000);
632
- },
633
- getTracker: () => job.tracker,
634
- };
635
- }
636
-
637
- /**
638
- * Kill all active jobs (for shutdown)
639
- */
640
- killAll(): void {
641
- for (const [jobId, job] of this.activeJobs) {
642
- console.log(`[Executor] Killing job ${jobId}`);
643
- clearTimeout(job.timeout);
644
- job.tailer.stop();
645
- job.child.kill("SIGTERM");
646
- }
647
- this.activeJobs.clear();
648
- }
649
- }