@bookedsolid/reagent 0.4.0 → 0.5.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 (92) hide show
  1. package/README.md +407 -54
  2. package/agents/product-owner.md +44 -0
  3. package/commands/plan-work.md +19 -0
  4. package/commands/tasks.md +26 -0
  5. package/dist/cli/commands/cache.d.ts +2 -0
  6. package/dist/cli/commands/cache.d.ts.map +1 -0
  7. package/dist/cli/commands/cache.js +114 -0
  8. package/dist/cli/commands/cache.js.map +1 -0
  9. package/dist/cli/commands/init/agents.d.ts +3 -0
  10. package/dist/cli/commands/init/agents.d.ts.map +1 -0
  11. package/dist/cli/commands/init/agents.js +47 -0
  12. package/dist/cli/commands/init/agents.js.map +1 -0
  13. package/dist/cli/commands/init/claude-hooks.d.ts +3 -0
  14. package/dist/cli/commands/init/claude-hooks.d.ts.map +1 -0
  15. package/dist/cli/commands/init/claude-hooks.js +134 -0
  16. package/dist/cli/commands/init/claude-hooks.js.map +1 -0
  17. package/dist/cli/commands/init/claude-md.d.ts +3 -0
  18. package/dist/cli/commands/init/claude-md.d.ts.map +1 -0
  19. package/dist/cli/commands/init/claude-md.js +52 -0
  20. package/dist/cli/commands/init/claude-md.js.map +1 -0
  21. package/dist/cli/commands/init/commands.d.ts +3 -0
  22. package/dist/cli/commands/init/commands.d.ts.map +1 -0
  23. package/dist/cli/commands/init/commands.js +31 -0
  24. package/dist/cli/commands/init/commands.js.map +1 -0
  25. package/dist/cli/commands/init/cursor-rules.d.ts +3 -0
  26. package/dist/cli/commands/init/cursor-rules.d.ts.map +1 -0
  27. package/dist/cli/commands/init/cursor-rules.js +30 -0
  28. package/dist/cli/commands/init/cursor-rules.js.map +1 -0
  29. package/dist/cli/commands/init/gateway-config.d.ts +3 -0
  30. package/dist/cli/commands/init/gateway-config.d.ts.map +1 -0
  31. package/dist/cli/commands/init/gateway-config.js +51 -0
  32. package/dist/cli/commands/init/gateway-config.js.map +1 -0
  33. package/dist/cli/commands/init/gitignore.d.ts +3 -0
  34. package/dist/cli/commands/init/gitignore.d.ts.map +1 -0
  35. package/dist/cli/commands/init/gitignore.js +20 -0
  36. package/dist/cli/commands/init/gitignore.js.map +1 -0
  37. package/dist/cli/commands/init/husky-hooks.d.ts +3 -0
  38. package/dist/cli/commands/init/husky-hooks.d.ts.map +1 -0
  39. package/dist/cli/commands/init/husky-hooks.js +73 -0
  40. package/dist/cli/commands/init/husky-hooks.js.map +1 -0
  41. package/dist/cli/commands/{init.d.ts → init/index.d.ts} +1 -1
  42. package/dist/cli/commands/init/index.d.ts.map +1 -0
  43. package/dist/cli/commands/init/index.js +121 -0
  44. package/dist/cli/commands/init/index.js.map +1 -0
  45. package/dist/cli/commands/init/pm.d.ts +9 -0
  46. package/dist/cli/commands/init/pm.d.ts.map +1 -0
  47. package/dist/cli/commands/init/pm.js +40 -0
  48. package/dist/cli/commands/init/pm.js.map +1 -0
  49. package/dist/cli/commands/init/policy.d.ts +3 -0
  50. package/dist/cli/commands/init/policy.d.ts.map +1 -0
  51. package/dist/cli/commands/init/policy.js +61 -0
  52. package/dist/cli/commands/init/policy.js.map +1 -0
  53. package/dist/cli/commands/init/types.d.ts +29 -0
  54. package/dist/cli/commands/init/types.d.ts.map +1 -0
  55. package/dist/cli/commands/init/types.js +2 -0
  56. package/dist/cli/commands/init/types.js.map +1 -0
  57. package/dist/cli/index.js +6 -1
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/gateway/native-tools.d.ts +8 -0
  60. package/dist/gateway/native-tools.d.ts.map +1 -0
  61. package/dist/gateway/native-tools.js +190 -0
  62. package/dist/gateway/native-tools.js.map +1 -0
  63. package/dist/gateway/server.d.ts.map +1 -1
  64. package/dist/gateway/server.js +7 -3
  65. package/dist/gateway/server.js.map +1 -1
  66. package/dist/pm/github-bridge.d.ts +36 -0
  67. package/dist/pm/github-bridge.d.ts.map +1 -0
  68. package/dist/pm/github-bridge.js +138 -0
  69. package/dist/pm/github-bridge.js.map +1 -0
  70. package/dist/pm/task-store.d.ts +39 -0
  71. package/dist/pm/task-store.d.ts.map +1 -0
  72. package/dist/pm/task-store.js +189 -0
  73. package/dist/pm/task-store.js.map +1 -0
  74. package/dist/pm/types.d.ts +70 -0
  75. package/dist/pm/types.d.ts.map +1 -0
  76. package/dist/pm/types.js +22 -0
  77. package/dist/pm/types.js.map +1 -0
  78. package/hooks/_lib/common.sh +87 -0
  79. package/hooks/architecture-review-gate.sh +84 -0
  80. package/hooks/blocked-paths-enforcer.sh +169 -0
  81. package/hooks/commit-review-gate.sh +131 -0
  82. package/hooks/dangerous-bash-interceptor.sh +32 -0
  83. package/hooks/dependency-audit-gate.sh +118 -0
  84. package/hooks/push-review-gate.sh +105 -0
  85. package/hooks/settings-protection.sh +145 -0
  86. package/hooks/task-link-gate.sh +70 -0
  87. package/package.json +1 -1
  88. package/profiles/bst-internal.json +20 -3
  89. package/profiles/client-engagement.json +20 -3
  90. package/dist/cli/commands/init.d.ts.map +0 -1
  91. package/dist/cli/commands/init.js +0 -560
  92. package/dist/cli/commands/init.js.map +0 -1
