@braingrid/cli 0.2.33 → 0.2.35

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,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.35] - 2026-02-14
11
+
12
+ ### Added
13
+
14
+ - **Acceptance criteria extraction in `/build` skill**
15
+ - `/build` now parses the `## Acceptance Criteria` section from requirement content
16
+ - Extracts all criteria into a flat `[]` checklist at `.braingrid/temp/REQ-{id}-acceptance-criteria.md`
17
+ - Handles both flat and sub-sectioned (`###` headings) criteria formats
18
+ - Strips markdown bold formatting and collapses multi-line criteria
19
+ - Added `Write` to allowed tools in build command
20
+
21
+ ### Fixed
22
+
23
+ - **Add `create-braingrid-task.sh` to Claude Code sync script**
24
+ - The setup handler was fetching this hook from GitHub but the sync script never pushed it
25
+ - Without this fix, `braingrid setup claude-code` would fail to install the TaskCreate hook
26
+
27
+ ## [0.2.34] - 2026-02-05
28
+
29
+ ### Fixed
30
+
31
+ - **Deep merge `.claude/settings.json` during setup**
32
+ - `updateClaudeSettings()` now deep-merges statusLine and hooks instead of overwriting
33
+ - Preserves user customizations (extra pipes, additional hooks, other event types)
34
+ - Updates TaskUpdate hook in-place without duplicating entries
35
+ - Gracefully handles malformed hooks (non-object) and PostToolUse (non-array)
36
+ - Removed stale `| bunx cc-safety-net --statusline` pipe from default settings
37
+
10
38
  ## [0.2.33] - 2026-02-03
11
39
 
