@bookedsolid/rea 0.10.3 → 0.12.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 (77) hide show
  1. package/.husky/pre-push +48 -162
  2. package/README.md +834 -552
  3. package/agents/codex-adversarial.md +5 -3
  4. package/commands/codex-review.md +3 -5
  5. package/dist/audit/append.d.ts +7 -32
  6. package/dist/audit/append.js +7 -35
  7. package/dist/cli/audit.d.ts +0 -31
  8. package/dist/cli/audit.js +5 -74
  9. package/dist/cli/doctor.d.ts +12 -0
  10. package/dist/cli/doctor.js +96 -17
  11. package/dist/cli/hook.d.ts +55 -0
  12. package/dist/cli/hook.js +138 -0
  13. package/dist/cli/index.js +5 -80
  14. package/dist/cli/init.js +1 -1
  15. package/dist/cli/install/gitignore.d.ts +2 -2
  16. package/dist/cli/install/gitignore.js +3 -3
  17. package/dist/cli/install/pre-push.d.ts +158 -272
  18. package/dist/cli/install/pre-push.js +491 -2633
  19. package/dist/cli/install/settings-merge.d.ts +17 -0
  20. package/dist/cli/install/settings-merge.js +48 -1
  21. package/dist/cli/upgrade.js +131 -3
  22. package/dist/config/tier-map.js +18 -25
  23. package/dist/hooks/push-gate/base.d.ts +104 -0
  24. package/dist/hooks/push-gate/base.js +198 -0
  25. package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
  26. package/dist/hooks/push-gate/codex-runner.js +223 -0
  27. package/dist/hooks/push-gate/findings.d.ts +68 -0
  28. package/dist/hooks/push-gate/findings.js +142 -0
  29. package/dist/hooks/push-gate/halt.d.ts +28 -0
  30. package/dist/hooks/push-gate/halt.js +49 -0
  31. package/dist/hooks/push-gate/index.d.ts +98 -0
  32. package/dist/hooks/push-gate/index.js +416 -0
  33. package/dist/hooks/push-gate/policy.d.ts +55 -0
  34. package/dist/hooks/push-gate/policy.js +64 -0
  35. package/dist/hooks/push-gate/report.d.ts +89 -0
  36. package/dist/hooks/push-gate/report.js +140 -0
  37. package/dist/policy/loader.d.ts +15 -10
  38. package/dist/policy/loader.js +8 -6
  39. package/dist/policy/types.d.ts +73 -22
  40. package/package.json +1 -1
  41. package/scripts/tarball-smoke.sh +7 -2
  42. package/dist/cache/review-cache.d.ts +0 -115
  43. package/dist/cache/review-cache.js +0 -200
  44. package/dist/cli/cache.d.ts +0 -84
  45. package/dist/cli/cache.js +0 -150
  46. package/dist/hooks/review-gate/args.d.ts +0 -126
  47. package/dist/hooks/review-gate/args.js +0 -315
  48. package/dist/hooks/review-gate/audit.d.ts +0 -131
  49. package/dist/hooks/review-gate/audit.js +0 -181
  50. package/dist/hooks/review-gate/banner.d.ts +0 -97
  51. package/dist/hooks/review-gate/banner.js +0 -172
  52. package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
  53. package/dist/hooks/review-gate/base-resolve.js +0 -247
  54. package/dist/hooks/review-gate/cache-key.d.ts +0 -55
  55. package/dist/hooks/review-gate/cache-key.js +0 -41
  56. package/dist/hooks/review-gate/cache.d.ts +0 -108
  57. package/dist/hooks/review-gate/cache.js +0 -120
  58. package/dist/hooks/review-gate/constants.d.ts +0 -26
  59. package/dist/hooks/review-gate/constants.js +0 -34
  60. package/dist/hooks/review-gate/diff.d.ts +0 -181
  61. package/dist/hooks/review-gate/diff.js +0 -232
  62. package/dist/hooks/review-gate/errors.d.ts +0 -72
  63. package/dist/hooks/review-gate/errors.js +0 -100
  64. package/dist/hooks/review-gate/hash.d.ts +0 -43
  65. package/dist/hooks/review-gate/hash.js +0 -46
  66. package/dist/hooks/review-gate/index.d.ts +0 -31
  67. package/dist/hooks/review-gate/index.js +0 -35
  68. package/dist/hooks/review-gate/metadata.d.ts +0 -98
  69. package/dist/hooks/review-gate/metadata.js +0 -158
  70. package/dist/hooks/review-gate/policy.d.ts +0 -55
  71. package/dist/hooks/review-gate/policy.js +0 -71
  72. package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
  73. package/dist/hooks/review-gate/protected-paths.js +0 -76
  74. package/hooks/_lib/push-review-core.sh +0 -1250
  75. package/hooks/commit-review-gate.sh +0 -330
  76. package/hooks/push-review-gate-git.sh +0 -94
  77. package/hooks/push-review-gate.sh +0 -92
