@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.
@@ -1,20 +1,33 @@
1
1
  /**
2
- * `rea audit specialists` — 0.29.0 reader CLI for delegation-telemetry.
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`, 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`).
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
- * # Current-session-only in v1
10
+ * # Session filtering
10
11
  *
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:
12
+ * Three ways to scope, in precedence order:
13
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.
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 }` where
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 so they don't
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
- * 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.
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. The CLI is read-only
103
- * no audit-chain writes, no exit-code-as-verdict semantics.
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` — 0.29.0 reader CLI for delegation-telemetry.
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`, 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`).
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
- * # Current-session-only in v1
10
+ * # Session filtering
10
11
  *
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:
12
+ * Three ways to scope, in precedence order:
13
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.
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 }` where
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
- * 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.
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 loadDelegationRecords(baseDir, sessionFilter) {
59
- const auditFile = path.join(baseDir, REA_DIR, AUDIT_FILE);
60
- let raw;
124
+ export async function listRotatedAuditFiles(reaDir) {
125
+ let entries;
61
126
  try {
62
- raw = await fs.readFile(auditFile, 'utf8');
127
+ entries = await fs.readdir(reaDir);
63
128
  }
64
- catch (e) {
65
- if (e.code === 'ENOENT') {
66
- return { records: [], filesScanned: [] };
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
- throw e;
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
- for (const line of raw.split('\n')) {
72
- if (line.length === 0)
73
- continue;
74
- let parsed;
199
+ const filesScanned = [];
200
+ for (const auditFile of filesToWalk) {
201
+ let raw;
75
202
  try {
76
- parsed = JSON.parse(line);
203
+ raw = await fs.readFile(auditFile, 'utf8');
77
204
  }
78
- catch {
79
- continue;
205
+ catch (e) {
206
+ if (e.code === 'ENOENT')
207
+ continue;
208
+ throw e;
80
209
  }
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] };
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
- if (options.sessionFilter === undefined) {
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
- else {
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. The CLI is read-only
190
- * no audit-chain writes, no exit-code-as-verdict semantics.
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
- const result = await computeAuditSpecialists(options);
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 — set $CLAUDE_SESSION_ID to scope; v1 omits --since / --session by design):');
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. 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.')
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({ ...(opts.json === true ? { json: true } : {}) });
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
  }