codebyplan 1.5.1 → 1.8.0

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.
Files changed (205) hide show
  1. package/dist/cli.js +4462 -748
  2. package/package.json +5 -1
  3. package/templates/.gitkeep +0 -0
  4. package/templates/README.md +20 -0
  5. package/templates/agents/cbp-cc-executor.md +213 -0
  6. package/templates/agents/cbp-database-agent.md +229 -0
  7. package/templates/agents/cbp-improve-claude.md +245 -0
  8. package/templates/agents/cbp-improve-round.md +284 -0
  9. package/templates/agents/cbp-mechanical-edits.md +111 -0
  10. package/templates/agents/cbp-research.md +282 -0
  11. package/templates/agents/cbp-round-executor.md +604 -0
  12. package/templates/agents/cbp-security-agent.md +134 -0
  13. package/templates/agents/cbp-task-check.md +213 -0
  14. package/templates/agents/cbp-task-planner.md +582 -0
  15. package/templates/agents/cbp-test-e2e-agent.md +363 -0
  16. package/templates/agents/cbp-testing-qa-agent.md +400 -0
  17. package/templates/context/mcp-docs.md +139 -0
  18. package/templates/hooks/README.md +236 -0
  19. package/templates/hooks/cbp-auto-test-hooks.sh +44 -0
  20. package/templates/hooks/cbp-lint-format-on-edit.sh +159 -0
  21. package/templates/hooks/cbp-maestro-yaml-validate.sh +100 -0
  22. package/templates/hooks/cbp-mcp-migration-guard.sh +32 -0
  23. package/templates/hooks/cbp-mcp-round-sync.sh +79 -0
  24. package/templates/hooks/cbp-mcp-worktree-inject.sh +76 -0
  25. package/templates/hooks/cbp-notify.sh +68 -0
  26. package/templates/hooks/cbp-plugin-dispatch.sh +29 -0
  27. package/templates/hooks/cbp-pre-commit-quality-gate.sh +204 -0
  28. package/templates/hooks/cbp-statusline.sh +347 -0
  29. package/templates/hooks/cbp-subagent-statusline.sh +182 -0
  30. package/templates/hooks/cbp-test-coverage-gate.sh +144 -0
  31. package/templates/hooks/cbp-test-hooks.sh +320 -0
  32. package/templates/hooks/hooks.json +85 -0
  33. package/templates/hooks/validate-context-usage.sh +59 -0
  34. package/templates/hooks/validate-git-commit.sh +78 -0
  35. package/templates/hooks/validate-git-stash-deny.sh +32 -0
  36. package/templates/hooks/validate-structure-lengths.sh +57 -0
  37. package/templates/hooks/validate-structure-lib.sh +104 -0
  38. package/templates/hooks/validate-structure-patterns.sh +54 -0
  39. package/templates/hooks/validate-structure-scope.sh +33 -0
  40. package/templates/hooks/validate-structure-smoke.sh +95 -0
  41. package/templates/hooks/validate-structure-templates.sh +34 -0
  42. package/templates/hooks/validate-structure.sh +69 -0
  43. package/templates/rules/.gitkeep +0 -0
  44. package/templates/rules/README.md +47 -0
  45. package/templates/rules/context-file-loading.md +52 -0
  46. package/templates/rules/scope-vocabulary.md +64 -0
  47. package/templates/rules/todo-backend.md +109 -0
  48. package/templates/settings.project.base.json +55 -0
  49. package/templates/settings.user.base.json +25 -0
  50. package/templates/skills/cbp-build-cc-agent/SKILL.md +139 -0
  51. package/templates/skills/cbp-build-cc-agent/examples/read-only-reviewer.md +32 -0
  52. package/templates/skills/cbp-build-cc-agent/examples/with-hooks.md +41 -0
  53. package/templates/skills/cbp-build-cc-agent/examples/with-skills-preload.md +25 -0
  54. package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +153 -0
  55. package/templates/skills/cbp-build-cc-agent/reference/frontmatter-fields.md +37 -0
  56. package/templates/skills/cbp-build-cc-agent/reference/permission-modes.md +18 -0
  57. package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +67 -0
  58. package/templates/skills/cbp-build-cc-agent/templates/agent.md +66 -0
  59. package/templates/skills/cbp-build-cc-claude-file/SKILL.md +178 -0
  60. package/templates/skills/cbp-build-cc-claude-file/examples/minimal-project.md +33 -0
  61. package/templates/skills/cbp-build-cc-claude-file/examples/monorepo-with-imports.md +39 -0
  62. package/templates/skills/cbp-build-cc-claude-file/reference/imports.md +72 -0
  63. package/templates/skills/cbp-build-cc-claude-file/reference/what-belongs.md +39 -0
  64. package/templates/skills/cbp-build-cc-claude-file/templates/project-claude-md.md +48 -0
  65. package/templates/skills/cbp-build-cc-claude-file/templates/user-claude-md.md +22 -0
  66. package/templates/skills/cbp-build-cc-memory/SKILL.md +201 -0
  67. package/templates/skills/cbp-build-cc-memory/examples/feedback-memory.md +11 -0
  68. package/templates/skills/cbp-build-cc-memory/examples/project-memory.md +11 -0
  69. package/templates/skills/cbp-build-cc-memory/examples/reference-memory.md +13 -0
  70. package/templates/skills/cbp-build-cc-memory/examples/user-memory.md +14 -0
  71. package/templates/skills/cbp-build-cc-memory/reference/memory-types.md +59 -0
  72. package/templates/skills/cbp-build-cc-memory/reference/when-to-save.md +62 -0
  73. package/templates/skills/cbp-build-cc-memory/templates/MEMORY-index.md +4 -0
  74. package/templates/skills/cbp-build-cc-memory/templates/memory-entry.md +15 -0
  75. package/templates/skills/cbp-build-cc-mode/SKILL.md +99 -0
  76. package/templates/skills/cbp-build-cc-rule/SKILL.md +176 -0
  77. package/templates/skills/cbp-build-cc-rule/examples/global-rule.md +19 -0
  78. package/templates/skills/cbp-build-cc-rule/examples/scoped-rule.md +41 -0
  79. package/templates/skills/cbp-build-cc-rule/reference/paths-patterns.md +48 -0
  80. package/templates/skills/cbp-build-cc-rule/templates/rule.md +32 -0
  81. package/templates/skills/cbp-build-cc-settings/SKILL.md +220 -0
  82. package/templates/skills/cbp-build-cc-settings/examples/hooks-config.json +64 -0
  83. package/templates/skills/cbp-build-cc-settings/examples/permissions-config.json +34 -0
  84. package/templates/skills/cbp-build-cc-settings/examples/sandbox-config.json +42 -0
  85. package/templates/skills/cbp-build-cc-settings/reference/cbp-conventions.md +104 -0
  86. package/templates/skills/cbp-build-cc-settings/reference/permission-rules.md +61 -0
  87. package/templates/skills/cbp-build-cc-settings/reference/scope-precedence.md +73 -0
  88. package/templates/skills/cbp-build-cc-settings/reference/settings-fields.md +166 -0
  89. package/templates/skills/cbp-build-cc-settings/templates/settings.json +23 -0
  90. package/templates/skills/cbp-build-cc-settings/templates/settings.local.json +10 -0
  91. package/templates/skills/cbp-build-cc-skill/SKILL.md +154 -0
  92. package/templates/skills/cbp-build-cc-skill/examples/dynamic-context.md +31 -0
  93. package/templates/skills/cbp-build-cc-skill/examples/fork-skill.md +22 -0
  94. package/templates/skills/cbp-build-cc-skill/examples/knowledge-skill.md +25 -0
  95. package/templates/skills/cbp-build-cc-skill/examples/task-skill.md +29 -0
  96. package/templates/skills/cbp-build-cc-skill/reference/cbp-quality.md +157 -0
  97. package/templates/skills/cbp-build-cc-skill/reference/frontmatter-fields.md +35 -0
  98. package/templates/skills/cbp-build-cc-skill/reference/string-substitutions.md +60 -0
  99. package/templates/skills/cbp-build-cc-skill/scripts/validate-skill.sh +90 -0
  100. package/templates/skills/cbp-build-cc-skill/templates/skill.md +51 -0
  101. package/templates/skills/cbp-checkpoint-check/SKILL.md +156 -0
  102. package/templates/skills/cbp-checkpoint-complete/SKILL.md +109 -0
  103. package/templates/skills/cbp-checkpoint-create/SKILL.md +287 -0
  104. package/templates/skills/cbp-checkpoint-end/SKILL.md +241 -0
  105. package/templates/skills/cbp-checkpoint-update/SKILL.md +115 -0
  106. package/templates/skills/cbp-frontend-a11y/SKILL.md +109 -0
  107. package/templates/skills/cbp-frontend-a11y/reference/aria-roles-states.md +130 -0
  108. package/templates/skills/cbp-frontend-a11y/reference/contrast-visual.md +122 -0
  109. package/templates/skills/cbp-frontend-a11y/reference/keyboard-patterns.md +154 -0
  110. package/templates/skills/cbp-frontend-a11y/reference/semantic-html.md +111 -0
  111. package/templates/skills/cbp-frontend-design/SKILL.md +145 -0
  112. package/templates/skills/cbp-frontend-design/reference/nextjs-scss.md +118 -0
  113. package/templates/skills/cbp-frontend-design/reference/rn-expo.md +101 -0
  114. package/templates/skills/cbp-frontend-design/reference/tauri-react.md +82 -0
  115. package/templates/skills/cbp-frontend-ui/SKILL.md +262 -0
  116. package/templates/skills/cbp-frontend-ui/reference/ui-label-maps.md +42 -0
  117. package/templates/skills/cbp-frontend-ui/reference/ui-layout-patterns.md +105 -0
  118. package/templates/skills/cbp-frontend-ui/reference/variant-defaults.md +149 -0
  119. package/templates/skills/cbp-frontend-ux/SKILL.md +181 -0
  120. package/templates/skills/cbp-git-branch-feat-create/SKILL.md +115 -0
  121. package/templates/skills/cbp-git-commit/SKILL.md +278 -0
  122. package/templates/skills/cbp-git-worktree-create/SKILL.md +226 -0
  123. package/templates/skills/cbp-git-worktree-remove/SKILL.md +145 -0
  124. package/templates/skills/cbp-merge-main/SKILL.md +228 -0
  125. package/templates/skills/cbp-round-check/SKILL.md +104 -0
  126. package/templates/skills/cbp-round-end/SKILL.md +183 -0
  127. package/templates/skills/cbp-round-end/reference/findings-presentation.md +44 -0
  128. package/templates/skills/cbp-round-end/reference/inline-fallback.md +35 -0
  129. package/templates/skills/cbp-round-execute/SKILL.md +211 -0
  130. package/templates/skills/cbp-round-execute/reference/inline-fallback.md +59 -0
  131. package/templates/skills/cbp-round-input/SKILL.md +165 -0
  132. package/templates/skills/cbp-round-start/SKILL.md +222 -0
  133. package/templates/skills/cbp-round-update/SKILL.md +163 -0
  134. package/templates/skills/cbp-session-end/SKILL.md +187 -0
  135. package/templates/skills/cbp-session-start/SKILL.md +155 -0
  136. package/templates/skills/cbp-ship/SKILL.md +332 -0
  137. package/templates/skills/cbp-ship/reference/changesets-overview.md +120 -0
  138. package/templates/skills/cbp-ship/reference/eas-cli-overview.md +60 -0
  139. package/templates/skills/cbp-ship/reference/gh-cli-overview.md +135 -0
  140. package/templates/skills/cbp-ship/reference/gh-cli-shipment-commands.md +283 -0
  141. package/templates/skills/cbp-ship/reference/npm-publish-monorepo.md +252 -0
  142. package/templates/skills/cbp-ship/reference/npm-publish-oidc-trusted.md +157 -0
  143. package/templates/skills/cbp-ship/reference/npm-publish-overview.md +171 -0
  144. package/templates/skills/cbp-ship/reference/preflight-checklist.md +88 -0
  145. package/templates/skills/cbp-ship/reference/railway-nestjs-deployment.md +169 -0
  146. package/templates/skills/cbp-ship/reference/railway-overview.md +120 -0
  147. package/templates/skills/cbp-ship/reference/railway-troubleshooting.md +168 -0
  148. package/templates/skills/cbp-ship/reference/release-please-overview.md +99 -0
  149. package/templates/skills/cbp-ship/reference/surface-expo-eas.md +155 -0
  150. package/templates/skills/cbp-ship/reference/surface-npm.md +180 -0
  151. package/templates/skills/cbp-ship/reference/surface-railway.md +152 -0
  152. package/templates/skills/cbp-ship/reference/surface-supabase.md +178 -0
  153. package/templates/skills/cbp-ship/reference/surface-tauri.md +138 -0
  154. package/templates/skills/cbp-ship/reference/surface-vercel.md +124 -0
  155. package/templates/skills/cbp-ship/reference/surface-vscode-ext.md +144 -0
  156. package/templates/skills/cbp-ship/reference/surfaces.md +60 -0
  157. package/templates/skills/cbp-ship/reference/testflight-automation.md +215 -0
  158. package/templates/skills/cbp-ship/reference/testflight-internal-vs-external.md +69 -0
  159. package/templates/skills/cbp-ship/reference/testflight-overview.md +98 -0
  160. package/templates/skills/cbp-ship/reference/versioning.md +116 -0
  161. package/templates/skills/cbp-ship/scripts/detect-surfaces.sh +217 -0
  162. package/templates/skills/cbp-ship/scripts/verify-expo-eas.sh +35 -0
  163. package/templates/skills/cbp-ship/scripts/verify-npm.sh +21 -0
  164. package/templates/skills/cbp-ship/scripts/verify-railway.sh +41 -0
  165. package/templates/skills/cbp-ship/scripts/verify-supabase.sh +19 -0
  166. package/templates/skills/cbp-ship/scripts/verify-tauri.sh +24 -0
  167. package/templates/skills/cbp-ship/scripts/verify-vercel.sh +32 -0
  168. package/templates/skills/cbp-ship/scripts/verify-vscode-ext.sh +25 -0
  169. package/templates/skills/cbp-ship/templates/eas.json +66 -0
  170. package/templates/skills/cbp-ship/templates/railway.toml +15 -0
  171. package/templates/skills/cbp-ship/templates/release-please-config.json +17 -0
  172. package/templates/skills/cbp-ship/templates/vercel.json +19 -0
  173. package/templates/skills/cbp-ship/templates/vscodeignore +21 -0
  174. package/templates/skills/cbp-ship/templates/workflow-changesets.yml +41 -0
  175. package/templates/skills/cbp-ship/templates/workflow-eas-submit.yml +53 -0
  176. package/templates/skills/cbp-ship/templates/workflow-npm-publish.yml +36 -0
  177. package/templates/skills/cbp-ship/templates/workflow-release-please.yml +21 -0
  178. package/templates/skills/cbp-ship/templates/workflow-tauri-release.yml +69 -0
  179. package/templates/skills/cbp-ship/templates/workflow-vsce-publish.yml +31 -0
  180. package/templates/skills/cbp-ship-configure/SKILL.md +296 -0
  181. package/templates/skills/cbp-ship-configure/reference/expo-mobile.md +204 -0
  182. package/templates/skills/cbp-ship-configure/reference/npm-package.md +165 -0
  183. package/templates/skills/cbp-ship-configure/reference/railway-backend.md +199 -0
  184. package/templates/skills/cbp-ship-configure/reference/supabase.md +200 -0
  185. package/templates/skills/cbp-ship-configure/reference/tauri-desktop.md +181 -0
  186. package/templates/skills/cbp-ship-configure/reference/vercel.md +117 -0
  187. package/templates/skills/cbp-ship-configure/reference/vscode-ext.md +155 -0
  188. package/templates/skills/cbp-ship-main/SKILL.md +65 -0
  189. package/templates/skills/cbp-supabase-branch-check/SKILL.md +337 -0
  190. package/templates/skills/cbp-supabase-branch-check/reference/dag-steps.md +29 -0
  191. package/templates/skills/cbp-supabase-migrate/SKILL.md +314 -0
  192. package/templates/skills/cbp-supabase-migrate/reference/advisor-triage.md +70 -0
  193. package/templates/skills/cbp-supabase-migrate/reference/cli-fallback.md +87 -0
  194. package/templates/skills/cbp-supabase-migrate/reference/preflight-dry-run.md +58 -0
  195. package/templates/skills/cbp-supabase-setup/SKILL.md +239 -0
  196. package/templates/skills/cbp-supabase-setup/reference/branching-setup.md +121 -0
  197. package/templates/skills/cbp-supabase-setup/reference/cli-fallback.md +109 -0
  198. package/templates/skills/cbp-task-check/SKILL.md +166 -0
  199. package/templates/skills/cbp-task-complete/SKILL.md +206 -0
  200. package/templates/skills/cbp-task-complete/reference/checkpoint-done-branching.md +48 -0
  201. package/templates/skills/cbp-task-complete/reference/next-step-heuristic.md +56 -0
  202. package/templates/skills/cbp-task-create/SKILL.md +167 -0
  203. package/templates/skills/cbp-task-start/SKILL.md +239 -0
  204. package/templates/skills/cbp-task-testing/SKILL.md +277 -0
  205. package/templates/skills/cbp-todo/SKILL.md +97 -0