@@ -1,335 +1,221 @@
1
1
  /**
2
- * G6 — Pre-push hook fallback installer.
2
+ * Pre-push hook installer (0.11.0 stateless push-gate).
3
3
  *
4
- * Ships alongside `commit-msg.ts` as a second-line defender for the
5
- * protected-path Codex audit gate. The primary path is `.husky/pre-push`,
6
- * which rea copies into the consumer's `.husky/` via the canonical copy
7
- * module. That file only runs when the consumer has husky active
8
- * (`core.hooksPath` points at `.husky/`). Consumers who have never run
9
- * `husky install`, or who have disabled husky entirely, would otherwise get
10
- * ZERO pre-push enforcement — and the protected-path gate is exactly the
11
- * thing we cannot let silently lapse.
4
+ * The 0.11.0 push-gate is a single 15-line shell stub that delegates to
5
+ * `rea hook push-gate` no structural parsing of a bash body, no audit-log
6
+ * grep, no cache lookup. This module writes that stub into the right
7
+ * location and refuses to stomp foreign hooks.
12
8
  *
13
- * The fallback writes a small shell script that `exec`s the same
14
- * `push-review-gate.sh` logic the Claude Code hook already runs. The gate
15
- * itself is shared — we do NOT duplicate its 700 lines.
9
+ * ## Install policy (decision tree)
16
10
  *
17
- * ## Install policy (decision tree, documented)
11
+ * 1. `core.hooksPath` unset (vanilla git):
12
+ * → Install `.git/hooks/pre-push` (via `git rev-parse --git-path`). The
13
+ * `.husky/pre-push` file is shipped by `rea init` as a source-of-truth
14
+ * copy but is not consulted by git unless `core.hooksPath=.husky` is
15
+ * set.
18
16
  *
19
- * Given a consumer repo, we must decide where (if anywhere) to install a
20
- * fallback `pre-push`:
17
+ * 2. `core.hooksPath=.husky` (typical Husky 9 install):
18
+ * Do NOT install the `.git/hooks/pre-push` fallback. `.husky/pre-push`
19
+ * is already rea's canonical gate and lives under the canonical copy
20
+ * module (see `src/cli/install/copy.ts`). `rea upgrade` refreshes it
21
+ * there.
21
22
  *
22
- * 1. `core.hooksPath` unset (vanilla git):
23
- * Install `.git/hooks/pre-push`. This is the only path git will fire.
24
- * `.husky/pre-push` sits on disk as a source-of-truth copy but is not
25
- * consulted by git directly.
23
+ * 3. `core.hooksPath` set to anything else, and a foreign pre-push lives
24
+ * under it:
25
+ * Leave it alone, warn the operator, let `rea doctor` flag the gap.
26
26
  *
27
- * 2. `core.hooksPath` set to a directory containing an EXECUTABLE,
28
- * governance-carrying `pre-push`:
29
- * → Do NOT install. A hook is "governance-carrying" when it either
30
- * carries our `FALLBACK_MARKER` (rea-managed) or execs / invokes
31
- * `.claude/hooks/push-review-gate.sh` (consumer-wired delegation).
32
- * This is the happy path for any project running husky 9+ that has
33
- * wired the gate.
27
+ * 4. `core.hooksPath` set but the target directory has no pre-push:
28
+ * Install the stub there — that's where git will look.
34
29
  *
35
- * 3. `core.hooksPath` set to a directory with a pre-push that is NOT
36
- * governance-carrying (wrong bits, unrelated script, lint-only husky
37
- * hook, directory, etc.):
38
- * Classify as foreign. Leave it alone, warn the user, and let
39
- * `rea doctor` downgrade the check to `warn` so the gap is visible.
30
+ * Idempotency: every install writes a stable marker header. Re-running
31
+ * `rea init` / `rea upgrade` refreshes files carrying the marker and
32
+ * NEVER overwrites a hook without one. The marker comparison is
33
+ * anchored at byte 0 (exact line after the shebang), not a substring
34
+ * match otherwise a comment or log output that happens to contain
35
+ * the marker text could cause a foreign hook to be reclassified as
36
+ * rea-managed and silently stomped.
40
37
  *
41
- * 4. `core.hooksPath` set to a directory WITHOUT a pre-push:
42
- * → Install into the configured hooksPath (as `pre-push`). This is the
43
- * "hooksPath is set but nothing lives there yet" case. The active
44
- * hook directory has changed; we install where git will actually look.
38
+ * ## Stub body
45
39
  *
46
- * Idempotency: every install writes a stable managed header
47
- * (`# rea:pre-push-fallback v1`). Re-running `rea init` detects the header
48
- * by ANCHORED match (exact second line after the shebang) and refreshes in
49
- * place; it NEVER overwrites a hook without our marker — if the consumer
50
- * has their own pre-push already, we warn and skip. Substring matches are
51
- * deliberately rejected: a consumer comment, a grep log, or copy-pasted
52
- * snippet containing the sentinel must not reclassify a foreign file as
53
- * rea-managed.
40
+ * The body is 15 lines of POSIX sh:
54
41
  *
55
- * ## Why not just rely on `.husky/pre-push`?
42
+ * - If `.rea/HALT` exists, print the reason and exit 1.
43
+ * - Otherwise `exec rea hook push-gate`, which runs `codex exec review`
44
+ * against the diff and exits 0/1/2 accordingly.
56
45
  *
57
- * Three concrete failure modes we saw during 0.2.x dogfooding:
58
- * - Consumer hasn't run `husky install` (fresh clone, pnpm hasn't run
59
- * postinstall yet, etc.). `.husky/pre-push` exists but git's hooksPath
60
- * still points at `.git/hooks/`. No enforcement.
61
- * - Consumer deliberately uses `core.hooksPath=./custom-hooks` with a
62
- * different tool. `.husky/pre-push` is dead weight.
63
- * - CI or release automation disables husky via `HUSKY=0`. Again, no
64
- * enforcement at push time.
65
- *
66
- * The protected-path Codex audit requirement is too important to let any
67
- * of those slip through silently. See THREAT_MODEL.md §Governance for the
68
- * full rationale.
46
+ * All real work lives in `src/hooks/push-gate/index.ts`. Keeping the
47
+ * shell body minimal means the only things that could regress are HALT
48
+ * detection and the exec path both trivially testable.
69
49
  */
