@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.
@@ -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
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Live `.claude/agents/` roster discovery (0.31.0+).
3
+ *
4
+ * 0.29.0 shipped the delegation-telemetry observability layer. 0.31.0
5
+ * closes the loop with the *nudge* — and the nudge needs to know what
6
+ * counts as a "real" specialist delegation versus a built-in helper.
7
+ *
8
+ * # Why discovery, not a hardcoded list
9
+ *
10
+ * `src/cli/doctor.ts` already carries an `EXPECTED_AGENTS` constant,
11
+ * but it is a deliberately-frozen 10-entry subset — the minimum roster
12
+ * `rea init` guarantees, pinned so a regression that drops a curated
13
+ * agent from the install manifest trips a doctor failure. It is NOT
14
+ * the live roster: this repo ships 23 agents, consumers add their own,
15
+ * and the curated set grows release over release. Keying the
16
+ * delegation nudge off `EXPECTED_AGENTS` would mean a session that
17
+ * delegated exclusively to `principal-engineer` and `data-architect`
18
+ * (both real, curated, neither in the frozen subset) still got nudged
19
+ * — exactly the false positive that erodes trust in an advisory.
20
+ *
21
+ * So the roster is discovered at read time: every `*.md` file under
22
+ * `<baseDir>/.claude/agents/` is a curated specialist. The basename
23
+ * (sans `.md`) is the `subagent_type` Claude Code's `Agent` tool
24
+ * reports, so the mapping is direct.
25
+ *
26
+ * # The exempt set
27
+ *
28
+ * Claude Code ships built-in helpers — `general-purpose`, `Explore`,
29
+ * `Plan`, `output-style-setup`, `statusline-setup` — that are dispatched
30
+ * through the same `Agent` tool but are NOT curated specialists. A
31
+ * session that only ever delegated to those has not actually routed
32
+ * work to the engineering team. The exempt set is policy-configurable
33
+ * (`policy.delegation_advisory.exempt_subagents`) with a 5-entry
34
+ * built-in default; this module takes the resolved list as an argument
35
+ * rather than reading policy itself, so it stays a pure filesystem
36
+ * helper with no policy-loader dependency.
37
+ *
38
+ * # Skills
39
+ *
40
+ * The `Skill` delegation tool is intentionally NOT roster-gated. A
41
+ * skill invocation (`deep-dive`, `due-diligence`, …) is always a real
42
+ * delegation signal — there is no "built-in skill" equivalent of
43
+ * `general-purpose`. The advisory's "did this session delegate"
44
+ * predicate counts every `Skill` signal and every non-exempt `Agent`
45
+ * signal whose `subagent_type` is in the discovered roster.
46
+ */
47
+ /**
48
+ * Default exempt subagent names — Claude Code's built-in helper agents
49
+ * that are dispatched through the `Agent` tool but are not curated
50
+ * specialists. Mirrors the schema-layer default in
51
+ * `DelegationAdvisoryPolicySchema` (`src/policy/loader.ts`). Exported
52
+ * so callers that have no policy in scope (the bash-hook fallback
53
+ * path) can still apply the same filter.
54
+ */
55
+ export declare const DEFAULT_EXEMPT_SUBAGENTS: readonly string[];
56
+ export interface RosterDiscoveryResult {
57
+ /**
58
+ * Sorted list of discovered curated-specialist names — the basename
59
+ * (sans `.md`) of every file under `.claude/agents/`. Empty when the
60
+ * directory is absent or unreadable.
61
+ */
62
+ roster: string[];
63
+ /**
64
+ * Absolute path actually scanned. Returned for diagnostics — doctor
65
+ * surfaces it, the `rea hook` subcommand echoes it in `--json` mode.
66
+ */
67
+ agentsDir: string;
68
+ /**
69
+ * `true` when `.claude/agents/` exists and was read. `false` when it
70
+ * is absent or a read error occurred — callers treat that as "no
71
+ * roster, every Agent delegation is non-exempt-but-also-unverifiable"
72
+ * and fall back to the exempt-list-only filter.
73
+ */
74
+ discovered: boolean;
75
+ }
76
+ /**
77
+ * Discover the curated-specialist roster by listing `*.md` files under
78
+ * `<baseDir>/.claude/agents/`. Pure filesystem read — never throws; an
79
+ * absent or unreadable directory yields `discovered: false` with an
80
+ * empty roster.
81
+ *
82
+ * The `.md` extension match is case-insensitive (`.MD` on a
83
+ * case-preserving-but-insensitive filesystem still counts) and the
84
+ * basename is taken verbatim — `rea-orchestrator.md` →
85
+ * `rea-orchestrator`. Subdirectories and non-`.md` files (READMEs,
86
+ * `.DS_Store`, editor swap files) are skipped.
87
+ */
88
+ export declare function discoverRoster(baseDir: string): RosterDiscoveryResult;
89
+ /**
90
+ * The delegation-nudge predicate, factored out so the CLI subcommand,
91
+ * the `audit specialists` reader, and the doctor smoke check all apply
92
+ * identical logic.
93
+ *
94
+ * Returns `true` when the given `(delegation_tool, subagent_type)` pair
95
+ * counts as a REAL delegation — i.e. the kind that should suppress the
96
+ * advisory nudge:
97
+ *
98
+ * - `Skill` → always counts. There is no "built-in skill" exemption.
99
+ * - `Agent` → counts when the `subagent_type` is NOT in `exempt` AND
100
+ * (the roster was discovered AND contains the name, OR the roster
101
+ * was NOT discovered — in which case we cannot verify and fall
102
+ * back to "non-exempt name counts"). The non-discovered fallback
103
+ * is deliberately permissive: a consumer who deleted
104
+ * `.claude/agents/` has bigger problems than a missed nudge, and a
105
+ * false negative (no nudge when one was due) is far less corrosive
106
+ * than a false positive.
107
+ *
108
+ * `exempt` comparison is case-sensitive — the built-in helper names
109
+ * (`Explore`, `Plan`) are capitalized and the curated specialists are
110
+ * kebab-case, so there is no realistic collision, and a case-folded
111
+ * compare would risk a curated `Plan-something` agent being wrongly
112
+ * exempted.
113
+ */
114
+ export declare function countsAsRealDelegation(args: {
115
+ delegationTool: 'Agent' | 'Skill';
116
+ subagentType: string;
117
+ roster: RosterDiscoveryResult;
118
+ exempt: readonly string[];
119
+ }): boolean;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Live `.claude/agents/` roster discovery (0.31.0+).
3
+ *
4
+ * 0.29.0 shipped the delegation-telemetry observability layer. 0.31.0
5
+ * closes the loop with the *nudge* — and the nudge needs to know what
6
+ * counts as a "real" specialist delegation versus a built-in helper.
7
+ *
8
+ * # Why discovery, not a hardcoded list
9
+ *
10
+ * `src/cli/doctor.ts` already carries an `EXPECTED_AGENTS` constant,
11
+ * but it is a deliberately-frozen 10-entry subset — the minimum roster
12
+ * `rea init` guarantees, pinned so a regression that drops a curated
13
+ * agent from the install manifest trips a doctor failure. It is NOT
14
+ * the live roster: this repo ships 23 agents, consumers add their own,
15
+ * and the curated set grows release over release. Keying the
16
+ * delegation nudge off `EXPECTED_AGENTS` would mean a session that
17
+ * delegated exclusively to `principal-engineer` and `data-architect`
18
+ * (both real, curated, neither in the frozen subset) still got nudged
19
+ * — exactly the false positive that erodes trust in an advisory.
20
+ *
21
+ * So the roster is discovered at read time: every `*.md` file under
22
+ * `<baseDir>/.claude/agents/` is a curated specialist. The basename
23
+ * (sans `.md`) is the `subagent_type` Claude Code's `Agent` tool
24
+ * reports, so the mapping is direct.
25
+ *
26
+ * # The exempt set
27
+ *
28
+ * Claude Code ships built-in helpers — `general-purpose`, `Explore`,
29
+ * `Plan`, `output-style-setup`, `statusline-setup` — that are dispatched
30
+ * through the same `Agent` tool but are NOT curated specialists. A
31
+ * session that only ever delegated to those has not actually routed
32
+ * work to the engineering team. The exempt set is policy-configurable
33
+ * (`policy.delegation_advisory.exempt_subagents`) with a 5-entry
34
+ * built-in default; this module takes the resolved list as an argument
35
+ * rather than reading policy itself, so it stays a pure filesystem
36
+ * helper with no policy-loader dependency.
37
+ *
38
+ * # Skills
39
+ *
40
+ * The `Skill` delegation tool is intentionally NOT roster-gated. A
41
+ * skill invocation (`deep-dive`, `due-diligence`, …) is always a real
42
+ * delegation signal — there is no "built-in skill" equivalent of
43
+ * `general-purpose`. The advisory's "did this session delegate"
44
+ * predicate counts every `Skill` signal and every non-exempt `Agent`
45
+ * signal whose `subagent_type` is in the discovered roster.
46
+ */
47
+ import fs from 'node:fs';
48
+ import path from 'node:path';
49
+ /**
50
+ * Default exempt subagent names — Claude Code's built-in helper agents
51
+ * that are dispatched through the `Agent` tool but are not curated
52
+ * specialists. Mirrors the schema-layer default in
53
+ * `DelegationAdvisoryPolicySchema` (`src/policy/loader.ts`). Exported
54
+ * so callers that have no policy in scope (the bash-hook fallback
55
+ * path) can still apply the same filter.
56
+ */
57
+ export const DEFAULT_EXEMPT_SUBAGENTS = [
58
+ 'general-purpose',
59
+ 'Explore',
60
+ 'Plan',
61
+ 'output-style-setup',
62
+ 'statusline-setup',
63
+ ];
64
+ /**
65
+ * Discover the curated-specialist roster by listing `*.md` files under
66
+ * `<baseDir>/.claude/agents/`. Pure filesystem read — never throws; an
67
+ * absent or unreadable directory yields `discovered: false` with an
68
+ * empty roster.
69
+ *
70
+ * The `.md` extension match is case-insensitive (`.MD` on a
71
+ * case-preserving-but-insensitive filesystem still counts) and the
72
+ * basename is taken verbatim — `rea-orchestrator.md` →
73
+ * `rea-orchestrator`. Subdirectories and non-`.md` files (READMEs,
74
+ * `.DS_Store`, editor swap files) are skipped.
75
+ */
76
+ export function discoverRoster(baseDir) {
77
+ const agentsDir = path.join(baseDir, '.claude', 'agents');
78
+ let entries;
79
+ try {
80
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
81
+ }
82
+ catch {
83
+ // ENOENT (no .claude/agents/), ENOTDIR, EACCES — all collapse to
84
+ // "no roster discovered". The caller falls back to the exempt-list
85
+ // filter alone.
86
+ return { roster: [], agentsDir, discovered: false };
87
+ }
88
+ const roster = [];
89
+ for (const entry of entries) {
90
+ // Only regular files. A directory named `foo.md` is not an agent.
91
+ if (!entry.isFile())
92
+ continue;
93
+ const name = entry.name;
94
+ if (!name.toLowerCase().endsWith('.md'))
95
+ continue;
96
+ const base = name.slice(0, name.length - 3);
97
+ if (base.length === 0)
98
+ continue;
99
+ roster.push(base);
100
+ }
101
+ roster.sort();
102
+ return { roster, agentsDir, discovered: true };
103
+ }
104
+ /**
105
+ * The delegation-nudge predicate, factored out so the CLI subcommand,
106
+ * the `audit specialists` reader, and the doctor smoke check all apply
107
+ * identical logic.
108
+ *
109
+ * Returns `true` when the given `(delegation_tool, subagent_type)` pair
110
+ * counts as a REAL delegation — i.e. the kind that should suppress the
111
+ * advisory nudge:
112
+ *
113
+ * - `Skill` → always counts. There is no "built-in skill" exemption.
114
+ * - `Agent` → counts when the `subagent_type` is NOT in `exempt` AND
115
+ * (the roster was discovered AND contains the name, OR the roster
116
+ * was NOT discovered — in which case we cannot verify and fall
117
+ * back to "non-exempt name counts"). The non-discovered fallback
118
+ * is deliberately permissive: a consumer who deleted
119
+ * `.claude/agents/` has bigger problems than a missed nudge, and a
120
+ * false negative (no nudge when one was due) is far less corrosive
121
+ * than a false positive.
122
+ *
123
+ * `exempt` comparison is case-sensitive — the built-in helper names
124
+ * (`Explore`, `Plan`) are capitalized and the curated specialists are
125
+ * kebab-case, so there is no realistic collision, and a case-folded
126
+ * compare would risk a curated `Plan-something` agent being wrongly
127
+ * exempted.
128
+ */
129
+ export function countsAsRealDelegation(args) {
130
+ if (args.delegationTool === 'Skill')
131
+ return true;
132
+ // Agent path.
133
+ if (args.exempt.includes(args.subagentType))
134
+ return false;
135
+ if (!args.roster.discovered) {
136
+ // Roster unverifiable — a non-exempt Agent name is the best signal
137
+ // we have. Count it.
138
+ return true;
139
+ }
140
+ return args.roster.roster.includes(args.subagentType);
141
+ }
@@ -2066,8 +2066,20 @@ export interface SettingsValidationResult {
2066
2066
  * best-effort scan of any `hooks: { PreToolUse: [...] }` shape we
2067
2067
  * recognize, so the operator still sees traversal + missing-hooks
2068
2068
  * findings.
2069
+ *
2070
+ * `strict` selects the schema:
2071
+ * - `false` (default) — `SettingsSchema`, top-level `.passthrough()`.
2072
+ * Unknown harness keys pass. Used by `rea upgrade` and advisory
2073
+ * `rea doctor`.
2074
+ * - `true` — `SettingsSchemaStrict`, top-level `.strict()`. Unknown
2075
+ * keys fail the parse. Used only by `rea doctor --strict` (the CI
2076
+ * gate path). 0.30.1 round-5 P2: the strict schema existed since
2077
+ * 0.30.0 but `validateSettings` never accepted the selector, so
2078
+ * `rea doctor --strict` silently ran the lenient schema.
2069
2079
  */
2070
- export declare function validateSettings(input: unknown): SettingsValidationResult;
2080
+ export declare function validateSettings(input: unknown, options?: {
2081
+ strict?: boolean;
2082
+ }): SettingsValidationResult;
2071
2083
  /**
2072
2084
  * Cross-check: every name in `EXPECTED_HOOKS` should appear as the
2073
2085
  * basename of at least one `command` across PreToolUse + PostToolUse.
@@ -154,8 +154,18 @@ export function validateNoTraversal(settings) {
154
154
  * best-effort scan of any `hooks: { PreToolUse: [...] }` shape we
155
155
  * recognize, so the operator still sees traversal + missing-hooks
156
156
  * findings.
157
+ *
158
+ * `strict` selects the schema:
159
+ * - `false` (default) — `SettingsSchema`, top-level `.passthrough()`.
160
+ * Unknown harness keys pass. Used by `rea upgrade` and advisory
161
+ * `rea doctor`.
162
+ * - `true` — `SettingsSchemaStrict`, top-level `.strict()`. Unknown
163
+ * keys fail the parse. Used only by `rea doctor --strict` (the CI
164
+ * gate path). 0.30.1 round-5 P2: the strict schema existed since
165
+ * 0.30.0 but `validateSettings` never accepted the selector, so
166
+ * `rea doctor --strict` silently ran the lenient schema.
157
167
  */
158
- export function validateSettings(input) {
168
+ export function validateSettings(input, options = {}) {
159
169
  const result = {
160
170
  parsed: false,
161
171
  settings: null,
@@ -164,7 +174,8 @@ export function validateSettings(input) {
164
174
  missingReaHooks: [],
165
175
  warnings: [],
166
176
  };
167
- const parsed = SettingsSchema.safeParse(input);
177
+ const schema = options.strict === true ? SettingsSchemaStrict : SettingsSchema;
178
+ const parsed = schema.safeParse(input);
168
179
  if (parsed.success) {
169
180
  result.parsed = true;
170
181
  result.settings = parsed.data;
@@ -249,7 +249,7 @@ declare const PolicySchema: z.ZodObject<{
249
249
  attribution: z.ZodOptional<z.ZodObject<{
250
250
  co_author: z.ZodOptional<z.ZodEffects<z.ZodObject<{
251
251
  enabled: z.ZodOptional<z.ZodBoolean>;
252
- name: z.ZodOptional<z.ZodString>;
252
+ name: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
253
253
  email: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
254
254
  skip_merge: z.ZodOptional<z.ZodBoolean>;
255
255
  }, "strict", z.ZodTypeAny, {
@@ -288,6 +288,19 @@ declare const PolicySchema: z.ZodObject<{
288
288
  skip_merge?: boolean | undefined;
289
289
  } | undefined;
290
290
  }>>;
291
+ delegation_advisory: z.ZodOptional<z.ZodObject<{
292
+ enabled: z.ZodDefault<z.ZodBoolean>;
293
+ threshold: z.ZodDefault<z.ZodNumber>;
294
+ exempt_subagents: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
295
+ }, "strict", z.ZodTypeAny, {
296
+ enabled: boolean;
297
+ threshold: number;
298
+ exempt_subagents: string[];
299
+ }, {
300
+ enabled?: boolean | undefined;
301
+ threshold?: number | undefined;
302
+ exempt_subagents?: string[] | undefined;
303
+ }>>;
291
304
  }, "strict", z.ZodTypeAny, {
292
305
  version: string;
293
306
  profile: string;
@@ -361,6 +374,11 @@ declare const PolicySchema: z.ZodObject<{
361
374
  skip_merge?: boolean | undefined;
362
375
  } | undefined;
363
376
  } | undefined;
377
+ delegation_advisory?: {
378
+ enabled: boolean;
379
+ threshold: number;
380
+ exempt_subagents: string[];
381
+ } | undefined;
364
382
  }, {
365
383
  version: string;
366
384
  profile: string;
@@ -434,6 +452,11 @@ declare const PolicySchema: z.ZodObject<{
434
452
  skip_merge?: boolean | undefined;
435
453
  } | undefined;
436
454
  } | undefined;
455
+ delegation_advisory?: {
456
+ enabled?: boolean | undefined;
457
+ threshold?: number | undefined;
458
+ exempt_subagents?: string[] | undefined;
459
+ } | undefined;
437
460
  }>;
438
461
  /**
439
462
  * Async policy loader with TTL cache and mtime-based invalidation.
@@ -231,10 +231,24 @@ const GatewayPolicySchema = z
231
231
  * Stricter validation is the consumer's job — RFC 5322 is too permissive
232
232
  * for an opt-in audit footprint anyway.
233
233
  */
234
+ // 0.30.1 round-5 P2: the `name` value lands verbatim inside a
235
+ // `Co-Authored-By: <name> <email>` git trailer. A newline or carriage
236
+ // return in `name` would split the trailer across lines — git's
237
+ // `interpret-trailers` would drop the continuation and the augmenter
238
+ // could even inject arbitrary extra trailer lines. Reject any ASCII
239
+ // control character (newline, CR, tab, NUL, …) in `name`.
240
+ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
234
241
  const AttributionCoAuthorSchema = z
235
242
  .object({
236
243
  enabled: z.boolean().optional(),
237
- name: z.string().optional(),
244
+ name: z
245
+ .string()
246
+ .refine((v) => !CONTROL_CHAR_RE.test(v), {
247
+ message: 'attribution.co_author.name must not contain control characters ' +
248
+ '(newlines, tabs, carriage returns) — the value is written verbatim ' +
249
+ 'into a single-line Co-Authored-By git trailer.',
250
+ })
251
+ .optional(),
238
252
  email: z
239
253
  .string()
240
254
  .regex(/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/, {
@@ -275,6 +289,45 @@ const AttributionPolicySchema = z
275
289
  co_author: AttributionCoAuthorSchema.optional(),
276
290
  })
277
291
  .strict();
292
+ /**
293
+ * 0.31.0 — delegation-advisory nudge policy. Drives the
294
+ * `delegation-advisory.sh` PostToolUse hook (matcher
295
+ * `Bash|Edit|Write|MultiEdit|NotebookEdit`): when a session crosses
296
+ * `threshold` write-class tool calls without a `rea.delegation_signal`
297
+ * record (to a non-exempt subagent), the hook emits a one-time stderr
298
+ * advisory. The hook is advisory-only — exit 0 always except HALT.
299
+ *
300
+ * Defaults live here at the schema layer, not in the hook: a vanilla
301
+ * install with no `delegation_advisory` block gets `enabled: false`
302
+ * (silent no-op), `threshold: 25`, and the 5-entry built-in exempt
303
+ * list. The `bst-internal*` profiles pin `enabled: true`; OSS profiles
304
+ * leave it `false` so consumers opt in.
305
+ *
306
+ * `threshold` is a positive integer — a single write-class count
307
+ * rather than the 0.29.0 design memo's "15 edits + 5 Bash" split.
308
+ * Modeling the threshold as one number keeps the hook's counter file
309
+ * a single integer and the policy surface a single knob; the
310
+ * distinction between an Edit and a Bash call doesn't change the
311
+ * signal the nudge exists to send ("you've done a lot solo").
312
+ *
313
+ * Strict mode rejects unknown keys so a typo (`thresholds`,
314
+ * `exempt_subagent`) fails loudly at policy load.
315
+ */
316
+ const DelegationAdvisoryPolicySchema = z
317
+ .object({
318
+ enabled: z.boolean().default(false),
319
+ threshold: z.number().int().positive().default(25),
320
+ exempt_subagents: z
321
+ .array(z.string())
322
+ .default([
323
+ 'general-purpose',
324
+ 'Explore',
325
+ 'Plan',
326
+ 'output-style-setup',
327
+ 'statusline-setup',
328
+ ]),
329
+ })
330
+ .strict();
278
331
  const PolicySchema = z
279
332
  .object({
280
333
  version: z.string(),
@@ -327,6 +380,13 @@ const PolicySchema = z
327
380
  // `AttributionCoAuthorSchema` fails closed when `enabled: true` but
328
381
  // `name`/`email` are empty so we never ship a half-configured trailer.
329
382
  attribution: AttributionPolicySchema.optional(),
383
+ // 0.31.0 delegation-advisory nudge — drives the
384
+ // `delegation-advisory.sh` PostToolUse hook. `.optional()` so a
385
+ // vanilla install with no block sees the hook as a silent no-op
386
+ // (the hook reads `enabled` via `rea hook policy-get` and exits 0
387
+ // when unset/false). When the block IS present the inner schema
388
+ // supplies defaults for any omitted field.
389
+ delegation_advisory: DelegationAdvisoryPolicySchema.optional(),
330
390
  })
331
391
  .strict();
332
392
  const DEFAULT_CACHE_TTL_MS = 30_000;
@@ -108,6 +108,19 @@ export declare const ProfileSchema: z.ZodObject<{
108
108
  skip_merge?: boolean | undefined;
109
109
  } | undefined;
110
110
  }>>;
111
+ delegation_advisory: z.ZodOptional<z.ZodObject<{
112
+ enabled: z.ZodOptional<z.ZodBoolean>;
113
+ threshold: z.ZodOptional<z.ZodNumber>;
114
+ exempt_subagents: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
115
+ }, "strict", z.ZodTypeAny, {
116
+ enabled?: boolean | undefined;
117
+ threshold?: number | undefined;
118
+ exempt_subagents?: string[] | undefined;
119
+ }, {
120
+ enabled?: boolean | undefined;
121
+ threshold?: number | undefined;
122
+ exempt_subagents?: string[] | undefined;
123
+ }>>;
111
124
  }, "strict", z.ZodTypeAny, {
112
125
  autonomy_level?: AutonomyLevel | undefined;
113
126
  max_autonomy_level?: AutonomyLevel | undefined;
@@ -142,6 +155,11 @@ export declare const ProfileSchema: z.ZodObject<{
142
155
  skip_merge?: boolean | undefined;
143
156
  } | undefined;
144
157
  } | undefined;
158
+ delegation_advisory?: {
159
+ enabled?: boolean | undefined;
160
+ threshold?: number | undefined;
161
+ exempt_subagents?: string[] | undefined;
162
+ } | undefined;
145
163
  }, {
146
164
  autonomy_level?: AutonomyLevel | undefined;
147
165
  max_autonomy_level?: AutonomyLevel | undefined;
@@ -176,6 +194,11 @@ export declare const ProfileSchema: z.ZodObject<{
176
194
  skip_merge?: boolean | undefined;
177
195
  } | undefined;
178
196
  } | undefined;
197
+ delegation_advisory?: {
198
+ enabled?: boolean | undefined;
199
+ threshold?: number | undefined;
200
+ exempt_subagents?: string[] | undefined;
201
+ } | undefined;
179
202
  }>;
180
203
  export type Profile = z.infer<typeof ProfileSchema>;
181
204
  /** Hard defaults applied before any profile or wizard answer. */
@@ -100,6 +100,22 @@ export const ProfileSchema = z
100
100
  })
