@bookedsolid/rea 0.10.3 → 0.11.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.
Files changed (74) hide show
  1. package/.husky/pre-push +22 -167
  2. package/agents/codex-adversarial.md +5 -3
  3. package/commands/codex-review.md +3 -5
  4. package/dist/audit/append.d.ts +7 -32
  5. package/dist/audit/append.js +7 -35
  6. package/dist/cli/audit.d.ts +0 -31
  7. package/dist/cli/audit.js +5 -74
  8. package/dist/cli/doctor.js +6 -16
  9. package/dist/cli/hook.d.ts +48 -0
  10. package/dist/cli/hook.js +127 -0
  11. package/dist/cli/index.js +5 -80
  12. package/dist/cli/init.js +1 -1
  13. package/dist/cli/install/gitignore.d.ts +2 -2
  14. package/dist/cli/install/gitignore.js +3 -3
  15. package/dist/cli/install/pre-push.d.ts +146 -271
  16. package/dist/cli/install/pre-push.js +471 -2633
  17. package/dist/cli/install/settings-merge.d.ts +17 -0
  18. package/dist/cli/install/settings-merge.js +48 -1
  19. package/dist/cli/upgrade.js +131 -3
  20. package/dist/config/tier-map.js +18 -25
  21. package/dist/hooks/push-gate/base.d.ts +57 -0
  22. package/dist/hooks/push-gate/base.js +77 -0
  23. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  24. package/dist/hooks/push-gate/codex-runner.js +223 -0
  25. package/dist/hooks/push-gate/findings.d.ts +68 -0
  26. package/dist/hooks/push-gate/findings.js +142 -0
  27. package/dist/hooks/push-gate/halt.d.ts +28 -0
  28. package/dist/hooks/push-gate/halt.js +49 -0
  29. package/dist/hooks/push-gate/index.d.ts +90 -0
  30. package/dist/hooks/push-gate/index.js +351 -0
  31. package/dist/hooks/push-gate/policy.d.ts +41 -0
  32. package/dist/hooks/push-gate/policy.js +55 -0
  33. package/dist/hooks/push-gate/report.d.ts +89 -0
  34. package/dist/hooks/push-gate/report.js +140 -0
  35. package/dist/policy/loader.d.ts +10 -10
  36. package/dist/policy/loader.js +7 -6
  37. package/dist/policy/types.d.ts +31 -22
  38. package/package.json +1 -1
  39. package/dist/cache/review-cache.d.ts +0 -115
  40. package/dist/cache/review-cache.js +0 -200
  41. package/dist/cli/cache.d.ts +0 -84
  42. package/dist/cli/cache.js +0 -150
  43. package/dist/hooks/review-gate/args.d.ts +0 -126
  44. package/dist/hooks/review-gate/args.js +0 -315
  45. package/dist/hooks/review-gate/audit.d.ts +0 -131
  46. package/dist/hooks/review-gate/audit.js +0 -181
  47. package/dist/hooks/review-gate/banner.d.ts +0 -97
  48. package/dist/hooks/review-gate/banner.js +0 -172
  49. package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
  50. package/dist/hooks/review-gate/base-resolve.js +0 -247
  51. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  52. package/dist/hooks/review-gate/cache-key.js +0 -41
  53. package/dist/hooks/review-gate/cache.d.ts +0 -108
  54. package/dist/hooks/review-gate/cache.js +0 -120
  55. package/dist/hooks/review-gate/constants.d.ts +0 -26
  56. package/dist/hooks/review-gate/constants.js +0 -34
  57. package/dist/hooks/review-gate/diff.d.ts +0 -181
  58. package/dist/hooks/review-gate/diff.js +0 -232
  59. package/dist/hooks/review-gate/errors.d.ts +0 -72
  60. package/dist/hooks/review-gate/errors.js +0 -100
  61. package/dist/hooks/review-gate/hash.d.ts +0 -43
  62. package/dist/hooks/review-gate/hash.js +0 -46
  63. package/dist/hooks/review-gate/index.d.ts +0 -31
  64. package/dist/hooks/review-gate/index.js +0 -35
  65. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  66. package/dist/hooks/review-gate/metadata.js +0 -158
  67. package/dist/hooks/review-gate/policy.d.ts +0 -55
  68. package/dist/hooks/review-gate/policy.js +0 -71
  69. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  70. package/dist/hooks/review-gate/protected-paths.js +0 -76
  71. package/hooks/_lib/push-review-core.sh +0 -1250
  72. package/hooks/commit-review-gate.sh +0 -330
  73. package/hooks/push-review-gate-git.sh +0 -94
  74. package/hooks/push-review-gate.sh +0 -92