70
50
  /**
71
- * Marker baked into every rea-installed fallback pre-push hook. Used for
72
- * idempotency: on re-run we refresh files carrying the marker and refuse
73
- * to touch anything that doesn't.
74
- *
75
- * Bump the version suffix whenever the embedded script semantics change so
76
- * upgrades can migrate old installs. Comparison is NOT a substring match —
77
- * see `isReaManagedFallback` for the anchored form required to classify
78
- * a file as rea-managed.
51
+ * Marker baked into every rea-installed fallback pre-push hook. Anchored on
52
+ * the second line of the file (immediately after the shebang) for
53
+ * classification. Bump the version suffix whenever the body semantics
54
+ * change so upgrades can migrate old installs cleanly.
55
+ *
56
+ * v3 0.12.0 BODY_TEMPLATE rewrite: positional-args dispatch via `case`
57
+ * arms with `set --`, removing the `exec $REA_BIN` word-splitting bug
58
+ * that broke pushes from repo paths containing spaces.
59
+ * v2 — 0.11.0 stateless push-gate body (no bash core, no audit grep).
60
+ * v1 — 0.10.x and prior, delegated to `.claude/hooks/push-review-gate.sh`.
79
61
  */
80
- export declare const FALLBACK_MARKER = "# rea:pre-push-fallback v1";
62
+ export declare const FALLBACK_MARKER = "# rea:pre-push-fallback v3";
63
+ /** Legacy v2 marker (0.11.x bodies). Refresh-on-upgrade. */
64
+ export declare const LEGACY_FALLBACK_MARKER_V2 = "# rea:pre-push-fallback v2";
65
+ /** Legacy v1 marker — used by upgrade migration to detect old installs. */
66
+ export declare const LEGACY_FALLBACK_MARKER_V1 = "# rea:pre-push-fallback v1";
81
67
  /**
82
- * Marker present in the shipped `.husky/pre-push` governance gate. Detection
83
- * requires the marker to appear on the SECOND LINE of the file (immediately
84
- * after the shebang) to prevent a consumer comment or copy-pasted snippet
85
- * that mentions the string from causing a foreign hook to be misclassified
86
- * as rea-managed and then silently overwritten. See `isReaManagedHuskyGate`
87
- * for the anchored check.
68
+ * Marker present in the shipped `.husky/pre-push` governance gate. The
69
+ * second line of the shipped husky hook is this marker rea upgrade
70
+ * detects it to refresh in-place. Bump the suffix whenever the body
71
+ * changes; pre-0.12 markers live in `LEGACY_HUSKY_GATE_MARKER_V{1,2}`.
88
72
  */
