@paths.design/caws-cli 9.3.2 → 10.1.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 (286) hide show
  1. package/README.md +71 -32
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/archive.js +67 -28
  4. package/dist/commands/burnup.js +20 -11
  5. package/dist/commands/diagnose.js +34 -22
  6. package/dist/commands/evaluate.js +41 -15
  7. package/dist/commands/gates.js +149 -0
  8. package/dist/commands/init.js +150 -19
  9. package/dist/commands/iterate.js +81 -4
  10. package/dist/commands/parallel.js +4 -0
  11. package/dist/commands/plan.js +9 -19
  12. package/dist/commands/provenance.js +53 -17
  13. package/dist/commands/quality-monitor.js +64 -45
  14. package/dist/commands/scope.js +264 -0
  15. package/dist/commands/sidecar.js +74 -0
  16. package/dist/commands/specs.js +381 -45
  17. package/dist/commands/status.js +117 -9
  18. package/dist/commands/templates.js +0 -8
  19. package/dist/commands/tutorial.js +10 -9
  20. package/dist/commands/validate.js +70 -6
  21. package/dist/commands/verify-acs.js +48 -76
  22. package/dist/commands/waivers.js +212 -13
  23. package/dist/commands/worktree.js +131 -26
  24. package/dist/error-handler.js +2 -13
  25. package/dist/gates/budget-limit.js +121 -0
  26. package/dist/gates/feedback.js +260 -0
  27. package/dist/gates/format.js +179 -0
  28. package/dist/gates/god-object.js +117 -0
  29. package/dist/gates/pipeline.js +167 -0
  30. package/dist/gates/scope-boundary.js +93 -0
  31. package/dist/gates/spec-completeness.js +109 -0
  32. package/dist/gates/todo-detection.js +205 -0
  33. package/dist/index.js +157 -151
  34. package/dist/parallel/parallel-manager.js +3 -3
  35. package/dist/policy/PolicyManager.js +51 -17
  36. package/dist/scaffold/claude-hooks.js +24 -1
  37. package/dist/scaffold/git-hooks.js +45 -102
  38. package/dist/scaffold/index.js +4 -3
  39. package/dist/session/session-manager.js +105 -14
  40. package/dist/sidecars/index.js +33 -0
  41. package/dist/sidecars/listeners.js +40 -0
  42. package/dist/sidecars/provenance-summary.js +238 -0
  43. package/dist/sidecars/quality-gaps.js +258 -0
  44. package/dist/sidecars/schema.js +149 -0
  45. package/dist/sidecars/spec-drift.js +151 -0
  46. package/dist/sidecars/waiver-draft.js +176 -0
  47. package/dist/templates/.caws/schemas/policy.schema.json +112 -0
  48. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
  50. package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
  51. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  52. package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
  53. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  54. package/dist/templates/.claude/README.md +1 -1
  55. package/dist/templates/.claude/hooks/audit.sh +0 -0
  56. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  57. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  58. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  59. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  60. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  61. package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
  62. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  63. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  64. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  65. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  66. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  67. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  68. package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  69. package/dist/templates/.claude/settings.json +31 -0
  70. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  71. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  72. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  73. package/dist/templates/.cursor/hooks.json +25 -0
  74. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  75. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  76. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  77. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  78. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  79. package/dist/templates/.github/copilot-instructions.md +5 -5
  80. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  81. package/dist/templates/.junie/guidelines.md +2 -2
  82. package/dist/templates/.vscode/settings.json +3 -1
  83. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  84. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  85. package/dist/templates/CLAUDE.md +77 -8
  86. package/dist/templates/agents.md +50 -9
  87. package/dist/templates/docs/README.md +8 -7
  88. package/dist/templates/scripts/new_feature.sh +80 -0
  89. package/dist/test-analysis.js +43 -30
  90. package/dist/tool-loader.js +1 -1
  91. package/dist/utils/agent-session.js +202 -0
  92. package/dist/utils/detection.js +8 -2
  93. package/dist/utils/event-log.js +584 -0
  94. package/dist/utils/event-renderer.js +521 -0
  95. package/dist/utils/finalization.js +7 -6
  96. package/dist/utils/gitignore-updater.js +3 -0
  97. package/dist/utils/lifecycle-events.js +94 -0
  98. package/dist/utils/quality-gates-utils.js +29 -44
  99. package/dist/utils/schema-validator.js +50 -0
  100. package/dist/utils/spec-resolver.js +93 -21
  101. package/dist/utils/working-state.js +530 -0
  102. package/dist/validation/spec-validation.js +191 -31
  103. package/dist/waivers-manager.js +144 -6
  104. package/dist/worktree/worktree-manager.js +598 -95
  105. package/package.json +9 -8
  106. package/templates/.caws/schemas/policy.schema.json +112 -0
  107. package/templates/.caws/schemas/scope.schema.json +3 -3
  108. package/templates/.caws/schemas/waivers.schema.json +96 -20
  109. package/templates/.caws/schemas/working-spec.schema.json +264 -57
  110. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  111. package/templates/.caws/templates/working-spec.template.yml +10 -4
  112. package/templates/.caws/tools/scope-guard.js +66 -15
  113. package/templates/.claude/README.md +1 -1
  114. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  115. package/templates/.claude/hooks/classify_command.py +592 -0
  116. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  117. package/templates/.claude/hooks/protected-paths.sh +39 -0
  118. package/templates/.claude/hooks/quality-check.sh +23 -10
  119. package/templates/.claude/hooks/scope-guard.sh +136 -55
  120. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  121. package/templates/.claude/hooks/session-log.sh +76 -3
  122. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  123. package/templates/.claude/hooks/test_classify_command.py +370 -0
  124. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  125. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  126. package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  127. package/templates/.claude/settings.json +31 -0
  128. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  129. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  130. package/templates/.cursor/hooks/session-log.sh +924 -0
  131. package/templates/.cursor/hooks.json +25 -0
  132. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  133. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  134. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  135. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  136. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  137. package/templates/.github/copilot-instructions.md +5 -5
  138. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  139. package/templates/.junie/guidelines.md +2 -2
  140. package/templates/.vscode/settings.json +3 -1
  141. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  142. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  143. package/templates/CLAUDE.md +77 -8
  144. package/templates/{AGENTS.md → agents.md} +50 -9
  145. package/templates/docs/README.md +8 -7
  146. package/templates/scripts/new_feature.sh +80 -0
  147. package/dist/budget-derivation.d.ts +0 -74
  148. package/dist/budget-derivation.d.ts.map +0 -1
  149. package/dist/cicd-optimizer.d.ts +0 -142
  150. package/dist/cicd-optimizer.d.ts.map +0 -1
  151. package/dist/commands/archive.d.ts +0 -51
  152. package/dist/commands/archive.d.ts.map +0 -1
  153. package/dist/commands/burnup.d.ts +0 -6
  154. package/dist/commands/burnup.d.ts.map +0 -1
  155. package/dist/commands/diagnose.d.ts +0 -52
  156. package/dist/commands/diagnose.d.ts.map +0 -1
  157. package/dist/commands/evaluate.d.ts +0 -8
  158. package/dist/commands/evaluate.d.ts.map +0 -1
  159. package/dist/commands/init.d.ts +0 -5
  160. package/dist/commands/init.d.ts.map +0 -1
  161. package/dist/commands/iterate.d.ts +0 -8
  162. package/dist/commands/iterate.d.ts.map +0 -1
  163. package/dist/commands/mode.d.ts +0 -25
  164. package/dist/commands/mode.d.ts.map +0 -1
  165. package/dist/commands/parallel.d.ts +0 -7
  166. package/dist/commands/parallel.d.ts.map +0 -1
  167. package/dist/commands/plan.d.ts +0 -49
  168. package/dist/commands/plan.d.ts.map +0 -1
  169. package/dist/commands/provenance.d.ts +0 -32
  170. package/dist/commands/provenance.d.ts.map +0 -1
  171. package/dist/commands/quality-gates.d.ts +0 -6
  172. package/dist/commands/quality-gates.d.ts.map +0 -1
  173. package/dist/commands/quality-gates.js +0 -444
  174. package/dist/commands/quality-monitor.d.ts +0 -17
  175. package/dist/commands/quality-monitor.d.ts.map +0 -1
  176. package/dist/commands/session.d.ts +0 -7
  177. package/dist/commands/session.d.ts.map +0 -1
  178. package/dist/commands/specs.d.ts +0 -77
  179. package/dist/commands/specs.d.ts.map +0 -1
  180. package/dist/commands/status.d.ts +0 -44
  181. package/dist/commands/status.d.ts.map +0 -1
  182. package/dist/commands/templates.d.ts +0 -74
  183. package/dist/commands/templates.d.ts.map +0 -1
  184. package/dist/commands/tool.d.ts +0 -13
  185. package/dist/commands/tool.d.ts.map +0 -1
  186. package/dist/commands/troubleshoot.d.ts +0 -8
  187. package/dist/commands/troubleshoot.d.ts.map +0 -1
  188. package/dist/commands/troubleshoot.js +0 -104
  189. package/dist/commands/tutorial.d.ts +0 -55
  190. package/dist/commands/tutorial.d.ts.map +0 -1
  191. package/dist/commands/validate.d.ts +0 -15
  192. package/dist/commands/validate.d.ts.map +0 -1
  193. package/dist/commands/waivers.d.ts +0 -8
  194. package/dist/commands/waivers.d.ts.map +0 -1
  195. package/dist/commands/workflow.d.ts +0 -85
  196. package/dist/commands/workflow.d.ts.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -7
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/config/index.d.ts +0 -29
  200. package/dist/config/index.d.ts.map +0 -1
  201. package/dist/config/lite-scope.d.ts +0 -33
  202. package/dist/config/lite-scope.d.ts.map +0 -1
  203. package/dist/config/modes.d.ts +0 -264
  204. package/dist/config/modes.d.ts.map +0 -1
  205. package/dist/constants/spec-types.d.ts +0 -93
  206. package/dist/constants/spec-types.d.ts.map +0 -1
  207. package/dist/error-handler.d.ts +0 -151
  208. package/dist/error-handler.d.ts.map +0 -1
  209. package/dist/generators/jest-config-generator.d.ts +0 -32
  210. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  211. package/dist/generators/jest-config.d.ts +0 -32
  212. package/dist/generators/jest-config.d.ts.map +0 -1
  213. package/dist/generators/jest-config.js +0 -242
  214. package/dist/generators/working-spec.d.ts +0 -13
  215. package/dist/generators/working-spec.d.ts.map +0 -1
  216. package/dist/index-new.d.ts +0 -5
  217. package/dist/index-new.d.ts.map +0 -1
  218. package/dist/index-new.js +0 -317
  219. package/dist/index.d.ts +0 -5
  220. package/dist/index.d.ts.map +0 -1
  221. package/dist/index.js.backup +0 -4711
  222. package/dist/minimal-cli.d.ts +0 -3
  223. package/dist/minimal-cli.d.ts.map +0 -1
  224. package/dist/parallel/parallel-manager.d.ts +0 -67
  225. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  226. package/dist/policy/PolicyManager.d.ts +0 -104
  227. package/dist/policy/PolicyManager.d.ts.map +0 -1
  228. package/dist/scaffold/claude-hooks.d.ts +0 -28
  229. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  230. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  231. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  232. package/dist/scaffold/git-hooks.d.ts +0 -38
  233. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  234. package/dist/scaffold/index.d.ts +0 -17
  235. package/dist/scaffold/index.d.ts.map +0 -1
  236. package/dist/session/session-manager.d.ts +0 -94
  237. package/dist/session/session-manager.d.ts.map +0 -1
  238. package/dist/spec/SpecFileManager.d.ts +0 -146
  239. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  240. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  241. package/dist/templates/.github/copilot/instructions.md +0 -311
  242. package/dist/test-analysis.d.ts +0 -231
  243. package/dist/test-analysis.d.ts.map +0 -1
  244. package/dist/tool-interface.d.ts +0 -236
  245. package/dist/tool-interface.d.ts.map +0 -1
  246. package/dist/tool-loader.d.ts +0 -77
  247. package/dist/tool-loader.d.ts.map +0 -1
  248. package/dist/tool-validator.d.ts +0 -72
  249. package/dist/tool-validator.d.ts.map +0 -1
  250. package/dist/utils/async-utils.d.ts +0 -73
  251. package/dist/utils/async-utils.d.ts.map +0 -1
  252. package/dist/utils/command-wrapper.d.ts +0 -66
  253. package/dist/utils/command-wrapper.d.ts.map +0 -1
  254. package/dist/utils/detection.d.ts +0 -14
  255. package/dist/utils/detection.d.ts.map +0 -1
  256. package/dist/utils/error-categories.d.ts +0 -52
  257. package/dist/utils/error-categories.d.ts.map +0 -1
  258. package/dist/utils/finalization.d.ts +0 -17
  259. package/dist/utils/finalization.d.ts.map +0 -1
  260. package/dist/utils/git-lock.d.ts +0 -13
  261. package/dist/utils/git-lock.d.ts.map +0 -1
  262. package/dist/utils/gitignore-updater.d.ts +0 -39
  263. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  264. package/dist/utils/ide-detection.d.ts +0 -89
  265. package/dist/utils/ide-detection.d.ts.map +0 -1
  266. package/dist/utils/project-analysis.d.ts +0 -34
  267. package/dist/utils/project-analysis.d.ts.map +0 -1
  268. package/dist/utils/promise-utils.d.ts +0 -30
  269. package/dist/utils/promise-utils.d.ts.map +0 -1
  270. package/dist/utils/quality-gates-utils.d.ts +0 -49
  271. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  272. package/dist/utils/quality-gates.d.ts +0 -49
  273. package/dist/utils/quality-gates.d.ts.map +0 -1
  274. package/dist/utils/quality-gates.js +0 -402
  275. package/dist/utils/spec-resolver.d.ts +0 -80
  276. package/dist/utils/spec-resolver.d.ts.map +0 -1
  277. package/dist/utils/typescript-detector.d.ts +0 -66
  278. package/dist/utils/typescript-detector.d.ts.map +0 -1
  279. package/dist/utils/yaml-validation.d.ts +0 -32
  280. package/dist/utils/yaml-validation.d.ts.map +0 -1
  281. package/dist/validation/spec-validation.d.ts +0 -43
  282. package/dist/validation/spec-validation.d.ts.map +0 -1
  283. package/dist/waivers-manager.d.ts +0 -167
  284. package/dist/waivers-manager.d.ts.map +0 -1
  285. package/dist/worktree/worktree-manager.d.ts +0 -54
  286. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -0,0 +1,173 @@
