@bookedsolid/rea 0.24.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/data-architect.md +181 -0
- package/agents/devex-architect.md +172 -0
- package/agents/platform-architect.md +171 -0
- package/agents/rea-orchestrator.md +18 -3
- 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/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
|
package/dist/cli/hook.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import fs from 'node:fs';
|
|
32
32
|
import path from 'node:path';
|
|
33
|
+
import { parse as parseYaml } from 'yaml';
|
|
33
34
|
import { parsePrePushStdin, runPushGate } from '../hooks/push-gate/index.js';
|
|
34
35
|
import { runBlockedScan, runProtectedScan } from '../hooks/bash-scanner/index.js';
|
|
35
36
|
import { loadPolicy } from '../policy/loader.js';
|
|
@@ -250,6 +251,74 @@ export async function runHookScanBash(options) {
|
|
|
250
251
|
}
|
|
251
252
|
process.exit(0);
|
|
252
253
|
}
|
|
254
|
+
export async function runHookPolicyGet(options) {
|
|
255
|
+
// 0.27.0+: validate the key shape so a malformed dot-path can't be
|
|
256
|
+
// exploited by a misbehaving caller. Allow only POSIX identifier
|
|
257
|
+
// segments separated by single dots; reject empty segments, slashes,
|
|
258
|
+
// shell metacharacters, etc.
|
|
259
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(options.key)) {
|
|
260
|
+
process.stderr.write(`rea hook policy-get: invalid key ${JSON.stringify(options.key)}\n`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
const reaRoot = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
|
|
264
|
+
const policyPath = path.join(reaRoot, '.rea', 'policy.yaml');
|
|
265
|
+
const finishMissing = () => {
|
|
266
|
+
if (options.json === true)
|
|
267
|
+
process.stdout.write('null');
|
|
268
|
+
process.exit(0);
|
|
269
|
+
};
|
|
270
|
+
if (!fs.existsSync(policyPath)) {
|
|
271
|
+
finishMissing();
|
|
272
|
+
}
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
const raw = fs.readFileSync(policyPath, 'utf8');
|
|
276
|
+
parsed = parseYaml(raw);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Unparseable YAML — emit empty / null and exit 1 so the bash caller
|
|
280
|
+
// can distinguish "no value" from "actual parse failure" if it
|
|
281
|
+
// wants to (the local-review-gate caller swallows exit codes).
|
|
282
|
+
if (options.json === true)
|
|
283
|
+
process.stdout.write('null');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
287
|
+
finishMissing();
|
|
288
|
+
}
|
|
289
|
+
// Walk the dotted path. Bail (empty stdout / null) at any non-object
|
|
290
|
+
// intermediate.
|
|
291
|
+
const segments = options.key.split('.');
|
|
292
|
+
let cursor = parsed;
|
|
293
|
+
for (const seg of segments) {
|
|
294
|
+
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor)) {
|
|
295
|
+
finishMissing();
|
|
296
|
+
}
|
|
297
|
+
cursor = cursor[seg];
|
|
298
|
+
if (cursor === undefined) {
|
|
299
|
+
finishMissing();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (options.json === true) {
|
|
303
|
+
// Emit JSON for scalar/object/array/null. Objects + arrays serialize
|
|
304
|
+
// recursively. Bash callers parse via jq.
|
|
305
|
+
process.stdout.write(JSON.stringify(cursor ?? null));
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
// Scalar mode: only print scalar leaves. Objects/arrays print empty
|
|
309
|
+
// (legacy behavior from initial F2 implementation).
|
|
310
|
+
if (cursor === null) {
|
|
311
|
+
process.exit(0);
|
|
312
|
+
}
|
|
313
|
+
if (typeof cursor === 'string') {
|
|
314
|
+
process.stdout.write(cursor);
|
|
315
|
+
}
|
|
316
|
+
else if (typeof cursor === 'number' || typeof cursor === 'boolean') {
|
|
317
|
+
process.stdout.write(String(cursor));
|
|
318
|
+
}
|
|
319
|
+
// Object/Array → no output (caller treats as unset).
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
253
322
|
/**
|
|
254
323
|
* Attach the `rea hook` subcommand tree to a commander Program. Two
|
|
255
324
|
* subcommands today: `push-gate` and `scan-bash`. New hooks should land
|
|
@@ -297,4 +366,12 @@ export function registerHookCommand(program) {
|
|
|
297
366
|
...(opts.lastNCommits !== undefined ? { lastNCommits: opts.lastNCommits } : {}),
|
|
298
367
|
});
|
|
299
368
|
});
|
|
369
|
+
hook
|
|
370
|
+
.command('policy-get')
|
|
371
|
+
.description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
|
|
372
|
+
.argument('<key>', 'dotted path, e.g. `review.local_review.mode`. POSIX-identifier segments only.')
|
|
373
|
+
.option('--json', 'emit JSON instead of a scalar — supports object/array leaves. Bash callers can then parse with jq.')
|
|
374
|
+
.action(async (key, opts) => {
|
|
375
|
+
await runHookPolicyGet({ key, ...(opts.json === true ? { json: true } : {}) });
|
|
376
|
+
});
|
|
300
377
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { registerHookCommand } from './hook.js';
|
|
|
6
6
|
import { runDoctor } from './doctor.js';
|
|
7
7
|
import { runFreeze, runUnfreeze } from './freeze.js';
|
|
8
8
|
import { runInit } from './init.js';
|
|
9
|
+
import { registerPreflightCommand } from './preflight.js';
|
|
10
|
+
import { registerReviewCommand } from './review.js';
|
|
9
11
|
import { runServe } from './serve.js';
|
|
10
12
|
import { runStatus } from './status.js';
|
|
11
13
|
import { runTofuAccept, runTofuList } from './tofu.js';
|
|
@@ -106,6 +108,13 @@ async function main() {
|
|
|
106
108
|
// Register `rea hook push-gate` — the stateless pre-push Codex gate
|
|
107
109
|
// called by `.husky/pre-push` and `.git/hooks/pre-push`.
|
|
108
110
|
registerHookCommand(program);
|
|
111
|
+
// 0.26.0 local-first enforcement (CTO directive 2026-05-05). Two new
|
|
112
|
+
// top-level CLIs: `rea review` writes `rea.local_review` audit entries;
|
|
113
|
+
// `rea preflight` reads them and refuses pushes/commits without a
|
|
114
|
+
// recent matching entry. The husky pre-push template + Bash-tier
|
|
115
|
+
// `local-review-gate.sh` hook both delegate to `rea preflight --strict`.
|
|
116
|
+
registerReviewCommand(program);
|
|
117
|
+
registerPreflightCommand(program);
|
|
109
118
|
const tofu = program
|
|
110
119
|
.command('tofu')
|
|
111
120
|
.description('TOFU fingerprint operations (G7) — inspect and rebase `.rea/fingerprints.json` when a legitimate registry edit has triggered drift fail-close. Emits audit records.');
|