@bookedsolid/rea 0.10.0 → 0.10.2

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 (33) hide show
  1. package/dist/audit/append.d.ts +35 -1
  2. package/dist/audit/append.js +79 -11
  3. package/dist/cli/audit.js +130 -34
  4. package/dist/cli/doctor.js +1 -1
  5. package/dist/cli/index.js +18 -0
  6. package/dist/cli/tofu.d.ts +57 -0
  7. package/dist/cli/tofu.js +134 -0
  8. package/dist/gateway/audit/rotator.js +4 -0
  9. package/dist/gateway/middleware/audit-types.d.ts +35 -0
  10. package/dist/gateway/middleware/audit.js +6 -0
  11. package/dist/hooks/review-gate/args.d.ts +126 -0
  12. package/dist/hooks/review-gate/args.js +315 -0
  13. package/dist/hooks/review-gate/banner.d.ts +97 -0
  14. package/dist/hooks/review-gate/banner.js +172 -0
  15. package/dist/hooks/review-gate/cache-key.d.ts +55 -0
  16. package/dist/hooks/review-gate/cache-key.js +41 -0
  17. package/dist/hooks/review-gate/constants.d.ts +26 -0
  18. package/dist/hooks/review-gate/constants.js +34 -0
  19. package/dist/hooks/review-gate/errors.d.ts +72 -0
  20. package/dist/hooks/review-gate/errors.js +100 -0
  21. package/dist/hooks/review-gate/hash.d.ts +43 -0
  22. package/dist/hooks/review-gate/hash.js +46 -0
  23. package/dist/hooks/review-gate/index.d.ts +21 -0
  24. package/dist/hooks/review-gate/index.js +21 -0
  25. package/dist/hooks/review-gate/metadata.d.ts +98 -0
  26. package/dist/hooks/review-gate/metadata.js +158 -0
  27. package/dist/hooks/review-gate/policy.d.ts +55 -0
  28. package/dist/hooks/review-gate/policy.js +71 -0
  29. package/dist/hooks/review-gate/protected-paths.d.ts +46 -0
  30. package/dist/hooks/review-gate/protected-paths.js +76 -0
  31. package/dist/registry/tofu-gate.js +4 -1
  32. package/hooks/_lib/push-review-core.sh +121 -25
  33. package/package.json +1 -1