@@ -0,0 +1,347 @@
1
+ #!/bin/bash
2
+ # @hook: NOT-A-HOOK (statusLine renderer, invoked by settings.json statusLine.command)
3
+ # Claude Code Status Line - All fields visible
4
+ # Purpose: Renders up to 6 structured lines of Claude Code status from stdin JSON.
5
+ # Not a PreToolUse/PostToolUse/Notification hook — do NOT register in hooks[].
6
+ # Shows: Identity | Context | Cost | Rate Limits | Repo/PR | Worktree
7
+ #
8
+ # --------------------------------------------------------------------------
9
+ # ENV-VAR TOGGLES — set to 1 to suppress a render section
10
+ # CBP_STATUSLINE_HIDE_IDENTITY=1 suppress line 1 (model, effort, session, agent)
11
+ # CBP_STATUSLINE_HIDE_CONTEXT=1 suppress line 2 (context bar, tokens, cache)
12
+ # CBP_STATUSLINE_HIDE_COST=1 suppress line 3 (cost, duration, lines)
13
+ # CBP_STATUSLINE_HIDE_RATE_LIMITS=1 suppress line 4 (5h / 7d rate limits)
14
+ # CBP_STATUSLINE_HIDE_REPO_PR=1 suppress line 5 (repo host/owner/name, PR)
15
+ # CBP_STATUSLINE_HIDE_WORKTREE=1 suppress line 6 (worktree name/branch/path)
16
+ # CBP_STATUSLINE_NO_COLOR=1 strip all ANSI colour codes (also honoured by $NO_COLOR)
17
+ # --------------------------------------------------------------------------
18
+
19
+ INPUT=$(cat)
20
+
21
+ # ---- One jq invocation captures all ~40 fields --------------------------------
22
+ eval "$(echo "$INPUT" | jq -r '
23
+ @sh "MODEL_ID=\(.model.id // "")",
24
+ @sh "MODEL_NAME=\(.model.display_name // "")",
25
+ @sh "CWD=\(.cwd // "")",
26
+ @sh "WS_CURRENT_DIR=\(.workspace.current_dir // "")",
27
+ @sh "WS_PROJECT_DIR=\(.workspace.project_dir // "")",
28
+ @sh "WS_ADDED_DIRS_LEN=\((.workspace.added_dirs // []) | length)",
29
+ @sh "WS_GIT_WORKTREE=\(.workspace.git_worktree // "")",
30
+ @sh "WS_REPO_HOST=\(.workspace.repo.host // "")",
31
+ @sh "WS_REPO_OWNER=\(.workspace.repo.owner // "")",
32
+ @sh "WS_REPO_NAME=\(.workspace.repo.name // "")",
33
+ @sh "COST=\(.cost.total_cost_usd // 0)",
34
+ @sh "DURATION=\(.cost.total_duration_ms // 0)",
35
+ @sh "API_DURATION=\(.cost.total_api_duration_ms // 0)",
36
+ @sh "LINES_ADD=\(.cost.total_lines_added // 0)",
37
+ @sh "LINES_DEL=\(.cost.total_lines_removed // 0)",
38
+ @sh "CTX_TOT_IN=\(.context_window.total_input_tokens // 0)",
39
+ @sh "CTX_TOT_OUT=\(.context_window.total_output_tokens // 0)",
40
+ @sh "CTX_SIZE=\(.context_window.context_window_size // 200000)",
41
+ @sh "CTX_PCT=\(.context_window.used_percentage // 0)",
42
+ @sh "CTX_REM=\(.context_window.remaining_percentage // 100)",
43
+ @sh "CUR_IN=\(.context_window.current_usage.input_tokens // 0)",
44
+ @sh "CUR_OUT=\(.context_window.current_usage.output_tokens // 0)",
45
+ @sh "CACHE_CREATE=\(.context_window.current_usage.cache_creation_input_tokens // 0)",
46
+ @sh "CACHE_READ=\(.context_window.current_usage.cache_read_input_tokens // 0)",
47
+ @sh "EXCEEDS_200K=\(.exceeds_200k_tokens // false)",
48
+ @sh "EFFORT=\(.effort.level // "")",
49
+ @sh "THINKING=\(.thinking.enabled // false)",
50
+ @sh "RATE_5H_PCT=\(.rate_limits.five_hour.used_percentage // "")",
51
+ @sh "RATE_5H_RESETS=\(.rate_limits.five_hour.resets_at // 0)",
52
+ @sh "RATE_7D_PCT=\(.rate_limits.seven_day.used_percentage // "")",
53
+ @sh "RATE_7D_RESETS=\(.rate_limits.seven_day.resets_at // 0)",
54
+ # Note: RATE_*_PCT use `// ""` (not `// 0`) so the `[ -n "$RATE_*_PCT" ]` absence gate
55
+ # below distinguishes "API said 0% used" from "rate_limits object absent". Do not
56
+ # change to `// 0` — it would silently suppress the rate-limit line on subscriber
57
+ # accounts at 0% usage at session start.
58
+ @sh "SESSION_ID=\(.session_id // "")",
59
+ @sh "SESSION_NAME=\(.session_name // "")",
60
+ @sh "TRANSCRIPT_PATH=\(.transcript_path // "")",
61
+ @sh "VERSION=\(.version // "")",
62
+ @sh "OUTPUT_STYLE=\(.output_style.name // "")",
63
+ @sh "VIM_MODE=\(.vim.mode // "")",
64
+ @sh "AGENT_NAME=\(.agent.name // "")",
65
+ @sh "PR_NUMBER=\(.pr.number // "")",
66
+ @sh "PR_URL=\(.pr.url // "")",
67
+ @sh "PR_REVIEW_STATE=\(.pr.review_state // "")",
68
+ @sh "WT_NAME=\(.worktree.name // "")",
69
+ @sh "WT_PATH=\(.worktree.path // "")",
70
+ @sh "WT_BRANCH=\(.worktree.branch // "")",
71
+ @sh "WT_ORIG_CWD=\(.worktree.original_cwd // "")",
72
+ @sh "WT_ORIG_BRANCH=\(.worktree.original_branch // "")"
73
+ ')"
74
+
75
+ # ---- Colour setup ------------------------------------------------------------
76
+ # Strip colours when NO_COLOR or CBP_STATUSLINE_NO_COLOR is set
77
+ if [ -n "${NO_COLOR:-}" ] || [ "${CBP_STATUSLINE_NO_COLOR:-0}" = "1" ]; then
78
+ RST=''; DIM=''; BOLD=''; GREEN=''; YELLOW=''; RED=''; CYAN=''; MAGENTA=''; BLUE=''
79
+ else
80
+ RST='\033[0m'
81
+ DIM='\033[2m'
82
+ BOLD='\033[1m'
83
+ GREEN='\033[32m'
84
+ YELLOW='\033[33m'
85
+ RED='\033[31m'
86
+ CYAN='\033[36m'
87
+ MAGENTA='\033[35m'
88
+ BLUE='\033[34m'
89
+ fi
90
+
91
+ # ---- Float-safe percentage comparison ----------------------------------------
92
+ # Usage: awk_gte VALUE THRESHOLD (returns 0 = true, 1 = false)
93
+ awk_gte() { awk -v v="$1" -v t="$2" 'BEGIN{exit !(v+0 >= t+0)}'; }
94
+
95
+ # ---- Token/size formatter (K / M) -------------------------------------------
96
+ fmt_k() {
97
+ local val=$1
98
+ if [ "$val" -ge 1000000 ] 2>/dev/null; then
99
+ printf "%.1fM" "$(echo "$val / 1000000" | bc -l)"
100
+ elif [ "$val" -ge 1000 ] 2>/dev/null; then
101
+ printf "%.1fK" "$(echo "$val / 1000" | bc -l)"
102
+ else
103
+ printf "%d" "$val"
104
+ fi
105
+ }
106
+
107
+ # ---- Duration formatter (ms → Xh Xm Xs) ------------------------------------
108
+ fmt_dur() {
109
+ local ms=$1
110
+ local secs=$(( ms / 1000 ))
111
+ if [ "$secs" -ge 3600 ]; then
112
+ printf "%dh%dm" $(( secs / 3600 )) $(( (secs % 3600) / 60 ))
113
+ elif [ "$secs" -ge 60 ]; then
114
+ printf "%dm%ds" $(( secs / 60 )) $(( secs % 60 ))
115
+ else
116
+ printf "%ds" "$secs"
117
+ fi
118
+ }
119
+
120
+ # ---- Relative-time formatter (epoch → "now" / Xm / Xh / Xd) ----------------
121
+ # Callers should gate on `epoch != 0` upstream (the absence sentinel from the
122
+ # `// 0` jq fallback). Passing epoch=0 here yields "now" by the delta<=0 branch.
123
+ fmt_rel_time() {
124
+ local epoch=$1
125
+ local now
126
+ now=$(date +%s)
127
+ local delta=$(( epoch - now ))
128
+ if [ "$delta" -le 0 ]; then
129
+ printf "now"
130
+ elif [ "$delta" -ge 86400 ]; then
131
+ printf "%dd" $(( delta / 86400 ))
132
+ elif [ "$delta" -ge 3600 ]; then
133
+ printf "%dh" $(( delta / 3600 ))
134
+ else
135
+ printf "%dm" $(( delta / 60 ))
136
+ fi
137
+ }
138
+
139
+ # ============================================================
140
+ # LINE 1 — Identity
141
+ # ============================================================
142
+ if [ "${CBP_STATUSLINE_HIDE_IDENTITY:-0}" != "1" ]; then
143
+ L1=""
144
+
145
+ # Prefix: wt > session > agent (pick first present)
146
+ if [ -n "$WT_NAME" ]; then
147
+ L1="${DIM}wt:${RST}${MAGENTA}${WT_NAME}${RST} "
148
+ elif [ -n "$SESSION_NAME" ]; then
149
+ L1="${DIM}session:${RST}${MAGENTA}${SESSION_NAME}${RST} "
150
+ elif [ -n "$AGENT_NAME" ]; then
151
+ L1="${DIM}agent:${RST}${MAGENTA}${AGENT_NAME}${RST} "
152
+ fi
153
+
154
+ # Model (display_name + id in parens when id differs or both present)
155
+ if [ -n "$MODEL_NAME" ] && [ -n "$MODEL_ID" ] && [ "$MODEL_NAME" != "$MODEL_ID" ]; then
156
+ L1="${L1}${BOLD}${CYAN}${MODEL_NAME}${RST} ${DIM}(${MODEL_ID})${RST}"
157
+ elif [ -n "$MODEL_NAME" ]; then
158
+ L1="${L1}${BOLD}${CYAN}${MODEL_NAME}${RST}"
159
+ elif [ -n "$MODEL_ID" ]; then
160
+ L1="${L1}${BOLD}${CYAN}${MODEL_ID}${RST}"
161
+ fi
162
+
163
+ # Effort level (when present)
164
+ if [ -n "$EFFORT" ]; then
165
+ L1="${L1} ${DIM}effort:${RST}${EFFORT}"
166
+ fi
167
+
168
+ # Thinking (only when explicitly true)
169
+ if [ "$THINKING" = "true" ]; then
170
+ L1="${L1} ${YELLOW}thinking:on${RST}"
171
+ fi
172
+
173
+ # Output style (when present and not "default")
174
+ if [ -n "$OUTPUT_STYLE" ] && [ "$OUTPUT_STYLE" != "default" ]; then
175
+ L1="${L1} ${DIM}style:${RST}${OUTPUT_STYLE}"
176
+ fi
177
+
178
+ # Version
179
+ if [ -n "$VERSION" ]; then
180
+ L1="${L1} ${DIM}v:${VERSION}${RST}"
181
+ fi
182
+
183
+ # Vim mode
184
+ if [ -n "$VIM_MODE" ]; then
185
+ L1="${L1} ${DIM}[${VIM_MODE}]${RST}"
186
+ fi
187
+
188
+ # Session id (short 8-char prefix — distinguishes parallel sessions)
189
+ if [ -n "$SESSION_ID" ]; then
190
+ L1="${L1} ${DIM}sid:${SESSION_ID:0:8}${RST}"
191
+ fi
192
+
193
+ # Transcript file (basename only — full path would blow out the line)
194
+ if [ -n "$TRANSCRIPT_PATH" ]; then
195
+ L1="${L1} ${DIM}log:$(basename "$TRANSCRIPT_PATH")${RST}"
196
+ fi
197
+
198
+ # Only print when we have actual content
199
+ if [ -n "$L1" ]; then
200
+ printf "%b\n" "$L1"
201
+ fi
202
+ fi
203
+
204
+ # ============================================================
205
+ # LINE 2 — Context window
206
+ # ============================================================
207
+ if [ "${CBP_STATUSLINE_HIDE_CONTEXT:-0}" != "1" ]; then
208
+ # Context bar colour by usage
209
+ if awk_gte "$CTX_PCT" 75; then
210
+ BAR_COLOR="$RED"
211
+ elif awk_gte "$CTX_PCT" 50; then
212
+ BAR_COLOR="$YELLOW"
213
+ else
214
+ BAR_COLOR="$GREEN"
215
+ fi
216
+
217
+ # 20-char progress bar (ceiling division so 99% shows 20/20, not 19/20)
218
+ FILLED=$(( (CTX_PCT + 4) / 5 ))
219
+ [ "$FILLED" -gt 20 ] && FILLED=20
220
+ EMPTY=$(( 20 - FILLED ))
221
+ BAR=""
222
+ for ((i=0; i<FILLED; i++)); do BAR+="▓"; done
223
+ for ((i=0; i<EMPTY; i++)); do BAR+="░"; done
224
+
225
+ CTX_SIZE_F=$(fmt_k "$CTX_SIZE")
226
+ CUR_IN_F=$(fmt_k "$CUR_IN")
227
+ CUR_OUT_F=$(fmt_k "$CUR_OUT")
228
+ CACHE_CR_F=$(fmt_k "$CACHE_CREATE")
229
+ CACHE_RD_F=$(fmt_k "$CACHE_READ")
230
+
231
+ L2="${BAR_COLOR}${BAR}${RST} ${BAR_COLOR}${CTX_PCT}%${RST}${DIM}/${CTX_SIZE_F}${RST}"
232
+ L2="${L2} ${DIM}in:${RST}${BLUE}${CUR_IN_F}${RST} ${DIM}out:${RST}${MAGENTA}${CUR_OUT_F}${RST} ${DIM}cache_cr:${RST}${CACHE_CR_F} ${DIM}cache_rd:${RST}${CACHE_RD_F}"
233
+
234
+ if [ "$EXCEEDS_200K" = "true" ]; then
235
+ L2="${L2} ${YELLOW}⚠ 200k+${RST}"
236
+ fi
237
+
238
+ printf "%b\n" "$L2"
239
+ fi
240
+
241
+ # ============================================================
242
+ # LINE 3 — Cost
243
+ # ============================================================
244
+ if [ "${CBP_STATUSLINE_HIDE_COST:-0}" != "1" ]; then
245
+ COST_F=$(printf '$%.4f' "$COST")
246
+ DUR_F=$(fmt_dur "$DURATION")
247
+ API_DUR_F=$(fmt_dur "$API_DURATION")
248
+
249
+ L3="${GREEN}${COST_F}${RST} ${DIM}dur:${RST}${DUR_F} ${DIM}api:${RST}${API_DUR_F} ${GREEN}+${LINES_ADD}${RST} ${RED}-${LINES_DEL}${RST} ${DIM}lines${RST}"
250
+ printf "%b\n" "$L3"
251
+ fi
252
+
253
+ # ============================================================
254
+ # LINE 4 — Rate limits
255
+ # ============================================================
256
+ if [ "${CBP_STATUSLINE_HIDE_RATE_LIMITS:-0}" != "1" ]; then
257
+ # Emit this line only when at least one window has resets_at != 0
258
+ HAVE_RATE=0
259
+ [ -n "$RATE_5H_PCT" ] && [ "$RATE_5H_RESETS" != "0" ] && HAVE_RATE=1
260
+ [ -n "$RATE_7D_PCT" ] && [ "$RATE_7D_RESETS" != "0" ] && HAVE_RATE=1
261
+
262
+ if [ "$HAVE_RATE" = "1" ]; then
263
+ L4=""
264
+
265
+ if [ -n "$RATE_5H_PCT" ] && [ "$RATE_5H_RESETS" != "0" ]; then
266
+ if awk_gte "$RATE_5H_PCT" 80; then
267
+ C5="$RED"
268
+ elif awk_gte "$RATE_5H_PCT" 60; then
269
+ C5="$YELLOW"
270
+ else
271
+ C5="$GREEN"
272
+ fi
273
+ REL5=$(fmt_rel_time "$RATE_5H_RESETS")
274
+ L4="${DIM}5h:${RST}${C5}${RATE_5H_PCT}%${RST} ${DIM}(resets in ${REL5})${RST}"
275
+ fi
276
+
277
+ if [ -n "$RATE_7D_PCT" ] && [ "$RATE_7D_RESETS" != "0" ]; then
278
+ if awk_gte "$RATE_7D_PCT" 80; then
279
+ C7="$RED"
280
+ elif awk_gte "$RATE_7D_PCT" 60; then
281
+ C7="$YELLOW"
282
+ else
283
+ C7="$GREEN"
284
+ fi
285
+ REL7=$(fmt_rel_time "$RATE_7D_RESETS")
286
+ SEG7="${DIM}7d:${RST}${C7}${RATE_7D_PCT}%${RST} ${DIM}(resets in ${REL7})${RST}"
287
+ if [ -n "$L4" ]; then
288
+ L4="${L4} ${DIM}|${RST} ${SEG7}"
289
+ else
290
+ L4="$SEG7"
291
+ fi
292
+ fi
293
+
294
+ printf "%b\n" "$L4"
295
+ fi
296
+ fi
297
+
298
+ # ============================================================
299
+ # LINE 5 — Repo / PR
300
+ # ============================================================
301
+ if [ "${CBP_STATUSLINE_HIDE_REPO_PR:-0}" != "1" ]; then
302
+ L5=""
303
+
304
+ # Repo segment (host/owner/name when host is present)
305
+ if [ -n "$WS_REPO_HOST" ]; then
306
+ L5="${DIM}${WS_REPO_HOST}/${WS_REPO_OWNER}/${WS_REPO_NAME}${RST}"
307
+ fi
308
+
309
+ # PR segment
310
+ if [ -n "$PR_NUMBER" ]; then
311
+ PR_SEG="${DIM}PR${RST} ${CYAN}#${PR_NUMBER}${RST}"
312
+ if [ -n "$PR_REVIEW_STATE" ]; then
313
+ PR_SEG="${PR_SEG} ${DIM}${PR_REVIEW_STATE}${RST}"
314
+ fi
315
+ if [ -n "$PR_URL" ]; then
316
+ PR_SEG="${PR_SEG} ${BLUE}${PR_URL}${RST}"
317
+ fi
318
+ if [ -n "$L5" ]; then
319
+ L5="${L5} ${DIM}|${RST} ${PR_SEG}"
320
+ else
321
+ L5="$PR_SEG"
322
+ fi
323
+ fi
324
+
325
+ if [ -n "$L5" ]; then
326
+ printf "%b\n" "$L5"
327
+ fi
328
+ fi
329
+
330
+ # ============================================================
331
+ # LINE 6 — Worktree
332
+ # ============================================================
333
+ if [ "${CBP_STATUSLINE_HIDE_WORKTREE:-0}" != "1" ]; then
334
+ if [ -n "$WT_NAME" ]; then
335
+ L6="${MAGENTA}${WT_NAME}${RST} ${DIM}@${RST} ${CYAN}${WT_BRANCH}${RST}"
336
+ if [ -n "$WT_ORIG_BRANCH" ] && [ "$WT_ORIG_BRANCH" != "$WT_BRANCH" ]; then
337
+ L6="${L6} ${DIM}(was: ${WT_ORIG_BRANCH})${RST}"
338
+ fi
339
+ # Truncate path if longer than 60 chars
340
+ WT_PATH_DISP="$WT_PATH"
341
+ if [ "${#WT_PATH}" -gt 60 ]; then
342
+ WT_PATH_DISP="...${WT_PATH: -57}"
343
+ fi
344
+ L6="${L6} ${DIM}${WT_PATH_DISP}${RST}"
345
+ printf "%b\n" "$L6"
346
+ fi
347
+ fi
@@ -0,0 +1,182 @@
1
+ #!/bin/bash
2
+ # Portability: bash 3.2+ (macOS default /bin/bash). UTF-8 multibyte chars in
3
+ # user-controlled fields are byte-iterated under 3.2 (over-counts visible width
4
+ # for non-ASCII content); acceptable safe-direction approximation.
5
+ # @hook: NOT-A-HOOK (subagentStatusLine renderer, invoked by settings.json subagentStatusLine.command)
6
+ # Claude Code Subagent Status Line — one JSON line per active subagent
7
+ # Purpose: Renders one row per running subagent. Output protocol per upstream docs:
8
+ # {"id": "<task_id>", "content": "<rendered_body>"}
9
+ # - Omitted tasks keep their default Claude Code rendering
10
+ # - Empty content strings hide the row
11
+ # NOT a PreToolUse/PostToolUse/Notification hook — do NOT register in hooks[].
12
+ #
13
+ # ENV-VAR TOGGLES — set to 1 to suppress a column / color
14
+ # CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION=1 omit DESCRIPTION segment
15
+ # CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS=1 omit tokens:N segment
16
+ # CBP_SUBAGENT_STATUSLINE_NO_COLOR=1 strip all ANSI codes (also honoured by $NO_COLOR)
17
+
18
+ INPUT=$(cat)
19
+
20
+ # One jq pass for base fields (cwd, session_id, columns)
21
+ eval "$(echo "$INPUT" | jq -r '
22
+ @sh "CWD=\(.cwd // "")",
23
+ @sh "SESSION_ID=\(.session_id // "")",
24
+ @sh "COLUMNS=\(.columns // 0)"
25
+ ')"
26
+
27
+ # ---- Colour setup ----------------------------------------------------------
28
+ # Only the colours referenced by the render block below are defined here.
29
+ if [ -n "${NO_COLOR:-}" ] || [ "${CBP_SUBAGENT_STATUSLINE_NO_COLOR:-0}" = "1" ]; then
30
+ RST=''; DIM=''; BOLD=''; GREEN=''; RED=''; BLUE=''
31
+ else
32
+ RST='\033[0m'
33
+ DIM='\033[2m'
34
+ BOLD='\033[1m'
35
+ GREEN='\033[32m'
36
+ RED='\033[31m'
37
+ BLUE='\033[34m'
38
+ fi
39
+
40
+ # ---- Token/size formatter K / M (verbatim from cbp-statusline.sh L96-105) ---
41
+ fmt_k() {
42
+ local val=$1
43
+ if [ "$val" -ge 1000000 ] 2>/dev/null; then
44
+ printf "%.1fM" "$(echo "$val / 1000000" | bc -l)"
45
+ elif [ "$val" -ge 1000 ] 2>/dev/null; then
46
+ printf "%.1fK" "$(echo "$val / 1000" | bc -l)"
47
+ else
48
+ printf "%d" "$val"
49
+ fi
50
+ }
51
+
52
+ # ---- Age formatter: epoch seconds → "Xs" / "XmYs" / "XhYm" -----------------
53
+ # NEW helper — cannot reuse fmt_dur (which divides by 1000 for ms input).
54
+ # startTime is a Unix epoch in seconds, same as rate_limits.*.resets_at.
55
+ fmt_age_secs() {
56
+ local start=$1
57
+ local now
58
+ now=$(date +%s)
59
+ local delta=$(( now - start ))
60
+ [ "$delta" -lt 0 ] && delta=0
61
+ if [ "$delta" -ge 3600 ]; then
62
+ printf "%dh%dm" $(( delta / 3600 )) $(( (delta % 3600) / 60 ))
63
+ elif [ "$delta" -ge 60 ]; then
64
+ printf "%dm%ds" $(( delta / 60 )) $(( delta % 60 ))
65
+ else
66
+ printf "%ds" "$delta"
67
+ fi
68
+ }
69
+
70
+ # ---- Strip ANSI codes for visible-length measurement (column truncation) ----
71
+ strip_ansi() {
72
+ printf '%s' "$1" | sed $'s/\033\\[[0-9;]*m//g'
73
+ }
74
+
75
+ # ---- JSON-string-escape a bash variable (embed in {"id":..., "content":...}) -
76
+ json_escape() {
77
+ printf '%s' "$1" | jq -Rs '.'
78
+ }
79
+
80
+ # ---- Per-task render: one jq TSV emission, looped in bash ------------------
81
+ # Single jq pass for tasks[] (locked decision #3 — two jq calls total).
82
+ echo "$INPUT" | jq -r '
83
+ .tasks // [] | .[] | [
84
+ .id // "",
85
+ .name // "",
86
+ .type // "",
87
+ .status // "",
88
+ .description // "",
89
+ (.startTime // 0 | tostring),
90
+ (.tokenCount // 0 | tostring)
91
+ ] | @tsv
92
+ ' | while IFS=$'\t' read -r T_ID T_NAME T_TYPE T_STATUS T_DESC T_START T_TOKENS; do
93
+ # Skip blank rows (empty tasks[] → no output)
94
+ [ -z "$T_ID" ] && continue
95
+
96
+ # Status icon + colour
97
+ case "$T_STATUS" in
98
+ running) ICON="${GREEN}●${RST}" ;;
99
+ pending) ICON="${DIM}○${RST}" ;;
100
+ completed) ICON="${BLUE}✓${RST}" ;;
101
+ failed) ICON="${RED}✗${RST}" ;;
102
+ *) ICON="${DIM}•${RST}" ;;
103
+ esac
104
+
105
+ # Build content segments
106
+ ROW="${ICON} ${BOLD}${T_NAME}${RST}"
107
+ [ -n "$T_TYPE" ] && ROW="${ROW} ${DIM}(${T_TYPE})${RST}"
108
+
109
+ if [ "${CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION:-0}" != "1" ] && [ -n "$T_DESC" ]; then
110
+ ROW="${ROW} ${DIM}—${RST} ${T_DESC}"
111
+ fi
112
+
113
+ if [ "${CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS:-0}" != "1" ] && [ "$T_TOKENS" -gt 0 ] 2>/dev/null; then
114
+ TOKENS_F=$(fmt_k "$T_TOKENS")
115
+ ROW="${ROW} ${DIM}·${RST} ${DIM}tokens:${RST}${TOKENS_F}"
116
+ fi
117
+
118
+ if [ "$T_START" -gt 0 ] 2>/dev/null; then
119
+ AGE_F=$(fmt_age_secs "$T_START")
120
+ ROW="${ROW} ${DIM}·${RST} ${DIM}age:${RST}${AGE_F}"
121
+ fi
122
+
123
+ # Apply printf to interpret escape sequences, then truncate to $COLUMNS
124
+ RENDERED=$(printf '%b' "$ROW")
125
+ if [ "$COLUMNS" -gt 0 ] 2>/dev/null; then
126
+ # ANSI-aware iterative truncation: walk RENDERED char-by-char treating
127
+ # \033[...letter escape sequences as zero-width and ordinary chars as
128
+ # one-width. Stop when the visible counter reaches COLUMNS - 3, then
129
+ # append RST + ellipsis. Correct under any ANSI-density distribution,
130
+ # unlike a single OVERHEAD-add formula which assumes uniform spread.
131
+ # Pure bash (no awk/perl subprocess); byte-based slicing means non-ASCII
132
+ # multi-byte UTF-8 in user-controlled descriptions counts as multiple
133
+ # visible units — acceptable approximation for the ASCII-dominant case.
134
+ TARGET=$(( COLUMNS - 3 ))
135
+ [ "$TARGET" -lt 0 ] && TARGET=0
136
+ RAW_LEN=${#RENDERED}
137
+ vis=0
138
+ i=0
139
+ in_esc=0
140
+ while [ $i -lt $RAW_LEN ] && [ $vis -lt $TARGET ]; do
141
+ ch="${RENDERED:$i:1}"
142
+ if [ "$in_esc" = "1" ]; then
143
+ case "$ch" in
144
+ [a-zA-Z]) in_esc=0 ;;
145
+ esac
146
+ elif [ "$ch" = $'\033' ]; then
147
+ in_esc=1
148
+ else
149
+ vis=$(( vis + 1 ))
150
+ fi
151
+ i=$(( i + 1 ))
152
+ done
153
+ # Post-loop drain: every rendered row ends with a trailing ${RST} from the
154
+ # last segment. Without this drain, a row whose visible content fits within
155
+ # COLUMNS still has `i < RAW_LEN` (pointing inside the trailing reset),
156
+ # which would spuriously trigger truncation. Walk past any in-progress
157
+ # escape and any further escape sequences until we hit a real visible byte
158
+ # or end-of-string.
159
+ while [ $i -lt $RAW_LEN ]; do
160
+ ch="${RENDERED:$i:1}"
161
+ if [ "$in_esc" = "1" ]; then
162
+ case "$ch" in
163
+ [a-zA-Z]) in_esc=0 ;;
164
+ esac
165
+ i=$(( i + 1 ))
166
+ elif [ "$ch" = $'\033' ]; then
167
+ in_esc=1
168
+ i=$(( i + 1 ))
169
+ else
170
+ break
171
+ fi
172
+ done
173
+ if [ $i -lt $RAW_LEN ]; then
174
+ # printf '%b' converts the literal-text RST (`\033[0m`) into an actual
175
+ # ESC byte sequence, matching the byte semantics of RENDERED's prefix.
176
+ RENDERED="${RENDERED:0:$i}$(printf '%b' "$RST")..."
177
+ fi
178
+ fi
179
+
180
+ # Emit one JSON line per task (upstream protocol: JSON-per-line)
181
+ printf '{"id":%s,"content":%s}\n' "$(json_escape "$T_ID")" "$(json_escape "$RENDERED")"
182
+ done
@@ -0,0 +1,144 @@
1
+ #!/bin/bash
2
+ # @hook: PreToolUse Bash
3
+ # Hook: PreToolUse for Bash (git commit commands)
4
+ # Purpose: Block git commits when new source files lack test companions
5
+ # Exit 0 = allow, Exit 2 = block (stderr sent to Claude)
6
+
7
+ set -e
8
+
9
+ echo "[HOOK START] test-coverage-gate"
10
+
11
+ # Read JSON input from stdin
12
+ INPUT=$(cat)
13
+
14
+ # Extract command from tool input
15
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
16
+
17
+ if [ -z "$COMMAND" ]; then
18
+ echo "[HOOK END] test-coverage-gate (no command)"
19
+ exit 0
20
+ fi
21
+
22
+ # Only check git commit commands
23
+ if ! echo "$COMMAND" | grep -qE 'git\s+commit'; then
24
+ echo "[HOOK END] test-coverage-gate (not git commit)"
25
+ exit 0
26
+ fi
27
+
28
+ # Get repo root
29
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
30
+ if [ -z "$REPO_ROOT" ]; then
31
+ echo "[HOOK END] test-coverage-gate (not a git repo)"
32
+ exit 0
33
+ fi
34
+
35
+ # Skip merge commits — the diff inherits files from the other parent that
36
+ # arrived without test companions; flagging them here is a false positive
37
+ # (the coverage gap belongs to whichever upstream branch introduced them).
38
+ # Use `git rev-parse --git-dir` to resolve the real git dir (handles worktrees,
39
+ # where .git is a file pointing at the main repo's worktrees/<name>/ dir).
40
+ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo "")
41
+ if [ -n "$GIT_DIR" ] && [ -f "${GIT_DIR}/MERGE_HEAD" ]; then
42
+ echo "[HOOK END] test-coverage-gate (merge in progress; skipping)"
43
+ exit 0
44
+ fi
45
+
46
+ # Get staged files (new or modified source files)
47
+ # Only check NEW files (Added), not modifications to existing files.
48
+ # Modified files that lack tests are a pre-existing gap — captured by
49
+ # test-coverage tooling, not blocked at commit time.
50
+ STAGED_SOURCE=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx|js|jsx)$' || true)
51
+
52
+ if [ -z "$STAGED_SOURCE" ]; then
53
+ echo "[HOOK END] test-coverage-gate (no source files staged)"
54
+ exit 0
55
+ fi
56
+
57
+ MISSING_TESTS=""
58
+ CHECKED=0
59
+ SKIPPED=0
60
+
61
+ while IFS= read -r FILE; do
62
+ # Skip test files themselves
63
+ if echo "$FILE" | grep -qE '\.(test|spec)\.(ts|tsx|js|jsx)$'; then
64
+ continue
65
+ fi
66
+
67
+ # Skip infrastructure / generated / config files (generic skips — harmless if user doesn't have these dirs)
68
+ if echo "$FILE" | grep -qE '^\.claude/|^docs/|^supabase/|\.config\.|\.json$|\.md$|\.ya?ml$|\.sh$|\.scss$|\.css$'; then
69
+ continue
70
+ fi
71
+
72
+ # Skip type definition files
73
+ if echo "$FILE" | grep -qE '\.d\.ts$|/types\.ts$|/types/'; then
74
+ continue
75
+ fi
76
+
77
+ # Skip barrel exports (index.ts with only re-exports)
78
+ if echo "$FILE" | grep -qE '/index\.(ts|tsx)$'; then
79
+ SKIPPED=$((SKIPPED + 1))
80
+ continue
81
+ fi
82
+
83
+ # Skip Next.js App Router framework files — page/layout/loading/error/route/
84
+ # etc. are framework-convention wrappers, not logic-bearing modules. Same
85
+ # category as the barrel/index skip above: behavior lives in the components
86
+ # they delegate to, which carry their own tests.
87
+ if echo "$FILE" | grep -qE '/app/(.*/)?(page|layout|loading|error|global-error|not-found|template|default|route|forbidden|unauthorized)\.(tsx?|jsx?)$'; then
88
+ SKIPPED=$((SKIPPED + 1))
89
+ continue
90
+ fi
91
+
92
+ CHECKED=$((CHECKED + 1))
93
+
94
+ # Derive expected test file locations
95
+ DIR=$(dirname "$FILE")
96
+ BASE=$(basename "$FILE" | sed -E 's/\.(ts|tsx|js|jsx)$//')
97
+ EXT=$(echo "$FILE" | grep -oE '\.(ts|tsx|js|jsx)$')
98
+
99
+ # Check multiple conventions:
100
+ # 1. Sibling: dir/name.test.ext
101
+ # 2. Co-located: dir/__tests__/name.test.ext
102
+ # 3. Spec variant: dir/name.spec.ext
103
+ # 4. Co-located spec: dir/__tests__/name.spec.ext
104
+ FOUND=0
105
+ for PATTERN in \
106
+ "${DIR}/${BASE}.test${EXT}" \
107
+ "${DIR}/__tests__/${BASE}.test${EXT}" \
108
+ "${DIR}/${BASE}.spec${EXT}" \
109
+ "${DIR}/__tests__/${BASE}.spec${EXT}" \
110
+ "${DIR}/${BASE}.test.ts" \
111
+ "${DIR}/__tests__/${BASE}.test.ts" \
112
+ "${DIR}/${BASE}.test.tsx" \
113
+ "${DIR}/__tests__/${BASE}.test.tsx"; do
114
+ if [ -f "${REPO_ROOT}/${PATTERN}" ]; then
115
+ FOUND=1
116
+ break
117
+ fi
118
+ done
119
+
120
+ # Also check if test file is being staged in this same commit
121
+ if [ "$FOUND" -eq 0 ]; then
122
+ if echo "$STAGED_SOURCE" | grep -qE "${BASE}\.(test|spec)\.(ts|tsx|js|jsx)$"; then
123
+ FOUND=1
124
+ fi
125
+ fi
126
+
127
+ if [ "$FOUND" -eq 0 ]; then
128
+ MISSING_TESTS="${MISSING_TESTS} - ${FILE}\n"
129
+ fi
130
+ done <<< "$STAGED_SOURCE"
131
+
132
+ if [ -n "$MISSING_TESTS" ]; then
133
+ echo "" >&2
134
+ echo "TEST COVERAGE GATE: Cannot commit — source files without test companions:" >&2
135
+ echo -e "$MISSING_TESTS" >&2
136
+ echo "Write tests for these files before committing." >&2
137
+ echo "Checked ${CHECKED} source files, skipped ${SKIPPED} (index/barrel/framework)." >&2
138
+ echo "" >&2
139
+ echo "[HOOK END] test-coverage-gate (BLOCKED)"
140
+ exit 2
141
+ fi
142
+
143
+ echo "[HOOK END] test-coverage-gate (${CHECKED} files checked, all have tests)"
144
+ exit 0