specweave 1.0.31 → 1.0.33

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 (123) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CLAUDE.md +205 -148
  3. package/README.md +0 -2
  4. package/bin/specweave.js +11 -0
  5. package/dist/src/cli/commands/init.js +1 -1
  6. package/dist/src/cli/commands/init.js.map +1 -1
  7. package/dist/src/cli/commands/update-instructions.d.ts +16 -0
  8. package/dist/src/cli/commands/update-instructions.d.ts.map +1 -0
  9. package/dist/src/cli/commands/update-instructions.js +134 -0
  10. package/dist/src/cli/commands/update-instructions.js.map +1 -0
  11. package/dist/src/cli/helpers/init/directory-structure.d.ts +28 -1
  12. package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
  13. package/dist/src/cli/helpers/init/directory-structure.js +163 -33
  14. package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
  15. package/dist/src/cli/helpers/init/index.d.ts +2 -1
  16. package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
  17. package/dist/src/cli/helpers/init/index.js +3 -1
  18. package/dist/src/cli/helpers/init/index.js.map +1 -1
  19. package/dist/src/cli/helpers/init/instruction-file-merger.d.ts +23 -0
  20. package/dist/src/cli/helpers/init/instruction-file-merger.d.ts.map +1 -0
  21. package/dist/src/cli/helpers/init/instruction-file-merger.js +243 -0
  22. package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -0
  23. package/dist/src/cli/helpers/init/plugin-installer.js +49 -0
  24. package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
  25. package/dist/src/config/types.d.ts +2 -2
  26. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +26 -0
  27. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -1
  28. package/dist/src/core/living-docs/external-sync-orchestrator.js +61 -0
  29. package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -1
  30. package/dist/src/core/living-docs/scaffolding/index.d.ts +12 -0
  31. package/dist/src/core/living-docs/scaffolding/index.d.ts.map +1 -0
  32. package/dist/src/core/living-docs/scaffolding/index.js +15 -0
  33. package/dist/src/core/living-docs/scaffolding/index.js.map +1 -0
  34. package/dist/src/core/living-docs/scaffolding/merger.d.ts +183 -0
  35. package/dist/src/core/living-docs/scaffolding/merger.d.ts.map +1 -0
  36. package/dist/src/core/living-docs/scaffolding/merger.js +523 -0
  37. package/dist/src/core/living-docs/scaffolding/merger.js.map +1 -0
  38. package/dist/src/core/living-docs/scaffolding/scaffold.d.ts +102 -0
  39. package/dist/src/core/living-docs/scaffolding/scaffold.d.ts.map +1 -0
  40. package/dist/src/core/living-docs/scaffolding/scaffold.js +346 -0
  41. package/dist/src/core/living-docs/scaffolding/scaffold.js.map +1 -0
  42. package/dist/src/core/living-docs/scaffolding/template-engine.d.ts +108 -0
  43. package/dist/src/core/living-docs/scaffolding/template-engine.d.ts.map +1 -0
  44. package/dist/src/core/living-docs/scaffolding/template-engine.js +204 -0
  45. package/dist/src/core/living-docs/scaffolding/template-engine.js.map +1 -0
  46. package/dist/src/core/living-docs/sync-helpers/generators.d.ts +38 -2
  47. package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
  48. package/dist/src/core/living-docs/sync-helpers/generators.js +65 -10
  49. package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
  50. package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
  51. package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
  52. package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
  53. package/dist/src/core/tools/index.d.ts +11 -0
  54. package/dist/src/core/tools/index.d.ts.map +1 -0
  55. package/dist/src/core/tools/index.js +10 -0
  56. package/dist/src/core/tools/index.js.map +1 -0
  57. package/dist/src/core/tools/tool-event-bus.d.ts +33 -0
  58. package/dist/src/core/tools/tool-event-bus.d.ts.map +1 -0
  59. package/dist/src/core/tools/tool-event-bus.js +84 -0
  60. package/dist/src/core/tools/tool-event-bus.js.map +1 -0
  61. package/dist/src/core/tools/tool-index-builder.d.ts +27 -0
  62. package/dist/src/core/tools/tool-index-builder.d.ts.map +1 -0
  63. package/dist/src/core/tools/tool-index-builder.js +289 -0
  64. package/dist/src/core/tools/tool-index-builder.js.map +1 -0
  65. package/dist/src/core/tools/tool-registry.d.ts +51 -0
  66. package/dist/src/core/tools/tool-registry.d.ts.map +1 -0
  67. package/dist/src/core/tools/tool-registry.js +224 -0
  68. package/dist/src/core/tools/tool-registry.js.map +1 -0
  69. package/dist/src/core/tools/tool-search-engine.d.ts +22 -0
  70. package/dist/src/core/tools/tool-search-engine.d.ts.map +1 -0
  71. package/dist/src/core/tools/tool-search-engine.js +174 -0
  72. package/dist/src/core/tools/tool-search-engine.js.map +1 -0
  73. package/dist/src/core/tools/types/tool-registry-types.d.ts +112 -0
  74. package/dist/src/core/tools/types/tool-registry-types.d.ts.map +1 -0
  75. package/dist/src/core/tools/types/tool-registry-types.js +7 -0
  76. package/dist/src/core/tools/types/tool-registry-types.js.map +1 -0
  77. package/dist/src/init/compliance/types.d.ts +1 -1
  78. package/package.json +1 -1
  79. package/plugins/specweave/hooks/hooks.json +3 -13
  80. package/plugins/specweave/hooks/lib/common-setup.sh +47 -321
  81. package/plugins/specweave/hooks/lib/migrate-increment-work.sh +5 -5
  82. package/plugins/specweave/hooks/lib/sync-spec-content.sh +5 -5
  83. package/plugins/specweave/hooks/universal/dispatcher.mjs +4 -5
  84. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +43 -296
  85. package/plugins/specweave/hooks/universal/hook-wrapper.sh +3 -1
  86. package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
  87. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +2 -2
  88. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -10
  89. package/plugins/specweave/hooks/v2/guards/completion-guard.sh +12 -29
  90. package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +27 -29
  91. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +10 -4
  92. package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +139 -0
  93. package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +4 -2
  94. package/plugins/specweave/hooks/v2/session-end.sh +3 -1
  95. package/plugins/specweave/hooks/v2/session-start.sh +3 -1
  96. package/plugins/specweave/skills/increment-planner/templates/plan.md +14 -0
  97. package/plugins/specweave/skills/update-instructions/SKILL.md +80 -0
  98. package/plugins/specweave-ado/hooks/post-living-docs-update.sh +1 -1
  99. package/plugins/specweave-mobile/README.md +55 -35
  100. package/plugins/specweave-mobile/agents/mobile-architect/AGENT.md +805 -329
  101. package/plugins/specweave-mobile/skills/expo-workflow/SKILL.md +226 -9
  102. package/plugins/specweave-mobile/skills/native-modules/SKILL.md +221 -20
  103. package/plugins/specweave-mobile/skills/performance-optimization/SKILL.md +186 -14
  104. package/plugins/specweave-mobile/skills/react-native-setup/SKILL.md +151 -54
  105. package/plugins/specweave-release/commands/npm.md +61 -17
  106. package/plugins/specweave-release/hooks/post-task-completion.sh +2 -3
  107. package/src/templates/AGENTS.md.template +34 -0
  108. package/src/templates/CLAUDE.md.template +121 -155
  109. package/plugins/specweave/hooks/config-env-separator.sh +0 -99
  110. package/plugins/specweave/hooks/github-metadata-guard.sh +0 -73
  111. package/plugins/specweave/hooks/lib/circuit-breaker.sh +0 -381
  112. package/plugins/specweave/hooks/lib/crash-prevention.sh +0 -336
  113. package/plugins/specweave/hooks/lib/logging.sh +0 -231
  114. package/plugins/specweave/hooks/lib/metrics.sh +0 -347
  115. package/plugins/specweave/hooks/lib/semaphore.sh +0 -216
  116. package/plugins/specweave/hooks/project-folder-guard.sh +0 -274
  117. package/plugins/specweave/hooks/spec-project-validator.sh +0 -210
  118. package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +0 -212
  119. package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +0 -163
  120. package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +0 -51
  121. package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +0 -63
  122. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +0 -335
  123. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +0 -406
