specweave 1.0.39 → 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 -18
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +7 -69
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +7 -142
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +8 -92
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +7 -168
- 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,24 +77,6 @@ guard_allow() {
|
|
|
77
77
|
exit 0
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
# Block the tool call with reason (DEPRECATED v1.0.37 - use guard_warn instead)
|
|
81
|
-
# NOTE: Blocking is discouraged. Use guard_warn for most cases.
|
|
82
|
-
guard_block() {
|
|
83
|
-
local reason="$1"
|
|
84
|
-
reason=$(echo "$reason" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
|
85
|
-
printf '{"decision":"block","reason":"%s"}\n' "$reason"
|
|
86
|
-
exit 2
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# Warn but ALLOW the tool call (v1.0.37+)
|
|
90
|
-
# This is the PREFERRED approach - never block operations, always allow with warnings.
|
|
91
|
-
guard_warn() {
|
|
92
|
-
local message="$1"
|
|
93
|
-
message=$(echo "$message" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
|
94
|
-
printf '{"decision":"allow","message":"%s"}\n' "$message"
|
|
95
|
-
exit 0
|
|
96
|
-
}
|
|
97
|
-
|
|
98
80
|
# ============================================================================
|
|
99
81
|
# SIMPLE LOGGING (optional, writes to .specweave/logs/hooks.log)
|
|
100
82
|
# ============================================================================
|
|
@@ -1,76 +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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Business logic and validation should be in scripts/agents, not hard blocks.
|
|
7
|
+
# Completion workflow should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# IMPORTANT: This is a safety guard. Exit 0 allows (with warning if needed).
|
|
14
|
-
# All exit paths MUST output JSON for proper Claude Code handling.
|
|
15
|
-
set +e
|
|
16
|
-
|
|
17
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
18
|
-
|
|
19
|
-
# Read stdin for tool input
|
|
20
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
21
|
-
|
|
22
|
-
# Check if this is editing metadata.json with status: completed
|
|
23
|
-
# Pattern: file_path contains metadata.json AND (new_string OR content) contains "status"..."completed"
|
|
24
|
-
|
|
25
|
-
# Extract file_path
|
|
26
|
-
# Claude Code passes tool input in .tool_input.file_path format
|
|
27
|
-
if command -v jq &> /dev/null; then
|
|
28
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)
|
|
29
|
-
else
|
|
30
|
-
FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
31
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
32
11
|
|
|
33
|
-
|
|
34
|
-
if [[ "$FILE_PATH" != *metadata.json ]]; then
|
|
35
|
-
echo '{"decision":"allow"}'
|
|
36
|
-
exit 0 # Allow
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
# Extract the content being written (new_string for Edit, content for Write)
|
|
40
|
-
# Claude Code passes tool input in .tool_input format
|
|
41
|
-
if command -v jq &> /dev/null; then
|
|
42
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // .new_string // .content // empty' 2>/dev/null)
|
|
43
|
-
else
|
|
44
|
-
NEW_CONTENT=$(echo "$INPUT" | grep -o '"new_string"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1)
|
|
45
|
-
if [[ -z "$NEW_CONTENT" ]]; then
|
|
46
|
-
NEW_CONTENT=$(echo "$INPUT" | grep -o '"content"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1)
|
|
47
|
-
fi
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Check if trying to set status to "completed" directly
|
|
51
|
-
# This is a simple pattern match - if the edit/write contains status...completed
|
|
52
|
-
if echo "$NEW_CONTENT" | grep -q '"status"[[:space:]]*:[[:space:]]*"completed"'; then
|
|
53
|
-
# Read current status from file to check if coming from ready_for_review
|
|
54
|
-
if [[ -f "$FILE_PATH" ]]; then
|
|
55
|
-
CURRENT_STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$FILE_PATH" | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
56
|
-
|
|
57
|
-
if [[ "$CURRENT_STATUS" == "ready_for_review" ]]; then
|
|
58
|
-
# This is a valid transition - allow
|
|
59
|
-
echo '{"decision":"allow"}'
|
|
60
|
-
exit 0
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
# WARN - trying to set completed without going through ready_for_review (v1.0.37)
|
|
65
|
-
cat << 'WARN_EOF'
|
|
66
|
-
{
|
|
67
|
-
"decision": "allow",
|
|
68
|
-
"message": "⚠️ WARNING: Direct status change to \"completed\" detected!\n\n🚨 RECOMMENDED 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)\n\nOperation ALLOWED - proceeding with status change."
|
|
69
|
-
}
|
|
70
|
-
WARN_EOF
|
|
71
|
-
exit 0 # ALLOW with warning (v1.0.37)
|
|
72
|
-
fi
|
|
73
|
-
|
|
74
|
-
# Allow other edits to metadata.json
|
|
12
|
+
set +e
|
|
75
13
|
echo '{"decision":"allow"}'
|
|
76
14
|
exit 0
|
|
@@ -1,149 +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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Business logic and validation should be in scripts/agents, not hard blocks.
|
|
7
|
+
# Duplicate detection should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# Exit 0 = allow (with JSON warning message if duplicate detected)
|
|
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
|
-
# CRITICAL FIX (v1.0.37): ALWAYS allow metadata.json writes!
|
|
36
|
-
# metadata.json is the FIRST file created for any increment - it MUST succeed.
|
|
37
|
-
# Duplicate detection only makes sense for the INCREMENT FOLDER, not individual files.
|
|
38
|
-
# Once the folder exists, any file writes within it should be allowed.
|
|
39
|
-
if [[ "$FILE_PATH" == *metadata.json ]]; then
|
|
40
|
-
echo '{"decision":"allow","message":"metadata.json write allowed (increment creation)"}'
|
|
41
|
-
exit 0
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# Extract the increment folder name from the path
|
|
45
|
-
# Pattern: .specweave/increments/XXXX-name/file.md or .specweave/increments/XXXX-name/subfolder/file
|
|
46
|
-
# We need to extract "XXXX-name" part
|
|
47
|
-
|
|
48
|
-
# Remove the .specweave/increments/ prefix
|
|
49
|
-
AFTER_INCREMENTS=${FILE_PATH#*.specweave/increments/}
|
|
50
|
-
|
|
51
|
-
# Get the first path component (the increment folder)
|
|
52
|
-
INCREMENT_FOLDER=$(echo "$AFTER_INCREMENTS" | cut -d'/' -f1)
|
|
53
|
-
|
|
54
|
-
# Skip special folders
|
|
55
|
-
if [[ "$INCREMENT_FOLDER" == "_archive" ]] || [[ "$INCREMENT_FOLDER" == "_abandoned" ]] || [[ "$INCREMENT_FOLDER" == "_paused" ]] || [[ "$INCREMENT_FOLDER" == "README.md" ]]; then
|
|
56
|
-
echo '{"decision":"allow"}'
|
|
57
|
-
exit 0
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# Extract the increment number from folder name (handles both 0121-name and 0121E-name)
|
|
61
|
-
INCREMENT_NUM=$(echo "$INCREMENT_FOLDER" | grep -oE '^[0-9]{3,4}' | head -1)
|
|
62
|
-
|
|
63
|
-
if [[ -z "$INCREMENT_NUM" ]]; then
|
|
64
|
-
echo '{"decision":"allow"}'
|
|
65
|
-
exit 0 # Not a standard increment folder pattern - allow
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
# Normalize to 4 digits (strip leading zeros to avoid octal interpretation)
|
|
69
|
-
INCREMENT_NUM=$(printf "%04d" "$((10#$INCREMENT_NUM))")
|
|
70
|
-
|
|
71
|
-
# Find the increments root directory
|
|
72
|
-
INCREMENTS_DIR=$(echo "$FILE_PATH" | grep -o '.*/\.specweave/increments' | head -1)
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
73
11
|
|
|
74
|
-
|
|
75
|
-
# Increments directory doesn't exist yet - first increment, allow creation
|
|
76
|
-
echo '{"decision":"allow"}'
|
|
77
|
-
exit 0
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# Scan ALL directories for existing increment with the same number
|
|
81
|
-
DIRS_TO_CHECK=(
|
|
82
|
-
"$INCREMENTS_DIR"
|
|
83
|
-
"$INCREMENTS_DIR/_archive"
|
|
84
|
-
"$INCREMENTS_DIR/_abandoned"
|
|
85
|
-
"$INCREMENTS_DIR/_paused"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
FOUND_DUPLICATES=()
|
|
89
|
-
|
|
90
|
-
for DIR in "${DIRS_TO_CHECK[@]}"; do
|
|
91
|
-
if [[ ! -d "$DIR" ]]; then
|
|
92
|
-
continue
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# Find all folders matching this increment number (including E suffix variants)
|
|
96
|
-
while IFS= read -r -d '' EXISTING_FOLDER; do
|
|
97
|
-
EXISTING_NAME=$(basename "$EXISTING_FOLDER")
|
|
98
|
-
|
|
99
|
-
# Extract number from existing folder
|
|
100
|
-
EXISTING_NUM=$(echo "$EXISTING_NAME" | grep -oE '^[0-9]{3,4}' | head -1)
|
|
101
|
-
|
|
102
|
-
if [[ -z "$EXISTING_NUM" ]]; then
|
|
103
|
-
continue
|
|
104
|
-
fi
|
|
105
|
-
|
|
106
|
-
EXISTING_NUM=$(printf "%04d" "$((10#$EXISTING_NUM))")
|
|
107
|
-
|
|
108
|
-
# Check if same base number (0121 matches 0121, 0121E, etc.)
|
|
109
|
-
if [[ "$EXISTING_NUM" == "$INCREMENT_NUM" ]]; then
|
|
110
|
-
# Skip if it's the exact same folder we're creating
|
|
111
|
-
if [[ "$EXISTING_NAME" == "$INCREMENT_FOLDER" ]]; then
|
|
112
|
-
continue
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
# Found a duplicate!
|
|
116
|
-
FOUND_DUPLICATES+=("$EXISTING_NAME (in $(basename "$DIR"))")
|
|
117
|
-
fi
|
|
118
|
-
done < <(find "$DIR" -maxdepth 1 -type d -name "${INCREMENT_NUM}*" -print0 2>/dev/null)
|
|
119
|
-
done
|
|
120
|
-
|
|
121
|
-
# If duplicates found, WARN but ALLOW the operation (v1.0.37+: no blocking!)
|
|
122
|
-
if [[ ${#FOUND_DUPLICATES[@]} -gt 0 ]]; then
|
|
123
|
-
# Format duplicates for JSON
|
|
124
|
-
DUP_LIST=""
|
|
125
|
-
for DUP in "${FOUND_DUPLICATES[@]}"; do
|
|
126
|
-
DUP_LIST="${DUP_LIST}\\n - ${DUP}"
|
|
127
|
-
done
|
|
128
|
-
|
|
129
|
-
# Calculate the next available number using gap-filling strategy
|
|
130
|
-
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)
|
|
131
|
-
NEXT_NUM=1
|
|
132
|
-
while echo "$EXISTING_NUMS" | grep -q "^$(printf "%04d" $NEXT_NUM)$\|^$(printf "%03d" $NEXT_NUM)$"; do
|
|
133
|
-
NEXT_NUM=$((NEXT_NUM + 1))
|
|
134
|
-
done
|
|
135
|
-
NEXT_NUM_PADDED=$(printf "%04d" $NEXT_NUM)
|
|
136
|
-
|
|
137
|
-
# Extract the name part from the attempted folder
|
|
138
|
-
FOLDER_NAME_PART=$(echo "$INCREMENT_FOLDER" | sed 's/^[0-9]\{3,4\}E\?-//')
|
|
139
|
-
SUGGESTED_FOLDER="${NEXT_NUM_PADDED}-${FOLDER_NAME_PART}"
|
|
140
|
-
|
|
141
|
-
# v1.0.37: ALLOW with WARNING instead of blocking
|
|
142
|
-
# User can still proceed - the warning provides guidance
|
|
143
|
-
printf '{"decision":"allow","message":"⚠️ DUPLICATE INCREMENT ID DETECTED\\n\\n✅ SUGGESTED: %s\\n⚠️ CURRENT: %s\\n\\nNumber %s already exists:%s\\n\\n💡 RECOMMENDATION: Consider using .specweave/increments/%s/ instead.\\n\\nOperation ALLOWED - proceeding with current path."}\n' "$SUGGESTED_FOLDER" "$INCREMENT_FOLDER" "$INCREMENT_NUM" "$DUP_LIST" "$SUGGESTED_FOLDER"
|
|
144
|
-
exit 0 # ALLOW with warning (v1.0.37: no blocking!)
|
|
145
|
-
fi
|
|
146
|
-
|
|
147
|
-
# No duplicates - allow
|
|
12
|
+
set +e
|
|
148
13
|
echo '{"decision":"allow"}'
|
|
149
14
|
exit 0
|
|
@@ -1,98 +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
|
-
# v1.0.37+: CRITICAL CHANGE - Now WARNS instead of BLOCKING!
|
|
16
|
-
# User feedback: "you MUST NEVER block such operations... do at least warning"
|
|
17
|
-
# Business logic and validation should be in scripts/agents, not hard blocks.
|
|
18
|
-
#
|
|
19
|
-
# SOLUTION: WARN if spec.md is created without metadata.json, but ALLOW the operation.
|
|
20
|
-
# The warning prompts Claude to create metadata.json immediately after.
|
|
21
|
-
#
|
|
22
|
-
# Activation:
|
|
23
|
-
# - tool_name: Write
|
|
24
|
-
# - file_path matches: .specweave/increments/*/spec.md
|
|
25
|
-
#
|
|
26
|
-
# Returns exit 0 (allow) with warning message if metadata.json missing
|
|
27
|
-
#
|
|
28
|
-
# Bypass: Set SPECWEAVE_FORCE_METADATA=1 to skip validation
|
|
29
|
-
#
|
|
30
|
-
# v0.34.0 - Initial implementation based on user project bug analysis
|
|
31
|
-
# v1.0.37 - Changed from BLOCK to WARN (allow with warning message)
|
|
32
|
-
|
|
33
|
-
set +e # CRITICAL: Never use set -e in hooks (causes cascading failures)
|
|
34
|
-
|
|
35
|
-
# Check for force bypass
|
|
36
|
-
if [ "$SPECWEAVE_FORCE_METADATA" = "1" ]; then
|
|
37
|
-
echo '{"decision": "allow", "message": "metadata.json guard bypassed (SPECWEAVE_FORCE_METADATA=1)"}'
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
# Disable hooks bypass
|
|
42
|
-
if [ "$SPECWEAVE_DISABLE_HOOKS" = "1" ]; then
|
|
43
|
-
echo '{"decision": "allow"}'
|
|
44
|
-
exit 0
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
# Read tool input from stdin (safe handling)
|
|
48
|
-
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
49
|
-
|
|
50
|
-
# Check jq availability - allow if not present
|
|
51
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
52
|
-
echo '{"decision": "allow"}'
|
|
53
|
-
exit 0
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Extract tool name - with jq fallback
|
|
57
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool_input.tool_name // ""' 2>/dev/null || echo "")
|
|
58
|
-
|
|
59
|
-
# Only validate Write tool calls
|
|
60
|
-
if [ "$TOOL_NAME" != "Write" ]; then
|
|
61
|
-
echo '{"decision": "allow"}'
|
|
62
|
-
exit 0
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# Extract file path - handle both formats
|
|
66
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
67
|
-
|
|
68
|
-
# Only validate spec.md files in increments folder
|
|
69
|
-
# Match: 3-4 digits, optional E suffix, kebab-case name, spec.md
|
|
70
|
-
if [[ ! "$FILE_PATH" =~ \.specweave/increments/([0-9]{3,4}E?-[^/]+)/spec\.md$ ]]; then
|
|
71
|
-
echo '{"decision": "allow"}'
|
|
72
|
-
exit 0
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# Extract increment folder path
|
|
76
|
-
INCREMENT_DIR=$(dirname "$FILE_PATH")
|
|
77
|
-
INCREMENT_ID="${BASH_REMATCH[1]}"
|
|
78
|
-
|
|
79
|
-
# Check if metadata.json exists in the same increment folder
|
|
80
|
-
METADATA_PATH="${INCREMENT_DIR}/metadata.json"
|
|
81
|
-
|
|
82
|
-
if [ -f "$METADATA_PATH" ]; then
|
|
83
|
-
# metadata.json exists, allow spec.md creation
|
|
84
|
-
echo '{"decision": "allow"}'
|
|
85
|
-
exit 0
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
# metadata.json doesn't exist - WARN but ALLOW spec.md creation (v1.0.37)
|
|
89
|
-
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
90
|
-
|
|
91
|
-
cat << WARN_EOF
|
|
92
|
-
{
|
|
93
|
-
"decision": "allow",
|
|
94
|
-
"message": "⚠️ WARNING: metadata.json MISSING - Creating spec.md without it!\n\n🚨 IMMEDIATE ACTION REQUIRED:\nCreate metadata.json NOW for this increment to work properly.\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📋 CREATE metadata.json:\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\nOperation ALLOWED - proceeding with spec.md write."
|
|
95
|
-
}
|
|
96
|
-
WARN_EOF
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
97
11
|
|
|
12
|
+
set +e
|
|
13
|
+
echo '{"decision":"allow"}'
|
|
98
14
|
exit 0
|
|
@@ -1,175 +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
|
-
# User feedback: "you MUST NEVER block such operations... do at least warning"
|
|
15
|
-
# Business logic and validation should be in scripts/agents, not hard blocks.
|
|
16
|
-
#
|
|
17
|
-
# Exit 0 = allow (with JSON warning message if validation fails)
|
|
18
|
-
#
|
|
19
|
-
# Bypasses:
|
|
20
|
-
# - SPECWEAVE_DISABLE_HOOKS=1 - Disable all hooks
|
|
21
|
-
# - SPECWEAVE_FORCE_PROJECT=1 - Skip project validation
|
|
22
|
-
# - SPECWEAVE_FORCE_METADATA=1 - Skip all spec validation
|
|
23
|
-
|
|
24
|
-
set +e # CRITICAL: Never use set -e in hooks
|
|
25
|
-
|
|
26
|
-
# Source shared library if available
|
|
27
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
-
LIB_DIR="${SCRIPT_DIR}/../../lib"
|
|
29
|
-
if [[ -f "$LIB_DIR/common-setup.sh" ]]; then
|
|
30
|
-
source "$LIB_DIR/common-setup.sh"
|
|
31
|
-
init_pretool_guard || exit 0
|
|
32
|
-
else
|
|
33
|
-
# Fallback inline implementation
|
|
34
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
35
|
-
HOOK_INPUT=$(cat 2>/dev/null || echo '{}')
|
|
36
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
37
|
-
echo '{"decision":"allow"}'
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
40
|
-
HOOK_TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
|
41
|
-
HOOK_FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
42
|
-
HOOK_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null || echo "")
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# Check bypass flags
|
|
46
|
-
[[ "$SPECWEAVE_FORCE_PROJECT" == "1" ]] && echo '{"decision":"allow","message":"Project validation bypassed"}' && exit 0
|
|
47
|
-
[[ "$SPECWEAVE_FORCE_METADATA" == "1" ]] && echo '{"decision":"allow","message":"Spec validation bypassed"}' && exit 0
|
|
48
|
-
|
|
49
|
-
# Only validate Write tool
|
|
50
|
-
[[ "$HOOK_TOOL_NAME" != "Write" ]] && echo '{"decision":"allow"}' && exit 0
|
|
51
|
-
|
|
52
|
-
# No file path = allow
|
|
53
|
-
[[ -z "$HOOK_FILE_PATH" ]] && echo '{"decision":"allow"}' && exit 0
|
|
54
|
-
|
|
55
|
-
# ============================================================================
|
|
56
|
-
# VALIDATION 1: spec.md placeholder detection
|
|
57
|
-
# ============================================================================
|
|
58
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/increments/[0-9]{3,4}E?-[^/]+/spec\.md$ ]]; then
|
|
59
|
-
|
|
60
|
-
# Check for unresolved {{...}} placeholders - WARN only (v1.0.37)
|
|
61
|
-
if echo "$HOOK_CONTENT" | grep -qE '\{\{[A-Z_]+\}\}'; then
|
|
62
|
-
PLACEHOLDERS=$(echo "$HOOK_CONTENT" | grep -oE '\{\{[A-Z_]+\}\}' | sort -u | tr '\n' ', ' | sed 's/,$//')
|
|
63
|
-
printf '{"decision":"allow","message":"⚠️ UNRESOLVED PLACEHOLDERS DETECTED\\n\\nFound: %s\\n\\n🔧 FIX: Replace placeholders with actual values.\\nRun: specweave context projects\\nThen use values from the JSON output.\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PLACEHOLDERS"
|
|
64
|
-
exit 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# Check for **Project**: field in User Stories (soft validation - warn, don't block)
|
|
68
|
-
# Pattern: ### US-XXX or #### US-XXX followed by **Project**:
|
|
69
|
-
US_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^#{3,4} US-' 2>/dev/null || echo "0")
|
|
70
|
-
PROJECT_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Project\*\*:' 2>/dev/null || echo "0")
|
|
71
|
-
|
|
72
|
-
# Trim to just the number
|
|
73
|
-
US_COUNT="${US_COUNT//[^0-9]/}"
|
|
74
|
-
PROJECT_COUNT="${PROJECT_COUNT//[^0-9]/}"
|
|
75
|
-
[[ -z "$US_COUNT" ]] && US_COUNT=0
|
|
76
|
-
[[ -z "$PROJECT_COUNT" ]] && PROJECT_COUNT=0
|
|
77
|
-
|
|
78
|
-
# Only warn if there are User Stories but no Project fields
|
|
79
|
-
# Don't block - just allow with warning
|
|
80
|
-
if [[ "$US_COUNT" -gt 0 ]] && [[ "$PROJECT_COUNT" -eq 0 ]]; then
|
|
81
|
-
echo '{"decision":"allow","message":"⚠️ WARNING: No **Project**: fields found. Add **Project**: after each US heading for proper sync."}'
|
|
82
|
-
exit 0
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
# Check for comma-separated projects (1:1 mapping required) - WARN only (v1.0.37)
|
|
86
|
-
if echo "$HOOK_CONTENT" | grep -qE '^\*\*Project\*\*:.*,'; then
|
|
87
|
-
printf '{"decision":"allow","message":"⚠️ MULTIPLE PROJECTS IN ONE US DETECTED\\n\\nEach User Story should map to exactly ONE project.\\n\\n💡 RECOMMENDATION: 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\\nOperation ALLOWED - proceeding anyway."}\n'
|
|
88
|
-
exit 0
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Check structure level to validate **Board**: fields
|
|
92
|
-
# For 1-level structures (GitHub), **Board**: should NOT be present
|
|
93
|
-
# For 2-level structures (ADO/JIRA with boards), **Board**: is required
|
|
94
|
-
# NOTE: This is a WARNING only - do NOT block spec writes!
|
|
95
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
96
|
-
BOARD_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Board\*\*:' 2>/dev/null || echo "0")
|
|
97
|
-
BOARD_COUNT="${BOARD_COUNT//[^0-9]/}"
|
|
98
|
-
[[ -z "$BOARD_COUNT" ]] && BOARD_COUNT=0
|
|
99
|
-
|
|
100
|
-
if [[ "$BOARD_COUNT" -gt 0 ]]; then
|
|
101
|
-
# Has **Board**: fields - check if this is a 2-level structure
|
|
102
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
103
|
-
IS_2LEVEL="false"
|
|
104
|
-
|
|
105
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
106
|
-
# 2-level indicators: ADO areaPathMapping, JIRA boardMapping with multiple boards
|
|
107
|
-
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")
|
|
108
|
-
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")
|
|
109
|
-
HAS_MULTI_TEAMS=$(jq -r '.umbrella.childRepos | map(.team) | unique | length > 1' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
110
|
-
|
|
111
|
-
if [[ "$HAS_AREA_MAPPING" -gt 0 ]] || [[ "$HAS_BOARD_MAPPING" -gt 0 ]] || [[ "$HAS_MULTI_TEAMS" == "true" ]]; then
|
|
112
|
-
IS_2LEVEL="true"
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
# WARN only - never block spec.md writes (too disruptive)
|
|
117
|
-
if [[ "$IS_2LEVEL" != "true" ]]; then
|
|
118
|
-
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."}'
|
|
119
|
-
exit 0
|
|
120
|
-
fi
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
echo '{"decision":"allow"}'
|
|
124
|
-
exit 0
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
# ============================================================================
|
|
128
|
-
# VALIDATION 2: Living docs folder validation
|
|
129
|
-
# ============================================================================
|
|
130
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/docs/internal/specs/([^/]+)/ ]]; then
|
|
131
|
-
PROJECT_NAME="${BASH_REMATCH[1]}"
|
|
132
|
-
|
|
133
|
-
# Skip README.md and _features/_archive special folders
|
|
134
|
-
[[ "$PROJECT_NAME" == "README.md" ]] && echo '{"decision":"allow"}' && exit 0
|
|
135
|
-
[[ "$PROJECT_NAME" == "_features" ]] && echo '{"decision":"allow"}' && exit 0
|
|
136
|
-
[[ "$PROJECT_NAME" == "_archive" ]] && echo '{"decision":"allow"}' && exit 0
|
|
137
|
-
|
|
138
|
-
# Check for template placeholders - WARN only (v1.0.37)
|
|
139
|
-
if [[ "$PROJECT_NAME" =~ \{\{.*\}\} ]]; then
|
|
140
|
-
printf '{"decision":"allow","message":"⚠️ UNRESOLVED PLACEHOLDER: %s\\n\\n🔧 FIX: Replace {{...}} with actual project name\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME"
|
|
141
|
-
exit 0
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
# Check for comma-separated (invalid) - WARN only (v1.0.37)
|
|
145
|
-
if [[ "$PROJECT_NAME" =~ , ]]; then
|
|
146
|
-
printf '{"decision":"allow","message":"⚠️ COMMA-SEPARATED PROJECTS: %s\\n\\nEach User Story should have ONE project folder.\\n\\n💡 RECOMMENDATION: Split into separate specs\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME"
|
|
147
|
-
exit 0
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
# Check for common example/placeholder names - WARN only (v1.0.37)
|
|
151
|
-
EXAMPLE_NAMES="frontend-app|backend-api|mobile-app|shared-lib|acme-corp|my-app|myapp|example-project|test-project"
|
|
152
|
-
if [[ "$PROJECT_NAME" =~ ^($EXAMPLE_NAMES)$ ]]; then
|
|
153
|
-
# Try to get valid projects from config
|
|
154
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
155
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
156
|
-
|
|
157
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
158
|
-
# Check if this example name is actually configured
|
|
159
|
-
IS_CONFIGURED=$(jq -r --arg name "$PROJECT_NAME" '.multiProject.projects[$name] // .project.name == $name' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
160
|
-
|
|
161
|
-
if [[ "$IS_CONFIGURED" != "true" ]]; then
|
|
162
|
-
VALID_PROJECTS=$(jq -r '.multiProject.projects | keys | join(", ") // .project.name // "specweave"' "$CONFIG_FILE" 2>/dev/null || echo "specweave")
|
|
163
|
-
printf '{"decision":"allow","message":"⚠️ EXAMPLE PROJECT NAME DETECTED: %s\\n\\nThis looks like a placeholder/example name from documentation.\\n\\nConfigured projects: %s\\n\\n💡 RECOMMENDATION: Edit spec.md and use a real project name\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME" "$VALID_PROJECTS"
|
|
164
|
-
exit 0
|
|
165
|
-
fi
|
|
166
|
-
fi
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
echo '{"decision":"allow"}'
|
|
170
|
-
exit 0
|
|
171
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
172
11
|
|
|
173
|
-
|
|
12
|
+
set +e
|
|
174
13
|
echo '{"decision":"allow"}'
|
|
175
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
|