1
+ #!/bin/bash
2
+ # Document Frontmatter Check Hook for Claude Code
3
+ # Warns when docs/**/*.md files are written/edited without proper frontmatter.
4
+ # Advisory only — does not block.
5
+ #
6
+ # Validates YAML frontmatter with required fields, authority/status enums,
7
+ # governs requirements for high-authority docs, and verified_at_commit for
8
+ # implementation-state claims.
9
+
10
+ set -euo pipefail
11
+
12
+ INPUT=$(cat)
13
+
14
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
15
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
16
+
17
+ # Only check Write and Edit tools
18
+ if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; then
19
+ exit 0
20
+ fi
21
+
22
+ if [[ -z "$FILE_PATH" ]]; then
23
+ exit 0
24
+ fi
25
+
26
+ # Only check .md files under docs/
27
+ if [[ ! "$FILE_PATH" =~ docs/.*\.md$ ]]; then
28
+ exit 0
29
+ fi
30
+
31
+ # Skip exempt filenames
32
+ BASENAME=$(basename "$FILE_PATH")
33
+ if [[ "$BASENAME" == "README.md" ]] || [[ "$BASENAME" == "INDEX.md" ]] || [[ "$BASENAME" == "index.md" ]] || [[ "$BASENAME" == "00_INDEX.md" ]]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Skip archive and templates directories
38
+ if [[ "$FILE_PATH" =~ docs/archive/ ]] || [[ "$FILE_PATH" =~ docs/templates/ ]]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Skip ephemeral (gitignored, not governed)
43
+ if [[ "$FILE_PATH" =~ docs/ephemeral/ ]]; then
44
+ exit 0
45
+ fi
46
+
47
+ # Check if file exists (Write creates it, Edit modifies it)
48
+ if [[ ! -f "$FILE_PATH" ]]; then
49
+ exit 0
50
+ fi
51
+
52
+ # --- Frontmatter validation ---
53
+
54
+ # V1: Check for frontmatter delimiters
55
+ FIRST_LINE=$(head -1 "$FILE_PATH" 2>/dev/null || echo "")
56
+ if [[ "$FIRST_LINE" != "---" ]]; then
57
+ echo '{
58
+ "hookSpecificOutput": {
59
+ "hookEventName": "PostToolUse",
60
+ "additionalContext": "Doc governance (V1): '"$FILE_PATH"' is missing YAML frontmatter. All docs under docs/ (except README.md, archive/, templates/) must start with --- delimiters containing doc_id, authority, status, title, owner, and updated fields."
61
+ }
62
+ }'
63
+ exit 0
64
+ fi
65
+
66
+ # Extract frontmatter block (between first and second ---)
67
+ FRONTMATTER=$(awk 'NR==1 && /^---$/{found=1; next} found && /^---$/{exit} found{print}' "$FILE_PATH")
68
+
69
+ if [[ -z "$FRONTMATTER" ]]; then
70
+ echo '{
71
+ "hookSpecificOutput": {
72
+ "hookEventName": "PostToolUse",
73
+ "additionalContext": "Doc governance (V1): '"$FILE_PATH"' has opening --- but no closing --- for frontmatter block."
74
+ }
75
+ }'
76
+ exit 0
77
+ fi
78
+
79
+ # V2: Check required fields
80
+ MISSING=""
81
+ for field in doc_id authority status title owner updated; do
82
+ if ! echo "$FRONTMATTER" | grep -q "^${field}:"; then
83
+ MISSING="${MISSING} ${field}"
84
+ fi
85
+ done
86
+
87
+ if [[ -n "$MISSING" ]]; then
88
+ echo '{
89
+ "hookSpecificOutput": {
90
+ "hookEventName": "PostToolUse",
91
+ "additionalContext": "Doc governance (V2): '"$FILE_PATH"' is missing required frontmatter fields:'"$MISSING"'."
92
+ }
93
+ }'
94
+ exit 0
95
+ fi
96
+
97
+ # V2: Check authority value
98
+ AUTHORITY=$(echo "$FRONTMATTER" | grep "^authority:" | head -1 | sed 's/^authority: *//' | tr -d '"' | tr -d "'")
99
+ case "$AUTHORITY" in
100
+ canonical|policy|architecture|adr|spec|roadmap|reference|working|ephemeral)
101
+ ;;
102
+ *)
103
+ echo '{
104
+ "hookSpecificOutput": {
105
+ "hookEventName": "PostToolUse",
106
+ "additionalContext": "Doc governance (V2): '"$FILE_PATH"' has invalid authority '"'"''"$AUTHORITY"''"'"'. Must be one of: canonical, policy, architecture, adr, spec, roadmap, reference, working, ephemeral."
107
+ }
108
+ }'
109
+ exit 0
110
+ ;;
111
+ esac
112
+
113
+ # V2: Check status value
114
+ STATUS=$(echo "$FRONTMATTER" | grep "^status:" | head -1 | sed 's/^status: *//' | tr -d '"' | tr -d "'")
115
+ case "$STATUS" in
116
+ draft|active|implemented|proven|superseded|archived)
117
+ ;;
118
+ *)
119
+ echo '{
120
+ "hookSpecificOutput": {
121
+ "hookEventName": "PostToolUse",
122
+ "additionalContext": "Doc governance (V2): '"$FILE_PATH"' has invalid status '"'"''"$STATUS"''"'"'. Must be one of: draft, active, implemented, proven, superseded, archived."
123
+ }
124
+ }'
125
+ exit 0
126
+ ;;
127
+ esac
128
+
129
+ # V3: Check governs for high-authority docs
130
+ case "$AUTHORITY" in
131
+ canonical|architecture|adr|spec)
132
+ if ! echo "$FRONTMATTER" | grep -q "^governs:"; then
133
+ echo '{
134
+ "hookSpecificOutput": {
135
+ "hookEventName": "PostToolUse",
136
+ "additionalContext": "Doc governance (V3): '"$FILE_PATH"' has authority '"'"''"$AUTHORITY"''"'"' but no governs section. Docs with authority canonical/architecture/adr/spec must declare what they govern (modules, schemas, or specs)."
137
+ }
138
+ }'
139
+ exit 0
140
+ fi
141
+ ;;
142
+ esac
143
+
144
+ # V4: Check verified_at_commit for implementation-state claims
145
+ case "$STATUS" in
146
+ implemented|proven)
147
+ if ! echo "$FRONTMATTER" | grep -q "^verified_at_commit:"; then
148
+ echo '{
149
+ "hookSpecificOutput": {
150
+ "hookEventName": "PostToolUse",
151
+ "additionalContext": "Doc governance (V4): '"$FILE_PATH"' has status '"'"''"$STATUS"''"'"' but no verified_at_commit. Docs claiming implementation state must declare the commit SHA where claims were verified."
152
+ }
153
+ }'
154
+ exit 0
155
+ fi
156
+ ;;
157
+ esac
158
+
159
+ # V5: Check superseded_by for superseded docs
160
+ if [[ "$STATUS" == "superseded" ]]; then
161
+ if ! echo "$FRONTMATTER" | grep -q "^superseded_by:"; then
162
+ echo '{
163
+ "hookSpecificOutput": {
164
+ "hookEventName": "PostToolUse",
165
+ "additionalContext": "Doc governance (V5): '"$FILE_PATH"' has status '"'"'superseded'"'"' but no superseded_by. Superseded docs must declare their replacement doc_id."
166
+ }
167
+ }'
168
+ exit 0
169
+ fi
170
+ fi
171
+
172
+ # All checks passed
173
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # CAWS Protected Paths Guard for Claude Code
3
+ # Blocks direct Write/Edit access to guard code and guard state.
4
+ # @author @darianrosebrook
5
+
6
+ set -euo pipefail
7
+
8
+ INPUT=$(cat)
9
+
10
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
11
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
12
+
13
+ case "$TOOL_NAME" in
14
+ Write|Edit) ;;
15
+ *) exit 0 ;;
16
+ esac
17
+
18
+ if [[ -z "$FILE_PATH" ]]; then
19
+ exit 0
20
+ fi
21
+
22
+ # If you are reading this because a write was blocked, do not edit hook files or
23
+ # strike-state files to bypass a guard. Switch into the correct worktree, fix the
24
+ # active spec scope, or ask the user if the guard itself is wrong.
25
+ case "$FILE_PATH" in
26
+ */.claude/hooks/*)
27
+ echo "BLOCKED: $FILE_PATH is protected." >&2
28
+ echo "Ask the user for permission before editing Claude hook scripts." >&2
29
+ exit 1
30
+ ;;
31
+ */.claude/logs/guard-strikes-*.json)
32
+ echo "BLOCKED: $FILE_PATH is protected guard state." >&2
33
+ echo "Do not reset or edit strike counters to bypass enforcement." >&2
34
+ echo "Switch into the correct worktree, update the active CAWS spec scope, or ask the user for direction instead." >&2
35
+ exit 2
36
+ ;;
37
+ esac
38
+
39
+ exit 0
@@ -29,25 +29,39 @@ fi
29
29
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
30
30
 
