specweave 1.0.38 → 1.0.40
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.md +18 -18
- package/package.json +1 -1
- package/plugins/specweave/commands/save.md +28 -0
- package/plugins/specweave/hooks/hooks.json +0 -32
- package/plugins/specweave/hooks/lib/common-setup.sh +0 -8
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +7 -65
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +7 -131
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +9 -88
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +7 -164
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +0 -302
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!-- SW:META template="claude" version="1.0.
|
|
1
|
+
<!-- SW:META template="claude" version="1.0.39" sections="header,start,autodetect,metarule,rules,workflow,structure,taskformat,secrets,syncing,mapping,testing,limits,troubleshooting,principles,linking,docs" -->
|
|
2
2
|
|
|
3
|
-
<!-- SW:SECTION:header version="1.0.
|
|
3
|
+
<!-- SW:SECTION:header version="1.0.39" -->
|
|
4
4
|
**Framework**: SpecWeave | **Truth**: `spec.md` + `tasks.md`
|
|
5
5
|
<!-- SW:END:header -->
|
|
6
6
|
|
|
7
|
-
<!-- SW:SECTION:start version="1.0.
|
|
7
|
+
<!-- SW:SECTION:start version="1.0.39" -->
|
|
8
8
|
## Getting Started
|
|
9
9
|
|
|
10
10
|
**Initial increment**: `0001-project-setup` (auto-created by `specweave init`)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
2. **Customize**: Edit spec.md and use for setup tasks
|
|
15
15
|
<!-- SW:END:start -->
|
|
16
16
|
|
|
17
|
-
<!-- SW:SECTION:autodetect version="1.0.
|
|
17
|
+
<!-- SW:SECTION:autodetect version="1.0.39" -->
|
|
18
18
|
## Auto-Detection
|
|
19
19
|
|
|
20
20
|
SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
@@ -24,7 +24,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
24
24
|
**Opt-out phrases**: "Just brainstorm first" | "Don't plan yet" | "Quick discussion" | "Let's explore ideas"
|
|
25
25
|
<!-- SW:END:autodetect -->
|
|
26
26
|
|
|
27
|
-
<!-- SW:SECTION:metarule version="1.0.
|
|
27
|
+
<!-- SW:SECTION:metarule version="1.0.39" -->
|
|
28
28
|
## Meta-Rule: Think-Before-Act
|
|
29
29
|
|
|
30
30
|
**Satisfy dependencies BEFORE dependent operations.**
|
|
@@ -35,7 +35,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
35
35
|
```
|
|
36
36
|
<!-- SW:END:metarule -->
|
|
37
37
|
|
|
38
|
-
<!-- SW:SECTION:rules version="1.0.
|
|
38
|
+
<!-- SW:SECTION:rules version="1.0.39" -->
|
|
39
39
|
## Rules
|
|
40
40
|
|
|
41
41
|
1. **Files** → `.specweave/increments/####-name/` (spec.md, plan.md, tasks.md at root; reports/, scripts/, logs/ subfolders)
|
|
@@ -45,7 +45,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
45
45
|
5. **Root clean**: NEVER create .md/reports/scripts in project root → use increment folders
|
|
46
46
|
<!-- SW:END:rules -->
|
|
47
47
|
|
|
48
|
-
<!-- SW:SECTION:workflow version="1.0.
|
|
48
|
+
<!-- SW:SECTION:workflow version="1.0.39" -->
|
|
49
49
|
## Workflow
|
|
50
50
|
|
|
51
51
|
`/sw:increment "X"` → `/sw:do` → `/sw:progress` → `/sw:done 0001`
|
|
@@ -62,7 +62,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
62
62
|
**Natural language**: "Let's build X" → `/sw:increment` | "What's status?" → `/sw:progress` | "We're done" → `/sw:done`
|
|
63
63
|
<!-- SW:END:workflow -->
|
|
64
64
|
|
|
65
|
-
<!-- SW:SECTION:structure version="1.0.
|
|
65
|
+
<!-- SW:SECTION:structure version="1.0.39" -->
|
|
66
66
|
## Structure
|
|
67
67
|
|
|
68
68
|
```
|
|
@@ -78,7 +78,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
78
78
|
**Multi-repo**: Clone to `/repositories`, not root → `repositories/backend/src/...`
|
|
79
79
|
<!-- SW:END:structure -->
|
|
80
80
|
|
|
81
|
-
<!-- SW:SECTION:taskformat version="1.0.
|
|
81
|
+
<!-- SW:SECTION:taskformat version="1.0.39" -->
|
|
82
82
|
## Task Format
|
|
83
83
|
|
|
84
84
|
```markdown
|
|
@@ -88,7 +88,7 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
|
|
|
88
88
|
```
|
|
89
89
|
<!-- SW:END:taskformat -->
|
|
90
90
|
|
|
91
|
-
<!-- SW:SECTION:secrets version="1.0.
|
|
91
|
+
<!-- SW:SECTION:secrets version="1.0.39" -->
|
|
92
92
|
## Secrets Check
|
|
93
93
|
|
|
94
94
|
**BEFORE CLI tools**: Check existing config first!
|
|
@@ -99,7 +99,7 @@ gh auth status
|
|
|
99
99
|
```
|
|
100
100
|
<!-- SW:END:secrets -->
|
|
101
101
|
|
|
102
|
-
<!-- SW:SECTION:syncing version="1.0.
|
|
102
|
+
<!-- SW:SECTION:syncing version="1.0.39" -->
|
|
103
103
|
## Auto-Sync (Hooks)
|
|
104
104
|
|
|
105
105
|
Post-task: updates tasks.md → living docs → external trackers (if configured)
|
|
@@ -107,7 +107,7 @@ Post-task: updates tasks.md → living docs → external trackers (if configured
|
|
|
107
107
|
Config: `.specweave/config.json` → `hooks.post_task_completion`
|
|
108
108
|
<!-- SW:END:syncing -->
|
|
109
109
|
|
|
110
|
-
<!-- SW:SECTION:mapping version="1.0.
|
|
110
|
+
<!-- SW:SECTION:mapping version="1.0.39" -->
|
|
111
111
|
## GitHub Mapping
|
|
112
112
|
|
|
113
113
|
| SpecWeave | GitHub |
|
|
@@ -117,7 +117,7 @@ Config: `.specweave/config.json` → `hooks.post_task_completion`
|
|
|
117
117
|
| Task T-XXX | Checkbox |
|
|
118
118
|
<!-- SW:END:mapping -->
|
|
119
119
|
|
|
120
|
-
<!-- SW:SECTION:testing version="1.0.
|
|
120
|
+
<!-- SW:SECTION:testing version="1.0.39" -->
|
|
121
121
|
## Testing
|
|
122
122
|
|
|
123
123
|
BDD in tasks.md | Unit >80% | `.test.ts` (Vitest)
|
|
@@ -129,13 +129,13 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
129
129
|
```
|
|
130
130
|
<!-- SW:END:testing -->
|
|
131
131
|
|
|
132
|
-
<!-- SW:SECTION:limits version="1.0.
|
|
132
|
+
<!-- SW:SECTION:limits version="1.0.39" -->
|
|
133
133
|
## Limits
|
|
134
134
|
|
|
135
135
|
**Max 1500 lines/file** — extract before adding
|
|
136
136
|
<!-- SW:END:limits -->
|
|
137
137
|
|
|
138
|
-
<!-- SW:SECTION:troubleshooting version="1.0.
|
|
138
|
+
<!-- SW:SECTION:troubleshooting version="1.0.39" -->
|
|
139
139
|
## Troubleshooting
|
|
140
140
|
|
|
141
141
|
| Issue | Fix |
|
|
@@ -149,7 +149,7 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
149
149
|
| External not syncing | Check `config.json` → `external_tracker_sync: true` |
|
|
150
150
|
<!-- SW:END:troubleshooting -->
|
|
151
151
|
|
|
152
|
-
<!-- SW:SECTION:principles version="1.0.
|
|
152
|
+
<!-- SW:SECTION:principles version="1.0.39" -->
|
|
153
153
|
## Principles
|
|
154
154
|
|
|
155
155
|
1. **Spec-first**: `/sw:increment` before coding
|
|
@@ -159,7 +159,7 @@ vi.mock('fs', () => ({ readFile: vi.fn() }));
|
|
|
159
159
|
5. **Clean**: All files in increment folders
|
|
160
160
|
<!-- SW:END:principles -->
|
|
161
161
|
|
|
162
|
-
<!-- SW:SECTION:linking version="1.0.
|
|
162
|
+
<!-- SW:SECTION:linking version="1.0.39" -->
|
|
163
163
|
## Bidirectional Linking
|
|
164
164
|
|
|
165
165
|
Tasks ↔ User Stories auto-linked via AC-IDs: `AC-US1-01` → `US-001`
|
|
@@ -167,7 +167,7 @@ Tasks ↔ User Stories auto-linked via AC-IDs: `AC-US1-01` → `US-001`
|
|
|
167
167
|
Task format: `**AC**: AC-US1-01, AC-US1-02` (CRITICAL for linking)
|
|
168
168
|
<!-- SW:END:linking -->
|
|
169
169
|
|
|
170
|
-
<!-- SW:SECTION:docs version="1.0.
|
|
170
|
+
<!-- SW:SECTION:docs version="1.0.39" -->
|
|
171
171
|
## Docs
|
|
172
172
|
|
|
173
173
|
[spec-weave.com](https://spec-weave.com) | `.specweave/docs/internal/`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -897,6 +897,34 @@ Summary:
|
|
|
897
897
|
Saved: 1/1 repository
|
|
898
898
|
```
|
|
899
899
|
|
|
900
|
+
### No Remote Configured (Single Repo or Parent Project)
|
|
901
|
+
|
|
902
|
+
If operating in single-repo mode and no git remote is configured, prompt the user with the **EXACT project name** in the question:
|
|
903
|
+
|
|
904
|
+
```markdown
|
|
905
|
+
⚠️ No remote repository configured for 'sw-content-repurposer' (parent project).
|
|
906
|
+
|
|
907
|
+
How would you like to proceed?
|
|
908
|
+
1. 📝 Enter URL manually - I'll provide the GitHub/GitLab URL for this repository
|
|
909
|
+
2. ⏭️ Skip push (commit only) - Just commit locally, I'll set up remote later
|
|
910
|
+
3. ❌ Cancel - Don't commit or push anything right now
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**IMPORTANT**: Always include the project/repository name in the dialog so the user knows WHICH repository is missing the remote. For umbrella setups, this helps distinguish between:
|
|
914
|
+
- Parent project (the umbrella root directory)
|
|
915
|
+
- Child repositories (cloned into `repositories/` folder)
|
|
916
|
+
|
|
917
|
+
**Check for pending clone jobs**: If `umbrella.childRepos` is empty but a clone job is running (check `/sw:jobs`), inform the user:
|
|
918
|
+
|
|
919
|
+
```markdown
|
|
920
|
+
ℹ️ Repository cloning is in progress (job: a84e4fe5).
|
|
921
|
+
|
|
922
|
+
The child repositories are being cloned in the background. Options:
|
|
923
|
+
1. ⏳ Wait for cloning to complete (run `/sw:jobs` to check status)
|
|
924
|
+
2. 💾 Save parent project only (commit .specweave/ changes)
|
|
925
|
+
3. ❌ Cancel and retry later
|
|
926
|
+
```
|
|
927
|
+
|
|
900
928
|
## Flags and Options
|
|
901
929
|
|
|
902
930
|
| Flag | Description |
|
|
@@ -20,38 +20,6 @@
|
|
|
20
20
|
]
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
|
-
"PreToolUse": [
|
|
24
|
-
{
|
|
25
|
-
"matcher": "Write",
|
|
26
|
-
"matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/spec\\.md",
|
|
27
|
-
"hooks": [
|
|
28
|
-
{
|
|
29
|
-
"type": "command",
|
|
30
|
-
"command": "bash -c 'W=\"${CLAUDE_PLUGIN_ROOT}/hooks/universal/fail-fast-wrapper.sh\"; S=\"${CLAUDE_PLUGIN_ROOT}/hooks/v2/guards/metadata-json-guard.sh\"; [[ -x \"$W\" ]] && exec \"$W\" \"$S\" || (cat >/dev/null && printf \"{\\\"decision\\\":\\\"allow\\\"}\")'"
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"matcher": "Write",
|
|
36
|
-
"matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/",
|
|
37
|
-
"hooks": [
|
|
38
|
-
{
|
|
39
|
-
"type": "command",
|
|
40
|
-
"command": "bash -c 'W=\"${CLAUDE_PLUGIN_ROOT}/hooks/universal/fail-fast-wrapper.sh\"; S=\"${CLAUDE_PLUGIN_ROOT}/hooks/v2/guards/increment-duplicate-guard.sh\"; [[ -x \"$W\" ]] && exec \"$W\" \"$S\" || (cat >/dev/null && printf \"{\\\"decision\\\":\\\"allow\\\"}\")'"
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"matcher": "Write",
|
|
46
|
-
"matcher_content": "\\.specweave/(increments/\\d{3,4}E?-[^/]+/spec\\.md|docs/internal/specs/)",
|
|
47
|
-
"hooks": [
|
|
48
|
-
{
|
|
49
|
-
"type": "command",
|
|
50
|
-
"command": "bash -c 'W=\"${CLAUDE_PLUGIN_ROOT}/hooks/universal/fail-fast-wrapper.sh\"; S=\"${CLAUDE_PLUGIN_ROOT}/hooks/v2/guards/spec-validation-guard.sh\"; [[ -x \"$W\" ]] && exec \"$W\" \"$S\" || (cat >/dev/null && printf \"{\\\"decision\\\":\\\"allow\\\"}\")'"
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
23
|
"PostToolUse": [
|
|
56
24
|
{
|
|
57
25
|
"matcher": "Edit|Write",
|
|
@@ -77,14 +77,6 @@ guard_allow() {
|
|
|
77
77
|
exit 0
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
# Block the tool call with reason
|
|
81
|
-
guard_block() {
|
|
82
|
-
local reason="$1"
|
|
83
|
-
reason=$(echo "$reason" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
|
84
|
-
printf '{"decision":"block","reason":"%s"}\n' "$reason"
|
|
85
|
-
exit 2
|
|
86
|
-
}
|
|
87
|
-
|
|
88
80
|
# ============================================================================
|
|
89
81
|
# SIMPLE LOGGING (optional, writes to .specweave/logs/hooks.log)
|
|
90
82
|
# ============================================================================
|
|
@@ -1,72 +1,14 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# completion-guard.sh -
|
|
2
|
+
# completion-guard.sh - DISABLED (v1.0.38)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# This guard was converted to WARNING-only in v1.0.37, and now completely
|
|
5
|
+
# disabled in v1.0.38 per user feedback: "you MUST NEVER block such operations"
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# Completion workflow should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
# All exit paths MUST output JSON for proper Claude Code handling.
|
|
11
|
-
set +e
|
|
12
|
-
|
|
13
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
14
|
-
|
|
15
|
-
# Read stdin for tool input
|
|
16
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
17
|
-
|
|
18
|
-
# Check if this is editing metadata.json with status: completed
|
|
19
|
-
# Pattern: file_path contains metadata.json AND (new_string OR content) contains "status"..."completed"
|
|
20
|
-
|
|
21
|
-
# Extract file_path
|
|
22
|
-
# Claude Code passes tool input in .tool_input.file_path format
|
|
23
|
-
if command -v jq &> /dev/null; then
|
|
24
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)
|
|
25
|
-
else
|
|
26
|
-
FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
# Only care about metadata.json files
|
|
30
|
-
if [[ "$FILE_PATH" != *metadata.json ]]; then
|
|
31
|
-
echo '{"decision":"allow"}'
|
|
32
|
-
exit 0 # Allow
|
|
33
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
# Claude Code passes tool input in .tool_input format
|
|
37
|
-
if command -v jq &> /dev/null; then
|
|
38
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // .new_string // .content // empty' 2>/dev/null)
|
|
39
|
-
else
|
|
40
|
-
NEW_CONTENT=$(echo "$INPUT" | grep -o '"new_string"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1)
|
|
41
|
-
if [[ -z "$NEW_CONTENT" ]]; then
|
|
42
|
-
NEW_CONTENT=$(echo "$INPUT" | grep -o '"content"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1)
|
|
43
|
-
fi
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# Check if trying to set status to "completed" directly
|
|
47
|
-
# This is a simple pattern match - if the edit/write contains status...completed
|
|
48
|
-
if echo "$NEW_CONTENT" | grep -q '"status"[[:space:]]*:[[:space:]]*"completed"'; then
|
|
49
|
-
# Read current status from file to check if coming from ready_for_review
|
|
50
|
-
if [[ -f "$FILE_PATH" ]]; then
|
|
51
|
-
CURRENT_STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$FILE_PATH" | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
52
|
-
|
|
53
|
-
if [[ "$CURRENT_STATUS" == "ready_for_review" ]]; then
|
|
54
|
-
# This is a valid transition - allow
|
|
55
|
-
echo '{"decision":"allow"}'
|
|
56
|
-
exit 0
|
|
57
|
-
fi
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# BLOCK - trying to set completed without going through ready_for_review
|
|
61
|
-
cat << 'BLOCK_EOF'
|
|
62
|
-
{
|
|
63
|
-
"decision": "block",
|
|
64
|
-
"reason": "🚫 BLOCKED: Direct status change to \"completed\" is not allowed (v0.28.63+)\n\nYou cannot directly set status to \"completed\" in metadata.json.\n\nCORRECT WORKFLOW:\n1. All tasks completed → status auto-transitions to \"ready_for_review\"\n2. Run /sw:done <increment-id> with explicit user confirmation\n3. Only then does status become \"completed\"\n\nWHY THIS MATTERS:\n• Ensures all ACs are checked in spec.md before closure\n• Requires explicit user approval\n• Maintains audit trail (approvedAt timestamp)\n\n🔧 If implementing closure logic, use:\n MetadataManager.updateStatus(incrementId, IncrementStatus.COMPLETED)"
|
|
65
|
-
}
|
|
66
|
-
BLOCK_EOF
|
|
67
|
-
exit 2 # Block the tool call
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Allow other edits to metadata.json
|
|
12
|
+
set +e
|
|
71
13
|
echo '{"decision":"allow"}'
|
|
72
14
|
exit 0
|
|
@@ -1,138 +1,14 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# increment-duplicate-guard.sh -
|
|
2
|
+
# increment-duplicate-guard.sh - DISABLED (v1.0.38)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# This guard was converted to WARNING-only in v1.0.37, and now completely
|
|
5
|
+
# disabled in v1.0.38 per user feedback: "you MUST NEVER block such operations"
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# Duplicate detection should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
# - 0121-ado-jira-feature-parity-p2-p3
|
|
11
|
-
# - 0121-intelligent-living-docs-content
|
|
12
|
-
#
|
|
13
|
-
# Exit 0 = allow (with JSON), Exit 2 = block (with JSON)
|
|
14
|
-
set +e
|
|
15
|
-
|
|
16
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
17
|
-
|
|
18
|
-
# Read stdin for tool input
|
|
19
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
20
|
-
|
|
21
|
-
# Extract file_path from the tool call
|
|
22
|
-
# Claude Code passes tool input in .tool_input.file_path format
|
|
23
|
-
if command -v jq &> /dev/null; then
|
|
24
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)
|
|
25
|
-
else
|
|
26
|
-
FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
# Only care about .specweave/increments/ paths
|
|
30
|
-
if [[ "$FILE_PATH" != *.specweave/increments/* ]]; then
|
|
31
|
-
echo '{"decision":"allow"}'
|
|
32
|
-
exit 0 # Not an increment file - allow
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# Extract the increment folder name from the path
|
|
36
|
-
# Pattern: .specweave/increments/XXXX-name/file.md or .specweave/increments/XXXX-name/subfolder/file
|
|
37
|
-
# We need to extract "XXXX-name" part
|
|
38
|
-
|
|
39
|
-
# Remove the .specweave/increments/ prefix
|
|
40
|
-
AFTER_INCREMENTS=${FILE_PATH#*.specweave/increments/}
|
|
41
|
-
|
|
42
|
-
# Get the first path component (the increment folder)
|
|
43
|
-
INCREMENT_FOLDER=$(echo "$AFTER_INCREMENTS" | cut -d'/' -f1)
|
|
44
|
-
|
|
45
|
-
# Skip special folders
|
|
46
|
-
if [[ "$INCREMENT_FOLDER" == "_archive" ]] || [[ "$INCREMENT_FOLDER" == "_abandoned" ]] || [[ "$INCREMENT_FOLDER" == "_paused" ]] || [[ "$INCREMENT_FOLDER" == "README.md" ]]; then
|
|
47
|
-
echo '{"decision":"allow"}'
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# Extract the increment number from folder name (handles both 0121-name and 0121E-name)
|
|
52
|
-
INCREMENT_NUM=$(echo "$INCREMENT_FOLDER" | grep -oE '^[0-9]{3,4}' | head -1)
|
|
53
|
-
|
|
54
|
-
if [[ -z "$INCREMENT_NUM" ]]; then
|
|
55
|
-
echo '{"decision":"allow"}'
|
|
56
|
-
exit 0 # Not a standard increment folder pattern - allow
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# Normalize to 4 digits (strip leading zeros to avoid octal interpretation)
|
|
60
|
-
INCREMENT_NUM=$(printf "%04d" "$((10#$INCREMENT_NUM))")
|
|
61
|
-
|
|
62
|
-
# Find the increments root directory
|
|
63
|
-
INCREMENTS_DIR=$(echo "$FILE_PATH" | grep -o '.*/\.specweave/increments' | head -1)
|
|
64
|
-
|
|
65
|
-
if [[ ! -d "$INCREMENTS_DIR" ]]; then
|
|
66
|
-
# Increments directory doesn't exist yet - first increment, allow creation
|
|
67
|
-
echo '{"decision":"allow"}'
|
|
68
|
-
exit 0
|
|
69
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
70
11
|
|
|
71
|
-
|
|
72
|
-
DIRS_TO_CHECK=(
|
|
73
|
-
"$INCREMENTS_DIR"
|
|
74
|
-
"$INCREMENTS_DIR/_archive"
|
|
75
|
-
"$INCREMENTS_DIR/_abandoned"
|
|
76
|
-
"$INCREMENTS_DIR/_paused"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
FOUND_DUPLICATES=()
|
|
80
|
-
|
|
81
|
-
for DIR in "${DIRS_TO_CHECK[@]}"; do
|
|
82
|
-
if [[ ! -d "$DIR" ]]; then
|
|
83
|
-
continue
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# Find all folders matching this increment number (including E suffix variants)
|
|
87
|
-
while IFS= read -r -d '' EXISTING_FOLDER; do
|
|
88
|
-
EXISTING_NAME=$(basename "$EXISTING_FOLDER")
|
|
89
|
-
|
|
90
|
-
# Extract number from existing folder
|
|
91
|
-
EXISTING_NUM=$(echo "$EXISTING_NAME" | grep -oE '^[0-9]{3,4}' | head -1)
|
|
92
|
-
|
|
93
|
-
if [[ -z "$EXISTING_NUM" ]]; then
|
|
94
|
-
continue
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
EXISTING_NUM=$(printf "%04d" "$((10#$EXISTING_NUM))")
|
|
98
|
-
|
|
99
|
-
# Check if same base number (0121 matches 0121, 0121E, etc.)
|
|
100
|
-
if [[ "$EXISTING_NUM" == "$INCREMENT_NUM" ]]; then
|
|
101
|
-
# Skip if it's the exact same folder we're creating
|
|
102
|
-
if [[ "$EXISTING_NAME" == "$INCREMENT_FOLDER" ]]; then
|
|
103
|
-
continue
|
|
104
|
-
fi
|
|
105
|
-
|
|
106
|
-
# Found a duplicate!
|
|
107
|
-
FOUND_DUPLICATES+=("$EXISTING_NAME (in $(basename "$DIR"))")
|
|
108
|
-
fi
|
|
109
|
-
done < <(find "$DIR" -maxdepth 1 -type d -name "${INCREMENT_NUM}*" -print0 2>/dev/null)
|
|
110
|
-
done
|
|
111
|
-
|
|
112
|
-
# If duplicates found, BLOCK the operation and provide the correct number to use
|
|
113
|
-
if [[ ${#FOUND_DUPLICATES[@]} -gt 0 ]]; then
|
|
114
|
-
# Format duplicates for JSON
|
|
115
|
-
DUP_LIST=""
|
|
116
|
-
for DUP in "${FOUND_DUPLICATES[@]}"; do
|
|
117
|
-
DUP_LIST="${DUP_LIST}\\n - ${DUP}"
|
|
118
|
-
done
|
|
119
|
-
|
|
120
|
-
# Calculate the next available number using gap-filling strategy
|
|
121
|
-
EXISTING_NUMS=$(find "$INCREMENTS_DIR" "$INCREMENTS_DIR/_archive" "$INCREMENTS_DIR/_abandoned" "$INCREMENTS_DIR/_paused" -maxdepth 1 -type d -name "[0-9]*-*" 2>/dev/null | xargs -I {} basename {} | grep -oE '^[0-9]{3,4}' | sort -n | uniq)
|
|
122
|
-
NEXT_NUM=1
|
|
123
|
-
while echo "$EXISTING_NUMS" | grep -q "^$(printf "%04d" $NEXT_NUM)$\|^$(printf "%03d" $NEXT_NUM)$"; do
|
|
124
|
-
NEXT_NUM=$((NEXT_NUM + 1))
|
|
125
|
-
done
|
|
126
|
-
NEXT_NUM_PADDED=$(printf "%04d" $NEXT_NUM)
|
|
127
|
-
|
|
128
|
-
# Extract the name part from the attempted folder
|
|
129
|
-
FOLDER_NAME_PART=$(echo "$INCREMENT_FOLDER" | sed 's/^[0-9]\{3,4\}E\?-//')
|
|
130
|
-
SUGGESTED_FOLDER="${NEXT_NUM_PADDED}-${FOLDER_NAME_PART}"
|
|
131
|
-
|
|
132
|
-
printf '{"decision":"block","reason":"🚫 DUPLICATE INCREMENT ID - Use this instead:\\n\\n✅ CORRECT: %s\\n❌ ATTEMPTED: %s\\n\\nNumber %s already exists:%s\\n\\n🔧 IMMEDIATE FIX: Replace your file path with:\\n .specweave/increments/%s/...\\n\\nThe guard calculated the next available number for you."}\n' "$SUGGESTED_FOLDER" "$INCREMENT_FOLDER" "$INCREMENT_NUM" "$DUP_LIST" "$SUGGESTED_FOLDER"
|
|
133
|
-
exit 2 # Block the tool call
|
|
134
|
-
fi
|
|
135
|
-
|
|
136
|
-
# No duplicates - allow
|
|
12
|
+
set +e
|
|
137
13
|
echo '{"decision":"allow"}'
|
|
138
14
|
exit 0
|
|
@@ -1,93 +1,14 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# metadata-json-guard.sh - DISABLED (v1.0.38)
|
|
2
3
|
#
|
|
3
|
-
#
|
|
4
|
+
# This guard was converted to WARNING-only in v1.0.37, and now completely
|
|
5
|
+
# disabled in v1.0.38 per user feedback: "you MUST NEVER block such operations"
|
|
4
6
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
+
# Metadata validation should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
# metadata.json may be forgotten, causing:
|
|
10
|
-
# - Status tracking broken
|
|
11
|
-
# - WIP limits don't work
|
|
12
|
-
# - External sync fails (GitHub/Jira/ADO)
|
|
13
|
-
# - All increment commands fail
|
|
14
|
-
#
|
|
15
|
-
# SOLUTION: Block spec.md creation if metadata.json doesn't exist in same increment folder.
|
|
16
|
-
# Claude MUST create metadata.json FIRST, then spec.md.
|
|
17
|
-
#
|
|
18
|
-
# Activation:
|
|
19
|
-
# - tool_name: Write
|
|
20
|
-
# - file_path matches: .specweave/increments/*/spec.md
|
|
21
|
-
#
|
|
22
|
-
# Returns exit code 2 (block) if metadata.json missing, 0 (allow) otherwise.
|
|
23
|
-
#
|
|
24
|
-
# Bypass: Set SPECWEAVE_FORCE_METADATA=1 to skip validation
|
|
25
|
-
#
|
|
26
|
-
# v0.34.0 - Initial implementation based on user project bug analysis
|
|
27
|
-
|
|
28
|
-
set +e # CRITICAL: Never use set -e in hooks (causes cascading failures)
|
|
29
|
-
|
|
30
|
-
# Check for force bypass
|
|
31
|
-
if [ "$SPECWEAVE_FORCE_METADATA" = "1" ]; then
|
|
32
|
-
echo '{"decision": "allow", "message": "metadata.json guard bypassed (SPECWEAVE_FORCE_METADATA=1)"}'
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
# Disable hooks bypass
|
|
37
|
-
if [ "$SPECWEAVE_DISABLE_HOOKS" = "1" ]; then
|
|
38
|
-
echo '{"decision": "allow"}'
|
|
39
|
-
exit 0
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
# Read tool input from stdin (safe handling)
|
|
43
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
44
|
-
|
|
45
|
-
# Check jq availability - allow if not present
|
|
46
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
47
|
-
echo '{"decision": "allow"}'
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# Extract tool name - with jq fallback
|
|
52
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool_input.tool_name // ""' 2>/dev/null || echo "")
|
|
53
|
-
|
|
54
|
-
# Only validate Write tool calls
|
|
55
|
-
if [ "$TOOL_NAME" != "Write" ]; then
|
|
56
|
-
echo '{"decision": "allow"}'
|
|
57
|
-
exit 0
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# Extract file path - handle both formats
|
|
61
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
62
|
-
|
|
63
|
-
# Only validate spec.md files in increments folder
|
|
64
|
-
# Match: 3-4 digits, optional E suffix, kebab-case name, spec.md
|
|
65
|
-
if [[ ! "$FILE_PATH" =~ \.specweave/increments/([0-9]{3,4}E?-[^/]+)/spec\.md$ ]]; then
|
|
66
|
-
echo '{"decision": "allow"}'
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Extract increment folder path
|
|
71
|
-
INCREMENT_DIR=$(dirname "$FILE_PATH")
|
|
72
|
-
INCREMENT_ID="${BASH_REMATCH[1]}"
|
|
73
|
-
|
|
74
|
-
# Check if metadata.json exists in the same increment folder
|
|
75
|
-
METADATA_PATH="${INCREMENT_DIR}/metadata.json"
|
|
76
|
-
|
|
77
|
-
if [ -f "$METADATA_PATH" ]; then
|
|
78
|
-
# metadata.json exists, allow spec.md creation
|
|
79
|
-
echo '{"decision": "allow"}'
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# metadata.json doesn't exist - BLOCK spec.md creation
|
|
84
|
-
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
85
|
-
|
|
86
|
-
cat << BLOCK_EOF
|
|
87
|
-
{
|
|
88
|
-
"decision": "block",
|
|
89
|
-
"reason": "🚫 BLOCKED: Cannot create spec.md without metadata.json\n\n⚠️ CRITICAL RULE: metadata.json MUST be created FIRST!\n\nWithout metadata.json:\n - ❌ Status tracking broken\n - ❌ WIP limits don't work\n - ❌ External sync fails (GitHub/Jira/ADO)\n - ❌ All increment commands fail\n\n📋 CORRECT WORKFLOW:\n1. Create metadata.json FIRST:\n Write({\n file_path: \"${METADATA_PATH}\",\n content: {\n \"id\": \"${INCREMENT_ID}\",\n \"status\": \"planned\",\n \"type\": \"feature\",\n \"priority\": \"P1\",\n \"created\": \"${NOW}\",\n \"lastActivity\": \"${NOW}\",\n \"testMode\": \"TDD\",\n \"coverageTarget\": 95,\n \"feature_id\": null,\n \"epic_id\": null,\n \"externalLinks\": {}\n }\n })\n\n2. THEN create spec.md\n\n📖 See: CLAUDE.md section '3. metadata.json is MANDATORY'\n\n💡 Bypass: Set SPECWEAVE_FORCE_METADATA=1 to skip this validation"
|
|
90
|
-
}
|
|
91
|
-
BLOCK_EOF
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
92
11
|
|
|
93
|
-
|
|
12
|
+
set +e
|
|
13
|
+
echo '{"decision":"allow"}'
|
|
14
|
+
exit 0
|
|
@@ -1,171 +1,14 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# spec-validation-guard.sh -
|
|
2
|
+
# spec-validation-guard.sh - DISABLED (v1.0.38)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# - spec-project-validator.sh (deleted) - Placeholder detection
|
|
7
|
-
# - project-folder-guard.sh (deleted) - Living docs folder validation
|
|
4
|
+
# This guard was converted to WARNING-only in v1.0.37, and now completely
|
|
5
|
+
# disabled in v1.0.38 per user feedback: "you MUST NEVER block such operations"
|
|
8
6
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# - file_path matches: .specweave/increments/*/spec.md OR .specweave/docs/internal/specs/*/
|
|
7
|
+
# Spec validation should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
12
9
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Bypasses:
|
|
16
|
-
# - SPECWEAVE_DISABLE_HOOKS=1 - Disable all hooks
|
|
17
|
-
# - SPECWEAVE_FORCE_PROJECT=1 - Skip project validation
|
|
18
|
-
# - SPECWEAVE_FORCE_METADATA=1 - Skip all spec validation
|
|
19
|
-
|
|
20
|
-
set +e # CRITICAL: Never use set -e in hooks
|
|
21
|
-
|
|
22
|
-
# Source shared library if available
|
|
23
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
-
LIB_DIR="${SCRIPT_DIR}/../../lib"
|
|
25
|
-
if [[ -f "$LIB_DIR/common-setup.sh" ]]; then
|
|
26
|
-
source "$LIB_DIR/common-setup.sh"
|
|
27
|
-
init_pretool_guard || exit 0
|
|
28
|
-
else
|
|
29
|
-
# Fallback inline implementation
|
|
30
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
31
|
-
HOOK_INPUT=$(cat 2>/dev/null || echo '{}')
|
|
32
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
33
|
-
echo '{"decision":"allow"}'
|
|
34
|
-
exit 0
|
|
35
|
-
fi
|
|
36
|
-
HOOK_TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
|
37
|
-
HOOK_FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
38
|
-
HOOK_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null || echo "")
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
# Check bypass flags
|
|
42
|
-
[[ "$SPECWEAVE_FORCE_PROJECT" == "1" ]] && echo '{"decision":"allow","message":"Project validation bypassed"}' && exit 0
|
|
43
|
-
[[ "$SPECWEAVE_FORCE_METADATA" == "1" ]] && echo '{"decision":"allow","message":"Spec validation bypassed"}' && exit 0
|
|
44
|
-
|
|
45
|
-
# Only validate Write tool
|
|
46
|
-
[[ "$HOOK_TOOL_NAME" != "Write" ]] && echo '{"decision":"allow"}' && exit 0
|
|
47
|
-
|
|
48
|
-
# No file path = allow
|
|
49
|
-
[[ -z "$HOOK_FILE_PATH" ]] && echo '{"decision":"allow"}' && exit 0
|
|
50
|
-
|
|
51
|
-
# ============================================================================
|
|
52
|
-
# VALIDATION 1: spec.md placeholder detection
|
|
53
|
-
# ============================================================================
|
|
54
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/increments/[0-9]{3,4}E?-[^/]+/spec\.md$ ]]; then
|
|
55
|
-
|
|
56
|
-
# Check for unresolved {{...}} placeholders
|
|
57
|
-
if echo "$HOOK_CONTENT" | grep -qE '\{\{[A-Z_]+\}\}'; then
|
|
58
|
-
PLACEHOLDERS=$(echo "$HOOK_CONTENT" | grep -oE '\{\{[A-Z_]+\}\}' | sort -u | tr '\n' ', ' | sed 's/,$//')
|
|
59
|
-
printf '{"decision":"block","reason":"🚫 UNRESOLVED PLACEHOLDERS\\n\\nFound: %s\\n\\n🔧 FIX: Replace placeholders with actual values.\\nRun: specweave context projects\\nThen use values from the JSON output."}\n' "$PLACEHOLDERS"
|
|
60
|
-
exit 2
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
# Check for **Project**: field in User Stories (soft validation - warn, don't block)
|
|
64
|
-
# Pattern: ### US-XXX or #### US-XXX followed by **Project**:
|
|
65
|
-
US_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^#{3,4} US-' 2>/dev/null || echo "0")
|
|
66
|
-
PROJECT_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Project\*\*:' 2>/dev/null || echo "0")
|
|
67
|
-
|
|
68
|
-
# Trim to just the number
|
|
69
|
-
US_COUNT="${US_COUNT//[^0-9]/}"
|
|
70
|
-
PROJECT_COUNT="${PROJECT_COUNT//[^0-9]/}"
|
|
71
|
-
[[ -z "$US_COUNT" ]] && US_COUNT=0
|
|
72
|
-
[[ -z "$PROJECT_COUNT" ]] && PROJECT_COUNT=0
|
|
73
|
-
|
|
74
|
-
# Only warn if there are User Stories but no Project fields
|
|
75
|
-
# Don't block - just allow with warning
|
|
76
|
-
if [[ "$US_COUNT" -gt 0 ]] && [[ "$PROJECT_COUNT" -eq 0 ]]; then
|
|
77
|
-
echo '{"decision":"allow","message":"⚠️ WARNING: No **Project**: fields found. Add **Project**: after each US heading for proper sync."}'
|
|
78
|
-
exit 0
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
# Check for comma-separated projects (forbidden - 1:1 mapping required)
|
|
82
|
-
if echo "$HOOK_CONTENT" | grep -qE '^\*\*Project\*\*:.*,'; then
|
|
83
|
-
printf '{"decision":"block","reason":"🚫 MULTIPLE PROJECTS IN ONE US\\n\\nEach User Story MUST map to exactly ONE project.\\n\\n🔧 FIX: Split cross-project features into separate User Stories:\\n\\nWRONG:\\n### US-001: OAuth Implementation\\n**Project**: frontend, backend\\n\\nCORRECT:\\n### US-001: OAuth Login Form\\n**Project**: frontend\\n\\n### US-002: OAuth API\\n**Project**: backend"}\n'
|
|
84
|
-
exit 2
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
# Check structure level to validate **Board**: fields
|
|
88
|
-
# For 1-level structures (GitHub), **Board**: should NOT be present
|
|
89
|
-
# For 2-level structures (ADO/JIRA with boards), **Board**: is required
|
|
90
|
-
# NOTE: This is a WARNING only - do NOT block spec writes!
|
|
91
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
92
|
-
BOARD_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Board\*\*:' 2>/dev/null || echo "0")
|
|
93
|
-
BOARD_COUNT="${BOARD_COUNT//[^0-9]/}"
|
|
94
|
-
[[ -z "$BOARD_COUNT" ]] && BOARD_COUNT=0
|
|
95
|
-
|
|
96
|
-
if [[ "$BOARD_COUNT" -gt 0 ]]; then
|
|
97
|
-
# Has **Board**: fields - check if this is a 2-level structure
|
|
98
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
99
|
-
IS_2LEVEL="false"
|
|
100
|
-
|
|
101
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
102
|
-
# 2-level indicators: ADO areaPathMapping, JIRA boardMapping with multiple boards
|
|
103
|
-
HAS_AREA_MAPPING=$(jq -r '.sync.profiles | to_entries[] | select(.value.provider == "ado") | .value.config.areaPathMapping.mappings | length > 0' "$CONFIG_FILE" 2>/dev/null | grep -c "true" || echo "0")
|
|
104
|
-
HAS_BOARD_MAPPING=$(jq -r '.sync.profiles | to_entries[] | select(.value.provider == "jira") | .value.config.boardMapping.boards | length > 1' "$CONFIG_FILE" 2>/dev/null | grep -c "true" || echo "0")
|
|
105
|
-
HAS_MULTI_TEAMS=$(jq -r '.umbrella.childRepos | map(.team) | unique | length > 1' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
106
|
-
|
|
107
|
-
if [[ "$HAS_AREA_MAPPING" -gt 0 ]] || [[ "$HAS_BOARD_MAPPING" -gt 0 ]] || [[ "$HAS_MULTI_TEAMS" == "true" ]]; then
|
|
108
|
-
IS_2LEVEL="true"
|
|
109
|
-
fi
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# WARN only - never block spec.md writes (too disruptive)
|
|
113
|
-
if [[ "$IS_2LEVEL" != "true" ]]; then
|
|
114
|
-
echo '{"decision":"allow","message":"⚠️ WARNING: **Board**: fields found but this is a 1-level structure (GitHub). Board fields are only needed for ADO/JIRA with multiple boards. Consider removing **Board**: lines."}'
|
|
115
|
-
exit 0
|
|
116
|
-
fi
|
|
117
|
-
fi
|
|
118
|
-
|
|
119
|
-
echo '{"decision":"allow"}'
|
|
120
|
-
exit 0
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
# ============================================================================
|
|
124
|
-
# VALIDATION 2: Living docs folder validation
|
|
125
|
-
# ============================================================================
|
|
126
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/docs/internal/specs/([^/]+)/ ]]; then
|
|
127
|
-
PROJECT_NAME="${BASH_REMATCH[1]}"
|
|
128
|
-
|
|
129
|
-
# Skip README.md and _features/_archive special folders
|
|
130
|
-
[[ "$PROJECT_NAME" == "README.md" ]] && echo '{"decision":"allow"}' && exit 0
|
|
131
|
-
[[ "$PROJECT_NAME" == "_features" ]] && echo '{"decision":"allow"}' && exit 0
|
|
132
|
-
[[ "$PROJECT_NAME" == "_archive" ]] && echo '{"decision":"allow"}' && exit 0
|
|
133
|
-
|
|
134
|
-
# Check for template placeholders
|
|
135
|
-
if [[ "$PROJECT_NAME" =~ \{\{.*\}\} ]]; then
|
|
136
|
-
printf '{"decision":"block","reason":"🚫 UNRESOLVED PLACEHOLDER: %s\\n\\n🔧 FIX: Replace {{...}} with actual project name"}\n' "$PROJECT_NAME"
|
|
137
|
-
exit 2
|
|
138
|
-
fi
|
|
139
|
-
|
|
140
|
-
# Check for comma-separated (invalid)
|
|
141
|
-
if [[ "$PROJECT_NAME" =~ , ]]; then
|
|
142
|
-
printf '{"decision":"block","reason":"🚫 COMMA-SEPARATED PROJECTS: %s\\n\\nEach User Story = ONE project folder.\\n\\n🔧 FIX: Split into separate specs"}\n' "$PROJECT_NAME"
|
|
143
|
-
exit 2
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
# Check for common example/placeholder names
|
|
147
|
-
EXAMPLE_NAMES="frontend-app|backend-api|mobile-app|shared-lib|acme-corp|my-app|myapp|example-project|test-project"
|
|
148
|
-
if [[ "$PROJECT_NAME" =~ ^($EXAMPLE_NAMES)$ ]]; then
|
|
149
|
-
# Try to get valid projects from config
|
|
150
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
151
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
152
|
-
|
|
153
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
154
|
-
# Check if this example name is actually configured
|
|
155
|
-
IS_CONFIGURED=$(jq -r --arg name "$PROJECT_NAME" '.multiProject.projects[$name] // .project.name == $name' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
156
|
-
|
|
157
|
-
if [[ "$IS_CONFIGURED" != "true" ]]; then
|
|
158
|
-
VALID_PROJECTS=$(jq -r '.multiProject.projects | keys | join(", ") // .project.name // "specweave"' "$CONFIG_FILE" 2>/dev/null || echo "specweave")
|
|
159
|
-
printf '{"decision":"block","reason":"🚫 EXAMPLE PROJECT NAME: %s\\n\\nThis looks like a placeholder/example name from documentation.\\n\\nConfigured projects: %s\\n\\n🔧 FIX: Edit spec.md and use a real project name"}\n' "$PROJECT_NAME" "$VALID_PROJECTS"
|
|
160
|
-
exit 2
|
|
161
|
-
fi
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
echo '{"decision":"allow"}'
|
|
166
|
-
exit 0
|
|
167
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
168
11
|
|
|
169
|
-
|
|
12
|
+
set +e
|
|
170
13
|
echo '{"decision":"allow"}'
|
|
171
14
|
exit 0
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Comprehensive test suite for metadata-json-guard.sh
|
|
3
|
-
# Tests that spec.md creation is blocked when metadata.json is missing
|
|
4
|
-
#
|
|
5
|
-
# Usage: bash metadata-json-guard.test.sh
|
|
6
|
-
#
|
|
7
|
-
# v0.34.0 - Initial test suite
|
|
8
|
-
|
|
9
|
-
set -e
|
|
10
|
-
|
|
11
|
-
GUARD="$(dirname "$0")/metadata-json-guard.sh"
|
|
12
|
-
TEST_DIR=$(mktemp -d)
|
|
13
|
-
PASS=0
|
|
14
|
-
FAIL=0
|
|
15
|
-
TOTAL=0
|
|
16
|
-
|
|
17
|
-
# Colors for output
|
|
18
|
-
RED='\033[0;31m'
|
|
19
|
-
GREEN='\033[0;32m'
|
|
20
|
-
YELLOW='\033[1;33m'
|
|
21
|
-
NC='\033[0m' # No Color
|
|
22
|
-
|
|
23
|
-
cleanup() {
|
|
24
|
-
rm -rf "$TEST_DIR"
|
|
25
|
-
}
|
|
26
|
-
trap cleanup EXIT
|
|
27
|
-
|
|
28
|
-
# Create test increment structure
|
|
29
|
-
setup_test_increment() {
|
|
30
|
-
local with_metadata="$1"
|
|
31
|
-
local increment_name="${2:-0001-test-feature}"
|
|
32
|
-
|
|
33
|
-
mkdir -p "$TEST_DIR/.specweave/increments/$increment_name"
|
|
34
|
-
|
|
35
|
-
if [ "$with_metadata" = "true" ]; then
|
|
36
|
-
cat > "$TEST_DIR/.specweave/increments/$increment_name/metadata.json" << 'EOF'
|
|
37
|
-
{
|
|
38
|
-
"id": "0001-test-feature",
|
|
39
|
-
"status": "planned",
|
|
40
|
-
"type": "feature",
|
|
41
|
-
"priority": "P1",
|
|
42
|
-
"created": "2025-12-10T00:00:00Z",
|
|
43
|
-
"lastActivity": "2025-12-10T00:00:00Z",
|
|
44
|
-
"testMode": "TDD",
|
|
45
|
-
"coverageTarget": 95,
|
|
46
|
-
"feature_id": null,
|
|
47
|
-
"epic_id": null,
|
|
48
|
-
"externalLinks": {}
|
|
49
|
-
}
|
|
50
|
-
EOF
|
|
51
|
-
fi
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Test helper: should block
|
|
55
|
-
test_should_block() {
|
|
56
|
-
local name="$1"
|
|
57
|
-
local file_path="$2"
|
|
58
|
-
local content="$3"
|
|
59
|
-
TOTAL=$((TOTAL + 1))
|
|
60
|
-
|
|
61
|
-
# Build JSON input for the guard
|
|
62
|
-
local json_input
|
|
63
|
-
json_input=$(jq -n \
|
|
64
|
-
--arg tool_name "Write" \
|
|
65
|
-
--arg file_path "$file_path" \
|
|
66
|
-
--arg content "$content" \
|
|
67
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
68
|
-
|
|
69
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
70
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
71
|
-
|
|
72
|
-
if [[ "$exit_code" == "2" ]]; then
|
|
73
|
-
echo -e "${GREEN}✓ BLOCKED${NC}: $name"
|
|
74
|
-
PASS=$((PASS + 1))
|
|
75
|
-
else
|
|
76
|
-
echo -e "${RED}✗ NOT BLOCKED${NC}: $name"
|
|
77
|
-
echo " File: $file_path"
|
|
78
|
-
echo " Exit code: $exit_code (expected 2)"
|
|
79
|
-
echo " Output: $(echo "$result" | head -5)"
|
|
80
|
-
FAIL=$((FAIL + 1))
|
|
81
|
-
fi
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Test helper: should allow
|
|
85
|
-
test_should_allow() {
|
|
86
|
-
local name="$1"
|
|
87
|
-
local file_path="$2"
|
|
88
|
-
local content="$3"
|
|
89
|
-
TOTAL=$((TOTAL + 1))
|
|
90
|
-
|
|
91
|
-
# Build JSON input for the guard
|
|
92
|
-
local json_input
|
|
93
|
-
json_input=$(jq -n \
|
|
94
|
-
--arg tool_name "Write" \
|
|
95
|
-
--arg file_path "$file_path" \
|
|
96
|
-
--arg content "$content" \
|
|
97
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
98
|
-
|
|
99
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
100
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
101
|
-
|
|
102
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
103
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: $name"
|
|
104
|
-
PASS=$((PASS + 1))
|
|
105
|
-
else
|
|
106
|
-
echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
|
|
107
|
-
echo " File: $file_path"
|
|
108
|
-
echo " Exit code: $exit_code (expected 0)"
|
|
109
|
-
FAIL=$((FAIL + 1))
|
|
110
|
-
fi
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
# Test non-Write tools (should always allow)
|
|
114
|
-
test_non_write_tool() {
|
|
115
|
-
local name="$1"
|
|
116
|
-
local tool_name="$2"
|
|
117
|
-
TOTAL=$((TOTAL + 1))
|
|
118
|
-
|
|
119
|
-
local json_input
|
|
120
|
-
json_input=$(jq -n \
|
|
121
|
-
--arg tool_name "$tool_name" \
|
|
122
|
-
--arg file_path "$TEST_DIR/.specweave/increments/0001-test/spec.md" \
|
|
123
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path}}')
|
|
124
|
-
|
|
125
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
126
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
127
|
-
|
|
128
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
129
|
-
echo -e "${GREEN}✓ ALLOWED (non-Write)${NC}: $name"
|
|
130
|
-
PASS=$((PASS + 1))
|
|
131
|
-
else
|
|
132
|
-
echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
|
|
133
|
-
FAIL=$((FAIL + 1))
|
|
134
|
-
fi
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
echo "========================================"
|
|
138
|
-
echo " METADATA JSON GUARD - COMPREHENSIVE TESTS"
|
|
139
|
-
echo "========================================"
|
|
140
|
-
echo ""
|
|
141
|
-
echo "Test directory: $TEST_DIR"
|
|
142
|
-
echo ""
|
|
143
|
-
|
|
144
|
-
echo -e "${YELLOW}=== CORE FUNCTIONALITY: BLOCK WHEN METADATA MISSING ===${NC}"
|
|
145
|
-
|
|
146
|
-
# Setup without metadata
|
|
147
|
-
setup_test_increment "false" "0001-no-metadata"
|
|
148
|
-
test_should_block "spec.md without metadata.json (3-digit)" \
|
|
149
|
-
"$TEST_DIR/.specweave/increments/0001-no-metadata/spec.md" \
|
|
150
|
-
"---\nincrement: 0001-no-metadata\n---"
|
|
151
|
-
|
|
152
|
-
setup_test_increment "false" "0012-no-metadata"
|
|
153
|
-
test_should_block "spec.md without metadata.json (4-digit)" \
|
|
154
|
-
"$TEST_DIR/.specweave/increments/0012-no-metadata/spec.md" \
|
|
155
|
-
"---\nincrement: 0012-no-metadata\n---"
|
|
156
|
-
|
|
157
|
-
setup_test_increment "false" "123-short"
|
|
158
|
-
test_should_block "spec.md without metadata.json (3-digit short)" \
|
|
159
|
-
"$TEST_DIR/.specweave/increments/123-short/spec.md" \
|
|
160
|
-
"---\nincrement: 123-short\n---"
|
|
161
|
-
|
|
162
|
-
setup_test_increment "false" "0001E-external"
|
|
163
|
-
test_should_block "spec.md without metadata.json (external E suffix)" \
|
|
164
|
-
"$TEST_DIR/.specweave/increments/0001E-external/spec.md" \
|
|
165
|
-
"---\nincrement: 0001E-external\n---"
|
|
166
|
-
|
|
167
|
-
echo ""
|
|
168
|
-
echo -e "${YELLOW}=== CORE FUNCTIONALITY: ALLOW WHEN METADATA EXISTS ===${NC}"
|
|
169
|
-
|
|
170
|
-
# Setup with metadata
|
|
171
|
-
setup_test_increment "true" "0002-has-metadata"
|
|
172
|
-
test_should_allow "spec.md WITH metadata.json present" \
|
|
173
|
-
"$TEST_DIR/.specweave/increments/0002-has-metadata/spec.md" \
|
|
174
|
-
"---\nincrement: 0002-has-metadata\n---"
|
|
175
|
-
|
|
176
|
-
setup_test_increment "true" "0123E-external-with-meta"
|
|
177
|
-
test_should_allow "spec.md WITH metadata.json (external E suffix)" \
|
|
178
|
-
"$TEST_DIR/.specweave/increments/0123E-external-with-meta/spec.md" \
|
|
179
|
-
"---\nincrement: 0123E-external-with-meta\n---"
|
|
180
|
-
|
|
181
|
-
echo ""
|
|
182
|
-
echo -e "${YELLOW}=== NON-SPEC FILES: SHOULD NOT VALIDATE ===${NC}"
|
|
183
|
-
|
|
184
|
-
# Create increment without metadata for these tests
|
|
185
|
-
setup_test_increment "false" "0003-other-files"
|
|
186
|
-
test_should_allow "plan.md (not spec.md)" \
|
|
187
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/plan.md" \
|
|
188
|
-
"# Plan"
|
|
189
|
-
|
|
190
|
-
test_should_allow "tasks.md (not spec.md)" \
|
|
191
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/tasks.md" \
|
|
192
|
-
"# Tasks"
|
|
193
|
-
|
|
194
|
-
test_should_allow "metadata.json itself" \
|
|
195
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/metadata.json" \
|
|
196
|
-
'{"id": "test"}'
|
|
197
|
-
|
|
198
|
-
test_should_allow "report in subfolder" \
|
|
199
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/reports/COMPLETION.md" \
|
|
200
|
-
"# Report"
|
|
201
|
-
|
|
202
|
-
echo ""
|
|
203
|
-
echo -e "${YELLOW}=== FILES OUTSIDE INCREMENTS: SHOULD NOT VALIDATE ===${NC}"
|
|
204
|
-
|
|
205
|
-
test_should_allow "spec.md outside increments (docs)" \
|
|
206
|
-
"$TEST_DIR/.specweave/docs/internal/spec.md" \
|
|
207
|
-
"# Documentation"
|
|
208
|
-
|
|
209
|
-
test_should_allow "spec.md in root" \
|
|
210
|
-
"$TEST_DIR/spec.md" \
|
|
211
|
-
"# Root spec"
|
|
212
|
-
|
|
213
|
-
test_should_allow "Random file" \
|
|
214
|
-
"$TEST_DIR/random.txt" \
|
|
215
|
-
"Random content"
|
|
216
|
-
|
|
217
|
-
echo ""
|
|
218
|
-
echo -e "${YELLOW}=== NON-WRITE TOOLS: SHOULD ALWAYS ALLOW ===${NC}"
|
|
219
|
-
|
|
220
|
-
test_non_write_tool "Read tool" "Read"
|
|
221
|
-
test_non_write_tool "Edit tool" "Edit"
|
|
222
|
-
test_non_write_tool "Glob tool" "Glob"
|
|
223
|
-
test_non_write_tool "Grep tool" "Grep"
|
|
224
|
-
test_non_write_tool "Bash tool" "Bash"
|
|
225
|
-
|
|
226
|
-
echo ""
|
|
227
|
-
echo -e "${YELLOW}=== BYPASS MODES ===${NC}"
|
|
228
|
-
|
|
229
|
-
# Test SPECWEAVE_FORCE_METADATA bypass
|
|
230
|
-
setup_test_increment "false" "0004-bypass"
|
|
231
|
-
TOTAL=$((TOTAL + 1))
|
|
232
|
-
json_input=$(jq -n \
|
|
233
|
-
--arg tool_name "Write" \
|
|
234
|
-
--arg file_path "$TEST_DIR/.specweave/increments/0004-bypass/spec.md" \
|
|
235
|
-
--arg content "---\nincrement: 0004-bypass\n---" \
|
|
236
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
237
|
-
|
|
238
|
-
result=$(SPECWEAVE_FORCE_METADATA=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
|
|
239
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
240
|
-
if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q "bypassed"; then
|
|
241
|
-
echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_FORCE_METADATA=1 works"
|
|
242
|
-
PASS=$((PASS + 1))
|
|
243
|
-
else
|
|
244
|
-
echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_FORCE_METADATA=1"
|
|
245
|
-
echo " Exit code: $exit_code"
|
|
246
|
-
FAIL=$((FAIL + 1))
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
# Test SPECWEAVE_DISABLE_HOOKS bypass
|
|
250
|
-
TOTAL=$((TOTAL + 1))
|
|
251
|
-
result=$(SPECWEAVE_DISABLE_HOOKS=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
|
|
252
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
253
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
254
|
-
echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_DISABLE_HOOKS=1 works"
|
|
255
|
-
PASS=$((PASS + 1))
|
|
256
|
-
else
|
|
257
|
-
echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_DISABLE_HOOKS=1"
|
|
258
|
-
FAIL=$((FAIL + 1))
|
|
259
|
-
fi
|
|
260
|
-
|
|
261
|
-
echo ""
|
|
262
|
-
echo -e "${YELLOW}=== EDGE CASES ===${NC}"
|
|
263
|
-
|
|
264
|
-
# Test with empty file path
|
|
265
|
-
TOTAL=$((TOTAL + 1))
|
|
266
|
-
json_input='{"tool_name": "Write", "tool_input": {"file_path": "", "content": "test"}}'
|
|
267
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
268
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
269
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
270
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: Empty file path"
|
|
271
|
-
PASS=$((PASS + 1))
|
|
272
|
-
else
|
|
273
|
-
echo -e "${RED}✗ FAILED${NC}: Empty file path should be allowed"
|
|
274
|
-
FAIL=$((FAIL + 1))
|
|
275
|
-
fi
|
|
276
|
-
|
|
277
|
-
# Test with malformed JSON (should fail gracefully)
|
|
278
|
-
TOTAL=$((TOTAL + 1))
|
|
279
|
-
result=$(echo "not json" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
280
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
281
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
282
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: Malformed JSON handled gracefully"
|
|
283
|
-
PASS=$((PASS + 1))
|
|
284
|
-
else
|
|
285
|
-
echo -e "${RED}✗ FAILED${NC}: Malformed JSON should be allowed (fail-safe)"
|
|
286
|
-
FAIL=$((FAIL + 1))
|
|
287
|
-
fi
|
|
288
|
-
|
|
289
|
-
echo ""
|
|
290
|
-
echo "========================================"
|
|
291
|
-
echo " RESULTS"
|
|
292
|
-
echo "========================================"
|
|
293
|
-
echo -e "Total: $TOTAL"
|
|
294
|
-
echo -e "${GREEN}Passed: $PASS${NC}"
|
|
295
|
-
if [[ $FAIL -gt 0 ]]; then
|
|
296
|
-
echo -e "${RED}Failed: $FAIL${NC}"
|
|
297
|
-
exit 1
|
|
298
|
-
else
|
|
299
|
-
echo -e "Failed: 0"
|
|
300
|
-
echo ""
|
|
301
|
-
echo -e "${GREEN}ALL TESTS PASSED!${NC}"
|
|
302
|
-
fi
|