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.
Files changed (106) hide show
  1. package/CLAUDE.md +85 -19
  2. package/dist/src/cli/cleanup-zombies.js +8 -5
  3. package/dist/src/cli/cleanup-zombies.js.map +1 -1
  4. package/dist/src/cli/commands/jobs.js +19 -2
  5. package/dist/src/cli/commands/jobs.js.map +1 -1
  6. package/dist/src/cli/commands/living-docs.js +1 -1
  7. package/dist/src/cli/commands/living-docs.js.map +1 -1
  8. package/dist/src/cli/helpers/init/external-import-grouping.d.ts.map +1 -1
  9. package/dist/src/cli/helpers/init/external-import-grouping.js +11 -7
  10. package/dist/src/cli/helpers/init/external-import-grouping.js.map +1 -1
  11. package/dist/src/cli/workers/clone-worker.js +22 -5
  12. package/dist/src/cli/workers/clone-worker.js.map +1 -1
  13. package/dist/src/config/types.d.ts +203 -1208
  14. package/dist/src/config/types.d.ts.map +1 -1
  15. package/dist/src/core/background/job-dependency.d.ts.map +1 -1
  16. package/dist/src/core/background/job-dependency.js +1 -0
  17. package/dist/src/core/background/job-dependency.js.map +1 -1
  18. package/dist/src/core/background/job-launcher.js +2 -2
  19. package/dist/src/core/background/job-launcher.js.map +1 -1
  20. package/dist/src/core/background/job-manager.d.ts +8 -0
  21. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  22. package/dist/src/core/background/job-manager.js +19 -1
  23. package/dist/src/core/background/job-manager.js.map +1 -1
  24. package/dist/src/core/background/types.d.ts +9 -1
  25. package/dist/src/core/background/types.d.ts.map +1 -1
  26. package/dist/src/core/background/types.js +8 -1
  27. package/dist/src/core/background/types.js.map +1 -1
  28. package/dist/src/importers/external-importer.d.ts +26 -5
  29. package/dist/src/importers/external-importer.d.ts.map +1 -1
  30. package/dist/src/importers/item-converter.d.ts.map +1 -1
  31. package/dist/src/importers/item-converter.js +18 -1
  32. package/dist/src/importers/item-converter.js.map +1 -1
  33. package/dist/src/importers/jira-importer.d.ts +10 -0
  34. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  35. package/dist/src/importers/jira-importer.js +70 -6
  36. package/dist/src/importers/jira-importer.js.map +1 -1
  37. package/dist/src/init/architecture/types.d.ts +33 -140
  38. package/dist/src/init/architecture/types.d.ts.map +1 -1
  39. package/dist/src/init/compliance/types.d.ts +30 -27
  40. package/dist/src/init/compliance/types.d.ts.map +1 -1
  41. package/dist/src/init/repo/types.d.ts +11 -34
  42. package/dist/src/init/repo/types.d.ts.map +1 -1
  43. package/dist/src/init/research/src/config/types.d.ts +15 -82
  44. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  45. package/dist/src/init/research/types.d.ts +38 -93
  46. package/dist/src/init/research/types.d.ts.map +1 -1
  47. package/dist/src/init/team/types.d.ts +4 -42
  48. package/dist/src/init/team/types.d.ts.map +1 -1
  49. package/dist/src/living-docs/smart-doc-organizer.js +1 -1
  50. package/dist/src/living-docs/smart-doc-organizer.js.map +1 -1
  51. package/dist/src/sync/closure-metrics.d.ts +102 -0
  52. package/dist/src/sync/closure-metrics.d.ts.map +1 -0
  53. package/dist/src/sync/closure-metrics.js +267 -0
  54. package/dist/src/sync/closure-metrics.js.map +1 -0
  55. package/dist/src/sync/sync-coordinator.d.ts +29 -0
  56. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  57. package/dist/src/sync/sync-coordinator.js +153 -16
  58. package/dist/src/sync/sync-coordinator.js.map +1 -1
  59. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  60. package/dist/src/utils/docs-preview/config-generator.js +4 -0
  61. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  62. package/dist/src/utils/notification-constants.d.ts +87 -0
  63. package/dist/src/utils/notification-constants.d.ts.map +1 -0
  64. package/dist/src/utils/notification-constants.js +131 -0
  65. package/dist/src/utils/notification-constants.js.map +1 -0
  66. package/dist/src/utils/notification-manager.d.ts +24 -0
  67. package/dist/src/utils/notification-manager.d.ts.map +1 -1
  68. package/dist/src/utils/notification-manager.js +29 -0
  69. package/dist/src/utils/notification-manager.js.map +1 -1
  70. package/dist/src/utils/platform-utils.d.ts +13 -3
  71. package/dist/src/utils/platform-utils.d.ts.map +1 -1
  72. package/dist/src/utils/platform-utils.js +17 -6
  73. package/dist/src/utils/platform-utils.js.map +1 -1
  74. package/package.json +1 -1
  75. package/plugins/specweave/commands/specweave-increment.md +46 -0
  76. package/plugins/specweave/commands/specweave-jobs.md +153 -8
  77. package/plugins/specweave/commands/specweave-judge-llm.md +296 -0
  78. package/plugins/specweave/commands/specweave-organize-docs.md +2 -2
  79. package/plugins/specweave/hooks/hooks.json +10 -0
  80. package/plugins/specweave/hooks/spec-project-validator.sh +24 -2
  81. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  82. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  83. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  84. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +87 -0
  85. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +302 -0
  86. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +72 -18
  87. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +406 -0
  88. package/plugins/specweave/scripts/session-watchdog.sh +288 -134
  89. package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
  90. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
  91. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
  92. package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
  93. package/plugins/specweave-docs/commands/build.md +4 -4
  94. package/plugins/specweave-docs/commands/generate.md +1 -1
  95. package/plugins/specweave-docs/commands/health.md +1 -1
  96. package/plugins/specweave-docs/commands/init.md +1 -1
  97. package/plugins/specweave-docs/commands/organize.md +2 -2
  98. package/plugins/specweave-docs/commands/validate.md +1 -1
  99. package/plugins/specweave-docs/commands/view.md +391 -0
  100. package/plugins/specweave-docs/skills/preview/SKILL.md +56 -17
  101. package/src/templates/AGENTS.md.template +24 -28
  102. package/src/templates/CLAUDE.md.template +12 -8
  103. package/plugins/specweave/commands/specweave-judge.md +0 -276
  104. package/plugins/specweave-docs/commands/preview.md +0 -274
  105. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
  106. 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