@bookedsolid/rea 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/SECURITY.md +104 -0
  4. package/THREAT_MODEL.md +245 -0
  5. package/agents/accessibility-engineer.md +101 -0
  6. package/agents/backend-engineer.md +126 -0
  7. package/agents/code-reviewer.md +144 -0
  8. package/agents/codex-adversarial.md +107 -0
  9. package/agents/frontend-specialist.md +84 -0
  10. package/agents/qa-engineer.md +138 -0
  11. package/agents/rea-orchestrator.md +101 -0
  12. package/agents/security-engineer.md +108 -0
  13. package/agents/technical-writer.md +140 -0
  14. package/agents/typescript-specialist.md +111 -0
  15. package/commands/codex-review.md +104 -0
  16. package/commands/freeze.md +81 -0
  17. package/commands/halt-check.md +120 -0
  18. package/commands/rea.md +52 -0
  19. package/commands/review.md +79 -0
  20. package/dist/cli/check.d.ts +1 -0
  21. package/dist/cli/check.js +66 -0
  22. package/dist/cli/doctor.d.ts +1 -0
  23. package/dist/cli/doctor.js +93 -0
  24. package/dist/cli/freeze.d.ts +8 -0
  25. package/dist/cli/freeze.js +61 -0
  26. package/dist/cli/index.d.ts +2 -0
  27. package/dist/cli/index.js +65 -0
  28. package/dist/cli/init.d.ts +6 -0
  29. package/dist/cli/init.js +237 -0
  30. package/dist/cli/serve.d.ts +1 -0
  31. package/dist/cli/serve.js +19 -0
  32. package/dist/cli/utils.d.ts +23 -0
  33. package/dist/cli/utils.js +51 -0
  34. package/dist/config/tier-map.d.ts +11 -0
  35. package/dist/config/tier-map.js +108 -0
  36. package/dist/config/types.d.ts +24 -0
  37. package/dist/config/types.js +1 -0
  38. package/dist/gateway/circuit-breaker.d.ts +43 -0
  39. package/dist/gateway/circuit-breaker.js +86 -0
  40. package/dist/gateway/middleware/audit-types.d.ts +16 -0
  41. package/dist/gateway/middleware/audit-types.js +1 -0
  42. package/dist/gateway/middleware/audit.d.ts +12 -0
  43. package/dist/gateway/middleware/audit.js +98 -0
  44. package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
  45. package/dist/gateway/middleware/blocked-paths.js +117 -0
  46. package/dist/gateway/middleware/chain.d.ts +28 -0
  47. package/dist/gateway/middleware/chain.js +40 -0
  48. package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
  49. package/dist/gateway/middleware/circuit-breaker.js +43 -0
  50. package/dist/gateway/middleware/injection.d.ts +22 -0
  51. package/dist/gateway/middleware/injection.js +128 -0
  52. package/dist/gateway/middleware/kill-switch.d.ts +10 -0
  53. package/dist/gateway/middleware/kill-switch.js +58 -0
  54. package/dist/gateway/middleware/policy.d.ts +12 -0
  55. package/dist/gateway/middleware/policy.js +70 -0
  56. package/dist/gateway/middleware/rate-limit.d.ts +12 -0
  57. package/dist/gateway/middleware/rate-limit.js +31 -0
  58. package/dist/gateway/middleware/redact.d.ts +16 -0
  59. package/dist/gateway/middleware/redact.js +128 -0
  60. package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
  61. package/dist/gateway/middleware/result-size-cap.js +48 -0
  62. package/dist/gateway/middleware/session.d.ts +10 -0
  63. package/dist/gateway/middleware/session.js +18 -0
  64. package/dist/gateway/middleware/tier.d.ts +6 -0
  65. package/dist/gateway/middleware/tier.js +10 -0
  66. package/dist/gateway/rate-limiter.d.ts +36 -0
  67. package/dist/gateway/rate-limiter.js +75 -0
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.js +2 -0
  70. package/dist/policy/loader.d.ts +80 -0
  71. package/dist/policy/loader.js +146 -0
  72. package/dist/policy/types.d.ts +34 -0
  73. package/dist/policy/types.js +19 -0
  74. package/hooks/_lib/common.sh +105 -0
  75. package/hooks/_lib/halt-check.sh +39 -0
  76. package/hooks/_lib/policy-read.sh +79 -0
  77. package/hooks/architecture-review-gate.sh +84 -0
  78. package/hooks/attribution-advisory.sh +126 -0
  79. package/hooks/blocked-paths-enforcer.sh +176 -0
  80. package/hooks/changeset-security-gate.sh +143 -0
  81. package/hooks/commit-review-gate.sh +166 -0
  82. package/hooks/dangerous-bash-interceptor.sh +362 -0
  83. package/hooks/dependency-audit-gate.sh +118 -0
  84. package/hooks/env-file-protection.sh +110 -0
  85. package/hooks/pr-issue-link-gate.sh +65 -0
  86. package/hooks/push-review-gate.sh +120 -0
  87. package/hooks/secret-scanner.sh +229 -0
  88. package/hooks/security-disclosure-gate.sh +146 -0
  89. package/hooks/settings-protection.sh +147 -0
  90. package/package.json +93 -0
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: attribution-advisory.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ #
5
+ # OPT-IN: Only enforces when .rea/policy.yaml contains:
6
+ # block_ai_attribution: true
7
+ #
8
+ # When disabled (default), this hook does nothing.
9
+ # When enabled, BLOCKS (exit 2) gh pr create/edit and git commit commands
10
+ # that contain structural AI attribution markers.
11
+ #
12
+ # Exit codes:
13
+ # 0 = allow (disabled, no attribution found, or not a relevant command)
14
+ # 2 = block (attribution detected, or HALT is active)
15
+
16
+ set -uo pipefail
17
+
18
+ # ── 1. Read ALL stdin immediately before doing anything else ──────────────────
19
+ INPUT=$(cat)
20
+
21
+ # ── 2. Dependency check ───────────────────────────────────────────────────────
22
+ if ! command -v jq >/dev/null 2>&1; then
23
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
24
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
25
+ exit 2
26
+ fi
27
+
28
+ # ── 3. HALT check ─────────────────────────────────────────────────────────────
29
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
30
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
31
+ if [ -f "$HALT_FILE" ]; then
32
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
33
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
34
+ exit 2
35
+ fi
36
+
37
+ # ── 4. Check if attribution blocking is enabled ──────────────────────────────
38
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
39
+ if [ ! -f "$POLICY_FILE" ]; then
40
+ exit 0
41
+ fi
42
+ if ! grep -qE '^block_ai_attribution:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
43
+ exit 0
44
+ fi
45
+
46
+ # ── 5. Parse tool_input.command from the hook payload ─────────────────────────
47
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
48
+
49
+ if [[ -z "$CMD" ]]; then
50
+ exit 0
51
+ fi
52
+
53
+ # ── 6. Check if this is a relevant command ────────────────────────────────────
54
+ IS_RELEVANT=0
55
+
56
+ if printf '%s' "$CMD" | grep -qiE 'gh[[:space:]]+pr[[:space:]]+(create|edit)'; then
57
+ IS_RELEVANT=1
58
+ fi
59
+
60
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
61
+ IS_RELEVANT=1
62
+ fi
63
+
64
+ if [[ $IS_RELEVANT -eq 0 ]]; then
65
+ exit 0
66
+ fi
67
+
68
+ # ── 7. Check for structural AI attribution markers ───────────────────────────
69
+
70
+ FOUND=0
71
+
72
+ # Co-Authored-By with noreply@ email
73
+ if printf '%s' "$CMD" | grep -qiE 'Co-Authored-By:.*noreply@'; then
74
+ FOUND=1
75
+ fi
76
+
77
+ # Co-Authored-By with known AI names
78
+ if printf '%s' "$CMD" | grep -qiE 'Co-Authored-By:.*\b(Claude|Sonnet|Opus|Haiku|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|Amazon Q|CodeWhisperer|Devin|Windsurf|Cline|Aider|Anthropic|OpenAI|GitHub Copilot)\b'; then
79
+ FOUND=1
80
+ fi
81
+
82
+ # "Generated/Built/Powered with/by [AI Tool]" lines
83
+ if printf '%s' "$CMD" | grep -qiE '(Generated|Created|Built|Powered|Authored|Written|Produced)[[:space:]]+(with|by)[[:space:]]+(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|AI|an? AI)\b'; then
84
+ FOUND=1
85
+ fi
86
+
87
+ # Markdown-linked attribution
88
+ if printf '%s' "$CMD" | grep -qiE '\[Claude Code\]|\[GitHub Copilot\]|\[ChatGPT\]|\[Gemini\]|\[Cursor\]'; then
89
+ FOUND=1
90
+ fi
91
+
92
+ # Emoji attribution
93
+ if printf '%s' "$CMD" | grep -qE '🤖.*[Gg]enerated'; then
94
+ FOUND=1
95
+ fi
96
+
97
+ if [[ $FOUND -eq 1 ]]; then
98
+ {
99
+ printf '\n'
100
+ printf '═══════════════════════════════════════════════════════════════════\n'
101
+ printf ' BLOCKED: AI attribution detected in command\n'
102
+ printf '═══════════════════════════════════════════════════════════════════\n'
103
+ printf '\n'
104
+ printf ' Your command contains structural AI attribution markers.\n'
105
+ printf '\n'
106
+ printf ' What gets BLOCKED (structural attribution):\n'
107
+ printf ' - Co-Authored-By with AI names or noreply@ emails\n'
108
+ printf ' - "Generated with/by [AI Tool]" footer lines\n'
109
+ printf ' - Markdown-linked tool names: [Claude Code](...)\n'
110
+ printf ' - Emoji attribution: 🤖 Generated...\n'
111
+ printf '\n'
112
+ printf ' What is ALLOWED (legitimate references):\n'
113
+ printf ' - "Fix Claude API integration"\n'
114
+ printf ' - "Update OpenAI SDK version"\n'
115
+ printf ' - "Add Copilot config"\n'
116
+ printf '\n'
117
+ printf ' Remove the attribution markers and rewrite the command.\n'
118
+ printf ' To disable: set block_ai_attribution: false in .rea/policy.yaml\n'
119
+ printf '═══════════════════════════════════════════════════════════════════\n'
120
+ printf '\n'
121
+ } >&2
122
+ exit 2
123
+ fi
124
+
125
+ # No attribution found — allow
126
+ exit 0
@@ -0,0 +1,176 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: blocked-paths-enforcer.sh
3
+ # Fires BEFORE every Write or Edit tool call.
4
+ # Reads blocked_paths from .rea/policy.yaml and blocks matching writes.
5
+ #
6
+ # This enforces the policy layer at the hook level — even if an agent ignores
7
+ # the CLAUDE.md rules or skips the orchestrator, the hook will catch it.
8
+ #
9
+ # Exit codes:
10
+ # 0 = allow (path not blocked)
11
+ # 2 = block (path matches a blocked_paths entry)
12
+
13
+ set -uo pipefail
14
+
15
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
16
+ INPUT=$(cat)
17
+
18
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
19
+ if ! command -v jq >/dev/null 2>&1; then
20
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
21
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
22
+ exit 2
23
+ fi
24
+
25
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
26
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
30
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
31
+ exit 2
32
+ fi
33
+
34
+ # ── 4. Extract file path from payload ─────────────────────────────────────────
35
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
36
+
37
+ if [[ -z "$FILE_PATH" ]]; then
38
+ exit 0
39
+ fi
40
+
41
+ # ── 5. Load blocked_paths from policy ─────────────────────────────────────────
42
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
43
+
44
+ if [[ ! -f "$POLICY_FILE" ]]; then
45
+ exit 0
46
+ fi
47
+
48
+ # Parse blocked_paths using grep + sed (avoid yaml parser dependency)
49
+ # Handles both inline array [] and block sequence - "..." formats
50
+ BLOCKED_PATHS=()
51
+ IN_BLOCK=0
52
+ while IFS= read -r line; do
53
+ # Check if we're entering blocked_paths section
54
+ if printf '%s' "$line" | grep -qE '^blocked_paths:'; then
55
+ # Check for inline empty array
56
+ if printf '%s' "$line" | grep -qE 'blocked_paths:[[:space:]]*\[\]'; then
57
+ break
58
+ fi
59
+ # Check for inline array with values
60
+ if printf '%s' "$line" | grep -qE 'blocked_paths:[[:space:]]*\['; then
61
+ # Extract inline array items
62
+ items=$(printf '%s' "$line" | sed 's/.*\[//; s/\].*//; s/,/ /g')
63
+ for item in $items; do
64
+ cleaned=$(printf '%s' "$item" | sed "s/^[[:space:]]*[\"']//; s/[\"'][[:space:]]*$//")
65
+ if [[ -n "$cleaned" ]]; then
66
+ BLOCKED_PATHS+=("$cleaned")
67
+ fi
68
+ done
69
+ break
70
+ fi
71
+ IN_BLOCK=1
72
+ continue
73
+ fi
74
+
75
+ if [[ $IN_BLOCK -eq 1 ]]; then
76
+ # Block sequence items start with " - "
77
+ if printf '%s' "$line" | grep -qE '^[[:space:]]+-'; then
78
+ cleaned=$(printf '%s' "$line" | sed 's/^[[:space:]]*-[[:space:]]*//; s/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//')
79
+ if [[ -n "$cleaned" ]]; then
80
+ BLOCKED_PATHS+=("$cleaned")
81
+ fi
82
+ else
83
+ # Non-indented line means we've left the block
84
+ break
85
+ fi
86
+ fi
87
+ done < "$POLICY_FILE"
88
+
89
+ if [[ ${#BLOCKED_PATHS[@]} -eq 0 ]]; then
90
+ exit 0
91
+ fi
92
+
93
+ # ── 6. Agent-writable allowlist ───────────────────────────────────────────────
94
+ # These paths under .rea/ must always be writable by agents regardless of
95
+ # what blocked_paths says. Blocking the whole .rea/ directory in policy
96
+ # is a common default, but tasks.jsonl is the PM data store — agents must
97
+ # write there. Settings-protection.sh guards the sensitive files explicitly.
98
+ AGENT_WRITABLE=(
99
+ '.rea/tasks.jsonl'
100
+ '.rea/audit/'
101
+ )
102
+
103
+ normalize_path() {
104
+ local p="$1"
105
+ local root="$REA_ROOT"
106
+ if [[ "$p" == "$root"/* ]]; then
107
+ p="${p#$root/}"
108
+ fi
109
+ p=$(printf '%s' "$p" | sed 's/%2[Ff]/\//g; s/%2[Ee]/./g; s/%20/ /g')
110
+ p="${p#./}"
111
+ printf '%s' "$p"
112
+ }
113
+
114
+ NORMALIZED=$(normalize_path "$FILE_PATH")
115
+
116
+ for writable in "${AGENT_WRITABLE[@]}"; do
117
+ if [[ "$NORMALIZED" == "$writable" ]] || [[ "$NORMALIZED" == "$writable"* && "$writable" == */ ]]; then
118
+ exit 0
119
+ fi
120
+ done
121
+
122
+ # ── 7. Match against blocked_paths ───────────────────────────────────────────
123
+ LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
124
+
125
+ for blocked in "${BLOCKED_PATHS[@]}"; do
126
+ LOWER_BLOCKED=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
127
+
128
+ # Directory match (blocked path ends with /)
129
+ if [[ "$LOWER_BLOCKED" == */ ]]; then
130
+ if [[ "$LOWER_NORM" == "$LOWER_BLOCKED"* ]] || [[ "$LOWER_NORM" == "${LOWER_BLOCKED%/}" ]]; then
131
+ {
132
+ printf 'BLOCKED PATH: Write denied by policy\n'
133
+ printf '\n'
134
+ printf ' File: %s\n' "$FILE_PATH"
135
+ printf ' Blocked by: %s\n' "$blocked"
136
+ printf ' Source: .rea/policy.yaml → blocked_paths\n'
137
+ printf '\n'
138
+ printf ' This path is protected by policy. To modify it, a human must\n'
139
+ printf ' either update blocked_paths in policy.yaml or edit the file directly.\n'
140
+ } >&2
141
+ exit 2
142
+ fi
143
+ continue
144
+ fi
145
+
146
+ # Glob pattern match (contains *)
147
+ if [[ "$blocked" == *'*'* ]]; then
148
+ # Convert glob to regex: . → \., * → .*
149
+ regex=$(printf '%s' "$LOWER_BLOCKED" | sed 's/\./\\./g; s/\*/.*/g')
150
+ if printf '%s' "$LOWER_NORM" | grep -qE "^${regex}$"; then
151
+ {
152
+ printf 'BLOCKED PATH: Write denied by policy\n'
153
+ printf '\n'
154
+ printf ' File: %s\n' "$FILE_PATH"
155
+ printf ' Blocked by: %s (glob pattern)\n' "$blocked"
156
+ printf ' Source: .rea/policy.yaml → blocked_paths\n'
157
+ } >&2
158
+ exit 2
159
+ fi
160
+ continue
161
+ fi
162
+
163
+ # Exact match
164
+ if [[ "$LOWER_NORM" == "$LOWER_BLOCKED" ]]; then
165
+ {
166
+ printf 'BLOCKED PATH: Write denied by policy\n'
167
+ printf '\n'
168
+ printf ' File: %s\n' "$FILE_PATH"
169
+ printf ' Blocked by: %s\n' "$blocked"
170
+ printf ' Source: .rea/policy.yaml → blocked_paths\n'
171
+ } >&2
172
+ exit 2
173
+ fi
174
+ done
175
+
176
+ exit 0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # changeset-security-gate.sh — PreToolUse: Write|Edit
3
+ #
4
+ # Guards .changeset/*.md files against two failure modes:
5
+ #
6
+ # 1. SECURITY DISCLOSURE LEAK — GHSA IDs or CVE numbers written to a changeset
7
+ # file before the advisory is published. Changeset files are committed to git
8
+ # and appear verbatim in CHANGELOG.md — referencing a GHSA ID pre-publish
9
+ # creates public pre-disclosure in git history.
10
+ #
11
+ # 2. MISSING OR MALFORMED FRONTMATTER — changeset files without proper frontmatter
12
+ # are silently ignored by the changesets tool, wasting the release entry.
13
+ #
14
+ # Triggered by: PreToolUse — Write and Edit tools
15
+
16
+ set -euo pipefail
17
+
18
+ # shellcheck source=_lib/common.sh
19
+ source "$(dirname "$0")/_lib/common.sh"
20
+
21
+ check_halt
22
+
23
+ INPUT="$(cat)"
24
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
25
+
26
+ # Only handle Write and Edit
27
+ if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
28
+ exit 0
29
+ fi
30
+
31
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
32
+
33
+ # Only care about .changeset/*.md files — exclude README.md (changeset tool metadata)
34
+ if ! echo "$FILE_PATH" | grep -qE '\.changeset/[^/]+\.md$' || echo "$FILE_PATH" | grep -qE '\.changeset/README\.md$'; then
35
+ exit 0
36
+ fi
37
+
38
+ require_jq
39
+
40
+ # Extract the content being written
41
+ if [[ "$TOOL_NAME" == "Write" ]]; then
42
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
43
+ else
44
+ # For Edit: check the new_string being inserted
45
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
46
+ fi
47
+
48
+ # ─── 1. SECURITY DISCLOSURE CHECK ───────────────────────────────────────────
49
+ #
50
+ # These patterns in a changeset mean security details are about to be committed
51
+ # to git history BEFORE the advisory is published — creating pre-disclosure.
52
+ # GHSA IDs and CVE numbers must NEVER appear in changeset files.
53
+
54
+ DISCLOSURE_PATTERNS=(
55
+ 'GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}'
56
+ 'CVE-[0-9]{4}-[0-9]+'
57
+ )
58
+
59
+ MATCHED_PATTERN=""
60
+ for PATTERN in "${DISCLOSURE_PATTERNS[@]}"; do
61
+ if echo "$CONTENT" | grep -qE "$PATTERN"; then
62
+ MATCHED_PATTERN="$PATTERN"
63
+ break
64
+ fi
65
+ done
66
+
67
+ if [[ -n "$MATCHED_PATTERN" ]]; then
68
+ json_output "block" \
69
+ "CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${MATCHED_PATTERN}').
70
+
71
+ Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
72
+ Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
73
+
74
+ CORRECT approach for security fix changesets:
75
+ Use vague language only — no identifiers, no vulnerability details.
76
+
77
+ WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
78
+ RIGHT: 'security: extend symlink protection to cover all write-capable tools'
79
+
80
+ WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
81
+ RIGHT: 'security: harden middleware chain against indirect instruction attacks'
82
+
83
+ After the release ships:
84
+ 1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
85
+ 2. The GHSA becomes the detailed public disclosure document
86
+ 3. Optionally update CHANGELOG.md post-publish to add the GHSA reference"
87
+ fi
88
+
89
+ # ─── 2. FRONTMATTER VALIDATION ───────────────────────────────────────────────
90
+ #
91
+ # A changeset without valid frontmatter is silently ignored by the changesets
92
+ # tool — the package bump and CHANGELOG entry never appear in the release.
93
+
94
+ # Must start with ---
95
+ if ! echo "$CONTENT" | head -1 | grep -qE '^---'; then
96
+ json_output "block" \
97
+ "CHANGESET FORMAT GATE: Missing frontmatter block.
98
+
99
+ Every changeset must start with a frontmatter block specifying which package to bump:
100
+
101
+ ---
102
+ '@bookedsolid/rea': patch
103
+ ---
104
+
105
+ Brief description of what changed and why (close #N if applicable).
106
+
107
+ Bump types: patch (bug fix/security), minor (new feature), major (breaking change)"
108
+ fi
109
+
110
+ # Must have at least one package bump entry and a closing ---
111
+ FRONTMATTER=$(echo "$CONTENT" | awk '/^---/{count++; if(count==2){exit} next} count==1{print}')
112
+ if ! echo "$FRONTMATTER" | grep -qE "^'.+': (patch|minor|major)"; then
113
+ json_output "block" \
114
+ "CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
115
+
116
+ The frontmatter must include at least one package/bump pair:
117
+
118
+ ---
119
+ '@bookedsolid/rea': patch
120
+ ---
121
+
122
+ Valid bump types: patch | minor | major"
123
+ fi
124
+
125
+ # Must have a non-empty description after the closing ---
126
+ DESCRIPTION=$(echo "$CONTENT" | awk 'BEGIN{count=0} /^---/{count++; next} count>=2{print}' | grep -v '^[[:space:]]*$' | head -1 || true)
127
+ if [[ -z "$DESCRIPTION" ]]; then
128
+ json_output "block" \
129
+ "CHANGESET FORMAT GATE: Missing description after frontmatter.
130
+
131
+ Add a meaningful description explaining what changed and why:
132
+
133
+ ---
134
+ '@bookedsolid/rea': patch
135
+ ---
136
+
137
+ fix(gateway): policy-loader now uses async I/O with 500ms TTL cache
138
+
139
+ Previously, loadPolicy used fs.readFileSync on every tool invocation, blocking
140
+ the event loop under concurrency. Closes #34."
141
+ fi
142
+
143
+ exit 0
@@ -0,0 +1,166 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: commit-review-gate.sh
3
+ # Fires BEFORE every Bash tool call that matches "git commit".
4
+ # Implements a triage-based review gate:
5
+ # - trivial (<20 changed lines, non-sensitive paths) → pass immediately
6
+ # - standard (20-200 lines) → check review cache, pass if cached
7
+ # - significant (>200 lines or sensitive paths) → block, request agent review
8
+ #
9
+ # Exit codes:
10
+ # 0 = allow (trivial change, or cached review found)
11
+ # 2 = block (needs review — returns additionalContext for agent)
12
+
13
+ set -uo pipefail
14
+
15
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
16
+ INPUT=$(cat)
17
+
18
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
19
+ if ! command -v jq >/dev/null 2>&1; then
20
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
21
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
22
+ exit 2
23
+ fi
24
+
25
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
26
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
30
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
31
+ exit 2
32
+ fi
33
+
34
+ # ── 4. Parse command ──────────────────────────────────────────────────────────
35
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
36
+
37
+ if [[ -z "$CMD" ]]; then
38
+ exit 0
39
+ fi
40
+
41
+ # Only trigger on git commit commands
42
+ if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
43
+ exit 0
44
+ fi
45
+
46
+ # Skip --amend (reviewing amendments is a future feature)
47
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
48
+ exit 0
49
+ fi
50
+
51
+ # ── 5. Check if quality gates are enabled ─────────────────────────────────────
52
+ # Fail-open if policy doesn't exist or doesn't have quality_gates
53
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
54
+ if [[ -f "$POLICY_FILE" ]]; then
55
+ if grep -qE '^quality_gates:' "$POLICY_FILE" 2>/dev/null; then
56
+ if grep -qE 'commit_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
57
+ exit 0
58
+ fi
59
+ fi
60
+ fi
61
+
62
+ # ── 6. Compute diff stats ────────────────────────────────────────────────────
63
+ # Get staged diff (what would be committed)
64
+ DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
65
+ DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
66
+
67
+ if [[ -z "$DIFF_OUTPUT" ]]; then
68
+ # No staged changes — let git commit handle the error
69
+ exit 0
70
+ fi
71
+
72
+ # Count changed lines (additions + deletions)
73
+ LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
74
+
75
+ # Check for sensitive paths
76
+ SENSITIVE=0
77
+ SENSITIVE_FILES=""
78
+ if printf '%s' "$DIFF_FULL" | grep -qE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)'; then
79
+ SENSITIVE=1
80
+ SENSITIVE_FILES=$(printf '%s' "$DIFF_FULL" | grep -oE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)[^ ]*' | sed 's/^\+\+\+ [ab]\// /' | head -5)
81
+ fi
82
+
83
+ # ── 7. Triage scoring ────────────────────────────────────────────────────────
84
+ TRIVIAL_THRESHOLD=20
85
+ SIGNIFICANT_THRESHOLD=200
86
+
87
+ if [[ $SENSITIVE -eq 1 ]] || [[ $LINE_COUNT -gt $SIGNIFICANT_THRESHOLD ]]; then
88
+ SCORE="significant"
89
+ elif [[ $LINE_COUNT -ge $TRIVIAL_THRESHOLD ]]; then
90
+ SCORE="standard"
91
+ else
92
+ SCORE="trivial"
93
+ fi
94
+
95
+ # ── 8. Trivial → pass immediately ─────────────────────────────────────────────
96
+ if [[ "$SCORE" == "trivial" ]]; then
97
+ exit 0
98
+ fi
99
+
100
+ # ── 9. Resolve rea CLI ────────────────────────────────────────────────────
101
+ # Try local installs first, then dist build, then global PATH install.
102
+ REA_CLI_ARGS=()
103
+ if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
104
+ REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
105
+ elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
106
+ REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
107
+ elif command -v rea >/dev/null 2>&1; then
108
+ REA_CLI_ARGS=(rea)
109
+ fi
110
+
111
+ # ── 10. Check review cache for all non-trivial commits ────────────────────────
112
+ # Compute SHA and branch here so both standard and significant tiers share them.
113
+ STAGED_SHA=$(cd "$REA_ROOT" && git diff --cached | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
114
+ BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
115
+ CACHE_FILE="${REA_ROOT}/.rea/review-cache.json"
116
+
117
+ if [[ -n "$STAGED_SHA" ]]; then
118
+ CACHE_HIT=false
119
+
120
+ # Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys
121
+ if [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
122
+ CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" 2>/dev/null || echo '{"hit":false}')
123
+ if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
124
+ CACHE_HIT=true
125
+ fi
126
+ fi
127
+
128
+ # Fallback: read cache JSON directly — works when rea is not on PATH.
129
+ # Checks branch-scoped key ("branch:sha") first, then bare SHA (empty-branch case).
130
+ if [[ "$CACHE_HIT" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
131
+ CACHE_KEY="${BRANCH}:${STAGED_SHA}"
132
+ DIRECT_HIT=$(jq -r --arg k1 "$CACHE_KEY" --arg k2 "$STAGED_SHA" \
133
+ '(.entries[$k1] // .entries[$k2]) | if . == null then "miss" elif .result == "pass" then "hit" else "miss" end' \
134
+ "$CACHE_FILE" 2>/dev/null || echo "miss")
135
+ if [[ "$DIRECT_HIT" == "hit" ]]; then
136
+ CACHE_HIT=true
137
+ fi
138
+ fi
139
+
140
+ if [[ "$CACHE_HIT" == "true" ]]; then
141
+ exit 0
142
+ fi
143
+ fi
144
+
145
+ # ── 11. Block and request review ──────────────────────────────────────────────
146
+ {
147
+ printf 'COMMIT REVIEW GATE: Review required before committing\n'
148
+ printf '\n'
149
+ printf ' Score: %s (%s changed lines)\n' "$SCORE" "$LINE_COUNT"
150
+ if [[ $SENSITIVE -eq 1 ]]; then
151
+ printf ' Sensitive paths detected:\n'
152
+ printf '%s\n' "$SENSITIVE_FILES"
153
+ fi
154
+ printf '\n'
155
+ printf ' YOU (the agent) are the reviewer. Do not ask the user to commit manually.\n'
156
+ printf ' Review the staged diff, make a pass/fail decision, then proceed:\n'
157
+ printf '\n'
158
+ printf ' 1. Inspect: git diff --cached\n'
159
+ printf ' 2. Decide: Is this safe to commit? (initial commits, refactors, and\n'
160
+ printf ' feature work are normal — use judgement, not ceremony)\n'
161
+ printf ' 3. Approve: rea cache set %s pass\n' "$STAGED_SHA"
162
+ printf ' 4. Retry the git commit command\n'
163
+ printf '\n'
164
+ printf ' Only escalate to the user if you find a genuine problem in the diff.\n'
165
+ } >&2
166
+ exit 2