31
31
  # Check if we're in a CAWS project
32
- if [[ ! -f "$PROJECT_DIR/.caws/working-spec.yaml" ]]; then
32
+ if [[ ! -f "$PROJECT_DIR/.caws/working-spec.yaml" ]] && [[ ! -d "$PROJECT_DIR/.caws/specs" ]]; then
33
33
  exit 0
34
34
  fi
35
35
 
36
36
  # Check if CAWS CLI is available
37
37
  if ! command -v caws &> /dev/null; then
38
- # Suggest installing CAWS
39
38
  echo '{
40
39
  "hookSpecificOutput": {
41
40
  "hookEventName": "PostToolUse",
42
- "additionalContext": "CAWS CLI not available. Consider installing with: npm install -g @caws/cli"
41
+ "additionalContext": "CAWS CLI not available. Consider installing with: npm install -g @paths.design/caws-cli"
43
42
  }
44
43
  }'
45
44
  exit 0
46
45
  fi
47
46
 
48
- # Run CAWS quality gates in quiet mode for quick feedback
49
- if caws quality-gates --context=commit --quiet 2>/dev/null; then
50
- # Quality check passed - provide positive feedback
47
+ # Run quality gates via the unified pipeline
48
+ RESULT=$(caws gates run --context=edit --file "$FILE_PATH" --json --quiet 2>&1) || GATE_EXIT=$?
49
+
50
+ if [ -z "$RESULT" ]; then
51
+ # No output — gates command not available or errored
52
+ echo '{
53
+ "hookSpecificOutput": {
54
+ "hookEventName": "PostToolUse",
55
+ "additionalContext": "Quality gates did not produce output (exit '"${GATE_EXIT:-0}"'). Run '\''caws gates run'\'' for details."
56
+ }
57
+ }'
58
+ exit 0
59
+ fi
60
+
61
+ # Check if gates passed
62
+ PASSED=$(echo "$RESULT" | jq -r '.passed // true' 2>/dev/null)
63
+
64
+ if [ "$PASSED" = "true" ]; then
51
65
  echo '{
