@bookedsolid/rea 0.9.2 → 0.9.4
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/hooks/_lib/common.sh +6 -1
- package/hooks/_lib/push-review-core.sh +81 -28
- package/hooks/commit-review-gate.sh +96 -19
- package/package.json +1 -1
- package/scripts/tarball-smoke.sh +121 -3
package/hooks/_lib/common.sh
CHANGED
|
@@ -87,7 +87,12 @@ triage_score() {
|
|
|
87
87
|
local diff_input
|
|
88
88
|
diff_input=$(cat)
|
|
89
89
|
local line_count
|
|
90
|
-
|
|
90
|
+
# Defect K (rea#62) sibling: see `hooks/commit-review-gate.sh` for the
|
|
91
|
+
# full bug rationale. `|| echo "0"` captures "0\n0" on no-match, which
|
|
92
|
+
# breaks arithmetic comparisons downstream. `|| true` + bash default keeps
|
|
93
|
+
# the branch arithmetic-safe.
|
|
94
|
+
line_count=$(printf '%s' "$diff_input" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
95
|
+
line_count="${line_count:-0}"
|
|
91
96
|
|
|
92
97
|
# Check for sensitive paths
|
|
93
98
|
local sensitive=0
|
|
@@ -341,15 +341,7 @@ pr_core_run() {
|
|
|
341
341
|
exit 0
|
|
342
342
|
fi
|
|
343
343
|
|
|
344
|
-
# ── 5.
|
|
345
|
-
local POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
346
|
-
if [[ -f "$POLICY_FILE" ]]; then
|
|
347
|
-
if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
348
|
-
exit 0
|
|
349
|
-
fi
|
|
350
|
-
fi
|
|
351
|
-
|
|
352
|
-
# ── 5a. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ────────────────────
|
|
344
|
+
# ── 5. REA_SKIP_PUSH_REVIEW — whole-gate escape hatch ─────────────────────
|
|
353
345
|
# An opt-in bypass for the ENTIRE push-review gate (not just the Codex
|
|
354
346
|
# branch). Exists to unblock consumers when rea itself is broken or a
|
|
355
347
|
# corrupt policy/audit file would otherwise deadlock a push. Requires an
|
|
@@ -443,8 +435,8 @@ pr_core_run() {
|
|
|
443
435
|
--arg os_uid "$SKIP_OS_UID" \
|
|
444
436
|
--arg os_whoami "$SKIP_OS_WHOAMI" \
|
|
445
437
|
--arg os_hostname "$SKIP_OS_HOST" \
|
|
446
|
-
--
|
|
447
|
-
--
|
|
438
|
+
--argjson os_pid "$SKIP_OS_PID" \
|
|
439
|
+
--argjson os_ppid "$SKIP_OS_PPID" \
|
|
448
440
|
--arg os_ppid_cmd "$SKIP_OS_PPID_CMD" \
|
|
449
441
|
--arg os_tty "$SKIP_OS_TTY" \
|
|
450
442
|
--arg os_ci "$SKIP_OS_CI" \
|
|
@@ -856,7 +848,7 @@ pr_core_run() {
|
|
|
856
848
|
} >&2
|
|
857
849
|
exit 2
|
|
858
850
|
fi
|
|
859
|
-
if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/)' '
|
|
851
|
+
if printf '%s\n' "$_refspec_hits" | awk -v re='^(src/gateway/middleware/|hooks/|[.]claude/hooks/|src/policy/|[.]github/workflows/|[.]rea/|[.]husky/)' '
|
|
860
852
|
{
|
|
861
853
|
status = $1
|
|
862
854
|
if (status !~ /^[ACDMRTU]/) next
|
|
@@ -896,6 +888,8 @@ pr_core_run() {
|
|
|
896
888
|
printf ' - .claude/hooks/\n'
|
|
897
889
|
printf ' - src/policy/\n'
|
|
898
890
|
printf ' - .github/workflows/\n'
|
|
891
|
+
printf ' - .rea/\n'
|
|
892
|
+
printf ' - .husky/\n'
|
|
899
893
|
printf '\n'
|
|
900
894
|
printf ' Run /codex-review against %s, then retry the push.\n' "$local_sha"
|
|
901
895
|
printf ' The codex-adversarial agent emits the required audit entry.\n'
|
|
@@ -930,17 +924,26 @@ pr_core_run() {
|
|
|
930
924
|
fi
|
|
931
925
|
done
|
|
932
926
|
|
|
927
|
+
# Defect J (rea#61): branch-deletion guard MUST fail closed regardless of
|
|
928
|
+
# whether another refspec in the same push resolved a SOURCE_SHA. A mixed
|
|
929
|
+
# push like `git push origin safe:safe :main` iterates both refspecs; the
|
|
930
|
+
# safe refspec sets SOURCE_SHA from its local_sha, and the deletion refspec
|
|
931
|
+
# sets only HAS_DELETE=1 via its `continue` branch. If we check HAS_DELETE
|
|
932
|
+
# INSIDE the `-z SOURCE_SHA` fallback, the delete slips through unchecked.
|
|
933
|
+
# Hoist the check above the fallback so any deletion anywhere in the push
|
|
934
|
+
# blocks the entire push.
|
|
935
|
+
if [[ "$HAS_DELETE" -eq 1 ]]; then
|
|
936
|
+
{
|
|
937
|
+
printf 'PUSH BLOCKED: refspec is a branch deletion.\n'
|
|
938
|
+
printf '\n'
|
|
939
|
+
printf ' Branch deletions are sensitive operations and require explicit\n'
|
|
940
|
+
printf ' human action outside the agent. Perform the deletion manually.\n'
|
|
941
|
+
printf '\n'
|
|
942
|
+
} >&2
|
|
943
|
+
exit 2
|
|
944
|
+
fi
|
|
945
|
+
|
|
933
946
|
if [[ -z "$SOURCE_SHA" || -z "$MERGE_BASE" ]]; then
|
|
934
|
-
if [[ "$HAS_DELETE" -eq 1 ]]; then
|
|
935
|
-
{
|
|
936
|
-
printf 'PUSH BLOCKED: refspec is a branch deletion.\n'
|
|
937
|
-
printf '\n'
|
|
938
|
-
printf ' Branch deletions are sensitive operations and require explicit\n'
|
|
939
|
-
printf ' human action outside the agent. Perform the deletion manually.\n'
|
|
940
|
-
printf '\n'
|
|
941
|
-
} >&2
|
|
942
|
-
exit 2
|
|
943
|
-
fi
|
|
944
947
|
{
|
|
945
948
|
printf 'PUSH BLOCKED: could not resolve a merge-base for any push refspec.\n'
|
|
946
949
|
printf '\n'
|
|
@@ -975,8 +978,13 @@ pr_core_run() {
|
|
|
975
978
|
exit 0
|
|
976
979
|
fi
|
|
977
980
|
|
|
981
|
+
# Defect K (rea#62): `grep -c ... || echo "0"` captures `0\n0` when grep
|
|
982
|
+
# exits non-zero on no-match — grep still prints its own `0` to stdout before
|
|
983
|
+
# exiting, and the `|| echo "0"` branch appends another. `|| true` swallows
|
|
984
|
+
# the non-zero exit, and `${LINE_COUNT:-0}` defaults an empty result to 0.
|
|
978
985
|
local LINE_COUNT
|
|
979
|
-
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null ||
|
|
986
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
987
|
+
LINE_COUNT="${LINE_COUNT:-0}"
|
|
980
988
|
|
|
981
989
|
# ── 7a. Protected-path Codex adversarial review gate ──────────────────────
|
|
982
990
|
# The per-refspec check runs inside the main loop (section 7, above) so
|
|
@@ -987,8 +995,36 @@ pr_core_run() {
|
|
|
987
995
|
# refspec was either clean or had an acceptable audit.
|
|
988
996
|
|
|
989
997
|
# ── 8. Check review cache ─────────────────────────────────────────────────
|
|
990
|
-
|
|
991
|
-
|
|
998
|
+
# Defect L (rea#63): `shasum` is not installed on Alpine, distroless, or
|
|
999
|
+
# most minimal Linux CI images — only `sha256sum` is. The prior `shasum -a
|
|
1000
|
+
# 256 ... || echo ""` chain silently produced an empty PUSH_SHA, which the
|
|
1001
|
+
# rest of the gate treats as "no cache entry" rather than "hasher missing".
|
|
1002
|
+
# Combined with the silent-cache-miss fallback (Defect F), every push from
|
|
1003
|
+
# such a runner burned a full fresh codex review invisibly.
|
|
1004
|
+
#
|
|
1005
|
+
# Portable chain: sha256sum → shasum → openssl. The openssl branch uses
|
|
1006
|
+
# `awk '{print $NF}'` WITHOUT `-r` — `-r` was added in OpenSSL 3.0 /
|
|
1007
|
+
# LibreSSL 3.3+; on OpenSSL 1.1.1 (Debian 11, Ubuntu 20.04, RHEL 8,
|
|
1008
|
+
# Amazon Linux 2, Alpine 3.13–3.14) `-r` is rejected and stdout is empty.
|
|
1009
|
+
# `$NF` handles BOTH default output shapes: `(stdin)= <hex>` (1.1.x) and
|
|
1010
|
+
# `<hex> *stdin` (3.x/LibreSSL coreutils-style).
|
|
1011
|
+
#
|
|
1012
|
+
# Hex-64 validation catches broken pipes, partial reads, or unexpected
|
|
1013
|
+
# hasher output that would otherwise be silently cached as garbage.
|
|
1014
|
+
local PUSH_SHA=""
|
|
1015
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
1016
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | sha256sum 2>/dev/null | awk '{print $1}')
|
|
1017
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
1018
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 2>/dev/null | awk '{print $1}')
|
|
1019
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
1020
|
+
PUSH_SHA=$(printf '%s' "$DIFF_FULL" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')
|
|
1021
|
+
else
|
|
1022
|
+
printf 'rea push-review: WARN no sha256 hasher found (sha256sum/shasum/openssl); cache disabled\n' >&2
|
|
1023
|
+
fi
|
|
1024
|
+
if [[ -n "$PUSH_SHA" && ! "$PUSH_SHA" =~ ^[0-9a-f]{64}$ ]]; then
|
|
1025
|
+
printf 'rea push-review: WARN hasher returned invalid output; cache disabled\n' >&2
|
|
1026
|
+
PUSH_SHA=""
|
|
1027
|
+
fi
|
|
992
1028
|
|
|
993
1029
|
local -a REA_CLI_ARGS
|
|
994
1030
|
REA_CLI_ARGS=()
|
|
@@ -1043,8 +1079,10 @@ pr_core_run() {
|
|
|
1043
1079
|
fi
|
|
1044
1080
|
|
|
1045
1081
|
# ── 9. Block and request review ───────────────────────────────────────────
|
|
1082
|
+
# Defect K (rea#62): same `0\n0` bug as LINE_COUNT above.
|
|
1046
1083
|
local FILE_COUNT
|
|
1047
|
-
FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null ||
|
|
1084
|
+
FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null || true)
|
|
1085
|
+
FILE_COUNT="${FILE_COUNT:-0}"
|
|
1048
1086
|
|
|
1049
1087
|
{
|
|
1050
1088
|
printf 'PUSH REVIEW GATE: Review required before pushing\n'
|
|
@@ -1056,8 +1094,23 @@ pr_core_run() {
|
|
|
1056
1094
|
printf ' Action required:\n'
|
|
1057
1095
|
printf ' 1. Spawn a code-reviewer agent to review: git diff %s..%s\n' "$MERGE_BASE" "$SOURCE_SHA"
|
|
1058
1096
|
printf ' 2. Spawn a security-engineer agent for security review\n'
|
|
1059
|
-
|
|
1060
|
-
|
|
1097
|
+
# Defect L (rea#63) follow-up: when no sha256 hasher is available the
|
|
1098
|
+
# cache is disabled and PUSH_SHA is empty. Emitting `rea cache set <blank>
|
|
1099
|
+
# pass ...` would be a dead-end — the CLI rejects the empty positional.
|
|
1100
|
+
# Print an alternate completion path in that case. The Codex-adversarial
|
|
1101
|
+
# review concerns list flagged this UX cliff in the 0.9.4 pass.
|
|
1102
|
+
if [[ -n "$PUSH_SHA" ]]; then
|
|
1103
|
+
printf ' 3. After both pass, cache the result:\n'
|
|
1104
|
+
printf ' rea cache set %s pass --branch %s --base %s\n' "$PUSH_SHA" "$SOURCE_BRANCH" "$TARGET_BRANCH"
|
|
1105
|
+
else
|
|
1106
|
+
printf ' 3. Cache is DISABLED on this host (no sha256 hasher found).\n'
|
|
1107
|
+
printf ' After both reviews pass, bypass the push-review gate with:\n'
|
|
1108
|
+
printf ' REA_SKIP_PUSH_REVIEW="<reason>" git push ...\n'
|
|
1109
|
+
printf ' The bypass is audited as push.review.skipped — this is the\n'
|
|
1110
|
+
printf ' documented escape hatch when cache is unavailable.\n'
|
|
1111
|
+
printf ' To restore the cache path, install one of: sha256sum,\n'
|
|
1112
|
+
printf ' shasum (Perl Digest::SHA), or openssl.\n'
|
|
1113
|
+
fi
|
|
1061
1114
|
printf '\n'
|
|
1062
1115
|
} >&2
|
|
1063
1116
|
exit 2
|
|
@@ -103,18 +103,7 @@ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
|
|
|
103
103
|
exit 0
|
|
104
104
|
fi
|
|
105
105
|
|
|
106
|
-
# ── 5.
|
|
107
|
-
# Fail-open if policy doesn't exist or doesn't have quality_gates
|
|
108
|
-
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
109
|
-
if [[ -f "$POLICY_FILE" ]]; then
|
|
110
|
-
if grep -qE '^quality_gates:' "$POLICY_FILE" 2>/dev/null; then
|
|
111
|
-
if grep -qE 'commit_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
112
|
-
exit 0
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
# ── 6. Compute diff stats ────────────────────────────────────────────────────
|
|
106
|
+
# ── 5. Compute diff stats ────────────────────────────────────────────────────
|
|
118
107
|
# Get staged diff (what would be committed)
|
|
119
108
|
DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
|
|
120
109
|
DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
|
|
@@ -125,7 +114,16 @@ if [[ -z "$DIFF_OUTPUT" ]]; then
|
|
|
125
114
|
fi
|
|
126
115
|
|
|
127
116
|
# Count changed lines (additions + deletions)
|
|
128
|
-
|
|
117
|
+
# Defect K (rea#62) sibling: `|| echo "0"` captures "0\n0" into LINE_COUNT
|
|
118
|
+
# when grep exits non-zero on a no-match — grep still prints its own `0` and
|
|
119
|
+
# `echo "0"` appends another. At this site the concatenated `"0\n0"` is then
|
|
120
|
+
# evaluated as arithmetic (`-gt $SIGNIFICANT_THRESHOLD`, `-ge $TRIVIAL_THRESHOLD`
|
|
121
|
+
# below) and bash emits a "syntax error in expression" at runtime on any
|
|
122
|
+
# rename-only / mode-only / empty-file-add diff. `|| true` + bash-default
|
|
123
|
+
# expansion fixes both the banner cosmetic and the arithmetic-unsafe control
|
|
124
|
+
# flow in one shot.
|
|
125
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || true)
|
|
126
|
+
LINE_COUNT="${LINE_COUNT:-0}"
|
|
129
127
|
|
|
130
128
|
# Check for sensitive paths
|
|
131
129
|
SENSITIVE=0
|
|
@@ -173,17 +171,79 @@ fi
|
|
|
173
171
|
|
|
174
172
|
# ── 10. Check review cache for all non-trivial commits ────────────────────────
|
|
175
173
|
# Compute SHA and branch here so both standard and significant tiers share them.
|
|
176
|
-
|
|
174
|
+
#
|
|
175
|
+
# Defect L (rea#63) sibling: `shasum` is not installed on Alpine, distroless,
|
|
176
|
+
# or most minimal Linux CI images — only `sha256sum` is. The prior chain
|
|
177
|
+
# silently produced an empty STAGED_SHA, which the cache block then skipped
|
|
178
|
+
# AND the banner at §11 rendered as `rea cache set pass` — a dead-end the
|
|
179
|
+
# agent cannot execute. Portable chain mirrors push-review-core.sh §8:
|
|
180
|
+
# sha256sum → shasum → openssl. The openssl branch uses `awk '{print $NF}'`
|
|
181
|
+
# WITHOUT `-r` to stay compatible with OpenSSL 1.1.x (Debian 11, Ubuntu
|
|
182
|
+
# 20.04, RHEL 8, Amazon Linux 2, Alpine 3.13–3.14).
|
|
183
|
+
STAGED_SHA=""
|
|
184
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
185
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | sha256sum 2>/dev/null | awk '{print $1}')
|
|
186
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
187
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 2>/dev/null | awk '{print $1}')
|
|
188
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
189
|
+
STAGED_SHA=$(printf '%s' "$DIFF_FULL" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')
|
|
190
|
+
else
|
|
191
|
+
printf 'rea commit-review: WARN no sha256 hasher found (sha256sum/shasum/openssl); cache disabled\n' >&2
|
|
192
|
+
fi
|
|
193
|
+
if [[ -n "$STAGED_SHA" && ! "$STAGED_SHA" =~ ^[0-9a-f]{64}$ ]]; then
|
|
194
|
+
printf 'rea commit-review: WARN hasher returned invalid output; cache disabled\n' >&2
|
|
195
|
+
STAGED_SHA=""
|
|
196
|
+
fi
|
|
177
197
|
BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
|
|
178
198
|
CACHE_FILE="${REA_ROOT}/.rea/review-cache.json"
|
|
179
199
|
|
|
200
|
+
# Codex pass-3 finding #1: `rea cache check` and `rea cache set` both declare
|
|
201
|
+
# `--base` as a `requiredOption` in src/cli/index.ts. Prior versions of this
|
|
202
|
+
# gate omitted `--base`, so (a) the CLI path exited non-zero and the
|
|
203
|
+
# `|| echo '{"hit":false}'` fallback quietly masked the contract error, and
|
|
204
|
+
# (b) the section-11 banner instructed the agent to run `rea cache set <sha>
|
|
205
|
+
# pass` — also missing `--base`, rejected by the CLI on every retry. A
|
|
206
|
+
# successful cache flow was unreachable.
|
|
207
|
+
#
|
|
208
|
+
# Resolve BASE_BRANCH by the same preference order the push-gate uses in
|
|
209
|
+
# push-review-core.sh §7 (lines 778-794): origin/HEAD → origin/main →
|
|
210
|
+
# origin/master → empty. If nothing resolves, disable the cache (the
|
|
211
|
+
# alternative is emitting a cache command the CLI rejects on every call).
|
|
212
|
+
BASE_BRANCH=""
|
|
213
|
+
_origin_head=$(cd "$REA_ROOT" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
214
|
+
if [[ -n "$_origin_head" ]]; then
|
|
215
|
+
BASE_BRANCH="${_origin_head#refs/remotes/origin/}"
|
|
216
|
+
fi
|
|
217
|
+
if [[ -z "$BASE_BRANCH" ]]; then
|
|
218
|
+
# Use `git -C` so the current-shell cwd is never mutated — matches the
|
|
219
|
+
# cross-repo guard at §1a and keeps the file's dominant idiom. Raw
|
|
220
|
+
# `cd "$REA_ROOT" && git …` would leave the hook process sitting in
|
|
221
|
+
# $REA_ROOT, which is safe today but breaks silently if a future edit
|
|
222
|
+
# adds a relative-path command downstream.
|
|
223
|
+
if git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/main >/dev/null 2>&1; then
|
|
224
|
+
BASE_BRANCH="main"
|
|
225
|
+
elif git -C "$REA_ROOT" rev-parse --verify --quiet refs/remotes/origin/master >/dev/null 2>&1; then
|
|
226
|
+
BASE_BRANCH="master"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
if [[ -z "$BASE_BRANCH" && -n "$STAGED_SHA" ]]; then
|
|
230
|
+
printf 'rea commit-review: WARN could not resolve base branch (no origin/HEAD, no origin/main, no origin/master); cache disabled\n' >&2
|
|
231
|
+
STAGED_SHA=""
|
|
232
|
+
fi
|
|
233
|
+
unset _origin_head
|
|
234
|
+
|
|
180
235
|
if [[ -n "$STAGED_SHA" ]]; then
|
|
181
236
|
CACHE_HIT=false
|
|
182
237
|
|
|
183
|
-
# Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys
|
|
238
|
+
# Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys.
|
|
239
|
+
# Cache predicate must require BOTH `.hit == true` AND `.result == "pass"` —
|
|
240
|
+
# a cached `fail` verdict would otherwise satisfy `.hit == true` and let the
|
|
241
|
+
# commit proceed despite a recorded negative review. Mirrors the push-gate
|
|
242
|
+
# predicate at push-review-core.sh §8; the §218-226 direct-cache fallback
|
|
243
|
+
# already enforces `result == "pass"`, so the two paths must agree.
|
|
184
244
|
if [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
|
|
185
|
-
CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" 2>/dev/null || echo '{"hit":false}')
|
|
186
|
-
if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
|
|
245
|
+
CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" --base "$BASE_BRANCH" 2>/dev/null || echo '{"hit":false}')
|
|
246
|
+
if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true and .result == "pass"' >/dev/null 2>&1; then
|
|
187
247
|
CACHE_HIT=true
|
|
188
248
|
fi
|
|
189
249
|
fi
|
|
@@ -221,8 +281,25 @@ fi
|
|
|
221
281
|
printf ' 1. Inspect: git diff --cached\n'
|
|
222
282
|
printf ' 2. Decide: Is this safe to commit? (initial commits, refactors, and\n'
|
|
223
283
|
printf ' feature work are normal — use judgement, not ceremony)\n'
|
|
224
|
-
|
|
225
|
-
|
|
284
|
+
# Defect L follow-up: when no sha256 hasher is available STAGED_SHA is empty
|
|
285
|
+
# and `rea cache set pass` is a dead-end the CLI rejects. Branch the banner
|
|
286
|
+
# to surface an actionable path instead. Unlike push-review-core.sh there is
|
|
287
|
+
# no `REA_SKIP_COMMIT_REVIEW` env escape hatch (the commit gate only fires
|
|
288
|
+
# under Claude Code's Bash `PreToolUse` matcher, so a human direct-shell
|
|
289
|
+
# commit bypasses it entirely). The only remediation is to install a sha256
|
|
290
|
+
# hasher or ask the user to commit directly.
|
|
291
|
+
if [[ -n "$STAGED_SHA" ]]; then
|
|
292
|
+
printf ' 3. Approve: rea cache set %s pass --branch %s --base %s\n' \
|
|
293
|
+
"$STAGED_SHA" "$BRANCH" "$BASE_BRANCH"
|
|
294
|
+
printf ' 4. Retry the git commit command\n'
|
|
295
|
+
else
|
|
296
|
+
printf ' 3. Cache is DISABLED on this host (no sha256 hasher or no base\n'
|
|
297
|
+
printf ' branch resolvable). Install one of: sha256sum (Linux coreutils),\n'
|
|
298
|
+
printf ' shasum (perl-core), or openssl; or ensure origin/HEAD is set so\n'
|
|
299
|
+
printf ' the gate can identify the merge target. Without these the cache\n'
|
|
300
|
+
printf ' path cannot complete — escalate to the user if neither can be\n'
|
|
301
|
+
printf ' provided.\n'
|
|
302
|
+
fi
|
|
226
303
|
printf '\n'
|
|
227
304
|
printf ' Only escalate to the user if you find a genuine problem in the diff.\n'
|
|
228
305
|
} >&2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
package/scripts/tarball-smoke.sh
CHANGED
|
@@ -222,17 +222,134 @@ if [ -n "$SEC_CHANGESETS" ]; then
|
|
|
222
222
|
echo "[smoke] security-claim gate: $(printf '%s\n' "$SEC_CHANGESETS" | wc -l | awk '{print $1}') changeset(s) tagged [security]"
|
|
223
223
|
|
|
224
224
|
SEC_SRC_TESTS="$(cd "$REPO_ROOT" && find src -type f \( -name '*sanitize*.test.ts' -o -name '*security*.test.ts' \) 2>/dev/null | sort)"
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
# 0.9.3 extension — some security hotfixes touch ONLY shell hooks (no TS/dist
|
|
226
|
+
# symbols), so the compiled-symbol gate above doesn't apply. For those, the
|
|
227
|
+
# regression proof lives under __tests__/hooks/*{security,bypass,injection,
|
|
228
|
+
# sanitize}*.test.ts and the tarball must ship the hook file(s) the test
|
|
229
|
+
# exercises. This block runs IN ADDITION to the src/ symbol gate — either
|
|
230
|
+
# layer alone can satisfy a [security] changeset, but at least one MUST.
|
|
231
|
+
SEC_HOOK_TESTS="$(cd "$REPO_ROOT" && find __tests__/hooks -type f \( -name '*security*.test.ts' -o -name '*bypass*.test.ts' -o -name '*sanitize*.test.ts' -o -name '*injection*.test.ts' \) 2>/dev/null | sort)"
|
|
232
|
+
|
|
233
|
+
if [ -z "$SEC_SRC_TESTS" ] && [ -z "$SEC_HOOK_TESTS" ]; then
|
|
234
|
+
echo "[smoke] FAIL — [security] changeset present but no matching regression test found:" >&2
|
|
235
|
+
echo "[smoke] - src/**/(*sanitize*|*security*).test.ts — for compiled-symbol fixes" >&2
|
|
236
|
+
echo "[smoke] - __tests__/hooks/(*security*|*bypass*|*sanitize*|*injection*).test.ts — for hook fixes" >&2
|
|
227
237
|
echo "[smoke] a security-claim changeset with no matching regression test is a trust violation" >&2
|
|
228
238
|
exit 2
|
|
229
239
|
fi
|
|
230
240
|
|
|
231
|
-
#
|
|
241
|
+
# Hook-level gate: for each hook-security test FILE, extract the hook
|
|
242
|
+
# file path(s) it installs/exercises (relative to REPO_ROOT) and assert
|
|
243
|
+
# the tarball ships that hook under node_modules/@bookedsolid/rea/hooks/
|
|
244
|
+
# AND that `rea init` fanned it out to $SMOKE_DIR/.claude/hooks/.
|
|
245
|
+
#
|
|
246
|
+
# Known narrowness (called out honestly rather than papered over):
|
|
247
|
+
# - Granularity is per-test-file, not per-`it()` block. A file may
|
|
248
|
+
# contain multiple `it(...)` cases; the extractor scans the whole
|
|
249
|
+
# file body. In practice a [security]-claim test file should focus
|
|
250
|
+
# on one defect class; mixing unrelated `it()` cases with different
|
|
251
|
+
# hook refs dilutes the proof. PR review is the mitigation.
|
|
252
|
+
# - A [security] file with zero extractable refs fails LOUDLY (see
|
|
253
|
+
# EMPTY_REF_TESTS below). The narrowness only applies to files that
|
|
254
|
+
# do extract refs but don't scope them per `it()`.
|
|
255
|
+
if [ -n "$SEC_HOOK_TESTS" ]; then
|
|
256
|
+
HOOK_MISSING=""
|
|
257
|
+
HOOK_COUNT=0
|
|
258
|
+
EMPTY_REF_TESTS=""
|
|
259
|
+
while IFS= read -r hook_test; do
|
|
260
|
+
[ -z "$hook_test" ] && continue
|
|
261
|
+
# Pull hook paths referenced by the test. Matches forms like:
|
|
262
|
+
# 'hooks', 'push-review-gate.sh'
|
|
263
|
+
# 'hooks', '_lib', 'push-review-core.sh'
|
|
264
|
+
# 'hooks', 'commit-review-gate.sh'
|
|
265
|
+
HOOK_REFS="$(perl -0777 -ne '
|
|
266
|
+
while (/path\.join\(\s*REPO_ROOT\s*,\s*[\x27"]hooks[\x27"](?:\s*,\s*[\x27"]([^\x27"]+)[\x27"])*\s*\)/sg) {
|
|
267
|
+
my $all = $&;
|
|
268
|
+
my @parts;
|
|
269
|
+
while ($all =~ /[\x27"]([^\x27"]+)[\x27"]/g) {
|
|
270
|
+
push @parts, $1 unless $1 eq "REPO_ROOT";
|
|
271
|
+
}
|
|
272
|
+
print join("/", @parts), "\n" if @parts;
|
|
273
|
+
}
|
|
274
|
+
' "$REPO_ROOT/$hook_test" 2>/dev/null | sort -u)"
|
|
275
|
+
|
|
276
|
+
# Per-test failure: a [security] hook-test that yields zero extractable
|
|
277
|
+
# refs (e.g. uses template literals, dynamic concatenation, or helper
|
|
278
|
+
# indirection) is invisible to this gate. Fail loud so the author is
|
|
279
|
+
# forced to use the extractable path.join(REPO_ROOT, 'hooks', ...) shape,
|
|
280
|
+
# rather than having a lone extractable neighbor test silently satisfy
|
|
281
|
+
# the whole gate.
|
|
282
|
+
if [ -z "$HOOK_REFS" ]; then
|
|
283
|
+
EMPTY_REF_TESTS="$EMPTY_REF_TESTS
|
|
284
|
+
$hook_test"
|
|
285
|
+
continue
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
while IFS= read -r rel; do
|
|
289
|
+
[ -z "$rel" ] && continue
|
|
290
|
+
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
291
|
+
# `rel` already includes the leading `hooks/` segment from perl. It
|
|
292
|
+
# looks like `hooks/push-review-gate.sh` or
|
|
293
|
+
# `hooks/_lib/push-review-core.sh`. The tarball ships hooks under
|
|
294
|
+
# `node_modules/@bookedsolid/rea/hooks/` and `rea init` fans them out
|
|
295
|
+
# to `$SMOKE_DIR/.claude/hooks/`. Assert BOTH — the tarball
|
|
296
|
+
# source-of-truth AND the post-init install surface.
|
|
297
|
+
rel_no_prefix="${rel#hooks/}"
|
|
298
|
+
TARBALL_HOOK="$SMOKE_DIR/node_modules/@bookedsolid/rea/hooks/$rel_no_prefix"
|
|
299
|
+
INSTALLED_HOOK="$SMOKE_DIR/.claude/hooks/$rel_no_prefix"
|
|
300
|
+
if [ ! -f "$TARBALL_HOOK" ] || [ ! -f "$INSTALLED_HOOK" ]; then
|
|
301
|
+
HOOK_MISSING="$HOOK_MISSING
|
|
302
|
+
$rel (exercised by $hook_test)"
|
|
303
|
+
fi
|
|
304
|
+
done <<< "$HOOK_REFS"
|
|
305
|
+
done <<< "$SEC_HOOK_TESTS"
|
|
306
|
+
|
|
307
|
+
if [ -n "$HOOK_MISSING" ]; then
|
|
308
|
+
echo "[smoke] FAIL — [security] hook-test gate: hook file(s) under test are MISSING from tarball:" >&2
|
|
309
|
+
printf '%s\n' "$HOOK_MISSING" >&2
|
|
310
|
+
exit 2
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
if [ -n "$EMPTY_REF_TESTS" ]; then
|
|
314
|
+
echo "[smoke] FAIL — [security] hook-test gate: one or more hook-security tests yielded zero extractable hook references:" >&2
|
|
315
|
+
printf '%s\n' "$EMPTY_REF_TESTS" >&2
|
|
316
|
+
echo "[smoke] hook-security tests MUST reference hook files via the literal shape" >&2
|
|
317
|
+
echo "[smoke] path.join(REPO_ROOT, 'hooks', '<name>.sh') (or with a nested '_lib' arg)" >&2
|
|
318
|
+
echo "[smoke] Template literals, dynamic concatenation, and helper indirection are" >&2
|
|
319
|
+
echo "[smoke] invisible to this gate and would let hook changes ship unverified." >&2
|
|
320
|
+
exit 2
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if [ "$HOOK_COUNT" -eq 0 ]; then
|
|
324
|
+
echo "[smoke] FAIL — [security] hook-test gate: no checkable hook references extracted" >&2
|
|
325
|
+
echo "[smoke] hook-security tests must reference hook files via path.join(REPO_ROOT, 'hooks', ...)" >&2
|
|
326
|
+
echo "[smoke] so the gate can verify the hook ships in the tarball" >&2
|
|
327
|
+
exit 2
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
echo "[smoke] → $(printf '%s\n' "$SEC_HOOK_TESTS" | wc -l | awk '{print $1}') hook-security test(s), $HOOK_COUNT hook ref(s) all present in tarball"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# The compiled-symbol gate below only runs when src/ security tests exist.
|
|
334
|
+
# A hook-only hotfix satisfies via the block above. Flag the src/ gate to
|
|
335
|
+
# skip gracefully — the remaining smoke checks (export resolution, tree
|
|
336
|
+
# equality) still run unconditionally below.
|
|
337
|
+
SKIP_SRC_SYMBOL_GATE=0
|
|
338
|
+
if [ -z "$SEC_SRC_TESTS" ]; then
|
|
339
|
+
SKIP_SRC_SYMBOL_GATE=1
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# For each src security test, collect the named imports pulled from relative
|
|
232
343
|
# paths — those are the symbols under test and must be compiled into dist/.
|
|
233
344
|
# Example line we want to match:
|
|
234
345
|
# import { sanitizeHealthSnapshot, INJECTION_REDACTED_PLACEHOLDER } from './health';
|
|
235
346
|
# We ignore imports from bare package names ('vitest', 'node:fs', etc.).
|
|
347
|
+
#
|
|
348
|
+
# Skipped when only __tests__/hooks/ security tests exist (hook-only hotfix);
|
|
349
|
+
# the hook-test gate above is authoritative for that case.
|
|
350
|
+
if [ "$SKIP_SRC_SYMBOL_GATE" = "1" ]; then
|
|
351
|
+
echo "[smoke] → src/ symbol gate skipped (no src/*{security,sanitize}*.test.ts — hook-test gate is authoritative)"
|
|
352
|
+
else
|
|
236
353
|
MISSING_SYMBOLS=""
|
|
237
354
|
SYMBOL_COUNT=0
|
|
238
355
|
while IFS= read -r src_test; do
|
|
@@ -294,6 +411,7 @@ if [ -n "$SEC_CHANGESETS" ]; then
|
|
|
294
411
|
fi
|
|
295
412
|
|
|
296
413
|
echo "[smoke] → $(printf '%s\n' "$SEC_SRC_TESTS" | wc -l | awk '{print $1}') security regression test(s), $SYMBOL_COUNT imported symbol(s) all present in dist/"
|
|
414
|
+
fi # SKIP_SRC_SYMBOL_GATE
|
|
297
415
|
fi
|
|
298
416
|
|
|
299
417
|
# Verify every declared public export resolves. If the exports map points at a
|