@bookedsolid/rea 0.38.1 → 0.39.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/dist/cli/doctor.d.ts +114 -0
- package/dist/cli/doctor.js +585 -2
- package/package.json +1 -1
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -97,6 +97,120 @@ export declare function checkPrepareCommitMsgHook(baseDir: string): CheckResult;
|
|
|
97
97
|
* (the doctor then short-circuits past every Codex check).
|
|
98
98
|
*/
|
|
99
99
|
export declare function checkCodexBinaryOnPath(): CheckResult;
|
|
100
|
+
/**
|
|
101
|
+
* Probe interface accepted by the policy-reader tier checks. Each
|
|
102
|
+
* field is optional; when omitted the check uses the real-environment
|
|
103
|
+
* default (PATH walk, spawnSync). Tests inject stubs to get
|
|
104
|
+
* deterministic, fast verdicts without touching the real filesystem or
|
|
105
|
+
* spawning subprocesses.
|
|
106
|
+
*
|
|
107
|
+
* - `cliDistExists` — does the rea CLI binary exist on disk at one of
|
|
108
|
+
* the two shim-resolved paths? Cheap (single `existsSync`). Used to
|
|
109
|
+
* give a clear "missing vs. broken" error message when Tier 1 is
|
|
110
|
+
* unreachable.
|
|
111
|
+
* - `cliInvokable` — does the resolved CLI actually respond to
|
|
112
|
+
* `rea hook policy-get version --json`? The expensive probe (one
|
|
113
|
+
* subprocess spawn). Mirrors EXACTLY what `_pr_load_full_json`
|
|
114
|
+
* does in `hooks/_lib/policy-reader.sh` so a stale or broken dist
|
|
115
|
+
* reports `warn` here — same outcome the real shim ladder would
|
|
116
|
+
* produce. Codex round-1 P2 (2026-05-16).
|
|
117
|
+
* - `python3OnPath` / `python3PyYamlReachable` — Tier 2 reachability.
|
|
118
|
+
* `python3PyYamlReachable` returns `true` when both python3 AND the
|
|
119
|
+
* `yaml` stdlib (PyYAML) can be imported (the Tier 2 loader needs
|
|
120
|
+
* both).
|
|
121
|
+
* - `awkOnPath` / `jqOnPath` — Tier 3 + JSON-accelerator reachability.
|
|
122
|
+
*/
|
|
123
|
+
export interface PolicyReaderProbes {
|
|
124
|
+
cliDistExists?: (baseDir: string) => boolean;
|
|
125
|
+
cliInvokable?: (baseDir: string) => boolean;
|
|
126
|
+
python3OnPath?: () => string | null;
|
|
127
|
+
python3PyYamlReachable?: () => boolean;
|
|
128
|
+
awkOnPath?: () => string | null;
|
|
129
|
+
jqOnPath?: () => string | null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Tier 1 — `rea hook policy-get`. Reachable when the rea CLI is
|
|
133
|
+
* present at one of the two shim-resolved paths (consumer install OR
|
|
134
|
+
* dogfood `dist/`) AND actually responds to `rea hook policy-get
|
|
135
|
+
* version --json`. The shim ladder uses that exact invocation as its
|
|
136
|
+
* Tier 1 probe (see `_pr_load_full_json` in `hooks/_lib/policy-reader.sh`);
|
|
137
|
+
* mirroring it here means a stale or broken dist (file present but
|
|
138
|
+
* import-throws / postinstall failed) reports `warn` — matching the
|
|
139
|
+
* real fall-through to Tier 2/3 the shim would do at runtime.
|
|
140
|
+
*
|
|
141
|
+
* Three states:
|
|
142
|
+
* - dist present + CLI responds → `pass` (canonical loader fully wired).
|
|
143
|
+
* - dist present + CLI broken → `warn` (stale build, missing native
|
|
144
|
+
* module, broken postinstall — needs `pnpm build` / `rea upgrade`).
|
|
145
|
+
* - dist absent → `warn` (not installed; Tier 2/3 still cover).
|
|
146
|
+
*
|
|
147
|
+
* Codex round-1 P2 (2026-05-16) replaced the file-existence-only
|
|
148
|
+
* probe with this CLI-invocation probe — pre-fix, a consumer with
|
|
149
|
+
* `dist/cli/index.js` present but throwing on load would see `pass`
|
|
150
|
+
* here while every real shim would silently fall through.
|
|
151
|
+
*/
|
|
152
|
+
export declare function checkPolicyReaderTier1(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
|
|
153
|
+
/**
|
|
154
|
+
* Tier 2 — python3 + stdlib `yaml` (PyYAML). Handles BOTH block-form
|
|
155
|
+
* and flow-form YAML; the practical floor when Tier 1 is unreachable.
|
|
156
|
+
*
|
|
157
|
+
* Three states:
|
|
158
|
+
* - python3 present + PyYAML importable → `pass`.
|
|
159
|
+
* - python3 present, PyYAML missing → `warn` (the loader will fall
|
|
160
|
+
* through to Tier 3, which only handles block-form).
|
|
161
|
+
* - python3 absent → `warn` (same Tier 3 fall-through).
|
|
162
|
+
*
|
|
163
|
+
* Never `fail` — Tier 3 is still a valid floor for block-form policy.
|
|
164
|
+
* The warning highlights the silent no-op risk for flow-form lookups
|
|
165
|
+
* when CLI is also unreachable.
|
|
166
|
+
*/
|
|
167
|
+
export declare function checkPolicyReaderTier2(probes?: PolicyReaderProbes): CheckResult;
|
|
168
|
+
/**
|
|
169
|
+
* Tier 3 — awk block-form parser. Last-resort no-dep fallback.
|
|
170
|
+
* Practically always present (POSIX requirement); hard-fail only when
|
|
171
|
+
* truly absent (in which case the consumer has ZERO working fallback
|
|
172
|
+
* tiers and any CLI-absent shim invocation will silently fail-closed
|
|
173
|
+
* on every policy lookup).
|
|
174
|
+
*/
|
|
175
|
+
export declare function checkPolicyReaderTier3(probes?: PolicyReaderProbes): CheckResult;
|
|
176
|
+
/**
|
|
177
|
+
* jq — optional accelerator used by Tier 1/2's JSON subtree parsing.
|
|
178
|
+
* Per the 0.37.0 round-1 P2 fix the helper falls back to a python3
|
|
179
|
+
* walker when jq is absent (still correct, just an extra spawn per
|
|
180
|
+
* leaf). `warn` when missing so operators know they're paying the
|
|
181
|
+
* latency cost.
|
|
182
|
+
*
|
|
183
|
+
* `info` when present — no action needed, just confirming the
|
|
184
|
+
* accelerator is wired.
|
|
185
|
+
*/
|
|
186
|
+
export declare function checkPolicyReaderJq(probes?: PolicyReaderProbes): CheckResult;
|
|
187
|
+
/**
|
|
188
|
+
* Summary roll-up: which tiers are reachable, what's the effective
|
|
189
|
+
* floor when the CLI is unreachable, and is flow-form policy at risk
|
|
190
|
+
* of silent no-op.
|
|
191
|
+
*
|
|
192
|
+
* Four verdicts:
|
|
193
|
+
* - `pass` — Tier 1 OR Tier 2 reachable AND a JSON list walker
|
|
194
|
+
* (jq or python3) is available. Flow-form scalars AND flow-form
|
|
195
|
+
* arrays both parse correctly via whichever tier is hit first.
|
|
196
|
+
* - `warn` (flow-form-lists-degraded) — Tier 1 reachable but neither
|
|
197
|
+
* jq nor python3 on PATH. Flow-form SCALARS parse correctly via
|
|
198
|
+
* the CLI's JSON output, but `policy_reader_get_list` cannot
|
|
199
|
+
* iterate the resulting JSON array — it falls through to Tier 3
|
|
200
|
+
* awk, which silently misses flow-form arrays like
|
|
201
|
+
* `blocked_paths: [.env, ...]`. Codex round-1 P2 (2026-05-16).
|
|
202
|
+
* - `warn` (Tier-3-only) — Only Tier 3 (awk) reachable. Block-form
|
|
203
|
+
* policy works; flow-form scalars AND arrays both silently no-op
|
|
204
|
+
* on every shim fallback.
|
|
205
|
+
* - `fail` — No tiers reachable. Shims fail closed on every policy
|
|
206
|
+
* lookup. (Practically requires losing awk too — see Tier 3.)
|
|
207
|
+
*
|
|
208
|
+
* Tier 2 implies python3 is on PATH (it's the interpreter that runs
|
|
209
|
+
* the loader), so when Tier 2 is reachable the list-iteration python3
|
|
210
|
+
* fallback is also reachable — only the Tier-1-without-list-walker
|
|
211
|
+
* shape can produce the degraded warning.
|
|
212
|
+
*/
|
|
213
|
+
export declare function checkPolicyReaderTierSummary(baseDir: string, probes?: PolicyReaderProbes): CheckResult;
|
|
100
214
|
/**
|
|
101
215
|
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
102
216
|
* responsiveness (pass/warn) and one informational line about the last
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import fsPromises from 'node:fs/promises';
|
|
@@ -950,6 +950,559 @@ export function checkCodexBinaryOnPath() {
|
|
|
950
950
|
'To disable the push-gate instead, set policy.review.codex_required: false in .rea/policy.yaml.',
|
|
951
951
|
};
|
|
952
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* 0.39.0 — `rea doctor` visibility into the 4-tier shim policy reader.
|
|
955
|
+
*
|
|
956
|
+
* `hooks/_lib/policy-reader.sh` (introduced 0.37.0) is the unified
|
|
957
|
+
* shim-side policy reader. Each shim sources it and reads policy
|
|
958
|
+
* values via a graceful-degradation ladder:
|
|
959
|
+
*
|
|
960
|
+
* Tier 1: `rea hook policy-get --json` — canonical TS loader.
|
|
961
|
+
* Tier 2: `python3` + stdlib `yaml` (PyYAML).
|
|
962
|
+
* Tier 3: `awk` block-form parser (last resort, block-form ONLY).
|
|
963
|
+
* Tier 4: fail-closed sentinel.
|
|
964
|
+
*
|
|
965
|
+
* The Tier 1/2 path handles BOTH block-form and flow-form YAML
|
|
966
|
+
* (`local_review: { mode: off }`). Tier 3 only handles block-form, so
|
|
967
|
+
* a consumer with flow-form policy AND no reachable CLI AND no python3
|
|
968
|
+
* silently no-ops on every shim fallback path — exactly the split-brain
|
|
969
|
+
* 0.37.0 set out to fix. The risk persists if the consumer's box lacks
|
|
970
|
+
* the upper tiers; operators currently have no way to see which tier
|
|
971
|
+
* their shims would actually use.
|
|
972
|
+
*
|
|
973
|
+
* These doctor checks surface the tier inventory so the gap is visible
|
|
974
|
+
* before it produces a silent regression. Each check is independent and
|
|
975
|
+
* uses optional probe-function injection so unit tests can simulate any
|
|
976
|
+
* combination of tier availability without manipulating PATH.
|
|
977
|
+
*
|
|
978
|
+
* Pure environment probes — no policy read, no shim spawn. Doctor calls
|
|
979
|
+
* each one in turn and the summary check aggregates the verdicts.
|
|
980
|
+
*/
|
|
981
|
+
/**
|
|
982
|
+
* Cheap PATH walker — returns the absolute path of `bin` when found
|
|
983
|
+
* with an executable bit set, or `null` otherwise. Mirrors
|
|
984
|
+
* `resolveCodexBinary`'s POSIX path but generalized for any binary.
|
|
985
|
+
*
|
|
986
|
+
* Windows path: walks PATHEXT and the bare name like `resolveCodexBinary`
|
|
987
|
+
* does for `codex`. Most consumer machines that run the shim ladder are
|
|
988
|
+
* POSIX (the shim is bash); Windows support is best-effort.
|
|
989
|
+
*/
|
|
990
|
+
function resolveBinaryOnPath(bin) {
|
|
991
|
+
const isWindows = process.platform === 'win32';
|
|
992
|
+
const pathEnv = process.env.PATH ?? process.env.Path ?? '';
|
|
993
|
+
if (pathEnv.length === 0)
|
|
994
|
+
return null;
|
|
995
|
+
const sep = isWindows ? ';' : ':';
|
|
996
|
+
const entries = pathEnv.split(sep).filter((p) => p.length > 0);
|
|
997
|
+
if (isWindows) {
|
|
998
|
+
const pathExt = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';');
|
|
999
|
+
for (const dir of entries) {
|
|
1000
|
+
for (const ext of pathExt) {
|
|
1001
|
+
const candidate = path.join(dir, `${bin}${ext}`);
|
|
1002
|
+
try {
|
|
1003
|
+
const st = fs.statSync(candidate);
|
|
1004
|
+
if (st.isFile())
|
|
1005
|
+
return candidate;
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
// not present — keep walking
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const bare = path.join(dir, bin);
|
|
1012
|
+
try {
|
|
1013
|
+
const st = fs.statSync(bare);
|
|
1014
|
+
if (st.isFile())
|
|
1015
|
+
return bare;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// not present — keep walking
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
for (const dir of entries) {
|
|
1024
|
+
const candidate = path.join(dir, bin);
|
|
1025
|
+
try {
|
|
1026
|
+
const st = fs.statSync(candidate);
|
|
1027
|
+
if (st.isFile() && (st.mode & 0o111) !== 0)
|
|
1028
|
+
return candidate;
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
// not present — keep walking
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
/** Resolve the shim's preferred CLI dist path, or null when no layout matches. */
|
|
1037
|
+
function resolveCliDistPath(baseDir) {
|
|
1038
|
+
// The shim's Tier 1 path requires the rea CLI binary to be
|
|
1039
|
+
// resolvable from the consumer's tree. Two layouts cover every
|
|
1040
|
+
// real-world install:
|
|
1041
|
+
// 1. <baseDir>/node_modules/@bookedsolid/rea/dist/cli/index.js
|
|
1042
|
+
// (consumer install — `pnpm i @bookedsolid/rea`)
|
|
1043
|
+
// 2. <baseDir>/dist/cli/index.js
|
|
1044
|
+
// (rea-repo dogfood after `pnpm build`)
|
|
1045
|
+
// Either presence is enough for the shim's sandboxed CLI resolution
|
|
1046
|
+
// (see hooks/_lib/shim-runtime.sh).
|
|
1047
|
+
const consumerCli = path.join(baseDir, 'node_modules', '@bookedsolid', 'rea', 'dist', 'cli', 'index.js');
|
|
1048
|
+
if (fs.existsSync(consumerCli))
|
|
1049
|
+
return consumerCli;
|
|
1050
|
+
const dogfoodCli = path.join(baseDir, 'dist', 'cli', 'index.js');
|
|
1051
|
+
if (fs.existsSync(dogfoodCli))
|
|
1052
|
+
return dogfoodCli;
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
function defaultCliDistExists(baseDir) {
|
|
1056
|
+
return resolveCliDistPath(baseDir) !== null;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Sandbox check — mirrors `shim_sandbox_check` in
|
|
1060
|
+
* `hooks/_lib/shim-runtime.sh` (introduced 0.38.0).
|
|
1061
|
+
*
|
|
1062
|
+
* Codex round-2 P1 (2026-05-16): the pre-fix `defaultCliInvokable`
|
|
1063
|
+
* spawned the resolved CLI WITHOUT this validation. An attacker who
|
|
1064
|
+
* could plant a `dist/cli/index.js` outside `realpath(baseDir)` (via
|
|
1065
|
+
* a symlink) — OR plant one inside the tree but WITHOUT an ancestor
|
|
1066
|
+
* `package.json` whose `name === "@bookedsolid/rea"` — would have
|
|
1067
|
+
* their forged code executed every time doctor probed Tier 1
|
|
1068
|
+
* reachability. The real shim chain refuses these layouts; the
|
|
1069
|
+
* doctor probe MUST refuse them identically so it cannot be tricked
|
|
1070
|
+
* into reporting `pass` on a layout the production shims would
|
|
1071
|
+
* never trust.
|
|
1072
|
+
*
|
|
1073
|
+
* Returns `true` when:
|
|
1074
|
+
* 1. `realpath(cli)` resolves AND lives INSIDE `realpath(baseDir)`
|
|
1075
|
+
* (no symlink-out of the project)
|
|
1076
|
+
* 2. an ancestor `package.json` (walking up from
|
|
1077
|
+
* `dirname(dirname(dirname(real)))` — i.e. the package root for
|
|
1078
|
+
* a `dist/cli/index.js` shape) has `name === "@bookedsolid/rea"`
|
|
1079
|
+
* (max 20 hops)
|
|
1080
|
+
*
|
|
1081
|
+
* Returns `false` on any failure (realpath miss, escapes-project,
|
|
1082
|
+
* missing/wrong package.json). Doctor's Tier 1 check then treats a
|
|
1083
|
+
* sandbox-failed CLI identically to a CLI-missing layout — both
|
|
1084
|
+
* report `warn` ("Tier 1 unreachable") rather than `pass`.
|
|
1085
|
+
*
|
|
1086
|
+
* This mirrors the bash logic EXACTLY:
|
|
1087
|
+
* - `fs.realpathSync` on both paths (no symlink slippage)
|
|
1088
|
+
* - path-prefix containment via `realProj + sep` (so a sibling
|
|
1089
|
+
* directory whose name STARTS with realProj cannot match)
|
|
1090
|
+
* - ancestor walk capped at 20 hops with a filesystem-root break
|
|
1091
|
+
* (`cur === path.dirname(cur)`)
|
|
1092
|
+
* - JSON parse failures in any candidate `package.json` are
|
|
1093
|
+
* swallowed and the walk continues (mirrors the bash `try/catch`)
|
|
1094
|
+
*
|
|
1095
|
+
* Kept in sync with the bash helper: any future change to the
|
|
1096
|
+
* sandbox-check shape (e.g. CLI-shape enforcement) MUST be applied
|
|
1097
|
+
* in both places.
|
|
1098
|
+
*/
|
|
1099
|
+
function sandboxCheckCli(cli, baseDir) {
|
|
1100
|
+
let real;
|
|
1101
|
+
let realProj;
|
|
1102
|
+
try {
|
|
1103
|
+
real = fs.realpathSync(cli);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
realProj = fs.realpathSync(baseDir);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
const sep = path.sep;
|
|
1115
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
1116
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
// Walk ancestor directories from the package root (3 levels up
|
|
1120
|
+
// from a `<root>/dist/cli/index.js` shape) looking for a
|
|
1121
|
+
// package.json whose `name === "@bookedsolid/rea"`. Max 20 hops
|
|
1122
|
+
// with a filesystem-root break so we never loop forever on
|
|
1123
|
+
// exotic mount layouts.
|
|
1124
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
1125
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
1126
|
+
const pj = path.join(cur, 'package.json');
|
|
1127
|
+
if (fs.existsSync(pj)) {
|
|
1128
|
+
try {
|
|
1129
|
+
const data = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
1130
|
+
if (data && data.name === '@bookedsolid/rea') {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
// keep walking — malformed package.json on the path is not fatal
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
cur = path.dirname(cur);
|
|
1139
|
+
}
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Codex round-1 P2 (2026-05-16): the file-presence probe alone allows
|
|
1144
|
+
* a stale or broken dist (e.g. an upgrade-lagged consumer who never
|
|
1145
|
+
* re-ran `pnpm build`) to falsely report `pass` while the real shim
|
|
1146
|
+
* ladder in `hooks/_lib/policy-reader.sh` would skip Tier 1 because
|
|
1147
|
+
* `rea hook policy-get version --json` exits non-zero. We mirror that
|
|
1148
|
+
* exact probe verbatim — same key (`version`), same `--json` flag,
|
|
1149
|
+
* same accept-criterion (exit 0 + non-empty stdout).
|
|
1150
|
+
*
|
|
1151
|
+
* Codex round-2 P1 (2026-05-16): BEFORE invoking the resolved CLI,
|
|
1152
|
+
* apply the same realpath + ancestor-package.json sandbox check the
|
|
1153
|
+
* shims apply in `hooks/_lib/shim-runtime.sh::shim_sandbox_check`.
|
|
1154
|
+
* Pre-fix, an attacker who could plant a `dist/cli/index.js` via a
|
|
1155
|
+
* symlink-out (or without a `@bookedsolid/rea` package.json ancestor)
|
|
1156
|
+
* would have their forged code executed every probe call — yet the
|
|
1157
|
+
* real shim ladder would refuse the same layout. This probe MUST
|
|
1158
|
+
* refuse identically so it cannot mis-report `pass` on an
|
|
1159
|
+
* unsandboxed CLI.
|
|
1160
|
+
*
|
|
1161
|
+
* Returns `true` when the CLI responds correctly; `false` when the
|
|
1162
|
+
* dist is missing OR present-but-broken OR present-but-unsandboxed.
|
|
1163
|
+
* Doctor's Tier 1 check then surfaces the difference: missing →
|
|
1164
|
+
* install guidance; broken/unsandboxed → rebuild guidance. (The
|
|
1165
|
+
* unsandboxed branch deliberately collapses into the "broken" bucket
|
|
1166
|
+
* because either way Tier 1 is unreachable for the shim chain.)
|
|
1167
|
+
*
|
|
1168
|
+
* 8s timeout: the CLI's `hook policy-get` path is local-only (zod
|
|
1169
|
+
* load + YAML parse + JSON walk); on any reasonable machine it
|
|
1170
|
+
* resolves in under a second. The timeout is a defense against a CLI
|
|
1171
|
+
* that hangs on import (a broken postinstall, a missing native module)
|
|
1172
|
+
* rather than a normal-operation budget.
|
|
1173
|
+
*/
|
|
1174
|
+
function defaultCliInvokable(baseDir) {
|
|
1175
|
+
const cli = resolveCliDistPath(baseDir);
|
|
1176
|
+
if (cli === null)
|
|
1177
|
+
return false;
|
|
1178
|
+
// Codex round-2 P1: sandbox check BEFORE spawn. Pre-fix the probe
|
|
1179
|
+
// spawned arbitrary code that happened to live at the expected
|
|
1180
|
+
// shim-resolved path; if a symlink-out OR a missing rea
|
|
1181
|
+
// package.json ancestor existed, we executed an attacker payload.
|
|
1182
|
+
if (!sandboxCheckCli(cli, baseDir))
|
|
1183
|
+
return false;
|
|
1184
|
+
try {
|
|
1185
|
+
const res = spawnSync('node', [cli, 'hook', 'policy-get', 'version', '--json'], {
|
|
1186
|
+
cwd: baseDir,
|
|
1187
|
+
timeout: 8_000,
|
|
1188
|
+
// Tier 1 reads policy.yaml at REA_ROOT — propagate so the probe
|
|
1189
|
+
// honors the same scope the real shim chain would (a missing
|
|
1190
|
+
// `CLAUDE_PROJECT_DIR` falls back to cwd, which doctor has
|
|
1191
|
+
// already set).
|
|
1192
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: baseDir },
|
|
1193
|
+
encoding: 'utf8',
|
|
1194
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1195
|
+
});
|
|
1196
|
+
if (res.status !== 0)
|
|
1197
|
+
return false;
|
|
1198
|
+
const out = (res.stdout ?? '').trim();
|
|
1199
|
+
return out.length > 0;
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function defaultPython3PyYamlReachable() {
|
|
1206
|
+
// The Tier 2 loader runs `python3 -c "import yaml"`. We mirror that
|
|
1207
|
+
// probe verbatim so a `yaml`-installable-but-broken interpreter is
|
|
1208
|
+
// not falsely reported as "reachable". Apply the SAME env scrub
|
|
1209
|
+
// (PYTHONPATH / PYTHONHOME / PYTHONSTARTUP unset, PYTHONSAFEPATH=1)
|
|
1210
|
+
// that policy-reader.sh applies, so a repo-local `yaml.py` cannot
|
|
1211
|
+
// shadow the stdlib copy here either — otherwise this probe would
|
|
1212
|
+
// report `true` against a malicious repo where the actual loader
|
|
1213
|
+
// would (correctly) refuse to import.
|
|
1214
|
+
//
|
|
1215
|
+
// Codex round-3 P1 (2026-05-16): `PYTHONSAFEPATH=1` is the env-var
|
|
1216
|
+
// form of `python3 -P` and is only honored on Python 3.11+. On
|
|
1217
|
+
// Python 3.4-3.10 (still installed by default on macOS Big Sur /
|
|
1218
|
+
// Monterey / Ventura, RHEL 8, Ubuntu 20.04, …) it is SILENTLY
|
|
1219
|
+
// IGNORED — meaning the interpreter will still prepend `""`/`"."`/
|
|
1220
|
+
// CWD to `sys.path[0]` and import a repo-local `./yaml.py` instead
|
|
1221
|
+
// of the stdlib copy. The production loader in
|
|
1222
|
+
// hooks/_lib/policy-reader.sh closes this gap with a defensive
|
|
1223
|
+
// sys.path scrub at the top of every `python3 -c` body (see the
|
|
1224
|
+
// "Codex round 2 P1" comment block in policy-reader.sh:256-267).
|
|
1225
|
+
// We MUST mirror that scrub here — without it, a malicious repo
|
|
1226
|
+
// could plant `./yaml.py`, get this probe to report `true`, while
|
|
1227
|
+
// the real Tier 2 loader (which DOES scrub) refuses to import and
|
|
1228
|
+
// falls through to Tier 3. The doctor verdict would then point
|
|
1229
|
+
// operators at the wrong tier when diagnosing a stuck shim.
|
|
1230
|
+
try {
|
|
1231
|
+
const probeEnv = { ...process.env, PYTHONSAFEPATH: '1' };
|
|
1232
|
+
delete probeEnv['PYTHONPATH'];
|
|
1233
|
+
delete probeEnv['PYTHONHOME'];
|
|
1234
|
+
delete probeEnv['PYTHONSTARTUP'];
|
|
1235
|
+
// Same scrub shape as policy-reader.sh's Tier 2 body — strip
|
|
1236
|
+
// empty/CWD entries from sys.path BEFORE the `import yaml` so
|
|
1237
|
+
// the probe and the production loader produce the same answer
|
|
1238
|
+
// on Python 3.4-3.10.
|
|
1239
|
+
const probeBody = [
|
|
1240
|
+
'import sys',
|
|
1241
|
+
'import os',
|
|
1242
|
+
'_cwd = os.getcwd()',
|
|
1243
|
+
'_cwd_real = os.path.realpath(_cwd)',
|
|
1244
|
+
'sys.path[:] = [p for p in sys.path if p not in ("", ".", _cwd, _cwd_real)]',
|
|
1245
|
+
'import yaml',
|
|
1246
|
+
].join('\n');
|
|
1247
|
+
const res = spawnSync('python3', ['-c', probeBody], {
|
|
1248
|
+
env: probeEnv,
|
|
1249
|
+
timeout: 5_000,
|
|
1250
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1251
|
+
});
|
|
1252
|
+
return res.status === 0;
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const DEFAULT_PROBES = {
|
|
1259
|
+
cliDistExists: defaultCliDistExists,
|
|
1260
|
+
cliInvokable: defaultCliInvokable,
|
|
1261
|
+
python3OnPath: () => resolveBinaryOnPath('python3'),
|
|
1262
|
+
python3PyYamlReachable: defaultPython3PyYamlReachable,
|
|
1263
|
+
awkOnPath: () => resolveBinaryOnPath('awk'),
|
|
1264
|
+
jqOnPath: () => resolveBinaryOnPath('jq'),
|
|
1265
|
+
};
|
|
1266
|
+
function resolveProbes(probes) {
|
|
1267
|
+
if (probes === undefined)
|
|
1268
|
+
return DEFAULT_PROBES;
|
|
1269
|
+
return { ...DEFAULT_PROBES, ...probes };
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Tier 1 — `rea hook policy-get`. Reachable when the rea CLI is
|
|
1273
|
+
* present at one of the two shim-resolved paths (consumer install OR
|
|
1274
|
+
* dogfood `dist/`) AND actually responds to `rea hook policy-get
|
|
1275
|
+
* version --json`. The shim ladder uses that exact invocation as its
|
|
1276
|
+
* Tier 1 probe (see `_pr_load_full_json` in `hooks/_lib/policy-reader.sh`);
|
|
1277
|
+
* mirroring it here means a stale or broken dist (file present but
|
|
1278
|
+
* import-throws / postinstall failed) reports `warn` — matching the
|
|
1279
|
+
* real fall-through to Tier 2/3 the shim would do at runtime.
|
|
1280
|
+
*
|
|
1281
|
+
* Three states:
|
|
1282
|
+
* - dist present + CLI responds → `pass` (canonical loader fully wired).
|
|
1283
|
+
* - dist present + CLI broken → `warn` (stale build, missing native
|
|
1284
|
+
* module, broken postinstall — needs `pnpm build` / `rea upgrade`).
|
|
1285
|
+
* - dist absent → `warn` (not installed; Tier 2/3 still cover).
|
|
1286
|
+
*
|
|
1287
|
+
* Codex round-1 P2 (2026-05-16) replaced the file-existence-only
|
|
1288
|
+
* probe with this CLI-invocation probe — pre-fix, a consumer with
|
|
1289
|
+
* `dist/cli/index.js` present but throwing on load would see `pass`
|
|
1290
|
+
* here while every real shim would silently fall through.
|
|
1291
|
+
*/
|
|
1292
|
+
export function checkPolicyReaderTier1(baseDir, probes) {
|
|
1293
|
+
const label = 'policy-reader Tier 1 (rea CLI)';
|
|
1294
|
+
const p = resolveProbes(probes);
|
|
1295
|
+
const distPresent = p.cliDistExists(baseDir);
|
|
1296
|
+
if (!distPresent) {
|
|
1297
|
+
return {
|
|
1298
|
+
label,
|
|
1299
|
+
status: 'warn',
|
|
1300
|
+
detail: 'rea CLI dist not found at node_modules/@bookedsolid/rea/dist/cli/index.js or <baseDir>/dist/cli/index.js — ' +
|
|
1301
|
+
'shims fall through to Tier 2/3 (works, but loses validated schema + full subtree shapes). ' +
|
|
1302
|
+
'Consumer: run `pnpm i @bookedsolid/rea`. Dogfood: run `pnpm build`.',
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (!p.cliInvokable(baseDir)) {
|
|
1306
|
+
return {
|
|
1307
|
+
label,
|
|
1308
|
+
status: 'warn',
|
|
1309
|
+
detail: 'rea CLI dist exists but `rea hook policy-get version --json` failed — the dist is ' +
|
|
1310
|
+
'stale or broken (incomplete build, missing native module, broken postinstall). The ' +
|
|
1311
|
+
'shim ladder will skip Tier 1 and fall through to Tier 2/3 just as this probe did. ' +
|
|
1312
|
+
'Run `pnpm build` (dogfood) or `rea upgrade` (consumer) to rebuild.',
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
label,
|
|
1317
|
+
status: 'pass',
|
|
1318
|
+
detail: 'rea CLI dist responds to `hook policy-get version --json` — canonical loader fully wired',
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Tier 2 — python3 + stdlib `yaml` (PyYAML). Handles BOTH block-form
|
|
1323
|
+
* and flow-form YAML; the practical floor when Tier 1 is unreachable.
|
|
1324
|
+
*
|
|
1325
|
+
* Three states:
|
|
1326
|
+
* - python3 present + PyYAML importable → `pass`.
|
|
1327
|
+
* - python3 present, PyYAML missing → `warn` (the loader will fall
|
|
1328
|
+
* through to Tier 3, which only handles block-form).
|
|
1329
|
+
* - python3 absent → `warn` (same Tier 3 fall-through).
|
|
1330
|
+
*
|
|
1331
|
+
* Never `fail` — Tier 3 is still a valid floor for block-form policy.
|
|
1332
|
+
* The warning highlights the silent no-op risk for flow-form lookups
|
|
1333
|
+
* when CLI is also unreachable.
|
|
1334
|
+
*/
|
|
1335
|
+
export function checkPolicyReaderTier2(probes) {
|
|
1336
|
+
const label = 'policy-reader Tier 2 (python3 + PyYAML)';
|
|
1337
|
+
const p = resolveProbes(probes);
|
|
1338
|
+
const py = p.python3OnPath();
|
|
1339
|
+
if (py === null) {
|
|
1340
|
+
return {
|
|
1341
|
+
label,
|
|
1342
|
+
status: 'warn',
|
|
1343
|
+
detail: 'python3 not on PATH — Tier 2 unavailable. Shims fall through to Tier 3 (awk, ' +
|
|
1344
|
+
'block-form only). Flow-form policy (e.g. `local_review: { mode: off }`) silently ' +
|
|
1345
|
+
'no-ops when the rea CLI is also unreachable. Install python3 to close this gap.',
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
if (!p.python3PyYamlReachable()) {
|
|
1349
|
+
return {
|
|
1350
|
+
label,
|
|
1351
|
+
status: 'warn',
|
|
1352
|
+
detail: `python3 found at ${py} but \`import yaml\` failed — PyYAML missing. ` +
|
|
1353
|
+
'Shims fall through to Tier 3 (awk, block-form only). Flow-form policy silently ' +
|
|
1354
|
+
'no-ops when the rea CLI is also unreachable. Install: `pip3 install pyyaml`.',
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
label,
|
|
1359
|
+
status: 'pass',
|
|
1360
|
+
detail: `python3 + PyYAML reachable at ${py} — flow-form policy parses correctly`,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Tier 3 — awk block-form parser. Last-resort no-dep fallback.
|
|
1365
|
+
* Practically always present (POSIX requirement); hard-fail only when
|
|
1366
|
+
* truly absent (in which case the consumer has ZERO working fallback
|
|
1367
|
+
* tiers and any CLI-absent shim invocation will silently fail-closed
|
|
1368
|
+
* on every policy lookup).
|
|
1369
|
+
*/
|
|
1370
|
+
export function checkPolicyReaderTier3(probes) {
|
|
1371
|
+
const label = 'policy-reader Tier 3 (awk)';
|
|
1372
|
+
const p = resolveProbes(probes);
|
|
1373
|
+
const awk = p.awkOnPath();
|
|
1374
|
+
if (awk !== null) {
|
|
1375
|
+
return {
|
|
1376
|
+
label,
|
|
1377
|
+
status: 'pass',
|
|
1378
|
+
detail: `awk at ${awk} — block-form fallback available`,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
label,
|
|
1383
|
+
status: 'fail',
|
|
1384
|
+
detail: 'awk not on PATH — no fallback tier reachable. If the rea CLI and python3+PyYAML are ' +
|
|
1385
|
+
'ALSO unreachable, every shim policy lookup fails closed. This is unusual; awk is a ' +
|
|
1386
|
+
'POSIX requirement. Install awk (`mawk`, `gawk`, or `nawk`).',
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* jq — optional accelerator used by Tier 1/2's JSON subtree parsing.
|
|
1391
|
+
* Per the 0.37.0 round-1 P2 fix the helper falls back to a python3
|
|
1392
|
+
* walker when jq is absent (still correct, just an extra spawn per
|
|
1393
|
+
* leaf). `warn` when missing so operators know they're paying the
|
|
1394
|
+
* latency cost.
|
|
1395
|
+
*
|
|
1396
|
+
* `info` when present — no action needed, just confirming the
|
|
1397
|
+
* accelerator is wired.
|
|
1398
|
+
*/
|
|
1399
|
+
export function checkPolicyReaderJq(probes) {
|
|
1400
|
+
const label = 'policy-reader jq (JSON accelerator)';
|
|
1401
|
+
const p = resolveProbes(probes);
|
|
1402
|
+
const jq = p.jqOnPath();
|
|
1403
|
+
if (jq !== null) {
|
|
1404
|
+
return {
|
|
1405
|
+
label,
|
|
1406
|
+
status: 'pass',
|
|
1407
|
+
detail: `jq at ${jq} — used by Tier 1/2 JSON subtree walking`,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
label,
|
|
1412
|
+
status: 'warn',
|
|
1413
|
+
detail: 'jq not on PATH — Tier 1/2 fall back to a python3 JSON walker per leaf (correct, ' +
|
|
1414
|
+
'just slower). Install jq to reduce per-leaf spawn overhead.',
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Summary roll-up: which tiers are reachable, what's the effective
|
|
1419
|
+
* floor when the CLI is unreachable, and is flow-form policy at risk
|
|
1420
|
+
* of silent no-op.
|
|
1421
|
+
*
|
|
1422
|
+
* Four verdicts:
|
|
1423
|
+
* - `pass` — Tier 1 OR Tier 2 reachable AND a JSON list walker
|
|
1424
|
+
* (jq or python3) is available. Flow-form scalars AND flow-form
|
|
1425
|
+
* arrays both parse correctly via whichever tier is hit first.
|
|
1426
|
+
* - `warn` (flow-form-lists-degraded) — Tier 1 reachable but neither
|
|
1427
|
+
* jq nor python3 on PATH. Flow-form SCALARS parse correctly via
|
|
1428
|
+
* the CLI's JSON output, but `policy_reader_get_list` cannot
|
|
1429
|
+
* iterate the resulting JSON array — it falls through to Tier 3
|
|
1430
|
+
* awk, which silently misses flow-form arrays like
|
|
1431
|
+
* `blocked_paths: [.env, ...]`. Codex round-1 P2 (2026-05-16).
|
|
1432
|
+
* - `warn` (Tier-3-only) — Only Tier 3 (awk) reachable. Block-form
|
|
1433
|
+
* policy works; flow-form scalars AND arrays both silently no-op
|
|
1434
|
+
* on every shim fallback.
|
|
1435
|
+
* - `fail` — No tiers reachable. Shims fail closed on every policy
|
|
1436
|
+
* lookup. (Practically requires losing awk too — see Tier 3.)
|
|
1437
|
+
*
|
|
1438
|
+
* Tier 2 implies python3 is on PATH (it's the interpreter that runs
|
|
1439
|
+
* the loader), so when Tier 2 is reachable the list-iteration python3
|
|
1440
|
+
* fallback is also reachable — only the Tier-1-without-list-walker
|
|
1441
|
+
* shape can produce the degraded warning.
|
|
1442
|
+
*/
|
|
1443
|
+
export function checkPolicyReaderTierSummary(baseDir, probes) {
|
|
1444
|
+
const label = 'policy-reader effective floor';
|
|
1445
|
+
const p = resolveProbes(probes);
|
|
1446
|
+
// Mirror Tier 1's two-stage check — dist present + CLI invokable.
|
|
1447
|
+
// A stale/broken dist that fails the invokable probe is treated as
|
|
1448
|
+
// "Tier 1 not reachable" so the summary matches what the shim
|
|
1449
|
+
// ladder would actually do at runtime.
|
|
1450
|
+
const tier1 = p.cliDistExists(baseDir) && p.cliInvokable(baseDir);
|
|
1451
|
+
const py = p.python3OnPath();
|
|
1452
|
+
const tier2 = py !== null && p.python3PyYamlReachable();
|
|
1453
|
+
const tier3 = p.awkOnPath() !== null;
|
|
1454
|
+
const jq = p.jqOnPath();
|
|
1455
|
+
// List iteration after Tier 1/2 needs jq OR python3 to walk the
|
|
1456
|
+
// JSON. Tier 2 implies python3 on PATH (the interpreter that ran
|
|
1457
|
+
// the loader); so the only "lists broken" shape is Tier 1 reachable
|
|
1458
|
+
// but neither jq nor python3 on PATH.
|
|
1459
|
+
const listWalker = jq !== null || py !== null;
|
|
1460
|
+
const reachable = [];
|
|
1461
|
+
if (tier1)
|
|
1462
|
+
reachable.push('Tier 1 (CLI)');
|
|
1463
|
+
if (tier2)
|
|
1464
|
+
reachable.push('Tier 2 (python3+PyYAML)');
|
|
1465
|
+
if (tier3)
|
|
1466
|
+
reachable.push('Tier 3 (awk)');
|
|
1467
|
+
if (tier1 || tier2) {
|
|
1468
|
+
if (!listWalker) {
|
|
1469
|
+
// Tier 1 + no python3/jq. flow-form scalars work; flow-form
|
|
1470
|
+
// arrays silently no-op via Tier 3 fallthrough. (Tier 2 path
|
|
1471
|
+
// is unreachable here because Tier 2 requires python3.)
|
|
1472
|
+
return {
|
|
1473
|
+
label,
|
|
1474
|
+
status: 'warn',
|
|
1475
|
+
detail: `${reachable.join(', ')} reachable — flow-form scalars parse via Tier 1 CLI, ` +
|
|
1476
|
+
'BUT neither jq nor python3 is on PATH so `policy_reader_get_list` cannot iterate ' +
|
|
1477
|
+
'the resulting JSON arrays. Flow-form list policy (e.g. `blocked_paths: [.env, ...]`) ' +
|
|
1478
|
+
'silently falls through to Tier 3 awk and misses inline arrays. Install jq ' +
|
|
1479
|
+
'(`brew install jq` / `apt-get install jq`) or python3 to close the gap.',
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
label,
|
|
1484
|
+
status: 'pass',
|
|
1485
|
+
detail: `${reachable.join(', ')} reachable — flow-form policy parses correctly`,
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
if (tier3) {
|
|
1489
|
+
return {
|
|
1490
|
+
label,
|
|
1491
|
+
status: 'warn',
|
|
1492
|
+
detail: 'only Tier 3 (awk, block-form ONLY) reachable — flow-form policy ' +
|
|
1493
|
+
'(e.g. `local_review: { mode: off }`, `blocked_paths: [.env, ...]`) silently ' +
|
|
1494
|
+
'no-ops on every shim fallback path. Restore Tier 1 (rea CLI dist) or Tier 2 ' +
|
|
1495
|
+
'(python3 + PyYAML) to close the gap.',
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
label,
|
|
1500
|
+
status: 'fail',
|
|
1501
|
+
detail: 'no policy-reader tier reachable — every shim policy lookup fails closed. ' +
|
|
1502
|
+
'Install at least one of: rea CLI dist (Tier 1), python3 + PyYAML (Tier 2), ' +
|
|
1503
|
+
'awk (Tier 3).',
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
953
1506
|
/**
|
|
954
1507
|
* Translate a `CodexProbeState` into two doctor CheckResults: one for
|
|
955
1508
|
* responsiveness (pass/warn) and one informational line about the last
|
|
@@ -1457,9 +2010,15 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
|
|
|
1457
2010
|
const policyPath = reaPath(baseDir, POLICY_FILE);
|
|
1458
2011
|
const registryPath = reaPath(baseDir, REGISTRY_FILE);
|
|
1459
2012
|
const reaDirPath = path.join(baseDir, REA_DIR);
|
|
2013
|
+
// Run checkPolicyParses up-front so we can both push its result and
|
|
2014
|
+
// use the verdict to gate the 0.39.0 policy-reader tier checks below.
|
|
2015
|
+
// A malformed policy file should NOT trigger the tier-reachability
|
|
2016
|
+
// probes — those reports would misattribute a parse failure to a
|
|
2017
|
+
// runtime/install problem (codex round-3 P2, 2026-05-16).
|
|
2018
|
+
const policyParsesResult = checkPolicyParses(baseDir, policyPath);
|
|
1460
2019
|
const checks = [
|
|
1461
2020
|
checkFileExists('.rea/ directory exists', reaDirPath, true),
|
|
1462
|
-
|
|
2021
|
+
policyParsesResult,
|
|
1463
2022
|
checkRegistryParses(baseDir, registryPath),
|
|
1464
2023
|
checkAgentsPresent(baseDir),
|
|
1465
2024
|
checkHooksInstalled(baseDir),
|
|
@@ -1482,6 +2041,30 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
|
|
|
1482
2041
|
// went through in 0.29.0 → 0.30.0, after 4 release cycles of
|
|
1483
2042
|
// propagation).
|
|
1484
2043
|
checkDelegationAdvisoryHookRegistered(baseDir),
|
|
2044
|
+
// 0.39.0 — policy-reader tier visibility. Surfaces which tiers of
|
|
2045
|
+
// the 4-tier `hooks/_lib/policy-reader.sh` ladder are reachable in
|
|
2046
|
+
// this environment so operators can SEE whether flow-form policy
|
|
2047
|
+
// would silently no-op when the CLI is unreachable.
|
|
2048
|
+
//
|
|
2049
|
+
// Codex round-3 P2 (2026-05-16): gated on `policyParsesResult`
|
|
2050
|
+
// being a `pass` — NOT just `existsSync(policyPath)`. A
|
|
2051
|
+
// malformed policy file (present but unparseable) should report
|
|
2052
|
+
// exactly ONE failure — the parse-error from `checkPolicyParses`
|
|
2053
|
+
// above — and not also light up the tier probes with misleading
|
|
2054
|
+
// "Tier 1 dist exists but failed" or summary "ladder degraded"
|
|
2055
|
+
// diagnostics that misattribute a config bug to an
|
|
2056
|
+
// install/runtime problem. The parse-failure row already tells
|
|
2057
|
+
// the operator the right thing to fix; adding more downstream
|
|
2058
|
+
// noise would obscure it.
|
|
2059
|
+
...(policyParsesResult.status === 'pass'
|
|
2060
|
+
? [
|
|
2061
|
+
checkPolicyReaderTier1(baseDir),
|
|
2062
|
+
checkPolicyReaderTier2(),
|
|
2063
|
+
checkPolicyReaderTier3(),
|
|
2064
|
+
checkPolicyReaderJq(),
|
|
2065
|
+
checkPolicyReaderTierSummary(baseDir),
|
|
2066
|
+
]
|
|
2067
|
+
: []),
|
|
1485
2068
|
];
|
|
1486
2069
|
// Non-git escape hatch: when `.git/` is absent, both git-hook checks are
|
|
1487
2070
|
// meaningless (commit-msg + pre-push can't be invoked without git). Emit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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)",
|