@@ -0,0 +1,131 @@
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 'REAGENT 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
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent 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="${REAGENT_ROOT}/.reagent/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 "$REAGENT_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
65
+ DIFF_FULL=$(cd "$REAGENT_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 '^\+\+\+ .*(\.reagent/|\.claude/|\.env|auth|security|\.github/workflows)'; then
79
+ SENSITIVE=1
80
+ SENSITIVE_FILES=$(printf '%s' "$DIFF_FULL" | grep -oE '^\+\+\+ .*(\.reagent/|\.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. Standard → check review cache ─────────────────────────────────────────
101
+ if [[ "$SCORE" == "standard" ]]; then
102
+ # Compute SHA of staged content for cache lookup
103
+ STAGED_SHA=$(cd "$REAGENT_ROOT" && git diff --cached | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
104
+ BRANCH=$(cd "$REAGENT_ROOT" && git branch --show-current 2>/dev/null || echo "")
105
+
106
+ if [[ -n "$STAGED_SHA" ]]; then
107
+ # Check review cache via reagent CLI
108
+ CACHE_RESULT=$(node "${REAGENT_ROOT}/node_modules/.bin/reagent" cache check "$STAGED_SHA" --branch "$BRANCH" 2>/dev/null || echo '{"hit":false}')
109
+ if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
110
+ exit 0
111
+ fi
112
+ fi
113
+ fi
114
+
115
+ # ── 10. Block and request review ──────────────────────────────────────────────
116
+ {
117
+ printf 'COMMIT REVIEW GATE: Review required before committing\n'
118
+ printf '\n'
119
+ printf ' Score: %s (%s changed lines)\n' "$SCORE" "$LINE_COUNT"
120
+ if [[ $SENSITIVE -eq 1 ]]; then
121
+ printf ' Sensitive paths detected:\n'
122
+ printf '%s\n' "$SENSITIVE_FILES"
123
+ fi
124
+ printf '\n'
125
+ printf ' Action required: Spawn a code-reviewer agent to review the staged changes.\n'
126
+ printf ' The reviewer should produce structured JSON output with findings.\n'
127
+ printf ' After review, cache the result with: reagent cache set <sha> pass\n'
128
+ printf '\n'
129
+ printf ' To review staged changes: git diff --cached\n'
130
+ } >&2
131
+ exit 2
@@ -229,6 +229,38 @@ if printf '%s' "$CMD" | grep -qiE '(curl|wget)[^|]*\|[[:space:]]*(bash|sh|zsh|fi
229
229
  "Alt: Download first, inspect the script, then execute: curl -o script.sh URL && cat script.sh && bash script.sh"
230
230
  fi
231
231
 
232
+ # H13: git push --no-verify — bypasses pre-push hooks
233
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push.*--no-verify'; then
234
+ add_high \
235
+ "git push --no-verify — skipping pre-push hooks" \
236
+ "Bypasses all pre-push safety gates including CI checks." \
237
+ "Alt: Fix the underlying hook failure rather than bypassing it."
238
+ fi
239
+
240
+ # H14: git -c core.hooksPath= — redirects or disables hook execution
241
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+-c[[:space:]]+core\.hookspath'; then
242
+ add_high \
243
+ "git -c core.hooksPath — overriding hooks directory" \
244
+ "Redirecting the hooks path can disable all safety hooks." \
245
+ "Alt: Fix the underlying hook issue. Do not bypass the hooks directory."
246
+ fi
247
+
248
+ # H15: REAGENT_BYPASS env var — attempted escape hatch
249
+ if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)REAGENT_BYPASS[[:space:]]*='; then
250
+ add_high \
251
+ "REAGENT_BYPASS env var — unauthorized bypass attempt" \
252
+ "Setting REAGENT_BYPASS is not a supported escape mechanism and indicates a bypass attempt." \
253
+ "Alt: If you need to override a gate, request human escalation."
254
+ fi
255
+
256
+ # H16: alias/function definitions containing bypass strings
257
+ if printf '%s' "$CMD" | grep -qiE '(alias|function)[[:space:]]+[a-zA-Z_]+.*(--(no-verify|force)|HUSKY=0|core\.hookspath)'; then
258
+ add_high \
259
+ "Alias/function definition with bypass — circumventing safety gates" \
260
+ "Defining aliases or functions that embed bypass flags defeats safety hooks." \
261
+ "Alt: Do not wrap bypass patterns in aliases or functions."
262
+ fi
263
+
232
264
  # ── 10. MEDIUM severity checks ────────────────────────────────────────────────
233
265
 
234
266
  # M1: npm install --force
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: dependency-audit-gate.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Detects package install commands (npm install, pnpm add, yarn add) and
5
+ # verifies the package exists on the registry before allowing the install.
6
+ #
7
+ # Exit codes:
8
+ # 0 = allow (not an install command, or package verified)
9
+ # 2 = block (package not found on registry)
10
+
11
+ set -uo pipefail
12
+
13
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
14
+ INPUT=$(cat)
15
+
16
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
17
+ if ! command -v jq >/dev/null 2>&1; then
18
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
19
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
20
+ exit 2
21
+ fi
22
+
23
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
24
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
25
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
26
+ if [ -f "$HALT_FILE" ]; then
27
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
28
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
29
+ exit 2
30
+ fi
31
+
32
+ # ── 4. Parse command ──────────────────────────────────────────────────────────
33
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
34
+
35
+ if [[ -z "$CMD" ]]; then
36
+ exit 0
37
+ fi
38
+
39
+ # ── 5. Detect package install commands ────────────────────────────────────────
40
+ # Match: npm install <pkg>, npm i <pkg>, pnpm add <pkg>, yarn add <pkg>
41
+ # Skip: npm install (no args), npm ci, npm install --save-dev (without new pkg)
42
+
43
+ extract_packages() {
44
+ local cmd="$1"
45
+
46
+ # npm install/add with packages (skip flags and local paths)
47
+ if printf '%s' "$cmd" | grep -qiE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]'; then
48
+ # Extract the part after the install command
49
+ local after_cmd
50
+ after_cmd=$(printf '%s' "$cmd" | sed -E 's/.*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]+//')
51
+
52
+ # Split on spaces and filter
53
+ for token in $after_cmd; do
54
+ # Skip flags
55
+ if [[ "$token" == -* ]]; then continue; fi
56
+ # Skip local paths
57
+ if [[ "$token" == ./* || "$token" == /* || "$token" == ../* ]]; then continue; fi
58
+ # Skip empty
59
+ if [[ -z "$token" ]]; then continue; fi
60
+ # Strip version specifier for lookup
61
+ local pkg_name
62
+ pkg_name=$(printf '%s' "$token" | sed -E 's/@[^@/]+$//')
63
+ # Handle scoped packages (@scope/name)
64
+ if [[ -z "$pkg_name" ]]; then
65
+ pkg_name="$token"
66
+ fi
67
+ printf '%s\n' "$pkg_name"
68
+ done
69
+ fi
70
+ }
71
+
72
+ PACKAGES=$(extract_packages "$CMD")
73
+
74
+ if [[ -z "$PACKAGES" ]]; then
75
+ exit 0
76
+ fi
77
+
78
+ # ── 6. Verify packages exist on registry ──────────────────────────────────────
79
+ FAILED=""
80
+ CHECKED=0
81
+
82
+ while IFS= read -r pkg; do
83
+ [[ -z "$pkg" ]] && continue
84
+ CHECKED=$((CHECKED + 1))
85
+
86
+ # Cap at 5 packages per command to avoid slow hook
87
+ if [[ $CHECKED -gt 5 ]]; then
88
+ break
89
+ fi
90
+
91
+ # Use npm view to check if package exists
92
+ # macOS doesn't have `timeout` by default, use a background process with kill
93
+ if command -v timeout >/dev/null 2>&1; then
94
+ if ! timeout 5 npm view "$pkg" name >/dev/null 2>&1; then
95
+ FAILED="${FAILED} - ${pkg}\n"
96
+ fi
97
+ else
98
+ # Fallback: run npm view without timeout (still fast for simple checks)
99
+ if ! npm view "$pkg" name >/dev/null 2>&1; then
100
+ FAILED="${FAILED} - ${pkg}\n"
101
+ fi
102
+ fi
103
+ done <<< "$PACKAGES"
104
+
105
+ if [[ -n "$FAILED" ]]; then
106
+ {
107
+ printf 'DEPENDENCY AUDIT: Package not found on npm registry\n'
108
+ printf '\n'
109
+ printf ' The following packages could not be verified:\n'
110
+ printf '%b' "$FAILED"
111
+ printf '\n'
112
+ printf ' Rule: All packages must exist on the npm registry before installation.\n'
113
+ printf ' Check: Is the package name spelled correctly? Does it exist on npmjs.com?\n'
114
+ } >&2
115
+ exit 2
116
+ fi
117
+
118
+ exit 0
@@ -0,0 +1,105 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: push-review-gate.sh
3
+ # Fires BEFORE every Bash tool call that matches "git push".
4
+ # Runs a full diff analysis against the target branch and requests
5
+ # security + code review before allowing the push.
6
+ #
7
+ # Exit codes:
8
+ # 0 = allow (no meaningful diff, or review cached)
9
+ # 2 = block (needs review)
10
+
11
+ set -uo pipefail
12
+
13
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
14
+ INPUT=$(cat)
15
+
16
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
17
+ if ! command -v jq >/dev/null 2>&1; then
18
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
19
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
20
+ exit 2
21
+ fi
22
+
23
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
24
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
25
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
26
+ if [ -f "$HALT_FILE" ]; then
27
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
28
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
29
+ exit 2
30
+ fi
31
+
32
+ # ── 4. Parse command ──────────────────────────────────────────────────────────
33
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
34
+
35
+ if [[ -z "$CMD" ]]; then
36
+ exit 0
37
+ fi
38
+
39
+ # Only trigger on git push commands
40
+ if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push'; then
41
+ exit 0
42
+ fi
43
+
44
+ # ── 5. Check if quality gates are enabled ─────────────────────────────────────
45
+ POLICY_FILE="${REAGENT_ROOT}/.reagent/policy.yaml"
46
+ if [[ -f "$POLICY_FILE" ]]; then
47
+ if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
48
+ exit 0
49
+ fi
50
+ fi
51
+
52
+ # ── 6. Determine target branch ───────────────────────────────────────────────
53
+ CURRENT_BRANCH=$(cd "$REAGENT_ROOT" && git branch --show-current 2>/dev/null || echo "")
54
+ TARGET_BRANCH="main"
55
+
56
+ # Try to extract target from push command (git push origin <branch>)
57
+ PUSH_TARGET=$(printf '%s' "$CMD" | grep -oE 'git[[:space:]]+push[[:space:]]+[a-zA-Z_-]+[[:space:]]+([a-zA-Z0-9/_-]+)' | awk '{print $NF}' 2>/dev/null || echo "")
58
+ if [[ -n "$PUSH_TARGET" ]]; then
59
+ TARGET_BRANCH="$PUSH_TARGET"
60
+ fi
61
+
62
+ # ── 7. Get diff against target ───────────────────────────────────────────────
63
+ MERGE_BASE=$(cd "$REAGENT_ROOT" && git merge-base "$TARGET_BRANCH" HEAD 2>/dev/null || echo "")
64
+
65
+ if [[ -z "$MERGE_BASE" ]]; then
66
+ # Can't determine merge base — fail-open
67
+ exit 0
68
+ fi
69
+
70
+ DIFF_FULL=$(cd "$REAGENT_ROOT" && git diff "$MERGE_BASE"...HEAD 2>/dev/null || echo "")
71
+
72
+ if [[ -z "$DIFF_FULL" ]]; then
73
+ # No diff — nothing to review
74
+ exit 0
75
+ fi
76
+
77
+ LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
78
+
79
+ # ── 8. Check review cache ────────────────────────────────────────────────────
80
+ PUSH_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
81
+
82
+ if [[ -n "$PUSH_SHA" ]]; then
83
+ CACHE_RESULT=$(node "${REAGENT_ROOT}/node_modules/.bin/reagent" cache check "$PUSH_SHA" --branch "$CURRENT_BRANCH" --base "$TARGET_BRANCH" 2>/dev/null || echo '{"hit":false}')
84
+ if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
85
+ exit 0
86
+ fi
87
+ fi
88
+
89
+ # ── 9. Block and request review ──────────────────────────────────────────────
90
+ FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null || echo "0")
91
+
92
+ {
93
+ printf 'PUSH REVIEW GATE: Review required before pushing\n'
94
+ printf '\n'
95
+ printf ' Branch: %s → %s\n' "$CURRENT_BRANCH" "$TARGET_BRANCH"
96
+ printf ' Scope: %s files changed, %s lines\n' "$FILE_COUNT" "$LINE_COUNT"
97
+ printf '\n'
98
+ printf ' Action required:\n'
99
+ printf ' 1. Spawn a code-reviewer agent to review: git diff %s...HEAD\n' "$MERGE_BASE"
100
+ printf ' 2. Spawn a security-engineer agent for security review\n'
101
+ printf ' 3. After both pass, cache the result:\n'
102
+ printf ' reagent cache set %s pass --branch %s --base %s\n' "$PUSH_SHA" "$CURRENT_BRANCH" "$TARGET_BRANCH"
103
+ printf '\n'
104
+ } >&2
105
+ exit 2
@@ -0,0 +1,145 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: settings-protection.sh
3
+ # Fires BEFORE every Write or Edit tool call.
4
+ # Blocks modifications to critical configuration files that, if tampered with,
5
+ # would disable the entire hook safety layer.
6
+ #
7
+ # Protected paths:
8
+ # .claude/settings.json — hook configuration
9
+ # .claude/settings.local.json — local hook overrides
10
+ # .claude/hooks/* — hook scripts themselves
11
+ # .husky/* — git hook scripts
12
+ # .reagent/policy.yaml — autonomy/blocking policy
13
+ # .reagent/HALT — kill switch file
14
+ # .reagent/review-cache.json — review cache (integrity-sensitive)
15
+ #
16
+ # Exit codes:
17
+ # 0 = allow (path not protected)
18
+ # 2 = block (protected path modification attempt)
19
+
20
+ set -uo pipefail
21
+
22
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
23
+ INPUT=$(cat)
24
+
25
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
26
+ if ! command -v jq >/dev/null 2>&1; then
27
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
28
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
29
+ exit 2
30
+ fi
31
+
32
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
33
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
34
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
35
+ if [ -f "$HALT_FILE" ]; then
36
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
37
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
38
+ exit 2
39
+ fi
40
+
41
+ # ── 4. Extract file path from payload ─────────────────────────────────────────
42
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
43
+
44
+ if [[ -z "$FILE_PATH" ]]; then
45
+ exit 0
46
+ fi
47
+
48
+ # ── 5. Normalize path for comparison ──────────────────────────────────────────
49
+ # Convert to relative path from project root for consistent matching
50
+ normalize_path() {
51
+ local p="$1"
52
+ local root="$REAGENT_ROOT"
53
+
54
+ # Strip project root prefix if present
55
+ if [[ "$p" == "$root"/* ]]; then
56
+ p="${p#$root/}"
57
+ fi
58
+
59
+ # URL decode common sequences
60
+ p=$(printf '%s' "$p" | sed 's/%2[Ff]/\//g; s/%2[Ee]/./g; s/%20/ /g')
61
+
62
+ # Collapse path traversals
63
+ # Remove ./ components
64
+ p=$(printf '%s' "$p" | sed 's|\./||g')
65
+
66
+ # Remove leading ./
67
+ p="${p#./}"
68
+
69
+ printf '%s' "$p"
70
+ }
71
+
72
+ NORMALIZED=$(normalize_path "$FILE_PATH")
73
+
74
+ # ── 6. Protected path patterns ────────────────────────────────────────────────
75
+ PROTECTED_PATTERNS=(
76
+ '.claude/settings.json'
77
+ '.claude/settings.local.json'
78
+ '.claude/hooks/'
79
+ '.husky/'
80
+ '.reagent/policy.yaml'
81
+ '.reagent/HALT'
82
+ '.reagent/review-cache.json'
83
+ )
84
+
85
+ for pattern in "${PROTECTED_PATTERNS[@]}"; do
86
+ # Exact match
87
+ if [[ "$NORMALIZED" == "$pattern" ]]; then
88
+ {
89
+ printf 'SETTINGS PROTECTION: Modification blocked\n'
90
+ printf '\n'
91
+ printf ' File: %s\n' "$FILE_PATH"
92
+ printf ' Rule: This file is protected from agent modification.\n'
93
+ printf '\n'
94
+ printf ' Protected files include hook scripts, settings, policy,\n'
95
+ printf ' and kill switch files. These must be modified by humans\n'
96
+ printf ' via reagent CLI or direct editing.\n'
97
+ printf '\n'
98
+ printf ' Use: reagent init (to update hooks/settings)\n'
99
+ printf ' reagent freeze/unfreeze (for HALT file)\n'
100
+ printf ' Edit .reagent/policy.yaml manually\n'
101
+ } >&2
102
+ exit 2
103
+ fi
104
+
105
+ # Directory prefix match (patterns ending in /)
106
+ if [[ "$pattern" == */ ]] && [[ "$NORMALIZED" == "$pattern"* ]]; then
107
+ {
108
+ printf 'SETTINGS PROTECTION: Modification blocked\n'
109
+ printf '\n'
110
+ printf ' File: %s\n' "$FILE_PATH"
111
+ printf ' Rule: Files under %s are protected from agent modification.\n' "$pattern"
112
+ printf '\n'
113
+ printf ' These files control the hook safety layer and must be\n'
114
+ printf ' modified by humans via reagent CLI or direct editing.\n'
115
+ } >&2
116
+ exit 2
117
+ fi
118
+ done
119
+
120
+ # ── 7. Case-insensitive fallback check ────────────────────────────────────────
121
+ # Catch case-manipulation bypass attempts (e.g., .Claude/Settings.json)
122
+ LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
123
+ for pattern in "${PROTECTED_PATTERNS[@]}"; do
124
+ LOWER_PATTERN=$(printf '%s' "$pattern" | tr '[:upper:]' '[:lower:]')
125
+ if [[ "$LOWER_NORM" == "$LOWER_PATTERN" ]]; then
126
+ {
127
+ printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
128
+ printf '\n'
129
+ printf ' File: %s\n' "$FILE_PATH"
130
+ printf ' Matched: %s\n' "$pattern"
131
+ } >&2
132
+ exit 2
133
+ fi
134
+ if [[ "$LOWER_PATTERN" == */ ]] && [[ "$LOWER_NORM" == "$LOWER_PATTERN"* ]]; then
135
+ {
136
+ printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
137
+ printf '\n'
138
+ printf ' File: %s\n' "$FILE_PATH"
139
+ printf ' Matched: %s*\n' "$pattern"
140
+ } >&2
141
+ exit 2
142
+ fi
143
+ done
144
+
145
+ exit 0
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: task-link-gate.sh
3
+ # Fires BEFORE every Bash tool call that matches "git commit".
4
+ # Checks that the commit message references a task ID (T-NNN format).
5
+ #
6
+ # OPT-IN: Only enforces when .reagent/policy.yaml contains:
7
+ # task_link_gate: true
8
+ #
9
+ # Exit codes:
10
+ # 0 = allow (disabled, task ref found, or not a commit command)
11
+ # 2 = block (no task reference in commit message)
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 'REAGENT 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
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
30
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
31
+ exit 2
32
+ fi
33
+
34
+ # ── 4. Check if task link gate is enabled (opt-in) ───────────────────────────
35
+ POLICY_FILE="${REAGENT_ROOT}/.reagent/policy.yaml"
36
+ if [[ ! -f "$POLICY_FILE" ]]; then
37
+ exit 0
38
+ fi
39
+ if ! grep -qE '^task_link_gate:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
40
+ exit 0
41
+ fi
42
+
43
+ # ── 5. Parse command ──────────────────────────────────────────────────────────
44
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
45
+
46
+ if [[ -z "$CMD" ]]; then
47
+ exit 0
48
+ fi
49
+
50
+ # Only trigger on git commit commands
51
+ if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
52
+ exit 0
53
+ fi
54
+
55
+ # ── 6. Check for task ID reference (T-NNN) ───────────────────────────────────
56
+ if printf '%s' "$CMD" | grep -qE 'T-[0-9]+'; then
57
+ exit 0
58
+ fi
59
+
60
+ # ── 7. Block — no task reference ──────────────────────────────────────────────
61
+ {
62
+ printf 'TASK LINK GATE: Commit message must reference a task ID\n'
63
+ printf '\n'
64
+ printf ' Pattern: T-NNN (e.g., T-001, T-042)\n'
65
+ printf ' Example: git commit -m "feat: implement cache CLI (T-012)"\n'
66
+ printf '\n'
67
+ printf ' To disable: set task_link_gate: false in .reagent/policy.yaml\n'
68
+ printf ' To see tasks: /tasks\n'
69
+ } >&2
70
+ exit 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/reagent",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Zero-trust MCP gateway — policy enforcement, secret redaction, and audit logging for AI-assisted projects",
5
5
  "license": "MIT",
6
6
  "author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
@@ -8,6 +8,12 @@
8
8
  "cursorRules": ["001-no-hallucination", "002-verify-before-act", "003-attribution"],
9
9
  "blockedPaths": [".reagent/", ".env"],
10
10
  "gitignoreEntries": [".claude/agents/", ".claude/hooks/", ".claude/settings.json", "RESTART.md"],
11
+ "qualityGates": {
12
+ "commitReview": { "enabled": true, "trivialThreshold": 20, "significantThreshold": 200 },
13
+ "pushReview": { "enabled": true },
14
+ "architectureAdvisory": { "enabled": true }
15
+ },
16
+ "pm": { "enabled": true, "taskLinkGate": false, "maxOpenTasks": 50 },
11
17
  "claudeMd": {
12
18
  "preflightCmd": "pnpm preflight",
13
19
  "attributionRule": "Do NOT include AI attribution in commits, PR bodies, code comments, or any content. When block_ai_attribution is enabled in .reagent/policy.yaml, the commit-msg hook REJECTS commits containing structural AI attribution (Co-Authored-By with AI names, 'Generated with [Tool]' footers, etc.). The attribution-advisory hook also blocks gh pr create/edit and git commit commands with attribution. You must remove all attribution markers before committing — the hooks will NOT silently fix them."
@@ -16,17 +22,28 @@
16
22
  "PreToolUse": [
17
23
  {
18
24
  "matcher": "Bash",
19
- "hooks": ["dangerous-bash-interceptor", "env-file-protection"]
25
+ "hooks": [
26
+ "dangerous-bash-interceptor",
27
+ "env-file-protection",
28
+ "dependency-audit-gate",
29
+ "commit-review-gate",
30
+ "push-review-gate"
31
+ ]
20
32
  },
21
33
  {
22
34
  "matcher": "Write|Edit",
23
- "hooks": ["secret-scanner"]
35
+ "hooks": ["secret-scanner", "settings-protection", "blocked-paths-enforcer"]
24
36
  },
25
37
  {
26
38
  "matcher": "Bash",
27
39
  "hooks": ["attribution-advisory"]
28
40
  }
29
41
  ],
30
- "PostToolUse": []
42
+ "PostToolUse": [
43
+ {
44
+ "matcher": "Write|Edit",
45
+ "hooks": ["architecture-review-gate"]
46
+ }
47
+ ]
31
48
  }
32
49
  }