reviewflow 3.8.1 → 3.10.0

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 (109) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -0
  3. package/dist/config/projectConfig.d.ts +3 -5
  4. package/dist/config/projectConfig.d.ts.map +1 -1
  5. package/dist/config/projectConfig.js +19 -1
  6. package/dist/config/projectConfig.js.map +1 -1
  7. package/dist/entities/modelRouting/modelRouting.gateway.d.ts +5 -0
  8. package/dist/entities/modelRouting/modelRouting.gateway.d.ts.map +1 -0
  9. package/dist/entities/modelRouting/modelRouting.gateway.js +2 -0
  10. package/dist/entities/modelRouting/modelRouting.gateway.js.map +1 -0
  11. package/dist/entities/modelRouting/modelRouting.schema.d.ts +13 -0
  12. package/dist/entities/modelRouting/modelRouting.schema.d.ts.map +1 -0
  13. package/dist/entities/modelRouting/modelRouting.schema.js +7 -0
  14. package/dist/entities/modelRouting/modelRouting.schema.js.map +1 -0
  15. package/dist/entities/tokenUsage/tokenUsage.gateway.d.ts +6 -0
  16. package/dist/entities/tokenUsage/tokenUsage.gateway.d.ts.map +1 -0
  17. package/dist/entities/tokenUsage/tokenUsage.gateway.js +2 -0
  18. package/dist/entities/tokenUsage/tokenUsage.gateway.js.map +1 -0
  19. package/dist/entities/tokenUsage/tokenUsage.schema.d.ts +30 -0
  20. package/dist/entities/tokenUsage/tokenUsage.schema.d.ts.map +1 -0
  21. package/dist/entities/tokenUsage/tokenUsage.schema.js +19 -0
  22. package/dist/entities/tokenUsage/tokenUsage.schema.js.map +1 -0
  23. package/dist/frameworks/claude/claudeInvoker.d.ts +4 -0
  24. package/dist/frameworks/claude/claudeInvoker.d.ts.map +1 -1
  25. package/dist/frameworks/claude/claudeInvoker.js +86 -27
  26. package/dist/frameworks/claude/claudeInvoker.js.map +1 -1
  27. package/dist/frameworks/claude/streamJsonParser.d.ts +44 -0
  28. package/dist/frameworks/claude/streamJsonParser.d.ts.map +1 -0
  29. package/dist/frameworks/claude/streamJsonParser.js +96 -0
  30. package/dist/frameworks/claude/streamJsonParser.js.map +1 -0
  31. package/dist/frameworks/queue/pQueueAdapter.d.ts +2 -0
  32. package/dist/frameworks/queue/pQueueAdapter.d.ts.map +1 -1
  33. package/dist/frameworks/queue/pQueueAdapter.js.map +1 -1
  34. package/dist/frameworks/settings/runtimeSettings.d.ts +1 -1
  35. package/dist/frameworks/settings/runtimeSettings.d.ts.map +1 -1
  36. package/dist/frameworks/settings/runtimeSettings.js +1 -1
  37. package/dist/frameworks/settings/runtimeSettings.js.map +1 -1
  38. package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.d.ts +6 -0
  39. package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.d.ts.map +1 -0
  40. package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.js +8 -0
  41. package/dist/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.js.map +1 -0
  42. package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.d.ts +7 -0
  43. package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.d.ts.map +1 -0
  44. package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.js +37 -0
  45. package/dist/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.js.map +1 -0
  46. package/dist/tests/factories/routingPolicy.factory.d.ts +5 -0
  47. package/dist/tests/factories/routingPolicy.factory.d.ts.map +1 -0
  48. package/dist/tests/factories/routingPolicy.factory.js +10 -0
  49. package/dist/tests/factories/routingPolicy.factory.js.map +1 -0
  50. package/dist/tests/factories/tokenUsage.factory.d.ts +8 -0
  51. package/dist/tests/factories/tokenUsage.factory.d.ts.map +1 -0
  52. package/dist/tests/factories/tokenUsage.factory.js +28 -0
  53. package/dist/tests/factories/tokenUsage.factory.js.map +1 -0
  54. package/dist/tests/stubs/tokenUsage.stub.d.ts +11 -0
  55. package/dist/tests/stubs/tokenUsage.stub.d.ts.map +1 -0
  56. package/dist/tests/stubs/tokenUsage.stub.js +19 -0
  57. package/dist/tests/stubs/tokenUsage.stub.js.map +1 -0
  58. package/dist/tests/units/config/projectConfig.test.js +47 -0
  59. package/dist/tests/units/config/projectConfig.test.js.map +1 -1
  60. package/dist/tests/units/frameworks/claude/streamJsonParser.test.d.ts +2 -0
  61. package/dist/tests/units/frameworks/claude/streamJsonParser.test.d.ts.map +1 -0
  62. package/dist/tests/units/frameworks/claude/streamJsonParser.test.js +83 -0
  63. package/dist/tests/units/frameworks/claude/streamJsonParser.test.js.map +1 -0
  64. package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.d.ts +2 -0
  65. package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.d.ts.map +1 -0
  66. package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.js +44 -0
  67. package/dist/tests/units/interface-adapters/gateways/projectConfig/routingPolicy.projectConfig.gateway.test.js.map +1 -0
  68. package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.d.ts +2 -0
  69. package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.d.ts.map +1 -0
  70. package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.js +57 -0
  71. package/dist/tests/units/interface-adapters/gateways/tokenUsage/tokenUsage.filesystem.gateway.test.js.map +1 -0
  72. package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.d.ts +2 -0
  73. package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.d.ts.map +1 -0
  74. package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.js +55 -0
  75. package/dist/tests/units/usecases/selectModelForReview/selectModelForReview.usecase.test.js.map +1 -0
  76. package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.d.ts +2 -0
  77. package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.d.ts.map +1 -0
  78. package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.js +64 -0
  79. package/dist/tests/units/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.test.js.map +1 -0
  80. package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.d.ts +2 -0
  81. package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.d.ts.map +1 -0
  82. package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.js +26 -0
  83. package/dist/tests/units/usecases/trackTokenUsage/trackTokenUsage.usecase.test.js.map +1 -0
  84. package/dist/usecases/selectModelForReview/selectModelForReview.usecase.d.ts +15 -0
  85. package/dist/usecases/selectModelForReview/selectModelForReview.usecase.d.ts.map +1 -0
  86. package/dist/usecases/selectModelForReview/selectModelForReview.usecase.js +16 -0
  87. package/dist/usecases/selectModelForReview/selectModelForReview.usecase.js.map +1 -0
  88. package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.d.ts +23 -0
  89. package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.d.ts.map +1 -0
  90. package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.js +38 -0
  91. package/dist/usecases/summarizeTokenUsage/summarizeTokenUsage.usecase.js.map +1 -0
  92. package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.d.ts +8 -0
  93. package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.d.ts.map +1 -0
  94. package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.js +10 -0
  95. package/dist/usecases/trackTokenUsage/trackTokenUsage.usecase.js.map +1 -0
  96. package/package.json +3 -3
  97. package/scripts/hooks/enforce-dependency-rule.sh +61 -0
  98. package/scripts/hooks/enforce-gateway-port-purity.sh +35 -0
  99. package/scripts/hooks/enforce-presenter-class.sh +34 -0
  100. package/scripts/hooks/no-barrel-exports.sh +23 -0
  101. package/scripts/hooks/parse-json.sh +17 -0
  102. package/scripts/hooks/pre-commit-gate.sh +29 -0
  103. package/scripts/hooks/protect-main-branch.sh +28 -0
  104. package/scripts/hooks/protect-main-push.sh +38 -0
  105. package/scripts/hooks/require-spec.sh +58 -0
  106. package/scripts/hooks/session-context.sh +47 -0
  107. package/scripts/hooks/tests/run-tests.sh +189 -0
  108. package/scripts/hooks/tests/test-architecture-hooks.sh +163 -0
  109. package/scripts/hooks/verify-spec-updated.sh +75 -0
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks creation or editing of barrel export files (index.ts).
5
+ # Used as PreToolUse hook on Write|Edit.
6
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ INPUT=$(cat)
10
+ FILE_PATH=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.file_path)
11
+
12
+ is_barrel_export() {
13
+ local filename
14
+ filename=$(basename "$FILE_PATH")
15
+ [[ "$filename" == "index.ts" || "$filename" == "index.js" ]]
16
+ }
17
+
18
+ if is_barrel_export; then
19
+ echo "Barrel exports are forbidden. Import directly from the source file instead of using index.ts." >&2
20
+ exit 2
21
+ fi
22
+
23
+ exit 0
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Extracts a value from JSON on stdin using a dot-separated path.
4
+ # Usage: echo '{"a":{"b":"c"}}' | parse-json.sh a.b
5
+ # Returns empty string if path not found. No external dependencies beyond python3.
6
+
7
+ python3 -c "
8
+ import json, sys
9
+ data = json.load(sys.stdin)
10
+ path = sys.argv[1].split('.')
11
+ for key in path:
12
+ if isinstance(data, dict) and key in data:
13
+ data = data[key]
14
+ else:
15
+ sys.exit(0)
16
+ print(data if data is not None else '')
17
+ " "$1"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks git commit if tests fail.
5
+ # Used as PreToolUse hook on Bash(git commit *).
6
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ INPUT=$(cat)
10
+ COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
11
+
12
+ is_commit_command() {
13
+ echo "$COMMAND" | grep -qE "git commit"
14
+ }
15
+
16
+ if ! is_commit_command; then
17
+ exit 0
18
+ fi
19
+
20
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
21
+
22
+ cd "$PROJECT_DIR"
23
+
24
+ if yarn test:ci 2>&1; then
25
+ exit 0
26
+ else
27
+ echo "Tests failed. Fix them before committing." >&2
28
+ exit 2
29
+ fi
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks git commit on main/master branch.
5
+ # Used as PreToolUse hook on Bash(git commit *).
6
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ INPUT=$(cat)
10
+ COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
11
+
12
+ is_commit_command() {
13
+ echo "$COMMAND" | grep -qE "git commit"
14
+ }
15
+
16
+ if ! is_commit_command; then
17
+ exit 0
18
+ fi
19
+
20
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
21
+ CURRENT_BRANCH=$(cd "$PROJECT_DIR" && git branch --show-current 2>/dev/null || true)
22
+
23
+ if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
24
+ echo "You are on '$CURRENT_BRANCH'. Create a feature branch or use /worktree add <name> first." >&2
25
+ exit 2
26
+ fi
27
+
28
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks git push to main/master branch and force pushes.
5
+ # Used as PreToolUse hook on Bash(git push *).
6
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ INPUT=$(cat)
10
+ COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
11
+
12
+ is_push_command() {
13
+ echo "$COMMAND" | grep -qE "git push"
14
+ }
15
+
16
+ if ! is_push_command; then
17
+ exit 0
18
+ fi
19
+
20
+ is_force_push() {
21
+ echo "$COMMAND" | grep -qE "\-\-force|\-f"
22
+ }
23
+
24
+ if is_force_push; then
25
+ echo "Force push is forbidden. Push normally to your feature branch instead." >&2
26
+ exit 2
27
+ fi
28
+
29
+ pushes_to_protected_branch() {
30
+ echo "$COMMAND" | grep -qE "git push.*(origin\s+(main|master)|origin/main|origin/master)"
31
+ }
32
+
33
+ if pushes_to_protected_branch; then
34
+ echo "Push to main/master is forbidden. Push to your feature branch instead." >&2
35
+ exit 2
36
+ fi
37
+
38
+ exit 0
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks feature-implementer agent if no spec file exists or spec is incomplete.
5
+ # Used as PreToolUse hook on Agent.
6
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ INPUT=$(cat)
10
+ PROMPT=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.prompt)
11
+
12
+ is_feature_agent() {
13
+ echo "$PROMPT" | grep -qi "feature-implementer\|feature-planner"
14
+ }
15
+
16
+ if ! is_feature_agent; then
17
+ exit 0
18
+ fi
19
+
20
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
21
+
22
+ spec_file_referenced() {
23
+ echo "$PROMPT" | grep -oP 'docs/specs/[a-z0-9-]+\.md' | head -1 || true
24
+ }
25
+
26
+ SPEC_REF=$(spec_file_referenced || true)
27
+
28
+ if [[ -z "$SPEC_REF" ]]; then
29
+ echo "No spec file referenced in the prompt. Create a spec first with /product-manager." >&2
30
+ exit 2
31
+ fi
32
+
33
+ if [[ ! -f "$PROJECT_DIR/$SPEC_REF" ]]; then
34
+ echo "Spec file '$SPEC_REF' does not exist. Create it first with /product-manager." >&2
35
+ exit 2
36
+ fi
37
+
38
+ SPEC_CONTENT=$(cat "$PROJECT_DIR/$SPEC_REF")
39
+
40
+ has_rules_section() {
41
+ echo "$SPEC_CONTENT" | grep -qE "^## (Rules|Business Rules|Remaining Scope|Functional Requirements)"
42
+ }
43
+
44
+ has_scenarios_section() {
45
+ echo "$SPEC_CONTENT" | grep -qE "^## (Scenarios|Acceptance Criteria|Gherkin Scenarios)"
46
+ }
47
+
48
+ if ! has_rules_section; then
49
+ echo "Spec '$SPEC_REF' is missing the '## Rules' (or '## Business Rules') section. Fix it before implementing." >&2
50
+ exit 2
51
+ fi
52
+
53
+ if ! has_scenarios_section; then
54
+ echo "Spec '$SPEC_REF' is missing the '## Scenarios' (or '## Acceptance Criteria') section. Fix it before implementing." >&2
55
+ exit 2
56
+ fi
57
+
58
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Injects feature tracker status into session context on startup.
5
+ # Used as SessionStart hook.
6
+ # Outputs JSON with additionalContext field.
7
+
8
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
10
+ TRACKER="$PROJECT_DIR/docs/feature-tracker.md"
11
+
12
+ if [[ ! -f "$TRACKER" ]]; then
13
+ exit 0
14
+ fi
15
+
16
+ FEATURE_COUNT=$(tail -n +3 "$TRACKER" | grep -c '|' || true)
17
+
18
+ if [[ "$FEATURE_COUNT" -eq 0 ]]; then
19
+ exit 0
20
+ fi
21
+
22
+ IMPLEMENTING=$(tail -n +3 "$TRACKER" | grep -c "| implementing |" || true)
23
+ IMPLEMENTED=$(tail -n +3 "$TRACKER" | grep -c "| implemented |" || true)
24
+ DRAFTED=$(tail -n +3 "$TRACKER" | grep -c "| drafted |" || true)
25
+ PLANNED=$(tail -n +3 "$TRACKER" | grep -c "| planned |" || true)
26
+
27
+ CONTEXT="Feature tracker: ${FEATURE_COUNT} features total"
28
+
29
+ if [[ "$IMPLEMENTING" -gt 0 ]]; then
30
+ CONTEXT="$CONTEXT, ${IMPLEMENTING} in progress"
31
+ fi
32
+ if [[ "$PLANNED" -gt 0 ]]; then
33
+ CONTEXT="$CONTEXT, ${PLANNED} planned"
34
+ fi
35
+ if [[ "$DRAFTED" -gt 0 ]]; then
36
+ CONTEXT="$CONTEXT, ${DRAFTED} drafted"
37
+ fi
38
+ if [[ "$IMPLEMENTED" -gt 0 ]]; then
39
+ CONTEXT="$CONTEXT, ${IMPLEMENTED} implemented"
40
+ fi
41
+
42
+ CONTEXT="$CONTEXT. See docs/feature-tracker.md for details."
43
+
44
+ python3 -c "
45
+ import json, sys
46
+ print(json.dumps({'additionalContext': sys.argv[1]}))
47
+ " "$CONTEXT"
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Test runner for Claude Code hook scripts.
5
+ # Run: bash scripts/hooks/tests/run-tests.sh
6
+
7
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ PASSED=0
10
+ FAILED=0
11
+ TOTAL=0
12
+
13
+ green() { printf "\033[32m%s\033[0m\n" "$1"; }
14
+ red() { printf "\033[31m%s\033[0m\n" "$1"; }
15
+ bold() { printf "\033[1m%s\033[0m\n" "$1"; }
16
+
17
+ assert_exit() {
18
+ local test_name="$1"
19
+ local expected_exit="$2"
20
+ local actual_exit="$3"
21
+ TOTAL=$((TOTAL + 1))
22
+
23
+ if [[ "$actual_exit" -eq "$expected_exit" ]]; then
24
+ green " PASS: $test_name (exit $actual_exit)"
25
+ PASSED=$((PASSED + 1))
26
+ else
27
+ red " FAIL: $test_name (expected exit $expected_exit, got $actual_exit)"
28
+ FAILED=$((FAILED + 1))
29
+ fi
30
+ }
31
+
32
+ assert_stderr_contains() {
33
+ local test_name="$1"
34
+ local pattern="$2"
35
+ local stderr_output="$3"
36
+ TOTAL=$((TOTAL + 1))
37
+
38
+ if echo "$stderr_output" | grep -q "$pattern"; then
39
+ green " PASS: $test_name (stderr contains '$pattern')"
40
+ PASSED=$((PASSED + 1))
41
+ else
42
+ red " FAIL: $test_name (stderr missing '$pattern')"
43
+ red " actual stderr: $stderr_output"
44
+ FAILED=$((FAILED + 1))
45
+ fi
46
+ }
47
+
48
+ # ─────────────────────────────────────────────
49
+ bold "=== parse-json.sh ==="
50
+
51
+ OUTPUT=$(echo '{"tool_input":{"command":"git commit -m test"}}' | "$HOOK_DIR/parse-json.sh" tool_input.command)
52
+ TOTAL=$((TOTAL + 1))
53
+ if [[ "$OUTPUT" == "git commit -m test" ]]; then
54
+ green " PASS: extracts nested value"
55
+ PASSED=$((PASSED + 1))
56
+ else
57
+ red " FAIL: extracts nested value (got: $OUTPUT)"
58
+ FAILED=$((FAILED + 1))
59
+ fi
60
+
61
+ OUTPUT=$(echo '{"tool_input":{"command":"test"}}' | "$HOOK_DIR/parse-json.sh" tool_input.missing_key)
62
+ TOTAL=$((TOTAL + 1))
63
+ if [[ -z "$OUTPUT" ]]; then
64
+ green " PASS: returns empty for missing key"
65
+ PASSED=$((PASSED + 1))
66
+ else
67
+ red " FAIL: returns empty for missing key (got: $OUTPUT)"
68
+ FAILED=$((FAILED + 1))
69
+ fi
70
+
71
+ # ─────────────────────────────────────────────
72
+ bold "=== no-barrel-exports.sh ==="
73
+
74
+ echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1 || true
75
+ EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
76
+ assert_exit "blocks index.ts" 2 "$EXIT_CODE"
77
+
78
+ STDERR=$(echo '{"tool_input":{"file_path":"/src/entities/index.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" 2>&1 > /dev/null || true)
79
+ assert_stderr_contains "error message mentions barrel" "Barrel exports" "$STDERR"
80
+
81
+ EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/review.ts"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
82
+ assert_exit "allows normal .ts file" 0 "$EXIT_CODE"
83
+
84
+ EXIT_CODE=$(echo '{"tool_input":{"file_path":"/src/entities/index.js"}}' | "$HOOK_DIR/no-barrel-exports.sh" > /dev/null 2>&1; echo $?) || true
85
+ assert_exit "blocks index.js" 2 "$EXIT_CODE"
86
+
87
+ # ─────────────────────────────────────────────
88
+ bold "=== protect-main-branch.sh ==="
89
+
90
+ # Create temp git repo to test
91
+ TEMP_REPO=$(mktemp -d)
92
+ cd "$TEMP_REPO"
93
+ git init -q
94
+ git checkout -q -b master
95
+
96
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
97
+ assert_exit "blocks commit on master" 2 "$EXIT_CODE"
98
+
99
+ STDERR=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" 2>&1 > /dev/null || true)
100
+ assert_stderr_contains "mentions master" "master" "$STDERR"
101
+
102
+ git checkout -q -b feat/test
103
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git commit -m test"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
104
+ assert_exit "allows commit on feature branch" 0 "$EXIT_CODE"
105
+
106
+ EXIT_CODE=$(echo '{"tool_input":{"command":"ls -la"}}' | CLAUDE_PROJECT_DIR="$TEMP_REPO" "$HOOK_DIR/protect-main-branch.sh" > /dev/null 2>&1; echo $?) || true
107
+ assert_exit "ignores non-commit commands" 0 "$EXIT_CODE"
108
+
109
+ rm -rf "$TEMP_REPO"
110
+
111
+ # ─────────────────────────────────────────────
112
+ bold "=== protect-main-push.sh ==="
113
+
114
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin master"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
115
+ assert_exit "blocks push to master" 2 "$EXIT_CODE"
116
+
117
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin main"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
118
+ assert_exit "blocks push to main" 2 "$EXIT_CODE"
119
+
120
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git push --force origin feat/test"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
121
+ assert_exit "blocks force push" 2 "$EXIT_CODE"
122
+
123
+ EXIT_CODE=$(echo '{"tool_input":{"command":"git push origin feat/test"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
124
+ assert_exit "allows push to feature branch" 0 "$EXIT_CODE"
125
+
126
+ EXIT_CODE=$(echo '{"tool_input":{"command":"ls -la"}}' | "$HOOK_DIR/protect-main-push.sh" > /dev/null 2>&1; echo $?) || true
127
+ assert_exit "ignores non-push commands" 0 "$EXIT_CODE"
128
+
129
+ # ─────────────────────────────────────────────
130
+ bold "=== require-spec.sh ==="
131
+
132
+ PROJECT_DIR="$HOOK_DIR/../.."
133
+
134
+ # Test: no spec reference → block
135
+ EXIT_CODE=$(echo '{"tool_input":{"prompt":"implement the feature-implementer for login"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
136
+ assert_exit "blocks agent without spec reference" 2 "$EXIT_CODE"
137
+
138
+ STDERR=$(echo '{"tool_input":{"prompt":"implement the feature-implementer for login"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" 2>&1 > /dev/null || true)
139
+ assert_stderr_contains "suggests /product-manager" "product-manager" "$STDERR"
140
+
141
+ # Test: non-feature agent → allow
142
+ EXIT_CODE=$(echo '{"tool_input":{"prompt":"review the code in src/entities"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
143
+ assert_exit "allows non-feature agents" 0 "$EXIT_CODE"
144
+
145
+ # Test: valid spec reference with existing file
146
+ EXIT_CODE=$(echo '{"tool_input":{"prompt":"run feature-planner with docs/specs/44-zod-guard-gitlab-webhook.md"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
147
+ assert_exit "allows with valid spec reference" 0 "$EXIT_CODE"
148
+
149
+ # Test: spec file does not exist
150
+ EXIT_CODE=$(echo '{"tool_input":{"prompt":"run feature-implementer with docs/specs/999-nonexistent.md"}}' | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/require-spec.sh" > /dev/null 2>&1; echo $?) || true
151
+ assert_exit "blocks with nonexistent spec" 2 "$EXIT_CODE"
152
+
153
+ # ─────────────────────────────────────────────
154
+ bold "=== session-context.sh ==="
155
+
156
+ EXIT_CODE=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/session-context.sh" > /dev/null 2>&1; echo $?) || true
157
+ assert_exit "runs without error" 0 "$EXIT_CODE"
158
+
159
+ OUTPUT=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$HOOK_DIR/session-context.sh" 2>/dev/null || true)
160
+ TOTAL=$((TOTAL + 1))
161
+ if echo "$OUTPUT" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
162
+ green " PASS: outputs valid JSON"
163
+ PASSED=$((PASSED + 1))
164
+ else
165
+ red " FAIL: outputs valid JSON (got: $OUTPUT)"
166
+ FAILED=$((FAILED + 1))
167
+ fi
168
+
169
+ TOTAL=$((TOTAL + 1))
170
+ if echo "$OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert 'additionalContext' in d" 2>/dev/null; then
171
+ green " PASS: JSON contains additionalContext"
172
+ PASSED=$((PASSED + 1))
173
+ else
174
+ red " FAIL: JSON contains additionalContext (got: $OUTPUT)"
175
+ FAILED=$((FAILED + 1))
176
+ fi
177
+
178
+ # ─────────────────────────────────────────────
179
+ echo ""
180
+ bold "=== RESULTS ==="
181
+ echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED"
182
+
183
+ if [[ "$FAILED" -gt 0 ]]; then
184
+ red "SOME TESTS FAILED"
185
+ exit 1
186
+ else
187
+ green "ALL TESTS PASSED"
188
+ exit 0
189
+ fi
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Tests for the 3 Clean Architecture enforcement hooks.
5
+ # Run: bash scripts/hooks/tests/test-architecture-hooks.sh
6
+
7
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ PASSED=0
9
+ FAILED=0
10
+ TOTAL=0
11
+
12
+ green() { printf "\033[32m%s\033[0m\n" "$1"; }
13
+ red() { printf "\033[31m%s\033[0m\n" "$1"; }
14
+ bold() { printf "\033[1m%s\033[0m\n" "$1"; }
15
+
16
+ assert_exit() {
17
+ local name="$1" expected="$2" actual="$3"
18
+ TOTAL=$((TOTAL + 1))
19
+ if [[ "$actual" -eq "$expected" ]]; then
20
+ green " PASS: $name (exit $actual)"
21
+ PASSED=$((PASSED + 1))
22
+ else
23
+ red " FAIL: $name (expected exit $expected, got $actual)"
24
+ FAILED=$((FAILED + 1))
25
+ fi
26
+ }
27
+
28
+ payload() {
29
+ python3 -c "
30
+ import json, sys
31
+ print(json.dumps({'tool_input': {'file_path': sys.argv[1], 'content': sys.argv[2], 'new_string': ''}}))
32
+ " "$1" "$2"
33
+ }
34
+
35
+ # ─────────────────────────────────────────────────────────────
36
+ bold "=== enforce-dependency-rule.sh ==="
37
+
38
+ # Entity importing interface-adapters → blocked
39
+ EXIT=$(payload "/project/src/entities/foo/foo.gateway.ts" \
40
+ "import type { Bar } from '@/interface-adapters/gateways/bar.js'" \
41
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
42
+ assert_exit "entity cannot import interface-adapters" 2 "$EXIT"
43
+
44
+ # Entity importing usecases → blocked
45
+ EXIT=$(payload "/project/src/entities/foo/foo.ts" \
46
+ "import type { Usecase } from '@/usecases/something.usecase.js'" \
47
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
48
+ assert_exit "entity cannot import usecases" 2 "$EXIT"
49
+
50
+ # Entity importing frameworks → blocked
51
+ EXIT=$(payload "/project/src/entities/foo/foo.ts" \
52
+ "import type { Queue } from '@/frameworks/queue/pQueueAdapter.js'" \
53
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
54
+ assert_exit "entity cannot import frameworks" 2 "$EXIT"
55
+
56
+ # Entity importing within entities → allowed
57
+ EXIT=$(payload "/project/src/entities/foo/foo.ts" \
58
+ "import type { Bar } from '@/entities/bar/bar.js'" \
59
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
60
+ assert_exit "entity can import other entities" 0 "$EXIT"
61
+
62
+ # Usecase importing interface-adapters → blocked
63
+ EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
64
+ "import type { SomeGateway } from '@/interface-adapters/gateways/some.gateway.js'" \
65
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
66
+ assert_exit "usecase cannot import interface-adapters" 2 "$EXIT"
67
+
68
+ # Usecase importing frameworks → blocked
69
+ EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
70
+ "import type { ReviewJob } from '@/frameworks/queue/pQueueAdapter.js'" \
71
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
72
+ assert_exit "usecase cannot import frameworks" 2 "$EXIT"
73
+
74
+ # Usecase importing entities (valid) → allowed
75
+ EXIT=$(payload "/project/src/usecases/doSomething.usecase.ts" \
76
+ "import type { ReviewContextGateway } from '@/entities/reviewContext/reviewContext.gateway.js'" \
77
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
78
+ assert_exit "usecase can import entities" 0 "$EXIT"
79
+
80
+ # File outside src/ → always allowed
81
+ EXIT=$(payload "/project/scripts/build.ts" \
82
+ "import type { Foo } from '@/interface-adapters/gateways/foo.js'" \
83
+ | "$HOOK_DIR/enforce-dependency-rule.sh" > /dev/null 2>&1; echo $?) || true
84
+ assert_exit "non-src file always allowed" 0 "$EXIT"
85
+
86
+ # ─────────────────────────────────────────────────────────────
87
+ bold "=== enforce-gateway-port-purity.sh ==="
88
+
89
+ # Entity gateway with interface → allowed
90
+ EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
91
+ "export interface ReviewContextGateway { create(): void }" \
92
+ | "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
93
+ assert_exit "entity gateway with interface is allowed" 0 "$EXIT"
94
+
95
+ # Entity gateway with abstract class → allowed
96
+ EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
97
+ "export abstract class ReviewContextGateway { abstract create(): void }" \
98
+ | "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
99
+ assert_exit "entity gateway with abstract class is allowed" 0 "$EXIT"
100
+
101
+ # Entity gateway with plain class → blocked
102
+ EXIT=$(payload "/project/src/entities/review/reviewContext.gateway.ts" \
103
+ "export class ReviewContextGateway { create(): void {} }" \
104
+ | "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
105
+ assert_exit "entity gateway with plain class is blocked" 2 "$EXIT"
106
+
107
+ # Implementation in interface-adapters with plain class → allowed (not in entities/)
108
+ EXIT=$(payload "/project/src/interface-adapters/gateways/reviewContext.fileSystem.gateway.ts" \
109
+ "export class ReviewContextFileSystemGateway implements ReviewContextGateway {}" \
110
+ | "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
111
+ assert_exit "gateway impl in interface-adapters with plain class is allowed" 0 "$EXIT"
112
+
113
+ # Non-gateway entity file → not checked
114
+ EXIT=$(payload "/project/src/entities/review/reviewContext.ts" \
115
+ "export class ReviewContext { private constructor() {} }" \
116
+ | "$HOOK_DIR/enforce-gateway-port-purity.sh" > /dev/null 2>&1; echo $?) || true
117
+ assert_exit "non-gateway entity file not checked" 0 "$EXIT"
118
+
119
+ # ─────────────────────────────────────────────────────────────
120
+ bold "=== enforce-presenter-class.sh ==="
121
+
122
+ # Presenter with class ending in Presenter → allowed
123
+ EXIT=$(payload "/project/src/interface-adapters/presenters/jobStatus.presenter.ts" \
124
+ "export class JobStatusPresenter { present(input: unknown) { return {} } }" \
125
+ | "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
126
+ assert_exit "presenter class *Presenter is allowed" 0 "$EXIT"
127
+
128
+ # Presenter with class ending in Calculator → allowed
129
+ EXIT=$(payload "/project/src/interface-adapters/presenters/score.presenter.ts" \
130
+ "export class ScoreCalculator { compute(input: unknown) { return 0 } }" \
131
+ | "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
132
+ assert_exit "presenter class *Calculator is allowed" 0 "$EXIT"
133
+
134
+ # Presenter with function export → blocked
135
+ EXIT=$(payload "/project/src/interface-adapters/presenters/jobStatus.presenter.ts" \
136
+ "export function presentJobStatus(input: unknown) { return {} }" \
137
+ | "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
138
+ assert_exit "presenter function export is blocked" 2 "$EXIT"
139
+
140
+ # Non-presenter .ts file → not checked
141
+ EXIT=$(payload "/project/src/interface-adapters/controllers/webhook.controller.ts" \
142
+ "export function handleWebhook() {}" \
143
+ | "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
144
+ assert_exit "non-presenter file not checked" 0 "$EXIT"
145
+
146
+ # presenter.ts outside interface-adapters/presenters/ → not checked
147
+ EXIT=$(payload "/project/src/entities/foo/foo.presenter.ts" \
148
+ "export function fooPresenter() {}" \
149
+ | "$HOOK_DIR/enforce-presenter-class.sh" > /dev/null 2>&1; echo $?) || true
150
+ assert_exit "presenter outside interface-adapters/presenters/ not checked" 0 "$EXIT"
151
+
152
+ # ─────────────────────────────────────────────────────────────
153
+ echo ""
154
+ bold "=== RESULTS ==="
155
+ echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED"
156
+
157
+ if [[ "$FAILED" -gt 0 ]]; then
158
+ red "SOME TESTS FAILED"
159
+ exit 1
160
+ else
161
+ green "ALL TESTS PASSED"
162
+ exit 0
163
+ fi
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Blocks git commit if staged files include src/ code but the corresponding
5
+ # spec does not have "## Status: implemented" and the feature-tracker is stale.
6
+ # Used as PreToolUse hook on Bash(git commit *).
7
+ # Exit 0 = allow, Exit 2 = block with feedback to Claude.
8
+
9
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ INPUT=$(cat)
11
+ COMMAND=$(echo "$INPUT" | "$HOOK_DIR/parse-json.sh" tool_input.command)
12
+
13
+ is_commit_command() {
14
+ echo "$COMMAND" | grep -qE "git commit"
15
+ }
16
+
17
+ if ! is_commit_command; then
18
+ exit 0
19
+ fi
20
+
21
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOK_DIR/../.." && pwd)}"
22
+ cd "$PROJECT_DIR"
23
+
24
+ STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
25
+
26
+ if [[ -z "$STAGED_FILES" ]]; then
27
+ exit 0
28
+ fi
29
+
30
+ has_source_code() {
31
+ echo "$STAGED_FILES" | grep -q "^src/" 2>/dev/null
32
+ }
33
+
34
+ if ! has_source_code; then
35
+ exit 0
36
+ fi
37
+
38
+ TRACKER="$PROJECT_DIR/docs/feature-tracker.md"
39
+ SPECS_DIR="$PROJECT_DIR/docs/specs"
40
+
41
+ ERRORS=""
42
+
43
+ if [[ -f "$TRACKER" ]]; then
44
+ DRAFTS_IN_TRACKER=$(grep -c "| drafted |" "$TRACKER" || true)
45
+ STAGED_HAS_TRACKER=$(echo "$STAGED_FILES" | grep -c "docs/feature-tracker.md" || true)
46
+
47
+ if [[ "$DRAFTS_IN_TRACKER" -gt 0 && "$STAGED_HAS_TRACKER" -eq 0 ]]; then
48
+ IMPLEMENTING=$(grep -c "| implementing |" "$TRACKER" || true)
49
+ if [[ "$IMPLEMENTING" -gt 0 ]]; then
50
+ ERRORS="${ERRORS}Feature tracker has features in 'implementing' status. Update docs/feature-tracker.md before committing.\n"
51
+ fi
52
+ fi
53
+ fi
54
+
55
+ if [[ -d "$SPECS_DIR" ]]; then
56
+ for SPEC_FILE in "$SPECS_DIR"/*.md; do
57
+ [[ -f "$SPEC_FILE" ]] || continue
58
+ SPEC_NAME=$(basename "$SPEC_FILE")
59
+
60
+ if grep -q "^## Status: implemented" "$SPEC_FILE" 2>/dev/null; then
61
+ continue
62
+ fi
63
+
64
+ if echo "$STAGED_FILES" | grep -q "$SPEC_NAME" 2>/dev/null; then
65
+ continue
66
+ fi
67
+ done
68
+ fi
69
+
70
+ if [[ -n "$ERRORS" ]]; then
71
+ echo -e "$ERRORS" >&2
72
+ exit 2
73
+ fi
74
+
75
+ exit 0