12
40
  ### Changed
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.33" : "0.0.0-test";
225
+ var CLI_VERSION = true ? "0.2.35" : "0.0.0-test";
226
226
  var PRODUCTION_CONFIG = {
227
227
  apiUrl: "https://app.braingrid.ai",
228
228
  workosAuthUrl: "https://auth.braingrid.ai",
@@ -2679,7 +2679,7 @@ 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") {
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") {
2683
2683
  try {
2684
2684
  let settings = {};
2685
2685
  try {
@@ -2687,25 +2687,89 @@ async function updateClaudeSettings(settingsPath = ".claude/settings.json", scri
2687
2687
  settings = JSON.parse(content2);
2688
2688
  } catch {
2689
2689
  }
2690
- settings.statusLine = {
2691
- type: "command",
2692
- command: scriptPath2,
2693
- padding: 0
2690
+ const existingStatusLine = settings.statusLine;
2691
+ if (existingStatusLine?.command?.includes(scriptPath2)) {
2692
+ } else {
2693
+ settings.statusLine = {
2694
+ type: "command",
2695
+ command: scriptPath2,
2696
+ padding: 0
2697
+ };
2698
+ }
2699
+ const ourHookEntry = {
2700
+ matcher: "TaskUpdate",
2701
+ hooks: [
2702
+ {
2703
+ type: "command",
2704
+ command: hookScriptPath,
2705
+ timeout: 1e4
2706
+ }
2707
+ ]
2694
2708
  };
2695
- settings.hooks = {
2696
- PostToolUse: [
2709
+ const existingHooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2710
+ const existingPostToolUse = Array.isArray(existingHooks.PostToolUse) ? existingHooks.PostToolUse : [];
2711
+ const taskUpdateIdx = existingPostToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2712
+ let mergedPostToolUse;
2713
+ if (taskUpdateIdx >= 0) {
2714
+ mergedPostToolUse = [...existingPostToolUse];
2715
+ mergedPostToolUse[taskUpdateIdx] = ourHookEntry;
2716
+ } else {
2717
+ mergedPostToolUse = [...existingPostToolUse, ourHookEntry];
2718
+ }
2719
+ const ourCreateHookEntry = {
2720
+ matcher: "TaskCreate",
2721
+ hooks: [
2697
2722
  {
2698
- matcher: "TaskUpdate",
2699
- hooks: [
2700
- {
2701
- type: "command",
2702
- command: hookScriptPath,
2703
- timeout: 1e4
2704
- }
2705
- ]
2723
+ type: "command",
2724
+ command: createHookScriptPath,
2725
+ timeout: 1e4
2726
+ }
2727
+ ]
2728
+ };
2729
+ const taskCreatePostIdx = mergedPostToolUse.findIndex((e) => e.matcher === "TaskCreate");
2730
+ if (taskCreatePostIdx >= 0) {
2731
+ mergedPostToolUse[taskCreatePostIdx] = ourCreateHookEntry;
2732
+ } else {
2733
+ mergedPostToolUse = [...mergedPostToolUse, ourCreateHookEntry];
2734
+ }
2735
+ const ourPreToolUseEntry = {
2736
+ matcher: "TaskCreate",
2737
+ hooks: [
2738
+ {
2739
+ type: "prompt",
2740
+ prompt: "Use sequential task numbering with conventional commit naming for task subjects. The subject MUST follow the format 'TASK N: type: description' where N is the next sequential number (1, 2, 3, ...) based on existing tasks in the current session. Valid types: feat, fix, docs, style, refactor, perf, test, chore. Optional scope in parentheses after type is allowed. Examples: 'TASK 1: feat: add user login', 'TASK 2: fix: resolve null pointer', 'TASK 3: feat(auth): add OAuth support'. Reject if the subject does not follow this convention. $ARGUMENTS"
2706
2741
  }
2707
2742
  ]
2708
2743
  };
2744
+ const existingPreToolUse = Array.isArray(existingHooks.PreToolUse) ? existingHooks.PreToolUse : [];
2745
+ const taskCreateIdx = existingPreToolUse.findIndex((e) => e.matcher === "TaskCreate");
2746
+ let mergedPreToolUse;
2747
+ if (taskCreateIdx >= 0) {
2748
+ mergedPreToolUse = [...existingPreToolUse];
2749
+ mergedPreToolUse[taskCreateIdx] = ourPreToolUseEntry;
2750
+ } else {
2751
+ mergedPreToolUse = [...existingPreToolUse, ourPreToolUseEntry];
2752
+ }
2753
+ const ourPreToolUseTaskUpdateEntry = {
2754
+ matcher: "TaskUpdate",
2755
+ hooks: [
2756
+ {
2757
+ type: "prompt",
2758
+ prompt: "Before completing a task, you MUST validate and commit. If status is being set to 'completed':\n\n1. Run the project's linter, test suite, and type checker (detect from project config \u2014 e.g. package.json, Makefile, pyproject.toml, Cargo.toml, etc.).\n2. If any check fails, DO NOT complete \u2014 fix first.\n3. Stage relevant files with git add (specific paths, not -A).\n4. Commit using the conventional commit part of the subject as the message (e.g. 'feat: add user login').\n5. Get the short hash: git rev-parse --short HEAD\n6. Update the task subject to: 'TASK N (HASH): type: description' (e.g. 'TASK 1 (abc1234): feat: add user login').\n7. Only then mark completed.\n\nFor other status changes, proceed normally.\n\n$ARGUMENTS"
2759
+ }
2760
+ ]
2761
+ };
2762
+ const taskUpdatePreIdx = mergedPreToolUse.findIndex((e) => e.matcher === "TaskUpdate");
2763
+ if (taskUpdatePreIdx >= 0) {
2764
+ mergedPreToolUse[taskUpdatePreIdx] = ourPreToolUseTaskUpdateEntry;
2765
+ } else {
2766
+ mergedPreToolUse = [...mergedPreToolUse, ourPreToolUseTaskUpdateEntry];
2767
+ }
2768
+ settings.hooks = {
2769
+ ...existingHooks,
2770
+ PreToolUse: mergedPreToolUse,
2771
+ PostToolUse: mergedPostToolUse
2772
+ };
2709
2773
  const parentDir = path2.dirname(settingsPath);
2710
2774
  await fs2.mkdir(parentDir, { recursive: true });
2711
2775
  const content = JSON.stringify(settings, null, " ");
@@ -3402,28 +3466,41 @@ async function _handleSetup(config2, opts) {
3402
3466
  );
3403
3467
  }
3404
3468
  }
3405
- let hookInstalled = false;
3469
+ let syncHookInstalled = false;
3470
+ let createHookInstalled = false;
3406
3471
  if (config2.name === "Claude Code") {
3407
3472
  try {
3408
- const hookContent = await fetchFileFromGitHub("claude-code/hooks/sync-braingrid-task.sh");
3409
- await installHookScript(hookContent);
3410
- hookInstalled = true;
3473
+ const syncContent = await fetchFileFromGitHub("claude-code/hooks/sync-braingrid-task.sh");
3474
+ await installHookScript(syncContent);
3475
+ syncHookInstalled = true;
3476
+ } catch (error) {
3477
+ console.error(
3478
+ chalk8.yellow("\u26A0\uFE0F Failed to install sync hook:"),
3479
+ error instanceof Error ? error.message : String(error)
3480
+ );
3481
+ }
3482
+ try {
3483
+ const createContent = await fetchFileFromGitHub(
3484
+ "claude-code/hooks/create-braingrid-task.sh"
3485
+ );
3486
+ await installHookScript(createContent, ".claude/hooks/create-braingrid-task.sh");
3487
+ createHookInstalled = true;
3411
3488
  } catch (error) {
3412
3489
  console.error(
3413
- chalk8.yellow("\u26A0\uFE0F Failed to install hook script:"),
3490
+ chalk8.yellow("\u26A0\uFE0F Failed to install create hook:"),
3414
3491
  error instanceof Error ? error.message : String(error)
3415
3492
  );
3416
3493
  }
3417
3494
  }
3418
3495
  await copyBraingridReadme();
3419
3496
  const statusLineMessage = statusLineInstalled ? chalk8.dim(" Status line: .claude/statusline.sh\n") : "";
3420
- const hookMessage = hookInstalled ? chalk8.dim(" Hook script: .claude/hooks/sync-braingrid-task.sh\n") : "";
3497
+ 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") : "");
3421
3498
  return {
3422
3499
  success: true,
3423
3500
  message: chalk8.green(`\u2705 ${config2.name} integration installed successfully!
3424
3501
 
3425
3502
  `) + chalk8.dim("Files installed:\n") + chalk8.dim(` Commands: ${result.installed} files
3426
- `) + statusLineMessage + hookMessage + chalk8.dim(` Content injected into: ${config2.injection.targetFile}
3503
+ `) + statusLineMessage + hooksMessage + chalk8.dim(` Content injected into: ${config2.injection.targetFile}
3427
3504
 
3428
3505
  `) + chalk8.dim("Next steps:\n") + chalk8.dim(" 1. Review the integration files\n") + chalk8.dim(` 2. Open ${config2.name}
3429
3506
  `) + chalk8.dim(" 3. Try the /specify or /breakdown commands\n") + chalk8.dim(" 4. Learn more: ") + chalk8.cyan(config2.docsUrl)