@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.
@@ -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.30.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
@@ -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
@@ -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 Python block-form parser.
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