52
66
  "hookSpecificOutput": {
53
67
  "hookEventName": "PostToolUse",
@@ -55,13 +69,12 @@ if caws quality-gates --context=commit --quiet 2>/dev/null; then
55
69
  }
56
70
  }'
57
71
  else
58
- # Quality check failed - provide feedback to Claude
59
- # Run again to get violations summary
60
- VIOLATIONS=$(caws quality-gates --context=commit --json 2>/dev/null | jq -r '.violations[:3] | .[] | "- \(.gate): \(.message)"' 2>/dev/null || echo "Run 'caws quality-gates' for details")
72
+ # Extract top 3 gate failure messages
73
+ VIOLATIONS=$(echo "$RESULT" | jq -r '[.gates[] | select(.status == "fail") | "- \(.name): \(.messages[0] // "failed")"] | .[0:3] | .[]' 2>/dev/null || echo "Run 'caws gates run' for details")
61
74
 
62
75
  echo '{
63
76
  "decision": "block",
64
- "reason": "Quality gate violations detected. Please address the following issues before continuing:\n'"$VIOLATIONS"'\n\nRun '\''caws quality-gates'\'' for full details."
77
+ "reason": "Quality gate violations detected. Please address the following issues before continuing:\n'"$VIOLATIONS"'\n\nRun '\''caws gates run'\'' for full details."
65
78
  }'