@@ -1,274 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Project Folder Guard (Pre-tool-use Hook)
3
- # Prevents creation of project folders unless project exists in config.json
4
- #
5
- # Triggers: Write tool creating files in .specweave/docs/internal/specs/{project}/
6
- # Blocks: Project folders not in config.json multiProject.projects
7
- # Version: 0.35.1+ (Enhanced with example project detection)
8
- #
9
- # CRITICAL v0.35.1: Added detection of common example project names
10
- # These are NEVER allowed unless explicitly configured:
11
- # - frontend-app, backend-api, mobile-app, shared-lib (common examples)
12
- # - acme-corp, my-app, myapp (placeholder names)
13
- # - Comma-separated values like "frontend-app, backend-api"
14
- # - Placeholder patterns like {{PROJECT_ID}}
15
-
16
- set -euo pipefail
17
-
18
- # Read stdin for tool input (Claude Code passes JSON via stdin)
19
- INPUT=$(cat)
20
-
21
- # Extract tool name - Claude Code passes it at top level
22
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
23
-
24
- # Only check Write tool operations to specs/ folder
25
- if [ "$TOOL_NAME" != "Write" ]; then
26
- exit 0
27
- fi
28
-
29
- # Extract file_path from tool_input (correct structure for Claude Code hooks)
30
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null || echo "")
31
-
32
- if [ -z "$FILE_PATH" ]; then
33
- exit 0
34
- fi
35
-
36
- # Check if writing to specs folder
37
- if [[ ! "$FILE_PATH" =~ \.specweave/docs/internal/specs/([^/]+)/ ]]; then
38
- exit 0
39
- fi
40
-
41
- # Extract project name from path
42
- PROJECT_NAME="${BASH_REMATCH[1]}"
43
-
44
- # Skip validation for README.md in specs root
45
- if [ "$PROJECT_NAME" = "README.md" ]; then
46
- exit 0
47
- fi
48
-
49
- # CRITICAL v0.35.1: Check for template placeholders like {{PROJECT_ID}}
50
- if [[ "$PROJECT_NAME" =~ \{\{.*\}\} ]]; then
51
- echo "❌ BLOCKED: Unresolved placeholder detected: '$PROJECT_NAME'"
52
- echo ""
53
- echo " Project name contains template placeholder syntax: {{ ... }}"
54
- echo " This is NOT a valid project name."
55
- echo ""
56
- echo " 🔧 Edit spec.md and replace {{...}} with your actual project name"
57
- exit 1
58
- fi
59
-
60
- # CRITICAL v0.35.1: Check for comma-separated values (invalid format)
61
- if [[ "$PROJECT_NAME" =~ , ]]; then
62
- echo "❌ BLOCKED: Comma-separated project names detected: '$PROJECT_NAME'"
63
- echo ""
64
- echo " Each User Story must have exactly ONE project."
65
- echo " Multiple comma-separated projects are NOT allowed."
66
- echo ""
67
- echo " 🔧 Split into separate User Stories, one per project"
68
- exit 1
69
- fi
70
-
71
- # CRITICAL v0.35.1: Check for parentheses (often seen in examples)
72
- if [[ "$PROJECT_NAME" =~ \( || "$PROJECT_NAME" =~ \) ]]; then
73
- echo "❌ BLOCKED: Invalid characters in project name: '$PROJECT_NAME'"
74
- echo ""
75
- echo " Project names cannot contain parentheses."
76
- echo " This looks like an example/documentation string."
77
- echo ""
78
- echo " 🔧 Use a valid kebab-case project name (e.g., my-project)"
79
- exit 1
80
- fi
81
-
82
- # CRITICAL v0.35.1: Check ProjectRegistry FIRST (.specweave/state/projects.json)
83
- # This is the single source of truth for projects (v0.35.0+)
84
- REGISTRY_FILE=".specweave/state/projects.json"
85
- CONFIG_FILE=".specweave/config.json"
86
-
87
- # CRITICAL v0.35.1: List of known example project names
88
- # These are blocked UNLESS explicitly registered
89
- EXAMPLE_PROJECTS="frontend-app backend-api mobile-app shared-lib acme-corp my-app myapp example-project sample-project test-project demo placeholder per default"
90
-
91
- # Check if project is in example list (case-insensitive)
92
- PROJECT_NAME_LOWER=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]')
93
- IS_EXAMPLE="false"
94
- for EXAMPLE in $EXAMPLE_PROJECTS; do
95
- if [ "$PROJECT_NAME_LOWER" = "$EXAMPLE" ]; then
96
- IS_EXAMPLE="true"
97
- break
98
- fi
99
- done
100
-
101
- # PRIMARY: Check ProjectRegistry (projects.json)
102
- if [ -f "$REGISTRY_FILE" ]; then
103
- # Get all project IDs from registry
104
- REGISTRY_PROJECTS=$(jq -r '.projects | keys[]' "$REGISTRY_FILE" 2>/dev/null || echo "")
105
-
106
- if [ -n "$REGISTRY_PROJECTS" ]; then
107
- # Check if project exists in registry (case-insensitive)
108
- PROJECT_IN_REGISTRY="false"
109
- for REG_PROJECT in $REGISTRY_PROJECTS; do
110
- REG_PROJECT_LOWER=$(echo "$REG_PROJECT" | tr '[:upper:]' '[:lower:]')
111
- if [ "$PROJECT_NAME_LOWER" = "$REG_PROJECT_LOWER" ]; then
112
- PROJECT_IN_REGISTRY="true"
113
- break
114
- fi
115
- done
116
-
117
- if [ "$PROJECT_IN_REGISTRY" = "true" ]; then
118
- # Project exists in registry - ALLOW
119
- exit 0
120
- fi
121
-
122
- # Project NOT in registry
123
- VALID_PROJECTS=$(echo "$REGISTRY_PROJECTS" | tr '\n' ', ' | sed 's/,$//')
124
-
125
- if [ "$IS_EXAMPLE" = "true" ]; then
126
- echo "❌ BLOCKED: Example project name detected: '$PROJECT_NAME'"
127
- echo ""
128
- echo " '$PROJECT_NAME' is a common example/placeholder name used in documentation."
129
- echo " It was likely extracted from example User Stories in spec.md."
130
- echo ""
131
- echo " ⚠️ This is NOT a real project - it's a documentation example!"
132
- echo ""
133
- echo " Registered projects: $VALID_PROJECTS"
134
- echo ""
135
- echo " 🔧 How to fix:"
136
- echo " 1. Edit the spec.md file and update **Project**: fields"
137
- echo " 2. Replace example names with a registered project"
138
- echo " 3. Re-run the sync command"
139
- else
140
- echo "❌ BLOCKED: Project '$PROJECT_NAME' not in registry"
141
- echo ""
142
- echo " Project '$PROJECT_NAME' is NOT registered in .specweave/state/projects.json"
143
- echo ""
144
- echo " Registered projects: $VALID_PROJECTS"
145
- echo ""
146
- echo " 🔧 To add this project:"
147
- echo " specweave project add $PROJECT_NAME --name \"Project Display Name\""
148
- fi
149
- exit 1
150
- fi
151
- fi
152
-
153
- # FALLBACK: Check config.json if registry doesn't exist or is empty
154
- if [ ! -f "$CONFIG_FILE" ]; then
155
- echo "❌ ERROR: Neither projects.json nor config.json found"
156
- echo " Cannot validate project folder creation without configuration"
157
- exit 1
158
- fi
159
-
160
- # Check if multi-project mode is enabled
161
- MULTI_PROJECT_ENABLED=$(jq -r '.multiProject.enabled // false' "$CONFIG_FILE")
162
-
163
- if [ "$MULTI_PROJECT_ENABLED" != "true" ]; then
164
- # Single-project mode - only allow configured project name
165
- ALLOWED_PROJECT=$(jq -r '.project.name // "specweave"' "$CONFIG_FILE")
166
-
167
- if [ "$PROJECT_NAME" != "$ALLOWED_PROJECT" ]; then
168
- # Special message for example projects
169
- if [ "$IS_EXAMPLE" = "true" ]; then
170
- echo "❌ BLOCKED: Example project name detected: '$PROJECT_NAME'"
171
- echo ""
172
- echo " '$PROJECT_NAME' is a common example/placeholder name used in documentation."
173
- echo " It was likely extracted from example User Stories in spec.md."
174
- echo ""
175
- echo " ⚠️ This is NOT a real project - it's a documentation example!"
176
- echo ""
177
- echo " 🔧 How to fix:"
178
- echo " 1. Edit the spec.md file and update **Project**: fields"
179
- echo " 2. Replace example names with: $ALLOWED_PROJECT"
180
- echo " 3. Re-run the sync command"
181
- echo ""
182
- echo " 📋 Example fix in spec.md:"
183
- echo " BEFORE: **Project**: $PROJECT_NAME ← EXAMPLE (WRONG)"
184
- echo " AFTER: **Project**: $ALLOWED_PROJECT ← Your actual project"
185
- echo ""
186
- else
187
- echo "❌ BLOCKED: Project folder creation for '$PROJECT_NAME'"
188
- echo ""
189
- echo " This repository is in SINGLE-PROJECT mode."
190
- echo " Only folder allowed: $ALLOWED_PROJECT"
191
- echo ""
192
- echo " Current config (.specweave/config.json):"
193
- echo " {"
194
- echo " \"project\": { \"name\": \"$ALLOWED_PROJECT\" },"
195
- echo " \"multiProject\": { \"enabled\": false }"
196
- echo " }"
197
- echo ""
198
- echo " ⚠️ If '$PROJECT_NAME' is an example/placeholder in spec.md, update it to:"
199
- echo " **Project**: $ALLOWED_PROJECT"
200
- echo ""
201
- echo " 💡 To add new projects:"
202
- echo " 1. Run: specweave config set multiProject.enabled true"
203
- echo " 2. Run: specweave config set multiProject.projects.$PROJECT_NAME.id \"$PROJECT_NAME\""
204
- echo " 3. Re-run your command"
205
- fi
206
- exit 1
207
- fi
208
-
209
- exit 0
210
- fi
211
-
212
- # Multi-project mode - check if project exists in config
213
- if jq -e ".multiProject.projects.\"$PROJECT_NAME\"" "$CONFIG_FILE" &>/dev/null; then
214
- PROJECT_EXISTS="true"
215
- else
216
- PROJECT_EXISTS="false"
217
- fi
218
-
219
- if [ "$PROJECT_EXISTS" = "false" ]; then
220
- # Get list of valid projects
221
- VALID_PROJECTS=$(jq -r '.multiProject.projects | keys[]' "$CONFIG_FILE" 2>/dev/null | tr '\n' ', ' | sed 's/,$//' || echo "")
222
-
223
- # Special message for example projects
224
- if [ "$IS_EXAMPLE" = "true" ]; then
225
- echo "❌ BLOCKED: Example project name detected: '$PROJECT_NAME'"
226
- echo ""
227
- echo " '$PROJECT_NAME' is a common example/placeholder name used in documentation."
228
- echo " It was likely extracted from example User Stories in spec.md."
229
- echo ""
230
- echo " ⚠️ This is NOT a real project - it's a documentation example!"
231
- echo ""
232
- if [ -n "$VALID_PROJECTS" ]; then
233
- echo " Valid projects: $VALID_PROJECTS"
234
- echo ""
235
- fi
236
- echo " 🔧 How to fix:"
237
- echo " 1. Edit the spec.md file and update **Project**: fields"
238
- echo " 2. Replace example names with a configured project"
239
- echo " 3. Re-run the sync command"
240
- echo ""
241
- else
242
- echo "❌ BLOCKED: Project folder creation for '$PROJECT_NAME'"
243
- echo ""
244
- echo " Project '$PROJECT_NAME' is NOT configured in .specweave/config.json"
245
- echo ""
246
- if [ -n "$VALID_PROJECTS" ]; then
247
- echo " Valid projects: $VALID_PROJECTS"
248
- echo ""
249
- fi
250
- echo " ⚠️ Common causes:"
251
- echo " 1. Example/placeholder project in spec.md User Stories"
252
- echo " 2. Typo in **Project**: field (e.g., 'MyApp (3 repos)' instead of real project)"
253
- echo " 3. Missing project configuration"
254
- echo ""
255
- echo " 🔧 How to fix:"
256
- if [ -n "$VALID_PROJECTS" ]; then
257
- echo " • If this is an EXAMPLE in spec.md, update to a real project:"
258
- echo " **Project**: ${VALID_PROJECTS%%,*} # Use first valid project"
259
- echo ""
260
- fi
261
- echo " • If this is a NEW project, add it to config:"
262
- echo " specweave config set multiProject.projects.$PROJECT_NAME.id \"$PROJECT_NAME\""
263
- echo " specweave config set multiProject.projects.$PROJECT_NAME.name \"Project Display Name\""
264
- echo ""
265
- echo " • Check spec.md for placeholder User Stories like:"
266
- echo " **Project**: frontend-app, backend-api ← FORBIDDEN (comma-separated)"
267
- echo " **Project**: MyApp (3 repos) ← FORBIDDEN (parentheses not allowed)"
268
- echo ""
269
- fi
270
- exit 1
271
- fi
272
-
273
- # Project exists - allow creation
274
- exit 0
@@ -1,210 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # spec-project-validator.sh
4
- #
5
- # Pre-tool-use hook that validates spec.md project/board configuration
6
- # before allowing Write tool to create/update spec.md files.
7
- #
8
- # Activation:
9
- # - tool_name: Write
10
- # - file_path matches: .specweave/increments/*/spec.md
11
- #
12
- # Rules (ADR-0140 v0.35.0+):
13
- # - Frontmatter `project:` and `board:` fields are OPTIONAL (deprecated)
14
- # - Per-US `**Project**:` and `**Board**:` fields are the PRIMARY source
15
- # - Validates no unresolved {{...}} placeholders exist
16
- # - Single-project mode: frontmatter project must match config if present
17
- # - Multi-project mode: validates projects exist in config if referenced
18
- #
19
- # Returns exit code 1 (block) if validation fails, 0 (allow) otherwise.
20
- #
21
- # Bypass: Set SPECWEAVE_FORCE_PROJECT=1 to skip validation
22
-
23
- set -e
24
-
25
- # Check for force bypass
26
- if [ "$SPECWEAVE_FORCE_PROJECT" = "1" ]; then
27
- echo '{"decision": "allow", "message": "⚠️ Project validation bypassed (SPECWEAVE_FORCE_PROJECT=1)"}'
28
- exit 0
29
- fi
30
-
31
- # Read tool input from stdin
32
- INPUT=$(cat)
33
-
34
- # Extract tool name
35
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
36
-
37
- # Only validate Write tool calls
38
- if [ "$TOOL_NAME" != "Write" ]; then
39
- echo '{"decision": "allow"}'
40
- exit 0
41
- fi
42
-
43
- # Extract file path
44
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
45
-
46
- # Only validate spec.md files in increments folder
47
- # Match: 3-4 digits, optional E suffix, kebab-case name, spec.md
48
- if [[ ! "$FILE_PATH" =~ \.specweave/increments/[0-9]{3,4}E?-[^/]+/spec\.md$ ]]; then
49
- echo '{"decision": "allow"}'
50
- exit 0
51
- fi
52
-
53
- # Extract file content
54
- CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
55
-
56
- # Check if content has YAML frontmatter
57
- if [[ ! "$CONTENT" =~ ^---$'\n' ]]; then
58
- echo '{"decision": "block", "reason": "spec.md must have YAML frontmatter (starting with ---)"}'
59
- exit 0
60
- fi
61
-
62
- # Extract YAML frontmatter
63
- FRONTMATTER=$(echo "$CONTENT" | sed -n '/^---$/,/^---$/p' | tail -n +2 | head -n -1)
64
-
65
- # Check for unresolved project placeholder (legacy {{PROJECT_ID}})
66
- if echo "$FRONTMATTER" | grep -q 'project:\s*{{PROJECT_ID}}'; then
67
- echo '{"decision": "block", "reason": "spec.md has unresolved placeholder {{PROJECT_ID}}. Run '\''specweave context projects'\'' to get available projects, then select one."}'
68
- exit 0
69
- fi
70
-
71
- # Check for unresolved board placeholder (legacy {{BOARD_ID}})
72
- if echo "$FRONTMATTER" | grep -q 'board:\s*{{BOARD_ID}}'; then
73
- echo '{"decision": "block", "reason": "spec.md has unresolved placeholder {{BOARD_ID}}. Run '\''specweave context boards --project=<id>'\'' to get available boards, then select one."}'
74
- exit 0
75
- fi
76
-
77
- # Check for ANY unresolved {{...}} placeholder in frontmatter (v0.34.0+)
78
- # This catches {{RESOLVED_PROJECT}}, {{RESOLVED_BOARD}}, and any other placeholders
79
- if echo "$FRONTMATTER" | grep -qE '\{\{[A-Z_]+\}\}'; then
80
- FOUND_PLACEHOLDERS=$(echo "$FRONTMATTER" | grep -oE '\{\{[A-Z_]+\}\}' | tr '\n' ', ' | sed 's/,$//')
81
- echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved placeholders: ${FOUND_PLACEHOLDERS}\\n\\nYOU MUST RESOLVE these BEFORE creating spec.md:\\n1. Run: specweave context projects\\n2. Parse the JSON output to get valid project/board IDs\\n3. Replace placeholders with actual values from step 2\\n\\n❌ FORBIDDEN: Using placeholder templates directly\\n✅ REQUIRED: Resolve ALL placeholders to actual values\"}"
82
- exit 0
83
- fi
84
-
85
- # Check for ANY unresolved {{...}} placeholder in full content (catches per-US **Project**: {{...}})
86
- if echo "$CONTENT" | grep -qE '\*\*Project\*\*:\s*\{\{[A-Z_]+\}\}'; then
87
- FOUND_PLACEHOLDERS=$(echo "$CONTENT" | grep -oE '\*\*Project\*\*:\s*\{\{[A-Z_]+\}\}' | head -1)
88
- echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved **Project**: placeholder\\n\\nFound: ${FOUND_PLACEHOLDERS}\\n\\nEach user story MUST have a resolved **Project**: field.\\n\\n1. Run: specweave context projects\\n2. Get valid project IDs from the JSON output\\n3. Replace the placeholder with an actual project ID\"}"
89
- exit 0
90
- fi
91
-
92
- # Check for unresolved **Board**: placeholders in full content (2-level structures)
93
- if echo "$CONTENT" | grep -qE '\*\*Board\*\*:\s*\{\{[A-Z_]+\}\}'; then
94
- FOUND_PLACEHOLDERS=$(echo "$CONTENT" | grep -oE '\*\*Board\*\*:\s*\{\{[A-Z_]+\}\}' | head -1)
95
- echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved **Board**: placeholder\\n\\nFound: ${FOUND_PLACEHOLDERS}\\n\\nEach user story MUST have a resolved **Board**: field (for 2-level structures).\\n\\n1. Run: specweave context projects\\n2. Get valid board IDs from boardsByProject in the JSON output\\n3. Replace the placeholder with an actual board ID\"}"
96
- exit 0
97
- fi
98
-
99
- # Extract project and board from frontmatter
100
- PROJECT=$(echo "$FRONTMATTER" | grep -E '^project:\s*' | sed 's/^project:\s*//' | tr -d '"'"'" | tr -d '[:space:]')
101
- BOARD=$(echo "$FRONTMATTER" | grep -E '^board:\s*' | sed 's/^board:\s*//' | tr -d '"'"'" | tr -d '[:space:]')
102
-
103
- # Get project root from file path
104
- PROJECT_ROOT="${FILE_PATH%%/.specweave/*}"
105
-
106
- # Change to project root for specweave command
107
- cd "$PROJECT_ROOT" 2>/dev/null || true
108
-
109
- # Check if single-project or multi-project mode (NEW - v0.34.0+)
110
- CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
111
- MULTI_PROJECT_ENABLED=$(jq -r '.multiProject.enabled // false' "$CONFIG_FILE" 2>/dev/null || echo "false")
112
-
113
- # Get project context via CLI command
114
- CONTEXT_OUTPUT=$(specweave context projects 2>/dev/null || echo '{"level": 1, "projects": []}')
115
-
116
- # Parse structure level
117
- STRUCTURE_LEVEL=$(echo "$CONTEXT_OUTPUT" | jq -r '.level // 1')
118
-
119
- # Parse available projects
120
- AVAILABLE_PROJECTS=$(echo "$CONTEXT_OUTPUT" | jq -r '.projects[].id // empty' | tr '\n' ', ' | sed 's/,$//')
121
-
122
- # NEW (v0.34.0+): Single-project mode validation
123
- if [ "$MULTI_PROJECT_ENABLED" = "false" ]; then
124
- # Single-project mode: project field is OPTIONAL
125
- # If provided, validate it matches config.project.name
126
- CONFIGURED_PROJECT=$(jq -r '.project.name // "specweave"' "$CONFIG_FILE" 2>/dev/null)
127
-
128
- if [ -n "$PROJECT" ] && [ "$PROJECT" != "null" ] && [ "$PROJECT" != "$CONFIGURED_PROJECT" ]; then
129
- echo "{\"decision\": \"block\", \"reason\": \"spec.md has project: '${PROJECT}' but config has project.name: '${CONFIGURED_PROJECT}'.\\n\\nIn single-project mode, the project: field MUST match config.project.name OR be omitted.\\n\\nFix:\\n1. Remove project: field (recommended for single-project mode)\\n2. OR change to: project: ${CONFIGURED_PROJECT}\\n3. OR enable multi-project mode: /sw:enable-multiproject\"}"
130
- exit 0
131
- fi
132
-
133
- # ALWAYS block board: field in single-project mode
134
- if [ -n "$BOARD" ] && [ "$BOARD" != "null" ]; then
135
- echo "{\"decision\": \"block\", \"reason\": \"spec.md has board: field but this is a SINGLE-PROJECT repository.\\n\\nThe board: field is ONLY for multi-project mode.\\n\\nFix:\\n1. Remove the board: field\\n2. OR enable multi-project mode: /sw:enable-multiproject\"}"
136
- exit 0
137
- fi
138
-
139
- # All validations passed for single-project mode
140
- echo '{"decision": "allow"}'
141
- exit 0
142
- fi
143
-
144
- # Multi-project mode validation continues below
145
- # ADR-0140 (v0.35.0+): Frontmatter project/board fields are OPTIONAL
146
- # Per-US fields are the PRIMARY source of truth
147
-
148
- # Validation based on structure level
149
- if [ "$STRUCTURE_LEVEL" = "2" ]; then
150
- # 2-level: Frontmatter project/board are OPTIONAL (per-US fields are primary)
151
-
152
- # If project is provided in frontmatter, validate it exists
153
- if [ -n "$PROJECT" ] && [ "$PROJECT" != "null" ]; then
154
- PROJECT_EXISTS=$(echo "$CONTEXT_OUTPUT" | jq --arg proj "$PROJECT" '.projects[] | select(.id == $proj)' 2>/dev/null)
155
- if [ -z "$PROJECT_EXISTS" ]; then
156
- # Try case-insensitive match
157
- PROJECT_EXISTS=$(echo "$CONTEXT_OUTPUT" | jq --arg proj "$PROJECT" '.projects[] | select(.id | ascii_downcase == ($proj | ascii_downcase))' 2>/dev/null)
158
- fi
159
-
160
- if [ -z "$PROJECT_EXISTS" ] && [ -n "$AVAILABLE_PROJECTS" ]; then
161
- echo "{\"decision\": \"block\", \"reason\": \"Frontmatter project '${PROJECT}' not found in configuration.\\n\\nAvailable projects: ${AVAILABLE_PROJECTS}\\n\\nFix:\\n1. Remove project: field (recommended - use per-US **Project**: instead)\\n2. OR update to a valid project\\n3. OR set SPECWEAVE_FORCE_PROJECT=1 to bypass\"}"
162
- exit 0
163
- fi
164
- fi
165
-
166
- # If board is provided in frontmatter, validate it exists under project
167
- if [ -n "$BOARD" ] && [ "$BOARD" != "null" ]; then
168
- # Need project to validate board
169
- if [ -z "$PROJECT" ] || [ "$PROJECT" = "null" ]; then
170
- echo "{\"decision\": \"block\", \"reason\": \"Frontmatter has board: '${BOARD}' but no project: field.\\n\\nIf using board: you must also specify project:.\\n\\nRecommended: Remove both and use per-US **Project**: and **Board**: fields instead.\"}"
171
- exit 0
172
- fi
173
-
174
- BOARDS_OUTPUT=$(specweave context boards --project="$PROJECT" 2>/dev/null || echo '{"boards": []}')
175
- BOARD_EXISTS=$(echo "$BOARDS_OUTPUT" | jq --arg board "$BOARD" '.boards[] | select(.id == $board)' 2>/dev/null)
176
-
177
- if [ -z "$BOARD_EXISTS" ]; then
178
- # Try case-insensitive match
179
- BOARD_EXISTS=$(echo "$BOARDS_OUTPUT" | jq --arg board "$BOARD" '.boards[] | select(.id | ascii_downcase == ($board | ascii_downcase))' 2>/dev/null)
180
- fi
181
-
182
- AVAILABLE_BOARDS=$(echo "$BOARDS_OUTPUT" | jq -r '.boards[].id // empty' | tr '\n' ', ' | sed 's/,$//')
183
-
184
- if [ -z "$BOARD_EXISTS" ] && [ -n "$AVAILABLE_BOARDS" ]; then
185
- echo "{\"decision\": \"block\", \"reason\": \"Frontmatter board '${BOARD}' not found under project '${PROJECT}'.\\n\\nAvailable boards: ${AVAILABLE_BOARDS}\\n\\nFix:\\n1. Remove board: field (recommended - use per-US **Board**: instead)\\n2. OR update to a valid board\\n3. OR set SPECWEAVE_FORCE_PROJECT=1 to bypass\"}"
186
- exit 0
187
- fi
188
- fi
189
-
190
- else
191
- # 1-level multi-project: Frontmatter project is OPTIONAL (per-US fields are primary)
192
-
193
- # If project is provided in frontmatter, validate it exists
194
- if [ -n "$PROJECT" ] && [ "$PROJECT" != "null" ] && [ -n "$AVAILABLE_PROJECTS" ]; then
195
- PROJECT_EXISTS=$(echo "$CONTEXT_OUTPUT" | jq --arg proj "$PROJECT" '.projects[] | select(.id == $proj)' 2>/dev/null)
196
- if [ -z "$PROJECT_EXISTS" ]; then
197
- # Try case-insensitive match
198
- PROJECT_EXISTS=$(echo "$CONTEXT_OUTPUT" | jq --arg proj "$PROJECT" '.projects[] | select(.id | ascii_downcase == ($proj | ascii_downcase))' 2>/dev/null)
199
- fi
200
-
201
- if [ -z "$PROJECT_EXISTS" ]; then
202
- echo "{\"decision\": \"block\", \"reason\": \"Frontmatter project '${PROJECT}' not found in configuration.\\n\\nAvailable projects: ${AVAILABLE_PROJECTS}\\n\\nFix:\\n1. Remove project: field (recommended - use per-US **Project**: instead)\\n2. OR update to a valid project\\n3. OR set SPECWEAVE_FORCE_PROJECT=1 to bypass\"}"
203
- exit 0
204
- fi
205
- fi
206
- fi
207
-
208
- # All validations passed
209
- echo '{"decision": "allow"}'
210
- exit 0