@bookedsolid/rea 0.30.0 → 0.31.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 +20 -1
- package/dist/cli/audit-specialists.d.ts +106 -24
- package/dist/cli/audit-specialists.js +239 -64
- package/dist/cli/delegation-advisory.d.ts +161 -0
- package/dist/cli/delegation-advisory.js +433 -0
- package/dist/cli/doctor.d.ts +110 -39
- package/dist/cli/doctor.js +333 -98
- package/dist/cli/hook.d.ts +6 -0
- package/dist/cli/hook.js +13 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/install/settings-merge.js +25 -0
- package/dist/cli/roster.d.ts +119 -0
- package/dist/cli/roster.js +141 -0
- package/dist/config/settings-schema.d.ts +13 -1
- package/dist/config/settings-schema.js +13 -2
- package/dist/policy/loader.d.ts +24 -1
- package/dist/policy/loader.js +61 -1
- package/dist/policy/profiles.d.ts +23 -0
- package/dist/policy/profiles.js +16 -0
- package/dist/policy/types.d.ts +61 -0
- package/hooks/delegation-advisory.sh +162 -0
- package/package.json +1 -1
- package/profiles/bst-internal-no-codex.yaml +12 -0
- package/profiles/bst-internal.yaml +13 -0
- package/profiles/client-engagement.yaml +11 -0
- package/profiles/lit-wc.yaml +10 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +11 -0
- package/profiles/open-source.yaml +11 -0
- package/templates/prepare-commit-msg.husky.sh +20 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook: delegation-advisory.sh
|
|
3
|
+
# 0.31.0+ — delegation-telemetry completion (the *nudge*).
|
|
4
|
+
#
|
|
5
|
+
# Fires AFTER every write-class tool call. The settings.json matcher is
|
|
6
|
+
# `Bash|Edit|Write|MultiEdit|NotebookEdit`. Reads the Claude Code hook
|
|
7
|
+
# payload from stdin, pipes it to `rea hook delegation-advisory`, and
|
|
8
|
+
# exits 0.
|
|
9
|
+
#
|
|
10
|
+
# 0.29.0 shipped the delegation-telemetry *observability* layer
|
|
11
|
+
# (`delegation-capture.sh` + `rea audit specialists`). 0.31.0 closes the
|
|
12
|
+
# loop with the *nudge*: `rea hook delegation-advisory` maintains a
|
|
13
|
+
# per-session write-class counter and, the FIRST time that counter
|
|
14
|
+
# crosses `policy.delegation_advisory.threshold` while the session has
|
|
15
|
+
# recorded zero real delegation signals, prints a one-time stderr
|
|
16
|
+
# advisory ("this session has done a lot of work without delegating to
|
|
17
|
+
# a specialist").
|
|
18
|
+
#
|
|
19
|
+
# # Advisory, never gating
|
|
20
|
+
#
|
|
21
|
+
# This hook ALWAYS exits 0 (under normal operation). The advisory is a
|
|
22
|
+
# nudge — it never blocks a tool call. The ONLY non-zero exit is 2
|
|
23
|
+
# under HALT, to keep the kill-switch contract uniform with the rest of
|
|
24
|
+
# the hook tree.
|
|
25
|
+
#
|
|
26
|
+
# # Synchronous, NOT detached
|
|
27
|
+
#
|
|
28
|
+
# Unlike `delegation-capture.sh` (which backgrounds `rea hook
|
|
29
|
+
# delegation-signal` with `& disown` because the audit write must not
|
|
30
|
+
# block tool dispatch), this hook runs the CLI SYNCHRONOUSLY. The
|
|
31
|
+
# advisory text must reach the operator's stderr before the hook
|
|
32
|
+
# returns — backgrounding it would race the hook's own exit and the
|
|
33
|
+
# message could be lost or interleaved with the next tool call's
|
|
34
|
+
# output. The CLI is cheap on the hot path: below the threshold it
|
|
35
|
+
# only bumps an integer counter file and exits, no audit scan, no
|
|
36
|
+
# roster discovery.
|
|
37
|
+
#
|
|
38
|
+
# # CLI-resolution trust boundary
|
|
39
|
+
#
|
|
40
|
+
# Same 2-tier sandboxed resolution `delegation-capture.sh`,
|
|
41
|
+
# `protected-paths-bash-gate.sh`, and `blocked-paths-bash-gate.sh` use:
|
|
42
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side
|
|
43
|
+
# published artifact)
|
|
44
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (the rea repo's own
|
|
45
|
+
# dogfood install)
|
|
46
|
+
# PATH lookup is INTENTIONALLY OMITTED — agent-controlled $PATH would
|
|
47
|
+
# let a forged `rea` binary intercept this hook on every write-class
|
|
48
|
+
# tool call. A realpath sandbox check ensures the resolved CLI lives
|
|
49
|
+
# INSIDE realpath(CLAUDE_PROJECT_DIR) with an ancestor package.json
|
|
50
|
+
# declaring `@bookedsolid/rea`.
|
|
51
|
+
#
|
|
52
|
+
# Exit codes:
|
|
53
|
+
# 0 — always (under normal operation). Disabled-by-policy,
|
|
54
|
+
# below-threshold, already-fired, just-fired — all exit 0.
|
|
55
|
+
# 2 — HALT active.
|
|
56
|
+
|
|
57
|
+
set -uo pipefail
|
|
58
|
+
|
|
59
|
+
# 1. HALT check. Even though this hook is advisory, refusing to run
|
|
60
|
+
# while frozen matches the rest of the hook tree and keeps the
|
|
61
|
+
# kill-switch contract uniform.
|
|
62
|
+
# shellcheck source=_lib/halt-check.sh
|
|
63
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
64
|
+
check_halt
|
|
65
|
+
REA_ROOT=$(rea_root)
|
|
66
|
+
|
|
67
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
68
|
+
|
|
69
|
+
# 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
70
|
+
# PATH lookup is omitted on purpose (see header). Other install
|
|
71
|
+
# shapes silently drop the advisory — matching the bash-gate
|
|
72
|
+
# posture; the nudge is a convenience, not a security claim.
|
|
73
|
+
REA_ARGV=()
|
|
74
|
+
RESOLVED_CLI_PATH=""
|
|
75
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
76
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
77
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
78
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
79
|
+
# rea repo dogfood: the project IS @bookedsolid/rea.
|
|
80
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
81
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
85
|
+
# No rea CLI in scope — drop the advisory silently. This is the
|
|
86
|
+
# expected state during bootstrap (consumer ran `rea init` but
|
|
87
|
+
# hasn't installed the npm package yet) or in non-rea repos. A
|
|
88
|
+
# noisy stderr warning here would fire on every write-class tool
|
|
89
|
+
# call and drown legitimate output.
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# 3. Realpath sandbox check — mirrors delegation-capture.sh §3 and
|
|
94
|
+
# protected-paths-bash-gate.sh §6. The resolved CLI MUST live inside
|
|
95
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor package.json
|
|
96
|
+
# declaring `@bookedsolid/rea` as its `name`. Catches symlink-out
|
|
97
|
+
# attacks where an attacker writes
|
|
98
|
+
# node_modules/@bookedsolid/rea → /tmp/forged-tree.
|
|
99
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
100
|
+
# Node not on PATH — we can't verify the CLI shape. Fail safe by
|
|
101
|
+
# dropping the advisory (it is not a security claim; the rest of
|
|
102
|
+
# the Bash gate suite refuses on this path).
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
sandbox_check=$(node -e '
|
|
107
|
+
const fs = require("fs");
|
|
108
|
+
const path = require("path");
|
|
109
|
+
const cli = process.argv[1];
|
|
110
|
+
const projDir = process.argv[2];
|
|
111
|
+
let real, realProj;
|
|
112
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
113
|
+
process.stdout.write("bad:realpath");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
117
|
+
process.stdout.write("bad:realpath-proj");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const sep = path.sep;
|
|
121
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
122
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
123
|
+
process.stdout.write("bad:cli-escapes-project");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
// Walk up looking for package.json with the protected name.
|
|
127
|
+
let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
|
|
128
|
+
let found = false;
|
|
129
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
130
|
+
const pj = path.join(cur, "package.json");
|
|
131
|
+
if (fs.existsSync(pj)) {
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
134
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
135
|
+
} catch (e) { /* keep walking */ }
|
|
136
|
+
}
|
|
137
|
+
cur = path.dirname(cur);
|
|
138
|
+
}
|
|
139
|
+
if (!found) {
|
|
140
|
+
process.stdout.write("bad:no-rea-pkg-json");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write("ok");
|
|
144
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
145
|
+
|
|
146
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
147
|
+
# CLI failed the sandbox check — silent drop. The forensic
|
|
148
|
+
# breadcrumb in stderr is intentional but trimmed so this doesn't
|
|
149
|
+
# become spammy on every tool call.
|
|
150
|
+
printf 'rea: delegation-advisory skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 4. Read stdin and pipe to the CLI SYNCHRONOUSLY. The advisory must
|
|
155
|
+
# print before this hook returns — see the "Synchronous" note in
|
|
156
|
+
# the header. We pass CLAUDE_PROJECT_DIR through explicitly so the
|
|
157
|
+
# CLI resolves the same REA_ROOT this shim did. The CLI's own exit
|
|
158
|
+
# code is the hook's exit code: 0 normally, 2 under HALT (the CLI
|
|
159
|
+
# re-checks HALT itself for defense-in-depth).
|
|
160
|
+
INPUT=$(cat)
|
|
161
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-advisory
|
|
162
|
+
exit $?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
@@ -58,3 +58,15 @@ context_protection:
|
|
|
58
58
|
attribution:
|
|
59
59
|
co_author:
|
|
60
60
|
enabled: false
|
|
61
|
+
# 0.31.0 delegation-advisory nudge — enabled for bst-internal-no-codex.
|
|
62
|
+
# This is a bst-internal variant, so it inherits BST's delegation
|
|
63
|
+
# discipline: the delegation-advisory.sh PostToolUse hook emits a
|
|
64
|
+
# one-time stderr advisory when a session crosses `threshold`
|
|
65
|
+
# write-class tool calls (Bash/Edit/Write/MultiEdit/NotebookEdit)
|
|
66
|
+
# without dispatching a curated specialist. Advisory only — never
|
|
67
|
+
# blocks. `exempt_subagents` omitted → schema default applies
|
|
68
|
+
# (general-purpose, Explore, Plan, output-style-setup,
|
|
69
|
+
# statusline-setup don't count as real delegation).
|
|
70
|
+
delegation_advisory:
|
|
71
|
+
enabled: true
|
|
72
|
+
threshold: 25
|
|
@@ -67,3 +67,16 @@ architecture_review:
|
|
|
67
67
|
attribution:
|
|
68
68
|
co_author:
|
|
69
69
|
enabled: false
|
|
70
|
+
# 0.31.0 delegation-advisory nudge — enabled for bst-internal.
|
|
71
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
72
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
73
|
+
# (Bash/Edit/Write/MultiEdit/NotebookEdit) without dispatching a
|
|
74
|
+
# curated specialist. Advisory only — never blocks a tool call. BST's
|
|
75
|
+
# own delegation discipline (CLAUDE.md routes all non-trivial work
|
|
76
|
+
# through rea-orchestrator) is load-bearing, so the nudge ships on.
|
|
77
|
+
# `exempt_subagents` omitted → the schema default applies
|
|
78
|
+
# (general-purpose, Explore, Plan, output-style-setup, statusline-setup
|
|
79
|
+
# don't count as real delegation).
|
|
80
|
+
delegation_advisory:
|
|
81
|
+
enabled: true
|
|
82
|
+
threshold: 25
|
|
@@ -35,3 +35,14 @@ context_protection:
|
|
|
35
35
|
attribution:
|
|
36
36
|
co_author:
|
|
37
37
|
enabled: false
|
|
38
|
+
# 0.31.0 delegation-advisory nudge — disabled for client-engagement.
|
|
39
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
40
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
41
|
+
# without dispatching a curated specialist. Client projects vary too
|
|
42
|
+
# much in their delegation conventions to ship the nudge on by
|
|
43
|
+
# default — opt in per-repo via .rea/policy.yaml:
|
|
44
|
+
# delegation_advisory:
|
|
45
|
+
# enabled: true
|
|
46
|
+
# threshold: 25
|
|
47
|
+
delegation_advisory:
|
|
48
|
+
enabled: false
|
package/profiles/lit-wc.yaml
CHANGED
|
@@ -29,3 +29,13 @@ notification_channel: ''
|
|
|
29
29
|
attribution:
|
|
30
30
|
co_author:
|
|
31
31
|
enabled: false
|
|
32
|
+
# 0.31.0 delegation-advisory nudge — disabled for lit-wc.
|
|
33
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
34
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
35
|
+
# without dispatching a curated specialist. External profiles ship
|
|
36
|
+
# `enabled: false` — opt in per-repo via .rea/policy.yaml:
|
|
37
|
+
# delegation_advisory:
|
|
38
|
+
# enabled: true
|
|
39
|
+
# threshold: 25
|
|
40
|
+
delegation_advisory:
|
|
41
|
+
enabled: false
|
package/profiles/minimal.yaml
CHANGED
|
@@ -25,3 +25,14 @@ notification_channel: ''
|
|
|
25
25
|
attribution:
|
|
26
26
|
co_author:
|
|
27
27
|
enabled: false
|
|
28
|
+
# 0.31.0 delegation-advisory nudge — disabled for minimal.
|
|
29
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
30
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
31
|
+
# without dispatching a curated specialist. The minimal profile ships
|
|
32
|
+
# bare defaults — `enabled: false` keeps it opinion-free. Opt in
|
|
33
|
+
# per-repo via .rea/policy.yaml:
|
|
34
|
+
# delegation_advisory:
|
|
35
|
+
# enabled: true
|
|
36
|
+
# threshold: 25
|
|
37
|
+
delegation_advisory:
|
|
38
|
+
enabled: false
|
|
@@ -44,3 +44,14 @@ notification_channel: ''
|
|
|
44
44
|
attribution:
|
|
45
45
|
co_author:
|
|
46
46
|
enabled: false
|
|
47
|
+
# 0.31.0 delegation-advisory nudge — disabled for open-source-no-codex.
|
|
48
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
49
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
50
|
+
# without dispatching a curated specialist. "You should delegate more"
|
|
51
|
+
# is an opinion not every OSS team shares, so external profiles ship
|
|
52
|
+
# `enabled: false` — opt in per-repo via .rea/policy.yaml:
|
|
53
|
+
# delegation_advisory:
|
|
54
|
+
# enabled: true
|
|
55
|
+
# threshold: 25
|
|
56
|
+
delegation_advisory:
|
|
57
|
+
enabled: false
|
|
@@ -29,3 +29,14 @@ notification_channel: ''
|
|
|
29
29
|
attribution:
|
|
30
30
|
co_author:
|
|
31
31
|
enabled: false
|
|
32
|
+
# 0.31.0 delegation-advisory nudge — disabled for open-source.
|
|
33
|
+
# The delegation-advisory.sh PostToolUse hook emits a one-time stderr
|
|
34
|
+
# advisory when a session crosses `threshold` write-class tool calls
|
|
35
|
+
# without dispatching a curated specialist. "You should delegate more"
|
|
36
|
+
# is an opinion not every OSS team shares, so external profiles ship
|
|
37
|
+
# `enabled: false` — opt in per-repo via .rea/policy.yaml:
|
|
38
|
+
# delegation_advisory:
|
|
39
|
+
# enabled: true
|
|
40
|
+
# threshold: 25
|
|
41
|
+
delegation_advisory:
|
|
42
|
+
enabled: false
|
|
@@ -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
|