buildcrew 1.8.7 → 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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readdir, copyFile, mkdir, readFile, writeFile, access } from "fs/promises";
4
- import { join, dirname } from "path";
4
+ import { join, dirname, resolve } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -9,6 +9,7 @@ const AGENTS_SRC = join(__dirname, "..", "agents");
9
9
  const TEMPLATES_SRC = join(__dirname, "..", "templates");
10
10
  const TARGET_DIR = join(process.cwd(), ".claude", "agents");
11
11
  const HARNESS_DIR = join(process.cwd(), ".claude", "harness");
12
+ // (HOOK_SCRIPT path no longer needed — hooks use `npx buildcrew-hook` bin)
12
13
  const PKG = JSON.parse(await readFile(join(__dirname, "..", "package.json"), "utf-8"));
13
14
  const VERSION = PKG.version;
14
15
 
@@ -556,6 +557,39 @@ async function runInstall(force) {
556
557
  }
557
558
  }
558
559
 
560
+ // ─── Step 2d: CC hooks (agent lifecycle banners + events.jsonl) ───
561
+ try {
562
+ const hooksInstalled = await areHooksInstalled();
563
+ if (hooksInstalled) {
564
+ log(` ${GREEN}Hooks:${RESET} installed ✓\n`);
565
+ } else {
566
+ log(` ${YELLOW}Hooks${RESET} print a colored banner to your terminal whenever an`);
567
+ log(` agent starts/completes or a file is written. Also feeds ${BOLD}npx buildcrew watch${RESET}.`);
568
+ log(` ${DIM}Writes .claude/settings.json (+ recommended permissions)${RESET}\n`);
569
+ const answer = await ask(` Install hooks? ${BOLD}(Y/n)${RESET} `);
570
+ if (answer === "" || answer === "y" || answer === "yes") {
571
+ try {
572
+ const { install } = await import("../lib/install-hooks.js");
573
+ const result = await install({
574
+ scope: "project",
575
+ cwd: process.cwd(),
576
+ withPermissions: true,
577
+ });
578
+ log(`\n ${GREEN}${BOLD}Hooks installed!${RESET}`);
579
+ log(` ${DIM} hooks: ${result.settingsPath}${RESET}`);
580
+ if (result.permissions?.permPath) {
581
+ log(` ${DIM} permissions: ${result.permissions.permPath}${RESET}`);
582
+ }
583
+ log(`\n ${BOLD}Live monitor:${RESET} ${CYAN}npx buildcrew watch${RESET} ${DIM}(in a separate pane)${RESET}\n`);
584
+ } catch (err) {
585
+ log(`\n ${RED}Hook install failed:${RESET} ${err.message}\n`);
586
+ }
587
+ } else {
588
+ log(`\n ${DIM}Skipped. Hooks not installed.${RESET}\n`);
589
+ }
590
+ }
591
+ } catch { /* ignore, non-fatal */ }
592
+
559
593
  // ─── Step 3: Project harness ───
560
594
  if (!(await exists(join(HARNESS_DIR, "project.md")))) {
561
595
  const initAnswer = await ask(` Generate project harness? ${DIM}(auto-detects your stack)${RESET} ${BOLD}(Y/n)${RESET} `);
@@ -588,6 +622,124 @@ async function runList() {
588
622
  log("");
589
623
  }
590
624
 
625
+ async function areHooksInstalled() {
626
+ try {
627
+ const settingsPath = join(process.cwd(), ".claude", "settings.json");
628
+ const content = await readFile(settingsPath, "utf-8");
629
+ return content.includes("buildcrew-hook");
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+
635
+ async function runWatch() {
636
+ // Pass through to bin/watch.js — the terminal-native live monitor.
637
+ const watchEntry = resolve(__dirname, "watch.js");
638
+ const passthrough = process.argv.slice(3);
639
+ const { spawn } = await import("child_process");
640
+ const child = spawn(process.execPath, [watchEntry, ...passthrough], {
641
+ stdio: "inherit",
642
+ cwd: process.cwd(),
643
+ env: process.env,
644
+ });
645
+ child.on("exit", (code) => process.exit(code ?? 0));
646
+ child.on("error", (err) => {
647
+ console.error(`${RED}Watch failed to start:${RESET} ${err.message}`);
648
+ process.exit(1);
649
+ });
650
+ }
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
+
591
743
  async function runUninstall() {
592
744
  const files = (await readdir(AGENTS_SRC)).filter(f => f.endsWith(".md"));
593
745
  if (!(await exists(TARGET_DIR))) { log(`${YELLOW}No agents found.${RESET}`); return; }
@@ -614,11 +766,14 @@ async function main() {
614
766
  ${BOLD}buildcrew${RESET} v${VERSION} — 15 AI agents for Claude Code
615
767
 
616
768
  ${BOLD}Commands:${RESET}
617
- npx buildcrew Install agents
769
+ npx buildcrew Install agents (also offers hooks + harness)
618
770
  npx buildcrew init Auto-generate project harness (zero questions)
619
771
  npx buildcrew add List harness templates
620
772
  npx buildcrew add <name> Add a harness template
621
773
  npx buildcrew harness Show harness file status
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
622
777
 
623
778
  ${BOLD}Options:${RESET}
624
779
  --force, -f Overwrite existing files
@@ -627,21 +782,25 @@ async function main() {
627
782
  --version Show version
628
783
 
629
784
  ${BOLD}Setup:${RESET}
630
- ${GREEN}1.${RESET} npx buildcrew ${DIM}Install agents${RESET}
631
- ${GREEN}2.${RESET} npx buildcrew init ${DIM}Auto-generate harness from codebase${RESET}
785
+ ${GREEN}1.${RESET} npx buildcrew ${DIM}Install agents + optional hooks${RESET}
786
+ ${GREEN}2.${RESET} npx buildcrew init ${DIM}Auto-generate harness from codebase${RESET}
632
787
  ${GREEN}3.${RESET} Edit .claude/harness/ ${DIM}Customize (replace <!-- comments -->)${RESET}
633
- ${GREEN}4.${RESET} @buildcrew [task] ${DIM}Start working${RESET}
788
+ ${GREEN}4.${RESET} npx buildcrew watch ${DIM}(optional) live terminal monitor${RESET}
789
+ ${GREEN}5.${RESET} @buildcrew [task] ${DIM}Start working${RESET}
634
790
 
635
791
  ${BOLD}More info:${RESET} https://github.com/z1nun/buildcrew
636
792
  `);
637
793
  return;
638
794
  }
639
795
 
640
- if (args.includes("--list") || args.includes("-l")) return runList();
641
- if (args.includes("--uninstall")) return runUninstall();
796
+ // Subcommand routing takes priority over global --list (so `report --list` works)
642
797
  if (command === "init") return runInit(force);
643
798
  if (command === "add") return runAdd(subcommand, force);
644
799
  if (command === "harness") return runHarnessStatus();
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();
645
804
 
646
805
  return runInstall(force);
647
806
  }