89
- export declare const HUSKY_GATE_MARKER = "# rea:husky-pre-push-gate v1";
73
+ export declare const HUSKY_GATE_MARKER = "# rea:husky-pre-push-gate v3";
74
+ /** Legacy v2 husky marker (0.11.x bodies). Refresh-on-upgrade. */
75
+ export declare const LEGACY_HUSKY_GATE_MARKER_V2 = "# rea:husky-pre-push-gate v2";
76
+ /** Legacy v1 husky marker for migration. */
77
+ export declare const LEGACY_HUSKY_GATE_MARKER_V1 = "# rea:husky-pre-push-gate v1";
90
78
  /**
91
- * Second versioned marker embedded in the body of the shipped `.husky/pre-push`.
92
- * Required alongside `HUSKY_GATE_MARKER` so that a hook containing only the
93
- * header marker + `exit 0` (or any stub body) is not classified as rea-managed.
94
- * A genuine rea Husky gate always carries both. The marker is versioned so it
95
- * can be bumped if the gate implementation changes significantly.
79
+ * Body-level marker so a hook that carries the header marker but has an
80
+ * empty body (stubbed out by a consumer) is NOT classified as rea-managed.
81
+ * A real rea hook always carries both markers.
96
82
  */
97
- export declare const HUSKY_GATE_BODY_MARKER = "# rea:gate-body-v1";
83
+ export declare const HUSKY_GATE_BODY_MARKER = "# rea:gate-body-v3";
84
+ /** Legacy v2 body marker (0.11.x bodies). Refresh-on-upgrade. */
85
+ export declare const LEGACY_HUSKY_GATE_BODY_MARKER_V2 = "# rea:gate-body-v2";
86
+ /** Legacy body marker — used by upgrade migration detection. */
87
+ export declare const LEGACY_HUSKY_GATE_BODY_MARKER_V1 = "# rea:gate-body-v1";
88
+ /** Fallback hook body — `.git/hooks/pre-push` in vanilla-git installs. */
89
+ export declare function fallbackHookContent(): string;
90
+ /** Husky hook body — `.husky/pre-push` when hooksPath=.husky. */
91
+ export declare function huskyHookContent(): string;
98
92
  /**
99
- * True when `content` starts with the exact rea fallback prelude. The
100
- * marker must appear as the second line, immediately after the shebang,
101
- * with no leading whitespace, no alternate shebang (`#!/usr/bin/env sh`),
102
- * and no interposed blank lines. Anything else is foreign.
103
- *
104
- * Rejecting a substring match is what stops a consumer comment like
105
- * `# Hint: the old rea:pre-push-fallback v1 marker moved into .husky/` from
106
- * accidentally classifying a user's own hook as rea-managed and then
107
- * getting overwritten on the next `rea init`.
93
+ * True when `content` starts with the exact rea fallback prelude (shebang
94
+ * + v2 marker). Strict: the marker must be on line 2, nothing interposed,
95
+ * no leading whitespace. Substring matches are deliberately rejected.
108
96
  */
