buildcrew 1.9.1 → 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/watch.js CHANGED
@@ -32,11 +32,23 @@ const c = NO_COLOR
32
32
  reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
33
33
  black: "\x1b[30m", red: "\x1b[31m", green: "\x1b[32m",
34
34
  gold: "\x1b[33m", blue: "\x1b[34m", mag: "\x1b[35m",
35
- cyan: "\x1b[36m", gray: "\x1b[90m",
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",
36
40
  bgWood: "\x1b[48;5;94m",
37
41
  };
38
42
 
39
- const CLEAR = "\x1b[2J\x1b[H";
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";
40
52
  const HIDE_CURSOR = "\x1b[?25l";
41
53
  const SHOW_CURSOR = "\x1b[?25h";
42
54
 
@@ -148,12 +160,37 @@ function handleEvent(ev) {
148
160
 
149
161
  switch (ev.type) {
150
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 = [];
151
176
  state.sessionStartAt = at;
152
177
  state.sessionEndAt = null;
153
178
  if (ev.session_id) state.sessionId = ev.session_id;
154
179
  break;
155
180
  case "session.end":
156
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
+ }
157
194
  break;
158
195
  case "agent.dispatched": {
159
196
  if (!ev.agent) break;
@@ -284,7 +321,7 @@ function renderNow(width) {
284
321
  const emoji = (AGENTS.find(a => a.id === id)?.emoji) ?? "●";
285
322
  const elapsed = formatDuration(Math.floor((now - info.startAt) / 1000));
286
323
  const prompt = truncate(info.prompt, Math.max(20, width - 28));
287
- console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.gray}${elapsed} · ${prompt}${c.reset}`);
324
+ console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.muted}${elapsed} ·${c.reset} ${prompt}`);
288
325
  }
289
326
  }
290
327
  console.log("");
@@ -411,7 +448,7 @@ function formatEvent(ev, maxLen) {
411
448
  let body;
412
449
  switch (ev.type) {
413
450
  case "agent.dispatched":
414
- body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.gray}· ${truncate(ev.prompt, 60)}${c.reset}`;
451
+ body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.muted}·${c.reset} ${truncate(ev.prompt, 60)}`;
415
452
  break;
416
453
  case "agent.completed":
417
454
  body = `${c.green}✓${c.reset} ${ev.agent ?? "*"} ${c.gray}done${c.reset}`;
@@ -439,16 +476,36 @@ function formatEvent(ev, maxLen) {
439
476
 
440
477
  function render() {
441
478
  const width = Math.max(60, process.stdout.columns ?? 80);
442
- process.stdout.write(CLEAR);
443
- renderHeader();
444
- renderNow(width);
445
- renderPipeline(width);
446
- renderAgents(width);
447
- renderFiles(width);
448
- renderIssues(width);
449
- renderCoherence(width);
450
- renderRecent(width);
451
- process.stdout.write(`\n${c.gray}q quit · r show full coherence report${c.reset}\n`);
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);
452
509
  }
453
510
 
454
511
  // ------------------------------------------------------------------
@@ -520,10 +577,13 @@ function subscribeTail() {
520
577
  // ------------------------------------------------------------------
521
578
  // Bootstrap
522
579
  // ------------------------------------------------------------------
523
- process.stdout.write(HIDE_CURSOR);
524
- process.on("exit", () => process.stdout.write(SHOW_CURSOR));
525
- process.on("SIGINT", () => { process.stdout.write(SHOW_CURSOR); process.exit(0); });
526
- process.on("SIGTERM", () => { process.stdout.write(SHOW_CURSOR); process.exit(0); });
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); });
527
587
 
528
588
  // Keypress handlers: q/Ctrl-C quit, r open full coherence report
529
589
  if (process.stdin.isTTY) {
@@ -536,20 +596,19 @@ if (process.stdin.isTTY) {
536
596
  }
537
597
  if (key?.name === "r") {
538
598
  // 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);
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);
542
602
  process.stdin.setRawMode(false);
543
- process.stdout.write(CLEAR);
544
603
  const setupEntry = resolve(__dirname, "setup.js");
545
604
  spawnSync(process.execPath, [setupEntry, "report"], {
546
605
  stdio: "inherit",
547
606
  cwd: process.cwd(),
548
607
  env: process.env,
549
608
  });
550
- // Restore raw mode + hide cursor + redraw
609
+ // Restore raw mode + alt screen + hide cursor + redraw
551
610
  process.stdin.setRawMode(true);
552
- process.stdout.write(HIDE_CURSOR);
611
+ process.stdout.write(ALT_SCREEN_ON + HIDE_CURSOR);
553
612
  scheduleRender();
554
613
  }
555
614
  });
@@ -2,10 +2,18 @@
2
2
  * buildcrew CC hook installer.
3
3
  *
4
4
  * Registers hook entries in .claude/settings.json that invoke
5
- * `npx buildcrew-hook <kind>` on each agent/file event. The hook writes
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
- const cmd = (kind) => `npx buildcrew-hook ${kind}`;
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.1",
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",