@bookedsolid/rea 0.30.1 → 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/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 +302 -90
- 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/policy/loader.d.ts +23 -0
- package/dist/policy/loader.js +46 -0
- 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/dist/cli/doctor.js
CHANGED
|
@@ -169,6 +169,22 @@ export const EXPECTED_HOOKS = [
|
|
|
169
169
|
'blocked-paths-enforcer.sh',
|
|
170
170
|
'changeset-security-gate.sh',
|
|
171
171
|
'dangerous-bash-interceptor.sh',
|
|
172
|
+
// 0.31.0 — `delegation-advisory.sh` is INTENTIONALLY NOT in this list.
|
|
173
|
+
// It is the brand-new PostToolUse delegation-nudge hook this release
|
|
174
|
+
// ships; adding it to EXPECTED_HOOKS would make `checkHooksInstalled`
|
|
175
|
+
// hard-`fail` on every pre-0.31.0 consumer install (and on this repo's
|
|
176
|
+
// own dogfood) the instant they upgrade the rea binary but before they
|
|
177
|
+
// run `rea upgrade` to lay down the new hook file — a regression that
|
|
178
|
+
// turns a green doctor red purely from upgrade lag. This mirrors the
|
|
179
|
+
// staged rollout `delegation-capture.sh` itself used: the file-presence
|
|
180
|
+
// entry joins EXPECTED_HOOKS in the SAME future minor that promotes
|
|
181
|
+
// `checkDelegationAdvisoryHookRegistered` from `warn` to `fail`, once
|
|
182
|
+
// consumers have had upgrade-lag time. Until then, a missing
|
|
183
|
+
// `delegation-advisory.sh` surfaces only through that `warn`-tier
|
|
184
|
+
// registration check — proportionate, since the hook is advisory at
|
|
185
|
+
// runtime and never blocks a tool call. See `git blame` on the 0.29.0
|
|
186
|
+
// round-2 P2 comment on `checkDelegationHookRegistered` for the prior
|
|
187
|
+
// application of this exact discipline.
|
|
172
188
|
// 0.29.0 — delegation-telemetry MVP. The PreToolUse hook on
|
|
173
189
|
// matcher `Agent|Skill` emits a `rea.delegation_signal` audit record
|
|
174
190
|
// on every subagent / skill dispatch. Observational only — fails
|
|
@@ -995,37 +1011,36 @@ function codexRequiredFromPolicy(baseDir) {
|
|
|
995
1011
|
* `.claude/settings.json` under PreToolUse with matcher `Agent|Skill`
|
|
996
1012
|
* AND that the hook file exists at the expected dogfood path.
|
|
997
1013
|
*
|
|
998
|
-
* Status posture
|
|
1014
|
+
* Status posture:
|
|
999
1015
|
*
|
|
1000
|
-
*
|
|
1001
|
-
* `defaultDesiredHooks()`
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1016
|
+
* 0.29.0 shipped this check as `warn` (advisory) — the
|
|
1017
|
+
* `defaultDesiredHooks()` entry was new, and existing consumer
|
|
1018
|
+
* installs (plus this repo's own dogfood, locked from agent-driven
|
|
1019
|
+
* edits by `settings-protection.sh`) wouldn't have the matcher
|
|
1020
|
+
* registered until the operator ran `rea upgrade`. The comments
|
|
1021
|
+
* promised promotion to `fail` "in 0.30.0".
|
|
1006
1022
|
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1023
|
+
* **0.31.0 makes good on that promise.** The 0.29.0 → 0.30.x consumer
|
|
1024
|
+
* cycles have propagated; the `Agent|Skill` matcher has been in
|
|
1025
|
+
* `defaultDesiredHooks()` for multiple minors. A consumer install
|
|
1026
|
+
* that still lacks the registration is a real governance gap (the
|
|
1027
|
+
* delegation telemetry — and now the 0.31.0 nudge — silently does
|
|
1028
|
+
* nothing), so the check is `fail`. The detail message still names
|
|
1029
|
+
* the exact `rea upgrade` fix.
|
|
1014
1030
|
*
|
|
1015
1031
|
* Hook-file presence is verified separately by `checkHooksInstalled`
|
|
1016
|
-
* via `EXPECTED_HOOKS` — that path
|
|
1017
|
-
* because file presence is part of the install manifest and doesn't
|
|
1018
|
-
* suffer the same template-propagation lag.
|
|
1032
|
+
* via `EXPECTED_HOOKS` — that path was always hard-`fail`.
|
|
1019
1033
|
*/
|
|
1020
1034
|
export function checkDelegationHookRegistered(baseDir) {
|
|
1021
1035
|
const label = 'delegation-capture hook registered';
|
|
1022
|
-
|
|
1036
|
+
// 0.31.0 — promoted from `warn` to `fail` (see the docstring).
|
|
1037
|
+
const REFUSE = 'fail';
|
|
1023
1038
|
const settingsPath = path.join(baseDir, '.claude', 'settings.json');
|
|
1024
1039
|
if (!fs.existsSync(settingsPath)) {
|
|
1025
1040
|
return {
|
|
1026
1041
|
label,
|
|
1027
|
-
status:
|
|
1028
|
-
detail: `missing: ${settingsPath} — run \`rea upgrade\` or \`rea init
|
|
1042
|
+
status: REFUSE,
|
|
1043
|
+
detail: `missing: ${settingsPath} — run \`rea upgrade\` or \`rea init\``,
|
|
1029
1044
|
};
|
|
1030
1045
|
}
|
|
1031
1046
|
let parsed;
|
|
@@ -1035,7 +1050,7 @@ export function checkDelegationHookRegistered(baseDir) {
|
|
|
1035
1050
|
catch (e) {
|
|
1036
1051
|
return {
|
|
1037
1052
|
label,
|
|
1038
|
-
status:
|
|
1053
|
+
status: REFUSE,
|
|
1039
1054
|
detail: e instanceof Error ? e.message : String(e),
|
|
1040
1055
|
};
|
|
1041
1056
|
}
|
|
@@ -1044,9 +1059,9 @@ export function checkDelegationHookRegistered(baseDir) {
|
|
|
1044
1059
|
if (group === undefined) {
|
|
1045
1060
|
return {
|
|
1046
1061
|
label,
|
|
1047
|
-
status:
|
|
1062
|
+
status: REFUSE,
|
|
1048
1063
|
detail: 'no PreToolUse group with matcher "Agent|Skill" found in .claude/settings.json — ' +
|
|
1049
|
-
'run `rea upgrade` to install
|
|
1064
|
+
'run `rea upgrade` to install. ' +
|
|
1050
1065
|
'NOTE: matcher MUST be exactly `Agent|Skill` ' +
|
|
1051
1066
|
'(NOT `Task|Skill` — `TaskCreate`/`TaskList` are unrelated todo-list tools).',
|
|
1052
1067
|
};
|
|
@@ -1055,52 +1070,228 @@ export function checkDelegationHookRegistered(baseDir) {
|
|
|
1055
1070
|
if (!cmds.some((c) => c.includes('delegation-capture.sh'))) {
|
|
1056
1071
|
return {
|
|
1057
1072
|
label,
|
|
1058
|
-
status:
|
|
1073
|
+
status: REFUSE,
|
|
1059
1074
|
detail: 'Agent|Skill matcher exists but no delegation-capture.sh command found in its hooks list',
|
|
1060
1075
|
};
|
|
1061
1076
|
}
|
|
1062
1077
|
return { label, status: 'pass' };
|
|
1063
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* 0.31.0 — verify the delegation-advisory hook is registered in
|
|
1081
|
+
* `.claude/settings.json` under PostToolUse with matcher
|
|
1082
|
+
* `Bash|Edit|Write|MultiEdit|NotebookEdit`, that a
|
|
1083
|
+
* `delegation-advisory.sh` command is present in that group, AND that
|
|
1084
|
+
* the `.claude/hooks/delegation-advisory.sh` file actually exists.
|
|
1085
|
+
*
|
|
1086
|
+
* Status posture: `warn` (advisory) for 0.31.0. This is a brand-new
|
|
1087
|
+
* `defaultDesiredHooks()` entry — the exact same upgrade-lag situation
|
|
1088
|
+
* `checkDelegationHookRegistered` faced in 0.29.0. Existing consumer
|
|
1089
|
+
* installs (and this repo's own dogfood, locked from agent-driven
|
|
1090
|
+
* edits by `settings-protection.sh`) won't have the PostToolUse group
|
|
1091
|
+
* until the operator runs `rea upgrade`. Holding at `warn` for one
|
|
1092
|
+
* release cycle keeps `rea doctor` green during propagation; a future
|
|
1093
|
+
* minor promotes it to `fail` once consumer installs have caught up —
|
|
1094
|
+
* the same ratchet `checkDelegationHookRegistered` just completed.
|
|
1095
|
+
*
|
|
1096
|
+
* The hook is ALSO advisory at runtime (it never blocks a tool call,
|
|
1097
|
+
* and `policy.delegation_advisory` defaults to disabled), so a missing
|
|
1098
|
+
* registration is a lower-stakes gap than a missing security gate —
|
|
1099
|
+
* `warn` is proportionate even setting the upgrade-lag aside.
|
|
1100
|
+
*
|
|
1101
|
+
* # Why this check verifies file presence AND executability (round-2/3 P2)
|
|
1102
|
+
*
|
|
1103
|
+
* `delegation-advisory.sh` is deliberately NOT in `EXPECTED_HOOKS` for
|
|
1104
|
+
* 0.31.0 (staged rollout — see the `EXPECTED_HOOKS` comment). That
|
|
1105
|
+
* leaves THIS function as the only 0.31.0 doctor signal covering the
|
|
1106
|
+
* new hook, so it must check the file too:
|
|
1107
|
+
*
|
|
1108
|
+
* - File MISSING — a settings.json that references
|
|
1109
|
+
* `delegation-advisory.sh` while the actual script is absent (a
|
|
1110
|
+
* partial `rea upgrade`, manual drift) would otherwise report
|
|
1111
|
+
* `pass`, and every matching PostToolUse dispatch would shell out
|
|
1112
|
+
* to a nonexistent path.
|
|
1113
|
+
* - File present but NOT EXECUTABLE — a script copied without its
|
|
1114
|
+
* mode bits (a manual `cp`, an archive extracted without `+x`
|
|
1115
|
+
* preservation) cannot be launched by Claude Code from
|
|
1116
|
+
* `settings.json` at all. `checkHooksInstalled` performs this exact
|
|
1117
|
+
* `0o111` check for every `EXPECTED_HOOKS` entry; because
|
|
1118
|
+
* `delegation-advisory.sh` is held out of that list, the parity
|
|
1119
|
+
* check has to live here.
|
|
1120
|
+
*
|
|
1121
|
+
* Both failures are held at the same `warn` tier as the registration
|
|
1122
|
+
* failures: consistent posture for 0.31.0, and they promote to `fail`
|
|
1123
|
+
* alongside them — at which point `delegation-advisory.sh` also joins
|
|
1124
|
+
* `EXPECTED_HOOKS` and gets the hard-`fail` `checkHooksInstalled`
|
|
1125
|
+
* coverage (presence + executability) the other hooks have.
|
|
1126
|
+
*/
|
|
1127
|
+
export function checkDelegationAdvisoryHookRegistered(baseDir) {
|
|
1128
|
+
const label = 'delegation-advisory hook registered';
|
|
1129
|
+
const ADVISORY = 'warn';
|
|
1130
|
+
const MATCHER = 'Bash|Edit|Write|MultiEdit|NotebookEdit';
|
|
1131
|
+
const settingsPath = path.join(baseDir, '.claude', 'settings.json');
|
|
1132
|
+
if (!fs.existsSync(settingsPath)) {
|
|
1133
|
+
return {
|
|
1134
|
+
label,
|
|
1135
|
+
status: ADVISORY,
|
|
1136
|
+
detail: `missing: ${settingsPath} — run \`rea upgrade\` or \`rea init\` (advisory in 0.31.0)`,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
let parsed;
|
|
1140
|
+
try {
|
|
1141
|
+
parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1142
|
+
}
|
|
1143
|
+
catch (e) {
|
|
1144
|
+
return {
|
|
1145
|
+
label,
|
|
1146
|
+
status: ADVISORY,
|
|
1147
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
const groups = parsed.hooks?.PostToolUse ?? [];
|
|
1151
|
+
const group = groups.find((g) => g.matcher === MATCHER);
|
|
1152
|
+
if (group === undefined) {
|
|
1153
|
+
return {
|
|
1154
|
+
label,
|
|
1155
|
+
status: ADVISORY,
|
|
1156
|
+
detail: `no PostToolUse group with matcher "${MATCHER}" found in .claude/settings.json — ` +
|
|
1157
|
+
'run `rea upgrade` to install (advisory in 0.31.0; promoted to fail in a later minor). ' +
|
|
1158
|
+
'NOTE: the matcher INCLUDES Bash — the delegation nudge counts every write-class ' +
|
|
1159
|
+
'tool call, not just file edits.',
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const cmds = (group.hooks ?? []).map((h) => (typeof h.command === 'string' ? h.command : ''));
|
|
1163
|
+
if (!cmds.some((c) => c.includes('delegation-advisory.sh'))) {
|
|
1164
|
+
return {
|
|
1165
|
+
label,
|
|
1166
|
+
status: ADVISORY,
|
|
1167
|
+
detail: `${MATCHER} matcher exists but no delegation-advisory.sh command found in its hooks list`,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
// Round-2/3 P2: the registration string is present — now confirm the
|
|
1171
|
+
// hook file it points at actually exists AND is executable. Because
|
|
1172
|
+
// `delegation-advisory.sh` is intentionally out of `EXPECTED_HOOKS`
|
|
1173
|
+
// for 0.31.0, `checkHooksInstalled` does NOT cover it; this is the
|
|
1174
|
+
// only signal, so it owns both the presence and the `0o111` checks
|
|
1175
|
+
// that `checkHooksInstalled` does for every other shipped hook.
|
|
1176
|
+
const hookFile = path.join(baseDir, '.claude', 'hooks', 'delegation-advisory.sh');
|
|
1177
|
+
let hookStat;
|
|
1178
|
+
try {
|
|
1179
|
+
hookStat = fs.statSync(hookFile);
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
return {
|
|
1183
|
+
label,
|
|
1184
|
+
status: ADVISORY,
|
|
1185
|
+
detail: `${MATCHER} matcher references delegation-advisory.sh but the hook file is missing: ` +
|
|
1186
|
+
`${hookFile} — run \`rea upgrade\` to lay it down (advisory in 0.31.0). ` +
|
|
1187
|
+
'Without the file every matching PostToolUse dispatch shells out to a nonexistent path.',
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
if ((hookStat.mode & 0o111) === 0) {
|
|
1191
|
+
return {
|
|
1192
|
+
label,
|
|
1193
|
+
status: ADVISORY,
|
|
1194
|
+
detail: `${MATCHER} matcher references delegation-advisory.sh but the hook file is not executable ` +
|
|
1195
|
+
`(mode=${(hookStat.mode & 0o777).toString(8)}): ${hookFile} — ` +
|
|
1196
|
+
'run `rea upgrade` or `chmod +x` it (advisory in 0.31.0). ' +
|
|
1197
|
+
'A non-executable hook cannot be launched by Claude Code from settings.json.',
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
return { label, status: 'pass' };
|
|
1201
|
+
}
|
|
1064
1202
|
/**
|
|
1065
1203
|
* 0.29.0 — synthetic round-trip of the delegation-signal audit path.
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* (same path the shell hook hits) and asserts:
|
|
1204
|
+
* 0.31.0 — drives the REAL `.claude/hooks/delegation-capture.sh` shell
|
|
1205
|
+
* hook, not just the `rea hook delegation-signal` CLI underneath it.
|
|
1069
1206
|
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1207
|
+
* Feeds a synthetic Claude Code PreToolUse hook payload to the shell
|
|
1208
|
+
* hook (the exact entry point Claude Code's `Agent|Skill` matcher
|
|
1209
|
+
* invokes in production) and asserts:
|
|
1210
|
+
*
|
|
1211
|
+
* - The shell hook exited 0.
|
|
1212
|
+
* - A new `rea.delegation_signal` record landed on disk — the smoke
|
|
1213
|
+
* check POLLS for it, because `delegation-capture.sh` backgrounds
|
|
1214
|
+
* + disowns the CLI (`& disown`) so the shell hook returns before
|
|
1215
|
+
* the audit append completes.
|
|
1072
1216
|
* - The record's metadata contains the probe tag (so we don't
|
|
1073
1217
|
* mistakenly attribute an existing record to our run).
|
|
1218
|
+
* - The recorded `invocation_description_sha256` matches the
|
|
1219
|
+
* expected hash of the probe description.
|
|
1074
1220
|
* - Chain integrity holds (recomputed hash == stored hash).
|
|
1075
1221
|
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1222
|
+
* # Why drive the shell hook, not the CLI directly
|
|
1223
|
+
*
|
|
1224
|
+
* 0.29.0's version spawned `rea hook delegation-signal` directly. That
|
|
1225
|
+
* exercised the CLI's stdin parsing / hashing / redaction / process-
|
|
1226
|
+
* lifecycle — but NOT the shell shim's own logic: the 2-tier sandboxed
|
|
1227
|
+
* CLI resolution, the realpath sandbox check, the `& disown`
|
|
1228
|
+
* backgrounding. A regression in the shim (a botched resolution order,
|
|
1229
|
+
* a sandbox check that rejects the legitimate dogfood CLI, a
|
|
1230
|
+
* backgrounding bug that drops the signal) would pass 0.29.0's smoke
|
|
1231
|
+
* check while breaking production. 0.31.0 closes that gap: the smoke
|
|
1232
|
+
* check now invokes `bash .claude/hooks/delegation-capture.sh` and
|
|
1233
|
+
* the CLI is reached only through the shim.
|
|
1234
|
+
*
|
|
1235
|
+
* # Prerequisites and graceful degradation
|
|
1081
1236
|
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* fail the smoke check loudly.
|
|
1237
|
+
* The check needs THREE things and degrades to `warn` (not `fail`)
|
|
1238
|
+
* when any is absent — a missing prerequisite is an environment gap,
|
|
1239
|
+
* not a wiring regression:
|
|
1086
1240
|
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
* `rea
|
|
1090
|
-
*
|
|
1241
|
+
* - `bash` on PATH.
|
|
1242
|
+
* - `.claude/hooks/delegation-capture.sh` present (the consumer
|
|
1243
|
+
* install path; absent before `rea init` / `rea upgrade`).
|
|
1244
|
+
* - A sandboxed rea CLI the shim can resolve — either
|
|
1245
|
+
* `<baseDir>/node_modules/@bookedsolid/rea/dist/cli/index.js` OR
|
|
1246
|
+
* `<baseDir>/dist/cli/index.js` (the rea-repo dogfood). Without
|
|
1247
|
+
* one the shim silently drops the signal by design, so the smoke
|
|
1248
|
+
* check would time out waiting for a record that will never land.
|
|
1249
|
+
*
|
|
1250
|
+
* Gated behind `--smoke` so a casual `rea doctor` doesn't write probe
|
|
1251
|
+
* records on every invocation. Operators run `rea doctor --smoke`
|
|
1252
|
+
* after install / upgrade to confirm the pipeline is wired end-to-end.
|
|
1091
1253
|
*/
|
|
1092
1254
|
export async function checkDelegationRoundTrip(baseDir) {
|
|
1255
|
+
const label = 'delegation-signal round-trip';
|
|
1093
1256
|
const probeTag = `doctor-smoke-${process.pid}-${Date.now()}`;
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1257
|
+
// Prerequisite 1: the shell hook file. The consumer install path is
|
|
1258
|
+
// `.claude/hooks/delegation-capture.sh`; that is the exact file
|
|
1259
|
+
// Claude Code's matcher invokes. We do NOT fall back to the source
|
|
1260
|
+
// `hooks/` copy — the point of the smoke check is to validate the
|
|
1261
|
+
// INSTALLED artifact.
|
|
1262
|
+
const hookPath = path.join(baseDir, '.claude', 'hooks', 'delegation-capture.sh');
|
|
1263
|
+
if (!fs.existsSync(hookPath)) {
|
|
1100
1264
|
return {
|
|
1101
|
-
label
|
|
1102
|
-
status: '
|
|
1103
|
-
detail:
|
|
1265
|
+
label,
|
|
1266
|
+
status: 'warn',
|
|
1267
|
+
detail: `shell hook not installed at ${hookPath} — run \`rea init\` / \`rea upgrade\`. ` +
|
|
1268
|
+
'Smoke check needs the installed hook to drive the full chain.',
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
// Prerequisite 2: bash on PATH.
|
|
1272
|
+
const { spawnSync } = await import('node:child_process');
|
|
1273
|
+
const bashProbe = spawnSync('bash', ['--version'], { encoding: 'utf8' });
|
|
1274
|
+
if (bashProbe.status !== 0) {
|
|
1275
|
+
return {
|
|
1276
|
+
label,
|
|
1277
|
+
status: 'warn',
|
|
1278
|
+
detail: 'bash not available — cannot drive the delegation-capture.sh shell hook',
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
// Prerequisite 3: a sandboxed rea CLI the shim can resolve. The
|
|
1282
|
+
// shim's 2-tier order is node_modules/@bookedsolid/rea/dist/... then
|
|
1283
|
+
// <proj>/dist/cli/index.js. If NEITHER exists the shim drops the
|
|
1284
|
+
// signal silently (by design) and the poll below would just time
|
|
1285
|
+
// out — surface the real reason instead.
|
|
1286
|
+
const consumerCli = path.join(baseDir, 'node_modules', '@bookedsolid', 'rea', 'dist', 'cli', 'index.js');
|
|
1287
|
+
const dogfoodCli = path.join(baseDir, 'dist', 'cli', 'index.js');
|
|
1288
|
+
if (!fs.existsSync(consumerCli) && !fs.existsSync(dogfoodCli)) {
|
|
1289
|
+
return {
|
|
1290
|
+
label,
|
|
1291
|
+
status: 'warn',
|
|
1292
|
+
detail: 'no sandboxed rea CLI in scope (need node_modules/@bookedsolid/rea/dist/cli/index.js ' +
|
|
1293
|
+
'or <baseDir>/dist/cli/index.js) — the shell hook drops the signal silently here. ' +
|
|
1294
|
+
'Run `pnpm build` (dogfood) or `pnpm i` (consumer) first.',
|
|
1104
1295
|
};
|
|
1105
1296
|
}
|
|
1106
1297
|
// Codex round 4 P3 (2026-05-12): exercise a NON-EMPTY description
|
|
@@ -1123,13 +1314,13 @@ export async function checkDelegationRoundTrip(baseDir) {
|
|
|
1123
1314
|
},
|
|
1124
1315
|
});
|
|
1125
1316
|
const auditPath = path.join(baseDir, '.rea', 'audit.jsonl');
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
const res = spawnSync(
|
|
1317
|
+
// Drive the REAL shell hook. `delegation-capture.sh` reads the
|
|
1318
|
+
// payload on stdin, resolves + sandbox-checks the rea CLI, then
|
|
1319
|
+
// backgrounds `rea hook delegation-signal --detach` with `& disown`.
|
|
1320
|
+
// The hook itself returns near-instantly; the audit append lands
|
|
1321
|
+
// asynchronously. CLAUDE_PROJECT_DIR is set so the shim resolves the
|
|
1322
|
+
// same baseDir doctor is checking.
|
|
1323
|
+
const res = spawnSync('bash', [hookPath], {
|
|
1133
1324
|
cwd: baseDir,
|
|
1134
1325
|
input: payload,
|
|
1135
1326
|
encoding: 'utf8',
|
|
@@ -1138,49 +1329,64 @@ export async function checkDelegationRoundTrip(baseDir) {
|
|
|
1138
1329
|
});
|
|
1139
1330
|
if (res.error !== undefined) {
|
|
1140
1331
|
return {
|
|
1141
|
-
label
|
|
1332
|
+
label,
|
|
1142
1333
|
status: 'fail',
|
|
1143
|
-
detail: `
|
|
1334
|
+
detail: `shell hook spawn failed: ${res.error.message}`,
|
|
1144
1335
|
};
|
|
1145
1336
|
}
|
|
1146
1337
|
if (res.status !== 0) {
|
|
1147
1338
|
return {
|
|
1148
|
-
label
|
|
1149
|
-
status: 'fail',
|
|
1150
|
-
detail: `CLI exited ${res.status ?? 'null'}; stderr: ${(res.stderr ?? '').slice(0, 240)}`,
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
// Read the audit log and find the record carrying our probe tag.
|
|
1154
|
-
let raw;
|
|
1155
|
-
try {
|
|
1156
|
-
raw = await fsPromises.readFile(auditPath, 'utf8');
|
|
1157
|
-
}
|
|
1158
|
-
catch (e) {
|
|
1159
|
-
return {
|
|
1160
|
-
label: 'delegation-signal round-trip',
|
|
1339
|
+
label,
|
|
1161
1340
|
status: 'fail',
|
|
1162
|
-
detail: `
|
|
1341
|
+
detail: `shell hook exited ${res.status ?? 'null'}; stderr: ${(res.stderr ?? '').slice(0, 240)}`,
|
|
1163
1342
|
};
|
|
1164
1343
|
}
|
|
1165
|
-
|
|
1344
|
+
/** Scan one audit-file's content for the probe record; null when absent. */
|
|
1345
|
+
const scanForProbe = (raw) => {
|
|
1346
|
+
let found = null;
|
|
1347
|
+
for (const line of raw.split('\n')) {
|
|
1348
|
+
if (line.length === 0)
|
|
1349
|
+
continue;
|
|
1350
|
+
try {
|
|
1351
|
+
const p = JSON.parse(line);
|
|
1352
|
+
if (p.tool_name === DELEGATION_SIGNAL_TOOL_NAME &&
|
|
1353
|
+
p.metadata?.subagent_type === probeTag) {
|
|
1354
|
+
found = { line, parsed: p };
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
// skip malformed
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return found;
|
|
1362
|
+
};
|
|
1166
1363
|
let matched = null;
|
|
1167
|
-
|
|
1364
|
+
const deadline = Date.now() + 10_000;
|
|
1365
|
+
while (Date.now() < deadline) {
|
|
1366
|
+
let raw = null;
|
|
1168
1367
|
try {
|
|
1169
|
-
|
|
1170
|
-
if (p.tool_name === DELEGATION_SIGNAL_TOOL_NAME && p.metadata?.subagent_type === probeTag) {
|
|
1171
|
-
matched = { line, parsed: p };
|
|
1172
|
-
}
|
|
1368
|
+
raw = await fsPromises.readFile(auditPath, 'utf8');
|
|
1173
1369
|
}
|
|
1174
1370
|
catch {
|
|
1175
|
-
//
|
|
1371
|
+
// Audit file may not exist yet on a brand-new install — keep
|
|
1372
|
+
// polling; the first append creates it.
|
|
1373
|
+
raw = null;
|
|
1176
1374
|
}
|
|
1375
|
+
if (raw !== null) {
|
|
1376
|
+
matched = scanForProbe(raw);
|
|
1377
|
+
if (matched !== null)
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1177
1381
|
}
|
|
1178
1382
|
if (matched === null) {
|
|
1179
1383
|
return {
|
|
1180
|
-
label
|
|
1384
|
+
label,
|
|
1181
1385
|
status: 'fail',
|
|
1182
|
-
detail: `
|
|
1183
|
-
|
|
1386
|
+
detail: `shell hook exited 0 but no rea.delegation_signal record with probe-tag ` +
|
|
1387
|
+
`${probeTag} appeared in audit.jsonl within 10s — the shim resolved/sandboxed ` +
|
|
1388
|
+
`the CLI but the backgrounded append never landed (check shell-hook stderr: ` +
|
|
1389
|
+
`${(res.stderr ?? '').slice(0, 160)})`,
|
|
1184
1390
|
};
|
|
1185
1391
|
}
|
|
1186
1392
|
// Codex round 4 P3 (2026-05-12): assert the recorded
|
|
@@ -1190,7 +1396,7 @@ export async function checkDelegationRoundTrip(baseDir) {
|
|
|
1190
1396
|
const recordedDescHash = matched.parsed.metadata?.invocation_description_sha256;
|
|
1191
1397
|
if (recordedDescHash !== expectedDescriptionHash) {
|
|
1192
1398
|
return {
|
|
1193
|
-
label
|
|
1399
|
+
label,
|
|
1194
1400
|
status: 'fail',
|
|
1195
1401
|
detail: `recorded invocation_description_sha256 mismatch: ` +
|
|
1196
1402
|
`expected ${expectedDescriptionHash.slice(0, 16)}…, ` +
|
|
@@ -1203,7 +1409,7 @@ export async function checkDelegationRoundTrip(baseDir) {
|
|
|
1203
1409
|
const storedHash = recordParsed.hash;
|
|
1204
1410
|
if (typeof storedHash !== 'string' || storedHash.length !== 64) {
|
|
1205
1411
|
return {
|
|
1206
|
-
label
|
|
1412
|
+
label,
|
|
1207
1413
|
status: 'fail',
|
|
1208
1414
|
detail: 'probe record has no valid `hash` field',
|
|
1209
1415
|
};
|
|
@@ -1213,15 +1419,15 @@ export async function checkDelegationRoundTrip(baseDir) {
|
|
|
1213
1419
|
const recomputed = computeHash(rest);
|
|
1214
1420
|
if (recomputed !== storedHash) {
|
|
1215
1421
|
return {
|
|
1216
|
-
label
|
|
1422
|
+
label,
|
|
1217
1423
|
status: 'fail',
|
|
1218
1424
|
detail: `chain integrity broken: stored=${storedHash} recomputed=${recomputed}`,
|
|
1219
1425
|
};
|
|
1220
1426
|
}
|
|
1221
1427
|
return {
|
|
1222
|
-
label
|
|
1428
|
+
label,
|
|
1223
1429
|
status: 'pass',
|
|
1224
|
-
detail: `probe via real
|
|
1430
|
+
detail: `probe via real .claude/hooks/delegation-capture.sh shell hook (hash=${storedHash.slice(0, 16)}, tag=${probeTag.slice(-8)})`,
|
|
1225
1431
|
};
|
|
1226
1432
|
}
|
|
1227
1433
|
/**
|
|
@@ -1259,8 +1465,14 @@ export function collectChecks(baseDir, codexProbeState, prePushState, options =
|
|
|
1259
1465
|
// checkSettingsJson because that check only validates the
|
|
1260
1466
|
// existence of the Bash + Write|Edit|MultiEdit|NotebookEdit
|
|
1261
1467
|
// matcher groups. The Agent|Skill matcher is new and needs its
|
|
1262
|
-
// own pass/fail signal.
|
|
1468
|
+
// own pass/fail signal. 0.31.0 — promoted warn → fail.
|
|
1263
1469
|
checkDelegationHookRegistered(baseDir),
|
|
1470
|
+
// 0.31.0 — delegation-telemetry completion. The PostToolUse
|
|
1471
|
+
// `Bash|Edit|Write|MultiEdit|NotebookEdit` matcher group drives
|
|
1472
|
+
// the delegation-advisory nudge hook. Advisory (warn) for 0.31.0
|
|
1473
|
+
// — same upgrade-lag ratchet checkDelegationHookRegistered went
|
|
1474
|
+
// through in 0.29.0.
|
|
1475
|
+
checkDelegationAdvisoryHookRegistered(baseDir),
|
|
1264
1476
|
];
|
|
1265
1477
|
// Non-git escape hatch: when `.git/` is absent, both git-hook checks are
|
|
1266
1478
|
// meaningless (commit-msg + pre-push can't be invoked without git). Emit
|
package/dist/cli/hook.d.ts
CHANGED
|
@@ -232,6 +232,12 @@ export declare function runHookDelegationSignal(options: HookDelegationSignalOpt
|
|
|
232
232
|
* - `delegation-signal` — 0.29.0 delegation-telemetry MVP. Reads a Claude
|
|
233
233
|
* Code PreToolUse hook payload for `Agent` / `Skill`
|
|
234
234
|
* and emits a `rea.delegation_signal` audit record.
|
|
235
|
+
* - `delegation-advisory` — 0.31.0 delegation nudge. Reads a Claude Code
|
|
236
|
+
* PostToolUse hook payload for the write-class
|
|
237
|
+
* tools, maintains a per-session counter, and emits
|
|
238
|
+
* a one-time stderr advisory when the session
|
|
239
|
+
* crosses `policy.delegation_advisory.threshold`
|
|
240
|
+
* without dispatching a curated specialist.
|
|
235
241
|
*
|
|
236
242
|
* New hooks should land here rather than as top-level commands so the
|
|
237
243
|
* CLI surface stays navigable.
|
package/dist/cli/hook.js
CHANGED
|
@@ -44,6 +44,7 @@ import { CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, Codex
|
|
|
44
44
|
import { resolveBaseRef } from '../hooks/push-gate/base.js';
|
|
45
45
|
import { resolvePushGatePolicy } from '../hooks/push-gate/policy.js';
|
|
46
46
|
import { summarizeReview } from '../hooks/push-gate/findings.js';
|
|
47
|
+
import { runHookDelegationAdvisory } from './delegation-advisory.js';
|
|
47
48
|
import { err } from './utils.js';
|
|
48
49
|
/**
|
|
49
50
|
* Public runner, exposed so integration tests and the commander binding can
|
|
@@ -869,6 +870,12 @@ export async function runHookDelegationSignal(options) {
|
|
|
869
870
|
* - `delegation-signal` — 0.29.0 delegation-telemetry MVP. Reads a Claude
|
|
870
871
|
* Code PreToolUse hook payload for `Agent` / `Skill`
|
|
871
872
|
* and emits a `rea.delegation_signal` audit record.
|
|
873
|
+
* - `delegation-advisory` — 0.31.0 delegation nudge. Reads a Claude Code
|
|
874
|
+
* PostToolUse hook payload for the write-class
|
|
875
|
+
* tools, maintains a per-session counter, and emits
|
|
876
|
+
* a one-time stderr advisory when the session
|
|
877
|
+
* crosses `policy.delegation_advisory.threshold`
|
|
878
|
+
* without dispatching a curated specialist.
|
|
872
879
|
*
|
|
873
880
|
* New hooks should land here rather than as top-level commands so the
|
|
874
881
|
* CLI surface stays navigable.
|
|
@@ -950,6 +957,12 @@ export function registerHookCommand(program) {
|
|
|
950
957
|
...(opts.lockTimeoutMs !== undefined ? { lockTimeoutMs: opts.lockTimeoutMs } : {}),
|
|
951
958
|
});
|
|
952
959
|
});
|
|
960
|
+
hook
|
|
961
|
+
.command('delegation-advisory')
|
|
962
|
+
.description('Read a Claude Code PostToolUse hook payload for a write-class tool (Bash/Edit/Write/MultiEdit/NotebookEdit) from stdin, bump a per-session write-class counter, and emit a one-time stderr advisory when the session crosses `policy.delegation_advisory.threshold` without dispatching a curated specialist (0.31.0+). Advisory only — exit ALWAYS 0 except HALT (exit 2). Disabled unless `policy.delegation_advisory.enabled: true`. The hook shim at `.claude/hooks/delegation-advisory.sh` invokes this.')
|
|
963
|
+
.action(async () => {
|
|
964
|
+
await runHookDelegationAdvisory();
|
|
965
|
+
});
|
|
953
966
|
hook
|
|
954
967
|
.command('policy-get')
|
|
955
968
|
.description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
|
package/dist/cli/index.js
CHANGED
|
@@ -148,7 +148,7 @@ async function main() {
|
|
|
148
148
|
.description('Validate the install: policy parses, .rea/ layout, hooks, Codex plugin.')
|
|
149
149
|
.option('--metrics', 'also print a 7-day summary of Codex telemetry (G11.5)')
|
|
150
150
|
.option('--drift', 'report drift vs. the install manifest (read-only; does not mutate)')
|
|
151
|
-
.option('--smoke', 'also run the
|
|
151
|
+
.option('--smoke', 'also run the delegation-signal round-trip: drives the real `.claude/hooks/delegation-capture.sh` shell hook end-to-end (writes a probe `rea.delegation_signal` audit record and verifies chain integrity)')
|
|
152
152
|
.option('--strict', '0.30.0 Class M — promote settings.json schema warnings (zod parse failures, path traversal, missing rea hooks) to hard fail. Use in CI gates.')
|
|
153
153
|
.action(async (opts) => {
|
|
154
154
|
await runDoctor({
|
|
@@ -380,5 +380,30 @@ export function defaultDesiredHooks() {
|
|
|
380
380
|
},
|
|
381
381
|
],
|
|
382
382
|
},
|
|
383
|
+
{
|
|
384
|
+
// 0.31.0 delegation-telemetry completion — the *nudge*. The
|
|
385
|
+
// matcher is `Bash|Edit|Write|MultiEdit|NotebookEdit`: every
|
|
386
|
+
// write-class tool call (note this group INCLUDES Bash, unlike
|
|
387
|
+
// the architecture-review group above). The hook maintains a
|
|
388
|
+
// per-session counter and emits a one-time stderr advisory when
|
|
389
|
+
// a session crosses `policy.delegation_advisory.threshold`
|
|
390
|
+
// without dispatching a curated specialist. Advisory only — it
|
|
391
|
+
// never blocks a tool call, and `policy.delegation_advisory`
|
|
392
|
+
// defaults to disabled (only `bst-internal*` profiles pin
|
|
393
|
+
// `enabled: true`), so a vanilla install sees the hook as a
|
|
394
|
+
// silent no-op. Kept as its own matcher group rather than
|
|
395
|
+
// chained onto the architecture-review group because the
|
|
396
|
+
// matcher string differs (Bash is in this one, not that one).
|
|
397
|
+
event: 'PostToolUse',
|
|
398
|
+
matcher: 'Bash|Edit|Write|MultiEdit|NotebookEdit',
|
|
399
|
+
hooks: [
|
|
400
|
+
{
|
|
401
|
+
type: 'command',
|
|
402
|
+
command: `${base}/delegation-advisory.sh`,
|
|
403
|
+
timeout: 10000,
|
|
404
|
+
statusMessage: 'Checking delegation cadence...',
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
383
408
|
];
|
|
384
409
|
}
|