109
97
  export declare function isReaManagedFallback(content: string): boolean;
110
98
  /**
111
- * True when `content` has the shipped Husky gate marker on the SECOND LINE
112
- * (immediately after the shebang). This is the canonical structure of the
113
- * rea-authored `.husky/pre-push` the shebang occupies line 1 and the marker
114
- * occupies line 2 with no intervening blank lines.
115
- *
116
- * Requiring line-2 placement prevents a consumer comment, copy-pasted snippet,
117
- * or any other text that merely *mentions* the marker string from reclassifying
118
- * a consumer-owned hook as rea-managed and triggering an overwrite on the next
119
- * `rea init`. A marker buried anywhere else in the file is not the canonical
120
- * structure and must not be trusted.
121
- *
122
- * This classification is checked BEFORE `isReaManagedFallback` in
123
- * `classifyExistingHook` so that the shipped `.husky/pre-push` is recognized
124
- * as a governance-carrying hook rather than `foreign/no-marker`.
99
+ * True when `content` is a legacy fallback hook authored by an earlier rea
100
+ * release: v2 (0.11.x broken `exec $REA_BIN` body) or v1 (0.10.x
101
+ * delegated to `.claude/hooks/push-review-gate.sh`). Used by `rea upgrade`
102
+ * to migrate we overwrite these unconditionally because we control the
103
+ * entire body shape.
104
+ */
105
+ export declare function isLegacyReaManagedFallback(content: string): boolean;
106
+ /**
107
+ * True when `content` carries the rea Husky gate markers in the canonical
108
+ * positions shebang on line 1, `HUSKY_GATE_MARKER` on line 2,
109
+ * `HUSKY_GATE_BODY_MARKER` on line 3.
110
+ *
111
+ * Why three anchored lines instead of a substring search: the 0.10.x
112
+ * implementation lived in ~2000 lines of structural parser because the old
113
+ * body varied. The 0.11.0 body is hand-templated and stable — anchored
114
+ * matching on three fixed lines closes the classification question with
115
+ * six comparisons.
125
116
  */
126
117
  export declare function isReaManagedHuskyGate(content: string): boolean;
127
118
  /**
128
- * Pre-0.4 rea-authored `.husky/pre-push` shape same governance behavior
129
- * as the current gate but lacks the line-2/3 versioned markers
130
- * (`# rea:husky-pre-push-gate v1` / `# rea:gate-body-v1`) introduced in
131
- * 0.4.
132
- *
133
- * Codex R21 F1: without this detector, any consumer upgrading from a rea
134
- * release that shipped the pre-marker hook fell into `foreign/no-marker`.
135
- * `classifyPrePushInstall` mapped that to `skip/foreign-pre-push` and
136
- * `rea init` refused to touch the file. `rea doctor` reported
137
- * `activeForeign=true`. Users had no self-heal path short of manually
138
- * deleting the hook — which is a bad migration story for a governance
139
- * primitive that they are supposed to trust.
140
- *
141
- * Shape-level detection:
142
- * 1. Line 2 is the canonical pre-0.4 filename header
143
- * `# .husky/pre-push — rea governance gate for terminal-initiated pushes.`
144
- * This header shipped verbatim across the 0.2.x/0.3.x rea releases.
145
- * 2. Real governance still present — `hasHaltEnforcement(content)` AND
146
- * `hasAuditCheck(content)` both pass. A stub that only matches the
147
- * header comment (no enforcement) fails the shape check and stays
148
- * classified as foreign.
149
- *
150
- * Classification consequence: `classifyExistingHook` returns
151
- * `rea-managed-husky` for legacy matches. `classifyPrePushInstall` maps
152
- * that to `skip/active-pre-push-present` — `rea init` does not touch the
153
- * hook (correctness: the file IS still functional governance), but
154
- * `inspectPrePushState` reports `ok=true, activeForeign=false` so doctor
155
- * stops flagging it. The canonical-manifest-driven upgrade path
156
- * (`rea upgrade`) detects the hash mismatch against the packaged
157
- * `.husky/pre-push` and surfaces the legacy shape as drift, letting the
158
- * operator opt into the refresh explicitly.
119
+ * True when `content` is a legacy Husky gate from an earlier rea release:
120
+ * v2 (0.11.x broken `exec $REA_BIN` body) or v1 (0.10.x — bash core
121
+ * delegating). Used to trigger the upgrade migration.
159
122
  */
