@bookedsolid/rea 0.25.0 → 0.26.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 +10 -7
- package/agents/codex-adversarial.md +4 -0
- package/agents/rea-orchestrator.md +9 -0
- package/commands/codex-review.md +4 -0
- package/dist/audit/append.d.ts +1 -0
- package/dist/audit/append.js +1 -0
- package/dist/audit/content-token.d.ts +98 -0
- package/dist/audit/content-token.js +136 -0
- package/dist/audit/local-review-event.d.ts +136 -0
- package/dist/audit/local-review-event.js +43 -0
- package/dist/cli/doctor.js +17 -0
- package/dist/cli/hook.d.ts +44 -0
- package/dist/cli/hook.js +77 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/init.js +197 -46
- package/dist/cli/install/pre-push.d.ts +15 -3
- package/dist/cli/install/pre-push.js +55 -5
- package/dist/cli/install/settings-merge.js +13 -0
- package/dist/cli/preflight.d.ts +120 -0
- package/dist/cli/preflight.js +487 -0
- package/dist/cli/review.d.ts +56 -0
- package/dist/cli/review.js +325 -0
- package/dist/policy/loader.d.ts +65 -0
- package/dist/policy/loader.js +33 -0
- package/dist/policy/types.d.ts +89 -0
- package/hooks/_lib/cmd-segments.sh +140 -2
- package/hooks/_lib/policy-read.sh +255 -0
- package/hooks/local-review-gate.sh +460 -0
- package/package.json +1 -1
- package/templates/CLAUDE.md.local-first.md +87 -0
- package/templates/pre-push.local-first.sh +65 -0
package/README.md
CHANGED
|
@@ -748,8 +748,8 @@ install so teams without a Codex bench get a first-class opt-out. Flip
|
|
|
748
748
|
|
|
749
749
|
## Hooks shipped
|
|
750
750
|
|
|
751
|
-
|
|
752
|
-
`rea init`. All
|
|
751
|
+
Fourteen hooks ship in `hooks/` and are copied into `.claude/hooks/` by
|
|
752
|
+
`rea init`. All fourteen are wired by default in the shipped
|
|
753
753
|
`.claude/settings.json`.
|
|
754
754
|
|
|
755
755
|
| Hook | Event | Purpose | Default |
|
|
@@ -760,11 +760,14 @@ Eleven hooks ship in `hooks/` and are copied into `.claude/hooks/` by
|
|
|
760
760
|
| `security-disclosure-gate.sh` | `PreToolUse: Bash` | Route security-keyword `gh issue create` to private disclosure | Registered |
|
|
761
761
|
| `pr-issue-link-gate.sh` | `PreToolUse: Bash` | Advisory warn when `gh pr create` has no linked issue | Registered |
|
|
762
762
|
| `attribution-advisory.sh` | `PreToolUse: Bash` | Block commits/PRs containing AI attribution markers | Registered |
|
|
763
|
-
| `
|
|
764
|
-
| `
|
|
765
|
-
| `
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
763
|
+
| `protected-paths-bash-gate.sh` | `PreToolUse: Bash` | Bash-tier parity with `settings-protection.sh` — refuses shell writes to `.claude/`/`.husky/`/policy paths (0.21.0+) | Registered |
|
|
764
|
+
| `blocked-paths-bash-gate.sh` | `PreToolUse: Bash` | Bash-tier parity with `blocked-paths-enforcer.sh` — refuses shell writes to `blocked_paths` policy entries (0.22.0+) | Registered |
|
|
765
|
+
| `local-review-gate.sh` | `PreToolUse: Bash` | Refuse `git push` (and optionally `git commit`) until a recent `rea.local_review` audit entry covers HEAD (0.26.0+) | Registered |
|
|
766
|
+
| `secret-scanner.sh` | `PreToolUse: Write\|Edit\|MultiEdit\|NotebookEdit` | Scan file writes for credential patterns | Registered |
|
|
767
|
+
| `settings-protection.sh` | `PreToolUse: Write\|Edit\|MultiEdit\|NotebookEdit` | Block agent writes to `.claude/settings.json`, hook dirs, policy | Registered |
|
|
768
|
+
| `blocked-paths-enforcer.sh` | `PreToolUse: Write\|Edit\|MultiEdit\|NotebookEdit` | Enforce `blocked_paths` from policy | Registered |
|
|
769
|
+
| `changeset-security-gate.sh` | `PreToolUse: Write\|Edit\|MultiEdit\|NotebookEdit` | Guard changesets against GHSA leaks and malformed frontmatter | Registered |
|
|
770
|
+
| `architecture-review-gate.sh` | `PostToolUse: Write\|Edit\|MultiEdit\|NotebookEdit` | Flag edits crossing architectural boundaries (advisory) | Registered |
|
|
768
771
|
|
|
769
772
|
The 0.10.x review-gate scripts (`push-review-gate.sh`,
|
|
770
773
|
`push-review-gate-git.sh`, `commit-review-gate.sh`) and the 1,250-line
|
|
@@ -5,8 +5,12 @@ description: Adversarial code review via the Codex plugin (GPT-5.4). Independent
|
|
|
5
5
|
|
|
6
6
|
# Codex Adversarial Reviewer
|
|
7
7
|
|
|
8
|
+
Run on the working tree before commit. Never let the push-gate be the first time codex sees the diff. Write the audit entry via `rea review` so the preflight gate accepts the push.
|
|
9
|
+
|
|
8
10
|
You wrap the Codex plugin (`/codex:adversarial-review`) inside REA's governance envelope. Your role is to provide an **independent** adversarial perspective on code that was planned and built by another model — typically Opus. Independence is the value: the authoring model is least likely to catch the mistakes it made.
|
|
9
11
|
|
|
12
|
+
As of 0.26.0 (CTO directive 2026-05-05) this review is a forceful step — the Bash-tier `local-review-gate.sh` hook + husky pre-push refuse `git push` when no recent `rea.local_review` audit entry covers HEAD. The cleanest gate-friendly invocation is `rea review`, which runs codex on the working tree and writes the canonical audit entry. The interactive `/codex-review` form is still useful for structured exploratory feedback, but it does NOT write the audit entry the gate looks for.
|
|
13
|
+
|
|
10
14
|
This is not a bolt-on. Adversarial review is a first-class, non-optional step in the REA engineering process. The default workflow is Plan → Build → Review, and you are the Review leg.
|
|
11
15
|
|
|
12
16
|
## When You Are Invoked
|
|
@@ -14,6 +14,15 @@ You are the REA orchestrator. Your role is to enforce the project's governance c
|
|
|
14
14
|
3. Verify the requested task falls within the current autonomy level
|
|
15
15
|
4. If the task exceeds autonomy, escalate to the user — do not attempt workarounds
|
|
16
16
|
|
|
17
|
+
## Before Dispatching Commit / Push
|
|
18
|
+
|
|
19
|
+
The local-first guardrail (CTO directive 2026-05-05) is forceful as of 0.26.0. Before delegating any commit-or-push step:
|
|
20
|
+
|
|
21
|
+
1. Ensure the implementing agent has run `rea review` against the working tree and addressed blocking findings.
|
|
22
|
+
2. The agent's `git push` will be refused at the Bash-tier `local-review-gate.sh` hook unless a recent `rea.local_review` audit entry covers HEAD. Plan for review BEFORE commit, not after.
|
|
23
|
+
3. If the consumer team has set `policy.review.local_review.mode: off`, the gate is a no-op — proceed normally. Do not assume review is unnecessary; some teams turn it off purely because they lack codex/claude installed.
|
|
24
|
+
4. The push-gate is a BACKUP layer, not the primary review surface. Routing the implementation agent to "let the push-gate catch it" is a process failure.
|
|
25
|
+
|
|
17
26
|
## Autonomy Levels
|
|
18
27
|
|
|
19
28
|
- **L0** — Read-only. Every write requires explicit user approval. Ask before any file change.
|
package/commands/codex-review.md
CHANGED
|
@@ -14,6 +14,10 @@ allowed-tools:
|
|
|
14
14
|
|
|
15
15
|
Invokes the Codex plugin (`/codex:adversarial-review`) on the current branch's diff, captures the result, and records it to the REA audit log. Adversarial review by an independent model (GPT-5.4) is a **first-class, non-optional step** in the REA engineering process — it is the counterweight to Opus-authored code.
|
|
16
16
|
|
|
17
|
+
## When to run
|
|
18
|
+
|
|
19
|
+
**Default: working tree before commit.** As of 0.26.0 (CTO directive 2026-05-05) the local-first guardrail is forceful — the Bash-tier `local-review-gate.sh` + husky pre-push refuse `git push` when no recent `rea.local_review` audit entry covers HEAD. Running `/codex-review` produces structured exploratory feedback but does NOT write the audit entry the gate looks for. For the gate-friendly form, use `rea review` from a Bash invocation — it runs codex on the working tree AND writes the canonical audit entry. `/codex-review` remains the interactive surface; `rea review` is the gate-aligned automation.
|
|
20
|
+
|
|
17
21
|
## Why this exists
|
|
18
22
|
|
|
19
23
|
The default workflow in REA is Plan → Build → Review, with the Review leg handed to a different model than the one that wrote the code. Codex adversarial review is free, fast, and independent — it catches the mistakes the authoring model is most likely to miss: security assumptions, correctness under edge cases, and logical gaps in tests. Treat it with the same weight as a human second set of eyes.
|
package/dist/audit/append.d.ts
CHANGED
|
@@ -82,3 +82,4 @@ export declare function appendAuditRecord(baseDir: string, input: AppendAuditInp
|
|
|
82
82
|
export type { AuditRecord, EmissionSource } from '../gateway/middleware/audit-types.js';
|
|
83
83
|
export { Tier, InvocationStatus } from '../policy/types.js';
|
|
84
84
|
export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, type CodexVerdict, type CodexReviewMetadata, } from './codex-event.js';
|
|
85
|
+
export { LOCAL_REVIEW_TOOL_NAME, LOCAL_REVIEW_SKIPPED_OVERRIDE_TOOL_NAME, LOCAL_REVIEW_SKIPPED_UNAVAILABLE_TOOL_NAME, LOCAL_REVIEW_PREFLIGHT_SKIPPED_TOOL_NAME, LOCAL_REVIEW_SERVER_NAME, type LocalReviewVerdict, type LocalReviewMetadata, type LocalReviewSkippedOverrideMetadata, type LocalReviewSkippedUnavailableMetadata, } from './local-review-event.js';
|
package/dist/audit/append.js
CHANGED
|
@@ -203,3 +203,4 @@ export async function appendAuditRecord(baseDir, input) {
|
|
|
203
203
|
}
|
|
204
204
|
export { Tier, InvocationStatus } from '../policy/types.js';
|
|
205
205
|
export { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from './codex-event.js';
|
|
206
|
+
export { LOCAL_REVIEW_TOOL_NAME, LOCAL_REVIEW_SKIPPED_OVERRIDE_TOOL_NAME, LOCAL_REVIEW_SKIPPED_UNAVAILABLE_TOOL_NAME, LOCAL_REVIEW_PREFLIGHT_SKIPPED_TOOL_NAME, LOCAL_REVIEW_SERVER_NAME, } from './local-review-event.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-token computation for the local-first review audit trail.
|
|
3
|
+
*
|
|
4
|
+
* 0.26.0 P1 fix (helix-026, codex finding 1): `rea preflight` originally
|
|
5
|
+
* keyed reviews to `git rev-parse HEAD`. The local-first flow is
|
|
6
|
+
* "working tree → review → fix → commit → push" — by design the COMMIT
|
|
7
|
+
* happens AFTER the review, so HEAD changes between them. Matching by
|
|
8
|
+
* `head_sha` guaranteed a stale lookup and broke the advertised loop.
|
|
9
|
+
*
|
|
10
|
+
* The fix is to record a token tied to the CONTENT codex actually
|
|
11
|
+
* reviewed instead of the commit it sat on top of.
|
|
12
|
+
*
|
|
13
|
+
* 0.26.0 round-25 P1-A fix: codex `exec review` diffs the WORKING TREE
|
|
14
|
+
* against base — meaning the token must reflect the working tree (with
|
|
15
|
+
* any uncommitted edits), NOT `HEAD^{tree}`. Pre-fix the token was
|
|
16
|
+
* computed via `git rev-parse HEAD^{tree}` — the LAST COMMIT's tree,
|
|
17
|
+
* not the working-tree state codex actually reviewed. The documented
|
|
18
|
+
* happy path is `edit → review → fix → commit → push`; after commit
|
|
19
|
+
* HEAD changes, preflight's token doesn't match the audit entry's
|
|
20
|
+
* token, and the gate REFUSES the very flow it documents.
|
|
21
|
+
*
|
|
22
|
+
* Fix: compute the WORKING-TREE token via `git stash create`. That
|
|
23
|
+
* command returns the SHA of a commit object whose tree is the
|
|
24
|
+
* working-tree + index merged. `git rev-parse <stash-sha>^{tree}` then
|
|
25
|
+
* gives a deterministic fingerprint of what codex actually reviewed:
|
|
26
|
+
*
|
|
27
|
+
* - At review time: working tree dirty → token = Tw (working-tree tree).
|
|
28
|
+
* - Operator commits: HEAD becomes B with tree Tb. Tb == Tw because
|
|
29
|
+
* `git commit` just persists the index, not new content.
|
|
30
|
+
* - Push time: working tree CLEAN, `git stash create` returns empty,
|
|
31
|
+
* fall back to `HEAD^{tree}` = Tb == Tw. Match. Allow.
|
|
32
|
+
*
|
|
33
|
+
* Stable behaviors preserved from the 0.26.0 finding-1 fix:
|
|
34
|
+
* - Stable across `git commit --amend` with no content change
|
|
35
|
+
* - Stable across rebases that don't touch content (fixup, reword)
|
|
36
|
+
* - Differs immediately on any real content edit
|
|
37
|
+
*
|
|
38
|
+
* NOTE: untracked AND `.gitignore`'d content is by design NOT part of
|
|
39
|
+
* the token, because:
|
|
40
|
+
* - `git stash create` does not include untracked files (without `-u`,
|
|
41
|
+
* and even `-u` excludes ignored files).
|
|
42
|
+
* - `git push` cannot transmit them either.
|
|
43
|
+
* So the token reflects what would actually be pushed. Codex review
|
|
44
|
+
* follows the same `git diff` semantics.
|
|
45
|
+
*
|
|
46
|
+
* The audit record continues to record `head_sha` for forensics —
|
|
47
|
+
* `content_token` is what `rea preflight` matches on. Legacy
|
|
48
|
+
* `codex.review` audit entries (pre-0.26.0) only have `head_sha`;
|
|
49
|
+
* preflight falls back to head-sha matching for those.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Git's well-known "empty tree" object SHA — the SHA-1 of an empty tree
|
|
53
|
+
* object, identical across every git repository on Earth.
|
|
54
|
+
*
|
|
55
|
+
* Round-27 F2 fix: this constant exists so the unborn-HEAD bootstrap path
|
|
56
|
+
* is symmetric between writer (`rea review` audit-record `head_sha`) and
|
|
57
|
+
* reader (`rea preflight` HEAD probe). Pre-fix:
|
|
58
|
+
*
|
|
59
|
+
* - `rea review` (writer) used `EMPTY_TREE_SHA` as the synthetic head
|
|
60
|
+
* when HEAD couldn't be resolved (round-25 P2-B).
|
|
61
|
+
* - `rea preflight` (reader) returned `''` for headSha AND empty
|
|
62
|
+
* contentToken on unborn HEAD, then refused with a both-empty guard.
|
|
63
|
+
*
|
|
64
|
+
* The asymmetry deadlocked the bootstrap flow `git init` →
|
|
65
|
+
* `rea review` → `rea preflight` under `refuse_at: both`. Both sides
|
|
66
|
+
* now reference THIS constant when HEAD cannot be resolved, so the
|
|
67
|
+
* head_sha-fallback path in `findRecentLocalReview` matches uniformly.
|
|
68
|
+
*/
|
|
69
|
+
export declare const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
70
|
+
/**
|
|
71
|
+
* Compute a deterministic content token for the working tree.
|
|
72
|
+
*
|
|
73
|
+
* Returns `''` (empty string) when the tree token cannot be resolved
|
|
74
|
+
* (no git repo, no HEAD, etc.). Callers MUST treat empty token as
|
|
75
|
+
* "no token" — the audit record's `content_token` field should be
|
|
76
|
+
* omitted in that case so `rea preflight` does not match a missing
|
|
77
|
+
* token against a missing token (which would be a trivial bypass).
|
|
78
|
+
*
|
|
79
|
+
* The token is the SHA of the working-tree tree object (or HEAD's
|
|
80
|
+
* tree object if the working tree is clean). Format: 40-char lowercase
|
|
81
|
+
* hex (or 64-char if the repo uses SHA-256). The function does not
|
|
82
|
+
* normalize or truncate — `rea preflight` does an exact-string match.
|
|
83
|
+
*
|
|
84
|
+
* Resolution order (round-25 P1-A):
|
|
85
|
+
* 1. `git stash create` — non-empty stdout means there are tracked
|
|
86
|
+
* changes. The stash SHA is a commit object; its tree is the
|
|
87
|
+
* working-tree + index merged. Use `<sha>^{tree}` as treeSource.
|
|
88
|
+
* `git stash create` does NOT modify the stash list, working tree,
|
|
89
|
+
* or index — it only synthesizes a commit object pointing at the
|
|
90
|
+
* current state.
|
|
91
|
+
* 2. Empty stdout from `git stash create` (or non-zero exit) → tree
|
|
92
|
+
* is clean OR repo is empty. Fall back to `HEAD^{tree}`. A clean
|
|
93
|
+
* working tree's tree object is identical to HEAD's tree by
|
|
94
|
+
* definition.
|
|
95
|
+
* 3. Both fail → return empty string. `head_sha` fallback in
|
|
96
|
+
* preflight takes over.
|
|
97
|
+
*/
|
|
98
|
+
export declare function computeTreeToken(cwd: string): string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-token computation for the local-first review audit trail.
|
|
3
|
+
*
|
|
4
|
+
* 0.26.0 P1 fix (helix-026, codex finding 1): `rea preflight` originally
|
|
5
|
+
* keyed reviews to `git rev-parse HEAD`. The local-first flow is
|
|
6
|
+
* "working tree → review → fix → commit → push" — by design the COMMIT
|
|
7
|
+
* happens AFTER the review, so HEAD changes between them. Matching by
|
|
8
|
+
* `head_sha` guaranteed a stale lookup and broke the advertised loop.
|
|
9
|
+
*
|
|
10
|
+
* The fix is to record a token tied to the CONTENT codex actually
|
|
11
|
+
* reviewed instead of the commit it sat on top of.
|
|
12
|
+
*
|
|
13
|
+
* 0.26.0 round-25 P1-A fix: codex `exec review` diffs the WORKING TREE
|
|
14
|
+
* against base — meaning the token must reflect the working tree (with
|
|
15
|
+
* any uncommitted edits), NOT `HEAD^{tree}`. Pre-fix the token was
|
|
16
|
+
* computed via `git rev-parse HEAD^{tree}` — the LAST COMMIT's tree,
|
|
17
|
+
* not the working-tree state codex actually reviewed. The documented
|
|
18
|
+
* happy path is `edit → review → fix → commit → push`; after commit
|
|
19
|
+
* HEAD changes, preflight's token doesn't match the audit entry's
|
|
20
|
+
* token, and the gate REFUSES the very flow it documents.
|
|
21
|
+
*
|
|
22
|
+
* Fix: compute the WORKING-TREE token via `git stash create`. That
|
|
23
|
+
* command returns the SHA of a commit object whose tree is the
|
|
24
|
+
* working-tree + index merged. `git rev-parse <stash-sha>^{tree}` then
|
|
25
|
+
* gives a deterministic fingerprint of what codex actually reviewed:
|
|
26
|
+
*
|
|
27
|
+
* - At review time: working tree dirty → token = Tw (working-tree tree).
|
|
28
|
+
* - Operator commits: HEAD becomes B with tree Tb. Tb == Tw because
|
|
29
|
+
* `git commit` just persists the index, not new content.
|
|
30
|
+
* - Push time: working tree CLEAN, `git stash create` returns empty,
|
|
31
|
+
* fall back to `HEAD^{tree}` = Tb == Tw. Match. Allow.
|
|
32
|
+
*
|
|
33
|
+
* Stable behaviors preserved from the 0.26.0 finding-1 fix:
|
|
34
|
+
* - Stable across `git commit --amend` with no content change
|
|
35
|
+
* - Stable across rebases that don't touch content (fixup, reword)
|
|
36
|
+
* - Differs immediately on any real content edit
|
|
37
|
+
*
|
|
38
|
+
* NOTE: untracked AND `.gitignore`'d content is by design NOT part of
|
|
39
|
+
* the token, because:
|
|
40
|
+
* - `git stash create` does not include untracked files (without `-u`,
|
|
41
|
+
* and even `-u` excludes ignored files).
|
|
42
|
+
* - `git push` cannot transmit them either.
|
|
43
|
+
* So the token reflects what would actually be pushed. Codex review
|
|
44
|
+
* follows the same `git diff` semantics.
|
|
45
|
+
*
|
|
46
|
+
* The audit record continues to record `head_sha` for forensics —
|
|
47
|
+
* `content_token` is what `rea preflight` matches on. Legacy
|
|
48
|
+
* `codex.review` audit entries (pre-0.26.0) only have `head_sha`;
|
|
49
|
+
* preflight falls back to head-sha matching for those.
|
|
50
|
+
*/
|
|
51
|
+
import { spawnSync } from 'node:child_process';
|
|
52
|
+
/**
|
|
53
|
+
* Git's well-known "empty tree" object SHA — the SHA-1 of an empty tree
|
|
54
|
+
* object, identical across every git repository on Earth.
|
|
55
|
+
*
|
|
56
|
+
* Round-27 F2 fix: this constant exists so the unborn-HEAD bootstrap path
|
|
57
|
+
* is symmetric between writer (`rea review` audit-record `head_sha`) and
|
|
58
|
+
* reader (`rea preflight` HEAD probe). Pre-fix:
|
|
59
|
+
*
|
|
60
|
+
* - `rea review` (writer) used `EMPTY_TREE_SHA` as the synthetic head
|
|
61
|
+
* when HEAD couldn't be resolved (round-25 P2-B).
|
|
62
|
+
* - `rea preflight` (reader) returned `''` for headSha AND empty
|
|
63
|
+
* contentToken on unborn HEAD, then refused with a both-empty guard.
|
|
64
|
+
*
|
|
65
|
+
* The asymmetry deadlocked the bootstrap flow `git init` →
|
|
66
|
+
* `rea review` → `rea preflight` under `refuse_at: both`. Both sides
|
|
67
|
+
* now reference THIS constant when HEAD cannot be resolved, so the
|
|
68
|
+
* head_sha-fallback path in `findRecentLocalReview` matches uniformly.
|
|
69
|
+
*/
|
|
70
|
+
export const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
71
|
+
/**
|
|
72
|
+
* Compute a deterministic content token for the working tree.
|
|
73
|
+
*
|
|
74
|
+
* Returns `''` (empty string) when the tree token cannot be resolved
|
|
75
|
+
* (no git repo, no HEAD, etc.). Callers MUST treat empty token as
|
|
76
|
+
* "no token" — the audit record's `content_token` field should be
|
|
77
|
+
* omitted in that case so `rea preflight` does not match a missing
|
|
78
|
+
* token against a missing token (which would be a trivial bypass).
|
|
79
|
+
*
|
|
80
|
+
* The token is the SHA of the working-tree tree object (or HEAD's
|
|
81
|
+
* tree object if the working tree is clean). Format: 40-char lowercase
|
|
82
|
+
* hex (or 64-char if the repo uses SHA-256). The function does not
|
|
83
|
+
* normalize or truncate — `rea preflight` does an exact-string match.
|
|
84
|
+
*
|
|
85
|
+
* Resolution order (round-25 P1-A):
|
|
86
|
+
* 1. `git stash create` — non-empty stdout means there are tracked
|
|
87
|
+
* changes. The stash SHA is a commit object; its tree is the
|
|
88
|
+
* working-tree + index merged. Use `<sha>^{tree}` as treeSource.
|
|
89
|
+
* `git stash create` does NOT modify the stash list, working tree,
|
|
90
|
+
* or index — it only synthesizes a commit object pointing at the
|
|
91
|
+
* current state.
|
|
92
|
+
* 2. Empty stdout from `git stash create` (or non-zero exit) → tree
|
|
93
|
+
* is clean OR repo is empty. Fall back to `HEAD^{tree}`. A clean
|
|
94
|
+
* working tree's tree object is identical to HEAD's tree by
|
|
95
|
+
* definition.
|
|
96
|
+
* 3. Both fail → return empty string. `head_sha` fallback in
|
|
97
|
+
* preflight takes over.
|
|
98
|
+
*/
|
|
99
|
+
export function computeTreeToken(cwd) {
|
|
100
|
+
// Step 1: try `git stash create`. Width-preserved by design — never
|
|
101
|
+
// touches the stash list, working tree, or index.
|
|
102
|
+
const stashResult = spawnSync('git', ['stash', 'create'], {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
});
|
|
106
|
+
let treeSource;
|
|
107
|
+
if (stashResult.status === 0 && (stashResult.stdout ?? '').toString().trim().length > 0) {
|
|
108
|
+
const stashSha = (stashResult.stdout ?? '').toString().trim();
|
|
109
|
+
// Defensive: only accept hex SHA shapes — otherwise treat as
|
|
110
|
+
// unresolvable and fall through to HEAD^{tree}.
|
|
111
|
+
if (!/^[0-9a-f]{40,64}$/.test(stashSha)) {
|
|
112
|
+
treeSource = 'HEAD^{tree}';
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
treeSource = `${stashSha}^{tree}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Step 2: clean working tree OR empty repo — clean tree's content
|
|
120
|
+
// is identical to HEAD^{tree} by definition; resolve that. Empty
|
|
121
|
+
// repo will fail in step 3 below and return empty string.
|
|
122
|
+
treeSource = 'HEAD^{tree}';
|
|
123
|
+
}
|
|
124
|
+
const treeResult = spawnSync('git', ['rev-parse', treeSource], {
|
|
125
|
+
cwd,
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
});
|
|
128
|
+
if (treeResult.status !== 0)
|
|
129
|
+
return '';
|
|
130
|
+
const out = (treeResult.stdout ?? '').toString().trim();
|
|
131
|
+
// Defensive: only accept hex strings — git's tree SHA is always hex.
|
|
132
|
+
// This rejects unexpected output (error messages, ANSI noise, etc.).
|
|
133
|
+
if (!/^[0-9a-f]{40,64}$/.test(out))
|
|
134
|
+
return '';
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `rea.local_review` audit event shape
|
|
3
|
+
* (0.26.0+).
|
|
4
|
+
*
|
|
5
|
+
* The local-first delegation enforcement (CTO directive 2026-05-05)
|
|
6
|
+
* replaces the soft "memory rule + orchestrator routing convention"
|
|
7
|
+
* with three forceful enforcement layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. Bash-tier PreToolUse hook (hooks/local-review-gate.sh) — refuses
|
|
10
|
+
* `git push`/`git commit` from the agent's Bash tool.
|
|
11
|
+
* 2. Husky pre-push (`exec rea preflight --strict`) — refuses git push
|
|
12
|
+
* at the terminal layer.
|
|
13
|
+
* 3. `rea preflight` CLI — the workhorse all enforcement layers call.
|
|
14
|
+
*
|
|
15
|
+
* `rea review` writes a `rea.local_review` audit entry. `rea preflight`
|
|
16
|
+
* reads the audit log, finds the most recent matching entry for HEAD,
|
|
17
|
+
* and exits 0/1/2 accordingly.
|
|
18
|
+
*
|
|
19
|
+
* # Provider seam
|
|
20
|
+
*
|
|
21
|
+
* The `provider` field is the LIGHT seam for future review providers
|
|
22
|
+
* (Claude-subagent, Pi, Gemma). Today the only writer is `rea review`
|
|
23
|
+
* with `provider: 'codex'`. Tomorrow a different provider writes the
|
|
24
|
+
* SAME shape with its own `provider:` value, and `rea preflight` reads
|
|
25
|
+
* any of them — no registry, no factory, no swap mechanism.
|
|
26
|
+
*
|
|
27
|
+
* # Skipped variants
|
|
28
|
+
*
|
|
29
|
+
* - `rea.local_review.skipped_override`: env-var bypass; reason logged
|
|
30
|
+
* - `rea.local_review.skipped_unavailable`: codex missing + `mode: off`;
|
|
31
|
+
* no review run, gate is a no-op
|
|
32
|
+
* - `rea.preflight.review_skipped`: `--no-review-check` CLI escape hatch
|
|
33
|
+
*
|
|
34
|
+
* Both `rea preflight` and any future audit-log reader treat these
|
|
35
|
+
* sibling tool names as INFORMATIONAL — they do NOT cover HEAD. Only
|
|
36
|
+
* the canonical `rea.local_review` entry (or the back-compat
|
|
37
|
+
* `codex.review` entry from pre-0.26.0 audit data) covers HEAD.
|
|
38
|
+
*/
|
|
39
|
+
export declare const LOCAL_REVIEW_TOOL_NAME: "rea.local_review";
|
|
40
|
+
export declare const LOCAL_REVIEW_SKIPPED_OVERRIDE_TOOL_NAME: "rea.local_review.skipped_override";
|
|
41
|
+
export declare const LOCAL_REVIEW_SKIPPED_UNAVAILABLE_TOOL_NAME: "rea.local_review.skipped_unavailable";
|
|
42
|
+
export declare const LOCAL_REVIEW_PREFLIGHT_SKIPPED_TOOL_NAME: "rea.preflight.review_skipped";
|
|
43
|
+
export declare const LOCAL_REVIEW_SERVER_NAME: "rea";
|
|
44
|
+
/**
|
|
45
|
+
* The verdict shape — same alphabet as the push-gate's CodexVerdict.
|
|
46
|
+
* `error` is reserved for future providers that surface a non-execution
|
|
47
|
+
* outcome (timeout, transport failure, etc.) so `rea preflight` can
|
|
48
|
+
* decide whether to honor a stale error-marker.
|
|
49
|
+
*/
|
|
50
|
+
export type LocalReviewVerdict = 'pass' | 'concerns' | 'blocking' | 'error';
|
|
51
|
+
/**
|
|
52
|
+
* Canonical metadata payload written under `metadata` on a
|
|
53
|
+
* `rea.local_review` audit record. `rea preflight` reads `head_sha`
|
|
54
|
+
* (must match `git rev-parse HEAD`) and `verdict` (must not be `error`
|
|
55
|
+
* for the entry to count as covering HEAD).
|
|
56
|
+
*
|
|
57
|
+
* `provider`, `provider_version`, `model`, `reasoning_effort` are
|
|
58
|
+
* non-semantic identification — the gate doesn't read them, but
|
|
59
|
+
* they're invaluable for forensic analysis ("which review producer
|
|
60
|
+
* did this concerns verdict come from?").
|
|
61
|
+
*/
|
|
62
|
+
export interface LocalReviewMetadata {
|
|
63
|
+
/**
|
|
64
|
+
* git rev-parse HEAD at review time. Recorded for forensics.
|
|
65
|
+
*
|
|
66
|
+
* 0.26.0 helix-026 finding-1: prior to this release `rea preflight`
|
|
67
|
+
* matched coverage by exact `head_sha`. That keyed coverage to a
|
|
68
|
+
* commit that didn't exist yet at review time (the local-first flow
|
|
69
|
+
* is "review → fix → commit → push" — HEAD changes between review
|
|
70
|
+
* and preflight). Coverage is now matched by `content_token` instead;
|
|
71
|
+
* `head_sha` remains in the payload purely for audit forensics.
|
|
72
|
+
*/
|
|
73
|
+
head_sha: string;
|
|
74
|
+
/**
|
|
75
|
+
* Tree SHA of HEAD at review time — the deterministic content
|
|
76
|
+
* fingerprint codex reviewed. `rea preflight` matches coverage on
|
|
77
|
+
* this field. Stable across content-equivalent commits (`--amend` with
|
|
78
|
+
* no edits, fixup rebases). Differs on any real content change.
|
|
79
|
+
*
|
|
80
|
+
* Optional for back-compat: legacy `codex.review` entries (pre-0.26.0)
|
|
81
|
+
* and any future provider that can't compute a tree fingerprint should
|
|
82
|
+
* omit this. `rea preflight` falls back to `head_sha` exact match when
|
|
83
|
+
* `content_token` is absent.
|
|
84
|
+
*/
|
|
85
|
+
content_token?: string;
|
|
86
|
+
/** Base ref (or SHA) the review diffed against. */
|
|
87
|
+
base_ref: string;
|
|
88
|
+
/** Verdict alphabet shared with the push-gate. */
|
|
89
|
+
verdict: LocalReviewVerdict;
|
|
90
|
+
/** Total findings extracted from the review prose. */
|
|
91
|
+
finding_count: number;
|
|
92
|
+
/**
|
|
93
|
+
* Logical name of the review provider. Today: `'codex'`. Future:
|
|
94
|
+
* `'claude-subagent'`, `'gemini'`, `'pi'`. Free-form lowercase
|
|
95
|
+
* identifier; the gate doesn't consume it.
|
|
96
|
+
*/
|
|
97
|
+
provider: string;
|
|
98
|
+
/** Version string of the provider binary, when available. */
|
|
99
|
+
provider_version?: string;
|
|
100
|
+
/** Model name passed to the provider, when applicable. */
|
|
101
|
+
model?: string;
|
|
102
|
+
/** Reasoning effort, when applicable to the model. */
|
|
103
|
+
reasoning_effort?: string;
|
|
104
|
+
/** Wall-time of the review subprocess in seconds. */
|
|
105
|
+
duration_seconds: number;
|
|
106
|
+
/**
|
|
107
|
+
* Identifier the reviewer attached to this run. Today this is the
|
|
108
|
+
* codex session id; future providers use whatever identifies a
|
|
109
|
+
* single review invocation.
|
|
110
|
+
*/
|
|
111
|
+
reviewer_session_id?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Metadata for the `rea.local_review.skipped_override` event. Carries
|
|
115
|
+
* the reason string from the bypass env-var so audit-log readers can
|
|
116
|
+
* see WHY the gate was bypassed.
|
|
117
|
+
*/
|
|
118
|
+
export interface LocalReviewSkippedOverrideMetadata {
|
|
119
|
+
head_sha: string;
|
|
120
|
+
/** Verbatim env-var value (the operator's reason). */
|
|
121
|
+
reason: string;
|
|
122
|
+
/** Which env-var was set (configurable via policy). */
|
|
123
|
+
bypass_env_var: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Metadata for the `rea.local_review.skipped_unavailable` event. This
|
|
127
|
+
* fires when codex is absent AND `policy.review.local_review.mode === 'off'`
|
|
128
|
+
* — `rea review` exits 0 silently with this entry recording the no-op.
|
|
129
|
+
*/
|
|
130
|
+
export interface LocalReviewSkippedUnavailableMetadata {
|
|
131
|
+
head_sha?: string;
|
|
132
|
+
/** Why the run was skipped: `'codex-not-installed'`, etc. */
|
|
133
|
+
reason: string;
|
|
134
|
+
/** Provider that was probed, e.g. `'codex'`. */
|
|
135
|
+
provider: string;
|
|
136
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `rea.local_review` audit event shape
|
|
3
|
+
* (0.26.0+).
|
|
4
|
+
*
|
|
5
|
+
* The local-first delegation enforcement (CTO directive 2026-05-05)
|
|
6
|
+
* replaces the soft "memory rule + orchestrator routing convention"
|
|
7
|
+
* with three forceful enforcement layers:
|
|
8
|
+
*
|
|
9
|
+
* 1. Bash-tier PreToolUse hook (hooks/local-review-gate.sh) — refuses
|
|
10
|
+
* `git push`/`git commit` from the agent's Bash tool.
|
|
11
|
+
* 2. Husky pre-push (`exec rea preflight --strict`) — refuses git push
|
|
12
|
+
* at the terminal layer.
|
|
13
|
+
* 3. `rea preflight` CLI — the workhorse all enforcement layers call.
|
|
14
|
+
*
|
|
15
|
+
* `rea review` writes a `rea.local_review` audit entry. `rea preflight`
|
|
16
|
+
* reads the audit log, finds the most recent matching entry for HEAD,
|
|
17
|
+
* and exits 0/1/2 accordingly.
|
|
18
|
+
*
|
|
19
|
+
* # Provider seam
|
|
20
|
+
*
|
|
21
|
+
* The `provider` field is the LIGHT seam for future review providers
|
|
22
|
+
* (Claude-subagent, Pi, Gemma). Today the only writer is `rea review`
|
|
23
|
+
* with `provider: 'codex'`. Tomorrow a different provider writes the
|
|
24
|
+
* SAME shape with its own `provider:` value, and `rea preflight` reads
|
|
25
|
+
* any of them — no registry, no factory, no swap mechanism.
|
|
26
|
+
*
|
|
27
|
+
* # Skipped variants
|
|
28
|
+
*
|
|
29
|
+
* - `rea.local_review.skipped_override`: env-var bypass; reason logged
|
|
30
|
+
* - `rea.local_review.skipped_unavailable`: codex missing + `mode: off`;
|
|
31
|
+
* no review run, gate is a no-op
|
|
32
|
+
* - `rea.preflight.review_skipped`: `--no-review-check` CLI escape hatch
|
|
33
|
+
*
|
|
34
|
+
* Both `rea preflight` and any future audit-log reader treat these
|
|
35
|
+
* sibling tool names as INFORMATIONAL — they do NOT cover HEAD. Only
|
|
36
|
+
* the canonical `rea.local_review` entry (or the back-compat
|
|
37
|
+
* `codex.review` entry from pre-0.26.0 audit data) covers HEAD.
|
|
38
|
+
*/
|
|
39
|
+
export const LOCAL_REVIEW_TOOL_NAME = 'rea.local_review';
|
|
40
|
+
export const LOCAL_REVIEW_SKIPPED_OVERRIDE_TOOL_NAME = 'rea.local_review.skipped_override';
|
|
41
|
+
export const LOCAL_REVIEW_SKIPPED_UNAVAILABLE_TOOL_NAME = 'rea.local_review.skipped_unavailable';
|
|
42
|
+
export const LOCAL_REVIEW_PREFLIGHT_SKIPPED_TOOL_NAME = 'rea.preflight.review_skipped';
|
|
43
|
+
export const LOCAL_REVIEW_SERVER_NAME = 'rea';
|
package/dist/cli/doctor.js
CHANGED
|
@@ -145,12 +145,29 @@ const EXPECTED_AGENTS = [
|
|
|
145
145
|
const EXPECTED_HOOKS = [
|
|
146
146
|
'architecture-review-gate.sh',
|
|
147
147
|
'attribution-advisory.sh',
|
|
148
|
+
// 0.22.0 — Bash-tier parity with `blocked-paths-enforcer.sh`.
|
|
149
|
+
// Round-27 F8 fix: was silently missing from EXPECTED_HOOKS, so
|
|
150
|
+
// doctor returned pass on consumer installs that lacked this
|
|
151
|
+
// security-load-bearing hook (any consumer who upgraded from
|
|
152
|
+
// 0.21.x → 0.22.x without `rea upgrade` was undetected).
|
|
153
|
+
'blocked-paths-bash-gate.sh',
|
|
148
154
|
'blocked-paths-enforcer.sh',
|
|
149
155
|
'changeset-security-gate.sh',
|
|
150
156
|
'dangerous-bash-interceptor.sh',
|
|
151
157
|
'dependency-audit-gate.sh',
|
|
152
158
|
'env-file-protection.sh',
|
|
159
|
+
// 0.26.0 local-first enforcement (CTO directive 2026-05-05).
|
|
160
|
+
// Round-25 P3 fix: doctor's EXPECTED_HOOKS list missed this entry.
|
|
161
|
+
// Without it, `rea doctor` returned pass on consumer installs that
|
|
162
|
+
// didn't actually have the new gate present after upgrade — silently
|
|
163
|
+
// disabling the local-first guardrail.
|
|
164
|
+
'local-review-gate.sh',
|
|
153
165
|
'pr-issue-link-gate.sh',
|
|
166
|
+
// 0.21.0 — Bash-tier parity with `settings-protection.sh`.
|
|
167
|
+
// Round-27 F8 fix: same class as blocked-paths-bash-gate.sh — silently
|
|
168
|
+
// missing since 0.21.0, doctor would pass even when the hook was
|
|
169
|
+
// absent from a consumer install.
|
|
170
|
+
'protected-paths-bash-gate.sh',
|
|
154
171
|
'secret-scanner.sh',
|
|
155
172
|
'security-disclosure-gate.sh',
|
|
156
173
|
'settings-protection.sh',
|
package/dist/cli/hook.d.ts
CHANGED
|
@@ -80,6 +80,50 @@ export interface HookScanBashOptions {
|
|
|
80
80
|
* writes the verdict JSON, exits with the appropriate code.
|
|
81
81
|
*/
|
|
82
82
|
export declare function runHookScanBash(options: HookScanBashOptions): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* `rea hook policy-get <dot.path>` — single source of truth for
|
|
85
|
+
* policy-value reads from the bash-tier hooks. Round-30 F2 structural
|
|
86
|
+
* fix.
|
|
87
|
+
*
|
|
88
|
+
* Pre-fix: `hooks/_lib/policy-read.sh::policy_nested_scalar` used a
|
|
89
|
+
* regex/awk parser that ONLY handled block-form mappings. The TS loader
|
|
90
|
+
* (`src/policy/loader.ts`) accepted inline-form mappings — `local_review:
|
|
91
|
+
* { mode: off }` — but the bash reader missed them. Silent split-brain:
|
|
92
|
+
* TS preflight saw `mode=off` (no-op), bash gate saw the field as unset
|
|
93
|
+
* and fell through to the enforced default → refused the push.
|
|
94
|
+
*
|
|
95
|
+
* Fix: have the bash gate shell out HERE for nested reads. The TS
|
|
96
|
+
* `yaml.parse()` call accepts both forms identically — single source of
|
|
97
|
+
* truth, drift impossible by construction.
|
|
98
|
+
*
|
|
99
|
+
* Contract:
|
|
100
|
+
* - `key` is dot-separated: `review.local_review.mode`. Only
|
|
101
|
+
* scalar leaves are supported (objects/arrays print empty).
|
|
102
|
+
* - Output is the raw scalar VALUE on stdout (no trailing newline,
|
|
103
|
+
* no quoting). Booleans render as `true`/`false`. Numbers render
|
|
104
|
+
* as their JS string form.
|
|
105
|
+
* - Unknown / missing path → empty stdout, exit 0. The bash caller
|
|
106
|
+
* treats empty as "default applies".
|
|
107
|
+
* - Unparseable YAML → empty stdout, exit 1. Bash callers swallow
|
|
108
|
+
* the exit and treat as default (matches pre-fix posture: any read
|
|
109
|
+
* error returns empty rather than refusing the gate).
|
|
110
|
+
*/
|
|
111
|
+
export interface HookPolicyGetOptions {
|
|
112
|
+
/** Dotted path; e.g. `review.local_review.mode`. */
|
|
113
|
+
key: string;
|
|
114
|
+
/**
|
|
115
|
+
* When true, emit the resolved subtree as JSON instead of a scalar.
|
|
116
|
+
* Object/array leaves print as their JSON form; scalars print as
|
|
117
|
+
* JSON-encoded scalars (`"off"`, `42`, `true`, `null`). Missing
|
|
118
|
+
* paths print `null`. Used by the bash hooks to read an entire
|
|
119
|
+
* sub-object in one node-spawn (e.g. all `review.local_review.*`
|
|
120
|
+
* fields at once) and parse client-side via jq.
|
|
121
|
+
*/
|
|
122
|
+
json?: boolean;
|
|
123
|
+
/** Override REA_ROOT. Production callers omit. */
|
|
124
|
+
reaRoot?: string;
|
|
125
|
+
}
|
|
126
|
+
export declare function runHookPolicyGet(options: HookPolicyGetOptions): Promise<void>;
|
|
83
127
|
/**
|
|
84
128
|
* Attach the `rea hook` subcommand tree to a commander Program. Two
|
|
85
129
|
* subcommands today: `push-gate` and `scan-bash`. New hooks should land
|