@bookedsolid/rea 0.28.2 → 0.30.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 +295 -0
- package/MIGRATING.md +75 -0
- package/dist/audit/append.d.ts +1 -0
- package/dist/audit/append.js +1 -0
- package/dist/audit/delegation-event.d.ts +215 -0
- package/dist/audit/delegation-event.js +113 -0
- package/dist/cli/audit-specialists.d.ts +113 -0
- package/dist/cli/audit-specialists.js +220 -0
- package/dist/cli/doctor.d.ts +114 -1
- package/dist/cli/doctor.js +523 -5
- package/dist/cli/hook.d.ts +40 -8
- package/dist/cli/hook.js +305 -8
- package/dist/cli/index.js +9 -0
- package/dist/cli/init.js +120 -0
- package/dist/cli/install/manifest-schema.d.ts +6 -6
- package/dist/cli/install/prepare-commit-msg.d.ts +83 -0
- package/dist/cli/install/prepare-commit-msg.js +208 -0
- package/dist/cli/install/settings-merge.js +20 -0
- package/dist/cli/upgrade.js +34 -0
- package/dist/config/settings-schema.d.ts +2087 -0
- package/dist/config/settings-schema.js +294 -0
- package/dist/config/tier-map.js +22 -1
- package/dist/policy/loader.d.ts +58 -0
- package/dist/policy/loader.js +68 -0
- package/dist/policy/profiles.d.ts +48 -0
- package/dist/policy/profiles.js +25 -0
- package/dist/policy/types.d.ts +51 -0
- package/dist/registry/loader.d.ts +12 -12
- package/hooks/delegation-capture.sh +158 -0
- package/package.json +1 -1
- package/profiles/bst-internal-no-codex.yaml +15 -0
- package/profiles/bst-internal.yaml +16 -0
- package/profiles/client-engagement.yaml +14 -0
- package/profiles/lit-wc.yaml +14 -0
- package/profiles/minimal.yaml +16 -0
- package/profiles/open-source-no-codex.yaml +13 -0
- package/profiles/open-source.yaml +13 -0
- package/templates/prepare-commit-msg.husky.sh +295 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `rea.delegation_signal` audit event shape
|
|
3
|
+
* (0.29.0+).
|
|
4
|
+
*
|
|
5
|
+
* 0.29.0 — delegation-telemetry MVP. Claude Code's PreToolUse hook tree
|
|
6
|
+
* gains a new matcher (`Agent|Skill`) that pipes a redacted, hashed
|
|
7
|
+
* record of every subagent dispatch and skill invocation into
|
|
8
|
+
* `.rea/audit.jsonl`. The signal is observational, not gating — it
|
|
9
|
+
* answers "which specialists is this session actually delegating to,
|
|
10
|
+
* and how often" without altering the autonomy tree.
|
|
11
|
+
*
|
|
12
|
+
* # The two delegation tools
|
|
13
|
+
*
|
|
14
|
+
* Current Claude Code exposes exactly two delegation surfaces:
|
|
15
|
+
*
|
|
16
|
+
* - `Agent` — dispatches a curated subagent (rea-orchestrator,
|
|
17
|
+
* code-reviewer, …). The agent name is at
|
|
18
|
+
* `tool_input.subagent_type`.
|
|
19
|
+
* - `Skill` — invokes a named skill (deep-dive, /loop, …). The skill
|
|
20
|
+
* name is at `tool_input.skill`.
|
|
21
|
+
*
|
|
22
|
+
* mcp-protocol-specialist verified BOTH payload paths against current
|
|
23
|
+
* Claude Code. A Skill that internally forks an Agent fires PreToolUse
|
|
24
|
+
* TWICE (Skill then Agent) for the same logical action; v1 records
|
|
25
|
+
* both — deduplication lives in the reader, not the writer.
|
|
26
|
+
*
|
|
27
|
+
* # Not `Task`
|
|
28
|
+
*
|
|
29
|
+
* In current Claude Code the tools are `Agent` and `Skill`. The names
|
|
30
|
+
* `TaskCreate`/`TaskList`/`TaskUpdate` belong to the unrelated todo-list
|
|
31
|
+
* tool surface and MUST NOT match. The settings.json matcher is
|
|
32
|
+
* `Agent|Skill` everywhere — anchored on a `^…$` boundary by the hook
|
|
33
|
+
* runtime, so `Agent` doesn't accidentally collide with hypothetical
|
|
34
|
+
* future tools named `Agentic…`.
|
|
35
|
+
*
|
|
36
|
+
* # Privacy invariant
|
|
37
|
+
*
|
|
38
|
+
* The raw `description` / `prompt` payload NEVER touches `.rea/audit.jsonl`
|
|
39
|
+
* — only its SHA-256 hash. The hash is collision-resistant identification
|
|
40
|
+
* (two identical prompts produce identical hashes, enabling
|
|
41
|
+
* delegation-pattern discovery) without persisting prompt content.
|
|
42
|
+
*
|
|
43
|
+
* The agent / skill name field DOES land in the audit log, but is run
|
|
44
|
+
* through `redactSecrets` first. A subagent_type that contains a
|
|
45
|
+
* planted credential string (synthetic AWS key, GitHub token, …) is
|
|
46
|
+
* replaced with `[REDACTED]` and the matching pattern names are
|
|
47
|
+
* appended to the record's `redacted_fields` envelope.
|
|
48
|
+
*
|
|
49
|
+
* # Provider seam (kept tiny)
|
|
50
|
+
*
|
|
51
|
+
* Unlike `rea.local_review`, this event does NOT have a `provider`
|
|
52
|
+
* field. The producer is always Claude Code's hook runtime and the
|
|
53
|
+
* `emission_source: 'rea-cli'` envelope is sufficient. If a future
|
|
54
|
+
* runtime (e.g. another agent host) wants to emit signals through the
|
|
55
|
+
* same channel, it writes the same shape with the same tool_name and
|
|
56
|
+
* relies on `session_id_observed` / `delegation_tool` for
|
|
57
|
+
* disambiguation.
|
|
58
|
+
*
|
|
59
|
+
* # Schema version
|
|
60
|
+
*
|
|
61
|
+
* The literal `schema_version: 1` is part of the metadata payload. Zod
|
|
62
|
+
* strict-mode rejects unknown fields, so a future v2 producer writing
|
|
63
|
+
* v2-only fields against a v1 consumer fails-loud rather than silently
|
|
64
|
+
* dropping data. Readers filter by `tool_name === 'rea.delegation_signal'`
|
|
65
|
+
* AND `metadata.schema_version === 1`.
|
|
66
|
+
*/
|
|
67
|
+
import { z } from 'zod';
|
|
68
|
+
/**
|
|
69
|
+
* Canonical `tool_name` on the audit record envelope. Readers filter on
|
|
70
|
+
* this exact literal — anything else is a different event class.
|
|
71
|
+
*/
|
|
72
|
+
export const DELEGATION_SIGNAL_TOOL_NAME = 'rea.delegation_signal';
|
|
73
|
+
/**
|
|
74
|
+
* `server_name` envelope value. The signal originates from Claude Code's
|
|
75
|
+
* hook runtime, captured by `rea hook delegation-signal` and appended
|
|
76
|
+
* via the public audit-record API. Naming it `claude-code-hooks` makes
|
|
77
|
+
* the producer surface unambiguous in forensic queries (vs.
|
|
78
|
+
* `'rea'` which is used for first-party rea CLI events like
|
|
79
|
+
* `rea.local_review`).
|
|
80
|
+
*/
|
|
81
|
+
export const DELEGATION_SIGNAL_SERVER_NAME = 'claude-code-hooks';
|
|
82
|
+
/**
|
|
83
|
+
* Schema version literal. Bumped only when the metadata shape gains a
|
|
84
|
+
* non-backwards-compatible change. Adding optional fields does NOT bump
|
|
85
|
+
* the version — zod's strict mode rejects them, so any new field MUST
|
|
86
|
+
* either ship with a major-version bump OR have its zod parser updated
|
|
87
|
+
* in lockstep.
|
|
88
|
+
*/
|
|
89
|
+
export const DELEGATION_SIGNAL_SCHEMA_VERSION = 1;
|
|
90
|
+
/**
|
|
91
|
+
* Strict-mode zod schema for the metadata payload. Unknown fields are
|
|
92
|
+
* rejected — a future v2 producer must bump `DELEGATION_SIGNAL_SCHEMA_VERSION`
|
|
93
|
+
* AND update this schema in the same commit, otherwise v1 readers fail
|
|
94
|
+
* loud rather than silently dropping new fields.
|
|
95
|
+
*
|
|
96
|
+
* The schema is exported so the CLI subcommand validates its OWN
|
|
97
|
+
* emitted metadata before passing it to `appendAuditRecord` — defense
|
|
98
|
+
* in depth against a future refactor that wires the field set
|
|
99
|
+
* incorrectly. (Same posture as `loadPolicy` self-validation.)
|
|
100
|
+
*/
|
|
101
|
+
export const DelegationSignalMetadataSchema = z
|
|
102
|
+
.object({
|
|
103
|
+
schema_version: z.literal(DELEGATION_SIGNAL_SCHEMA_VERSION),
|
|
104
|
+
delegation_tool: z.union([z.literal('Agent'), z.literal('Skill')]),
|
|
105
|
+
subagent_type: z.string(),
|
|
106
|
+
session_id_observed: z.string(),
|
|
107
|
+
parent_subagent_type: z.union([z.string(), z.null()]),
|
|
108
|
+
invocation_description_sha256: z
|
|
109
|
+
.string()
|
|
110
|
+
.regex(/^[0-9a-f]{64}$/, 'invocation_description_sha256 must be a lowercase 64-char hex SHA-256 digest'),
|
|
111
|
+
hook_event_timestamp: z.string().optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rea audit specialists` — 0.29.0 reader CLI for delegation-telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Walks `.rea/audit.jsonl`, filters records by
|
|
5
|
+
* `tool_name === 'rea.delegation_signal'`, groups by
|
|
6
|
+
* `metadata.subagent_type`, and prints a table (default) or JSON
|
|
7
|
+
* document (`--json`).
|
|
8
|
+
*
|
|
9
|
+
* # Current-session-only in v1
|
|
10
|
+
*
|
|
11
|
+
* v1 has NO `--since` flag and NO `--session=ID` flag. The principal-
|
|
12
|
+
* engineer scope-cut deferred both to 0.29.1. The filter is:
|
|
13
|
+
*
|
|
14
|
+
* - If `CLAUDE_SESSION_ID` is set, include only records whose
|
|
15
|
+
* `metadata.session_id_observed` matches.
|
|
16
|
+
* - Otherwise, include all records in the chain and print a note so
|
|
17
|
+
* the operator knows what they're seeing.
|
|
18
|
+
*
|
|
19
|
+
* # Output shape
|
|
20
|
+
*
|
|
21
|
+
* subagent_type count last_seen (UTC)
|
|
22
|
+
* rea-orchestrator 12 2026-05-12T21:30:00Z
|
|
23
|
+
* code-reviewer 5 2026-05-12T21:28:00Z
|
|
24
|
+
* deep-dive 2 2026-05-12T21:14:00Z
|
|
25
|
+
*
|
|
26
|
+
* JSON mode prints `{ session_filter, records, groups }` where
|
|
27
|
+
* `records` is the raw filtered subset (for piping into jq) and
|
|
28
|
+
* `groups` is the per-subagent rollup.
|
|
29
|
+
*/
|
|
30
|
+
import type { Command } from 'commander';
|
|
31
|
+
import { type DelegationTool } from '../audit/delegation-event.js';
|
|
32
|
+
export interface AuditSpecialistsOptions {
|
|
33
|
+
/** Emit a single JSON document on stdout instead of the table. */
|
|
34
|
+
json?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Override session filtering. Production callers omit (the CLI reads
|
|
37
|
+
* `CLAUDE_SESSION_ID` from the env). Tests set this so they don't
|
|
38
|
+
* mutate `process.env`.
|
|
39
|
+
*
|
|
40
|
+
* - `string` → filter to records whose `session_id_observed` matches.
|
|
41
|
+
* - `null` → no filter (show all records).
|
|
42
|
+
* - `undefined` → derive from env.
|
|
43
|
+
*/
|
|
44
|
+
sessionFilter?: string | null;
|
|
45
|
+
/** Override CWD. Tests set this; production uses `process.cwd()`. */
|
|
46
|
+
baseDir?: string;
|
|
47
|
+
}
|
|
48
|
+
interface DelegationGroup {
|
|
49
|
+
subagent_type: string;
|
|
50
|
+
count: number;
|
|
51
|
+
/** Latest envelope timestamp seen in the group. */
|
|
52
|
+
last_seen: string;
|
|
53
|
+
/** Breakdown of which delegation_tool fired (Agent vs. Skill). */
|
|
54
|
+
by_tool: Record<DelegationTool, number>;
|
|
55
|
+
}
|
|
56
|
+
interface DelegationRecord {
|
|
57
|
+
timestamp: string;
|
|
58
|
+
session_id_observed: string;
|
|
59
|
+
delegation_tool: DelegationTool;
|
|
60
|
+
subagent_type: string;
|
|
61
|
+
parent_subagent_type: string | null;
|
|
62
|
+
invocation_description_sha256: string;
|
|
63
|
+
hook_event_timestamp?: string;
|
|
64
|
+
}
|
|
65
|
+
interface AuditSpecialistsResult {
|
|
66
|
+
/** Which session filter was applied. `null` means "no filter". */
|
|
67
|
+
session_filter: string | null;
|
|
68
|
+
/** Was the filter derived from `CLAUDE_SESSION_ID` env? Informational. */
|
|
69
|
+
session_filter_source: 'env' | 'option' | 'none';
|
|
70
|
+
/** Raw matched records, in chain order. */
|
|
71
|
+
records: DelegationRecord[];
|
|
72
|
+
/** Per-subagent rollups, sorted by descending count then by name. */
|
|
73
|
+
groups: DelegationGroup[];
|
|
74
|
+
/**
|
|
75
|
+
* Files actually walked. v1 only walks `.rea/audit.jsonl`; future
|
|
76
|
+
* `--since` rotated-file support extends this.
|
|
77
|
+
*/
|
|
78
|
+
files_scanned: string[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the audit file and return delegation records (filtered + parsed
|
|
82
|
+
* into a reader-friendly shape). Malformed lines are skipped silently
|
|
83
|
+
* — `rea audit verify` is the right tool for chain integrity.
|
|
84
|
+
*/
|
|
85
|
+
export declare function loadDelegationRecords(baseDir: string, sessionFilter: string | null): Promise<{
|
|
86
|
+
records: DelegationRecord[];
|
|
87
|
+
filesScanned: string[];
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Group records by `subagent_type`. Sorts by descending count, then
|
|
91
|
+
* alphabetical on tie. `last_seen` is the latest envelope timestamp in
|
|
92
|
+
* the group.
|
|
93
|
+
*/
|
|
94
|
+
export declare function groupBySubagent(records: DelegationRecord[]): DelegationGroup[];
|
|
95
|
+
/**
|
|
96
|
+
* Computation-only entrypoint. Returns the full result so callers
|
|
97
|
+
* (CLI, tests) can render or assert. `runAuditSpecialists` is the thin
|
|
98
|
+
* commander wrapper that prints + exits.
|
|
99
|
+
*/
|
|
100
|
+
export declare function computeAuditSpecialists(options?: AuditSpecialistsOptions): Promise<AuditSpecialistsResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Commander entrypoint. Reads, renders, exits 0. The CLI is read-only
|
|
103
|
+
* — no audit-chain writes, no exit-code-as-verdict semantics.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runAuditSpecialists(options: AuditSpecialistsOptions): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Attach the `specialists` subcommand to the `rea audit` command group.
|
|
108
|
+
* Exported as a registrar so `src/cli/index.ts` can wire it next to the
|
|
109
|
+
* existing `rotate` and `verify` subcommands without leaking commander
|
|
110
|
+
* knowledge into this module.
|
|
111
|
+
*/
|
|
112
|
+
export declare function registerAuditSpecialistsSubcommand(auditCommand: Command): void;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rea audit specialists` — 0.29.0 reader CLI for delegation-telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Walks `.rea/audit.jsonl`, filters records by
|
|
5
|
+
* `tool_name === 'rea.delegation_signal'`, groups by
|
|
6
|
+
* `metadata.subagent_type`, and prints a table (default) or JSON
|
|
7
|
+
* document (`--json`).
|
|
8
|
+
*
|
|
9
|
+
* # Current-session-only in v1
|
|
10
|
+
*
|
|
11
|
+
* v1 has NO `--since` flag and NO `--session=ID` flag. The principal-
|
|
12
|
+
* engineer scope-cut deferred both to 0.29.1. The filter is:
|
|
13
|
+
*
|
|
14
|
+
* - If `CLAUDE_SESSION_ID` is set, include only records whose
|
|
15
|
+
* `metadata.session_id_observed` matches.
|
|
16
|
+
* - Otherwise, include all records in the chain and print a note so
|
|
17
|
+
* the operator knows what they're seeing.
|
|
18
|
+
*
|
|
19
|
+
* # Output shape
|
|
20
|
+
*
|
|
21
|
+
* subagent_type count last_seen (UTC)
|
|
22
|
+
* rea-orchestrator 12 2026-05-12T21:30:00Z
|
|
23
|
+
* code-reviewer 5 2026-05-12T21:28:00Z
|
|
24
|
+
* deep-dive 2 2026-05-12T21:14:00Z
|
|
25
|
+
*
|
|
26
|
+
* JSON mode prints `{ session_filter, records, groups }` where
|
|
27
|
+
* `records` is the raw filtered subset (for piping into jq) and
|
|
28
|
+
* `groups` is the per-subagent rollup.
|
|
29
|
+
*/
|
|
30
|
+
import fs from 'node:fs/promises';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { DELEGATION_SIGNAL_TOOL_NAME, DELEGATION_SIGNAL_SCHEMA_VERSION, } from '../audit/delegation-event.js';
|
|
33
|
+
import { AUDIT_FILE, REA_DIR, log } from './utils.js';
|
|
34
|
+
/**
|
|
35
|
+
* Type guard for a record that has the delegation-signal shape. Skips
|
|
36
|
+
* envelope shape validation (zod runs at write time); checks only the
|
|
37
|
+
* fields the reader actually uses.
|
|
38
|
+
*/
|
|
39
|
+
function isDelegationRecord(r) {
|
|
40
|
+
if (r.tool_name !== DELEGATION_SIGNAL_TOOL_NAME)
|
|
41
|
+
return false;
|
|
42
|
+
const m = r.metadata;
|
|
43
|
+
if (m === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
if (m.schema_version !== DELEGATION_SIGNAL_SCHEMA_VERSION)
|
|
46
|
+
return false;
|
|
47
|
+
if (m.delegation_tool !== 'Agent' && m.delegation_tool !== 'Skill')
|
|
48
|
+
return false;
|
|
49
|
+
if (typeof m.subagent_type !== 'string')
|
|
50
|
+
return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the audit file and return delegation records (filtered + parsed
|
|
55
|
+
* into a reader-friendly shape). Malformed lines are skipped silently
|
|
56
|
+
* — `rea audit verify` is the right tool for chain integrity.
|
|
57
|
+
*/
|
|
58
|
+
export async function loadDelegationRecords(baseDir, sessionFilter) {
|
|
59
|
+
const auditFile = path.join(baseDir, REA_DIR, AUDIT_FILE);
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await fs.readFile(auditFile, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e.code === 'ENOENT') {
|
|
66
|
+
return { records: [], filesScanned: [] };
|
|
67
|
+
}
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
const records = [];
|
|
71
|
+
for (const line of raw.split('\n')) {
|
|
72
|
+
if (line.length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(line);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!isDelegationRecord(parsed))
|
|
82
|
+
continue;
|
|
83
|
+
const m = parsed.metadata;
|
|
84
|
+
if (sessionFilter !== null && m.session_id_observed !== sessionFilter)
|
|
85
|
+
continue;
|
|
86
|
+
const rec = {
|
|
87
|
+
timestamp: parsed.timestamp,
|
|
88
|
+
session_id_observed: m.session_id_observed,
|
|
89
|
+
delegation_tool: m.delegation_tool,
|
|
90
|
+
subagent_type: m.subagent_type,
|
|
91
|
+
parent_subagent_type: m.parent_subagent_type,
|
|
92
|
+
invocation_description_sha256: m.invocation_description_sha256,
|
|
93
|
+
...(m.hook_event_timestamp !== undefined
|
|
94
|
+
? { hook_event_timestamp: m.hook_event_timestamp }
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
records.push(rec);
|
|
98
|
+
}
|
|
99
|
+
return { records, filesScanned: [auditFile] };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Group records by `subagent_type`. Sorts by descending count, then
|
|
103
|
+
* alphabetical on tie. `last_seen` is the latest envelope timestamp in
|
|
104
|
+
* the group.
|
|
105
|
+
*/
|
|
106
|
+
export function groupBySubagent(records) {
|
|
107
|
+
const byName = new Map();
|
|
108
|
+
for (const r of records) {
|
|
109
|
+
let g = byName.get(r.subagent_type);
|
|
110
|
+
if (g === undefined) {
|
|
111
|
+
g = {
|
|
112
|
+
subagent_type: r.subagent_type,
|
|
113
|
+
count: 0,
|
|
114
|
+
last_seen: r.timestamp,
|
|
115
|
+
by_tool: { Agent: 0, Skill: 0 },
|
|
116
|
+
};
|
|
117
|
+
byName.set(r.subagent_type, g);
|
|
118
|
+
}
|
|
119
|
+
g.count += 1;
|
|
120
|
+
g.by_tool[r.delegation_tool] += 1;
|
|
121
|
+
if (r.timestamp > g.last_seen)
|
|
122
|
+
g.last_seen = r.timestamp;
|
|
123
|
+
}
|
|
124
|
+
return Array.from(byName.values()).sort((a, b) => {
|
|
125
|
+
if (b.count !== a.count)
|
|
126
|
+
return b.count - a.count;
|
|
127
|
+
return a.subagent_type.localeCompare(b.subagent_type);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Computation-only entrypoint. Returns the full result so callers
|
|
132
|
+
* (CLI, tests) can render or assert. `runAuditSpecialists` is the thin
|
|
133
|
+
* commander wrapper that prints + exits.
|
|
134
|
+
*/
|
|
135
|
+
export async function computeAuditSpecialists(options = {}) {
|
|
136
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
137
|
+
let sessionFilter;
|
|
138
|
+
let source;
|
|
139
|
+
if (options.sessionFilter === undefined) {
|
|
140
|
+
const envId = process.env['CLAUDE_SESSION_ID'];
|
|
141
|
+
if (typeof envId === 'string' && envId.length > 0) {
|
|
142
|
+
sessionFilter = envId;
|
|
143
|
+
source = 'env';
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
sessionFilter = null;
|
|
147
|
+
source = 'none';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
sessionFilter = options.sessionFilter;
|
|
152
|
+
source = options.sessionFilter === null ? 'none' : 'option';
|
|
153
|
+
}
|
|
154
|
+
const { records, filesScanned } = await loadDelegationRecords(baseDir, sessionFilter);
|
|
155
|
+
const groups = groupBySubagent(records);
|
|
156
|
+
return {
|
|
157
|
+
session_filter: sessionFilter,
|
|
158
|
+
session_filter_source: source,
|
|
159
|
+
records,
|
|
160
|
+
groups,
|
|
161
|
+
files_scanned: filesScanned,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function renderTable(result) {
|
|
165
|
+
if (result.groups.length === 0) {
|
|
166
|
+
const note = result.session_filter !== null
|
|
167
|
+
? `No delegation signals recorded for session ${result.session_filter}.`
|
|
168
|
+
: 'No delegation signals recorded.';
|
|
169
|
+
return `${note}\n (Records are written by the .claude/hooks/delegation-capture.sh PreToolUse hook on every Agent/Skill dispatch.)\n`;
|
|
170
|
+
}
|
|
171
|
+
const headers = ['subagent_type', 'count', 'agent', 'skill', 'last_seen (UTC)'];
|
|
172
|
+
const rows = result.groups.map((g) => [
|
|
173
|
+
g.subagent_type,
|
|
174
|
+
String(g.count),
|
|
175
|
+
String(g.by_tool.Agent),
|
|
176
|
+
String(g.by_tool.Skill),
|
|
177
|
+
g.last_seen,
|
|
178
|
+
]);
|
|
179
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
|
|
180
|
+
const lines = [];
|
|
181
|
+
lines.push(headers.map((h, i) => h.padEnd(widths[i])).join(' '));
|
|
182
|
+
lines.push(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
lines.push(row.map((c, i) => c.padEnd(widths[i])).join(' '));
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n') + '\n';
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Commander entrypoint. Reads, renders, exits 0. The CLI is read-only
|
|
190
|
+
* — no audit-chain writes, no exit-code-as-verdict semantics.
|
|
191
|
+
*/
|
|
192
|
+
export async function runAuditSpecialists(options) {
|
|
193
|
+
const result = await computeAuditSpecialists(options);
|
|
194
|
+
if (options.json === true) {
|
|
195
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (result.session_filter !== null) {
|
|
199
|
+
log(`Delegation signals (session=${result.session_filter}, source=${result.session_filter_source}):`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
log('Delegation signals (no session filter — set $CLAUDE_SESSION_ID to scope; v1 omits --since / --session by design):');
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(renderTable(result));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Attach the `specialists` subcommand to the `rea audit` command group.
|
|
208
|
+
* Exported as a registrar so `src/cli/index.ts` can wire it next to the
|
|
209
|
+
* existing `rotate` and `verify` subcommands without leaking commander
|
|
210
|
+
* knowledge into this module.
|
|
211
|
+
*/
|
|
212
|
+
export function registerAuditSpecialistsSubcommand(auditCommand) {
|
|
213
|
+
auditCommand
|
|
214
|
+
.command('specialists')
|
|
215
|
+
.description('Summarize `rea.delegation_signal` audit records — counts per subagent / skill, last-seen timestamps, agent-vs-skill breakdown. Reads only the current `.rea/audit.jsonl`. Honors $CLAUDE_SESSION_ID for current-session filtering. v1 omits --since / --session by design (deferred to 0.29.1).')
|
|
216
|
+
.option('--json', 'emit JSON (records + groups) instead of the human-readable table. Composes with jq.')
|
|
217
|
+
.action(async (opts) => {
|
|
218
|
+
await runAuditSpecialists({ ...(opts.json === true ? { json: true } : {}) });
|
|
219
|
+
});
|
|
220
|
+
}
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -19,6 +19,44 @@ export interface CheckResult {
|
|
|
19
19
|
* Exported so tests can drive this without spinning up the full `runDoctor`.
|
|
20
20
|
*/
|
|
21
21
|
export declare function checkFingerprintStore(baseDir: string): Promise<CheckResult>;
|
|
22
|
+
/**
|
|
23
|
+
* 0.30.0 (Class M settings.json schema) — `EXPECTED_HOOKS` is exported
|
|
24
|
+
* so the schema validator at `src/config/settings-schema.ts` can
|
|
25
|
+
* cross-check rea-shipped hook filenames against entries it sees in
|
|
26
|
+
* a consumer's `.claude/settings.json`. The validator's `--strict`
|
|
27
|
+
* mode FAILS when a known rea-managed hook is missing from the
|
|
28
|
+
* consumer's registration; default mode logs a warn.
|
|
29
|
+
*/
|
|
30
|
+
export declare const EXPECTED_AGENTS: string[];
|
|
31
|
+
export declare const EXPECTED_HOOKS: string[];
|
|
32
|
+
/**
|
|
33
|
+
* 0.30.0 Class M — validate `.claude/settings.json` against the zod
|
|
34
|
+
* schema in `src/config/settings-schema.ts`.
|
|
35
|
+
*
|
|
36
|
+
* Status posture:
|
|
37
|
+
*
|
|
38
|
+
* - `strict: false` (default `rea doctor`) — emit a warn when:
|
|
39
|
+
* - zod parse fails (unknown top-level key, missing matcher,
|
|
40
|
+
* malformed hook entry, etc.),
|
|
41
|
+
* - any `command` contains a `..` traversal after stripping
|
|
42
|
+
* `$CLAUDE_PROJECT_DIR`,
|
|
43
|
+
* - any rea-shipped hook from `EXPECTED_HOOKS` is missing from
|
|
44
|
+
* the consumer's registrations.
|
|
45
|
+
* The harness keeps working — the schema only refuses to call
|
|
46
|
+
* malformed hook entries; we surface the issue without breaking
|
|
47
|
+
* the install.
|
|
48
|
+
*
|
|
49
|
+
* - `strict: true` (`rea doctor --strict`) — fail (hard) on the
|
|
50
|
+
* same conditions. Used by CI gates that want a hard floor on
|
|
51
|
+
* consumer settings.
|
|
52
|
+
*
|
|
53
|
+
* Returns `pass` when everything cleared. Returns one `CheckResult`
|
|
54
|
+
* per concern; called once and emits one result. Combined with the
|
|
55
|
+
* existing `checkSettingsJson` (which checks for the historical Bash
|
|
56
|
+
* + Write|Edit|MultiEdit|NotebookEdit matchers), gives consumers a
|
|
57
|
+
* complete picture.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkSettingsSchema(baseDir: string, strict: boolean): CheckResult;
|
|
22
60
|
/**
|
|
23
61
|
* Detect whether `baseDir` is a git repository. Returns true for the three
|
|
24
62
|
* shapes git itself accepts:
|
|
@@ -46,6 +84,7 @@ export declare function checkFingerprintStore(baseDir: string): Promise<CheckRes
|
|
|
46
84
|
* NOT a trust boundary. Do not key security decisions on the return value.
|
|
47
85
|
*/
|
|
48
86
|
export declare function isGitRepo(baseDir: string): boolean;
|
|
87
|
+
export declare function checkPrepareCommitMsgHook(baseDir: string): CheckResult;
|
|
49
88
|
/**
|
|
50
89
|
* Hard-fail when `policy.review.codex_required: true` but the `codex`
|
|
51
90
|
* binary is not on PATH. Pre-0.12.0 this prereq surfaced only at first
|
|
@@ -65,6 +104,63 @@ export declare function checkCodexBinaryOnPath(): CheckResult;
|
|
|
65
104
|
* the real probe.
|
|
66
105
|
*/
|
|
67
106
|
export declare function checksFromProbeState(state: CodexProbeState): CheckResult[];
|
|
107
|
+
/**
|
|
108
|
+
* 0.29.0 — verify the delegation-capture hook is registered in
|
|
109
|
+
* `.claude/settings.json` under PreToolUse with matcher `Agent|Skill`
|
|
110
|
+
* AND that the hook file exists at the expected dogfood path.
|
|
111
|
+
*
|
|
112
|
+
* Status posture for 0.29.0:
|
|
113
|
+
*
|
|
114
|
+
* The 0.29.0 release introduces a new desired-hook entry in
|
|
115
|
+
* `defaultDesiredHooks()` that `rea init` and `rea upgrade` will merge
|
|
116
|
+
* into consumer `.claude/settings.json` files. Existing consumer
|
|
117
|
+
* installs (and this repo's own dogfood, which is locked from
|
|
118
|
+
* agent-driven edits by `settings-protection.sh`) won't have the
|
|
119
|
+
* matcher registered until the operator runs `rea upgrade`.
|
|
120
|
+
*
|
|
121
|
+
* To keep the upgrade-lag period from breaking `rea doctor`, the
|
|
122
|
+
* check is `warn` (not `fail`) for 0.29.0. The detail message names
|
|
123
|
+
* the exact command to fix and points at the canonical
|
|
124
|
+
* `delegation-capture.sh` install. After 0.29.0+1 consumer-install
|
|
125
|
+
* cycles have propagated, this should be promoted to `fail` so a
|
|
126
|
+
* skipped upgrade is loud rather than silent. Codex round 2 P2
|
|
127
|
+
* (2026-05-12).
|
|
128
|
+
*
|
|
129
|
+
* Hook-file presence is verified separately by `checkHooksInstalled`
|
|
130
|
+
* via `EXPECTED_HOOKS` — that path stays at the hard-`fail` posture
|
|
131
|
+
* because file presence is part of the install manifest and doesn't
|
|
132
|
+
* suffer the same template-propagation lag.
|
|
133
|
+
*/
|
|
134
|
+
export declare function checkDelegationHookRegistered(baseDir: string): CheckResult;
|
|
135
|
+
/**
|
|
136
|
+
* 0.29.0 — synthetic round-trip of the delegation-signal audit path.
|
|
137
|
+
* Drives a synthetic Claude Code PreToolUse hook payload through the
|
|
138
|
+
* REAL `rea hook delegation-signal` CLI by spawning a child process
|
|
139
|
+
* (same path the shell hook hits) and asserts:
|
|
140
|
+
*
|
|
141
|
+
* - The CLI exited 0.
|
|
142
|
+
* - A new `rea.delegation_signal` record landed on disk.
|
|
143
|
+
* - The record's metadata contains the probe tag (so we don't
|
|
144
|
+
* mistakenly attribute an existing record to our run).
|
|
145
|
+
* - Chain integrity holds (recomputed hash == stored hash).
|
|
146
|
+
*
|
|
147
|
+
* Codex round 1 P2 (2026-05-12): the previous implementation called
|
|
148
|
+
* `appendAuditRecord()` directly — short-circuiting stdin parsing,
|
|
149
|
+
* SHA-256 hashing, redact-secrets timing, and the `process.exit`
|
|
150
|
+
* ordering that round 1's P1 exposed. That made the smoke check
|
|
151
|
+
* report success even when the real production path was broken.
|
|
152
|
+
*
|
|
153
|
+
* This rewrite exercises the same surface the `Agent|Skill`
|
|
154
|
+
* PreToolUse hook does in production, so future regressions in
|
|
155
|
+
* stdin parsing, hashing, redaction, or process-lifecycle behavior
|
|
156
|
+
* fail the smoke check loudly.
|
|
157
|
+
*
|
|
158
|
+
* Gated behind `--smoke` so a casual `rea doctor` doesn't write
|
|
159
|
+
* probe records on every invocation. Operators run
|
|
160
|
+
* `rea doctor --smoke` after install / upgrade to confirm the
|
|
161
|
+
* pipeline is wired end-to-end.
|
|
162
|
+
*/
|
|
163
|
+
export declare function checkDelegationRoundTrip(baseDir: string): Promise<CheckResult>;
|
|
68
164
|
/**
|
|
69
165
|
* Assemble the full checklist for a given baseDir. Exported so tests can
|
|
70
166
|
* exercise the conditional branching without capturing stdout from
|
|
@@ -80,7 +176,9 @@ export declare function checksFromProbeState(state: CodexProbeState): CheckResul
|
|
|
80
176
|
*
|
|
81
177
|
* `activeForeign` always yields `fail` — a foreign hook bypassing the gate is a hard governance gap.
|
|
82
178
|
*/
|
|
83
|
-
export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState, prePushState?: PrePushDoctorState
|
|
179
|
+
export declare function collectChecks(baseDir: string, codexProbeState?: CodexProbeState, prePushState?: PrePushDoctorState, options?: {
|
|
180
|
+
strict?: boolean;
|
|
181
|
+
}): CheckResult[];
|
|
84
182
|
export interface RunDoctorOptions {
|
|
85
183
|
/** When true, print a 7-day telemetry summary after the checks (G11.5). */
|
|
86
184
|
metrics?: boolean;
|
|
@@ -91,6 +189,21 @@ export interface RunDoctorOptions {
|
|
|
91
189
|
* `rea upgrade` to reconcile).
|
|
92
190
|
*/
|
|
93
191
|
drift?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* 0.29.0 — when true, run the synthetic delegation-signal round-trip
|
|
194
|
+
* check. Writes a probe `rea.delegation_signal` audit record (with
|
|
195
|
+
* the doctor-smoke session id) and verifies chain integrity. Gated
|
|
196
|
+
* behind a flag so casual `rea doctor` invocations don't pollute the
|
|
197
|
+
* audit log with probe records.
|
|
198
|
+
*/
|
|
199
|
+
smoke?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* 0.30.0 — when true, every advisory check (settings.json schema
|
|
202
|
+
* cross-check, prepare-commit-msg foreign-hook warn, etc.) is
|
|
203
|
+
* promoted to hard fail. Used by CI gates that want a strict floor
|
|
204
|
+
* on consumer installs. Default `false`.
|
|
205
|
+
*/
|
|
206
|
+
strict?: boolean;
|
|
94
207
|
}
|
|
95
208
|
export interface DriftRow {
|
|
96
209
|
path: string;
|