160
123
  export declare function isLegacyReaManagedHuskyGate(content: string): boolean;
161
124
  /**
162
- * True when `content` contains a REAL shell invocation of
163
- * `push-review-gate.sh`. Used as a softer signal that a consumer-owned
164
- * pre-push still wires the shared gate (e.g. a husky 9 file that runs
165
- * lint AND execs the gate). Combined with "exists AND executable", a
166
- * gate-referencing foreign hook is a legitimate integration point —
167
- * doctor reports `pass`, install skips.
168
- *
169
- * Accepts (positive-match allowlist):
170
- * - Bare invocation: `.claude/hooks/push-review-gate.sh "$@"`
171
- * - POSIX exec keyword: `exec`, `.`, `sh`, `bash`, `zsh` followed by the
172
- * gate path. The bash-only `source` keyword is NOT accepted the POSIX
173
- * equivalent `.` (dot) is.
174
- * - Quoted/expanded path prefix: `exec "$REA_ROOT"/.claude/hooks/push-review-gate.sh "$@"`
175
- * — double- or single-quoted variable expansions before the literal path
176
- * are treated as part of the path, not as a mention context.
177
- * - Trailing `;` after `exec <gate>`: `exec gate.sh "$@";` — exec replaces
178
- * the shell, so the `;` and anything after it never runs; gate exit IS
179
- * the hook's exit status.
180
- * - Variable indirection: `GATE=<path-containing-gate>` on one line plus
181
- * `exec "$GATE"` / `. "$GATE"` / etc. on a later line.
182
- *
183
- * Rejects:
184
- * - Comment lines starting with `#`
185
- * - Shell tests: `[ -x .claude/hooks/push-review-gate.sh ]`
186
- * - File tests: `test -f .claude/hooks/push-review-gate.sh`
187
- * - Chmod / cp / mv / cat / printf / echo mentioning the path
188
- * - String literals inside quoted arguments to non-invocation commands
189
- * - Invocations inside `if`/`for`/`while`/`case` blocks (conditional —
190
- * not guaranteed to run)
191
- * - Invocations after an unconditional top-level `exit`
192
- * - Non-`exec` invocations followed by `||`, `&&`, `;`, or trailing `&`
193
- * (status-swallowing operators)
194
- *
195
- * This is a pragmatic heuristic, not a full shell parser. R12 F2 broadened
196
- * the allowlist to match the forms Codex flagged as valid but previously
197
- * rejected; narrower patterns silently hard-failed `rea doctor` on
198
- * correctly-governed consumer repos.
125
+ * True when `content` looks like a user-authored pre-push that still
126
+ * invokes `rea hook push-gate` (a legitimate governance-carrying custom
127
+ * hook). We don't attempt to parse control flow the 0.10.x attempt at
128
+ * that produced 800 lines of heuristics that still had gaps. Instead, we
129
+ * match only on the substring `rea hook push-gate` preceded by one of
130
+ * `exec`, `$(`, \``, `;`, or line-start whitespace. A comment containing
131
+ * the phrase does NOT qualify (leading `#`).
132
+ *
133
+ * Governance-carrying is a soft signal: `rea doctor` uses it to print
134
+ * "external (delegates to rea hook push-gate)" rather than "foreign".
135
+ * `classifyPrePushInstall` maps it to "skip / active-pre-push-present"
136
+ * we don't overwrite consumer-authored hooks that respect the gate.
199
137
  */
200
138
  export declare function referencesReviewGate(content: string): boolean;
