@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
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `rea audit specialists` —
|
|
2
|
+
* `rea audit specialists` — reader CLI for delegation-telemetry
|
|
3
|
+
* (0.29.0; `--since` / `--session` added 0.31.0).
|
|
3
4
|
*
|
|
4
|
-
* Walks `.rea/audit.jsonl
|
|
5
|
-
* `tool_name === 'rea.delegation_signal'`,
|
|
6
|
-
* `metadata.subagent_type`, and prints a table (default) or
|
|
7
|
-
* document (`--json`).
|
|
5
|
+
* Walks `.rea/audit.jsonl` (plus any rotated files when `--since` is
|
|
6
|
+
* given), filters records by `tool_name === 'rea.delegation_signal'`,
|
|
7
|
+
* groups by `metadata.subagent_type`, and prints a table (default) or
|
|
8
|
+
* JSON document (`--json`).
|
|
8
9
|
*
|
|
9
|
-
* #
|
|
10
|
+
* # Session filtering
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* engineer scope-cut deferred both to 0.29.1. The filter is:
|
|
12
|
+
* Three ways to scope, in precedence order:
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 1. `--session <id>` — explicit. Filters to records whose
|
|
15
|
+
* `metadata.session_id_observed` matches `<id>`. The literal
|
|
16
|
+
* `--session all` disables filtering entirely (show every
|
|
17
|
+
* session). Wins over the env var.
|
|
18
|
+
* 2. `$CLAUDE_SESSION_ID` — when set and `--session` is omitted,
|
|
19
|
+
* filters to the current Claude Code session.
|
|
20
|
+
* 3. neither — no filter; every record in the walked files is shown,
|
|
21
|
+
* and a note tells the operator what they're seeing.
|
|
22
|
+
*
|
|
23
|
+
* # `--since <rotated-file>`
|
|
24
|
+
*
|
|
25
|
+
* By default the reader walks only the current `.rea/audit.jsonl`.
|
|
26
|
+
* `--since audit-YYYYMMDD-HHMMSS.jsonl` extends the walk backward: the
|
|
27
|
+
* named rotated file and every rotated file after it (timestamp-
|
|
28
|
+
* ascending) are scanned, then the current `audit.jsonl` as the tail.
|
|
29
|
+
* Mirrors `rea audit verify --since`. The filename must match the
|
|
30
|
+
* canonical rotated-audit shape or the CLI exits 1.
|
|
18
31
|
*
|
|
19
32
|
* # Output shape
|
|
20
33
|
*
|
|
@@ -23,8 +36,8 @@
|
|
|
23
36
|
* code-reviewer 5 2026-05-12T21:28:00Z
|
|
24
37
|
* deep-dive 2 2026-05-12T21:14:00Z
|
|
25
38
|
*
|
|
26
|
-
* JSON mode prints `{ session_filter, records, groups }`
|
|
27
|
-
* `records` is the raw filtered subset (for piping into jq) and
|
|
39
|
+
* JSON mode prints `{ session_filter, records, groups, files_scanned }`
|
|
40
|
+
* where `records` is the raw filtered subset (for piping into jq) and
|
|
28
41
|
* `groups` is the per-subagent rollup.
|
|
29
42
|
*/
|
|
30
43
|
import type { Command } from 'commander';
|
|
@@ -34,17 +47,48 @@ export interface AuditSpecialistsOptions {
|
|
|
34
47
|
json?: boolean;
|
|
35
48
|
/**
|
|
36
49
|
* Override session filtering. Production callers omit (the CLI reads
|
|
37
|
-
* `CLAUDE_SESSION_ID` from the env). Tests set this
|
|
38
|
-
* mutate `process.env`.
|
|
50
|
+
* `--session` then `CLAUDE_SESSION_ID` from the env). Tests set this
|
|
51
|
+
* so they don't mutate `process.env`.
|
|
39
52
|
*
|
|
40
53
|
* - `string` → filter to records whose `session_id_observed` matches.
|
|
41
54
|
* - `null` → no filter (show all records).
|
|
42
|
-
* - `undefined` → derive from env.
|
|
55
|
+
* - `undefined` → derive from `--session` flag then env.
|
|
43
56
|
*/
|
|
44
57
|
sessionFilter?: string | null;
|
|
58
|
+
/**
|
|
59
|
+
* 0.31.0 — explicit `--session <id>` flag value. Resolution rules:
|
|
60
|
+
*
|
|
61
|
+
* - `'all'` (case-insensitive) → no filter (equivalent to
|
|
62
|
+
* `sessionFilter: null`), `session_filter_source: 'option'`.
|
|
63
|
+
* - any other non-empty string → filter to that session,
|
|
64
|
+
* `session_filter_source: 'option'`.
|
|
65
|
+
* - `undefined` → fall through to `sessionFilter`, then env.
|
|
66
|
+
*
|
|
67
|
+
* `sessionFilter` (the test seam) still wins over this when both are
|
|
68
|
+
* set — tests inject `sessionFilter` directly and don't pass
|
|
69
|
+
* `sessionOption`.
|
|
70
|
+
*/
|
|
71
|
+
sessionOption?: string;
|
|
72
|
+
/**
|
|
73
|
+
* 0.31.0 — `--since <rotated-file>` flag value. When set, the named
|
|
74
|
+
* rotated audit file plus every later rotated file (timestamp-
|
|
75
|
+
* ascending) are walked before the current `.rea/audit.jsonl`.
|
|
76
|
+
* Must match `audit-YYYYMMDD-HHMMSS(-N).jsonl`. Validation failure
|
|
77
|
+
* throws `AuditSpecialistsSinceError` so the commander wrapper can
|
|
78
|
+
* exit 1 with a clear message.
|
|
79
|
+
*/
|
|
80
|
+
since?: string;
|
|
45
81
|
/** Override CWD. Tests set this; production uses `process.cwd()`. */
|
|
46
82
|
baseDir?: string;
|
|
47
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Thrown by `computeAuditSpecialists` when `--since` names something
|
|
86
|
+
* that is not a valid rotated-audit basename, or names a rotated file
|
|
87
|
+
* that does not exist. The commander wrapper catches it and exits 1.
|
|
88
|
+
*/
|
|
89
|
+
export declare class AuditSpecialistsSinceError extends Error {
|
|
90
|
+
constructor(message: string);
|
|
91
|
+
}
|
|
48
92
|
interface DelegationGroup {
|
|
49
93
|
subagent_type: string;
|
|
50
94
|
count: number;
|
|
@@ -53,7 +97,7 @@ interface DelegationGroup {
|
|
|
53
97
|
/** Breakdown of which delegation_tool fired (Agent vs. Skill). */
|
|
54
98
|
by_tool: Record<DelegationTool, number>;
|
|
55
99
|
}
|
|
56
|
-
interface DelegationRecord {
|
|
100
|
+
export interface DelegationRecord {
|
|
57
101
|
timestamp: string;
|
|
58
102
|
session_id_observed: string;
|
|
59
103
|
delegation_tool: DelegationTool;
|
|
@@ -78,11 +122,48 @@ interface AuditSpecialistsResult {
|
|
|
78
122
|
files_scanned: string[];
|
|
79
123
|
}
|
|
80
124
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
125
|
+
* List rotated audit files in `.rea/`, timestamp-ascending. Filenames
|
|
126
|
+
* follow `audit-YYYYMMDD-HHMMSS(-N).jsonl`. Sorted via
|
|
127
|
+
* `rotatedAuditSortKey` — the `YYYYMMDD-HHMMSS` block lexically (it is
|
|
128
|
+
* fixed-width zero-padded, so lexical == chronological) then the `-N`
|
|
129
|
+
* intra-second suffix NUMERICALLY (a plain lexical sort of the whole
|
|
130
|
+
* basename misorders two-digit suffixes — see `rotatedAuditSortKey`).
|
|
131
|
+
* Mirrors the private helper in `audit.ts` — kept local so this reader
|
|
132
|
+
* doesn't import the verify command's internals.
|
|
133
|
+
*
|
|
134
|
+
* Exported (0.31.0 round-2 P3) so `delegation-advisory.ts` can resolve
|
|
135
|
+
* the rotated-file set without duplicating the `ROTATED_AUDIT_RE` glob:
|
|
136
|
+
* the advisory's "did this session delegate" predicate must scan rotated
|
|
137
|
+
* segments too, or a delegation recorded before an audit rotation is
|
|
138
|
+
* invisible to the nudge and the session gets a false-positive advisory.
|
|
139
|
+
*/
|
|
140
|
+
export declare function listRotatedAuditFiles(reaDir: string): Promise<string[]>;
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the ordered list of absolute audit-file paths to walk.
|
|
143
|
+
*
|
|
144
|
+
* - `since === undefined` → just the current `.rea/audit.jsonl` (when
|
|
145
|
+
* it exists). Pre-0.31.0 behavior.
|
|
146
|
+
* - `since` set → validate it names a real rotated file, then walk
|
|
147
|
+
* that file + every later rotated file (timestamp-ascending), with
|
|
148
|
+
* the current `audit.jsonl` as the tail.
|
|
149
|
+
*
|
|
150
|
+
* The current `audit.jsonl` is included at the END of the walk
|
|
151
|
+
* whenever it exists — it is always the newest segment of the chain.
|
|
152
|
+
* A `--since` that names a non-rotated string, or a rotated file that
|
|
153
|
+
* isn't present on disk, throws `AuditSpecialistsSinceError`.
|
|
154
|
+
*/
|
|
155
|
+
export declare function resolveAuditFileWalk(baseDir: string, since: string | undefined): Promise<string[]>;
|
|
156
|
+
/**
|
|
157
|
+
* Read the audit file(s) and return delegation records (filtered +
|
|
158
|
+
* parsed into a reader-friendly shape). Malformed lines are skipped
|
|
159
|
+
* silently — `rea audit verify` is the right tool for chain integrity.
|
|
160
|
+
*
|
|
161
|
+
* `since` defaults to `undefined` (current `.rea/audit.jsonl` only) so
|
|
162
|
+
* existing callers — including `computeDelegationAdvisory` in
|
|
163
|
+
* `delegation-advisory.ts` — keep their pre-0.31.0 single-file
|
|
164
|
+
* behavior without passing the argument.
|
|
84
165
|
*/
|
|
85
|
-
export declare function loadDelegationRecords(baseDir: string, sessionFilter: string | null): Promise<{
|
|
166
|
+
export declare function loadDelegationRecords(baseDir: string, sessionFilter: string | null, since?: string): Promise<{
|
|
86
167
|
records: DelegationRecord[];
|
|
87
168
|
filesScanned: string[];
|
|
88
169
|
}>;
|
|
@@ -99,8 +180,9 @@ export declare function groupBySubagent(records: DelegationRecord[]): Delegation
|
|
|
99
180
|
*/
|
|
100
181
|
export declare function computeAuditSpecialists(options?: AuditSpecialistsOptions): Promise<AuditSpecialistsResult>;
|
|
101
182
|
/**
|
|
102
|
-
* Commander entrypoint. Reads, renders, exits 0
|
|
103
|
-
*
|
|
183
|
+
* Commander entrypoint. Reads, renders, exits 0 on success / 1 when
|
|
184
|
+
* `--since` is malformed or names a missing rotated file. The CLI is
|
|
185
|
+
* read-only — no audit-chain writes.
|
|
104
186
|
*/
|
|
105
187
|
export declare function runAuditSpecialists(options: AuditSpecialistsOptions): Promise<void>;
|
|
106
188
|
/**
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `rea audit specialists` —
|
|
2
|
+
* `rea audit specialists` — reader CLI for delegation-telemetry
|
|
3
|
+
* (0.29.0; `--since` / `--session` added 0.31.0).
|
|
3
4
|
*
|
|
4
|
-
* Walks `.rea/audit.jsonl
|
|
5
|
-
* `tool_name === 'rea.delegation_signal'`,
|
|
6
|
-
* `metadata.subagent_type`, and prints a table (default) or
|
|
7
|
-
* document (`--json`).
|
|
5
|
+
* Walks `.rea/audit.jsonl` (plus any rotated files when `--since` is
|
|
6
|
+
* given), filters records by `tool_name === 'rea.delegation_signal'`,
|
|
7
|
+
* groups by `metadata.subagent_type`, and prints a table (default) or
|
|
8
|
+
* JSON document (`--json`).
|
|
8
9
|
*
|
|
9
|
-
* #
|
|
10
|
+
* # Session filtering
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* engineer scope-cut deferred both to 0.29.1. The filter is:
|
|
12
|
+
* Three ways to scope, in precedence order:
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 1. `--session <id>` — explicit. Filters to records whose
|
|
15
|
+
* `metadata.session_id_observed` matches `<id>`. The literal
|
|
16
|
+
* `--session all` disables filtering entirely (show every
|
|
17
|
+
* session). Wins over the env var.
|
|
18
|
+
* 2. `$CLAUDE_SESSION_ID` — when set and `--session` is omitted,
|
|
19
|
+
* filters to the current Claude Code session.
|
|
20
|
+
* 3. neither — no filter; every record in the walked files is shown,
|
|
21
|
+
* and a note tells the operator what they're seeing.
|
|
22
|
+
*
|
|
23
|
+
* # `--since <rotated-file>`
|
|
24
|
+
*
|
|
25
|
+
* By default the reader walks only the current `.rea/audit.jsonl`.
|
|
26
|
+
* `--since audit-YYYYMMDD-HHMMSS.jsonl` extends the walk backward: the
|
|
27
|
+
* named rotated file and every rotated file after it (timestamp-
|
|
28
|
+
* ascending) are scanned, then the current `audit.jsonl` as the tail.
|
|
29
|
+
* Mirrors `rea audit verify --since`. The filename must match the
|
|
30
|
+
* canonical rotated-audit shape or the CLI exits 1.
|
|
18
31
|
*
|
|
19
32
|
* # Output shape
|
|
20
33
|
*
|
|
@@ -23,14 +36,27 @@
|
|
|
23
36
|
* code-reviewer 5 2026-05-12T21:28:00Z
|
|
24
37
|
* deep-dive 2 2026-05-12T21:14:00Z
|
|
25
38
|
*
|
|
26
|
-
* JSON mode prints `{ session_filter, records, groups }`
|
|
27
|
-
* `records` is the raw filtered subset (for piping into jq) and
|
|
39
|
+
* JSON mode prints `{ session_filter, records, groups, files_scanned }`
|
|
40
|
+
* where `records` is the raw filtered subset (for piping into jq) and
|
|
28
41
|
* `groups` is the per-subagent rollup.
|
|
29
42
|
*/
|
|
30
43
|
import fs from 'node:fs/promises';
|
|
31
44
|
import path from 'node:path';
|
|
32
45
|
import { DELEGATION_SIGNAL_TOOL_NAME, DELEGATION_SIGNAL_SCHEMA_VERSION, } from '../audit/delegation-event.js';
|
|
33
46
|
import { AUDIT_FILE, REA_DIR, log } from './utils.js';
|
|
47
|
+
/**
|
|
48
|
+
* Thrown by `computeAuditSpecialists` when `--since` names something
|
|
49
|
+
* that is not a valid rotated-audit basename, or names a rotated file
|
|
50
|
+
* that does not exist. The commander wrapper catches it and exits 1.
|
|
51
|
+
*/
|
|
52
|
+
export class AuditSpecialistsSinceError extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'AuditSpecialistsSinceError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Canonical rotated-audit filename shape — mirrors `audit.ts`. */
|
|
59
|
+
const ROTATED_AUDIT_RE = /^audit-\d{8}-\d{6}(-\d+)?\.jsonl$/;
|
|
34
60
|
/**
|
|
35
61
|
* Type guard for a record that has the delegation-signal shape. Skips
|
|
36
62
|
* envelope shape validation (zod runs at write time); checks only the
|
|
@@ -51,52 +77,167 @@ function isDelegationRecord(r) {
|
|
|
51
77
|
return true;
|
|
52
78
|
}
|
|
53
79
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
80
|
+
* Sort key for a rotated-audit basename. Returns `[stamp, suffix]`:
|
|
81
|
+
*
|
|
82
|
+
* - `stamp` — the `YYYYMMDD-HHMMSS` block. Zero-padded fixed-width, so
|
|
83
|
+
* a plain lexical compare on it IS chronological order.
|
|
84
|
+
* - `suffix` — the intra-second collision counter (`-N`), parsed as an
|
|
85
|
+
* integer. The base file (`audit-...jsonl`, no `-N`) is the FIRST
|
|
86
|
+
* rotation in its second, so its suffix is `0` and it sorts ahead of
|
|
87
|
+
* `audit-...-1.jsonl`.
|
|
88
|
+
*
|
|
89
|
+
* Round-2 P3: the previous implementation sorted the whole basename
|
|
90
|
+
* lexically, which misorders the `-N` suffix once it reaches two digits
|
|
91
|
+
* (`...-10.jsonl` sorts BEFORE `...-2.jsonl`). A repo that rotates more
|
|
92
|
+
* than 9 times in one second would then have `resolveAuditFileWalk`
|
|
93
|
+
* slice from the wrong index and silently drop later segments — and,
|
|
94
|
+
* post-0.31.0, the delegation-advisory predicate (which reuses this
|
|
95
|
+
* resolution) would miss delegations and fire false-positive nudges.
|
|
96
|
+
*/
|
|
97
|
+
function rotatedAuditSortKey(name) {
|
|
98
|
+
// `ROTATED_AUDIT_RE` already guaranteed the shape; capture the parts.
|
|
99
|
+
const m = /^audit-(\d{8}-\d{6})(?:-(\d+))?\.jsonl$/.exec(name);
|
|
100
|
+
if (m === null) {
|
|
101
|
+
// Defensive — callers only pass names that matched ROTATED_AUDIT_RE.
|
|
102
|
+
return [name, 0];
|
|
103
|
+
}
|
|
104
|
+
const stamp = m[1];
|
|
105
|
+
const suffix = m[2] !== undefined ? Number.parseInt(m[2], 10) : 0;
|
|
106
|
+
return [stamp, Number.isInteger(suffix) ? suffix : 0];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* List rotated audit files in `.rea/`, timestamp-ascending. Filenames
|
|
110
|
+
* follow `audit-YYYYMMDD-HHMMSS(-N).jsonl`. Sorted via
|
|
111
|
+
* `rotatedAuditSortKey` — the `YYYYMMDD-HHMMSS` block lexically (it is
|
|
112
|
+
* fixed-width zero-padded, so lexical == chronological) then the `-N`
|
|
113
|
+
* intra-second suffix NUMERICALLY (a plain lexical sort of the whole
|
|
114
|
+
* basename misorders two-digit suffixes — see `rotatedAuditSortKey`).
|
|
115
|
+
* Mirrors the private helper in `audit.ts` — kept local so this reader
|
|
116
|
+
* doesn't import the verify command's internals.
|
|
117
|
+
*
|
|
118
|
+
* Exported (0.31.0 round-2 P3) so `delegation-advisory.ts` can resolve
|
|
119
|
+
* the rotated-file set without duplicating the `ROTATED_AUDIT_RE` glob:
|
|
120
|
+
* the advisory's "did this session delegate" predicate must scan rotated
|
|
121
|
+
* segments too, or a delegation recorded before an audit rotation is
|
|
122
|
+
* invisible to the nudge and the session gets a false-positive advisory.
|
|
57
123
|
*/
|
|
58
|
-
export async function
|
|
59
|
-
|
|
60
|
-
let raw;
|
|
124
|
+
export async function listRotatedAuditFiles(reaDir) {
|
|
125
|
+
let entries;
|
|
61
126
|
try {
|
|
62
|
-
|
|
127
|
+
entries = await fs.readdir(reaDir);
|
|
63
128
|
}
|
|
64
|
-
catch
|
|
65
|
-
|
|
66
|
-
|
|
129
|
+
catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const rotated = entries.filter((n) => ROTATED_AUDIT_RE.test(n));
|
|
133
|
+
rotated.sort((a, b) => {
|
|
134
|
+
const [stampA, suffixA] = rotatedAuditSortKey(a);
|
|
135
|
+
const [stampB, suffixB] = rotatedAuditSortKey(b);
|
|
136
|
+
if (stampA !== stampB)
|
|
137
|
+
return stampA < stampB ? -1 : 1;
|
|
138
|
+
return suffixA - suffixB;
|
|
139
|
+
});
|
|
140
|
+
return rotated;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the ordered list of absolute audit-file paths to walk.
|
|
144
|
+
*
|
|
145
|
+
* - `since === undefined` → just the current `.rea/audit.jsonl` (when
|
|
146
|
+
* it exists). Pre-0.31.0 behavior.
|
|
147
|
+
* - `since` set → validate it names a real rotated file, then walk
|
|
148
|
+
* that file + every later rotated file (timestamp-ascending), with
|
|
149
|
+
* the current `audit.jsonl` as the tail.
|
|
150
|
+
*
|
|
151
|
+
* The current `audit.jsonl` is included at the END of the walk
|
|
152
|
+
* whenever it exists — it is always the newest segment of the chain.
|
|
153
|
+
* A `--since` that names a non-rotated string, or a rotated file that
|
|
154
|
+
* isn't present on disk, throws `AuditSpecialistsSinceError`.
|
|
155
|
+
*/
|
|
156
|
+
export async function resolveAuditFileWalk(baseDir, since) {
|
|
157
|
+
const reaDir = path.join(baseDir, REA_DIR);
|
|
158
|
+
const currentAudit = path.join(reaDir, AUDIT_FILE);
|
|
159
|
+
const files = [];
|
|
160
|
+
if (since !== undefined && since.length > 0) {
|
|
161
|
+
const sinceName = path.basename(since);
|
|
162
|
+
if (!ROTATED_AUDIT_RE.test(sinceName)) {
|
|
163
|
+
throw new AuditSpecialistsSinceError(`--since must name a rotated audit file (audit-YYYYMMDD-HHMMSS.jsonl); got ${JSON.stringify(since)}`);
|
|
67
164
|
}
|
|
68
|
-
|
|
165
|
+
const allRotated = await listRotatedAuditFiles(reaDir);
|
|
166
|
+
const startIdx = allRotated.indexOf(sinceName);
|
|
167
|
+
if (startIdx === -1) {
|
|
168
|
+
throw new AuditSpecialistsSinceError(`Rotated file not found: ${path.join(REA_DIR, sinceName)}`);
|
|
169
|
+
}
|
|
170
|
+
for (const name of allRotated.slice(startIdx)) {
|
|
171
|
+
files.push(path.join(reaDir, name));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// The current audit.jsonl is always the tail of the walk (when present).
|
|
175
|
+
try {
|
|
176
|
+
const stat = await fs.stat(currentAudit);
|
|
177
|
+
if (stat.isFile())
|
|
178
|
+
files.push(currentAudit);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e.code !== 'ENOENT')
|
|
182
|
+
throw e;
|
|
69
183
|
}
|
|
184
|
+
return files;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read the audit file(s) and return delegation records (filtered +
|
|
188
|
+
* parsed into a reader-friendly shape). Malformed lines are skipped
|
|
189
|
+
* silently — `rea audit verify` is the right tool for chain integrity.
|
|
190
|
+
*
|
|
191
|
+
* `since` defaults to `undefined` (current `.rea/audit.jsonl` only) so
|
|
192
|
+
* existing callers — including `computeDelegationAdvisory` in
|
|
193
|
+
* `delegation-advisory.ts` — keep their pre-0.31.0 single-file
|
|
194
|
+
* behavior without passing the argument.
|
|
195
|
+
*/
|
|
196
|
+
export async function loadDelegationRecords(baseDir, sessionFilter, since) {
|
|
197
|
+
const filesToWalk = await resolveAuditFileWalk(baseDir, since);
|
|
70
198
|
const records = [];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let parsed;
|
|
199
|
+
const filesScanned = [];
|
|
200
|
+
for (const auditFile of filesToWalk) {
|
|
201
|
+
let raw;
|
|
75
202
|
try {
|
|
76
|
-
|
|
203
|
+
raw = await fs.readFile(auditFile, 'utf8');
|
|
77
204
|
}
|
|
78
|
-
catch {
|
|
79
|
-
|
|
205
|
+
catch (e) {
|
|
206
|
+
if (e.code === 'ENOENT')
|
|
207
|
+
continue;
|
|
208
|
+
throw e;
|
|
80
209
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
210
|
+
filesScanned.push(auditFile);
|
|
211
|
+
for (const line of raw.split('\n')) {
|
|
212
|
+
if (line.length === 0)
|
|
213
|
+
continue;
|
|
214
|
+
let parsed;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(line);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!isDelegationRecord(parsed))
|
|
222
|
+
continue;
|
|
223
|
+
const m = parsed.metadata;
|
|
224
|
+
if (sessionFilter !== null && m.session_id_observed !== sessionFilter)
|
|
225
|
+
continue;
|
|
226
|
+
const rec = {
|
|
227
|
+
timestamp: parsed.timestamp,
|
|
228
|
+
session_id_observed: m.session_id_observed,
|
|
229
|
+
delegation_tool: m.delegation_tool,
|
|
230
|
+
subagent_type: m.subagent_type,
|
|
231
|
+
parent_subagent_type: m.parent_subagent_type,
|
|
232
|
+
invocation_description_sha256: m.invocation_description_sha256,
|
|
233
|
+
...(m.hook_event_timestamp !== undefined
|
|
234
|
+
? { hook_event_timestamp: m.hook_event_timestamp }
|
|
235
|
+
: {}),
|
|
236
|
+
};
|
|
237
|
+
records.push(rec);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { records, filesScanned };
|
|
100
241
|
}
|
|
101
242
|
/**
|
|
102
243
|
* Group records by `subagent_type`. Sorts by descending count, then
|
|
@@ -136,7 +277,24 @@ export async function computeAuditSpecialists(options = {}) {
|
|
|
136
277
|
const baseDir = options.baseDir ?? process.cwd();
|
|
137
278
|
let sessionFilter;
|
|
138
279
|
let source;
|
|
139
|
-
|
|
280
|
+
// Precedence: explicit `sessionFilter` test seam > `--session` flag >
|
|
281
|
+
// `$CLAUDE_SESSION_ID` env > no filter.
|
|
282
|
+
if (options.sessionFilter !== undefined) {
|
|
283
|
+
sessionFilter = options.sessionFilter;
|
|
284
|
+
source = options.sessionFilter === null ? 'none' : 'option';
|
|
285
|
+
}
|
|
286
|
+
else if (options.sessionOption !== undefined && options.sessionOption.length > 0) {
|
|
287
|
+
// `--session all` (case-insensitive) means "show every session".
|
|
288
|
+
if (options.sessionOption.toLowerCase() === 'all') {
|
|
289
|
+
sessionFilter = null;
|
|
290
|
+
source = 'none';
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
sessionFilter = options.sessionOption;
|
|
294
|
+
source = 'option';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
140
298
|
const envId = process.env['CLAUDE_SESSION_ID'];
|
|
141
299
|
if (typeof envId === 'string' && envId.length > 0) {
|
|
142
300
|
sessionFilter = envId;
|
|
@@ -147,11 +305,7 @@ export async function computeAuditSpecialists(options = {}) {
|
|
|
147
305
|
source = 'none';
|
|
148
306
|
}
|
|
149
307
|
}
|
|
150
|
-
|
|
151
|
-
sessionFilter = options.sessionFilter;
|
|
152
|
-
source = options.sessionFilter === null ? 'none' : 'option';
|
|
153
|
-
}
|
|
154
|
-
const { records, filesScanned } = await loadDelegationRecords(baseDir, sessionFilter);
|
|
308
|
+
const { records, filesScanned } = await loadDelegationRecords(baseDir, sessionFilter, options.since);
|
|
155
309
|
const groups = groupBySubagent(records);
|
|
156
310
|
return {
|
|
157
311
|
session_filter: sessionFilter,
|
|
@@ -186,11 +340,22 @@ function renderTable(result) {
|
|
|
186
340
|
return lines.join('\n') + '\n';
|
|
187
341
|
}
|
|
188
342
|
/**
|
|
189
|
-
* Commander entrypoint. Reads, renders, exits 0
|
|
190
|
-
*
|
|
343
|
+
* Commander entrypoint. Reads, renders, exits 0 on success / 1 when
|
|
344
|
+
* `--since` is malformed or names a missing rotated file. The CLI is
|
|
345
|
+
* read-only — no audit-chain writes.
|
|
191
346
|
*/
|
|
192
347
|
export async function runAuditSpecialists(options) {
|
|
193
|
-
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
result = await computeAuditSpecialists(options);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
if (e instanceof AuditSpecialistsSinceError) {
|
|
354
|
+
process.stderr.write(`rea audit specialists: ${e.message}\n`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
throw e;
|
|
358
|
+
}
|
|
194
359
|
if (options.json === true) {
|
|
195
360
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
196
361
|
return;
|
|
@@ -199,7 +364,11 @@ export async function runAuditSpecialists(options) {
|
|
|
199
364
|
log(`Delegation signals (session=${result.session_filter}, source=${result.session_filter_source}):`);
|
|
200
365
|
}
|
|
201
366
|
else {
|
|
202
|
-
log('Delegation signals (no session filter —
|
|
367
|
+
log('Delegation signals (no session filter — pass --session <id> to scope, ' +
|
|
368
|
+
'--session all to show every session, or set $CLAUDE_SESSION_ID):');
|
|
369
|
+
}
|
|
370
|
+
if (result.files_scanned.length > 1) {
|
|
371
|
+
log(` scanned ${String(result.files_scanned.length)} audit files (--since walk).`);
|
|
203
372
|
}
|
|
204
373
|
process.stdout.write(renderTable(result));
|
|
205
374
|
}
|
|
@@ -212,9 +381,15 @@ export async function runAuditSpecialists(options) {
|
|
|
212
381
|
export function registerAuditSpecialistsSubcommand(auditCommand) {
|
|
213
382
|
auditCommand
|
|
214
383
|
.command('specialists')
|
|
215
|
-
.description('Summarize `rea.delegation_signal` audit records — counts per subagent / skill, last-seen timestamps, agent-vs-skill breakdown.
|
|
216
|
-
.option('--json', 'emit JSON (records + groups) instead of the human-readable table. Composes with jq.')
|
|
384
|
+
.description('Summarize `rea.delegation_signal` audit records — counts per subagent / skill, last-seen timestamps, agent-vs-skill breakdown. Walks `.rea/audit.jsonl` (plus rotated files with --since). Honors --session / $CLAUDE_SESSION_ID for session scoping.')
|
|
385
|
+
.option('--json', 'emit JSON (records + groups + files_scanned) instead of the human-readable table. Composes with jq.')
|
|
386
|
+
.option('--session <id>', 'filter to a specific session_id_observed. Wins over $CLAUDE_SESSION_ID. The literal `all` disables session filtering (shows every session). When omitted, falls back to $CLAUDE_SESSION_ID then no filter.')
|
|
387
|
+
.option('--since <rotated-file>', 'extend the walk backward through rotated audit files. Names a rotated file (audit-YYYYMMDD-HHMMSS.jsonl); that file and every later rotated file are scanned, then the current audit.jsonl. Mirrors `rea audit verify --since`.')
|
|
217
388
|
.action(async (opts) => {
|
|
218
|
-
await runAuditSpecialists({
|
|
389
|
+
await runAuditSpecialists({
|
|
390
|
+
...(opts.json === true ? { json: true } : {}),
|
|
391
|
+
...(opts.session !== undefined ? { sessionOption: opts.session } : {}),
|
|
392
|
+
...(opts.since !== undefined ? { since: opts.since } : {}),
|
|
393
|
+
});
|
|
219
394
|
});
|
|
220
395
|
}
|