specweave 1.0.39 → 1.0.41

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.
@@ -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,13 +1,24 @@
1
1
  #!/bin/bash
2
- # github-sync-handler.sh - Sync increment status to GitHub issue
2
+ # github-sync-handler.sh - Sync increment to GitHub (create issues for User Stories)
3
3
  # Called async by processor, non-blocking, error-tolerant
4
4
  #
5
+ # Argument formats supported:
6
+ # 1. (event_type, increment_id) - from lifecycle/spec.updated events
7
+ # 2. (increment_id) - from metadata.changed events (legacy)
8
+ #
5
9
  # IMPORTANT: Never crash Claude, always exit 0
6
10
  set +e
7
11
 
8
12
  [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
9
13
 
14
+ # Support both argument formats:
15
+ # - Called from increment.created/spec.updated: $1 = event_type, $2 = increment_id
16
+ # - Called from metadata.changed: $1 = increment_id
10
17
  INC_ID="${1:-}"
18
+ if [[ "$INC_ID" == increment.* ]] || [[ "$INC_ID" == spec.* ]] || [[ "$INC_ID" == metadata.* ]]; then
19
+ # First arg is event type, second is increment ID
20
+ INC_ID="${2:-}"
21
+ fi
11
22
  [[ -z "$INC_ID" ]] && exit 0
12
23
 
13
24
  # Find project root
@@ -5,14 +5,17 @@
5
5
  # Usage: processor.sh [--daemon]
6
6
  #
7
7
  # Event routing (EDA v2):
8
- # - increment.created/done/archived/reopened -> living-specs-handler + status-line-handler + project-bridge-handler
8
+ # - increment.created/done/archived/reopened -> living-specs-handler + status-line-handler + project-bridge-handler + github-sync-handler
9
9
  # - user-story.completed/reopened -> status-line-handler + project-bridge-handler
10
- # - task.updated/spec.updated -> living-docs-handler (legacy)
10
+ # - spec.updated -> living-docs-handler + ac-validation-handler + github-sync-handler (creates GitHub issues for User Stories)
11
+ # - task.updated -> living-docs-handler + ac-validation-handler (legacy)
11
12
  # - metadata.changed -> github-sync-handler
12
13
  #
13
14
  # The project-bridge-handler connects increment events to project-level EDA,
14
15
  # enabling automatic sync to GitHub, ADO, and JIRA via ProjectService.
15
16
  #
17
+ # The github-sync-handler creates GitHub issues for User Stories when spec.md is updated.
18
+ #
16
19
  # Self-terminates after 60s of idle
17
20
  #
18
21
  # IMPORTANT: Uses cross-platform locking (mkdir is atomic on all POSIX systems)
@@ -152,13 +155,16 @@ process_event() {
152
155
  # EDA Event Routing (new architecture)
153
156
  # ========================================
154
157
 
155
- # Lifecycle events -> living-specs-handler + status-line-handler + project-bridge-handler
158
+ # Lifecycle events -> living-specs + status-line + project-bridge + github-sync
159
+ # Note: github-sync-handler added to ensure GitHub issues are created for User Stories
156
160
  increment.created|increment.done|increment.archived|increment.reopened)
157
161
  run_handler "$HANDLER_DIR/living-specs-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
158
162
  # Also update status line on lifecycle changes
159
163
  run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
160
164
  # Bridge to project-level EDA (triggers sync to GitHub/ADO/JIRA)
161
165
  run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
166
+ # Sync to GitHub (creates issues for User Stories)
167
+ run_handler "$HANDLER_DIR/github-sync-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
162
168
  ;;
163
169
 
164
170
  # User story events -> status-line-handler + project-bridge-handler
@@ -168,10 +174,18 @@ process_event() {
168
174
  run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
169
175
  ;;
170
176
 
171
- # ========================================
172
- # Legacy event routing (backward compat)
173
- # ========================================
174
- task.updated|spec.updated)
177
+ # Spec updated -> sync to living docs + validate ACs + sync to GitHub
178
+ # CRITICAL: spec.updated fires AFTER spec.md has User Stories defined
179
+ # This is the key event that triggers GitHub issue creation for USs
180
+ spec.updated)
181
+ run_handler "$HANDLER_DIR/living-docs-handler.sh" "" "$EVENT_DATA"
182
+ run_handler "$HANDLER_DIR/ac-validation-handler.sh" "" "$EVENT_DATA"
183
+ # Sync to GitHub (creates issues for User Stories when spec has USs)
184
+ run_handler "$HANDLER_DIR/github-sync-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
185
+ ;;
186
+
187
+ # Legacy task.updated event (backward compat)
188
+ task.updated)
175
189
  # Legacy: don't update status line on every task edit
176
190
  # That causes race conditions and flickering
177
191
  run_handler "$HANDLER_DIR/living-docs-handler.sh" "" "$EVENT_DATA"
@@ -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