201
- /**
202
- * Resolve a configured `core.hooksPath` (possibly relative) to an absolute
203
- * path relative to `targetDir`, or `null` if the key is unset.
204
- */
205
139
  export declare function resolveHooksDir(targetDir: string): Promise<{
206
140
  dir: string | null;
207
141
  configured: boolean;
208
142
  }>;
209
- export type InstallDecision =
210
- /** Active pre-push already present and governance-carrying. */
211
- {
143
+ export type ClassifyExistingHook = {
144
+ kind: 'absent';
145
+ } | {
146
+ kind: 'rea-managed';
147
+ } | {
148
+ kind: 'rea-managed-legacy-v1';
149
+ } | {
150
+ kind: 'rea-managed-husky';
151
+ } | {
152
+ kind: 'rea-managed-husky-legacy-v1';
153
+ } | {
154
+ kind: 'gate-delegating';
155
+ } | {
156
+ kind: 'foreign';
157
+ reason: string;
158
+ };
159
+ export declare function classifyExistingHook(hookPath: string): Promise<ClassifyExistingHook>;
160
+ export type InstallDecision = {
212
161
  action: 'skip';
213
162
  reason: 'active-pre-push-present';
214
163
  hookPath: string;
215
- }
216
- /** Consumer owns a non-rea pre-push; refusing to stomp it. */
217
- | {
164
+ } | {
218
165
  action: 'skip';
219
166
  reason: 'foreign-pre-push';
220
167
  hookPath: string;
221
- }
222
- /** Write a fresh hook. */
223
- | {
168
+ } | {
224
169
  action: 'install';
225
170
  hookPath: string;
226
- }
227
- /** Refresh an existing rea-managed hook (marker match). */
228
- | {
171
+ } | {
229
172
  action: 'refresh';
230
173
  hookPath: string;
231
174
  };
232
- /**
233
- * Classify what we should do at `targetDir` based on current state. Pure —
234
- * reads the filesystem and git config but performs no writes. Split out so
235
- * tests can drive every branch without going through the write path.
236
- *
237
- * NOTE: The result is a snapshot. `installPrePushFallback` re-resolves and
238
- * re-classifies immediately before writing to defend against a husky
239
- * install or concurrent `rea init` running between classify and write.
240
- */
241
175
  export declare function classifyPrePushInstall(targetDir: string): Promise<InstallDecision>;
242
176
  export interface PrePushInstallResult {
243
177
  decision: InstallDecision;
244
- /** Absolute path of the file written, if any. */
245
178
  written?: string;
246
- /** User-facing warnings accumulated during install. */
247
179
  warnings: string[];
248
180
  }
249
- export interface WriteExecutableResult {
250
- /**
251
- * R25 F2 — set to true when the install path had to use a non-atomic
252
- * fallback (copyFile after link() refused). Callers surface this as a
253
- * warning to the operator so they know publication was best-effort on
254
- * this filesystem rather than atomic.
255
- */
256
- degradedFromAtomic: boolean;
181
+ export interface InstallPrePushFallbackOptions {
182
+ targetDir: string;
257
183
  }
258
184
  /**
259
- * Options controlling `installPrePushFallback`. Exposed primarily for
260
- * tests production callers get sensible defaults.
185
+ * Install (or refresh) the `.git/hooks/pre-push` stub when there is no
186
+ * active governance-carrying hook. Never overwrites foreign hooks.
187
+ *
188
+ * Concurrency: a proper-lockfile-based advisory lock on the git common-dir
189
+ * serializes concurrent installs (two `rea init` runs in the same repo).
190
+ * Atomicity: fresh installs use `link(2)` (atomic create-or-fail); refresh
191
+ * uses `rename(2)` with a dev+ino+mtime guard against mid-write
192
+ * replacement.
261
193
  */
262
- export interface InstallPrePushOptions {
263
- /**
264
- * Serialize concurrent installs via an advisory lockfile under `.git/`.
265
- * Defaults to `true`. Tests that simulate concurrent races must keep
266
- * this on; the only reason to turn it off is unit-testing a specific
267
- * write branch in isolation.
268
- */
269
- useLock?: boolean;
270
- /**
271
- * Called exactly once inside the advisory lock, after classification
272
- * and before re-resolution + write. Test-only seam that lets a race
273
- * partner drop a file in between those two steps so we can assert on
274
- * the re-check behavior. Invoked with the classified target path.
275
- * Production callers never set this.
276
- */
277
- onBeforeReresolve?: (hookPath: string) => Promise<void> | void;
278
- /**
279
- * Called inside the lock, after the safety re-check passes but
280
- * immediately before `writeExecutable`. Test-only seam: creates a
281
- * file at the hook path to exercise the EEXIST-from-link path that
282
- * guards the remaining TOCTOU window. Production callers never set this.
283
- */
284
- onBeforeWrite?: (hookPath: string) => Promise<void> | void;
194
+ export declare function installPrePushFallback(options: InstallPrePushFallbackOptions): Promise<PrePushInstallResult>;
195
+ export interface PrePushCandidate {
196
+ path: string;
197
+ exists: boolean;
198
+ executable: boolean;
199
+ /** The marker classification of the file (if it exists). */
200
+ kind?: ClassifyExistingHook['kind'];
201
+ /** True when a rea-authored marker is present. */
202
+ reaManaged?: boolean;
203
+ /** True when the body contains `rea hook push-gate`. */
204
+ delegatesToGate?: boolean;
285
205
  }
286
- /**
287
- * Install (or refresh, or skip) the fallback pre-push hook at `targetDir`.
288
- * Idempotent: safe to call on every `rea init`, including re-runs over an
289
- * existing install. Never overwrites a foreign hook.
290
- *
291
- * Requires `targetDir/.git` to exist. Non-git directories are skipped with
292
- * a warning — same shape as `installCommitMsgHook`.
293
- */
294
- export declare function installPrePushFallback(targetDir: string, options?: InstallPrePushOptions): Promise<PrePushInstallResult>;
295
- /**
296
- * Doctor check: at least one pre-push hook (Husky OR git fallback OR the
297
- * configured hooksPath location) must exist AND be executable AND carry
298
- * governance (rea marker or gate delegation). Returns a small record the
299
- * doctor module can turn into a CheckResult.
300
- *
301
- * "Executable" is defined as having any of the user/group/other exec bits
302
- * set, matching the existing `checkHooksInstalled` convention. A file that
303
- * is executable but does not wire the Codex review gate is intentionally
304
- * classified as non-governing: `ok=false` + `activeForeign=true`, which
305
- * doctor turns into a `warn`, not a `pass`.
306
- */
307
206
  export interface PrePushDoctorState {
308
- /** Every candidate path we consulted, with its live status on disk. */
309
- candidates: Array<{
310
- path: string;
311
- exists: boolean;
312
- executable: boolean;
313
- /** `true` when the file content carries our anchored rea prelude. */
314
- reaManaged: boolean;
315
- /** `true` when the body references the shared review gate. */
316
- delegatesToGate: boolean;
317
- }>;
318
- /**
319
- * The candidate path git would actually fire right now, given current
320
- * `core.hooksPath`. May or may not exist.
321
- */
322
- activePath: string;
323
- /**
324
- * True when the active candidate exists, is executable, AND carries
325
- * governance (rea marker OR references the review gate).
326
- */
327
207
  ok: boolean;
208
+ activePath: string | null;
328
209
  /**
329
- * True when the active candidate exists + is executable but does NOT
330
- * carry governance. This is the "silent bypass" case doctor surfaces as
331
- * a warn. Distinct from `ok=false + absent` (which is a hard fail).
210
+ * Foreign file detected at the active path present + executable but
211
+ * neither rea-managed nor gate-delegating. Treated as a hard fail
212
+ * (silent-bypass risk).
332
213
  */
333
214
  activeForeign: boolean;
215
+ candidates: PrePushCandidate[];
334
216
  }
217
+ /**
218
+ * Read-only probe used by `rea doctor`. Inspects every plausible hook
219
+ * location and reports whether governance is active.
220
+ */
335
221
  export declare function inspectPrePushState(targetDir: string): Promise<PrePushDoctorState>;