specweave 1.0.32 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CLAUDE.md +205 -148
- package/README.md +0 -2
- package/bin/specweave.js +11 -0
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/update-instructions.d.ts +16 -0
- package/dist/src/cli/commands/update-instructions.d.ts.map +1 -0
- package/dist/src/cli/commands/update-instructions.js +134 -0
- package/dist/src/cli/commands/update-instructions.js.map +1 -0
- package/dist/src/cli/helpers/init/directory-structure.d.ts +28 -1
- package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/directory-structure.js +163 -33
- package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
- package/dist/src/cli/helpers/init/index.d.ts +2 -1
- package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/index.js +3 -1
- package/dist/src/cli/helpers/init/index.js.map +1 -1
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts +23 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js +243 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -0
- package/dist/src/cli/helpers/init/plugin-installer.js +49 -0
- package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +26 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -1
- package/dist/src/core/living-docs/external-sync-orchestrator.js +61 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -1
- package/dist/src/core/living-docs/scaffolding/index.d.ts +12 -0
- package/dist/src/core/living-docs/scaffolding/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/index.js +15 -0
- package/dist/src/core/living-docs/scaffolding/index.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts +183 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.js +523 -0
- package/dist/src/core/living-docs/scaffolding/merger.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts +102 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js +346 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts +108 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js +204 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts +38 -2
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/generators.js +65 -10
- package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
- package/dist/src/core/tools/index.d.ts +11 -0
- package/dist/src/core/tools/index.d.ts.map +1 -0
- package/dist/src/core/tools/index.js +10 -0
- package/dist/src/core/tools/index.js.map +1 -0
- package/dist/src/core/tools/tool-event-bus.d.ts +33 -0
- package/dist/src/core/tools/tool-event-bus.d.ts.map +1 -0
- package/dist/src/core/tools/tool-event-bus.js +84 -0
- package/dist/src/core/tools/tool-event-bus.js.map +1 -0
- package/dist/src/core/tools/tool-index-builder.d.ts +27 -0
- package/dist/src/core/tools/tool-index-builder.d.ts.map +1 -0
- package/dist/src/core/tools/tool-index-builder.js +289 -0
- package/dist/src/core/tools/tool-index-builder.js.map +1 -0
- package/dist/src/core/tools/tool-registry.d.ts +51 -0
- package/dist/src/core/tools/tool-registry.d.ts.map +1 -0
- package/dist/src/core/tools/tool-registry.js +224 -0
- package/dist/src/core/tools/tool-registry.js.map +1 -0
- package/dist/src/core/tools/tool-search-engine.d.ts +22 -0
- package/dist/src/core/tools/tool-search-engine.d.ts.map +1 -0
- package/dist/src/core/tools/tool-search-engine.js +174 -0
- package/dist/src/core/tools/tool-search-engine.js.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts +112 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.js +7 -0
- package/dist/src/core/tools/types/tool-registry-types.js.map +1 -0
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +3 -13
- package/plugins/specweave/hooks/lib/common-setup.sh +47 -321
- package/plugins/specweave/hooks/lib/migrate-increment-work.sh +5 -5
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +5 -5
- package/plugins/specweave/hooks/universal/dispatcher.mjs +4 -5
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +43 -296
- package/plugins/specweave/hooks/universal/hook-wrapper.sh +3 -1
- package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +2 -2
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -10
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +12 -29
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +27 -29
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +10 -4
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +139 -0
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +4 -2
- package/plugins/specweave/hooks/v2/session-end.sh +3 -1
- package/plugins/specweave/hooks/v2/session-start.sh +3 -1
- package/plugins/specweave/skills/increment-planner/templates/plan.md +14 -0
- package/plugins/specweave/skills/update-instructions/SKILL.md +80 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +1 -1
- package/plugins/specweave-mobile/README.md +55 -35
- package/plugins/specweave-mobile/agents/mobile-architect/AGENT.md +805 -329
- package/plugins/specweave-mobile/skills/expo-workflow/SKILL.md +226 -9
- package/plugins/specweave-mobile/skills/native-modules/SKILL.md +221 -20
- package/plugins/specweave-mobile/skills/performance-optimization/SKILL.md +186 -14
- package/plugins/specweave-mobile/skills/react-native-setup/SKILL.md +151 -54
- package/plugins/specweave-release/commands/npm.md +61 -17
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -3
- package/src/templates/AGENTS.md.template +34 -0
- package/src/templates/CLAUDE.md.template +121 -155
- package/plugins/specweave/hooks/config-env-separator.sh +0 -99
- package/plugins/specweave/hooks/github-metadata-guard.sh +0 -73
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +0 -381
- package/plugins/specweave/hooks/lib/crash-prevention.sh +0 -336
- package/plugins/specweave/hooks/lib/logging.sh +0 -231
- package/plugins/specweave/hooks/lib/metrics.sh +0 -347
- package/plugins/specweave/hooks/lib/semaphore.sh +0 -216
- package/plugins/specweave/hooks/project-folder-guard.sh +0 -274
- package/plugins/specweave/hooks/spec-project-validator.sh +0 -210
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +0 -212
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +0 -163
- package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +0 -51
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +0 -63
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +0 -335
- 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
|