@@ -719,12 +719,20 @@ pr_core_run() {
719
719
  # fail-closed and require an explicit review.
720
720
  local SOURCE_SHA="" MERGE_BASE="" TARGET_BRANCH="" SOURCE_REF=""
721
721
  local HAS_DELETE=0 BEST_COUNT=0
722
- local rec local_sha remote_sha local_ref remote_ref target mb mb_status count count_status
722
+ local rec local_sha remote_sha local_ref remote_ref target resolved_base mb mb_status count count_status
723
723
  for rec in "${REFSPEC_RECORDS[@]}"; do
724
724
  IFS='|' read -r local_sha remote_sha local_ref remote_ref <<<"$rec"
725
725
  target="${remote_ref#refs/heads/}"
726
726
  target="${target#refs/for/}"
727
727
  [[ -z "$target" ]] && target="main"
728
+ # Defect N: track the SEMANTIC base (the ref the diff was anchored on)
729
+ # distinctly from `target` (the pushed remote ref). For a tracked branch
730
+ # they coincide; for a new branch, `target` is the branch name being
731
+ # created — which is NOT what we reviewed against, so `Target:` must
732
+ # echo `resolved_base` instead. Default to `target` for the tracked
733
+ # case; the new-branch path overrides with the resolved default_ref
734
+ # short name below.
735
+ resolved_base="$target"
728
736
 
729
737
  if [[ "$local_sha" == "$ZERO_SHA" ]]; then
730
738
  HAS_DELETE=1
@@ -774,25 +782,81 @@ pr_core_run() {
774
782
  #
775
783
  # argv_remote is set from the adapter's argv (git passes the remote name
776
784
  # as $1 on pre-push); defaults to "origin" when absent (BUG-008 sniff).
777
- local default_ref default_ref_status
778
- default_ref=$(cd "$REA_ROOT" && git symbolic-ref "refs/remotes/${argv_remote}/HEAD" 2>/dev/null)
779
- default_ref_status=$?
780
- if [[ "$default_ref_status" -ne 0 || -z "$default_ref" ]]; then
781
- # symbolic-ref failed (common on shallow or mirror clones where
782
- # origin/HEAD was never set). Probe the common default-branch names in
783
- # order: main, then master. Both are remote-tracking refs and still
784
- # server-authoritative; the order matters only for projects that still
785
- # default to `master` (older internal forks), where without this
786
- # fallback the first push of a new branch would fail closed.
787
- if cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/remotes/${argv_remote}/main" >/dev/null 2>&1; then
788
- default_ref="refs/remotes/${argv_remote}/main"
789
- elif cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/remotes/${argv_remote}/master" >/dev/null 2>&1; then
790
- default_ref="refs/remotes/${argv_remote}/master"
791
- else
792
- default_ref=""
785
+ #
786
+ # Defect N (0.10.1): BEFORE falling back to the remote's default branch,
787
+ # consult per-branch config `branch.<source>.base`. A feature branch
788
+ # targeting `dev` in a main-as-production repo would otherwise resolve
789
+ # against `origin/main` silently, producing a diff that spans the entire
790
+ # dev→main history reviewers see "Scope: 28690 lines" for a 4-file
791
+ # change. The git-config route uses local branch knowledge that is
792
+ # authoritative for this working copy (set via `git branch --set-upstream`,
793
+ # or by CI tooling that tracks the intended target). This is consulted
794
+ # BEFORE origin/HEAD because the latter is a server-default that may
795
+ # mis-represent the reviewer's actual intent for this specific branch.
796
+ local default_ref default_ref_status configured_base source_branch
797
+ source_branch="${local_ref#refs/heads/}"
798
+ default_ref=""
799
+ # Codex 0.10.1 finding #1: `local` is function-scoped, not loop-
800
+ # iteration-scoped — without an explicit reset, iteration N inherits
801
+ # iteration N-1's configured_base and falsely promotes resolved_base
802
+ # when the current refspec's local_ref does NOT begin with refs/heads/
803
+ # (tag push, gerrit-style refs/for/, etc.). Reset before every
804
+ # potential assignment so each iteration sees a clean slate.
805
+ configured_base=""
806
+
807
+ if [[ -n "$source_branch" && "$source_branch" != "HEAD" ]]; then
808
+ configured_base=$(cd "$REA_ROOT" && git config --get "branch.${source_branch}.base" 2>/dev/null || echo "")
809
+ if [[ -n "$configured_base" ]]; then
810
+ # Prefer the REMOTE-TRACKING form so the gate still anchors on a
811
+ # server-authoritative ref (see the local-ref hijack explanation
812
+ # above). Fall back to the local short ref only if the remote
813
+ # counterpart doesn't exist, with a visible WARN on stderr — the
814
+ # local ref is less trustworthy and the reviewer should know.
815
+ if cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/remotes/${argv_remote}/${configured_base}" >/dev/null 2>&1; then
816
+ default_ref="refs/remotes/${argv_remote}/${configured_base}"
817
+ elif cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/heads/${configured_base}" >/dev/null 2>&1; then
818
+ default_ref="refs/heads/${configured_base}"
819
+ printf 'WARN: branch.%s.base=%s resolved to local ref; remote counterpart %s/%s missing — reviewer-side diff may be stale\n' \
820
+ "$source_branch" "$configured_base" "$argv_remote" "$configured_base" >&2
821
+ fi
822
+ fi
823
+ fi
824
+
825
+ if [[ -z "$default_ref" ]]; then
826
+ default_ref=$(cd "$REA_ROOT" && git symbolic-ref "refs/remotes/${argv_remote}/HEAD" 2>/dev/null)
827
+ default_ref_status=$?
828
+ if [[ "$default_ref_status" -ne 0 || -z "$default_ref" ]]; then
829
+ # symbolic-ref failed (common on shallow or mirror clones where
830
+ # origin/HEAD was never set). Probe the common default-branch names in
831
+ # order: main, then master. Both are remote-tracking refs and still
832
+ # server-authoritative; the order matters only for projects that still
833
+ # default to `master` (older internal forks), where without this
834
+ # fallback the first push of a new branch would fail closed.
835
+ if cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/remotes/${argv_remote}/main" >/dev/null 2>&1; then
836
+ default_ref="refs/remotes/${argv_remote}/main"
837
+ elif cd "$REA_ROOT" && git rev-parse --verify --quiet "refs/remotes/${argv_remote}/master" >/dev/null 2>&1; then
838
+ default_ref="refs/remotes/${argv_remote}/master"
839
+ else
840
+ default_ref=""
841
+ fi
793
842
  fi
794
843
  fi
795
844
  if [[ -n "$default_ref" ]]; then
845
+ # Defect N: if operator-configured `branch.<source>.base` resolved the
846
+ # ref we're about to diff against, overwrite `resolved_base` with the
847
+ # short name so TARGET_BRANCH (and the Target: label) reflect the
848
+ # actual review anchor. Without an explicit config override, leave
849
+ # `resolved_base` at the refspec target — this preserves the cache
850
+ # contract for new-branch pushes where remote_ref is the same as the
851
+ # source branch (the common case) and for bare pushes that
852
+ # argv-resolve via `@{upstream}`. Only operators who opted into a
853
+ # per-branch base get the label promoted, keeping the change
854
+ # backward-compatible for every other path.
855
+ if [[ -n "$configured_base" ]]; then
856
+ resolved_base="${default_ref#refs/remotes/${argv_remote}/}"
857
+ resolved_base="${resolved_base#refs/heads/}"
858
+ [[ -z "$resolved_base" ]] && resolved_base="$default_ref"
859
+ fi
796
860
  mb=$(cd "$REA_ROOT" && git merge-base "$default_ref" "$local_sha" 2>/dev/null || echo "")
797
861
  if [[ -z "$mb" ]]; then
798
862
  # default_ref resolved but merge-base came back empty (unrelated
@@ -867,13 +931,40 @@ pr_core_run() {
867
931
  if [[ "$CODEX_WAIVER_ACTIVE" == "1" ]]; then
868
932
  _codex_ok=1
869
933
  elif [[ -f "$_audit" ]]; then
870
- if jq -e --arg sha "$local_sha" '
871
- select(
872
- .tool_name == "codex.review"
873
- and .metadata.head_sha == $sha
874
- and (.metadata.verdict == "pass" or .metadata.verdict == "concerns")
875
- )
876
- ' "$_audit" >/dev/null 2>&1; then
934
+ # Defect P (0.10.1): require .emission_source == "rea-cli" or
935
+ # "codex-cli" so agents cannot forge a codex.review record by
936
+ # directly calling appendAuditRecord() from an ad-hoc .mjs script
937
+ # (the generic helper stamps "other"). Legacy records (pre-0.10.1)
938
+ # have no emission_source field and are rejected the first push
939
+ # on an upgraded consumer requires a fresh `rea audit record
940
+ # codex-review` (or Codex CLI emission) which stamps "rea-cli".
941
+ #
942
+ # Defect T/U (0.10.2): read the audit file as raw lines and parse
943
+ # each with `fromjson?`. Before 0.10.2 this scan used
944
+ # `jq -e '<filter>' "$_audit"` which feeds the file as a single
945
+ # JSON stream — a single malformed line (literal backslash-u
946
+ # followed by non-hex characters inside a string, for example)
947
+ # makes jq bail on the stream with exit 2 and the `select` never
948
+ # runs against ANY record, including legitimate codex.review
949
+ # entries further down the file. The failure is total: every
950
+ # cached codex.review receipt becomes unreachable until the
951
+ # corrupt line is hand-edited out. `-R` flips jq into raw-input
952
+ # mode (one string per line), and `fromjson?` is the error-
953
+ # suppressing parser — malformed lines silently yield empty
954
+ # output. The `select` filter then inspects each successfully
955
+ # parsed record exactly as before, and `grep -q .` detects
956
+ # whether ANY record survived the filter. Lines 1107 and the
957
+ # earlier cache_result scans at :432/:612 operate on a single
958
+ # printf'd JSON string, not audit.jsonl, so they remain `jq -e`.
959
+ if jq -R --arg sha "$local_sha" '
960
+ fromjson?
961
+ | select(
962
+ .tool_name == "codex.review"
963
+ and .metadata.head_sha == $sha
964
+ and (.metadata.verdict == "pass" or .metadata.verdict == "concerns")
965
+ and (.emission_source == "rea-cli" or .emission_source == "codex-cli")
966
+ )
967
+ ' "$_audit" 2>/dev/null | grep -q .; then
877
968
  _codex_ok=1
878
969
  fi
879
970
  fi
@@ -918,7 +1009,12 @@ pr_core_run() {
918
1009
  if [[ -z "$SOURCE_SHA" ]] || [[ "$count" -gt "$BEST_COUNT" ]]; then
919
1010
  SOURCE_SHA="$local_sha"
920
1011
  MERGE_BASE="$mb"
921
- TARGET_BRANCH="$target"
1012
+ # Defect N: use `resolved_base` (the actual merge-base anchor we
1013
+ # diffed against), not `target` (the pushed-ref name). For tracked
1014
+ # branches these are the same; for new branches without an upstream
1015
+ # the distinction is the difference between "Target: <source-branch>"
1016
+ # (misleading) and "Target: main" (or whichever base was resolved).
1017
+ TARGET_BRANCH="$resolved_base"
922
1018
  SOURCE_REF="$local_ref"
923
1019
  BEST_COUNT="$count"
924
1020
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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)",