@bookedsolid/rea 0.28.2 → 0.29.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/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 +65 -0
- package/dist/cli/doctor.js +258 -0
- package/dist/cli/hook.d.ts +40 -8
- package/dist/cli/hook.js +305 -8
- package/dist/cli/index.js +7 -0
- package/dist/cli/install/manifest-schema.d.ts +6 -6
- package/dist/cli/install/settings-merge.js +20 -0
- package/dist/config/tier-map.js +22 -1
- package/dist/registry/loader.d.ts +6 -6
- package/hooks/delegation-capture.sh +158 -0
- package/package.json +1 -1
package/dist/cli/hook.js
CHANGED
|
@@ -38,6 +38,8 @@ import { runBlockedScan, runProtectedScan } from '../hooks/bash-scanner/index.js
|
|
|
38
38
|
import { loadPolicy } from '../policy/loader.js';
|
|
39
39
|
import { appendAuditRecord, InvocationStatus, Tier } from '../audit/append.js';
|
|
40
40
|
import { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from '../audit/codex-event.js';
|
|
41
|
+
import { DELEGATION_SIGNAL_TOOL_NAME, DELEGATION_SIGNAL_SERVER_NAME, DELEGATION_SIGNAL_SCHEMA_VERSION, DelegationSignalMetadataSchema, } from '../audit/delegation-event.js';
|
|
42
|
+
import { compileDefaultSecretPatterns, redactSecrets, } from '../gateway/middleware/redact.js';
|
|
41
43
|
import { CodexNotInstalledError, CodexProtocolError, CodexSubprocessError, CodexTimeoutError, IRON_GATE_DEFAULT_MODEL, IRON_GATE_DEFAULT_REASONING, createRealGitExecutor, runCodexReview, } from '../hooks/push-gate/codex-runner.js';
|
|
42
44
|
import { resolveBaseRef } from '../hooks/push-gate/base.js';
|
|
43
45
|
import { resolvePushGatePolicy } from '../hooks/push-gate/policy.js';
|
|
@@ -577,18 +579,296 @@ export async function runHookCodexReview(options) {
|
|
|
577
579
|
}
|
|
578
580
|
process.exit(exitCode);
|
|
579
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Cached default secret patterns for the redact path. Compiling the
|
|
584
|
+
* regex set is non-trivial (it spawns a worker per pattern), so cache
|
|
585
|
+
* across invocations. CLI process lifecycle is short enough that this
|
|
586
|
+
* is effectively per-process.
|
|
587
|
+
*/
|
|
588
|
+
let _delegationSecretPatterns = null;
|
|
589
|
+
function getDelegationSecretPatterns() {
|
|
590
|
+
if (_delegationSecretPatterns === null) {
|
|
591
|
+
// Per-pattern timeout budget. The redact-safe wrapper runs each
|
|
592
|
+
// regex in a worker thread, and the worker spawn itself takes
|
|
593
|
+
// ~1ms on hot paths but 50–200ms cold. The spec asked for 50ms
|
|
594
|
+
// but that value caused spurious timeouts in cold-process tests
|
|
595
|
+
// (the very first PreToolUse hook invocation in a fresh shell)
|
|
596
|
+
// and converted every input into the timeout sentinel, leaking
|
|
597
|
+
// false-positive redactions. 250ms is generous enough to absorb
|
|
598
|
+
// a cold worker spawn while still bounded enough that the
|
|
599
|
+
// delegation-capture hook stays well under its 5s settings.json
|
|
600
|
+
// timeout. The hook itself backgrounds the CLI call so this
|
|
601
|
+
// budget never gates Claude Code's tool dispatch.
|
|
602
|
+
_delegationSecretPatterns = compileDefaultSecretPatterns({ timeoutMs: 250 });
|
|
603
|
+
}
|
|
604
|
+
return _delegationSecretPatterns;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Apply `redactSecrets` to a single string field. Returns the
|
|
608
|
+
* (possibly redacted) string plus the list of pattern names that
|
|
609
|
+
* fired. On timeout, returns `'[REDACTED: pattern timeout]'` (the
|
|
610
|
+
* sentinel from redact.ts) and the timeout pattern name so the audit
|
|
611
|
+
* envelope still records the redaction happened.
|
|
612
|
+
*
|
|
613
|
+
* Best-effort: any exception in the redact path returns the input
|
|
614
|
+
* unchanged + an empty pattern list. The audit record is observational
|
|
615
|
+
* — failing the whole signal because the redact timer threw would lose
|
|
616
|
+
* the signal entirely.
|
|
617
|
+
*/
|
|
618
|
+
/**
|
|
619
|
+
* Sentinel emitted when redaction is unable to make a definitive
|
|
620
|
+
* decision (regex timeout, worker error). The redactor's invariant is
|
|
621
|
+
* "never let a potentially secret-bearing string pass through
|
|
622
|
+
* unredacted on failure" — that invariant MUST hold for the
|
|
623
|
+
* delegation-signal path too. Falling back to the raw input on
|
|
624
|
+
* timeout would silently leak a planted credential into
|
|
625
|
+
* .rea/audit.jsonl. Codex round 2 P1 (2026-05-12).
|
|
626
|
+
*/
|
|
627
|
+
const REDACT_INDETERMINATE_SENTINEL = '[REDACTED: indeterminate]';
|
|
628
|
+
function redactField(value) {
|
|
629
|
+
try {
|
|
630
|
+
const { output, redacted, timedOut } = redactSecrets(value, getDelegationSecretPatterns());
|
|
631
|
+
// Timeout: the redactor's `[REDACTED: pattern timeout]` output
|
|
632
|
+
// already says "I couldn't decide". Treat that as a full-field
|
|
633
|
+
// redaction here too — under no circumstance let the raw input
|
|
634
|
+
// through when the scanner failed to complete. This is the
|
|
635
|
+
// fail-closed posture redact.ts itself takes; we mirror it.
|
|
636
|
+
// The redactor's own telemetry separately records the timeout
|
|
637
|
+
// (REDACT_TIMEOUT_METADATA_KEY), so observability isn't lost.
|
|
638
|
+
if (timedOut) {
|
|
639
|
+
return {
|
|
640
|
+
value: REDACT_INDETERMINATE_SENTINEL,
|
|
641
|
+
patterns: ['redact_timeout'],
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (redacted.length === 0)
|
|
645
|
+
return { value, patterns: [] };
|
|
646
|
+
// The redact contract replaces matched substrings with `[REDACTED]`.
|
|
647
|
+
// For a short identifier field like `subagent_type`, treat any hit
|
|
648
|
+
// as a full-field redaction so a partial match doesn't leak the
|
|
649
|
+
// surrounding context.
|
|
650
|
+
return { value: output.includes('[REDACTED') ? '[REDACTED]' : output, patterns: redacted };
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
// Synchronous redactor exception (extremely rare — the wrapper
|
|
654
|
+
// catches its own errors). Fail closed: indeterminate sentinel.
|
|
655
|
+
return {
|
|
656
|
+
value: REDACT_INDETERMINATE_SENTINEL,
|
|
657
|
+
patterns: ['redact_error'],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The actual audit-write — wrapped so it can run inline (default) or
|
|
663
|
+
* as a detached background tail call (`--detach`). Returns the
|
|
664
|
+
* promise; the caller decides whether to await it.
|
|
665
|
+
*/
|
|
666
|
+
async function writeDelegationSignal(baseDir, metadata, redactedFields, sessionId) {
|
|
667
|
+
// Defense-in-depth: validate the metadata shape against the strict
|
|
668
|
+
// zod schema before handing it off to `appendAuditRecord`. A future
|
|
669
|
+
// refactor that introduces a field-name typo here would otherwise
|
|
670
|
+
// silently land a malformed line in the chain.
|
|
671
|
+
const parsed = DelegationSignalMetadataSchema.safeParse(metadata);
|
|
672
|
+
if (!parsed.success) {
|
|
673
|
+
process.stderr.write(`[rea] delegation-signal: metadata failed strict-mode validation: ${parsed.error.message}\n`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
await appendAuditRecord(baseDir, {
|
|
677
|
+
tool_name: DELEGATION_SIGNAL_TOOL_NAME,
|
|
678
|
+
server_name: DELEGATION_SIGNAL_SERVER_NAME,
|
|
679
|
+
tier: Tier.Read,
|
|
680
|
+
status: InvocationStatus.Allowed,
|
|
681
|
+
session_id: sessionId,
|
|
682
|
+
...(redactedFields.length > 0 ? { redacted_fields: redactedFields } : {}),
|
|
683
|
+
metadata: parsed.data,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Read the hook stdin payload, redact + hash, and either await the
|
|
688
|
+
* audit append OR fire-and-forget it (when `--detach` is set).
|
|
689
|
+
*
|
|
690
|
+
* Exit-code contract: ALWAYS exit 0. The delegation signal is
|
|
691
|
+
* observational, not gating — failure to write the record must NOT
|
|
692
|
+
* block Claude Code's tool dispatch. Errors are surfaced on stderr.
|
|
693
|
+
*/
|
|
694
|
+
export async function runHookDelegationSignal(options) {
|
|
695
|
+
const baseDir = options.reaRoot ?? process.env['CLAUDE_PROJECT_DIR'] ?? process.cwd();
|
|
696
|
+
const lockTimeoutMs = options.lockTimeoutMs ?? 2000;
|
|
697
|
+
// Read stdin. TTY → empty (no harness payload available, nothing to
|
|
698
|
+
// emit — exit 0 silently). Timeout 1s; the hook shim feeds us a
|
|
699
|
+
// small JSON blob that fully prints in milliseconds.
|
|
700
|
+
const stdinRaw = process.stdin.isTTY ? '' : await readStdinWithTimeout(1_000);
|
|
701
|
+
if (stdinRaw.length === 0) {
|
|
702
|
+
process.exit(0);
|
|
703
|
+
}
|
|
704
|
+
let payload;
|
|
705
|
+
try {
|
|
706
|
+
payload = JSON.parse(stdinRaw);
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
// Malformed payload — observational signal only, exit 0 silently
|
|
710
|
+
// with stderr breadcrumb. Failing here would propagate to the hook
|
|
711
|
+
// shim and risk blocking the underlying Agent/Skill dispatch.
|
|
712
|
+
process.stderr.write(`[rea] delegation-signal: malformed stdin JSON (${e instanceof Error ? e.message : String(e)}), signal dropped\n`);
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
// Resolve which delegation tool fired. Anything else is a misfire at
|
|
716
|
+
// the matcher layer (Claude Code routed a non-delegation tool to us)
|
|
717
|
+
// — exit 0 silently.
|
|
718
|
+
const rawToolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
|
|
719
|
+
const delegationTool = rawToolName === 'Agent' ? 'Agent' : rawToolName === 'Skill' ? 'Skill' : null;
|
|
720
|
+
if (delegationTool === null) {
|
|
721
|
+
process.exit(0);
|
|
722
|
+
}
|
|
723
|
+
// Extract the agent / skill name. Agent → tool_input.subagent_type;
|
|
724
|
+
// Skill → tool_input.skill. Missing → emit a placeholder ('unknown')
|
|
725
|
+
// so the chain still records the delegation event.
|
|
726
|
+
const ti = payload.tool_input ?? {};
|
|
727
|
+
let rawSubagentType = '';
|
|
728
|
+
if (delegationTool === 'Agent' && typeof ti.subagent_type === 'string') {
|
|
729
|
+
rawSubagentType = ti.subagent_type;
|
|
730
|
+
}
|
|
731
|
+
else if (delegationTool === 'Skill' && typeof ti.skill === 'string') {
|
|
732
|
+
rawSubagentType = ti.skill;
|
|
733
|
+
}
|
|
734
|
+
if (rawSubagentType.length === 0)
|
|
735
|
+
rawSubagentType = 'unknown';
|
|
736
|
+
// Parent subagent — Claude Code surfaces this either as
|
|
737
|
+
// `tool_input.parent_subagent_type` (when the dispatcher attaches
|
|
738
|
+
// it to the payload) or as the `CLAUDE_PARENT_SUBAGENT` env var
|
|
739
|
+
// (the alternate source the integrated spec calls out). Payload
|
|
740
|
+
// wins when both are present — payload is closer to the originating
|
|
741
|
+
// event and the env var can be stale across subprocess fan-out.
|
|
742
|
+
// Codex round 4 P2 (2026-05-12): pre-fix the env-var source was
|
|
743
|
+
// ignored and every nested delegation was recorded as null,
|
|
744
|
+
// defeating the parent/child telemetry the field was added for.
|
|
745
|
+
let rawParent = null;
|
|
746
|
+
if (typeof ti.parent_subagent_type === 'string' && ti.parent_subagent_type.length > 0) {
|
|
747
|
+
rawParent = ti.parent_subagent_type;
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const envParent = process.env['CLAUDE_PARENT_SUBAGENT'];
|
|
751
|
+
if (typeof envParent === 'string' && envParent.length > 0) {
|
|
752
|
+
rawParent = envParent;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Description / prompt → SHA-256, never persisted in clear.
|
|
756
|
+
// Agent dispatches the prompt under `description`; Skill under
|
|
757
|
+
// `prompt`. When neither is present we hash the empty string so the
|
|
758
|
+
// field is always present.
|
|
759
|
+
const rawDescription = delegationTool === 'Agent' && typeof ti.description === 'string'
|
|
760
|
+
? ti.description
|
|
761
|
+
: delegationTool === 'Skill' && typeof ti.prompt === 'string'
|
|
762
|
+
? ti.prompt
|
|
763
|
+
: '';
|
|
764
|
+
const descriptionHash = crypto.createHash('sha256').update(rawDescription).digest('hex');
|
|
765
|
+
// Run subagent_type + parent_subagent_type through the redact path.
|
|
766
|
+
// A planted credential string in either field is replaced with
|
|
767
|
+
// [REDACTED] before landing in the audit log.
|
|
768
|
+
const redactedFields = [];
|
|
769
|
+
const sub = redactField(rawSubagentType);
|
|
770
|
+
if (sub.patterns.length > 0) {
|
|
771
|
+
redactedFields.push('metadata.subagent_type');
|
|
772
|
+
}
|
|
773
|
+
let parentValue = rawParent;
|
|
774
|
+
if (rawParent !== null) {
|
|
775
|
+
const parentRed = redactField(rawParent);
|
|
776
|
+
parentValue = parentRed.value;
|
|
777
|
+
if (parentRed.patterns.length > 0) {
|
|
778
|
+
redactedFields.push('metadata.parent_subagent_type');
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const sessionIdObserved = typeof payload.session_id === 'string' && payload.session_id.length > 0
|
|
782
|
+
? payload.session_id
|
|
783
|
+
: 'unknown';
|
|
784
|
+
const hookEventTimestamp = typeof payload.hook_event_timestamp === 'string' && payload.hook_event_timestamp.length > 0
|
|
785
|
+
? payload.hook_event_timestamp
|
|
786
|
+
: undefined;
|
|
787
|
+
const metadata = {
|
|
788
|
+
schema_version: DELEGATION_SIGNAL_SCHEMA_VERSION,
|
|
789
|
+
delegation_tool: delegationTool,
|
|
790
|
+
subagent_type: sub.value,
|
|
791
|
+
session_id_observed: sessionIdObserved,
|
|
792
|
+
parent_subagent_type: parentValue,
|
|
793
|
+
invocation_description_sha256: descriptionHash,
|
|
794
|
+
...(hookEventTimestamp !== undefined ? { hook_event_timestamp: hookEventTimestamp } : {}),
|
|
795
|
+
};
|
|
796
|
+
// Audit envelope `session_id` carries the observed session so a
|
|
797
|
+
// future reader can correlate without traversing metadata. (The
|
|
798
|
+
// envelope value is duplicated in metadata.session_id_observed for
|
|
799
|
+
// record-self-containment.)
|
|
800
|
+
const writePromise = writeDelegationSignal(baseDir, metadata, redactedFields, sessionIdObserved);
|
|
801
|
+
// The audit append must complete BEFORE this CLI process exits.
|
|
802
|
+
// Earlier iterations treated `--detach` as "fire-and-forget at the
|
|
803
|
+
// CLI level", but Node terminates a process when the event loop has
|
|
804
|
+
// no more sync work — and `appendAuditRecord()` is async filesystem
|
|
805
|
+
// work that does NOT keep the process alive across `process.exit`.
|
|
806
|
+
// The fire-and-forget concept lives one level UP, in the SHELL hook:
|
|
807
|
+
// the .sh stub backgrounds this entire CLI invocation with `&` +
|
|
808
|
+
// `disown` so Claude Code's tool dispatch is not blocked. From inside
|
|
809
|
+
// the CLI we always wait for the append.
|
|
810
|
+
//
|
|
811
|
+
// Codex round 1 P1 (2026-05-12): the previous implementation called
|
|
812
|
+
// `process.exit(0)` immediately after kicking off the promise under
|
|
813
|
+
// `--detach`. Tests stubbed `process.exit` so the promise still ran
|
|
814
|
+
// to completion in-test, masking the bug. In production every
|
|
815
|
+
// Agent/Skill dispatch silently dropped its delegation record.
|
|
816
|
+
//
|
|
817
|
+
// `--detach` is RETAINED as a flag for backwards compat with the
|
|
818
|
+
// shell hook stub's `--detach &` argv (and to document that the
|
|
819
|
+
// shell hook is the backgrounding layer, not the CLI). Its only
|
|
820
|
+
// remaining effect is the doc comment and an audit-append-failure
|
|
821
|
+
// mode that NEVER emits to stderr (no parent shell is listening
|
|
822
|
+
// when the CLI ran detached).
|
|
823
|
+
const detached = options.detach === true;
|
|
824
|
+
let timer = null;
|
|
825
|
+
try {
|
|
826
|
+
await Promise.race([
|
|
827
|
+
writePromise,
|
|
828
|
+
new Promise((resolve) => {
|
|
829
|
+
timer = setTimeout(() => resolve('timeout'), lockTimeoutMs);
|
|
830
|
+
timer.unref?.();
|
|
831
|
+
}).then((tag) => {
|
|
832
|
+
if (tag === 'timeout') {
|
|
833
|
+
if (!detached) {
|
|
834
|
+
process.stderr.write(`[rea] delegation-signal: lock timeout, signal dropped\n`);
|
|
835
|
+
}
|
|
836
|
+
// Surface the eventual error so it doesn't escape as an
|
|
837
|
+
// unhandled rejection after we've exited the await.
|
|
838
|
+
writePromise.catch(() => {
|
|
839
|
+
/* already reported via stderr */
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
}),
|
|
844
|
+
]);
|
|
845
|
+
}
|
|
846
|
+
catch (e) {
|
|
847
|
+
// Append failure (e.g. ENOSPC) — surface to stderr unless we ran
|
|
848
|
+
// detached (no parent shell is listening).
|
|
849
|
+
if (!detached) {
|
|
850
|
+
process.stderr.write(`[rea] delegation-signal: audit append failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
if (timer !== null)
|
|
855
|
+
clearTimeout(timer);
|
|
856
|
+
}
|
|
857
|
+
process.exit(0);
|
|
858
|
+
}
|
|
580
859
|
/**
|
|
581
860
|
* Attach the `rea hook` subcommand tree to a commander Program.
|
|
582
861
|
*
|
|
583
862
|
* Subcommands:
|
|
584
|
-
* - `push-gate`
|
|
585
|
-
* - `scan-bash`
|
|
586
|
-
*
|
|
587
|
-
* - `policy-get`
|
|
588
|
-
* - `codex-review`
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
863
|
+
* - `push-gate` — stateless pre-push Codex review (called by husky).
|
|
864
|
+
* - `scan-bash` — parser-backed bash-tier scanner (called by Claude
|
|
865
|
+
* Code shim hooks).
|
|
866
|
+
* - `policy-get` — single-source-of-truth policy reader for bash hooks.
|
|
867
|
+
* - `codex-review` — thin Bash-direct codex invocation (0.27.0+) for
|
|
868
|
+
* marathon-mode review cycles.
|
|
869
|
+
* - `delegation-signal` — 0.29.0 delegation-telemetry MVP. Reads a Claude
|
|
870
|
+
* Code PreToolUse hook payload for `Agent` / `Skill`
|
|
871
|
+
* and emits a `rea.delegation_signal` audit record.
|
|
592
872
|
*
|
|
593
873
|
* New hooks should land here rather than as top-level commands so the
|
|
594
874
|
* CLI surface stays navigable.
|
|
@@ -653,6 +933,23 @@ export function registerHookCommand(program) {
|
|
|
653
933
|
...(opts.json === true ? { json: true } : {}),
|
|
654
934
|
});
|
|
655
935
|
});
|
|
936
|
+
hook
|
|
937
|
+
.command('delegation-signal')
|
|
938
|
+
.description('Read a Claude Code PreToolUse hook payload for `Agent` or `Skill` from stdin and emit a `rea.delegation_signal` audit record (0.29.0+). Observational telemetry only — exit ALWAYS 0; failure to write the record never blocks tool dispatch. The hook shim at `.claude/hooks/delegation-capture.sh` invokes this with `--detach` so the Agent/Skill call proceeds without waiting on the audit lock.')
|
|
939
|
+
.option('--detach', 'fire the audit append in the background and return immediately. Set by the shell hook stub so worst-case latency stays low under lock contention.')
|
|
940
|
+
.option('--lock-timeout-ms <n>', 'milliseconds to wait for the audit-chain lock before dropping the signal. Default 2000.', (raw) => {
|
|
941
|
+
const n = Number(raw);
|
|
942
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
943
|
+
throw new Error(`--lock-timeout-ms must be a positive integer, got ${JSON.stringify(raw)}`);
|
|
944
|
+
}
|
|
945
|
+
return n;
|
|
946
|
+
})
|
|
947
|
+
.action(async (opts) => {
|
|
948
|
+
await runHookDelegationSignal({
|
|
949
|
+
...(opts.detach === true ? { detach: true } : {}),
|
|
950
|
+
...(opts.lockTimeoutMs !== undefined ? { lockTimeoutMs: opts.lockTimeoutMs } : {}),
|
|
951
|
+
});
|
|
952
|
+
});
|
|
656
953
|
hook
|
|
657
954
|
.command('policy-get')
|
|
658
955
|
.description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { runAuditRotate, runAuditVerify } from './audit.js';
|
|
4
|
+
import { registerAuditSpecialistsSubcommand } from './audit-specialists.js';
|
|
4
5
|
import { runCheck } from './check.js';
|
|
5
6
|
import { registerHookCommand } from './hook.js';
|
|
6
7
|
import { runDoctor } from './doctor.js';
|
|
@@ -106,6 +107,10 @@ async function main() {
|
|
|
106
107
|
.action(async (opts) => {
|
|
107
108
|
await runAuditVerify({ ...(opts.since !== undefined ? { since: opts.since } : {}) });
|
|
108
109
|
});
|
|
110
|
+
// 0.29.0 — `rea audit specialists` reader for delegation-telemetry
|
|
111
|
+
// records. Read-only; honors $CLAUDE_SESSION_ID for current-session
|
|
112
|
+
// filtering. v1 omits --since / --session (deferred to 0.29.1).
|
|
113
|
+
registerAuditSpecialistsSubcommand(audit);
|
|
109
114
|
// Register `rea hook push-gate` — the stateless pre-push Codex gate
|
|
110
115
|
// called by `.husky/pre-push` and `.git/hooks/pre-push`.
|
|
111
116
|
registerHookCommand(program);
|
|
@@ -143,10 +148,12 @@ async function main() {
|
|
|
143
148
|
.description('Validate the install: policy parses, .rea/ layout, hooks, Codex plugin.')
|
|
144
149
|
.option('--metrics', 'also print a 7-day summary of Codex telemetry (G11.5)')
|
|
145
150
|
.option('--drift', 'report drift vs. the install manifest (read-only; does not mutate)')
|
|
151
|
+
.option('--smoke', 'also run the 0.29.0 delegation-signal round-trip (writes a probe `rea.delegation_signal` audit record and verifies chain integrity)')
|
|
146
152
|
.action(async (opts) => {
|
|
147
153
|
await runDoctor({
|
|
148
154
|
...(opts.metrics === true ? { metrics: true } : {}),
|
|
149
155
|
...(opts.drift === true ? { drift: true } : {}),
|
|
156
|
+
...(opts.smoke === true ? { smoke: true } : {}),
|
|
150
157
|
});
|
|
151
158
|
});
|
|
152
159
|
await program.parseAsync(process.argv);
|
|
@@ -22,12 +22,12 @@ export declare const ManifestEntrySchema: z.ZodObject<{
|
|
|
22
22
|
}, "strict", z.ZodTypeAny, {
|
|
23
23
|
path: string;
|
|
24
24
|
sha256: string;
|
|
25
|
-
source: "
|
|
25
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
26
26
|
mode?: number | undefined;
|
|
27
27
|
}, {
|
|
28
28
|
path: string;
|
|
29
29
|
sha256: string;
|
|
30
|
-
source: "
|
|
30
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
31
31
|
mode?: number | undefined;
|
|
32
32
|
}>;
|
|
33
33
|
export type ManifestEntry = z.infer<typeof ManifestEntrySchema>;
|
|
@@ -45,12 +45,12 @@ export declare const InstallManifestSchema: z.ZodObject<{
|
|
|
45
45
|
}, "strict", z.ZodTypeAny, {
|
|
46
46
|
path: string;
|
|
47
47
|
sha256: string;
|
|
48
|
-
source: "
|
|
48
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
49
49
|
mode?: number | undefined;
|
|
50
50
|
}, {
|
|
51
51
|
path: string;
|
|
52
52
|
sha256: string;
|
|
53
|
-
source: "
|
|
53
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
54
54
|
mode?: number | undefined;
|
|
55
55
|
}>, "many">;
|
|
56
56
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -60,7 +60,7 @@ export declare const InstallManifestSchema: z.ZodObject<{
|
|
|
60
60
|
files: {
|
|
61
61
|
path: string;
|
|
62
62
|
sha256: string;
|
|
63
|
-
source: "
|
|
63
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
64
64
|
mode?: number | undefined;
|
|
65
65
|
}[];
|
|
66
66
|
upgraded_at?: string | undefined;
|
|
@@ -72,7 +72,7 @@ export declare const InstallManifestSchema: z.ZodObject<{
|
|
|
72
72
|
files: {
|
|
73
73
|
path: string;
|
|
74
74
|
sha256: string;
|
|
75
|
-
source: "
|
|
75
|
+
source: "agent" | "command" | "hook" | "husky" | "claude-md" | "settings";
|
|
76
76
|
mode?: number | undefined;
|
|
77
77
|
}[];
|
|
78
78
|
upgraded_at?: string | undefined;
|
|
@@ -318,6 +318,26 @@ export function defaultDesiredHooks() {
|
|
|
318
318
|
},
|
|
319
319
|
],
|
|
320
320
|
},
|
|
321
|
+
{
|
|
322
|
+
// 0.29.0 delegation-telemetry MVP. The matcher is `Agent|Skill` —
|
|
323
|
+
// the two delegation tools in current Claude Code. NOT `Task|Skill`:
|
|
324
|
+
// `TaskCreate`/`TaskList`/`TaskUpdate` are unrelated todo-list tools
|
|
325
|
+
// and MUST NOT match. The hook is observational; its only effect is
|
|
326
|
+
// to append a `rea.delegation_signal` audit record. Worst-case
|
|
327
|
+
// latency budget is 50ms even under audit-chain contention (the
|
|
328
|
+
// signal is backgrounded and the CLI uses a 2s lock-acquisition
|
|
329
|
+
// fallback that exits 0 on timeout).
|
|
330
|
+
event: 'PreToolUse',
|
|
331
|
+
matcher: 'Agent|Skill',
|
|
332
|
+
hooks: [
|
|
333
|
+
{
|
|
334
|
+
type: 'command',
|
|
335
|
+
command: `${base}/delegation-capture.sh`,
|
|
336
|
+
timeout: 5000,
|
|
337
|
+
statusMessage: 'Recording delegation signal...',
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
321
341
|
{
|
|
322
342
|
event: 'PreToolUse',
|
|
323
343
|
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
package/dist/config/tier-map.js
CHANGED
|
@@ -264,9 +264,18 @@ export function reaCommandTier(command) {
|
|
|
264
264
|
const subcommandTier = (() => {
|
|
265
265
|
switch (sub) {
|
|
266
266
|
case 'check':
|
|
267
|
-
case 'doctor':
|
|
268
267
|
case 'status':
|
|
269
268
|
return Tier.Read;
|
|
269
|
+
case 'doctor': {
|
|
270
|
+
// 0.29.0 — `rea doctor --smoke` appends a probe
|
|
271
|
+
// `rea.delegation_signal` record to `.rea/audit.jsonl` to
|
|
272
|
+
// verify the end-to-end pipeline. That makes it a writer
|
|
273
|
+
// (hash-chain mutation) and therefore Write-tier — bare
|
|
274
|
+
// `rea doctor` stays Read. Codex round 3 P2 (2026-05-12).
|
|
275
|
+
if (tokens.slice(idx + 1).includes('--smoke'))
|
|
276
|
+
return Tier.Write;
|
|
277
|
+
return Tier.Read;
|
|
278
|
+
}
|
|
270
279
|
case 'hook': {
|
|
271
280
|
// `rea hook push-gate` is execution-only — it runs codex exec review
|
|
272
281
|
// and writes `.rea/last-review.json` + an audit record, but the
|
|
@@ -275,11 +284,23 @@ export function reaCommandTier(command) {
|
|
|
275
284
|
// `audit record codex-review`).
|
|
276
285
|
if (sub2 === 'push-gate')
|
|
277
286
|
return Tier.Read;
|
|
287
|
+
// 0.29.0 — `rea hook delegation-signal` reads stdin, appends a
|
|
288
|
+
// single audit record, and exits 0. Read-tier: observational
|
|
289
|
+
// telemetry with no policy effect and no behavior change to the
|
|
290
|
+
// underlying Agent/Skill dispatch.
|
|
291
|
+
if (sub2 === 'delegation-signal')
|
|
292
|
+
return Tier.Read;
|
|
278
293
|
return Tier.Write;
|
|
279
294
|
}
|
|
280
295
|
case 'audit': {
|
|
281
296
|
if (sub2 === 'verify')
|
|
282
297
|
return Tier.Read;
|
|
298
|
+
// 0.29.0 — `rea audit specialists` is a read-only reader for
|
|
299
|
+
// the delegation-telemetry MVP. Classify Read so L0 sessions
|
|
300
|
+
// can summarize delegation patterns without tripping the
|
|
301
|
+
// Write-tier default. Codex round 3 P2 (2026-05-12).
|
|
302
|
+
if (sub2 === 'specialists')
|
|
303
|
+
return Tier.Read;
|
|
283
304
|
if (sub2 === 'rotate')
|
|
284
305
|
return Tier.Write;
|
|
285
306
|
return Tier.Write;
|
|
@@ -18,17 +18,17 @@ declare const RegistryServerSchema: z.ZodObject<{
|
|
|
18
18
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
19
19
|
}, "strict", z.ZodTypeAny, {
|
|
20
20
|
name: string;
|
|
21
|
+
env: Record<string, string>;
|
|
21
22
|
command: string;
|
|
22
23
|
args: string[];
|
|
23
|
-
env: Record<string, string>;
|
|
24
24
|
enabled: boolean;
|
|
25
25
|
env_passthrough?: string[] | undefined;
|
|
26
26
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
27
27
|
}, {
|
|
28
28
|
name: string;
|
|
29
29
|
command: string;
|
|
30
|
-
args?: string[] | undefined;
|
|
31
30
|
env?: Record<string, string> | undefined;
|
|
31
|
+
args?: string[] | undefined;
|
|
32
32
|
env_passthrough?: string[] | undefined;
|
|
33
33
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
34
34
|
enabled?: boolean | undefined;
|
|
@@ -45,17 +45,17 @@ declare const RegistrySchema: z.ZodObject<{
|
|
|
45
45
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
46
46
|
}, "strict", z.ZodTypeAny, {
|
|
47
47
|
name: string;
|
|
48
|
+
env: Record<string, string>;
|
|
48
49
|
command: string;
|
|
49
50
|
args: string[];
|
|
50
|
-
env: Record<string, string>;
|
|
51
51
|
enabled: boolean;
|
|
52
52
|
env_passthrough?: string[] | undefined;
|
|
53
53
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
54
54
|
}, {
|
|
55
55
|
name: string;
|
|
56
56
|
command: string;
|
|
57
|
-
args?: string[] | undefined;
|
|
58
57
|
env?: Record<string, string> | undefined;
|
|
58
|
+
args?: string[] | undefined;
|
|
59
59
|
env_passthrough?: string[] | undefined;
|
|
60
60
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
61
61
|
enabled?: boolean | undefined;
|
|
@@ -65,9 +65,9 @@ declare const RegistrySchema: z.ZodObject<{
|
|
|
65
65
|
version: "1";
|
|
66
66
|
servers: {
|
|
67
67
|
name: string;
|
|
68
|
+
env: Record<string, string>;
|
|
68
69
|
command: string;
|
|
69
70
|
args: string[];
|
|
70
|
-
env: Record<string, string>;
|
|
71
71
|
enabled: boolean;
|
|
72
72
|
env_passthrough?: string[] | undefined;
|
|
73
73
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
@@ -78,8 +78,8 @@ declare const RegistrySchema: z.ZodObject<{
|
|
|
78
78
|
servers?: {
|
|
79
79
|
name: string;
|
|
80
80
|
command: string;
|
|
81
|
-
args?: string[] | undefined;
|
|
82
81
|
env?: Record<string, string> | undefined;
|
|
82
|
+
args?: string[] | undefined;
|
|
83
83
|
env_passthrough?: string[] | undefined;
|
|
84
84
|
tier_overrides?: Record<string, Tier> | undefined;
|
|
85
85
|
enabled?: boolean | undefined;
|