@braingrid/cli 0.2.41 → 0.2.43

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/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.43] - 2026-02-17
11
+
12
+ ### Fixed
13
+
14
+ - **grep -c newline bug in verify-acceptance-criteria.sh** — `grep -c` exits non-zero on 0 matches, causing `|| echo "0"` to produce `"0\n0"` artifacts that break arithmetic expansion; replaced with `|| true` and `${VAR:-0}` default
15
+
16
+ ## [0.2.42] - 2026-02-17
17
+
18
+ ### Added
19
+
20
+ - **SessionStart and TaskCompleted hook installation** — `braingrid setup claude-code` now installs two additional hook scripts: `check-stale-build-sentinel.sh` (SessionStart) for cleaning up stale build sentinels, and `task-completed-validate.sh` (TaskCompleted) for commit validation before marking tasks complete
21
+ - **Sync script updated** — `sync-claude-code-to-braingrid.sh` now syncs 16 files (up from 14), including the two new hook scripts
22
+
10
23
  ## [0.2.41] - 2026-02-17
11
24
 
12
25
  ### Fixed
package/dist/cli.js CHANGED
@@ -222,7 +222,7 @@ async function axiosWithRetry(config2, options) {
222
222
 
223
223
  // src/build-config.ts
224
224
  var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
225
- var CLI_VERSION = true ? "0.2.41" : "0.0.0-test";
225
+ var CLI_VERSION = true ? "0.2.43" : "0.0.0-test";
226
226
  var PRODUCTION_CONFIG = {
227
227
  apiUrl: "https://app.braingrid.ai",
228
228
  workosAuthUrl: "https://auth.braingrid.ai",
@@ -2658,7 +2658,7 @@ async function installStatusLineScript(scriptContent, targetPath = ".claude/stat
2658
2658
  throw new Error(`Failed to install status line script to ${targetPath}: ${errorMessage}`);
2659
2659
  }
2660
2660
  }
2661
- async function installHookScript(scriptContent, targetPath = ".claude/hooks/sync-braingrid-task.sh") {
2661
+ async function installHookScript(scriptContent, targetPath) {
2662
2662
  try {
2663
2663
  const parentDir = path2.dirname(targetPath);
2664
2664
  await fs2.mkdir(parentDir, { recursive: true });
@@ -2679,7 +2679,64 @@ async function copyBraingridReadme(targetPath = ".braingrid/README.md") {
2679
2679
  return false;
2680
2680
  }
2681
2681
  }
2682
- async function updateClaudeSettings(settingsPath = ".claude/settings.json", scriptPath2 = ".claude/statusline.sh", hookScriptPath = ".claude/hooks/sync-braingrid-task.sh", createHookScriptPath = ".claude/hooks/create-braingrid-task.sh", verifyHookScriptPath = ".claude/hooks/verify-acceptance-criteria.sh", postTaskUpdatePromptPath = ".claude/hooks/post-task-update-prompt.sh", preTaskCreatePath = ".claude/hooks/pre-task-create-naming.sh", preTaskUpdatePath = ".claude/hooks/pre-task-update-instructions.sh") {
2682
+ var HOOK_REGISTRY = [
2683
+ {
2684
+ hookType: "PostToolUse",
2685
+ matcher: "TaskUpdate",
2686
+ mergeStrategy: "upsertByMatcher",
2687
+ commands: [
2688
+ { script: "sync-braingrid-task.sh", timeout: 1e4 },
2689
+ { script: "post-task-update-prompt.sh" }
2690
+ ]
2691
+ },
2692
+ {
2693
+ hookType: "PostToolUse",
2694
+ matcher: "TaskCreate",
2695
+ mergeStrategy: "upsertByMatcher",
2696
+ commands: [{ script: "create-braingrid-task.sh", timeout: 1e4 }]
2697
+ },
2698
+ {
2699
+ hookType: "PreToolUse",
2700
+ matcher: "TaskCreate",
2701
+ mergeStrategy: "upsertByMatcher",
2702
+ commands: [{ script: "pre-task-create-naming.sh" }]
2703
+ },
2704
+ {
2705
+ hookType: "PreToolUse",
2706
+ matcher: "TaskUpdate",
2707
+ mergeStrategy: "upsertByMatcher",
2708
+ commands: [{ script: "pre-task-update-instructions.sh" }]
2709
+ },
2710
+ {
2711
+ hookType: "Stop",
2712
+ mergeStrategy: "appendDedup",
2713
+ dedupSubstring: "verify-acceptance-criteria",
2714
+ commands: [{ script: "verify-acceptance-criteria.sh" }]
2715
+ },
2716
+ {
2717
+ hookType: "SessionStart",
2718
+ mergeStrategy: "appendDedup",
2719
+ dedupSubstring: "check-stale-build-sentinel",
2720
+ commands: [{ script: "check-stale-build-sentinel.sh" }]
2721
+ },
2722
+ {
2723
+ hookType: "TaskCompleted",
2724
+ mergeStrategy: "appendDedup",
2725
+ dedupSubstring: "task-completed-validate",
2726
+ commands: [{ script: "task-completed-validate.sh" }]
2727
+ }
2728
+ ];
2729
+ function buildHookCommand(cmd) {
2730
+ const result = {
2731
+ type: "command",
2732
+ command: `.claude/hooks/${cmd.script}`
2733
+ };
2734
+ if (cmd.timeout) {
2735
+ result.timeout = cmd.timeout;
2736
+ }
2737
+ return result;
2738
+ }
2739
+ async function updateClaudeSettings(settingsPath = ".claude/settings.json", statusLineScriptPath = ".claude/statusline.sh", hookRegistry = HOOK_REGISTRY) {
2683
2740
  try {
2684
2741
  let settings = {};
2685
2742
  try {
@@ -2688,105 +2745,46 @@ async function updateClaudeSettings(settingsPath = ".claude/settings.json", scri
2688
2745
  } catch {
2689
2746
  }
2690
2747
  const existingStatusLine = settings.statusLine;
2691
- if (existingStatusLine?.command?.includes(scriptPath2)) {
2748
+ if (existingStatusLine?.command?.includes(statusLineScriptPath)) {
2692
2749
  } else {
2693
2750
  settings.statusLine = {
2694
2751
  type: "command",
2695
- command: scriptPath2,
2752
+ command: statusLineScriptPath,
2696
2753
  padding: 0
2697
2754
  };
2698
2755
  }
2699
- const ourHookEntry = {
2700
- matcher: "TaskUpdate",
2701
- hooks: [
2702
- {
2703
- type: "command",
2704
- command: hookScriptPath,
2705
- timeout: 1e4
2706
- },
2707
- {
2708
- type: "command",
2709
- command: postTaskUpdatePromptPath
2710
- }
2711
- ]
2712
- };
2713
2756
  const existingHooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2714
- const existingPostToolUse = Array.isArray(existingHooks.PostToolUse) ? existingHooks.PostToolUse : [];
2715
- const taskUpdateIdx = existingPostToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2716
- let mergedPostToolUse;
2717
- if (taskUpdateIdx >= 0) {
2718
- mergedPostToolUse = [...existingPostToolUse];
2719
- mergedPostToolUse[taskUpdateIdx] = ourHookEntry;
2720
- } else {
2721
- mergedPostToolUse = [...existingPostToolUse, ourHookEntry];
2722
- }
2723
- const ourCreateHookEntry = {
2724
- matcher: "TaskCreate",
2725
- hooks: [
2726
- {
2727
- type: "command",
2728
- command: createHookScriptPath,
2729
- timeout: 1e4
2730
- }
2731
- ]
2732
- };
2733
- const taskCreatePostIdx = mergedPostToolUse.findIndex((e) => e.matcher === "TaskCreate");
2734
- if (taskCreatePostIdx >= 0) {
2735
- mergedPostToolUse[taskCreatePostIdx] = ourCreateHookEntry;
2736
- } else {
2737
- mergedPostToolUse = [...mergedPostToolUse, ourCreateHookEntry];
2738
- }
2739
- const ourPreToolUseEntry = {
2740
- matcher: "TaskCreate",
2741
- hooks: [
2742
- {
2743
- type: "command",
2744
- command: preTaskCreatePath
2757
+ const mergedByType = {};
2758
+ for (const entry of hookRegistry) {
2759
+ const hookType = entry.hookType;
2760
+ if (!mergedByType[hookType]) {
2761
+ const existing = Array.isArray(existingHooks[hookType]) ? [...existingHooks[hookType]] : [];
2762
+ mergedByType[hookType] = existing;
2763
+ }
2764
+ const merged = mergedByType[hookType];
2765
+ const hooks = entry.commands.map(buildHookCommand);
2766
+ if (entry.mergeStrategy === "upsertByMatcher") {
2767
+ const matcher = entry.matcher ?? "";
2768
+ const newEntry = { matcher, hooks };
2769
+ const idx = merged.findIndex((e) => e.matcher === matcher);
2770
+ if (idx >= 0) {
2771
+ merged[idx] = newEntry;
2772
+ } else {
2773
+ merged.push(newEntry);
2745
2774
  }
2746
- ]
2747
- };
2748
- const existingPreToolUse = Array.isArray(existingHooks.PreToolUse) ? existingHooks.PreToolUse : [];
2749
- const taskCreateIdx = existingPreToolUse.findIndex((e) => e.matcher === "TaskCreate");
2750
- let mergedPreToolUse;
2751
- if (taskCreateIdx >= 0) {
2752
- mergedPreToolUse = [...existingPreToolUse];
2753
- mergedPreToolUse[taskCreateIdx] = ourPreToolUseEntry;
2754
- } else {
2755
- mergedPreToolUse = [...existingPreToolUse, ourPreToolUseEntry];
2756
- }
2757
- const ourPreToolUseTaskUpdateEntry = {
2758
- matcher: "TaskUpdate",
2759
- hooks: [
2760
- {
2761
- type: "command",
2762
- command: preTaskUpdatePath
2775
+ } else {
2776
+ const substring = entry.dedupSubstring ?? "";
2777
+ const alreadyPresent = merged.some(
2778
+ (e) => e.hooks?.some((h) => h.command?.includes(substring))
2779
+ );
2780
+ if (!alreadyPresent) {
2781
+ merged.push({ hooks });
2763
2782
  }
2764
- ]
2765
- };
2766
- const taskUpdatePreIdx = mergedPreToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2767
- if (taskUpdatePreIdx >= 0) {
2768
- mergedPreToolUse[taskUpdatePreIdx] = ourPreToolUseTaskUpdateEntry;
2769
- } else {
2770
- mergedPreToolUse = [...mergedPreToolUse, ourPreToolUseTaskUpdateEntry];
2783
+ }
2771
2784
  }
2772
- const ourStopHookEntry = {
2773
- hooks: [
2774
- {
2775
- type: "command",
2776
- command: verifyHookScriptPath
2777
- }
2778
- ]
2779
- };
2780
- const existingStop = Array.isArray(existingHooks.Stop) ? existingHooks.Stop : [];
2781
- const hasVerifyHook = existingStop.some(
2782
- (entry) => entry.hooks?.some((h) => h.command?.includes("verify-acceptance-criteria"))
2783
- );
2784
- const mergedStop = hasVerifyHook ? existingStop : [...existingStop, ourStopHookEntry];
2785
2785
  settings.hooks = {
2786
2786
  ...existingHooks,
2787
- PreToolUse: mergedPreToolUse,
2788
- PostToolUse: mergedPostToolUse,
2789
- Stop: mergedStop
2787
+ ...mergedByType
2790
2788
  };
2791
2789
  const parentDir = path2.dirname(settingsPath);
2792
2790
  await fs2.mkdir(parentDir, { recursive: true });
@@ -3519,8 +3517,7 @@ function buildSuccessMessage(config2, installedPerDir, extras) {
3519
3517
 
3520
3518
  `) + chalk8.dim("Files installed:\n") + dirLines + extras + chalk8.dim(` Content injected into: ${config2.injection.targetFile}
3521
3519
 
3522
- `) + chalk8.dim("Next steps:\n") + chalk8.dim(" 1. Review the integration files\n") + chalk8.dim(` 2. Open ${config2.name}
3523
- `) + chalk8.dim(" 3. Try the /specify or /breakdown commands\n") + chalk8.dim(" 4. Learn more: ") + chalk8.cyan(config2.docsUrl);
3520
+ `) + chalk8.dim("Next, try:\n") + chalk8.dim(" /build REQ-X \u2192 build a requirement\n") + chalk8.dim(' /specify "add two-factor auth" \u2192 specify a requirement\n') + chalk8.dim(" Learn more: ") + chalk8.cyan(config2.docsUrl);
3524
3521
  }
3525
3522
  function isSetupResult(result) {
3526
3523
  return "data" in result && result.success === true && !("message" in result);
@@ -3568,85 +3565,23 @@ async function handleSetupClaudeCode(opts) {
3568
3565
  error instanceof Error ? error.message : String(error)
3569
3566
  );
3570
3567
  }
3571
- let syncHookInstalled = false;
3572
- try {
3573
- const syncContent = await fetchFileFromGitHub("claude-code/hooks/sync-braingrid-task.sh");
3574
- await installHookScript(syncContent);
3575
- syncHookInstalled = true;
3576
- } catch (error) {
3577
- console.error(
3578
- chalk8.yellow("\u26A0\uFE0F Failed to install sync hook:"),
3579
- error instanceof Error ? error.message : String(error)
3580
- );
3581
- }
3582
- let createHookInstalled = false;
3583
- try {
3584
- const createContent = await fetchFileFromGitHub("claude-code/hooks/create-braingrid-task.sh");
3585
- await installHookScript(createContent, ".claude/hooks/create-braingrid-task.sh");
3586
- createHookInstalled = true;
3587
- } catch (error) {
3588
- console.error(
3589
- chalk8.yellow("\u26A0\uFE0F Failed to install create hook:"),
3590
- error instanceof Error ? error.message : String(error)
3591
- );
3592
- }
3593
- let verifyHookInstalled = false;
3594
- try {
3595
- const verifyContent = await fetchFileFromGitHub(
3596
- "claude-code/hooks/verify-acceptance-criteria.sh"
3597
- );
3598
- await installHookScript(verifyContent, ".claude/hooks/verify-acceptance-criteria.sh");
3599
- verifyHookInstalled = true;
3600
- } catch (error) {
3601
- console.error(
3602
- chalk8.yellow("\u26A0\uFE0F Failed to install verify hook:"),
3603
- error instanceof Error ? error.message : String(error)
3604
- );
3605
- }
3606
- let preTaskCreateInstalled = false;
3607
- try {
3608
- const preTaskCreateContent = await fetchFileFromGitHub(
3609
- "claude-code/hooks/pre-task-create-naming.sh"
3610
- );
3611
- await installHookScript(preTaskCreateContent, ".claude/hooks/pre-task-create-naming.sh");
3612
- preTaskCreateInstalled = true;
3613
- } catch (error) {
3614
- console.error(
3615
- chalk8.yellow("\u26A0\uFE0F Failed to install pre-task-create hook:"),
3616
- error instanceof Error ? error.message : String(error)
3617
- );
3618
- }
3619
- let preTaskUpdateInstalled = false;
3620
- try {
3621
- const preTaskUpdateContent = await fetchFileFromGitHub(
3622
- "claude-code/hooks/pre-task-update-instructions.sh"
3623
- );
3624
- await installHookScript(
3625
- preTaskUpdateContent,
3626
- ".claude/hooks/pre-task-update-instructions.sh"
3627
- );
3628
- preTaskUpdateInstalled = true;
3629
- } catch (error) {
3630
- console.error(
3631
- chalk8.yellow("\u26A0\uFE0F Failed to install pre-task-update hook:"),
3632
- error instanceof Error ? error.message : String(error)
3633
- );
3634
- }
3635
- let postTaskUpdateInstalled = false;
3636
- try {
3637
- const postTaskUpdateContent = await fetchFileFromGitHub(
3638
- "claude-code/hooks/post-task-update-prompt.sh"
3639
- );
3640
- await installHookScript(postTaskUpdateContent, ".claude/hooks/post-task-update-prompt.sh");
3641
- postTaskUpdateInstalled = true;
3642
- } catch (error) {
3643
- console.error(
3644
- chalk8.yellow("\u26A0\uFE0F Failed to install post-task-update hook:"),
3645
- error instanceof Error ? error.message : String(error)
3646
- );
3568
+ const allScripts = HOOK_REGISTRY.flatMap((e) => e.commands.map((c) => c.script));
3569
+ const installedHooks = [];
3570
+ for (const script of allScripts) {
3571
+ try {
3572
+ const content = await fetchFileFromGitHub(`claude-code/hooks/${script}`);
3573
+ await installHookScript(content, `.claude/hooks/${script}`);
3574
+ installedHooks.push(script);
3575
+ } catch (error) {
3576
+ console.error(
3577
+ chalk8.yellow(`\u26A0\uFE0F Failed to install hook ${script}:`),
3578
+ error instanceof Error ? error.message : String(error)
3579
+ );
3580
+ }
3647
3581
  }
3648
3582
  const statusLineMessage = statusLineInstalled ? chalk8.dim(" Status line: .claude/statusline.sh\n") : "";
3649
- const hooksMessage = (syncHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/sync-braingrid-task.sh\n") : "") + (createHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/create-braingrid-task.sh\n") : "") + (verifyHookInstalled ? chalk8.dim(" Hook script: .claude/hooks/verify-acceptance-criteria.sh\n") : "") + (preTaskCreateInstalled ? chalk8.dim(" Hook script: .claude/hooks/pre-task-create-naming.sh\n") : "") + (preTaskUpdateInstalled ? chalk8.dim(" Hook script: .claude/hooks/pre-task-update-instructions.sh\n") : "") + (postTaskUpdateInstalled ? chalk8.dim(" Hook script: .claude/hooks/post-task-update-prompt.sh\n") : "");
3583
+ const hooksMessage = installedHooks.map((s) => chalk8.dim(` Hook script: .claude/hooks/${s}
3584
+ `)).join("");
3650
3585
  return {
3651
3586
  success: true,
3652
3587
  message: buildSuccessMessage(config2, displayPerDir, statusLineMessage + hooksMessage)