101
101
  .strict()
102
102
  .optional(),
103
+ // 0.31.0+ delegation-advisory nudge. `bst-internal*` profiles pin
104
+ // `enabled: true`; external profiles ship `enabled: false`. The
105
+ // profile-layer schema mirrors the policy-loader's
106
+ // `DelegationAdvisoryPolicySchema` but leaves every field optional
107
+ // — defaults are applied at the policy-loader layer when the
108
+ // materialized file is parsed, so a profile that only declares
109
+ // `enabled` doesn't need to also restate `threshold`. Strict mode
110
+ // still rejects typos at init time.
111
+ delegation_advisory: z
112
+ .object({
113
+ enabled: z.boolean().optional(),
114
+ threshold: z.number().int().positive().optional(),
115
+ exempt_subagents: z.array(z.string()).optional(),
116
+ })
117
+ .strict()
118
+ .optional(),
103
119
  })
104
120
  .strict();
105
121
  /** Hard defaults applied before any profile or wizard answer. */
@@ -367,6 +367,59 @@ export interface AttributionCoAuthorPolicy {
367
367
  email?: string;
368
368
  skip_merge?: boolean;
369
369
  }
370
+ /**
371
+ * Delegation-advisory nudge policy (0.31.0+).
372
+ *
373
+ * 0.29.0 shipped the delegation-telemetry *observability* layer (the
374
+ * `Agent|Skill` PreToolUse capture hook + `rea audit specialists`
375
+ * reader). 0.31.0 closes the loop with the *nudge*: the
376
+ * `delegation-advisory.sh` PostToolUse hook (matcher
377
+ * `Bash|Edit|Write|MultiEdit|NotebookEdit`) counts the current
378
+ * session's write-class tool calls and, when that count crosses
379
+ * `threshold` WITHOUT a `rea.delegation_signal` record landing in the
380
+ * session, prints a one-time stderr advisory: "this session has done a
381
+ * lot of work without delegating to a specialist".
382
+ *
383
+ * The advisory is purely informational — the hook always exits 0
384
+ * (except under HALT, which exits 2 to keep the kill-switch contract
385
+ * uniform). It NEVER blocks a tool call.
386
+ *
387
+ * Profile defaults: `enabled: true` for the `bst-internal*` profiles
388
+ * (BST's own delegation discipline is load-bearing); `enabled: false`
389
+ * for every external profile (`open-source*`, `minimal`,
390
+ * `client-engagement`, `lit-wc`) — OSS consumers opt in per-repo via
391
+ * `.rea/policy.yaml`, since "you should delegate more" is an opinion
392
+ * not every team shares.
393
+ */
394
+ export interface DelegationAdvisoryPolicy {
395
+ /**
396
+ * Master switch. When `false` (or the whole block is omitted) the
397
+ * `delegation-advisory.sh` hook is a silent no-op. Default `false` at
398
+ * the schema layer; `bst-internal*` profiles pin `true`.
399
+ */
400
+ enabled?: boolean;
401
+ /**
402
+ * Write-class tool-call count at which the advisory fires. The
403
+ * `delegation-advisory.sh` hook maintains a per-session counter file
404
+ * and emits the nudge the first time the counter reaches this value
405
+ * with zero delegation signals recorded for the session. Default
406
+ * `25` — a session that has run 25 Bash/Edit/Write/MultiEdit/
407
+ * NotebookEdit calls without once dispatching a specialist is doing
408
+ * meaningful work solo. Must be a positive integer.
409
+ */
410
+ threshold?: number;
411
+ /**
412
+ * Subagent / skill names that do NOT count as "real delegation" for
413
+ * the purpose of suppressing the advisory. A session that only ever
414
+ * delegated to `general-purpose` / `Explore` / `Plan` (the built-in
415
+ * Claude Code helpers) has not actually routed work to a curated
416
+ * specialist, so those signals don't reset the nudge. Default:
417
+ * `["general-purpose", "Explore", "Plan", "output-style-setup",
418
+ * "statusline-setup"]`. A delegation signal whose `subagent_type` is
419
+ * in this list is ignored when deciding whether to fire.
420
+ */
421
+ exempt_subagents?: string[];
422
+ }
370
423
  /**
371
424
  * G9 — injection tier escalation knobs. The classifier bucketed matches into
372
425
  * `clean` / `suspicious` / `likely_injection`; this block governs what happens
@@ -472,4 +525,12 @@ export interface Policy {
472
525
  * trailer are no-ops. See `AttributionPolicy` for the full contract.
473
526
  */
474
527
  attribution?: AttributionPolicy;
528
+ /**
529
+ * Delegation-advisory nudge (0.31.0+). When `enabled: true`, the
530
+ * `delegation-advisory.sh` PostToolUse hook emits a one-time stderr
531
+ * advisory when a session crosses `threshold` write-class tool calls
532
+ * without dispatching a curated specialist. Advisory only — never
533
+ * blocks. See `DelegationAdvisoryPolicy` for the full contract.
534
+ */
535
+ delegation_advisory?: DelegationAdvisoryPolicy;
475
536
  }