@bookedsolid/rea 0.31.0 → 0.33.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/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +60 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +124 -0
- package/dist/hooks/_lib/payload.js +245 -0
- package/dist/hooks/_lib/segments.d.ts +125 -0
- package/dist/hooks/_lib/segments.js +766 -0
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -0
- package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
- package/dist/hooks/dependency-audit-gate/index.js +294 -0
- package/dist/hooks/env-file-protection/index.d.ts +55 -0
- package/dist/hooks/env-file-protection/index.js +159 -0
- package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- package/templates/settings-protection.dogfood.patch +58 -0
|
@@ -1,124 +1,157 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: env-file-protection.sh
|
|
3
|
-
#
|
|
4
|
-
# Blocks commands that read .env* / .envrc files via shell text utilities.
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook env-file-protection`.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Pre-0.33.0 the gate's full body lived here as bash (124 LOC, the
|
|
6
|
+
# segment splitter + `source`/`cp` anchor patterns + utility-vs-.env
|
|
7
|
+
# co-occurrence check). The migration to the parser-backed Node binary
|
|
8
|
+
# moves all of that into `src/hooks/env-file-protection/index.ts`. This
|
|
9
|
+
# shim is the Claude Code dispatcher's view of the hook — it forwards
|
|
10
|
+
# stdin to the CLI and exits with whatever the CLI returns.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
12
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
13
|
+
# pass-through / no-match, exit 2 on HALT / .env access detected /
|
|
14
|
+
# malformed payload (fail-closed).
|
|
13
15
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# # CLI-resolution trust boundary
|
|
17
|
+
#
|
|
18
|
+
# Mirrors the 0.32.0 final shim shape (round-8 of the codex iteration
|
|
19
|
+
# on the three Phase 1 pilots). The resolved CLI MUST live INSIDE
|
|
20
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
21
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out and
|
|
22
|
+
# tarball-replacement attacks on the resolved CLI.
|
|
23
|
+
#
|
|
24
|
+
# # Fail-closed posture
|
|
25
|
+
#
|
|
26
|
+
# env-file-protection is a BLOCKING-tier gate — the pre-0.33.0 bash
|
|
27
|
+
# body refused on .env access without a compiled CLI. The early-exit
|
|
28
|
+
# branches (CLI missing, node missing, sandbox failed, version skew)
|
|
29
|
+
# fail closed AFTER the relevance pre-gate passes. Irrelevant Bash
|
|
30
|
+
# calls exit 0 regardless of CLI state.
|
|
17
31
|
|
|
18
32
|
set -uo pipefail
|
|
19
33
|
|
|
20
|
-
#
|
|
21
|
-
# grep that false-positives on commit messages mentioning `.env` (e.g.
|
|
22
|
-
# `git commit -m "stop reading .env via cat"`).
|
|
23
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
24
|
-
source "$(dirname "$0")/_lib/cmd-segments.sh"
|
|
25
|
-
|
|
26
|
-
INPUT=$(cat)
|
|
27
|
-
|
|
28
|
-
# ── Dependency check ──────────────────────────────────────────────────────────
|
|
29
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
30
|
-
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
31
|
-
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
32
|
-
exit 2
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# ── HALT check ────────────────────────────────────────────────────────────────
|
|
36
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
34
|
+
# 1. HALT check.
|
|
37
35
|
# shellcheck source=_lib/halt-check.sh
|
|
38
36
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
39
37
|
check_halt
|
|
40
38
|
REA_ROOT=$(rea_root)
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
# 2. Relevance pre-gate. Capture stdin + check for `.env` substring
|
|
43
|
+
# BEFORE any CLI/sandbox/probe work so unrelated Bash calls
|
|
44
|
+
# (`ls`, `pnpm test`, `git status`, …) exit 0 even when the CLI
|
|
45
|
+
# is missing/stale/sandboxed-out.
|
|
46
|
+
#
|
|
47
|
+
# 2026-05-15 codex round-2 P2 fix: the substring scan MUST run
|
|
48
|
+
# against `tool_input.command` ONLY, not the raw JSON payload —
|
|
49
|
+
# otherwise a benign `git commit -m "stop reading .env"` (where
|
|
50
|
+
# `.env` appears inside the commit message ARG, NOT as a file
|
|
51
|
+
# target) would hit the fail-closed branch on a fresh checkout
|
|
52
|
+
# where the CLI is unbuilt. Pre-fix the raw scan saw the substring
|
|
53
|
+
# inside the payload's "command" string-quoted body and refused.
|
|
54
|
+
#
|
|
55
|
+
# Strategy: extract `tool_input.command` via `jq` (already required
|
|
56
|
+
# by 5 other hooks; trust assumption is consistent). When `jq` is
|
|
57
|
+
# not installed, fall back to scanning the raw payload — the cost
|
|
58
|
+
# is the same over-trigger the bash original had, NOT a new
|
|
59
|
+
# regression. When `jq` IS installed (the common case), the
|
|
60
|
+
# pre-gate is field-scoped.
|
|
61
|
+
INPUT=$(cat)
|
|
62
|
+
RELEVANT=0
|
|
63
|
+
PROBE=""
|
|
64
|
+
if command -v jq >/dev/null 2>&1; then
|
|
65
|
+
PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
|
66
|
+
if printf '%s' "$PROBE" | grep -qE '\.env'; then
|
|
67
|
+
RELEVANT=1
|
|
68
|
+
fi
|
|
69
|
+
else
|
|
70
|
+
# jq-less fallback — match the pre-0.33.0 over-trigger posture.
|
|
71
|
+
if printf '%s' "$INPUT" | grep -qE '\.env'; then
|
|
72
|
+
RELEVANT=1
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
45
76
|
exit 0
|
|
46
77
|
fi
|
|
47
78
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
80
|
+
REA_ARGV=()
|
|
81
|
+
RESOLVED_CLI_PATH=""
|
|
82
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
83
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
84
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
85
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
86
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
87
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
88
|
+
fi
|
|
57
89
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
PATTERN_UTILITY='(cat|head|tail|less|more|grep|sed|awk|bat|strings|printf|xargs|tee|jq|python3?[[:space:]]+-c|ruby[[:space:]]+-e)[[:space:]]'
|
|
68
|
-
# Also catch: source/., cp (reads then writes elsewhere).
|
|
69
|
-
#
|
|
70
|
-
# 0.16.3 discord-ops Round 9 #4 fix: anchored on segment-start. Pre-fix
|
|
71
|
-
# `any_segment_matches` matched anywhere in the segment, so
|
|
72
|
-
# `git commit -m "fix: don't source .env files"` fired even though no
|
|
73
|
-
# real source-of-.env was happening — the trigger words appeared inside
|
|
74
|
-
# the quoted commit-message body. The patterns are command prefixes
|
|
75
|
-
# (`source PATH`, `. PATH`, `cp X PATH`), so segment-start anchoring is
|
|
76
|
-
# the correct shape.
|
|
77
|
-
PATTERN_SOURCE='(source|\.)[[:space:]]+[^;|&]*\.env'
|
|
78
|
-
PATTERN_CP_ENV='cp[[:space:]]+[^;|&]*\.env'
|
|
79
|
-
# .env* files or .envrc (direnv)
|
|
80
|
-
PATTERN_ENV_FILE='(\.env[a-zA-Z0-9._-]*|\.envrc)([[:space:]]|"|'"'"'|$)'
|
|
90
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
91
|
+
# Blocking-tier: fail closed. The pre-0.33.0 bash body enforced
|
|
92
|
+
# .env protection without a CLI. Refuse and tell the operator how
|
|
93
|
+
# to restore protection.
|
|
94
|
+
printf 'rea: env-file-protection cannot run — the rea CLI is not built.\n' >&2
|
|
95
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
96
|
+
printf 'This shim fails closed because the pre-0.33.0 bash body enforced .env protection without a CLI.\n' >&2
|
|
97
|
+
exit 2
|
|
98
|
+
fi
|
|
81
99
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# .env name in segment 2). Detection is fundamentally a same-segment
|
|
88
|
-
# co-occurrence property.
|
|
89
|
-
MATCHES_BOTH_SAME_SEGMENT=0
|
|
90
|
-
if any_segment_matches_both "$CMD" "$PATTERN_UTILITY" "$PATTERN_ENV_FILE"; then
|
|
91
|
-
MATCHES_BOTH_SAME_SEGMENT=1
|
|
100
|
+
# 4. Realpath sandbox check.
|
|
101
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
102
|
+
printf 'rea: env-file-protection cannot run — `node` is not on PATH.\n' >&2
|
|
103
|
+
printf 'Install Node 22+ (engines.node) to restore .env protection.\n' >&2
|
|
104
|
+
exit 2
|
|
92
105
|
fi
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
sandbox_check=$(node -e '
|
|
108
|
+
const fs = require("fs");
|
|
109
|
+
const path = require("path");
|
|
110
|
+
const cli = process.argv[1];
|
|
111
|
+
const projDir = process.argv[2];
|
|
112
|
+
let real, realProj;
|
|
113
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
114
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
117
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const sep = path.sep;
|
|
120
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
121
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
122
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
125
|
+
let found = false;
|
|
126
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
127
|
+
const pj = path.join(cur, "package.json");
|
|
128
|
+
if (fs.existsSync(pj)) {
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
131
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
132
|
+
} catch (e) { /* keep walking */ }
|
|
133
|
+
}
|
|
134
|
+
cur = path.dirname(cur);
|
|
135
|
+
}
|
|
136
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
137
|
+
process.stdout.write("ok");
|
|
138
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
139
|
+
|
|
140
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
141
|
+
printf 'rea: env-file-protection FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
107
142
|
exit 2
|
|
108
143
|
fi
|
|
109
144
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
printf ' Rule: Load credentials in code only, never via shell.\n'
|
|
118
|
-
printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
|
|
119
|
-
printf ' .env files must not be read via shell utilities in agent sessions.\n'
|
|
120
|
-
} >&2
|
|
145
|
+
# 5. Version-probe.
|
|
146
|
+
probe_out=$("${REA_ARGV[@]}" hook env-file-protection --help 2>&1)
|
|
147
|
+
probe_status=$?
|
|
148
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'env-file-protection'; then
|
|
149
|
+
printf 'rea: this shim requires the `rea hook env-file-protection` subcommand (introduced in 0.33.0).\n' >&2
|
|
150
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
151
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
121
152
|
exit 2
|
|
122
153
|
fi
|
|
123
154
|
|
|
124
|
-
|
|
155
|
+
# 6. Forward stdin (already captured up-front for the relevance gate).
|
|
156
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook env-file-protection
|
|
157
|
+
exit $?
|
|
@@ -1,65 +1,134 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
# pr-issue-link-gate.sh
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: pr-issue-link-gate.sh
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook pr-issue-link-gate`.
|
|
3
4
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash; the migration to
|
|
6
|
+
# the parser-backed Node binary moves the matching + advisory logic
|
|
7
|
+
# into `src/hooks/pr-issue-link-gate/index.ts`. This shim is the
|
|
8
|
+
# Claude Code dispatcher's view of the hook — it forwards stdin to the
|
|
9
|
+
# CLI and exits with whatever the CLI returns.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# advisory gives the agent an opportunity to add the link before proceeding.
|
|
11
|
+
# Behavioral contract is preserved byte-for-byte: ALWAYS exit 0 except
|
|
12
|
+
# under HALT (exit 2) or a malformed payload (exit 2, fail-closed).
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
-
# JSONL-only projects (no GitHub) are unaffected — gh is unavailable there.
|
|
14
|
+
# # CLI-resolution trust boundary
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# Codex round 1 P1 (2026-05-15): mirrors the realpath sandbox check
|
|
17
|
+
# from `delegation-advisory.sh` §3 and `protected-paths-bash-gate.sh`
|
|
18
|
+
# §6. The resolved CLI MUST live INSIDE realpath(CLAUDE_PROJECT_DIR)
|
|
19
|
+
# AND have an ancestor `package.json` whose `name` is
|
|
20
|
+
# `@bookedsolid/rea`. Pre-fix the shim executed
|
|
21
|
+
# `node_modules/@bookedsolid/rea/dist/cli/index.js` directly without
|
|
22
|
+
# realpathing the target, which would let an attacker who controlled
|
|
23
|
+
# `node_modules/@bookedsolid/rea` (symlink-out, postinstall script,
|
|
24
|
+
# tarball-replacement) ship forged review code that intercepts every
|
|
25
|
+
# Bash dispatch.
|
|
26
|
+
#
|
|
27
|
+
# Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
|
|
28
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
|
|
29
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
|
|
30
|
+
#
|
|
31
|
+
# When NO rea CLI is reachable through the sandboxed order, this hook
|
|
32
|
+
# falls through to allow (exit 0) — the advisory is a nudge, not a
|
|
33
|
+
# security claim. The bash-tier path gates fail-closed because they
|
|
34
|
+
# protect write surfaces; this gate only emits prose.
|
|
35
|
+
#
|
|
36
|
+
# # Version skew
|
|
37
|
+
#
|
|
38
|
+
# Codex round 1 P1 (2026-05-15): a fresh `rea init` against a stale
|
|
39
|
+
# `node_modules/@bookedsolid/rea` would deliver this 0.32.0 shim while
|
|
40
|
+
# the installed CLI lacks the `hook pr-issue-link-gate` subcommand —
|
|
41
|
+
# every Bash dispatch would then fail with `unknown command` (exit 1).
|
|
42
|
+
# Probe the subcommand's `--help` output before propagating the exit
|
|
43
|
+
# code; on probe failure, advise the operator to `pnpm install` and
|
|
44
|
+
# fall through silently so the workspace stays usable.
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
source "$(dirname "$0")/_lib/common.sh"
|
|
46
|
+
set -uo pipefail
|
|
22
47
|
|
|
48
|
+
# 1. HALT check. Even though the CLI re-checks for defense-in-depth,
|
|
49
|
+
# short-circuit here so we never spawn `node` while frozen.
|
|
50
|
+
# shellcheck source=_lib/halt-check.sh
|
|
51
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
23
52
|
check_halt
|
|
53
|
+
REA_ROOT=$(rea_root)
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
55
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
# 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
58
|
+
REA_ARGV=()
|
|
59
|
+
RESOLVED_CLI_PATH=""
|
|
60
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
61
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
62
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
63
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
64
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
65
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
30
66
|
fi
|
|
31
67
|
|
|
32
|
-
|
|
68
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
33
71
|
|
|
34
|
-
#
|
|
35
|
-
if !
|
|
72
|
+
# 3. Realpath sandbox check — mirrors delegation-advisory.sh §3.
|
|
73
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
36
74
|
exit 0
|
|
37
75
|
fi
|
|
38
76
|
|
|
39
|
-
|
|
77
|
+
sandbox_check=$(node -e '
|
|
78
|
+
const fs = require("fs");
|
|
79
|
+
const path = require("path");
|
|
80
|
+
const cli = process.argv[1];
|
|
81
|
+
const projDir = process.argv[2];
|
|
82
|
+
let real, realProj;
|
|
83
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
84
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
87
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const sep = path.sep;
|
|
90
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
91
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
92
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
95
|
+
let found = false;
|
|
96
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
97
|
+
const pj = path.join(cur, "package.json");
|
|
98
|
+
if (fs.existsSync(pj)) {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
101
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
102
|
+
} catch (e) { /* keep walking */ }
|
|
103
|
+
}
|
|
104
|
+
cur = path.dirname(cur);
|
|
105
|
+
}
|
|
106
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
107
|
+
process.stdout.write("ok");
|
|
108
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
40
109
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if echo "$COMMAND" | grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'; then
|
|
110
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
111
|
+
printf 'rea: pr-issue-link-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
44
112
|
exit 0
|
|
45
113
|
fi
|
|
46
114
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
printf '
|
|
54
|
-
printf '
|
|
55
|
-
printf '
|
|
56
|
-
printf '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
printf ' resolves #N same effect\n' >&2
|
|
60
|
-
printf ' closes #N, closes #M closes multiple issues\n' >&2
|
|
61
|
-
printf '\n' >&2
|
|
62
|
-
printf 'If this is a chore, release, or hotfix PR with no upstream issue, you may proceed.\n' >&2
|
|
115
|
+
# 4. Version-probe: confirm the resolved CLI implements the
|
|
116
|
+
# `hook pr-issue-link-gate` subcommand. A stale node_modules from
|
|
117
|
+
# a fresh `rea init` against an older installed version would
|
|
118
|
+
# otherwise turn every Bash dispatch into a hard failure.
|
|
119
|
+
probe_out=$("${REA_ARGV[@]}" hook pr-issue-link-gate --help 2>&1)
|
|
120
|
+
probe_status=$?
|
|
121
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'pr-issue-link-gate'; then
|
|
122
|
+
printf 'rea: this shim requires the `rea hook pr-issue-link-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
123
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
124
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI to the version this shim expects.\n' >&2
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
63
127
|
|
|
64
|
-
#
|
|
65
|
-
|
|
128
|
+
# 5. Forward stdin to the CLI synchronously. The advisory text must
|
|
129
|
+
# reach the operator's stderr before this hook returns; the CLI's
|
|
130
|
+
# own exit code is the hook's exit code (0 normally, 2 under HALT
|
|
131
|
+
# or malformed payload).
|
|
132
|
+
INPUT=$(cat)
|
|
133
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook pr-issue-link-gate
|
|
134
|
+
exit $?
|