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 CHANGED
@@ -1,10 +1,10 @@
1
- <!-- SW:META template="claude" version="1.0.38" 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.38" -->
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.38" -->
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.38" -->
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.38" -->
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" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.38" -->
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.39",
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 - WARN (not block) on direct editing of metadata.json to "completed" status
2
+ # completion-guard.sh - DISABLED (v1.0.38)
3
3
  #
4
- # v0.28.63+: Detects the auto-completion bug by warning on direct status changes to completed.
5
- # Status SHOULD go through ready_for_review first, and /sw:done marks completed with 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"
6
6
  #
7
- # v1.0.37+: CRITICAL CHANGE - Now WARNS instead of BLOCKING!
8
- # User feedback: "you MUST NEVER block such operations... do at least warning"
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
- # PreToolUse hook - WARNS but ALLOWS the tool call
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
- # Only care about metadata.json files
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 - WARN (not block) on duplicate increment IDs
2
+ # increment-duplicate-guard.sh - DISABLED (v1.0.38)
3
3
  #
4
- # v0.33.0+: Detects duplicate increment numbers (0121 and 0121 both existing)
5
- # Also detects 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
- # v1.0.37+: CRITICAL CHANGE - Now WARNS instead of BLOCKING!
8
- # User feedback: "you MUST NEVER block such operations... do at least warning"
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
- # PreToolUse hook for Write tool - WARNS but ALLOWS the tool call
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
- if [[ ! -d "$INCREMENTS_DIR" ]]; then
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
- # 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 WARNS (not blocks) if metadata.json is missing before spec.md creation.
6
- # This helps ensure proper increment creation workflow.
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
- # 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 - 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
- # v1.0.37+: CRITICAL CHANGE - All validations now WARN instead of BLOCK!
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
- # Not a spec.md or living docs file - allow
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