@bookedsolid/rea 0.13.2 → 0.14.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/MIGRATING.md +352 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +14 -0
- package/dist/hooks/push-gate/codex-runner.js +37 -1
- package/dist/hooks/push-gate/index.js +8 -0
- package/dist/hooks/push-gate/policy.d.ts +34 -0
- package/dist/hooks/push-gate/policy.js +25 -0
- package/dist/policy/loader.d.ts +38 -0
- package/dist/policy/loader.js +30 -0
- package/dist/policy/types.d.ts +28 -0
- package/hooks/blocked-paths-enforcer.sh +38 -0
- package/hooks/secret-scanner.sh +30 -0
- package/package.json +2 -1
package/MIGRATING.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Migrating to `@bookedsolid/rea` from a project with prior tooling
|
|
2
|
+
|
|
3
|
+
`rea` was originally written for greenfield projects. Real consumers
|
|
4
|
+
arrive with prior infrastructure already in place — commitlint,
|
|
5
|
+
lint-staged, gitleaks, act-CI, branch-policy linters, project-specific
|
|
6
|
+
gates wired into `.husky/`. This guide names the conflict patterns by
|
|
7
|
+
name and shows the supported migration path for each.
|
|
8
|
+
|
|
9
|
+
If you hit something this doc doesn't cover, file an issue at
|
|
10
|
+
https://github.com/bookedsolidtech/rea/issues with the offending hook
|
|
11
|
+
body and the prior tool name.
|
|
12
|
+
|
|
13
|
+
## Prerequisite — husky must be installed and `core.hooksPath` configured
|
|
14
|
+
|
|
15
|
+
The `.husky/{commit-msg,pre-push}.d/` extension surface is sourced from
|
|
16
|
+
the rea-managed bodies under `.husky/<hookname>`. Those bodies only fire
|
|
17
|
+
when git is configured to use husky. Confirm one of the following:
|
|
18
|
+
|
|
19
|
+
- Husky 9 (recommended): `pnpm dlx husky init` (or `npx husky init`)
|
|
20
|
+
during onboarding. Husky 9 sets `core.hooksPath=.husky/_` automatically;
|
|
21
|
+
rea's bodies live at `.husky/<hookname>` and husky's auto-generated
|
|
22
|
+
stubs at `.husky/_/<hookname>` source them at hook-fire time. `rea
|
|
23
|
+
doctor` (0.13.1+) follows the husky 9 stub indirection correctly.
|
|
24
|
+
- Husky 4-8 (legacy): `core.hooksPath=.husky` set, husky's
|
|
25
|
+
`_/husky.sh` runner installed. Functional but unsupported by husky
|
|
26
|
+
upstream — migrate to husky 9.
|
|
27
|
+
- Vanilla git (no husky): rea installs the fallback at
|
|
28
|
+
`.git/hooks/pre-push`. **The fragment recipes below DO NOT run** in
|
|
29
|
+
this configuration — `.git/hooks/pre-push` is not the same body as
|
|
30
|
+
`.husky/pre-push`. Either install husky (recommended) or chain your
|
|
31
|
+
per-tool commands directly into the fallback (you'll lose the
|
|
32
|
+
upgrade-safe property — `rea upgrade` will refresh the fallback and
|
|
33
|
+
drop your chain).
|
|
34
|
+
|
|
35
|
+
`pnpm rea doctor` reports the active hook path. If it shows
|
|
36
|
+
`.git/hooks/pre-push` (rea-managed at .../.git/hooks/pre-push), you
|
|
37
|
+
are on the vanilla-git path — install husky first.
|
|
38
|
+
|
|
39
|
+
## TL;DR
|
|
40
|
+
|
|
41
|
+
1. Confirm husky is installed (see prereq above).
|
|
42
|
+
2. Run `rea init` (fresh install) or `rea upgrade` (existing).
|
|
43
|
+
3. **Do not lose your existing chain.** rea now refuses to silently
|
|
44
|
+
overwrite an executable `.husky/pre-push` or `.husky/commit-msg`
|
|
45
|
+
that is not rea-managed; you'll see a `[fail]` from `rea doctor`
|
|
46
|
+
pointing here.
|
|
47
|
+
4. Move each chained command from your existing hook body to a
|
|
48
|
+
per-tool fragment under `.husky/pre-push.d/<NN>-<name>` or
|
|
49
|
+
`.husky/commit-msg.d/<NN>-<name>` (executable, lex-ordered).
|
|
50
|
+
5. Re-run `rea init`. The fresh hook body delegates to
|
|
51
|
+
`rea hook push-gate` and then runs your fragments AFTER the
|
|
52
|
+
governance gate.
|
|
53
|
+
6. `rea doctor` should now report all checks green.
|
|
54
|
+
|
|
55
|
+
## What rea ships and what it doesn't
|
|
56
|
+
|
|
57
|
+
`rea init` / `rea upgrade` install:
|
|
58
|
+
|
|
59
|
+
- `.husky/pre-push` — package-managed; **do not edit**. Refreshed on every
|
|
60
|
+
`rea upgrade`.
|
|
61
|
+
- `.husky/commit-msg` — package-managed; **do not edit**. Same.
|
|
62
|
+
- `.git/hooks/pre-push` (fallback when `core.hooksPath` is unset).
|
|
63
|
+
- `.claude/hooks/*.sh` — protection + audit + advisory hooks.
|
|
64
|
+
- `.claude/agents/*.md`, `.claude/commands/*.md`.
|
|
65
|
+
- `.rea/policy.yaml`, `.rea/registry.yaml`.
|
|
66
|
+
|
|
67
|
+
`rea` does **not** install:
|
|
68
|
+
|
|
69
|
+
- `.husky/pre-commit` — completely yours. Out of scope for the rea
|
|
70
|
+
push-gate. If you have one, keep it.
|
|
71
|
+
- `.husky/post-commit`, `post-merge`, `post-checkout`, etc. — yours.
|
|
72
|
+
- Any tool's binary (`commitlint`, `gitleaks`, `husky`, etc.) — yours.
|
|
73
|
+
|
|
74
|
+
The only files rea touches are explicitly enumerated above. Everything
|
|
75
|
+
else is the consumer's surface.
|
|
76
|
+
|
|
77
|
+
## Extension surface (added in 0.13.0)
|
|
78
|
+
|
|
79
|
+
`.husky/pre-push.d/*` and `.husky/commit-msg.d/*` are the
|
|
80
|
+
**upgrade-safe** place to layer your own gates. Files in those
|
|
81
|
+
directories must be executable; rea sources them in lex order AFTER
|
|
82
|
+
its own governance work succeeds. A non-zero exit from any fragment
|
|
83
|
+
fails the hook (matches husky's normal chaining).
|
|
84
|
+
|
|
85
|
+
- Fragment receives positional args from git (`<remote-name> <remote-url>`
|
|
86
|
+
for pre-push, `<commit-msg-file>` for commit-msg).
|
|
87
|
+
- Missing directory is a no-op (no fragments = no chained checks).
|
|
88
|
+
- Non-executable files are silently skipped (drop a `README` if you
|
|
89
|
+
want context next to the fragments — it won't run).
|
|
90
|
+
- Fragments run with the current shell's `set -eu`; an unset variable
|
|
91
|
+
or a non-zero exit anywhere in the fragment short-circuits.
|
|
92
|
+
|
|
93
|
+
`rea doctor` reports detected fragments at `[info]` level so you can
|
|
94
|
+
confirm the chain.
|
|
95
|
+
|
|
96
|
+
## Conflict pattern: commitlint
|
|
97
|
+
|
|
98
|
+
You probably have something like this in `.husky/commit-msg`:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
#!/usr/bin/env sh
|
|
102
|
+
. "$(dirname -- "$0")/_/husky.sh" # husky 4-8
|
|
103
|
+
npx --no-install commitlint --edit "$1"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or, with husky 9, your own command interleaved with `husky 9`'s body.
|
|
107
|
+
|
|
108
|
+
**rea 0.11.0+ overwrites `.husky/commit-msg` on `rea upgrade --force`.**
|
|
109
|
+
Your commitlint invocation will be lost.
|
|
110
|
+
|
|
111
|
+
### Migration
|
|
112
|
+
|
|
113
|
+
Move commitlint to a fragment:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
mkdir -p .husky/commit-msg.d
|
|
117
|
+
cat > .husky/commit-msg.d/01-commitlint <<'EOF'
|
|
118
|
+
#!/bin/sh
|
|
119
|
+
exec npx --no-install commitlint --edit "$1"
|
|
120
|
+
EOF
|
|
121
|
+
chmod +x .husky/commit-msg.d/01-commitlint
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Re-run `rea upgrade`. The package-managed `.husky/commit-msg` body now
|
|
125
|
+
runs first (HALT check, AI-attribution block when policy enables it),
|
|
126
|
+
then runs your fragment.
|
|
127
|
+
|
|
128
|
+
## Conflict pattern: lint-staged on pre-push
|
|
129
|
+
|
|
130
|
+
You probably have:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
#!/usr/bin/env sh
|
|
134
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
135
|
+
npx --no-install lint-staged
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Migration
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
mkdir -p .husky/pre-push.d
|
|
142
|
+
cat > .husky/pre-push.d/02-lint-staged <<'EOF'
|
|
143
|
+
#!/bin/sh
|
|
144
|
+
exec npx --no-install lint-staged
|
|
145
|
+
EOF
|
|
146
|
+
chmod +x .husky/pre-push.d/02-lint-staged
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Conflict pattern: gitleaks (pre-commit)
|
|
150
|
+
|
|
151
|
+
`rea` does NOT install a pre-commit hook. Your existing
|
|
152
|
+
`.husky/pre-commit` keeps working unchanged. Just confirm:
|
|
153
|
+
|
|
154
|
+
- Shebang is `#!/usr/bin/env bash` (not `#!/bin/sh`) if the body uses
|
|
155
|
+
`set -o pipefail`. On Linux where `/bin/sh = dash`, `pipefail`
|
|
156
|
+
aborts immediately.
|
|
157
|
+
- gitleaks invocation includes `--redact` so detected secrets don't
|
|
158
|
+
hit terminal scrollback.
|
|
159
|
+
- gitleaks binary is vendored or installed via postinstall (e.g.
|
|
160
|
+
`gitleaks-secret-scanner` npm wrapper) so fresh clones work without
|
|
161
|
+
manual install.
|
|
162
|
+
|
|
163
|
+
If you want gitleaks to run on push instead of commit, add a fragment:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cat > .husky/pre-push.d/03-gitleaks <<'EOF'
|
|
167
|
+
#!/bin/sh
|
|
168
|
+
exec gitleaks detect --redact --no-banner
|
|
169
|
+
EOF
|
|
170
|
+
chmod +x .husky/pre-push.d/03-gitleaks
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Conflict pattern: act-CI matrix
|
|
174
|
+
|
|
175
|
+
If you have a project-specific CI gate like `./scripts/act-ci.sh`
|
|
176
|
+
chained into `.husky/pre-push` (e.g. BST), it gets clobbered by
|
|
177
|
+
`rea upgrade --force`.
|
|
178
|
+
|
|
179
|
+
### Migration
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
cat > .husky/pre-push.d/00-act-ci <<'EOF'
|
|
183
|
+
#!/bin/sh
|
|
184
|
+
exec ./scripts/act-ci.sh
|
|
185
|
+
EOF
|
|
186
|
+
chmod +x .husky/pre-push.d/00-act-ci
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The `00-` prefix puts act-CI first in lex order so it runs before any
|
|
190
|
+
later fragments. Adjust ordering as needed.
|
|
191
|
+
|
|
192
|
+
## Conflict pattern: branch-policy linter
|
|
193
|
+
|
|
194
|
+
A common pattern that reads `$1` (remote name) and `$2` (remote URL)
|
|
195
|
+
to allow/deny pushes to specific remotes:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
#!/bin/sh
|
|
199
|
+
remote="$1"
|
|
200
|
+
url="$2"
|
|
201
|
+
if [ "$remote" = "origin" ] && echo "$url" | grep -q "production"; then
|
|
202
|
+
echo "Direct push to production blocked. PR via main." >&2
|
|
203
|
+
exit 1
|
|
204
|
+
fi
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This requires the standard pre-push argv. **rea 0.13.2+ preserves
|
|
208
|
+
git's argv unchanged** for fragments — earlier versions (0.13.0 /
|
|
209
|
+
0.13.1) had a known bug where `set --` mutation in the rea dispatch
|
|
210
|
+
clobbered `$@`. Upgrade to `^0.13.2` if branch-policy linters are part
|
|
211
|
+
of your chain.
|
|
212
|
+
|
|
213
|
+
### Migration
|
|
214
|
+
|
|
215
|
+
Drop the body into a fragment as-is:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
cat > .husky/pre-push.d/05-branch-policy <<'EOF'
|
|
219
|
+
#!/bin/sh
|
|
220
|
+
remote="$1"
|
|
221
|
+
url="$2"
|
|
222
|
+
if [ "$remote" = "origin" ] && echo "$url" | grep -q "production"; then
|
|
223
|
+
echo "Direct push to production blocked. PR via main." >&2
|
|
224
|
+
exit 1
|
|
225
|
+
fi
|
|
226
|
+
EOF
|
|
227
|
+
chmod +x .husky/pre-push.d/05-branch-policy
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Conflict pattern: pre-existing rea-CLI invocation
|
|
231
|
+
|
|
232
|
+
Some consumers had `exec rea hook push-gate "$@"` chained inline in a
|
|
233
|
+
foreign hook body. `rea doctor` recognizes this pattern and reports
|
|
234
|
+
the hook as `external (delegates to rea hook push-gate)` — `pass`,
|
|
235
|
+
not `fail`. No migration required, but you cannot benefit from the
|
|
236
|
+
extension-fragment chain unless you let rea own the hook body.
|
|
237
|
+
|
|
238
|
+
If you want both — rea ownership AND your other commands — migrate the
|
|
239
|
+
other commands to fragments.
|
|
240
|
+
|
|
241
|
+
## Conflict pattern: husky 9 layout (`core.hooksPath=.husky/_`)
|
|
242
|
+
|
|
243
|
+
This is the default husky 9 install. rea 0.13.1+ supports it
|
|
244
|
+
correctly: doctor follows the husky 9 stub indirection from
|
|
245
|
+
`.husky/_/<hookname>` through `.husky/_/h` to the canonical
|
|
246
|
+
`.husky/<hookname>`. No migration required.
|
|
247
|
+
|
|
248
|
+
If you're on rea 0.13.0 and seeing `[fail] pre-push hook` despite a
|
|
249
|
+
correctly-installed `.husky/pre-push`, upgrade to `^0.13.1`.
|
|
250
|
+
|
|
251
|
+
## What `rea doctor` will tell you
|
|
252
|
+
|
|
253
|
+
After migration, run `pnpm rea doctor`. The relevant lines:
|
|
254
|
+
|
|
255
|
+
- `[ok] pre-push hook installed` — rea-managed body active, fragments
|
|
256
|
+
(if any) detected
|
|
257
|
+
- `[fail] pre-push hook installed` with **"Detected prior tooling: X,
|
|
258
|
+
Y, Z"** — your existing hook still chains tooling that should be in
|
|
259
|
+
fragments. Move each named tool to a `.d/` fragment, then re-run
|
|
260
|
+
`rea init`.
|
|
261
|
+
- `[info] extension-hook fragments detected: N pre-push.d, M
|
|
262
|
+
commit-msg.d` — your fragment chain is active
|
|
263
|
+
|
|
264
|
+
## Codex model knobs (added in 0.14.0)
|
|
265
|
+
|
|
266
|
+
The push-gate now pins the flagship codex model and `high` reasoning
|
|
267
|
+
effort by default. Pre-0.14.0 it used codex's built-in default, which
|
|
268
|
+
is the special-purpose `codex-auto-review` model at `medium`
|
|
269
|
+
reasoning — a meaningfully weaker reviewer than the flagship.
|
|
270
|
+
Same-code-different-verdict thrashing on long-running branches was
|
|
271
|
+
substantially driven by the lower-reasoning default.
|
|
272
|
+
|
|
273
|
+
**Defaults (0.14.0+):**
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
review:
|
|
277
|
+
codex_model: gpt-5.4 # was codex-auto-review (codex's own default)
|
|
278
|
+
codex_reasoning_effort: high # was medium (codex's own default)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
You don't need to set these — `gpt-5.4` + `high` are baked in at the
|
|
282
|
+
package level. The policy keys exist for cost-bounded environments
|
|
283
|
+
that want to opt into a weaker model:
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
review:
|
|
287
|
+
codex_model: codex-auto-review # opts back into the prior default
|
|
288
|
+
codex_reasoning_effort: medium
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The model name is passed through to codex's TOML config layer
|
|
292
|
+
(`-c model="…"`); codex itself validates it. An unknown model name
|
|
293
|
+
surfaces as a clear runtime error at first push, not a silent
|
|
294
|
+
fallback. Codex's current catalog (as of 2026-05-03):
|
|
295
|
+
|
|
296
|
+
- `gpt-5.4` — flagship, reasoning-capable (recommended for review)
|
|
297
|
+
- `gpt-5.4-mini` — smaller, faster, cheaper, less reasoning depth
|
|
298
|
+
- `gpt-5.3-codex` — prior generation, code-specialized
|
|
299
|
+
- `gpt-5.3-codex-spark` — even faster prior gen
|
|
300
|
+
- `gpt-5.2` — older, generally avoid for security-relevant review
|
|
301
|
+
- `codex-auto-review` — special-purpose, lower reasoning ceiling
|
|
302
|
+
|
|
303
|
+
Reasoning effort is `low | medium | high`. `high` spends more compute
|
|
304
|
+
per finding and produces more consistent verdicts — fewer
|
|
305
|
+
same-code-different-verdict round-trips. Trade-off is push-gate
|
|
306
|
+
latency.
|
|
307
|
+
|
|
308
|
+
## Policy knobs worth setting
|
|
309
|
+
|
|
310
|
+
For consumers with a long-running migration branch (>30 commits since
|
|
311
|
+
last push), the push-gate auto-narrows the codex review window unless
|
|
312
|
+
you opt out. Pin explicit values to avoid surprises:
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
# .rea/policy.yaml
|
|
316
|
+
review:
|
|
317
|
+
codex_required: true
|
|
318
|
+
timeout_ms: 1800000 # 30 min — explicit pin
|
|
319
|
+
auto_narrow_threshold: 30 # 0 to disable auto-narrow
|
|
320
|
+
last_n_commits: 10 # explicit scope window
|
|
321
|
+
codex_model: gpt-5.4 # 0.14.0+ default; iron-gate
|
|
322
|
+
codex_reasoning_effort: high # 0.14.0+ default; iron-gate
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Bypass when you genuinely need to
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Audited skip: codex flips on a known-ambivalent file
|
|
329
|
+
REA_SKIP_CODEX_REVIEW="cemPath-ambivalence" git push
|
|
330
|
+
|
|
331
|
+
# Whole-gate skip: codex CLI itself is broken
|
|
332
|
+
REA_SKIP_PUSH_GATE="codex-cli-crash-pinging-team" git push
|
|
333
|
+
|
|
334
|
+
# Concerns-only override (P2 findings) without skipping the gate
|
|
335
|
+
REA_ALLOW_CONCERNS=1 git push
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Every bypass is audit-logged with the reason in `.rea/audit.jsonl`.
|
|
339
|
+
Reasons should be specific — "skip" is not a reason; the file or
|
|
340
|
+
verdict that triggered it is.
|
|
341
|
+
|
|
342
|
+
## When to file an issue vs handle in-tree
|
|
343
|
+
|
|
344
|
+
- **rea hook ate my chain on `rea upgrade`** → file an issue, that's
|
|
345
|
+
rea's fault. Workaround: migrate to `.d/` fragments.
|
|
346
|
+
- **rea doctor false-positives on my legitimate setup** → file an
|
|
347
|
+
issue.
|
|
348
|
+
- **codex flips verdicts on the same code** → upstream of rea (codex
|
|
349
|
+
CLI itself). Use `REA_SKIP_CODEX_REVIEW` with a specific reason and
|
|
350
|
+
document the ambivalence.
|
|
351
|
+
- **My pre-commit hook breaks on push** → not rea (rea ships no
|
|
352
|
+
pre-commit). Fix in your repo.
|
|
@@ -72,6 +72,20 @@ export interface CodexRunOptions {
|
|
|
72
72
|
timeoutMs: number;
|
|
73
73
|
/** Optional custom review prompt; defaults to Codex's built-in. */
|
|
74
74
|
prompt?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Codex CLI model override (0.13.4+). When set, the runner passes
|
|
77
|
+
* `-c model="<value>"` to `codex exec review`. Codex itself validates
|
|
78
|
+
* the name. `undefined` falls back to codex's own default
|
|
79
|
+
* (`codex-auto-review` today, NOT the `gpt-5.4` flagship).
|
|
80
|
+
*/
|
|
81
|
+
model?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Codex reasoning effort (0.13.4+). When set, the runner passes
|
|
84
|
+
* `-c model_reasoning_effort="<value>"`. Only meaningful when paired
|
|
85
|
+
* with a reasoning-capable model (gpt-5.4, gpt-5.3-codex). Codex's
|
|
86
|
+
* own default is `medium`.
|
|
87
|
+
*/
|
|
88
|
+
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
75
89
|
/**
|
|
76
90
|
* Env passthrough. Tests inject a clean env to prevent ambient overrides.
|
|
77
91
|
* Production passes `process.env`.
|
|
@@ -110,6 +110,22 @@ export function createRealGitExecutor(cwd) {
|
|
|
110
110
|
},
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Codex invocation
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Escape a string for safe inclusion inside a TOML basic-string literal.
|
|
118
|
+
* Codex's `-c key=value` parser runs the value through TOML, so we have to
|
|
119
|
+
* close over the same escape contract — namely backslash and double-quote
|
|
120
|
+
* (TOML basic strings forbid raw `"` and `\` in the body). The model names
|
|
121
|
+
* and reasoning levels we expect (`gpt-5.4`, `high`, etc.) never contain
|
|
122
|
+
* either character; this guard exists so a future model-name typo with a
|
|
123
|
+
* shell metacharacter cannot smuggle a TOML escape that codex misparses
|
|
124
|
+
* into something dangerous.
|
|
125
|
+
*/
|
|
126
|
+
function escapeTomlString(value) {
|
|
127
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
128
|
+
}
|
|
113
129
|
/**
|
|
114
130
|
* Execute `codex exec review` and return the concatenated review text on
|
|
115
131
|
* success. Callers then pass the text to `summarizeReview()` to get a
|
|
@@ -120,7 +136,27 @@ export function createRealGitExecutor(cwd) {
|
|
|
120
136
|
*/
|
|
121
137
|
export async function runCodexReview(options) {
|
|
122
138
|
const spawner = options.spawnImpl ?? spawn;
|
|
123
|
-
|
|
139
|
+
// Model + reasoning overrides go BEFORE the `exec` subcommand because
|
|
140
|
+
// `-c key=value` is a top-level codex CLI flag, not an `exec` flag.
|
|
141
|
+
// Codex's TOML parser interprets the value, so we wrap strings in TOML
|
|
142
|
+
// quotes — `-c model="gpt-5.4"` not `-c model=gpt-5.4` — to ensure the
|
|
143
|
+
// value lands as a string regardless of upstream parsing changes.
|
|
144
|
+
const overrideArgs = [];
|
|
145
|
+
if (options.model !== undefined && options.model.length > 0) {
|
|
146
|
+
overrideArgs.push('-c', `model="${escapeTomlString(options.model)}"`);
|
|
147
|
+
}
|
|
148
|
+
if (options.reasoningEffort !== undefined) {
|
|
149
|
+
overrideArgs.push('-c', `model_reasoning_effort="${escapeTomlString(options.reasoningEffort)}"`);
|
|
150
|
+
}
|
|
151
|
+
const baseArgs = [
|
|
152
|
+
...overrideArgs,
|
|
153
|
+
'exec',
|
|
154
|
+
'review',
|
|
155
|
+
'--base',
|
|
156
|
+
options.baseRef,
|
|
157
|
+
'--json',
|
|
158
|
+
'--ephemeral',
|
|
159
|
+
];
|
|
124
160
|
const args = options.prompt !== undefined && options.prompt.length > 0 ? [...baseArgs, options.prompt] : baseArgs;
|
|
125
161
|
let child;
|
|
126
162
|
try {
|
|
@@ -342,6 +342,14 @@ export async function runPushGate(deps) {
|
|
|
342
342
|
cwd: deps.baseDir,
|
|
343
343
|
timeoutMs: policy.timeout_ms,
|
|
344
344
|
env,
|
|
345
|
+
// 0.14.0+: pass the resolved policy's model + reasoning overrides so
|
|
346
|
+
// codex spawns with `-c model="<name>" -c model_reasoning_effort="<level>"`.
|
|
347
|
+
// Defaults (gpt-5.4 + high) are baked into resolvePushGatePolicy so
|
|
348
|
+
// policies that omit these keys still get the iron-gate defaults.
|
|
349
|
+
...(policy.codex_model !== undefined ? { model: policy.codex_model } : {}),
|
|
350
|
+
...(policy.codex_reasoning_effort !== undefined
|
|
351
|
+
? { reasoningEffort: policy.codex_reasoning_effort }
|
|
352
|
+
: {}),
|
|
345
353
|
});
|
|
346
354
|
const summary = summarizeReview(codexResult.reviewText);
|
|
347
355
|
const blocked = summary.verdict === 'blocking'
|
|
@@ -43,6 +43,19 @@ export interface ResolvedReviewPolicy {
|
|
|
43
43
|
* emits a stderr warning. Defaults to 30 when unset; 0 disables.
|
|
44
44
|
*/
|
|
45
45
|
auto_narrow_threshold: number;
|
|
46
|
+
/**
|
|
47
|
+
* Codex CLI model override (0.13.4+). When set, the runner passes
|
|
48
|
+
* `-c model="<value>"` to every `codex exec review`. `undefined` falls
|
|
49
|
+
* back to codex's own default (currently `codex-auto-review`, NOT the
|
|
50
|
+
* flagship `gpt-5.4`).
|
|
51
|
+
*/
|
|
52
|
+
codex_model: string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Codex reasoning effort (0.13.4+). When set, the runner passes
|
|
55
|
+
* `-c model_reasoning_effort="<value>"`. `undefined` falls back to
|
|
56
|
+
* codex's own default (currently `medium`).
|
|
57
|
+
*/
|
|
58
|
+
codex_reasoning_effort: 'low' | 'medium' | 'high' | undefined;
|
|
46
59
|
/** `true` when `.rea/policy.yaml` was absent; defaults apply. */
|
|
47
60
|
policyMissing: boolean;
|
|
48
61
|
}
|
|
@@ -63,6 +76,27 @@ export declare const PUSH_GATE_DEFAULT_AUTO_NARROW_THRESHOLD = 30;
|
|
|
63
76
|
* recent work.
|
|
64
77
|
*/
|
|
65
78
|
export declare const PUSH_GATE_DEFAULT_LAST_N_COMMITS_FALLBACK = 10;
|
|
79
|
+
/**
|
|
80
|
+
* Default codex model for the push-gate (0.14.0+). Pinned to the flagship
|
|
81
|
+
* (`gpt-5.4`) instead of falling through to codex's own default of
|
|
82
|
+
* `codex-auto-review` (a lower-reasoning special-purpose model). Verdict
|
|
83
|
+
* stability matters more than per-push compute cost for adversarial
|
|
84
|
+
* review of consumer codebases — the helixir 2026-04-26 thrashing came
|
|
85
|
+
* from the lower-reasoning default.
|
|
86
|
+
*
|
|
87
|
+
* Override via `policy.review.codex_model: <name>` in `.rea/policy.yaml`
|
|
88
|
+
* for cost-bounded environments. `codex-auto-review` is the explicit
|
|
89
|
+
* opt-in to the prior 0.13.x behavior.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PUSH_GATE_DEFAULT_CODEX_MODEL = "gpt-5.4";
|
|
92
|
+
/**
|
|
93
|
+
* Default codex reasoning effort (0.14.0+). Pinned to `high` for maximum
|
|
94
|
+
* compute per finding — fewer same-code-different-verdict round-trips.
|
|
95
|
+
* Trades latency for stability. Override via
|
|
96
|
+
* `policy.review.codex_reasoning_effort: medium | low` in
|
|
97
|
+
* `.rea/policy.yaml` for cost-bounded environments.
|
|
98
|
+
*/
|
|
99
|
+
export declare const PUSH_GATE_DEFAULT_CODEX_REASONING_EFFORT: 'low' | 'medium' | 'high';
|
|
66
100
|
/**
|
|
67
101
|
* Resolve the push-gate policy for `baseDir`. Never throws — a malformed
|
|
68
102
|
* policy file surfaces as a typed error via the underlying zod validator,
|
|
@@ -45,6 +45,27 @@ export const PUSH_GATE_DEFAULT_AUTO_NARROW_THRESHOLD = 30;
|
|
|
45
45
|
* recent work.
|
|
46
46
|
*/
|
|
47
47
|
export const PUSH_GATE_DEFAULT_LAST_N_COMMITS_FALLBACK = 10;
|
|
48
|
+
/**
|
|
49
|
+
* Default codex model for the push-gate (0.14.0+). Pinned to the flagship
|
|
50
|
+
* (`gpt-5.4`) instead of falling through to codex's own default of
|
|
51
|
+
* `codex-auto-review` (a lower-reasoning special-purpose model). Verdict
|
|
52
|
+
* stability matters more than per-push compute cost for adversarial
|
|
53
|
+
* review of consumer codebases — the helixir 2026-04-26 thrashing came
|
|
54
|
+
* from the lower-reasoning default.
|
|
55
|
+
*
|
|
56
|
+
* Override via `policy.review.codex_model: <name>` in `.rea/policy.yaml`
|
|
57
|
+
* for cost-bounded environments. `codex-auto-review` is the explicit
|
|
58
|
+
* opt-in to the prior 0.13.x behavior.
|
|
59
|
+
*/
|
|
60
|
+
export const PUSH_GATE_DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
61
|
+
/**
|
|
62
|
+
* Default codex reasoning effort (0.14.0+). Pinned to `high` for maximum
|
|
63
|
+
* compute per finding — fewer same-code-different-verdict round-trips.
|
|
64
|
+
* Trades latency for stability. Override via
|
|
65
|
+
* `policy.review.codex_reasoning_effort: medium | low` in
|
|
66
|
+
* `.rea/policy.yaml` for cost-bounded environments.
|
|
67
|
+
*/
|
|
68
|
+
export const PUSH_GATE_DEFAULT_CODEX_REASONING_EFFORT = 'high';
|
|
48
69
|
/**
|
|
49
70
|
* Resolve the push-gate policy for `baseDir`. Never throws — a malformed
|
|
50
71
|
* policy file surfaces as a typed error via the underlying zod validator,
|
|
@@ -64,6 +85,8 @@ export async function resolvePushGatePolicy(baseDir) {
|
|
|
64
85
|
timeout_ms: PUSH_GATE_DEFAULT_TIMEOUT_MS,
|
|
65
86
|
last_n_commits: undefined,
|
|
66
87
|
auto_narrow_threshold: PUSH_GATE_DEFAULT_AUTO_NARROW_THRESHOLD,
|
|
88
|
+
codex_model: PUSH_GATE_DEFAULT_CODEX_MODEL,
|
|
89
|
+
codex_reasoning_effort: PUSH_GATE_DEFAULT_CODEX_REASONING_EFFORT,
|
|
67
90
|
policyMissing: true,
|
|
68
91
|
};
|
|
69
92
|
}
|
|
@@ -75,6 +98,8 @@ export async function resolvePushGatePolicy(baseDir) {
|
|
|
75
98
|
timeout_ms: review.timeout_ms ?? PUSH_GATE_DEFAULT_TIMEOUT_MS,
|
|
76
99
|
last_n_commits: review.last_n_commits,
|
|
77
100
|
auto_narrow_threshold: review.auto_narrow_threshold ?? PUSH_GATE_DEFAULT_AUTO_NARROW_THRESHOLD,
|
|
101
|
+
codex_model: review.codex_model ?? PUSH_GATE_DEFAULT_CODEX_MODEL,
|
|
102
|
+
codex_reasoning_effort: review.codex_reasoning_effort ?? PUSH_GATE_DEFAULT_CODEX_REASONING_EFFORT,
|
|
78
103
|
policyMissing: false,
|
|
79
104
|
};
|
|
80
105
|
}
|
package/dist/policy/loader.d.ts
CHANGED
|
@@ -45,18 +45,52 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
45
45
|
* intent and auto-narrow stays out of the way).
|
|
46
46
|
*/
|
|
47
47
|
auto_narrow_threshold: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
/**
|
|
49
|
+
* Codex CLI model override (0.13.4+). Pinned via `-c model="<name>"` on
|
|
50
|
+
* every `codex exec review` invocation. When unset, codex's own default
|
|
51
|
+
* applies — which today is the special-purpose `codex-auto-review`
|
|
52
|
+
* model at `medium` reasoning, NOT the flagship.
|
|
53
|
+
*
|
|
54
|
+
* For serious adversarial review on consumer codebases (where verdict
|
|
55
|
+
* stability matters) the recommended setting is `gpt-5.4` with
|
|
56
|
+
* `codex_reasoning_effort: high`. Higher reasoning trades push-gate
|
|
57
|
+
* latency for finding consistency — fewer same-code-different-verdict
|
|
58
|
+
* round-trips like the 2026-04-26 helixir migration session.
|
|
59
|
+
*
|
|
60
|
+
* Loose string type: codex's model catalog evolves over time and we do
|
|
61
|
+
* NOT want to lock consumers to a hardcoded enum that drifts behind
|
|
62
|
+
* upstream. Codex itself validates the model name at exec time.
|
|
63
|
+
*/
|
|
64
|
+
codex_model: z.ZodOptional<z.ZodString>;
|
|
65
|
+
/**
|
|
66
|
+
* Codex reasoning effort knob (0.13.4+). Pinned via
|
|
67
|
+
* `-c model_reasoning_effort="<level>"` on every invocation. Only
|
|
68
|
+
* meaningful when paired with a reasoning-capable model (gpt-5.4,
|
|
69
|
+
* gpt-5.3-codex, etc.). The `codex-auto-review` model honors this
|
|
70
|
+
* but caps lower than gpt-5.4.
|
|
71
|
+
*
|
|
72
|
+
* Recommended: `high` for serious review on long-running branches
|
|
73
|
+
* (more compute spent per finding, fewer flips). `medium` is codex's
|
|
74
|
+
* own default. `low` for cost-bounded environments where consistency
|
|
75
|
+
* matters less than throughput.
|
|
76
|
+
*/
|
|
77
|
+
codex_reasoning_effort: z.ZodOptional<z.ZodEnum<["low", "medium", "high"]>>;
|
|
48
78
|
}, "strict", z.ZodTypeAny, {
|
|
49
79
|
codex_required?: boolean | undefined;
|
|
50
80
|
concerns_blocks?: boolean | undefined;
|
|
51
81
|
timeout_ms?: number | undefined;
|
|
52
82
|
last_n_commits?: number | undefined;
|
|
53
83
|
auto_narrow_threshold?: number | undefined;
|
|
84
|
+
codex_model?: string | undefined;
|
|
85
|
+
codex_reasoning_effort?: "low" | "medium" | "high" | undefined;
|
|
54
86
|
}, {
|
|
55
87
|
codex_required?: boolean | undefined;
|
|
56
88
|
concerns_blocks?: boolean | undefined;
|
|
57
89
|
timeout_ms?: number | undefined;
|
|
58
90
|
last_n_commits?: number | undefined;
|
|
59
91
|
auto_narrow_threshold?: number | undefined;
|
|
92
|
+
codex_model?: string | undefined;
|
|
93
|
+
codex_reasoning_effort?: "low" | "medium" | "high" | undefined;
|
|
60
94
|
}>>;
|
|
61
95
|
redact: z.ZodOptional<z.ZodObject<{
|
|
62
96
|
match_timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
@@ -152,6 +186,8 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
152
186
|
timeout_ms?: number | undefined;
|
|
153
187
|
last_n_commits?: number | undefined;
|
|
154
188
|
auto_narrow_threshold?: number | undefined;
|
|
189
|
+
codex_model?: string | undefined;
|
|
190
|
+
codex_reasoning_effort?: "low" | "medium" | "high" | undefined;
|
|
155
191
|
} | undefined;
|
|
156
192
|
redact?: {
|
|
157
193
|
match_timeout_ms?: number | undefined;
|
|
@@ -197,6 +233,8 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
197
233
|
timeout_ms?: number | undefined;
|
|
198
234
|
last_n_commits?: number | undefined;
|
|
199
235
|
auto_narrow_threshold?: number | undefined;
|
|
236
|
+
codex_model?: string | undefined;
|
|
237
|
+
codex_reasoning_effort?: "low" | "medium" | "high" | undefined;
|
|
200
238
|
} | undefined;
|
|
201
239
|
redact?: {
|
|
202
240
|
match_timeout_ms?: number | undefined;
|
package/dist/policy/loader.js
CHANGED
|
@@ -38,6 +38,36 @@ const ReviewPolicySchema = z
|
|
|
38
38
|
* intent and auto-narrow stays out of the way).
|
|
39
39
|
*/
|
|
40
40
|
auto_narrow_threshold: z.number().int().nonnegative().optional(),
|
|
41
|
+
/**
|
|
42
|
+
* Codex CLI model override (0.13.4+). Pinned via `-c model="<name>"` on
|
|
43
|
+
* every `codex exec review` invocation. When unset, codex's own default
|
|
44
|
+
* applies — which today is the special-purpose `codex-auto-review`
|
|
45
|
+
* model at `medium` reasoning, NOT the flagship.
|
|
46
|
+
*
|
|
47
|
+
* For serious adversarial review on consumer codebases (where verdict
|
|
48
|
+
* stability matters) the recommended setting is `gpt-5.4` with
|
|
49
|
+
* `codex_reasoning_effort: high`. Higher reasoning trades push-gate
|
|
50
|
+
* latency for finding consistency — fewer same-code-different-verdict
|
|
51
|
+
* round-trips like the 2026-04-26 helixir migration session.
|
|
52
|
+
*
|
|
53
|
+
* Loose string type: codex's model catalog evolves over time and we do
|
|
54
|
+
* NOT want to lock consumers to a hardcoded enum that drifts behind
|
|
55
|
+
* upstream. Codex itself validates the model name at exec time.
|
|
56
|
+
*/
|
|
57
|
+
codex_model: z.string().min(1).optional(),
|
|
58
|
+
/**
|
|
59
|
+
* Codex reasoning effort knob (0.13.4+). Pinned via
|
|
60
|
+
* `-c model_reasoning_effort="<level>"` on every invocation. Only
|
|
61
|
+
* meaningful when paired with a reasoning-capable model (gpt-5.4,
|
|
62
|
+
* gpt-5.3-codex, etc.). The `codex-auto-review` model honors this
|
|
63
|
+
* but caps lower than gpt-5.4.
|
|
64
|
+
*
|
|
65
|
+
* Recommended: `high` for serious review on long-running branches
|
|
66
|
+
* (more compute spent per finding, fewer flips). `medium` is codex's
|
|
67
|
+
* own default. `low` for cost-bounded environments where consistency
|
|
68
|
+
* matters less than throughput.
|
|
69
|
+
*/
|
|
70
|
+
codex_reasoning_effort: z.enum(['low', 'medium', 'high']).optional(),
|
|
41
71
|
})
|
|
42
72
|
.strict();
|
|
43
73
|
/**
|
package/dist/policy/types.d.ts
CHANGED
|
@@ -130,6 +130,34 @@ export interface ReviewPolicy {
|
|
|
130
130
|
* Non-negative integer. The loader rejects negative values.
|
|
131
131
|
*/
|
|
132
132
|
auto_narrow_threshold?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Codex CLI model override (0.13.4+). Pinned via `-c model="<name>"` on
|
|
135
|
+
* every `codex exec review` invocation. When unset, codex's own default
|
|
136
|
+
* applies — which today is the special-purpose `codex-auto-review` model
|
|
137
|
+
* at medium reasoning, NOT the flagship.
|
|
138
|
+
*
|
|
139
|
+
* Recommended for serious adversarial review: `gpt-5.4` paired with
|
|
140
|
+
* `codex_reasoning_effort: high`. Higher reasoning trades push-gate
|
|
141
|
+
* latency for verdict consistency — fewer same-code-different-verdict
|
|
142
|
+
* round-trips like the 2026-04-26 helixir migration session.
|
|
143
|
+
*
|
|
144
|
+
* Loose string type — codex's model catalog evolves. Codex itself
|
|
145
|
+
* validates the model name at exec time; an unknown name surfaces as
|
|
146
|
+
* a clear runtime error rather than a silent fallback.
|
|
147
|
+
*/
|
|
148
|
+
codex_model?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Codex reasoning effort (0.13.4+). Pinned via
|
|
151
|
+
* `-c model_reasoning_effort="<level>"` on every invocation. Only
|
|
152
|
+
* meaningful when paired with a reasoning-capable model (gpt-5.4,
|
|
153
|
+
* gpt-5.3-codex). Codex's own default is `medium`.
|
|
154
|
+
*
|
|
155
|
+
* Recommended: `high` for serious review on long-running branches
|
|
156
|
+
* (more compute spent per finding, fewer flips). `low` for
|
|
157
|
+
* cost-bounded environments where consistency matters less than
|
|
158
|
+
* throughput.
|
|
159
|
+
*/
|
|
160
|
+
codex_reasoning_effort?: 'low' | 'medium' | 'high';
|
|
133
161
|
}
|
|
134
162
|
/**
|
|
135
163
|
* User-supplied redaction pattern entry. Each pattern has a stable `name` used
|
|
@@ -113,6 +113,44 @@ normalize_path() {
|
|
|
113
113
|
|
|
114
114
|
NORMALIZED=$(normalize_path "$FILE_PATH")
|
|
115
115
|
|
|
116
|
+
# ── 5a. Path-traversal rejection (0.14.0 iron-gate fix) ───────────────────────
|
|
117
|
+
# Reject any path containing a `..` segment BEFORE the literal-match below.
|
|
118
|
+
# Without this, `foo/../CODEOWNERS` would get past `normalize_path()` (which
|
|
119
|
+
# only strips leading project root + URL-decodes) and the literal-match
|
|
120
|
+
# loop would compare `foo/../CODEOWNERS` against the literal `CODEOWNERS`
|
|
121
|
+
# entry — which doesn't match, so the policy lets the write through. The
|
|
122
|
+
# downstream Write/Edit tool then resolves the traversal and writes to
|
|
123
|
+
# `CODEOWNERS` anyway, defeating the gate.
|
|
124
|
+
#
|
|
125
|
+
# Mirrors settings-protection.sh §5a (which has had this guard since
|
|
126
|
+
# 0.10.x). Both pre- and post-decode forms are checked because
|
|
127
|
+
# normalize_path() URL-decodes earlier and an attacker could split the
|
|
128
|
+
# traversal across encodings (`%2E%2E/`, `..%2F`, etc.).
|
|
129
|
+
raw_has_traversal=0
|
|
130
|
+
norm_has_traversal=0
|
|
131
|
+
case "/$FILE_PATH/" in
|
|
132
|
+
*/../*) raw_has_traversal=1 ;;
|
|
133
|
+
esac
|
|
134
|
+
case "/$NORMALIZED/" in
|
|
135
|
+
*/../*) norm_has_traversal=1 ;;
|
|
136
|
+
esac
|
|
137
|
+
# Also catch URL-encoded traversal in case some tool routes raw-encoded
|
|
138
|
+
# paths through here (e.g. file:// inputs). normalize_path()'s decoder
|
|
139
|
+
# only handles a fixed set; an unrecognized encoding would slip past.
|
|
140
|
+
case "$FILE_PATH" in
|
|
141
|
+
*%2[Ee]%2[Ee]*|*%2[Ee].*|*.%2[Ee]*) raw_has_traversal=1 ;;
|
|
142
|
+
esac
|
|
143
|
+
if [[ "$raw_has_traversal" -eq 1 ]] || [[ "$norm_has_traversal" -eq 1 ]]; then
|
|
144
|
+
{
|
|
145
|
+
printf 'BLOCKED PATH: path traversal rejected\n'
|
|
146
|
+
printf '\n'
|
|
147
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
148
|
+
printf " Rule: path contains a '..' segment; rewrite to a canonical\n"
|
|
149
|
+
printf ' project-relative path without traversal.\n'
|
|
150
|
+
} >&2
|
|
151
|
+
exit 2
|
|
152
|
+
fi
|
|
153
|
+
|
|
116
154
|
for writable in "${AGENT_WRITABLE[@]}"; do
|
|
117
155
|
if [[ "$NORMALIZED" == "$writable" ]] || [[ "$NORMALIZED" == "$writable"* && "$writable" == */ ]]; then
|
|
118
156
|
exit 0
|
package/hooks/secret-scanner.sh
CHANGED
|
@@ -40,11 +40,41 @@ fi
|
|
|
40
40
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
41
41
|
CONTENT_WRITE=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
42
42
|
CONTENT_EDIT=$(printf '%s' "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
43
|
+
# MultiEdit (0.14.0 fix): the payload is at tool_input.edits[].new_string —
|
|
44
|
+
# an array, not a scalar — and the prior versions of this hook never read
|
|
45
|
+
# it. Result: any agent could route credential writes through MultiEdit and
|
|
46
|
+
# bypass the secret scanner entirely. We extract every `new_string` value
|
|
47
|
+
# from the edits array and concatenate them with newlines so the awk-based
|
|
48
|
+
# pattern scan below treats them like any other write content.
|
|
49
|
+
#
|
|
50
|
+
# Defensive coercion (codex round-1 P1): a malformed payload where
|
|
51
|
+
# `new_string` is a number, object, or array would make jq error out, the
|
|
52
|
+
# `2>/dev/null` would swallow stderr, `CONTENT_MULTIEDIT` would be empty,
|
|
53
|
+
# and the precedence chain below would fall through to `exit 0` —
|
|
54
|
+
# silently allowing the write. Same fail-open mode for a non-array
|
|
55
|
+
# `edits` value. We:
|
|
56
|
+
#
|
|
57
|
+
# 1. Coerce `.tool_input.edits` to `[]` if it's anything other than an
|
|
58
|
+
# array (`if type=="array" then . else [] end`)
|
|
59
|
+
# 2. Coerce every `new_string` to a string via `tostring` so jq cannot
|
|
60
|
+
# fail on heterogeneous types
|
|
61
|
+
#
|
|
62
|
+
# Both layers fail closed: a malformed payload either yields the empty
|
|
63
|
+
# string (no scan needed, exit 0 from the precedence chain) or yields a
|
|
64
|
+
# pattern-scannable string. There is no path where jq errors silently and
|
|
65
|
+
# the hook falls through to allow.
|
|
66
|
+
CONTENT_MULTIEDIT=$(printf '%s' "$INPUT" | jq -r '
|
|
67
|
+
(.tool_input.edits // [] | if type=="array" then . else [] end)
|
|
68
|
+
| map((.new_string // "") | tostring)
|
|
69
|
+
| join("\n")
|
|
70
|
+
' 2>/dev/null)
|
|
43
71
|
|
|
44
72
|
if [[ -n "$CONTENT_WRITE" ]]; then
|
|
45
73
|
CONTENT="$CONTENT_WRITE"
|
|
46
74
|
elif [[ -n "$CONTENT_EDIT" ]]; then
|
|
47
75
|
CONTENT="$CONTENT_EDIT"
|
|
76
|
+
elif [[ -n "$CONTENT_MULTIEDIT" ]]; then
|
|
77
|
+
CONTENT="$CONTENT_MULTIEDIT"
|
|
48
78
|
else
|
|
49
79
|
exit 0
|
|
50
80
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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)",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
".husky/",
|
|
50
50
|
"LICENSE",
|
|
51
51
|
"README.md",
|
|
52
|
+
"MIGRATING.md",
|
|
52
53
|
"SECURITY.md",
|
|
53
54
|
"THREAT_MODEL.md"
|
|
54
55
|
],
|