66
79
  fi
67
80
 
@@ -123,27 +123,28 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
123
123
  }
124
124
  " 2>&1)
125
125
 
126
+
127
+ if [[ "$LITE_CHECK" == error:* ]]; then
128
+ ERROR_MSG="${LITE_CHECK#error:}"
129
+ echo "BLOCKED: Scope check failed — cannot verify file is in scope" >&2
130
+ echo " Error: $ERROR_MSG" >&2
131
+ exit 2
132
+ fi
133
+
126
134
  if [[ "$LITE_CHECK" == banned:* ]]; then
127
135
  PATTERN="${LITE_CHECK#banned:}"
128
- echo '{
129
- "hookSpecificOutput": {
130
- "hookEventName": "PreToolUse",
131
- "permissionDecision": "ask",
132
- "permissionDecisionReason": "This file ('"$REL_PATH"') matches a banned pattern ('"$PATTERN"') in .caws/scope.json. Creating files with this pattern is blocked to prevent file sprawl."
133
- }
134
- }'
135
- exit 0
136
+ echo "BLOCKED: $REL_PATH matches banned pattern ($PATTERN) in .caws/scope.json"
137
+ echo " Scope allows: files not matching banned patterns"
138
+ echo " To modify scope, update bannedPatterns in .caws/scope.json"
139
+ exit 2
136
140
  fi
