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 CHANGED
@@ -1,10 +1,10 @@
1
- <!-- SW:META template="claude" version="1.0.34" sections="header,start,autodetect,metarule,rules,workflow,structure,taskformat,secrets,syncing,mapping,testing,limits,troubleshooting,principles,linking,docs" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.34" -->
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.38",
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 - Block direct editing of metadata.json to "completed" status
2
+ # completion-guard.sh - DISABLED (v1.0.38)
3
3
  #
4
- # v0.28.63+: Prevents the auto-completion bug by blocking direct status changes to completed.
5
- # Status MUST go through ready_for_review first, and only /sw:done can mark completed.
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
- # PreToolUse hook - can BLOCK the tool call by returning non-zero exit code
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
- # IMPORTANT: This is a safety guard. Exit 0 allows, exit 2 blocks.
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
- # Extract the content being written (new_string for Edit, content for Write)
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 - Block creation of duplicate increment IDs
2
+ # increment-duplicate-guard.sh - DISABLED (v1.0.38)
3
3
  #
4
- # v0.33.0+: Prevents duplicate increment numbers (0121 and 0121 both existing)
5
- # Also prevents 0001 and 0001E collisions (they share the same base number)
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
- # PreToolUse hook for Write tool - BLOCKS the tool call if duplicate detected
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
- # CRITICAL: This guards against the BUG where two increments get the same ID:
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
- # Scan ALL directories for existing increment with the same number
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
- # metadata-json-guard.sh
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
- # Pre-tool-use hook that ensures metadata.json exists BEFORE spec.md can be created.
6
- # This prevents increments from being created without proper metadata.
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
- # ROOT CAUSE: When Claude creates increments via user prompt (not /sw:increment),
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
- exit 2
12
+ set +e
13
+ echo '{"decision":"allow"}'
14
+ exit 0
@@ -1,171 +1,14 @@
1
1
  #!/bin/bash
2
- # spec-validation-guard.sh - Consolidated spec.md and living docs validation
2
+ # spec-validation-guard.sh - DISABLED (v1.0.38)
3
3
  #
4
- # COMBINES (v0.35.4+):
5
- # - per-us-project-validator.sh (deleted) - Per-US **Project**: validation
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
- # Activation:
10
- # - tool_name: Write
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
- # Exit 0 = allow (with JSON), Exit 2 = block (with JSON)
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
- # Not a spec.md or living docs file - allow
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