@@ -1,131 +0,0 @@
1
- /**
2
- * Audit-record emission + consumption for the review gate.
3
- *
4
- * ## Responsibilities
5
- *
6
- * 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
7
- * existing `appendAuditRecord()` helper. These are NEVER forgeable-
8
- * verdict records (the push-review gate never consults them as Codex
9
- * receipts), so they intentionally go through the `"other"`-stamped
10
- * public path rather than the `"rea-cli"` dedicated writer.
11
- *
12
- * 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
13
- * certifying a given `head_sha`. This is the TS equivalent of the
14
- * bash core's `jq -R 'fromjson? | select(...)'` predicate
15
- * (push-review-core.sh §959-966).
16
- *
17
- * ## Defect carry-forwards
18
- *
19
- * - **Defect P** (forgery rejection). The scan filter requires
20
- * `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
21
- * `appendAuditRecord()` helper stamps `"other"`; only the dedicated
22
- * `appendCodexReviewAuditRecord()` helper and the Codex CLI write
23
- * `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
24
- * or missing the field entirely are rejected here.
25
- *
26
- * - **Defect U** (streaming-parse tolerance). Every line in `.rea/
27
- * audit.jsonl` is parsed independently in a try/catch. A single
28
- * corrupt line mid-file does NOT abort the scan — later lines still
29
- * get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
30
- * first unparseable line and miss every subsequent legitimate record.
31
- *
32
- * - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
33
- * satisfy the protected-path gate. `blocking` and `error` verdicts are
34
- * receipts that a review HAPPENED but with a negative outcome, which
35
- * does NOT unblock the push. Mirrors push-review-core.sh §964.
36
- */
37
- import { type AuditRecord } from '../../audit/append.js';
38
- import { type OsIdentity } from './metadata.js';
39
- /** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
40
- export declare const PUSH_REVIEW_SKIPPED_TOOL = "push.review.skipped";
41
- export declare const CODEX_REVIEW_SKIPPED_TOOL = "codex.review.skipped";
42
- export declare const PUSH_REVIEW_CACHE_HIT_TOOL = "push.review.cache.hit";
43
- export declare const PUSH_REVIEW_CACHE_ERROR_TOOL = "push.review.cache.error";
44
- /** Server-names for the emit paths — carry forward from bash §473/§639. */
45
- export declare const ESCAPE_HATCH_SERVER = "rea.escape_hatch";
46
- export declare const PUSH_REVIEW_SERVER = "rea.push_review";
47
- /**
48
- * Input shape for the `REA_SKIP_PUSH_REVIEW` escape hatch's audit record.
49
- *
50
- * The `os_identity` field is captured inside this module (not by the
51
- * caller) so every emitter gets the same shape and failing fields degrade
52
- * to empty strings uniformly. The pid/ppid numeric-not-string invariant
53
- * (defect M) is enforced by `metadata.ts`.
54
- */
55
- export interface SkipPushReviewAuditInput {
56
- /** Repo root (the dir containing `.rea/`). */
57
- baseDir: string;
58
- /** `HEAD` SHA at the time of the skip. */
59
- head_sha: string;
60
- /** Current branch or empty string. */
61
- branch: string;
62
- /** The non-empty value of `REA_SKIP_PUSH_REVIEW` (the reason). */
63
- reason: string;
64
- /** The resolved git actor (email, then name, else empty). */
65
- actor: string;
66
- /**
67
- * OS-identity fields. Optional — when absent, `collectOsIdentity()` runs
68
- * and fills them. Tests inject a deterministic stub for snapshot stability.
69
- */
70
- os_identity?: OsIdentity;
71
- }
72
- /**
73
- * Emit the `push.review.skipped` audit record. Wraps the public
74
- * `appendAuditRecord()` helper — emission_source lands as `"other"`.
75
- *
76
- * The skipped record is intentionally NOT a `codex.review` receipt: the
77
- * push-review cache-gate scan rejects any record whose `tool_name` is not
78
- * `codex.review` AND any record whose `emission_source` is not
79
- * `rea-cli` / `codex-cli`. So this record is on the hash chain as
80
- * forensic evidence but cannot be confused with a real Codex review.
81
- */
82
- export declare function emitPushReviewSkipped(input: SkipPushReviewAuditInput): Promise<AuditRecord>;
83
- /**
84
- * Input shape for the `REA_SKIP_CODEX_REVIEW` (Codex-only) waiver.
85
- *
86
- * `metadata_source` records whether the skip metadata came from the
87
- * pre-push stdin (`"prepush-stdin"`) or from a local HEAD fallback
88
- * (`"local-fallback"`). Bash-core §594+§606.
89
- */
90
- export interface SkipCodexReviewAuditInput {
91
- baseDir: string;
92
- head_sha: string;
93
- target: string;
94
- reason: string;
95
- actor: string;
96
- metadata_source: 'prepush-stdin' | 'local-fallback';
97
- }
98
- export declare function emitCodexReviewSkipped(input: SkipCodexReviewAuditInput): Promise<AuditRecord>;
99
- /**
100
- * Predicate: does this parsed JSON object qualify as a valid
101
- * `codex.review` receipt for the given `head_sha`?
102
- *
103
- * Exported for unit tests; callers should usually use
104
- * `hasValidCodexReview()` below.
105
- */
106
- export declare function isQualifyingCodexReview(record: unknown, head_sha: string): boolean;
107
- /**
108
- * Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
109
- * the given `head_sha`. Returns true as soon as one is found.
110
- *
111
- * ## Defect U tolerance
112
- *
113
- * Each line is parsed independently via `JSON.parse` inside try/catch. A
114
- * malformed line logs nothing and the scan continues. The bash fix in
115
- * 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
116
- * native JS.
117
- *
118
- * ## Path safety
119
- *
120
- * The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
121
- * in from the caller and is the same resolved path used everywhere else.
122
- * No caller-supplied path segments.
123
- *
124
- * ## Missing file
125
- *
126
- * ENOENT resolves to `false` (no receipt exists yet). Any other error
127
- * propagates — the caller's policy is to fail-closed, and a permission
128
- * error on the audit file is a distinct operational concern the caller
129
- * should surface rather than silently mask as "no receipt".
130
- */
131
- export declare function hasValidCodexReview(baseDir: string, head_sha: string): Promise<boolean>;
@@ -1,181 +0,0 @@
1
- /**
2
- * Audit-record emission + consumption for the review gate.
3
- *
4
- * ## Responsibilities
5
- *
6
- * 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
7
- * existing `appendAuditRecord()` helper. These are NEVER forgeable-
8
- * verdict records (the push-review gate never consults them as Codex
9
- * receipts), so they intentionally go through the `"other"`-stamped
10
- * public path rather than the `"rea-cli"` dedicated writer.
11
- *
12
- * 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
13
- * certifying a given `head_sha`. This is the TS equivalent of the
14
- * bash core's `jq -R 'fromjson? | select(...)'` predicate
15
- * (push-review-core.sh §959-966).
16
- *
17
- * ## Defect carry-forwards
18
- *
19
- * - **Defect P** (forgery rejection). The scan filter requires
20
- * `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
21
- * `appendAuditRecord()` helper stamps `"other"`; only the dedicated
22
- * `appendCodexReviewAuditRecord()` helper and the Codex CLI write
23
- * `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
24
- * or missing the field entirely are rejected here.
25
- *
26
- * - **Defect U** (streaming-parse tolerance). Every line in `.rea/
27
- * audit.jsonl` is parsed independently in a try/catch. A single
28
- * corrupt line mid-file does NOT abort the scan — later lines still
29
- * get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
30
- * first unparseable line and miss every subsequent legitimate record.
31
- *
32
- * - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
33
- * satisfy the protected-path gate. `blocking` and `error` verdicts are
34
- * receipts that a review HAPPENED but with a negative outcome, which
35
- * does NOT unblock the push. Mirrors push-review-core.sh §964.
36
- */
37
- import fs from 'node:fs/promises';
38
- import path from 'node:path';
39
- import { appendAuditRecord, InvocationStatus, Tier, } from '../../audit/append.js';
40
- import { collectOsIdentity } from './metadata.js';
41
- /** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
42
- export const PUSH_REVIEW_SKIPPED_TOOL = 'push.review.skipped';
43
- export const CODEX_REVIEW_SKIPPED_TOOL = 'codex.review.skipped';
44
- export const PUSH_REVIEW_CACHE_HIT_TOOL = 'push.review.cache.hit';
45
- export const PUSH_REVIEW_CACHE_ERROR_TOOL = 'push.review.cache.error';
46
- /** Server-names for the emit paths — carry forward from bash §473/§639. */
47
- export const ESCAPE_HATCH_SERVER = 'rea.escape_hatch';
48
- export const PUSH_REVIEW_SERVER = 'rea.push_review';
49
- /**
50
- * Emit the `push.review.skipped` audit record. Wraps the public
51
- * `appendAuditRecord()` helper — emission_source lands as `"other"`.
52
- *
53
- * The skipped record is intentionally NOT a `codex.review` receipt: the
54
- * push-review cache-gate scan rejects any record whose `tool_name` is not
55
- * `codex.review` AND any record whose `emission_source` is not
56
- * `rea-cli` / `codex-cli`. So this record is on the hash chain as
57
- * forensic evidence but cannot be confused with a real Codex review.
58
- */
59
- export async function emitPushReviewSkipped(input) {
60
- const osIdentity = input.os_identity ?? collectOsIdentity();
61
- const metadata = {
62
- head_sha: input.head_sha,
63
- branch: input.branch,
64
- reason: input.reason,
65
- actor: input.actor,
66
- verdict: 'skipped',
67
- os_identity: osIdentity,
68
- };
69
- const record = {
70
- tool_name: PUSH_REVIEW_SKIPPED_TOOL,
71
- server_name: ESCAPE_HATCH_SERVER,
72
- status: InvocationStatus.Allowed,
73
- tier: Tier.Read,
74
- metadata,
75
- };
76
- return appendAuditRecord(input.baseDir, record);
77
- }
78
- export async function emitCodexReviewSkipped(input) {
79
- const metadata = {
80
- head_sha: input.head_sha,
81
- target: input.target,
82
- reason: input.reason,
83
- actor: input.actor,
84
- verdict: 'skipped',
85
- files_changed: null,
86
- metadata_source: input.metadata_source,
87
- };
88
- const record = {
89
- tool_name: CODEX_REVIEW_SKIPPED_TOOL,
90
- server_name: ESCAPE_HATCH_SERVER,
91
- status: InvocationStatus.Allowed,
92
- tier: Tier.Read,
93
- metadata,
94
- };
95
- return appendAuditRecord(input.baseDir, record);
96
- }
97
- /** Verdicts that satisfy the protected-path Codex-receipt gate. */
98
- const ACCEPTABLE_VERDICTS = new Set(['pass', 'concerns']);
99
- /** Emission sources that satisfy the protected-path Codex-receipt gate. */
100
- const ACCEPTABLE_SOURCES = new Set(['rea-cli', 'codex-cli']);
101
- /**
102
- * Predicate: does this parsed JSON object qualify as a valid
103
- * `codex.review` receipt for the given `head_sha`?
104
- *
105
- * Exported for unit tests; callers should usually use
106
- * `hasValidCodexReview()` below.
107
- */
108
- export function isQualifyingCodexReview(record, head_sha) {
109
- if (record === null || typeof record !== 'object')
110
- return false;
111
- const r = record;
112
- if (r.tool_name !== 'codex.review')
113
- return false;
114
- if (typeof r.emission_source !== 'string' || !ACCEPTABLE_SOURCES.has(r.emission_source)) {
115
- return false;
116
- }
117
- const md = r.metadata;
118
- if (md === null || md === undefined || typeof md !== 'object')
119
- return false;
120
- if (md.head_sha !== head_sha)
121
- return false;
122
- if (typeof md.verdict !== 'string' || !ACCEPTABLE_VERDICTS.has(md.verdict)) {
123
- return false;
124
- }
125
- return true;
126
- }
127
- /**
128
- * Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
129
- * the given `head_sha`. Returns true as soon as one is found.
130
- *
131
- * ## Defect U tolerance
132
- *
133
- * Each line is parsed independently via `JSON.parse` inside try/catch. A
134
- * malformed line logs nothing and the scan continues. The bash fix in
135
- * 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
136
- * native JS.
137
- *
138
- * ## Path safety
139
- *
140
- * The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
141
- * in from the caller and is the same resolved path used everywhere else.
142
- * No caller-supplied path segments.
143
- *
144
- * ## Missing file
145
- *
146
- * ENOENT resolves to `false` (no receipt exists yet). Any other error
147
- * propagates — the caller's policy is to fail-closed, and a permission
148
- * error on the audit file is a distinct operational concern the caller
149
- * should surface rather than silently mask as "no receipt".
150
- */
151
- export async function hasValidCodexReview(baseDir, head_sha) {
152
- const auditFile = path.join(baseDir, '.rea', 'audit.jsonl');
153
- let raw;
154
- try {
155
- raw = await fs.readFile(auditFile, 'utf8');
156
- }
157
- catch (err) {
158
- if (err.code === 'ENOENT')
159
- return false;
160
- throw err;
161
- }
162
- if (raw.length === 0)
163
- return false;
164
- // Walk lines. Each line is independently parsed; a corrupt line is
165
- // silently skipped. A matching record short-circuits the scan.
166
- for (const line of raw.split('\n')) {
167
- if (line.length === 0)
168
- continue;
169
- let parsed;
170
- try {
171
- parsed = JSON.parse(line);
172
- }
173
- catch {
174
- // Defect U tolerance — move on.
175
- continue;
176
- }
177
- if (isQualifyingCodexReview(parsed, head_sha))
178
- return true;
179
- }
180
- return false;
181
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Operator-facing banner composition.
3
- *
4
- * The bash core builds its banners via `printf` inside a `{ ... } >&2`
5
- * block, counting diff lines with `grep -cE ...` and file changes with
6
- * `grep -c '^+++ '`. Defect K (rea#62) surfaced because `grep -c`
7
- * emits `0` to stdout AND exits non-zero on no-match, and the bash author
8
- * wrote `$(grep -c ... || echo 0)` — which emitted `0\n0` when the pipe
9
- * produced no matches. The fix in the bash core was `|| true` + default
10
- * via `${LINE_COUNT:-0}`.
11
- *
12
- * The TS port closes this entire class of bug: counting happens over an
13
- * actual string in Node, not via a pipe-on-a-side-effect. The only way
14
- * LINE_COUNT / FILE_COUNT can ever be wrong now is a test-missed edge in
15
- * `countChangedLines` or `countChangedFiles` — unit tests in `banner.test.ts`
16
- * cover the zero case, the empty-diff case, the unicode-filename case, and
17
- * the `+++ b/-file` edge explicitly.
18
- *
19
- * ## Format parity
20
- *
21
- * `renderPushReviewRequiredBanner` reproduces the byte-exact output of the
22
- * bash core's "PUSH REVIEW GATE: Review required..." block, including the
23
- * cache-disabled fallback branch. A fixture test in `banner.test.ts` asserts
24
- * the output against a snapshot captured from the 0.10.1 bash core.
25
- */
26
- export interface DiffStats {
27
- /** Number of `^\+[^+]|^-[^-]` lines in the unified diff. */
28
- line_count: number;
29
- /** Number of `^\+\+\+ ` lines (one per changed file). */
30
- file_count: number;
31
- }
32
- /**
33
- * Count lines that begin with `+` followed by a non-`+` character, OR `-`
34
- * followed by a non-`-` character. Bash-core parity (push-review-core.sh
35
- * §1082): `grep -cE '^\+[^+]|^-[^-]'`. This rejects every line whose
36
- * SECOND character is the same as the first — not just `+++`/`---`
37
- * headers, but also pathological `++foo` and `--bar` strings (which bash
38
- * did not count). Codex pass-1 on phase 1 flagged the earlier too-lax
39
- * char-1-only TS implementation that would have silently changed the
40
- * Scope: banner line count vs. bash and broken phase-4 byte compatibility.
41
- *
42
- * Empty input → 0. Bare `+` or `-` (single char line) → 0, same as bash
43
- * (the regex requires a second character).
44
- */
45
- export declare function countChangedLines(diff: string): number;
46
- /**
47
- * Count `^\+\+\+ ` header lines (one per file in the diff). Parity with
48
- * the bash core's `grep -c '^\+\+\+ '`.
49
- */
50
- export declare function countChangedFiles(diff: string): number;
51
- /**
52
- * Compute `{line_count, file_count}` over a diff string. Exposed separately
53
- * so callers can use just the stats without generating the full banner.
54
- */
55
- export declare function computeDiffStats(diff: string): DiffStats;
56
- export interface PushReviewRequiredBannerInput {
57
- /** The ref being pushed (e.g. `refs/heads/feature/foo` or `HEAD`). */
58
- source_ref: string;
59
- /** The source commit SHA (12 chars + rest; full sha expected). */
60
- source_sha: string;
61
- /** Target branch / base label (defect N completion surfaces here). */
62
- target_branch: string;
63
- /** Resolved merge-base SHA. */
64
- merge_base: string;
65
- /** Diff stats — pre-computed by `computeDiffStats`. */
66
- stats: DiffStats;
67
- /**
68
- * The sha256-of-diff cache key. When empty, the banner emits the
69
- * cache-disabled fallback branch (`Cache is DISABLED on this host`).
70
- */
71
- push_sha: string;
72
- /** Source branch name for the `rea cache set` hint. */
73
- source_branch: string;
74
- }
75
- /**
76
- * Compose the "PUSH REVIEW GATE: Review required before pushing" banner.
77
- * Output goes to stderr via the caller; this function is pure. Returns the
78
- * exact text the bash core would have printed (including trailing blank
79
- * line and spacing), so the fixture snapshot can be compared byte-exactly.
80
- */
81
- export declare function renderPushReviewRequiredBanner(input: PushReviewRequiredBannerInput): string;
82
- export interface ProtectedPathsBlockedBannerInput {
83
- source_ref: string;
84
- source_sha: string;
85
- }
86
- /**
87
- * Compose the "PUSH BLOCKED: protected paths changed — /codex-review
88
- * required" banner. Pure; exit-2 translation happens in the CLI shim.
89
- */
90
- export declare function renderProtectedPathsBlockedBanner(input: ProtectedPathsBlockedBannerInput): string;
91
- /**
92
- * Strip C0 control characters (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) from a
93
- * string. Used when a banner embeds text from a subprocess's stderr (e.g.
94
- * the cache-check failure case). Mirrors the `LC_ALL=C tr -d` invocation
95
- * in the bash core's cache-error path. Codex LOW 5 on the 0.9.4 pass.
96
- */
97
- export declare function stripControlChars(input: string): string;
@@ -1,172 +0,0 @@
1
- /**
2
- * Operator-facing banner composition.
3
- *
4
- * The bash core builds its banners via `printf` inside a `{ ... } >&2`
5
- * block, counting diff lines with `grep -cE ...` and file changes with
6
- * `grep -c '^+++ '`. Defect K (rea#62) surfaced because `grep -c`
7
- * emits `0` to stdout AND exits non-zero on no-match, and the bash author
8
- * wrote `$(grep -c ... || echo 0)` — which emitted `0\n0` when the pipe
9
- * produced no matches. The fix in the bash core was `|| true` + default
10
- * via `${LINE_COUNT:-0}`.
11
- *
12
- * The TS port closes this entire class of bug: counting happens over an
13
- * actual string in Node, not via a pipe-on-a-side-effect. The only way
14
- * LINE_COUNT / FILE_COUNT can ever be wrong now is a test-missed edge in
15
- * `countChangedLines` or `countChangedFiles` — unit tests in `banner.test.ts`
16
- * cover the zero case, the empty-diff case, the unicode-filename case, and
17
- * the `+++ b/-file` edge explicitly.
18
- *
19
- * ## Format parity
20
- *
21
- * `renderPushReviewRequiredBanner` reproduces the byte-exact output of the
22
- * bash core's "PUSH REVIEW GATE: Review required..." block, including the
23
- * cache-disabled fallback branch. A fixture test in `banner.test.ts` asserts
24
- * the output against a snapshot captured from the 0.10.1 bash core.
25
- */
26
- /**
27
- * Count lines that begin with `+` followed by a non-`+` character, OR `-`
28
- * followed by a non-`-` character. Bash-core parity (push-review-core.sh
29
- * §1082): `grep -cE '^\+[^+]|^-[^-]'`. This rejects every line whose
30
- * SECOND character is the same as the first — not just `+++`/`---`
31
- * headers, but also pathological `++foo` and `--bar` strings (which bash
32
- * did not count). Codex pass-1 on phase 1 flagged the earlier too-lax
33
- * char-1-only TS implementation that would have silently changed the
34
- * Scope: banner line count vs. bash and broken phase-4 byte compatibility.
35
- *
36
- * Empty input → 0. Bare `+` or `-` (single char line) → 0, same as bash
37
- * (the regex requires a second character).
38
- */
39
- export function countChangedLines(diff) {
40
- if (diff.length === 0)
41
- return 0;
42
- let n = 0;
43
- const lines = diff.split('\n');
44
- for (const line of lines) {
45
- if (line.length < 2)
46
- continue;
47
- const c0 = line.charCodeAt(0);
48
- const c1 = line.charCodeAt(1);
49
- // `+` = 43: match only when char-2 is NOT `+`.
50
- if (c0 === 43 && c1 !== 43) {
51
- n++;
52
- continue;
53
- }
54
- // `-` = 45: match only when char-2 is NOT `-`.
55
- if (c0 === 45 && c1 !== 45) {
56
- n++;
57
- continue;
58
- }
59
- // Any other leading char, including `++...` and `--...`, is skipped.
60
- }
61
- return n;
62
- }
63
- /**
64
- * Count `^\+\+\+ ` header lines (one per file in the diff). Parity with
65
- * the bash core's `grep -c '^\+\+\+ '`.
66
- */
67
- export function countChangedFiles(diff) {
68
- if (diff.length === 0)
69
- return 0;
70
- let n = 0;
71
- const lines = diff.split('\n');
72
- for (const line of lines) {
73
- if (line.startsWith('+++ '))
74
- n++;
75
- }
76
- return n;
77
- }
78
- /**
79
- * Compute `{line_count, file_count}` over a diff string. Exposed separately
80
- * so callers can use just the stats without generating the full banner.
81
- */
82
- export function computeDiffStats(diff) {
83
- return {
84
- line_count: countChangedLines(diff),
85
- file_count: countChangedFiles(diff),
86
- };
87
- }
88
- /**
89
- * Compose the "PUSH REVIEW GATE: Review required before pushing" banner.
90
- * Output goes to stderr via the caller; this function is pure. Returns the
91
- * exact text the bash core would have printed (including trailing blank
92
- * line and spacing), so the fixture snapshot can be compared byte-exactly.
93
- */
94
- export function renderPushReviewRequiredBanner(input) {
95
- const lines = [];
96
- lines.push('PUSH REVIEW GATE: Review required before pushing');
97
- lines.push('');
98
- lines.push(` Source ref: ${input.source_ref} (${input.source_sha.slice(0, 12)})`);
99
- lines.push(` Target: ${input.target_branch}`);
100
- lines.push(` Scope: ${input.stats.file_count} files changed, ${input.stats.line_count} lines`);
101
- lines.push('');
102
- lines.push(' Action required:');
103
- lines.push(` 1. Spawn a code-reviewer agent to review: git diff ${input.merge_base}..${input.source_sha}`);
104
- lines.push(' 2. Spawn a security-engineer agent for security review');
105
- if (input.push_sha.length > 0) {
106
- lines.push(' 3. After both pass, cache the result:');
107
- lines.push(` rea cache set ${input.push_sha} pass --branch ${input.source_branch} --base ${input.target_branch}`);
108
- }
109
- else {
110
- lines.push(' 3. Cache is DISABLED on this host (no sha256 hasher found).');
111
- lines.push(' After both reviews pass, bypass the push-review gate with:');
112
- lines.push(' REA_SKIP_PUSH_REVIEW="<reason>" git push ...');
113
- lines.push(' The bypass is audited as push.review.skipped — this is the');
114
- lines.push(' documented escape hatch when cache is unavailable.');
115
- lines.push(' To restore the cache path, install one of: sha256sum,');
116
- lines.push(' shasum (Perl Digest::SHA), or openssl.');
117
- }
118
- lines.push('');
119
- // bash `printf '%s\n'` with no trailing args adds a final newline; the
120
- // block renders terminal-ready. `lines.join('\n') + '\n'` reproduces
121
- // exactly that shape.
122
- return lines.join('\n') + '\n';
123
- }
124
- /**
125
- * Compose the "PUSH BLOCKED: protected paths changed — /codex-review
126
- * required" banner. Pure; exit-2 translation happens in the CLI shim.
127
- */
128
- export function renderProtectedPathsBlockedBanner(input) {
129
- const lines = [];
130
- lines.push(`PUSH BLOCKED: protected paths changed — /codex-review required for ${input.source_sha}`);
131
- lines.push('');
132
- lines.push(` Source ref: ${input.source_ref}`);
133
- lines.push(' Diff touches one of:');
134
- lines.push(' - src/gateway/middleware/');
135
- lines.push(' - hooks/');
136
- lines.push(' - .claude/hooks/');
137
- lines.push(' - src/policy/');
138
- lines.push(' - .github/workflows/');
139
- lines.push(' - .rea/');
140
- lines.push(' - .husky/');
141
- lines.push('');
142
- lines.push(` Run /codex-review against ${input.source_sha}, then retry the push.`);
143
- lines.push(' The codex-adversarial agent emits the required audit entry.');
144
- lines.push(' Only `pass` or `concerns` verdicts satisfy this gate.');
145
- lines.push('');
146
- return lines.join('\n') + '\n';
147
- }
148
- /**
149
- * Strip C0 control characters (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) from a
150
- * string. Used when a banner embeds text from a subprocess's stderr (e.g.
151
- * the cache-check failure case). Mirrors the `LC_ALL=C tr -d` invocation
152
- * in the bash core's cache-error path. Codex LOW 5 on the 0.9.4 pass.
153
- */
154
- export function stripControlChars(input) {
155
- let out = '';
156
- for (let i = 0; i < input.length; i++) {
157
- const c = input.charCodeAt(i);
158
- // Allow tab (9), LF (10), CR (13) — but not any other C0/C1 byte.
159
- if (c === 9 || c === 10 || c === 13) {
160
- out += input[i];
161
- continue;
162
- }
163
- if (c <= 0x1f)
164
- continue;
165
- if (c === 0x7f)
166
- continue;
167
- if (c >= 0x80 && c <= 0x9f)
168
- continue;
169
- out += input[i];
170
- }
171
- return out;
172
- }