buildcrew 1.9.0 → 1.9.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.
package/bin/setup.js CHANGED
@@ -649,6 +649,97 @@ async function runWatch() {
649
649
  });
650
650
  }
651
651
 
652
+ async function runReport() {
653
+ // Show coherence-report.md output by the coherence-auditor agent.
654
+ // Usage:
655
+ // npx buildcrew report Show latest coherence-report
656
+ // npx buildcrew report --list List all reports with timestamps
657
+ // npx buildcrew report <feature> Show specific feature's report
658
+ // npx buildcrew report --raw Print raw markdown (for piping)
659
+ const args = process.argv.slice(3);
660
+ const wantList = args.includes("--list") || args.includes("-l");
661
+ const wantRaw = args.includes("--raw");
662
+ const featureArg = args.find(a => !a.startsWith("-"));
663
+
664
+ const PIPELINE_DIR = join(process.cwd(), ".claude", "pipeline");
665
+ if (!(await exists(PIPELINE_DIR))) {
666
+ log(`${YELLOW}No pipeline runs found yet.${RESET}`);
667
+ log(`${DIM}Run ${BOLD}@buildcrew <feature>${RESET}${DIM} in Claude Code to generate one.${RESET}\n`);
668
+ return;
669
+ }
670
+
671
+ // Collect all coherence-report.md files and their mtimes
672
+ const features = await readdir(PIPELINE_DIR, { withFileTypes: true });
673
+ const reports = [];
674
+ const { stat } = await import("fs/promises");
675
+ for (const entry of features) {
676
+ if (!entry.isDirectory()) continue;
677
+ const reportPath = join(PIPELINE_DIR, entry.name, "coherence-report.md");
678
+ if (await exists(reportPath)) {
679
+ const s = await stat(reportPath);
680
+ reports.push({ feature: entry.name, path: reportPath, mtime: s.mtime });
681
+ }
682
+ }
683
+
684
+ if (reports.length === 0) {
685
+ log(`${YELLOW}No coherence-report.md found in any pipeline run.${RESET}`);
686
+ log(`${DIM}coherence-auditor runs at the end of Feature mode. If you ran a feature recently and don't see a report, check your buildcrew version (need >= 1.9.0).${RESET}\n`);
687
+ return;
688
+ }
689
+
690
+ reports.sort((a, b) => b.mtime - a.mtime);
691
+
692
+ if (wantList) {
693
+ log(`\n ${BOLD}Coherence reports${RESET} ${DIM}(newest first)${RESET}\n`);
694
+ for (const r of reports) {
695
+ const ago = ((Date.now() - r.mtime) / 1000 / 60) | 0;
696
+ const when = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${(ago/60)|0}h ago` : `${(ago/1440)|0}d ago`;
697
+ log(` ${CYAN}${r.feature.padEnd(30)}${RESET} ${DIM}${when.padStart(8)}${RESET} ${DIM}${r.path}${RESET}`);
698
+ }
699
+ log(`\n ${DIM}Show one: ${BOLD}npx buildcrew report ${CYAN}<feature>${RESET}\n`);
700
+ return;
701
+ }
702
+
703
+ // Pick target report
704
+ let target;
705
+ if (featureArg) {
706
+ target = reports.find(r => r.feature === featureArg);
707
+ if (!target) {
708
+ log(`${RED}No coherence-report for feature "${featureArg}".${RESET}`);
709
+ log(`${DIM}List all: ${BOLD}npx buildcrew report --list${RESET}\n`);
710
+ process.exit(1);
711
+ }
712
+ } else {
713
+ target = reports[0]; // latest
714
+ }
715
+
716
+ const content = await readFile(target.path, "utf8");
717
+
718
+ if (wantRaw) {
719
+ process.stdout.write(content);
720
+ return;
721
+ }
722
+
723
+ // Pretty header + content. If TTY supports it, try less for paging.
724
+ const header = `${BOLD}${CYAN}═══ ${target.feature} ═══${RESET} ${DIM}${target.path}${RESET}\n\n`;
725
+
726
+ if (process.stdout.isTTY && content.split("\n").length > process.stdout.rows) {
727
+ // Try paging through `less -R` (preserves ANSI). Fallback to direct print.
728
+ try {
729
+ const { spawn } = await import("child_process");
730
+ const less = spawn("less", ["-R", "-X"], { stdio: ["pipe", "inherit", "inherit"] });
731
+ less.stdin.write(header + content);
732
+ less.stdin.end();
733
+ await new Promise((resolve) => less.on("exit", resolve));
734
+ return;
735
+ } catch {
736
+ // fall through to direct print
737
+ }
738
+ }
739
+
740
+ process.stdout.write(header + content + "\n");
741
+ }
742
+
652
743
  async function runUninstall() {
653
744
  const files = (await readdir(AGENTS_SRC)).filter(f => f.endsWith(".md"));
654
745
  if (!(await exists(TARGET_DIR))) { log(`${YELLOW}No agents found.${RESET}`); return; }
@@ -681,6 +772,8 @@ async function main() {
681
772
  npx buildcrew add <name> Add a harness template
682
773
  npx buildcrew harness Show harness file status
683
774
  npx buildcrew watch Live terminal monitor (stays in your shell)
775
+ npx buildcrew report Show latest coherence-report (team coordination score)
776
+ npx buildcrew report --list List all coherence reports
684
777
 
685
778
  ${BOLD}Options:${RESET}
686
779
  --force, -f Overwrite existing files
@@ -700,12 +793,14 @@ async function main() {
700
793
  return;
701
794
  }
702
795
 
703
- if (args.includes("--list") || args.includes("-l")) return runList();
704
- if (args.includes("--uninstall")) return runUninstall();
796
+ // Subcommand routing takes priority over global --list (so `report --list` works)
705
797
  if (command === "init") return runInit(force);
706
798
  if (command === "add") return runAdd(subcommand, force);
707
799
  if (command === "harness") return runHarnessStatus();
708
800
  if (command === "watch") return runWatch();
801
+ if (command === "report") return runReport();
802
+ if (args.includes("--list") || args.includes("-l")) return runList();
803
+ if (args.includes("--uninstall")) return runUninstall();
709
804
 
710
805
  return runInstall(force);
711
806
  }
package/bin/watch.js CHANGED
@@ -11,9 +11,13 @@
11
11
  * Exit with q or Ctrl-C.
12
12
  */
13
13
 
14
- import { createReadStream, watchFile, statSync, existsSync, mkdirSync, closeSync, openSync } from "node:fs";
15
- import { join } from "node:path";
14
+ import { createReadStream, watchFile, statSync, existsSync, mkdirSync, closeSync, openSync, readdirSync, readFileSync } from "node:fs";
15
+ import { join, resolve, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
16
17
  import readline, { createInterface } from "node:readline";
18
+ import { spawnSync } from "node:child_process";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
21
 
18
22
  const EVENTS_PATH = process.env.BUILDCREW_EVENTS_PATH
19
23
  ?? join(process.cwd(), ".claude", "buildcrew", "events.jsonl");
@@ -88,8 +92,54 @@ const state = {
88
92
  sessionId: null,
89
93
  sessionStartAt: null,
90
94
  sessionEndAt: null,
95
+ // Coherence: loaded from .claude/pipeline/*/coherence-report.md after coherence-auditor runs
96
+ coherence: null, // { score, status, feature, gaps, fabrications, edgesActual, edgesPossible, path, ts }
91
97
  };
92
98
 
99
+ // ------------------------------------------------------------------
100
+ // Coherence report loader — reads .claude/pipeline/{feature}/coherence-report.md
101
+ // Triggered on agent.completed(coherence-auditor) or file.written(*/coherence-report.md)
102
+ // ------------------------------------------------------------------
103
+ function loadLatestCoherence() {
104
+ try {
105
+ const pipelineDir = join(process.cwd(), ".claude", "pipeline");
106
+ if (!existsSync(pipelineDir)) return;
107
+ const features = readdirSync(pipelineDir, { withFileTypes: true }).filter(d => d.isDirectory());
108
+ let newest = null;
109
+ for (const f of features) {
110
+ const p = join(pipelineDir, f.name, "coherence-report.md");
111
+ if (!existsSync(p)) continue;
112
+ const s = statSync(p);
113
+ if (!newest || s.mtimeMs > newest.mtime) {
114
+ newest = { path: p, feature: f.name, mtime: s.mtimeMs };
115
+ }
116
+ }
117
+ if (!newest) return;
118
+ const content = readFileSync(newest.path, "utf8");
119
+ // Tolerant parsing — coherence-auditor writes Korean or English.
120
+ const score = parseInt(content.match(/Coordination Score\*?\*?:?\s*\*?\*?(\d+)\s*%/)?.[1] ?? "", 10);
121
+ const edges = content.match(/\((\d+)\s*\/\s*(\d+)\s+edges?\)/);
122
+ const status = content.match(/Status:\s*([A-Za-z]+)/)?.[1] ?? "";
123
+ const fabrications = parseInt(content.match(/Fabrications?:\s*\*?\*?(\d+)/)?.[1] ?? "0", 10);
124
+ // Gap count from "## Gaps (N)" heading
125
+ const gaps = parseInt(content.match(/##\s*Gaps?\s*\((\d+)\)/)?.[1] ?? "0", 10);
126
+ state.coherence = {
127
+ score: Number.isFinite(score) ? score : null,
128
+ status,
129
+ feature: newest.feature,
130
+ gaps,
131
+ fabrications,
132
+ edgesActual: edges ? parseInt(edges[1], 10) : null,
133
+ edgesPossible: edges ? parseInt(edges[2], 10) : null,
134
+ path: newest.path,
135
+ ts: newest.mtime,
136
+ };
137
+ scheduleRender();
138
+ } catch {
139
+ // Swallow — coherence is best-effort, never crashes the watch
140
+ }
141
+ }
142
+
93
143
  function handleEvent(ev) {
94
144
  state.events += 1;
95
145
  const at = Date.parse(ev.at) || Date.now();
@@ -130,10 +180,18 @@ function handleEvent(ev) {
130
180
  };
131
181
  if (ev.agent) closeAgent(ev.agent, at);
132
182
  else if (ev.sweep) for (const id of [...state.activeAgents.keys()]) closeAgent(id, at);
183
+ // Coherence: when coherence-auditor finishes, reload the latest report
184
+ if (ev.agent === "coherence-auditor") {
185
+ loadLatestCoherence();
186
+ }
133
187
  break;
134
188
  }
135
189
  case "file.written":
136
190
  state.files += 1;
191
+ // Coherence: if a coherence-report.md was just written, reload
192
+ if (ev.path && ev.path.endsWith("/coherence-report.md")) {
193
+ loadLatestCoherence();
194
+ }
137
195
  if (ev.path) {
138
196
  state.recentFiles.push({ path: ev.path, tool: ev.tool_name, agent: ev.agent, at });
139
197
  if (state.recentFiles.length > 6) state.recentFiles.shift();
@@ -299,6 +357,40 @@ function renderIssues(width) {
299
357
  console.log("");
300
358
  }
301
359
 
360
+ function renderCoherence(width) {
361
+ if (!state.coherence) return;
362
+ const co = state.coherence;
363
+ console.log(sectionTitle("COHERENCE", width));
364
+ // Score color: 90+ green, 70-89 cyan, 50-69 gold, <50 red
365
+ let scoreColor = c.gray, statusEmoji = "○";
366
+ if (co.score == null) {
367
+ scoreColor = c.gray;
368
+ statusEmoji = "?";
369
+ } else if (co.score >= 90) {
370
+ scoreColor = c.green; statusEmoji = "✓";
371
+ } else if (co.score >= 70) {
372
+ scoreColor = c.cyan; statusEmoji = "●";
373
+ } else if (co.score >= 50) {
374
+ scoreColor = c.gold; statusEmoji = "⚠";
375
+ } else {
376
+ scoreColor = c.red; statusEmoji = "✗";
377
+ }
378
+ const scoreStr = co.score == null ? `${c.gray}—${c.reset}` : `${scoreColor}${c.bold}${co.score}%${c.reset}`;
379
+ const statusStr = co.status ? `${scoreColor}${co.status}${c.reset}` : `${c.gray}—${c.reset}`;
380
+ const edgesStr = (co.edgesActual != null && co.edgesPossible != null)
381
+ ? `${c.gray}(${co.edgesActual}/${co.edgesPossible} edges)${c.reset}`
382
+ : "";
383
+ const fabBadge = co.fabrications > 0
384
+ ? ` ${c.red}🚨 ${co.fabrications} fabrication${co.fabrications > 1 ? "s" : ""}${c.reset}`
385
+ : "";
386
+ const gapBadge = co.gaps > 0
387
+ ? ` ${c.gold}⚠ ${co.gaps} gap${co.gaps > 1 ? "s" : ""}${c.reset}`
388
+ : ` ${c.gray}no gaps${c.reset}`;
389
+ console.log(` ${statusEmoji} ${scoreStr} ${statusStr} ${edgesStr}${gapBadge}${fabBadge}`);
390
+ console.log(` ${c.gray}feature ${c.cyan}${co.feature}${c.reset} ${c.gray}· press ${c.bold}r${c.reset}${c.gray} for full report${c.reset}`);
391
+ console.log("");
392
+ }
393
+
302
394
  function renderRecent(width) {
303
395
  console.log(sectionTitle("LOG", width));
304
396
  if (state.recent.length === 0) {
@@ -354,8 +446,9 @@ function render() {
354
446
  renderAgents(width);
355
447
  renderFiles(width);
356
448
  renderIssues(width);
449
+ renderCoherence(width);
357
450
  renderRecent(width);
358
- process.stdout.write(`\n${c.gray}q / Ctrl-C to exit${c.reset}\n`);
451
+ process.stdout.write(`\n${c.gray}q quit · r show full coherence report${c.reset}\n`);
359
452
  }
360
453
 
361
454
  // ------------------------------------------------------------------
@@ -381,6 +474,13 @@ async function replayExisting() {
381
474
  tailOffset = 0;
382
475
  return;
383
476
  }
477
+ // Empty events.jsonl (first run / fresh install) — nothing to replay.
478
+ // createReadStream with end: -1 would throw RangeError.
479
+ if (tailOffset === 0) {
480
+ state.connected = true;
481
+ scheduleRender();
482
+ return;
483
+ }
384
484
  const stream = createReadStream(EVENTS_PATH, { encoding: "utf8", end: tailOffset - 1 });
385
485
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
386
486
  for await (const line of rl) {
@@ -425,7 +525,7 @@ process.on("exit", () => process.stdout.write(SHOW_CURSOR));
425
525
  process.on("SIGINT", () => { process.stdout.write(SHOW_CURSOR); process.exit(0); });
426
526
  process.on("SIGTERM", () => { process.stdout.write(SHOW_CURSOR); process.exit(0); });
427
527
 
428
- // Allow 'q' to quit
528
+ // Keypress handlers: q/Ctrl-C quit, r open full coherence report
429
529
  if (process.stdin.isTTY) {
430
530
  readline.emitKeypressEvents(process.stdin);
431
531
  process.stdin.setRawMode(true);
@@ -434,6 +534,24 @@ if (process.stdin.isTTY) {
434
534
  process.stdout.write(SHOW_CURSOR);
435
535
  process.exit(0);
436
536
  }
537
+ if (key?.name === "r") {
538
+ // Open the full coherence report. Hand off the terminal to setup.js's
539
+ // report subcommand which uses `less -R` for paging. Restore TTY state
540
+ // after the child exits.
541
+ process.stdout.write(SHOW_CURSOR);
542
+ process.stdin.setRawMode(false);
543
+ process.stdout.write(CLEAR);
544
+ const setupEntry = resolve(__dirname, "setup.js");
545
+ spawnSync(process.execPath, [setupEntry, "report"], {
546
+ stdio: "inherit",
547
+ cwd: process.cwd(),
548
+ env: process.env,
549
+ });
550
+ // Restore raw mode + hide cursor + redraw
551
+ process.stdin.setRawMode(true);
552
+ process.stdout.write(HIDE_CURSOR);
553
+ scheduleRender();
554
+ }
437
555
  });
438
556
  }
439
557
 
@@ -444,6 +562,9 @@ setInterval(scheduleRender, 1000);
444
562
  (async () => {
445
563
  await ensureEventsFile();
446
564
  await replayExisting();
565
+ // Surface the latest coherence report (if any) on startup, even before any
566
+ // new events fire — so users see their last score when they open watch.
567
+ loadLatestCoherence();
447
568
  subscribeTail();
448
569
  render();
449
570
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildcrew",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "15 AI agents for Claude Code — full development lifecycle from product thinking to production monitoring",
5
5
  "homepage": "https://buildcrew-landing.vercel.app",
6
6
  "author": "z1nun",