specweave 0.33.4 → 0.33.5
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 +20 -12
- package/dist/src/cli/commands/jobs.js +19 -2
- package/dist/src/cli/commands/jobs.js.map +1 -1
- package/dist/src/cli/commands/living-docs.js +1 -1
- package/dist/src/cli/commands/living-docs.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import-grouping.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import-grouping.js +11 -7
- package/dist/src/cli/helpers/init/external-import-grouping.js.map +1 -1
- package/dist/src/cli/workers/clone-worker.js +22 -5
- package/dist/src/cli/workers/clone-worker.js.map +1 -1
- package/dist/src/core/background/job-dependency.d.ts.map +1 -1
- package/dist/src/core/background/job-dependency.js +1 -0
- package/dist/src/core/background/job-dependency.js.map +1 -1
- package/dist/src/core/background/job-launcher.js +2 -2
- package/dist/src/core/background/job-launcher.js.map +1 -1
- package/dist/src/core/background/job-manager.d.ts +8 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -1
- package/dist/src/core/background/job-manager.js +19 -1
- package/dist/src/core/background/job-manager.js.map +1 -1
- package/dist/src/core/background/types.d.ts +9 -1
- package/dist/src/core/background/types.d.ts.map +1 -1
- package/dist/src/core/background/types.js +8 -1
- package/dist/src/core/background/types.js.map +1 -1
- package/dist/src/importers/external-importer.d.ts +26 -5
- package/dist/src/importers/external-importer.d.ts.map +1 -1
- package/dist/src/importers/item-converter.d.ts.map +1 -1
- package/dist/src/importers/item-converter.js +18 -1
- package/dist/src/importers/item-converter.js.map +1 -1
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +15 -1
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/living-docs/smart-doc-organizer.js +1 -1
- package/dist/src/living-docs/smart-doc-organizer.js.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.js +4 -0
- package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
- package/dist/src/utils/notification-constants.d.ts +8 -6
- package/dist/src/utils/notification-constants.d.ts.map +1 -1
- package/dist/src/utils/notification-constants.js +8 -6
- package/dist/src/utils/notification-constants.js.map +1 -1
- package/dist/src/utils/notification-manager.d.ts +24 -0
- package/dist/src/utils/notification-manager.d.ts.map +1 -1
- package/dist/src/utils/notification-manager.js +29 -0
- package/dist/src/utils/notification-manager.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-judge-llm.md +296 -0
- package/plugins/specweave/commands/specweave-organize-docs.md +2 -2
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +87 -0
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +302 -0
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +72 -18
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +406 -0
- package/plugins/specweave/scripts/session-watchdog.sh +10 -4
- package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
- package/plugins/specweave-docs/commands/build.md +4 -4
- package/plugins/specweave-docs/commands/generate.md +1 -1
- package/plugins/specweave-docs/commands/health.md +1 -1
- package/plugins/specweave-docs/commands/init.md +1 -1
- package/plugins/specweave-docs/commands/organize.md +2 -2
- package/plugins/specweave-docs/commands/validate.md +1 -1
- package/plugins/specweave-docs/commands/view.md +391 -0
- package/plugins/specweave-docs/skills/preview/SKILL.md +56 -17
- package/src/templates/AGENTS.md.template +24 -28
- package/src/templates/CLAUDE.md.template +12 -8
- package/plugins/specweave/commands/specweave-judge.md +0 -276
- package/plugins/specweave-docs/commands/preview.md +0 -274
|
@@ -40,6 +40,16 @@
|
|
|
40
40
|
}
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
"matcher": "Write",
|
|
45
|
+
"matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/spec\\.md",
|
|
46
|
+
"hooks": [
|
|
47
|
+
{
|
|
48
|
+
"type": "command",
|
|
49
|
+
"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\\\"}\")'"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
43
53
|
{
|
|
44
54
|
"matcher": "Write",
|
|
45
55
|
"matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/spec\\.md",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# metadata-json-guard.sh
|
|
4
|
+
#
|
|
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
|
+
#
|
|
8
|
+
# ROOT CAUSE: When Claude creates increments via user prompt (not /specweave: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
|
|
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
|
|
43
|
+
INPUT=$(cat)
|
|
44
|
+
|
|
45
|
+
# Extract tool name
|
|
46
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool_input.tool_name // ""' 2>/dev/null || echo "")
|
|
47
|
+
|
|
48
|
+
# Only validate Write tool calls
|
|
49
|
+
if [ "$TOOL_NAME" != "Write" ]; then
|
|
50
|
+
echo '{"decision": "allow"}'
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Extract file path - handle both formats
|
|
55
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
56
|
+
|
|
57
|
+
# Only validate spec.md files in increments folder
|
|
58
|
+
# Match: 3-4 digits, optional E suffix, kebab-case name, spec.md
|
|
59
|
+
if [[ ! "$FILE_PATH" =~ \.specweave/increments/([0-9]{3,4}E?-[^/]+)/spec\.md$ ]]; then
|
|
60
|
+
echo '{"decision": "allow"}'
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Extract increment folder path
|
|
65
|
+
INCREMENT_DIR=$(dirname "$FILE_PATH")
|
|
66
|
+
INCREMENT_ID="${BASH_REMATCH[1]}"
|
|
67
|
+
|
|
68
|
+
# Check if metadata.json exists in the same increment folder
|
|
69
|
+
METADATA_PATH="${INCREMENT_DIR}/metadata.json"
|
|
70
|
+
|
|
71
|
+
if [ -f "$METADATA_PATH" ]; then
|
|
72
|
+
# metadata.json exists, allow spec.md creation
|
|
73
|
+
echo '{"decision": "allow"}'
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# metadata.json doesn't exist - BLOCK spec.md creation
|
|
78
|
+
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
79
|
+
|
|
80
|
+
cat << BLOCK_EOF
|
|
81
|
+
{
|
|
82
|
+
"decision": "block",
|
|
83
|
+
"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"
|
|
84
|
+
}
|
|
85
|
+
BLOCK_EOF
|
|
86
|
+
|
|
87
|
+
exit 2
|
|
@@ -0,0 +1,302 @@
|
|
|
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
|
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
# - file_path matches: .specweave/increments/*/spec.md
|
|
13
13
|
#
|
|
14
14
|
# Rules:
|
|
15
|
-
# - Each
|
|
15
|
+
# - Each US section MUST have **Project**: <value> on next few lines
|
|
16
16
|
# - For 2-level structures, each US MUST also have **Board**: <value>
|
|
17
17
|
# - Project values MUST match configured projects (not generic keywords)
|
|
18
|
+
# - Supports ALL US formats:
|
|
19
|
+
# - ### US-001: (simple)
|
|
20
|
+
# - #### US-001: (4 hashes)
|
|
21
|
+
# - ### US-FE-001: (with project prefix)
|
|
22
|
+
# - #### US-FE-001: (multi-project)
|
|
23
|
+
# - #### US-BE-001: (backend)
|
|
24
|
+
# - #### US-SHARED-001: (shared)
|
|
18
25
|
# - Fallback allowed for existing specs via SPECWEAVE_LEGACY_SPEC=1
|
|
19
26
|
#
|
|
20
27
|
# Returns exit code 1 (block) if validation fails, 0 (allow) otherwise.
|
|
@@ -23,6 +30,7 @@
|
|
|
23
30
|
# - SPECWEAVE_FORCE_PROJECT=1 - Skip all project validation
|
|
24
31
|
# - SPECWEAVE_LEGACY_SPEC=1 - Allow specs without per-US project (legacy mode)
|
|
25
32
|
#
|
|
33
|
+
# v0.34.0 - Fixed to handle all US ID formats (US-001, US-FE-001, etc.)
|
|
26
34
|
|
|
27
35
|
set -e
|
|
28
36
|
|
|
@@ -75,8 +83,13 @@ log_debug "Validating per-US project fields for: $FILE_PATH"
|
|
|
75
83
|
# Extract file content
|
|
76
84
|
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
77
85
|
|
|
78
|
-
# Count User Stories
|
|
79
|
-
|
|
86
|
+
# Count User Stories - support ALL formats:
|
|
87
|
+
# - ### US-001: (simple, 3 hashes)
|
|
88
|
+
# - #### US-001: (4 hashes)
|
|
89
|
+
# - ### US-FE-001: (with project prefix like FE, BE, SHARED)
|
|
90
|
+
# - #### US-FE-001: (multi-project with 4 hashes)
|
|
91
|
+
# Pattern: 3-4 hashes, space, US-, optional prefix (letters+dash), digits, colon
|
|
92
|
+
US_PATTERN='^#{3,4} US-([A-Z]+-)?[0-9]+:'
|
|
80
93
|
TOTAL_US=$(echo "$CONTENT" | grep -cE "$US_PATTERN" || echo 0)
|
|
81
94
|
|
|
82
95
|
log_debug "Total User Stories found: $TOTAL_US"
|
|
@@ -88,7 +101,7 @@ if [ "$TOTAL_US" -eq 0 ]; then
|
|
|
88
101
|
fi
|
|
89
102
|
|
|
90
103
|
# Extract User Story sections and check for **Project**: field
|
|
91
|
-
# Strategy: For each
|
|
104
|
+
# Strategy: For each US heading, look at next 10 lines for **Project**:
|
|
92
105
|
|
|
93
106
|
MISSING_PROJECT=()
|
|
94
107
|
MISSING_BOARD=()
|
|
@@ -96,26 +109,46 @@ MULTI_PROJECT=() # USs with multiple projects (comma-separated)
|
|
|
96
109
|
MULTI_BOARD=() # USs with multiple boards (comma-separated)
|
|
97
110
|
US_WITH_PROJECT=0
|
|
98
111
|
|
|
99
|
-
# Use
|
|
112
|
+
# Use grep to find all US headings (both ### and ####, with or without prefix)
|
|
100
113
|
while IFS= read -r us_line; do
|
|
101
|
-
# Extract US ID
|
|
102
|
-
US_ID=$(echo "$us_line" | grep -oE 'US-[0-9]+' | head -1)
|
|
114
|
+
# Extract US ID - handles US-001, US-FE-001, US-BE-001, US-SHARED-001, etc.
|
|
115
|
+
US_ID=$(echo "$us_line" | grep -oE 'US-([A-Z]+-)?[0-9]+' | head -1)
|
|
103
116
|
|
|
104
117
|
if [ -z "$US_ID" ]; then
|
|
105
118
|
continue
|
|
106
119
|
fi
|
|
107
120
|
|
|
108
|
-
# Get line number of this US heading
|
|
109
|
-
|
|
121
|
+
# Get line number of this US heading (works with both ### and ####)
|
|
122
|
+
# Escape the US_ID for grep (the dash needs no escape, but be safe)
|
|
123
|
+
LINE_NUM=$(echo "$CONTENT" | grep -nE "^#{3,4} ${US_ID}:" | head -1 | cut -d: -f1)
|
|
110
124
|
|
|
111
125
|
if [ -z "$LINE_NUM" ]; then
|
|
126
|
+
log_debug "Could not find line number for $US_ID"
|
|
112
127
|
continue
|
|
113
128
|
fi
|
|
114
129
|
|
|
115
|
-
# Extract next
|
|
116
|
-
|
|
130
|
+
# Extract lines after heading UNTIL next US heading, separator (---), or max 15 lines
|
|
131
|
+
# This prevents reading **Project** from a DIFFERENT user story
|
|
132
|
+
SECTION=""
|
|
133
|
+
line_count=0
|
|
134
|
+
while IFS= read -r line; do
|
|
135
|
+
# Stop at next US heading (### US- or #### US-)
|
|
136
|
+
if [[ "$line" =~ ^#{3,4}\ US- ]] && [ "$line_count" -gt 0 ]; then
|
|
137
|
+
break
|
|
138
|
+
fi
|
|
139
|
+
# Stop at separator
|
|
140
|
+
if [[ "$line" =~ ^---$ ]] && [ "$line_count" -gt 0 ]; then
|
|
141
|
+
break
|
|
142
|
+
fi
|
|
143
|
+
# Max 15 lines
|
|
144
|
+
if [ "$line_count" -ge 15 ]; then
|
|
145
|
+
break
|
|
146
|
+
fi
|
|
147
|
+
SECTION+="$line"$'\n'
|
|
148
|
+
line_count=$((line_count + 1))
|
|
149
|
+
done < <(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)))
|
|
117
150
|
|
|
118
|
-
# Check for **Project**: field
|
|
151
|
+
# Check for **Project**: field within this US section only
|
|
119
152
|
PROJECT_LINE=$(echo "$SECTION" | grep -E '^\*\*Project\*\*:\s*\S' | head -1)
|
|
120
153
|
|
|
121
154
|
if [ -n "$PROJECT_LINE" ]; then
|
|
@@ -136,7 +169,7 @@ while IFS= read -r us_line; do
|
|
|
136
169
|
log_debug "$US_ID MISSING **Project**: field ✗"
|
|
137
170
|
fi
|
|
138
171
|
|
|
139
|
-
done < <(echo "$CONTENT" | grep -E "
|
|
172
|
+
done < <(echo "$CONTENT" | grep -E "^#{3,4} US-([A-Z]+-)?[0-9]+:")
|
|
140
173
|
|
|
141
174
|
log_debug "User Stories with **Project**: $US_WITH_PROJECT / $TOTAL_US"
|
|
142
175
|
|
|
@@ -167,21 +200,42 @@ log_debug "Structure level: $STRUCTURE_LEVEL"
|
|
|
167
200
|
# For 2-level structures, also check for **Board**: field
|
|
168
201
|
if [ "$STRUCTURE_LEVEL" = "2" ]; then
|
|
169
202
|
while IFS= read -r us_line; do
|
|
170
|
-
|
|
203
|
+
# Extract US ID - handles all formats (US-001, US-FE-001, etc.)
|
|
204
|
+
US_ID=$(echo "$us_line" | grep -oE 'US-([A-Z]+-)?[0-9]+' | head -1)
|
|
171
205
|
|
|
172
206
|
if [ -z "$US_ID" ]; then
|
|
173
207
|
continue
|
|
174
208
|
fi
|
|
175
209
|
|
|
176
|
-
|
|
210
|
+
# Get line number (works with both ### and ####)
|
|
211
|
+
LINE_NUM=$(echo "$CONTENT" | grep -nE "^#{3,4} ${US_ID}:" | head -1 | cut -d: -f1)
|
|
177
212
|
|
|
178
213
|
if [ -z "$LINE_NUM" ]; then
|
|
214
|
+
log_debug "Could not find line number for $US_ID (board check)"
|
|
179
215
|
continue
|
|
180
216
|
fi
|
|
181
217
|
|
|
182
|
-
|
|
218
|
+
# Extract lines after heading UNTIL next US heading, separator, or max 15 lines
|
|
219
|
+
SECTION=""
|
|
220
|
+
line_count=0
|
|
221
|
+
while IFS= read -r line; do
|
|
222
|
+
# Stop at next US heading
|
|
223
|
+
if [[ "$line" =~ ^#{3,4}\ US- ]] && [ "$line_count" -gt 0 ]; then
|
|
224
|
+
break
|
|
225
|
+
fi
|
|
226
|
+
# Stop at separator
|
|
227
|
+
if [[ "$line" =~ ^---$ ]] && [ "$line_count" -gt 0 ]; then
|
|
228
|
+
break
|
|
229
|
+
fi
|
|
230
|
+
# Max 15 lines
|
|
231
|
+
if [ "$line_count" -ge 15 ]; then
|
|
232
|
+
break
|
|
233
|
+
fi
|
|
234
|
+
SECTION+="$line"$'\n'
|
|
235
|
+
line_count=$((line_count + 1))
|
|
236
|
+
done < <(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)))
|
|
183
237
|
|
|
184
|
-
# Check for **Board**: field
|
|
238
|
+
# Check for **Board**: field within this US section only
|
|
185
239
|
BOARD_LINE=$(echo "$SECTION" | grep -E '^\*\*Board\*\*:\s*\S' | head -1)
|
|
186
240
|
|
|
187
241
|
if [ -n "$BOARD_LINE" ]; then
|
|
@@ -200,7 +254,7 @@ if [ "$STRUCTURE_LEVEL" = "2" ]; then
|
|
|
200
254
|
log_debug "$US_ID MISSING **Board**: field ✗"
|
|
201
255
|
fi
|
|
202
256
|
|
|
203
|
-
done < <(echo "$CONTENT" | grep -E "
|
|
257
|
+
done < <(echo "$CONTENT" | grep -E "^#{3,4} US-([A-Z]+-)?[0-9]+:")
|
|
204
258
|
fi
|
|
205
259
|
|
|
206
260
|
# Build error message if validation fails
|