137
141
 
138
142
  if [[ "$LITE_CHECK" == "not_allowed" ]]; then
139
- echo '{
140
- "hookSpecificOutput": {
141
- "hookEventName": "PreToolUse",
142
- "permissionDecision": "ask",
143
- "permissionDecisionReason": "This file ('"$REL_PATH"') is outside the allowed directories in .caws/scope.json. Please confirm this edit is intentional."
144
- }
145
- }'
146
- exit 0
143
+ ALLOWED_DIRS=$(node -e "const s=JSON.parse(require('fs').readFileSync('$SCOPE_FILE','utf8')); console.log((s.allowedDirectories||[]).join(', '))" 2>/dev/null || echo "unknown")
144
+ echo "BLOCKED: $REL_PATH is outside allowed directories"
145
+ echo " Scope allows: $ALLOWED_DIRS"
146
+ echo " To modify scope, update allowedDirectories in .caws/scope.json"
147
+ exit 2
147
148
  fi
148
149
 
149
150
  # File is allowed - exit normally
@@ -203,31 +204,83 @@ if command -v node >/dev/null 2>&1; then
203
204
  process.exit(0);
204
205
  }
205
206
 
206
- // Collect all active specs (working-spec + feature specs)
207
- const specs = [];
207
+ const projectDir = '$PROJECT_DIR';
208
208
 
