specweave 0.33.3 → 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 +85 -19
- package/dist/src/cli/cleanup-zombies.js +8 -5
- package/dist/src/cli/cleanup-zombies.js.map +1 -1
- 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/config/types.d.ts +203 -1208
- package/dist/src/config/types.d.ts.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 +10 -0
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +70 -6
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +33 -140
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +30 -27
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +11 -34
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +15 -82
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +38 -93
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +4 -42
- package/dist/src/init/team/types.d.ts.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/sync/closure-metrics.d.ts +102 -0
- package/dist/src/sync/closure-metrics.d.ts.map +1 -0
- package/dist/src/sync/closure-metrics.js +267 -0
- package/dist/src/sync/closure-metrics.js.map +1 -0
- package/dist/src/sync/sync-coordinator.d.ts +29 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +153 -16
- package/dist/src/sync/sync-coordinator.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 +87 -0
- package/dist/src/utils/notification-constants.d.ts.map +1 -0
- package/dist/src/utils/notification-constants.js +131 -0
- package/dist/src/utils/notification-constants.js.map +1 -0
- 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/dist/src/utils/platform-utils.d.ts +13 -3
- package/dist/src/utils/platform-utils.d.ts.map +1 -1
- package/dist/src/utils/platform-utils.js +17 -6
- package/dist/src/utils/platform-utils.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-increment.md +46 -0
- package/plugins/specweave/commands/specweave-jobs.md +153 -8
- 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/spec-project-validator.sh +24 -2
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- 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 +288 -134
- package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
- package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
- 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
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1107
|
@@ -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",
|
|
@@ -61,18 +61,40 @@ fi
|
|
|
61
61
|
# Extract YAML frontmatter
|
|
62
62
|
FRONTMATTER=$(echo "$CONTENT" | sed -n '/^---$/,/^---$/p' | tail -n +2 | head -n -1)
|
|
63
63
|
|
|
64
|
-
# Check for unresolved project placeholder
|
|
64
|
+
# Check for unresolved project placeholder (legacy {{PROJECT_ID}})
|
|
65
65
|
if echo "$FRONTMATTER" | grep -q 'project:\s*{{PROJECT_ID}}'; then
|
|
66
66
|
echo '{"decision": "block", "reason": "spec.md has unresolved placeholder {{PROJECT_ID}}. Run '\''specweave context projects'\'' to get available projects, then select one."}'
|
|
67
67
|
exit 0
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
-
# Check for unresolved board placeholder
|
|
70
|
+
# Check for unresolved board placeholder (legacy {{BOARD_ID}})
|
|
71
71
|
if echo "$FRONTMATTER" | grep -q 'board:\s*{{BOARD_ID}}'; then
|
|
72
72
|
echo '{"decision": "block", "reason": "spec.md has unresolved placeholder {{BOARD_ID}}. Run '\''specweave context boards --project=<id>'\'' to get available boards, then select one."}'
|
|
73
73
|
exit 0
|
|
74
74
|
fi
|
|
75
75
|
|
|
76
|
+
# Check for ANY unresolved {{...}} placeholder in frontmatter (v0.34.0+)
|
|
77
|
+
# This catches {{RESOLVED_PROJECT}}, {{RESOLVED_BOARD}}, and any other placeholders
|
|
78
|
+
if echo "$FRONTMATTER" | grep -qE '\{\{[A-Z_]+\}\}'; then
|
|
79
|
+
FOUND_PLACEHOLDERS=$(echo "$FRONTMATTER" | grep -oE '\{\{[A-Z_]+\}\}' | tr '\n' ', ' | sed 's/,$//')
|
|
80
|
+
echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved placeholders: ${FOUND_PLACEHOLDERS}\\n\\nYOU MUST RESOLVE these BEFORE creating spec.md:\\n1. Run: specweave context projects\\n2. Parse the JSON output to get valid project/board IDs\\n3. Replace placeholders with actual values from step 2\\n\\n❌ FORBIDDEN: Using placeholder templates directly\\n✅ REQUIRED: Resolve ALL placeholders to actual values\"}"
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Check for ANY unresolved {{...}} placeholder in full content (catches per-US **Project**: {{...}})
|
|
85
|
+
if echo "$CONTENT" | grep -qE '\*\*Project\*\*:\s*\{\{[A-Z_]+\}\}'; then
|
|
86
|
+
FOUND_PLACEHOLDERS=$(echo "$CONTENT" | grep -oE '\*\*Project\*\*:\s*\{\{[A-Z_]+\}\}' | head -1)
|
|
87
|
+
echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved **Project**: placeholder\\n\\nFound: ${FOUND_PLACEHOLDERS}\\n\\nEach user story MUST have a resolved **Project**: field.\\n\\n1. Run: specweave context projects\\n2. Get valid project IDs from the JSON output\\n3. Replace the placeholder with an actual project ID\"}"
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Check for unresolved **Board**: placeholders in full content (2-level structures)
|
|
92
|
+
if echo "$CONTENT" | grep -qE '\*\*Board\*\*:\s*\{\{[A-Z_]+\}\}'; then
|
|
93
|
+
FOUND_PLACEHOLDERS=$(echo "$CONTENT" | grep -oE '\*\*Board\*\*:\s*\{\{[A-Z_]+\}\}' | head -1)
|
|
94
|
+
echo "{\"decision\": \"block\", \"reason\": \"spec.md has unresolved **Board**: placeholder\\n\\nFound: ${FOUND_PLACEHOLDERS}\\n\\nEach user story MUST have a resolved **Board**: field (for 2-level structures).\\n\\n1. Run: specweave context projects\\n2. Get valid board IDs from boardsByProject in the JSON output\\n3. Replace the placeholder with an actual board ID\"}"
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
76
98
|
# Extract project and board from frontmatter
|
|
77
99
|
PROJECT=$(echo "$FRONTMATTER" | grep -E '^project:\s*' | sed 's/^project:\s*//' | tr -d '"'"'" | tr -d '[:space:]')
|
|
78
100
|
BOARD=$(echo "$FRONTMATTER" | grep -E '^board:\s*' | sed 's/^board:\s*//' | tr -d '"'"'" | tr -d '[:space:]')
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM hook-wrapper.cmd - Windows resilient hook launcher
|
|
3
|
-
REM Prevents crashes when dispatcher.mjs is temporarily unavailable
|
|
4
|
-
|
|
5
|
-
setlocal enabledelayedexpansion
|
|
6
|
-
|
|
7
|
-
set "HOOK_TYPE=%~1"
|
|
8
|
-
if "%HOOK_TYPE%"=="" set "HOOK_TYPE=unknown"
|
|
9
|
-
|
|
10
|
-
set "SCRIPT_DIR=%~dp0"
|
|
11
|
-
set "DISPATCHER=%SCRIPT_DIR%dispatcher.mjs"
|
|
12
|
-
|
|
13
|
-
REM Check if dispatcher exists
|
|
14
|
-
if not exist "%DISPATCHER%" (
|
|
15
|
-
echo {"continue":true,"systemMessage":"Hook skipped: dispatcher.mjs not found"}
|
|
16
|
-
exit /b 0
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
REM Run dispatcher with error suppression
|
|
20
|
-
node "%DISPATCHER%" "%HOOK_TYPE%" 2>nul
|
|
21
|
-
if errorlevel 1 (
|
|
22
|
-
echo {"continue":true,"systemMessage":"Hook error, continuing"}
|
|
23
|
-
exit /b 0
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
exit /b 0
|
|
1
|
+
@echo off
|
|
2
|
+
REM hook-wrapper.cmd - Windows resilient hook launcher
|
|
3
|
+
REM Prevents crashes when dispatcher.mjs is temporarily unavailable
|
|
4
|
+
|
|
5
|
+
setlocal enabledelayedexpansion
|
|
6
|
+
|
|
7
|
+
set "HOOK_TYPE=%~1"
|
|
8
|
+
if "%HOOK_TYPE%"=="" set "HOOK_TYPE=unknown"
|
|
9
|
+
|
|
10
|
+
set "SCRIPT_DIR=%~dp0"
|
|
11
|
+
set "DISPATCHER=%SCRIPT_DIR%dispatcher.mjs"
|
|
12
|
+
|
|
13
|
+
REM Check if dispatcher exists
|
|
14
|
+
if not exist "%DISPATCHER%" (
|
|
15
|
+
echo {"continue":true,"systemMessage":"Hook skipped: dispatcher.mjs not found"}
|
|
16
|
+
exit /b 0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
REM Run dispatcher with error suppression
|
|
20
|
+
node "%DISPATCHER%" "%HOOK_TYPE%" 2>nul
|
|
21
|
+
if errorlevel 1 (
|
|
22
|
+
echo {"continue":true,"systemMessage":"Hook error, continuing"}
|
|
23
|
+
exit /b 0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
exit /b 0
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
:: Universal Session Start Hook for Windows
|
|
3
|
-
:: Calls the Node.js dispatcher
|
|
4
|
-
|
|
5
|
-
:: Find node.exe
|
|
6
|
-
where node >nul 2>&1
|
|
7
|
-
if %ERRORLEVEL% neq 0 (
|
|
8
|
-
echo {"continue": true, "error": "Node.js not found"}
|
|
9
|
-
exit /b 0
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
:: Get the directory of this script
|
|
13
|
-
set "SCRIPT_DIR=%~dp0"
|
|
14
|
-
|
|
15
|
-
:: Run the dispatcher
|
|
16
|
-
node "%SCRIPT_DIR%dispatcher.mjs" session-start
|
|
1
|
+
@echo off
|
|
2
|
+
:: Universal Session Start Hook for Windows
|
|
3
|
+
:: Calls the Node.js dispatcher
|
|
4
|
+
|
|
5
|
+
:: Find node.exe
|
|
6
|
+
where node >nul 2>&1
|
|
7
|
+
if %ERRORLEVEL% neq 0 (
|
|
8
|
+
echo {"continue": true, "error": "Node.js not found"}
|
|
9
|
+
exit /b 0
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
:: Get the directory of this script
|
|
13
|
+
set "SCRIPT_DIR=%~dp0"
|
|
14
|
+
|
|
15
|
+
:: Run the dispatcher
|
|
16
|
+
node "%SCRIPT_DIR%dispatcher.mjs" session-start
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# Universal Session Start Hook for Windows PowerShell
|
|
2
|
-
# Calls the Node.js dispatcher for cross-platform compatibility
|
|
3
|
-
|
|
4
|
-
# Find node.exe
|
|
5
|
-
$nodePath = Get-Command node -ErrorAction SilentlyContinue
|
|
6
|
-
|
|
7
|
-
if (-not $nodePath) {
|
|
8
|
-
Write-Host '{"continue": true, "error": "Node.js not found"}'
|
|
9
|
-
exit 0
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
# Get script directory
|
|
13
|
-
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
-
|
|
15
|
-
# Run the dispatcher
|
|
16
|
-
& node "$scriptDir\dispatcher.mjs" session-start
|
|
1
|
+
# Universal Session Start Hook for Windows PowerShell
|
|
2
|
+
# Calls the Node.js dispatcher for cross-platform compatibility
|
|
3
|
+
|
|
4
|
+
# Find node.exe
|
|
5
|
+
$nodePath = Get-Command node -ErrorAction SilentlyContinue
|
|
6
|
+
|
|
7
|
+
if (-not $nodePath) {
|
|
8
|
+
Write-Host '{"continue": true, "error": "Node.js not found"}'
|
|
9
|
+
exit 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Get script directory
|
|
13
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
+
|
|
15
|
+
# Run the dispatcher
|
|
16
|
+
& node "$scriptDir\dispatcher.mjs" session-start
|
|
@@ -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
|