@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.
- package/README.md +407 -54
- package/agents/product-owner.md +44 -0
- package/commands/plan-work.md +19 -0
- package/commands/tasks.md +26 -0
- package/dist/cli/commands/cache.d.ts +2 -0
- package/dist/cli/commands/cache.d.ts.map +1 -0
- package/dist/cli/commands/cache.js +114 -0
- package/dist/cli/commands/cache.js.map +1 -0
- package/dist/cli/commands/init/agents.d.ts +3 -0
- package/dist/cli/commands/init/agents.d.ts.map +1 -0
- package/dist/cli/commands/init/agents.js +47 -0
- package/dist/cli/commands/init/agents.js.map +1 -0
- package/dist/cli/commands/init/claude-hooks.d.ts +3 -0
- package/dist/cli/commands/init/claude-hooks.d.ts.map +1 -0
- package/dist/cli/commands/init/claude-hooks.js +134 -0
- package/dist/cli/commands/init/claude-hooks.js.map +1 -0
- package/dist/cli/commands/init/claude-md.d.ts +3 -0
- package/dist/cli/commands/init/claude-md.d.ts.map +1 -0
- package/dist/cli/commands/init/claude-md.js +52 -0
- package/dist/cli/commands/init/claude-md.js.map +1 -0
- package/dist/cli/commands/init/commands.d.ts +3 -0
- package/dist/cli/commands/init/commands.d.ts.map +1 -0
- package/dist/cli/commands/init/commands.js +31 -0
- package/dist/cli/commands/init/commands.js.map +1 -0
- package/dist/cli/commands/init/cursor-rules.d.ts +3 -0
- package/dist/cli/commands/init/cursor-rules.d.ts.map +1 -0
- package/dist/cli/commands/init/cursor-rules.js +30 -0
- package/dist/cli/commands/init/cursor-rules.js.map +1 -0
- package/dist/cli/commands/init/gateway-config.d.ts +3 -0
- package/dist/cli/commands/init/gateway-config.d.ts.map +1 -0
- package/dist/cli/commands/init/gateway-config.js +51 -0
- package/dist/cli/commands/init/gateway-config.js.map +1 -0
- package/dist/cli/commands/init/gitignore.d.ts +3 -0
- package/dist/cli/commands/init/gitignore.d.ts.map +1 -0
- package/dist/cli/commands/init/gitignore.js +20 -0
- package/dist/cli/commands/init/gitignore.js.map +1 -0
- package/dist/cli/commands/init/husky-hooks.d.ts +3 -0
- package/dist/cli/commands/init/husky-hooks.d.ts.map +1 -0
- package/dist/cli/commands/init/husky-hooks.js +73 -0
- package/dist/cli/commands/init/husky-hooks.js.map +1 -0
- package/dist/cli/commands/{init.d.ts → init/index.d.ts} +1 -1
- package/dist/cli/commands/init/index.d.ts.map +1 -0
- package/dist/cli/commands/init/index.js +121 -0
- package/dist/cli/commands/init/index.js.map +1 -0
- package/dist/cli/commands/init/pm.d.ts +9 -0
- package/dist/cli/commands/init/pm.d.ts.map +1 -0
- package/dist/cli/commands/init/pm.js +40 -0
- package/dist/cli/commands/init/pm.js.map +1 -0
- package/dist/cli/commands/init/policy.d.ts +3 -0
- package/dist/cli/commands/init/policy.d.ts.map +1 -0
- package/dist/cli/commands/init/policy.js +61 -0
- package/dist/cli/commands/init/policy.js.map +1 -0
- package/dist/cli/commands/init/types.d.ts +29 -0
- package/dist/cli/commands/init/types.d.ts.map +1 -0
- package/dist/cli/commands/init/types.js +2 -0
- package/dist/cli/commands/init/types.js.map +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/native-tools.d.ts +8 -0
- package/dist/gateway/native-tools.d.ts.map +1 -0
- package/dist/gateway/native-tools.js +190 -0
- package/dist/gateway/native-tools.js.map +1 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +7 -3
- package/dist/gateway/server.js.map +1 -1
- package/dist/pm/github-bridge.d.ts +36 -0
- package/dist/pm/github-bridge.d.ts.map +1 -0
- package/dist/pm/github-bridge.js +138 -0
- package/dist/pm/github-bridge.js.map +1 -0
- package/dist/pm/task-store.d.ts +39 -0
- package/dist/pm/task-store.d.ts.map +1 -0
- package/dist/pm/task-store.js +189 -0
- package/dist/pm/task-store.js.map +1 -0
- package/dist/pm/types.d.ts +70 -0
- package/dist/pm/types.d.ts.map +1 -0
- package/dist/pm/types.js +22 -0
- package/dist/pm/types.js.map +1 -0
- package/hooks/_lib/common.sh +87 -0
- package/hooks/architecture-review-gate.sh +84 -0
- package/hooks/blocked-paths-enforcer.sh +169 -0
- package/hooks/commit-review-gate.sh +131 -0
- package/hooks/dangerous-bash-interceptor.sh +32 -0
- package/hooks/dependency-audit-gate.sh +118 -0
- package/hooks/push-review-gate.sh +105 -0
- package/hooks/settings-protection.sh +145 -0
- package/hooks/task-link-gate.sh +70 -0
- package/package.json +1 -1
- package/profiles/bst-internal.json +20 -3
- package/profiles/client-engagement.json +20 -3
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -560
- 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.
|
|
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": [
|
|
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
|
}
|