209
- // Load working-spec.yaml if present
210
- const mainSpec = '$SPEC_FILE';
211
- if (fs.existsSync(mainSpec)) {
212
- try {
213
- const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
214
- if (s && !TERMINAL.has(s.status)) {
215
- specs.push({ source: 'working-spec', spec: s });
216
- }
217
- } catch (_) {}
209
+ // --- Authoritative spec detection ---
210
+ // If we are inside a worktree with a bound specId, ONLY check that spec.
211
+ // This prevents unrelated specs from blocking writes via broad scope.out.
212
+ let authoritativeSpec = null;
213
+ let mode = 'union';
214
+
215
+ const registryPath = path.join(projectDir, '.caws', 'worktrees.json');
216
+ const cwd = process.cwd();
217
+ const worktreesBase = path.join(projectDir, '.caws', 'worktrees');
218
+
219
+ if (cwd.startsWith(worktreesBase + '/')) {
220
+ const relative = cwd.slice(worktreesBase.length + 1);
221
+ const worktreeName = relative.split('/')[0];
222
+
223
+ if (worktreeName && fs.existsSync(registryPath)) {
224
+ try {
225
+ const reg = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
226
+ const entry = reg.worktrees && reg.worktrees[worktreeName];
227
+
228
+ if (entry && entry.specId) {
229
+ // Try to load the bound spec
230
+ const specsDir = '$SPECS_DIR';
231
+ const specCandidates = [
232
+ path.join(specsDir, entry.specId + '.yaml'),
233
+ path.join(specsDir, entry.specId + '.yml'),
234
+ ];
235
+ for (const candidate of specCandidates) {
236
+ if (fs.existsSync(candidate)) {
237
+ try {
238
+ const s = yaml.load(fs.readFileSync(candidate, 'utf8'));
239
+ if (s && !TERMINAL.has(s.status)) {
240
+ // Verify mutual binding: spec must also reference this worktree
241
+ if (s.worktree === worktreeName) {
242
+ authoritativeSpec = { source: path.basename(candidate), spec: s };
243
+ mode = 'authoritative';
244
+ }
245
+ }
246
+ } catch (_) {}
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ } catch (_) {}
252
+ }
218
253
  }
219
254
 
220
- // Load feature specs from .caws/specs/
221
- const specsDir = '$SPECS_DIR';
222
- if (fs.existsSync(specsDir)) {
223
- for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
255
+ // --- Collect specs based on mode ---
256
+ const specs = [];
257
+
258
+ if (authoritativeSpec) {
259
+ // Authoritative: only the bound spec matters
260
+ specs.push(authoritativeSpec);
261
+ } else {
262
+ // Union: load all active specs
263
+ const mainSpec = '$SPEC_FILE';
264
+ if (fs.existsSync(mainSpec)) {
224
265
  try {
225
- const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
266
+ const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
226
267
  if (s && !TERMINAL.has(s.status)) {
227
- specs.push({ source: f, spec: s });
268
+ specs.push({ source: 'working-spec', spec: s });
228
269
  }
229
270
  } catch (_) {}
230
271
  }
272
+
273
+ const specsDir = '$SPECS_DIR';
274
+ if (fs.existsSync(specsDir)) {
275
+ for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
276
+ try {
277
+ const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
278
+ if (s && !TERMINAL.has(s.status)) {
279
+ specs.push({ source: f, spec: s });
280
+ }
281
+ } catch (_) {}
282
+ }
283
+ }
231
284
  }
232
285
 
233
286
  // No active specs — allow everything
@@ -236,18 +289,18 @@ if command -v node >/dev/null 2>&1; then
236
289
  process.exit(0);
237
290
  }
238
291
 
239
- // Check scope.out across ALL active specs — any match blocks
292
+ // Check scope.out — any match blocks
240
293
  for (const { source, spec } of specs) {
241
294
  for (const pattern of (spec.scope?.out || [])) {
242
295
  const regex = globToRegex(pattern);
243
296
  if (regex.test(filePath)) {
244
- console.log('out_of_scope:' + source + ':' + pattern);
297
+ console.log('out_of_scope:' + mode + ':' + source + ':' + pattern);
245
298
  process.exit(0);
246
299
  }
247
300
  }
248
301
  }
249
302
 
250
- // Union all scope.in patterns — file must match at least one
303
+ // scope.in — file must match at least one pattern
251
304
  const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
252
305
  if (allInScope.length > 0) {
253
306
  let found = false;
@@ -259,7 +312,7 @@ if command -v node >/dev/null 2>&1; then
259
312
  }
260
313
  }
261
314
  if (!found) {
262
- console.log('not_in_scope');
315
+ console.log('not_in_scope:' + mode);
263
316
  process.exit(0);
264
317
  }
265
318
  }
@@ -270,29 +323,57 @@ if command -v node >/dev/null 2>&1; then
270
323
  }
271
324
  " 2>&1)
272
325
 
326
+
327
+ if [[ "$SCOPE_CHECK" == error:* ]]; then
328
+ ERROR_MSG="${SCOPE_CHECK#error:}"
329
+ echo "BLOCKED: Scope check failed — cannot verify file is in scope" >&2
330
+ echo " Error: $ERROR_MSG" >&2
331
+ echo " Fix the spec file or scope configuration before editing files" >&2
332
+ exit 2
333
+ fi
334
+
273
335
  if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
274
336
  DETAIL="${SCOPE_CHECK#out_of_scope:}"
275
- SOURCE="${DETAIL%%:*}"
276
- PATTERN="${DETAIL#*:}"
277
- echo '{
278
- "hookSpecificOutput": {
279
- "hookEventName": "PreToolUse",
280
- "permissionDecision": "ask",
281
- "permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in '"$SOURCE"' (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
282
- }
283
- }'
284
- exit 0
337
+ # Format: mode:source:pattern
338
+ MODE="${DETAIL%%:*}"
339
+ REST="${DETAIL#*:}"
340
+ SOURCE="${REST%%:*}"
341
+ PATTERN="${REST#*:}"
342
+ echo "BLOCKED: $REL_PATH is excluded by scope.out in $SOURCE (pattern: $PATTERN)"
343
+ if [[ "$MODE" == "union" ]]; then
344
+ echo " Mode: union (no authoritative spec bound to this worktree)"
345
+ echo " The scope guard is checking ALL active specs because the worktree<->spec"
346
+ echo " binding is missing. An unrelated spec may be blocking this edit."
347
+ echo " Fix: caws worktree bind <your-spec-id>"
348
+ echo " Diagnose: caws scope show"
349
+ else
350
+ echo " Mode: authoritative (checking only your bound spec)"
351
+ echo " To modify scope, update the spec's scope.out field"
352
+ fi
353
+ exit 2
285
354
  fi
286
355
 
356
+ if [[ "$SCOPE_CHECK" == not_in_scope:* ]]; then
357
+ MODE="${SCOPE_CHECK#not_in_scope:}"
358
+ echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
359
+ if [[ "$MODE" == "union" ]]; then
360
+ echo " Mode: union (no authoritative spec bound to this worktree)"
361
+ echo " The scope guard is checking ALL active specs because the worktree<->spec"
362
+ echo " binding is missing. Your file may be in a scope that no spec covers."
363
+ echo " Fix: caws worktree bind <your-spec-id>"
364
+ echo " Diagnose: caws scope show"
365
+ else
366
+ echo " Mode: authoritative (checking only your bound spec)"
367
+ echo " To modify scope, update the spec's scope.in field"
368
+ fi
369
+ exit 2
370
+ fi
371
+
372
+ # Legacy fallback for unqualified not_in_scope (shouldn't happen with updated logic)
287
373
  if [[ "$SCOPE_CHECK" == "not_in_scope" ]]; then
