buildcrew 1.9.0 → 1.9.2
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 +201 -21
- package/lib/install-hooks.js +18 -2
- 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");
|
|
@@ -28,11 +32,23 @@ const c = NO_COLOR
|
|
|
28
32
|
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
29
33
|
black: "\x1b[30m", red: "\x1b[31m", green: "\x1b[32m",
|
|
30
34
|
gold: "\x1b[33m", blue: "\x1b[34m", mag: "\x1b[35m",
|
|
31
|
-
cyan: "\x1b[36m",
|
|
35
|
+
cyan: "\x1b[36m",
|
|
36
|
+
// Primary secondary text — readable on dark terminals (was \x1b[90m which rendered too dim)
|
|
37
|
+
gray: "\x1b[38;5;250m",
|
|
38
|
+
// Muted — for truly tertiary metadata (timestamps, separators)
|
|
39
|
+
muted: "\x1b[38;5;244m",
|
|
32
40
|
bgWood: "\x1b[48;5;94m",
|
|
33
41
|
};
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
// Anti-flicker rendering primitives.
|
|
44
|
+
// HOME moves cursor to top-left WITHOUT clearing — we overwrite in place and
|
|
45
|
+
// use CLR_EOL per line + CLR_BELOW at the end to erase leftovers. The old
|
|
46
|
+
// `\x1b[2J\x1b[H` caused a visible flash every frame (blank → redraw).
|
|
47
|
+
const HOME = "\x1b[H";
|
|
48
|
+
const CLR_EOL = "\x1b[K"; // clear from cursor to end of line
|
|
49
|
+
const CLR_BELOW = "\x1b[J"; // clear from cursor to end of screen
|
|
50
|
+
const ALT_SCREEN_ON = "\x1b[?1049h";
|
|
51
|
+
const ALT_SCREEN_OFF = "\x1b[?1049l";
|
|
36
52
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
37
53
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
38
54
|
|
|
@@ -88,8 +104,54 @@ const state = {
|
|
|
88
104
|
sessionId: null,
|
|
89
105
|
sessionStartAt: null,
|
|
90
106
|
sessionEndAt: null,
|
|
107
|
+
// Coherence: loaded from .claude/pipeline/*/coherence-report.md after coherence-auditor runs
|
|
108
|
+
coherence: null, // { score, status, feature, gaps, fabrications, edgesActual, edgesPossible, path, ts }
|
|
91
109
|
};
|
|
92
110
|
|
|
111
|
+
// ------------------------------------------------------------------
|
|
112
|
+
// Coherence report loader — reads .claude/pipeline/{feature}/coherence-report.md
|
|
113
|
+
// Triggered on agent.completed(coherence-auditor) or file.written(*/coherence-report.md)
|
|
114
|
+
// ------------------------------------------------------------------
|
|
115
|
+
function loadLatestCoherence() {
|
|
116
|
+
try {
|
|
117
|
+
const pipelineDir = join(process.cwd(), ".claude", "pipeline");
|
|
118
|
+
if (!existsSync(pipelineDir)) return;
|
|
119
|
+
const features = readdirSync(pipelineDir, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
120
|
+
let newest = null;
|
|
121
|
+
for (const f of features) {
|
|
122
|
+
const p = join(pipelineDir, f.name, "coherence-report.md");
|
|
123
|
+
if (!existsSync(p)) continue;
|
|
124
|
+
const s = statSync(p);
|
|
125
|
+
if (!newest || s.mtimeMs > newest.mtime) {
|
|
126
|
+
newest = { path: p, feature: f.name, mtime: s.mtimeMs };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!newest) return;
|
|
130
|
+
const content = readFileSync(newest.path, "utf8");
|
|
131
|
+
// Tolerant parsing — coherence-auditor writes Korean or English.
|
|
132
|
+
const score = parseInt(content.match(/Coordination Score\*?\*?:?\s*\*?\*?(\d+)\s*%/)?.[1] ?? "", 10);
|
|
133
|
+
const edges = content.match(/\((\d+)\s*\/\s*(\d+)\s+edges?\)/);
|
|
134
|
+
const status = content.match(/Status:\s*([A-Za-z]+)/)?.[1] ?? "";
|
|
135
|
+
const fabrications = parseInt(content.match(/Fabrications?:\s*\*?\*?(\d+)/)?.[1] ?? "0", 10);
|
|
136
|
+
// Gap count from "## Gaps (N)" heading
|
|
137
|
+
const gaps = parseInt(content.match(/##\s*Gaps?\s*\((\d+)\)/)?.[1] ?? "0", 10);
|
|
138
|
+
state.coherence = {
|
|
139
|
+
score: Number.isFinite(score) ? score : null,
|
|
140
|
+
status,
|
|
141
|
+
feature: newest.feature,
|
|
142
|
+
gaps,
|
|
143
|
+
fabrications,
|
|
144
|
+
edgesActual: edges ? parseInt(edges[1], 10) : null,
|
|
145
|
+
edgesPossible: edges ? parseInt(edges[2], 10) : null,
|
|
146
|
+
path: newest.path,
|
|
147
|
+
ts: newest.mtime,
|
|
148
|
+
};
|
|
149
|
+
scheduleRender();
|
|
150
|
+
} catch {
|
|
151
|
+
// Swallow — coherence is best-effort, never crashes the watch
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
function handleEvent(ev) {
|
|
94
156
|
state.events += 1;
|
|
95
157
|
const at = Date.parse(ev.at) || Date.now();
|
|
@@ -98,12 +160,37 @@ function handleEvent(ev) {
|
|
|
98
160
|
|
|
99
161
|
switch (ev.type) {
|
|
100
162
|
case "session.start":
|
|
163
|
+
// New session → clear per-session state. Watch is a live observer for the
|
|
164
|
+
// current session; persistent project progress belongs in docs/ (PDCA).
|
|
165
|
+
// Keep: coherence (file-derived), session metadata itself.
|
|
166
|
+
state.currentStage = null;
|
|
167
|
+
state.completedStages = new Set();
|
|
168
|
+
state.activeAgents = new Map();
|
|
169
|
+
state.completedAgents = new Map();
|
|
170
|
+
state.events = 0;
|
|
171
|
+
state.files = 0;
|
|
172
|
+
state.issues = { critical: 0, high: 0, med: 0, low: 0 };
|
|
173
|
+
state.recent = [];
|
|
174
|
+
state.recentFiles = [];
|
|
175
|
+
state.recentIssues = [];
|
|
101
176
|
state.sessionStartAt = at;
|
|
102
177
|
state.sessionEndAt = null;
|
|
103
178
|
if (ev.session_id) state.sessionId = ev.session_id;
|
|
104
179
|
break;
|
|
105
180
|
case "session.end":
|
|
106
181
|
state.sessionEndAt = at;
|
|
182
|
+
// Sweep any agents still marked active — completed events can be missed
|
|
183
|
+
// (e.g. @mentions in prompt text that hook logs as dispatched but never
|
|
184
|
+
// actually invoke the Agent tool). Session end implies nothing is running.
|
|
185
|
+
for (const id of [...state.activeAgents.keys()]) {
|
|
186
|
+
const a = state.activeAgents.get(id);
|
|
187
|
+
state.activeAgents.delete(id);
|
|
188
|
+
state.completedAgents.set(id, {
|
|
189
|
+
lastAt: at,
|
|
190
|
+
duration: Math.max(0, at - a.startAt),
|
|
191
|
+
summary: "",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
107
194
|
break;
|
|
108
195
|
case "agent.dispatched": {
|
|
109
196
|
if (!ev.agent) break;
|
|
@@ -130,10 +217,18 @@ function handleEvent(ev) {
|
|
|
130
217
|
};
|
|
131
218
|
if (ev.agent) closeAgent(ev.agent, at);
|
|
132
219
|
else if (ev.sweep) for (const id of [...state.activeAgents.keys()]) closeAgent(id, at);
|
|
220
|
+
// Coherence: when coherence-auditor finishes, reload the latest report
|
|
221
|
+
if (ev.agent === "coherence-auditor") {
|
|
222
|
+
loadLatestCoherence();
|
|
223
|
+
}
|
|
133
224
|
break;
|
|
134
225
|
}
|
|
135
226
|
case "file.written":
|
|
136
227
|
state.files += 1;
|
|
228
|
+
// Coherence: if a coherence-report.md was just written, reload
|
|
229
|
+
if (ev.path && ev.path.endsWith("/coherence-report.md")) {
|
|
230
|
+
loadLatestCoherence();
|
|
231
|
+
}
|
|
137
232
|
if (ev.path) {
|
|
138
233
|
state.recentFiles.push({ path: ev.path, tool: ev.tool_name, agent: ev.agent, at });
|
|
139
234
|
if (state.recentFiles.length > 6) state.recentFiles.shift();
|
|
@@ -226,7 +321,7 @@ function renderNow(width) {
|
|
|
226
321
|
const emoji = (AGENTS.find(a => a.id === id)?.emoji) ?? "●";
|
|
227
322
|
const elapsed = formatDuration(Math.floor((now - info.startAt) / 1000));
|
|
228
323
|
const prompt = truncate(info.prompt, Math.max(20, width - 28));
|
|
229
|
-
console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.
|
|
324
|
+
console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.muted}${elapsed} ·${c.reset} ${prompt}`);
|
|
230
325
|
}
|
|
231
326
|
}
|
|
232
327
|
console.log("");
|
|
@@ -299,6 +394,40 @@ function renderIssues(width) {
|
|
|
299
394
|
console.log("");
|
|
300
395
|
}
|
|
301
396
|
|
|
397
|
+
function renderCoherence(width) {
|
|
398
|
+
if (!state.coherence) return;
|
|
399
|
+
const co = state.coherence;
|
|
400
|
+
console.log(sectionTitle("COHERENCE", width));
|
|
401
|
+
// Score color: 90+ green, 70-89 cyan, 50-69 gold, <50 red
|
|
402
|
+
let scoreColor = c.gray, statusEmoji = "○";
|
|
403
|
+
if (co.score == null) {
|
|
404
|
+
scoreColor = c.gray;
|
|
405
|
+
statusEmoji = "?";
|
|
406
|
+
} else if (co.score >= 90) {
|
|
407
|
+
scoreColor = c.green; statusEmoji = "✓";
|
|
408
|
+
} else if (co.score >= 70) {
|
|
409
|
+
scoreColor = c.cyan; statusEmoji = "●";
|
|
410
|
+
} else if (co.score >= 50) {
|
|
411
|
+
scoreColor = c.gold; statusEmoji = "⚠";
|
|
412
|
+
} else {
|
|
413
|
+
scoreColor = c.red; statusEmoji = "✗";
|
|
414
|
+
}
|
|
415
|
+
const scoreStr = co.score == null ? `${c.gray}—${c.reset}` : `${scoreColor}${c.bold}${co.score}%${c.reset}`;
|
|
416
|
+
const statusStr = co.status ? `${scoreColor}${co.status}${c.reset}` : `${c.gray}—${c.reset}`;
|
|
417
|
+
const edgesStr = (co.edgesActual != null && co.edgesPossible != null)
|
|
418
|
+
? `${c.gray}(${co.edgesActual}/${co.edgesPossible} edges)${c.reset}`
|
|
419
|
+
: "";
|
|
420
|
+
const fabBadge = co.fabrications > 0
|
|
421
|
+
? ` ${c.red}🚨 ${co.fabrications} fabrication${co.fabrications > 1 ? "s" : ""}${c.reset}`
|
|
422
|
+
: "";
|
|
423
|
+
const gapBadge = co.gaps > 0
|
|
424
|
+
? ` ${c.gold}⚠ ${co.gaps} gap${co.gaps > 1 ? "s" : ""}${c.reset}`
|
|
425
|
+
: ` ${c.gray}no gaps${c.reset}`;
|
|
426
|
+
console.log(` ${statusEmoji} ${scoreStr} ${statusStr} ${edgesStr}${gapBadge}${fabBadge}`);
|
|
427
|
+
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}`);
|
|
428
|
+
console.log("");
|
|
429
|
+
}
|
|
430
|
+
|
|
302
431
|
function renderRecent(width) {
|
|
303
432
|
console.log(sectionTitle("LOG", width));
|
|
304
433
|
if (state.recent.length === 0) {
|
|
@@ -319,7 +448,7 @@ function formatEvent(ev, maxLen) {
|
|
|
319
448
|
let body;
|
|
320
449
|
switch (ev.type) {
|
|
321
450
|
case "agent.dispatched":
|
|
322
|
-
body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.
|
|
451
|
+
body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.muted}·${c.reset} ${truncate(ev.prompt, 60)}`;
|
|
323
452
|
break;
|
|
324
453
|
case "agent.completed":
|
|
325
454
|
body = `${c.green}✓${c.reset} ${ev.agent ?? "*"} ${c.gray}done${c.reset}`;
|
|
@@ -347,15 +476,36 @@ function formatEvent(ev, maxLen) {
|
|
|
347
476
|
|
|
348
477
|
function render() {
|
|
349
478
|
const width = Math.max(60, process.stdout.columns ?? 80);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
479
|
+
|
|
480
|
+
// Capture every render*() call's output into an in-memory buffer by
|
|
481
|
+
// monkey-patching console.log for the duration of the render. This lets us
|
|
482
|
+
// emit the whole frame in a single process.stdout.write — eliminating the
|
|
483
|
+
// per-line flicker that came from 30+ separate writes.
|
|
484
|
+
const lines = [];
|
|
485
|
+
const origLog = console.log;
|
|
486
|
+
console.log = (...args) => {
|
|
487
|
+
lines.push(args.length === 0 ? "" : args.map(String).join(" "));
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
renderHeader();
|
|
491
|
+
renderNow(width);
|
|
492
|
+
renderPipeline(width);
|
|
493
|
+
renderAgents(width);
|
|
494
|
+
renderFiles(width);
|
|
495
|
+
renderIssues(width);
|
|
496
|
+
renderCoherence(width);
|
|
497
|
+
renderRecent(width);
|
|
498
|
+
} finally {
|
|
499
|
+
console.log = origLog;
|
|
500
|
+
}
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(`${c.gray}q quit · r show full coherence report${c.reset}`);
|
|
503
|
+
|
|
504
|
+
// Single atomic frame: cursor home → each line + clear-to-EOL (erases any
|
|
505
|
+
// leftover chars from a previous longer line) → clear-below (handles frame
|
|
506
|
+
// shrinkage). No `\x1b[2J` flash.
|
|
507
|
+
const frame = HOME + lines.map(l => l + CLR_EOL).join("\n") + "\n" + CLR_BELOW;
|
|
508
|
+
process.stdout.write(frame);
|
|
359
509
|
}
|
|
360
510
|
|
|
361
511
|
// ------------------------------------------------------------------
|
|
@@ -381,6 +531,13 @@ async function replayExisting() {
|
|
|
381
531
|
tailOffset = 0;
|
|
382
532
|
return;
|
|
383
533
|
}
|
|
534
|
+
// Empty events.jsonl (first run / fresh install) — nothing to replay.
|
|
535
|
+
// createReadStream with end: -1 would throw RangeError.
|
|
536
|
+
if (tailOffset === 0) {
|
|
537
|
+
state.connected = true;
|
|
538
|
+
scheduleRender();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
384
541
|
const stream = createReadStream(EVENTS_PATH, { encoding: "utf8", end: tailOffset - 1 });
|
|
385
542
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
386
543
|
for await (const line of rl) {
|
|
@@ -420,12 +577,15 @@ function subscribeTail() {
|
|
|
420
577
|
// ------------------------------------------------------------------
|
|
421
578
|
// Bootstrap
|
|
422
579
|
// ------------------------------------------------------------------
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
process.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
580
|
+
// Enter alternate screen so the dashboard doesn't scribble over the user's
|
|
581
|
+
// scrollback. On exit we return the terminal to its pre-watch state.
|
|
582
|
+
process.stdout.write(ALT_SCREEN_ON + HIDE_CURSOR);
|
|
583
|
+
const restoreTerm = () => process.stdout.write(SHOW_CURSOR + ALT_SCREEN_OFF);
|
|
584
|
+
process.on("exit", restoreTerm);
|
|
585
|
+
process.on("SIGINT", () => { restoreTerm(); process.exit(0); });
|
|
586
|
+
process.on("SIGTERM", () => { restoreTerm(); process.exit(0); });
|
|
587
|
+
|
|
588
|
+
// Keypress handlers: q/Ctrl-C quit, r open full coherence report
|
|
429
589
|
if (process.stdin.isTTY) {
|
|
430
590
|
readline.emitKeypressEvents(process.stdin);
|
|
431
591
|
process.stdin.setRawMode(true);
|
|
@@ -434,6 +594,23 @@ if (process.stdin.isTTY) {
|
|
|
434
594
|
process.stdout.write(SHOW_CURSOR);
|
|
435
595
|
process.exit(0);
|
|
436
596
|
}
|
|
597
|
+
if (key?.name === "r") {
|
|
598
|
+
// Open the full coherence report. Hand off the terminal to setup.js's
|
|
599
|
+
// report subcommand which uses `less -R` for paging. Leave the alt
|
|
600
|
+
// screen so less paints on the main buffer; re-enter on return.
|
|
601
|
+
process.stdout.write(SHOW_CURSOR + ALT_SCREEN_OFF);
|
|
602
|
+
process.stdin.setRawMode(false);
|
|
603
|
+
const setupEntry = resolve(__dirname, "setup.js");
|
|
604
|
+
spawnSync(process.execPath, [setupEntry, "report"], {
|
|
605
|
+
stdio: "inherit",
|
|
606
|
+
cwd: process.cwd(),
|
|
607
|
+
env: process.env,
|
|
608
|
+
});
|
|
609
|
+
// Restore raw mode + alt screen + hide cursor + redraw
|
|
610
|
+
process.stdin.setRawMode(true);
|
|
611
|
+
process.stdout.write(ALT_SCREEN_ON + HIDE_CURSOR);
|
|
612
|
+
scheduleRender();
|
|
613
|
+
}
|
|
437
614
|
});
|
|
438
615
|
}
|
|
439
616
|
|
|
@@ -444,6 +621,9 @@ setInterval(scheduleRender, 1000);
|
|
|
444
621
|
(async () => {
|
|
445
622
|
await ensureEventsFile();
|
|
446
623
|
await replayExisting();
|
|
624
|
+
// Surface the latest coherence report (if any) on startup, even before any
|
|
625
|
+
// new events fire — so users see their last score when they open watch.
|
|
626
|
+
loadLatestCoherence();
|
|
447
627
|
subscribeTail();
|
|
448
628
|
render();
|
|
449
629
|
})();
|
package/lib/install-hooks.js
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
* buildcrew CC hook installer.
|
|
3
3
|
*
|
|
4
4
|
* Registers hook entries in .claude/settings.json that invoke
|
|
5
|
-
* `
|
|
5
|
+
* `node <abs-path>/lib/hook.js <kind>` on each agent/file event. The hook writes
|
|
6
6
|
* a styled banner to the terminal AND appends to events.jsonl so that
|
|
7
7
|
* `npx buildcrew watch` can show a live view in a separate pane.
|
|
8
8
|
*
|
|
9
|
+
* We resolve an absolute path to the installed buildcrew package's hook.js at
|
|
10
|
+
* install time rather than using `npx buildcrew-hook` because:
|
|
11
|
+
* - Bare `npx buildcrew-hook` looks up a package literally named
|
|
12
|
+
* "buildcrew-hook" → E404 (it's a bin inside the `buildcrew` package).
|
|
13
|
+
* - `npx -p buildcrew buildcrew-hook` works but re-fetches on cache miss and
|
|
14
|
+
* adds 200-500ms latency per CC hook invocation.
|
|
15
|
+
* - Absolute node path is zero-overhead and immune to npx cache eviction.
|
|
16
|
+
*
|
|
9
17
|
* Idempotent — re-install replaces prior buildcrew entries without
|
|
10
18
|
* touching other hooks or permissions in the file.
|
|
11
19
|
*/
|
|
@@ -13,9 +21,15 @@
|
|
|
13
21
|
import { promises as fsp } from "node:fs";
|
|
14
22
|
import path from "node:path";
|
|
15
23
|
import os from "node:os";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
16
25
|
|
|
17
26
|
const BUILDCREW_TAG = "buildcrew-hook";
|
|
18
27
|
|
|
28
|
+
// Absolute path to this package's hook script — resolved at import time so the
|
|
29
|
+
// generated settings.json entries are self-contained (no PATH lookups needed).
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const HOOK_SCRIPT = path.resolve(__dirname, "hook.js");
|
|
32
|
+
|
|
19
33
|
export function resolveSettingsPath({ scope, cwd }) {
|
|
20
34
|
if (scope === "global") return path.join(os.homedir(), ".claude", "settings.json");
|
|
21
35
|
return path.join(cwd, ".claude", "settings.json");
|
|
@@ -27,7 +41,9 @@ export function resolvePermissionsPath({ scope, cwd }) {
|
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
export function buildcrewHooks() {
|
|
30
|
-
|
|
44
|
+
// Shell-escape the path in case the install location contains spaces or
|
|
45
|
+
// non-ASCII characters (e.g. Korean path segments on macOS).
|
|
46
|
+
const cmd = (kind) => `node "${HOOK_SCRIPT}" ${kind}`;
|
|
31
47
|
const mk = (kind, matcher) => ({
|
|
32
48
|
[BUILDCREW_TAG]: true,
|
|
33
49
|
...(matcher ? { matcher } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buildcrew",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
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",
|