@bookedsolid/rea 0.30.0 → 0.30.1
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/.husky/prepare-commit-msg +20 -1
- package/dist/cli/doctor.js +31 -8
- package/dist/config/settings-schema.d.ts +13 -1
- package/dist/config/settings-schema.js +13 -2
- package/dist/policy/loader.d.ts +1 -1
- package/dist/policy/loader.js +15 -1
- package/package.json +1 -1
- package/templates/prepare-commit-msg.husky.sh +20 -1
|
@@ -88,12 +88,31 @@ rea_invoke() {
|
|
|
88
88
|
ENABLED=$(rea_invoke hook policy-get attribution.co_author.enabled 2>/dev/null)
|
|
89
89
|
REA_RC=$?
|
|
90
90
|
|
|
91
|
+
# REA_RC interpretation:
|
|
92
|
+
# 0 — rea CLI ran and returned a value (or empty for an
|
|
93
|
+
# unset key). Use the CLI reads.
|
|
94
|
+
# non-zero — rea CLI unreachable (127 sentinel), too old to know
|
|
95
|
+
# `hook policy-get`, OR the policy YAML is unparseable.
|
|
96
|
+
# In every one of those cases the policy file ITSELF
|
|
97
|
+
# may still be valid block-form YAML, so fall back to
|
|
98
|
+
# the embedded python3 parser. The realistic invalid-
|
|
99
|
+
# config case — `enabled: true` with an empty name or
|
|
100
|
+
# email — is caught downstream by the `[ -z "$CO_NAME" ]`
|
|
101
|
+
# defense-in-depth guard, which exits 0 without
|
|
102
|
+
# augmenting regardless of which reader produced the
|
|
103
|
+
# values. (An earlier 0.30.1 revision fail-closed on
|
|
104
|
+
# non-127 exit codes; codex round 1 showed that
|
|
105
|
+
# regressed the supported stale-CLI / pre-`pnpm i` flow,
|
|
106
|
+
# because an old `rea` exits non-zero exactly like an
|
|
107
|
+
# unparseable policy — the two are indistinguishable by
|
|
108
|
+
# exit code.)
|
|
91
109
|
if [ "$REA_RC" = "0" ]; then
|
|
92
110
|
CO_NAME=$(rea_invoke hook policy-get attribution.co_author.name 2>/dev/null || printf '')
|
|
93
111
|
CO_EMAIL=$(rea_invoke hook policy-get attribution.co_author.email 2>/dev/null || printf '')
|
|
94
112
|
SKIP_MERGE=$(rea_invoke hook policy-get attribution.co_author.skip_merge 2>/dev/null || printf 'false')
|
|
95
113
|
elif command -v python3 >/dev/null 2>&1; then
|
|
96
|
-
# rea CLI unreachable — fall back to
|
|
114
|
+
# rea CLI unreachable / stale / policy unparseable — fall back to the
|
|
115
|
+
# Python block-form parser.
|
|
97
116
|
CO_AUTHOR_PARSE=$(python3 - "$POLICY_FILE" <<'PY' 2>/dev/null
|
|
98
117
|
import re
|
|
99
118
|
import sys
|
package/dist/cli/doctor.js
CHANGED
|
@@ -301,7 +301,7 @@ export function checkSettingsSchema(baseDir, strict) {
|
|
|
301
301
|
detail: `malformed JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
|
-
const result = validateSettings(parsed);
|
|
304
|
+
const result = validateSettings(parsed, { strict });
|
|
305
305
|
const issues = [];
|
|
306
306
|
if (!result.parsed) {
|
|
307
307
|
issues.push(...result.errors.map((e) => `schema: ${e}`));
|
|
@@ -460,12 +460,22 @@ export function isGitRepo(baseDir) {
|
|
|
460
460
|
*/
|
|
461
461
|
/**
|
|
462
462
|
* Resolve the active git hooks directory for the doctor's prepare-commit-msg
|
|
463
|
-
* check. Mirrors `
|
|
464
|
-
* synchronous
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* `.
|
|
468
|
-
*
|
|
463
|
+
* check. Mirrors `installPrepareCommitMsgHook`'s resolution order
|
|
464
|
+
* (synchronous — doctor is sync end-to-end):
|
|
465
|
+
*
|
|
466
|
+
* 1. `core.hooksPath` — explicit operator override (husky 9 installs
|
|
467
|
+
* land at `.husky/_/`). Honored verbatim.
|
|
468
|
+
* 2. `git rev-parse --git-path hooks` — resolves the canonical hooks
|
|
469
|
+
* dir even when `.git` is a FILE (linked worktrees, submodules).
|
|
470
|
+
* 0.30.1 round-5 P2: the prior implementation hardcoded
|
|
471
|
+
* `.git/hooks`, which is wrong for worktrees/submodules where
|
|
472
|
+
* `.git` is a gitdir pointer file, not a directory.
|
|
473
|
+
* 3. `.git/hooks` — last-resort fallback when git itself is missing.
|
|
474
|
+
*
|
|
475
|
+
* The Husky 9 STUB indirection (active file at the resolved path is a
|
|
476
|
+
* `. "${0%/*}/h"` stub that dispatches to `.husky/prepare-commit-msg`)
|
|
477
|
+
* is followed separately inside `checkPrepareCommitMsgHook` via
|
|
478
|
+
* `isHusky9Stub` / `resolveHusky9StubTarget`.
|
|
469
479
|
*/
|
|
470
480
|
function resolveHooksDirSync(baseDir) {
|
|
471
481
|
try {
|
|
@@ -479,7 +489,20 @@ function resolveHooksDirSync(baseDir) {
|
|
|
479
489
|
}
|
|
480
490
|
}
|
|
481
491
|
catch {
|
|
482
|
-
// git missing or `core.hooksPath` unset — fall through
|
|
492
|
+
// git missing or `core.hooksPath` unset — fall through.
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const out = execFileSync('git', ['-C', baseDir, 'rev-parse', '--git-path', 'hooks'], {
|
|
496
|
+
encoding: 'utf8',
|
|
497
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
498
|
+
});
|
|
499
|
+
const trimmed = out.trim();
|
|
500
|
+
if (trimmed.length > 0) {
|
|
501
|
+
return path.isAbsolute(trimmed) ? trimmed : path.join(baseDir, trimmed);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// git missing — fall through to the literal default.
|
|
483
506
|
}
|
|
484
507
|
return path.join(baseDir, '.git', 'hooks');
|
|
485
508
|
}
|
|
@@ -2066,8 +2066,20 @@ export interface SettingsValidationResult {
|
|
|
2066
2066
|
* best-effort scan of any `hooks: { PreToolUse: [...] }` shape we
|
|
2067
2067
|
* recognize, so the operator still sees traversal + missing-hooks
|
|
2068
2068
|
* findings.
|
|
2069
|
+
*
|
|
2070
|
+
* `strict` selects the schema:
|
|
2071
|
+
* - `false` (default) — `SettingsSchema`, top-level `.passthrough()`.
|
|
2072
|
+
* Unknown harness keys pass. Used by `rea upgrade` and advisory
|
|
2073
|
+
* `rea doctor`.
|
|
2074
|
+
* - `true` — `SettingsSchemaStrict`, top-level `.strict()`. Unknown
|
|
2075
|
+
* keys fail the parse. Used only by `rea doctor --strict` (the CI
|
|
2076
|
+
* gate path). 0.30.1 round-5 P2: the strict schema existed since
|
|
2077
|
+
* 0.30.0 but `validateSettings` never accepted the selector, so
|
|
2078
|
+
* `rea doctor --strict` silently ran the lenient schema.
|
|
2069
2079
|
*/
|
|
2070
|
-
export declare function validateSettings(input: unknown
|
|
2080
|
+
export declare function validateSettings(input: unknown, options?: {
|
|
2081
|
+
strict?: boolean;
|
|
2082
|
+
}): SettingsValidationResult;
|
|
2071
2083
|
/**
|
|
2072
2084
|
* Cross-check: every name in `EXPECTED_HOOKS` should appear as the
|
|
2073
2085
|
* basename of at least one `command` across PreToolUse + PostToolUse.
|
|
@@ -154,8 +154,18 @@ export function validateNoTraversal(settings) {
|
|
|
154
154
|
* best-effort scan of any `hooks: { PreToolUse: [...] }` shape we
|
|
155
155
|
* recognize, so the operator still sees traversal + missing-hooks
|
|
156
156
|
* findings.
|
|
157
|
+
*
|
|
158
|
+
* `strict` selects the schema:
|
|
159
|
+
* - `false` (default) — `SettingsSchema`, top-level `.passthrough()`.
|
|
160
|
+
* Unknown harness keys pass. Used by `rea upgrade` and advisory
|
|
161
|
+
* `rea doctor`.
|
|
162
|
+
* - `true` — `SettingsSchemaStrict`, top-level `.strict()`. Unknown
|
|
163
|
+
* keys fail the parse. Used only by `rea doctor --strict` (the CI
|
|
164
|
+
* gate path). 0.30.1 round-5 P2: the strict schema existed since
|
|
165
|
+
* 0.30.0 but `validateSettings` never accepted the selector, so
|
|
166
|
+
* `rea doctor --strict` silently ran the lenient schema.
|
|
157
167
|
*/
|
|
158
|
-
export function validateSettings(input) {
|
|
168
|
+
export function validateSettings(input, options = {}) {
|
|
159
169
|
const result = {
|
|
160
170
|
parsed: false,
|
|
161
171
|
settings: null,
|
|
@@ -164,7 +174,8 @@ export function validateSettings(input) {
|
|
|
164
174
|
missingReaHooks: [],
|
|
165
175
|
warnings: [],
|
|
166
176
|
};
|
|
167
|
-
const
|
|
177
|
+
const schema = options.strict === true ? SettingsSchemaStrict : SettingsSchema;
|
|
178
|
+
const parsed = schema.safeParse(input);
|
|
168
179
|
if (parsed.success) {
|
|
169
180
|
result.parsed = true;
|
|
170
181
|
result.settings = parsed.data;
|
package/dist/policy/loader.d.ts
CHANGED
|
@@ -249,7 +249,7 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
249
249
|
attribution: z.ZodOptional<z.ZodObject<{
|
|
250
250
|
co_author: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
251
251
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
252
|
-
name: z.ZodOptional<z.ZodString
|
|
252
|
+
name: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
253
253
|
email: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
254
254
|
skip_merge: z.ZodOptional<z.ZodBoolean>;
|
|
255
255
|
}, "strict", z.ZodTypeAny, {
|
package/dist/policy/loader.js
CHANGED
|
@@ -231,10 +231,24 @@ const GatewayPolicySchema = z
|
|
|
231
231
|
* Stricter validation is the consumer's job — RFC 5322 is too permissive
|
|
232
232
|
* for an opt-in audit footprint anyway.
|
|
233
233
|
*/
|
|
234
|
+
// 0.30.1 round-5 P2: the `name` value lands verbatim inside a
|
|
235
|
+
// `Co-Authored-By: <name> <email>` git trailer. A newline or carriage
|
|
236
|
+
// return in `name` would split the trailer across lines — git's
|
|
237
|
+
// `interpret-trailers` would drop the continuation and the augmenter
|
|
238
|
+
// could even inject arbitrary extra trailer lines. Reject any ASCII
|
|
239
|
+
// control character (newline, CR, tab, NUL, …) in `name`.
|
|
240
|
+
const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
234
241
|
const AttributionCoAuthorSchema = z
|
|
235
242
|
.object({
|
|
236
243
|
enabled: z.boolean().optional(),
|
|
237
|
-
name: z
|
|
244
|
+
name: z
|
|
245
|
+
.string()
|
|
246
|
+
.refine((v) => !CONTROL_CHAR_RE.test(v), {
|
|
247
|
+
message: 'attribution.co_author.name must not contain control characters ' +
|
|
248
|
+
'(newlines, tabs, carriage returns) — the value is written verbatim ' +
|
|
249
|
+
'into a single-line Co-Authored-By git trailer.',
|
|
250
|
+
})
|
|
251
|
+
.optional(),
|
|
238
252
|
email: z
|
|
239
253
|
.string()
|
|
240
254
|
.regex(/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.1",
|
|
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)",
|
|
@@ -88,12 +88,31 @@ rea_invoke() {
|
|
|
88
88
|
ENABLED=$(rea_invoke hook policy-get attribution.co_author.enabled 2>/dev/null)
|
|
89
89
|
REA_RC=$?
|
|
90
90
|
|
|
91
|
+
# REA_RC interpretation:
|
|
92
|
+
# 0 — rea CLI ran and returned a value (or empty for an
|
|
93
|
+
# unset key). Use the CLI reads.
|
|
94
|
+
# non-zero — rea CLI unreachable (127 sentinel), too old to know
|
|
95
|
+
# `hook policy-get`, OR the policy YAML is unparseable.
|
|
96
|
+
# In every one of those cases the policy file ITSELF
|
|
97
|
+
# may still be valid block-form YAML, so fall back to
|
|
98
|
+
# the embedded python3 parser. The realistic invalid-
|
|
99
|
+
# config case — `enabled: true` with an empty name or
|
|
100
|
+
# email — is caught downstream by the `[ -z "$CO_NAME" ]`
|
|
101
|
+
# defense-in-depth guard, which exits 0 without
|
|
102
|
+
# augmenting regardless of which reader produced the
|
|
103
|
+
# values. (An earlier 0.30.1 revision fail-closed on
|
|
104
|
+
# non-127 exit codes; codex round 1 showed that
|
|
105
|
+
# regressed the supported stale-CLI / pre-`pnpm i` flow,
|
|
106
|
+
# because an old `rea` exits non-zero exactly like an
|
|
107
|
+
# unparseable policy — the two are indistinguishable by
|
|
108
|
+
# exit code.)
|
|
91
109
|
if [ "$REA_RC" = "0" ]; then
|
|
92
110
|
CO_NAME=$(rea_invoke hook policy-get attribution.co_author.name 2>/dev/null || printf '')
|
|
93
111
|
CO_EMAIL=$(rea_invoke hook policy-get attribution.co_author.email 2>/dev/null || printf '')
|
|
94
112
|
SKIP_MERGE=$(rea_invoke hook policy-get attribution.co_author.skip_merge 2>/dev/null || printf 'false')
|
|
95
113
|
elif command -v python3 >/dev/null 2>&1; then
|
|
96
|
-
# rea CLI unreachable — fall back to
|
|
114
|
+
# rea CLI unreachable / stale / policy unparseable — fall back to the
|
|
115
|
+
# Python block-form parser.
|
|
97
116
|
CO_AUTHOR_PARSE=$(python3 - "$POLICY_FILE" <<'PY' 2>/dev/null
|
|
98
117
|
import re
|
|
99
118
|
import sys
|