288
- echo '{
289
- "hookSpecificOutput": {
290
- "hookEventName": "PreToolUse",
291
- "permissionDecision": "ask",
292
- "permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of any active spec. Editing it may cause scope creep. Please confirm this edit is intentional."
293
- }
294
- }'
295
- exit 0
374
+ echo "BLOCKED: $REL_PATH is not in the defined scope.in of any active spec"
375
+ echo " Diagnose: caws scope show"
376
+ exit 2
296
377
  fi
297
378
  fi
298
379
 
@@ -40,7 +40,7 @@ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1;
40
40
  WT_INFO=$(node -e "
41
41
  try {
42
42
  var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
43
- var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
43
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
44
44
  if (active.length > 0) {
45
45
  var names = active.map(function(w) { return w.name + ' (' + w.branch + ')'; });
46
46
  console.log(active.length + ':' + names.join(', '));
@@ -58,7 +58,7 @@ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1;
58
58
  BASE_BRANCH=$(node -e "
59
59
  try {
60
60
  var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
61
- var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
61
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
62
62
  if (active.length > 0) console.log(active[0].baseBranch || '');
63
63
  else console.log('');
64
64
  } catch(e) { console.log(''); }
@@ -548,13 +548,86 @@ handle_pre_compact() {
548
548
  generate_session_output "$(resolve_transcript)"
549
549
  }
550
550
 
551
+ # ============================================================
552
+ # Agent registry heartbeat — register this agent with CAWS
553
+ # ============================================================
554
+ AGENTS_REGISTRY="${CWD}/.caws/agents.json"
555
+
556
+ heartbeat_agent() {
557
+ [ "$SESSION_ID" = "unknown" ] && return
558
+ [ ! -d "${CWD}/.caws" ] && return
559
+
560
+ local model_val
561
+ model_val=$(echo "$INPUT" | jq -r '.model // "unknown"' 2>/dev/null)
562
+
563
+ local registry
564
+ if [ -f "$AGENTS_REGISTRY" ]; then
565
+ registry=$(cat "$AGENTS_REGISTRY" 2>/dev/null || echo '{"version":1,"agents":{}}')
566
+ else
567
+ registry='{"version":1,"agents":{}}'
568
+ fi
569
+
570
+ registry=$(echo "$registry" | python3 -c "
571
+ import json, sys
572
+ from datetime import datetime, timedelta, timezone
573
+
574
+ TTL = timedelta(minutes=30)
575
+ now = datetime.now(timezone.utc)
576
+ sid = '$SESSION_ID'
577
+ model = '$model_val'
578
+
579
+ data = json.load(sys.stdin)
580
+ agents = data.get('agents', {})
581
+
582
+ pruned = {}
583
+ for k, entry in agents.items():
584
+ try:
585
+ last = datetime.fromisoformat(entry['lastSeen'].replace('Z', '+00:00'))
586
+ if now - last < TTL:
587
+ pruned[k] = entry
588
+ except (KeyError, ValueError):
589
+ pass
590
+
591
+ existing = pruned.get(sid, {})
592
+ pruned[sid] = {
593
+ 'sessionId': sid,
594
+ 'platform': 'claude-code',
595
+ 'model': model if model != 'unknown' else existing.get('model'),
596
+ 'specId': existing.get('specId'),
597
+ 'ttl': 1800000,
598
+ 'firstSeen': existing.get('firstSeen', now.strftime('%Y-%m-%dT%H:%M:%SZ')),
599
+ 'lastSeen': now.strftime('%Y-%m-%dT%H:%M:%SZ'),
600
+ }
601
+
602
+ data['agents'] = pruned
603
+ json.dump(data, sys.stdout, indent=2)
604
+ " 2>/dev/null)
605
+
606
+ [ -n "$registry" ] && echo "$registry" > "$AGENTS_REGISTRY"
607
+ }
608
+
609
+ remove_agent() {
610
+ [ "$SESSION_ID" = "unknown" ] && return
611
+ [ ! -f "$AGENTS_REGISTRY" ] && return
612
+
613
+ python3 -c "
614
+ import json
615
+ sid = '$SESSION_ID'
616
+ with open('$AGENTS_REGISTRY', 'r') as f:
617
+ data = json.load(f)
618
+ data.get('agents', {}).pop(sid, None)
619
+ with open('$AGENTS_REGISTRY', 'w') as f:
620
+ json.dump(data, f, indent=2)
621
+ " 2>/dev/null || true
622
+ }
623
+
551
624
  # ============================================================
552
625
  # DISPATCH
553
626
  # ============================================================
554
627
  case "$HOOK_EVENT" in
555
- SessionStart) handle_session_start ;;
556
- Stop) handle_stop ;;
557
- PreCompact) handle_pre_compact ;;
628
+ SessionStart) handle_session_start; heartbeat_agent ;;
629
+ Stop) handle_stop; remove_agent ;;
630
+ PreCompact) handle_pre_compact; heartbeat_agent ;;
558
631
  *) ;; # Other events: no-op
559
632
  esac
560
633
 
@@ -25,7 +25,7 @@ if [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>
25
25
  ACTIVE_INFO=$(node -e "
26
26
  try {
27
27
  var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
28
- var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
28
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active' || w.status === 'fresh' || w.status === 'merged'; });
29
29
  if (active.length > 0) {
30
30
  console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(', '));
31
31
  } else {