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 +97 -2
- package/bin/watch.js +125 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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",
|