@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
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,14 @@ 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)')
|
|
152
|
+
.option('--strict', '0.30.0 Class M — promote settings.json schema warnings (zod parse failures, path traversal, missing rea hooks) to hard fail. Use in CI gates.')
|
|
146
153
|
.action(async (opts) => {
|
|
147
154
|
await runDoctor({
|
|
148
155
|
...(opts.metrics === true ? { metrics: true } : {}),
|
|
149
156
|
...(opts.drift === true ? { drift: true } : {}),
|
|
157
|
+
...(opts.smoke === true ? { smoke: true } : {}),
|
|
158
|
+
...(opts.strict === true ? { strict: true } : {}),
|
|
150
159
|
});
|
|
151
160
|
});
|
|
152
161
|
await program.parseAsync(process.argv);
|
package/dist/cli/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import { copyArtifacts } from './install/copy.js';
|
|
|
9
9
|
import { ensureReaGitignore } from './install/gitignore.js';
|
|
10
10
|
import { canonicalSettingsSubsetHash, defaultDesiredHooks, mergeSettings, readSettings, writeSettingsAtomic, } from './install/settings-merge.js';
|
|
11
11
|
import { installCommitMsgHook } from './install/commit-msg.js';
|
|
12
|
+
import { installPrepareCommitMsgHook } from './install/prepare-commit-msg.js';
|
|
12
13
|
import { installPrePushFallback } from './install/pre-push.js';
|
|
13
14
|
import { CodexProbe } from '../gateway/observability/codex-probe.js';
|
|
14
15
|
import { buildFragment, writeClaudeMdFragment } from './install/claude-md.js';
|
|
@@ -220,11 +221,56 @@ async function runWizard(options, targetDir, reagentPolicyPath, layeredBase, exi
|
|
|
220
221
|
...(existingPolicy?.commitHygieneRefuseAtCommits !== undefined
|
|
221
222
|
? { commitHygieneRefuseAtCommits: existingPolicy.commitHygieneRefuseAtCommits }
|
|
222
223
|
: {}),
|
|
224
|
+
// 0.30.0 attribution augmenter — preserved across re-init OR
|
|
225
|
+
// seeded from the layered profile (every shipped profile pins
|
|
226
|
+
// `enabled: false`). Conditional spread so undefined → key omitted
|
|
227
|
+
// (the field is exact-optional).
|
|
228
|
+
...attributionConfigSpread(layeredBase, existingPolicy),
|
|
223
229
|
fromReagent,
|
|
224
230
|
reagentPolicyPath,
|
|
225
231
|
reagentNotices: [],
|
|
226
232
|
};
|
|
227
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Compute the attribution-augmenter config spread to inject into a
|
|
236
|
+
* partial `ResolvedConfig` literal. Returns `{}` when neither the
|
|
237
|
+
* existing on-disk policy nor the layered profile declared the
|
|
238
|
+
* augmenter — the policy writer then omits the block entirely so
|
|
239
|
+
* consumers who haven't seen 0.30.0 don't get a mystery YAML block.
|
|
240
|
+
*
|
|
241
|
+
* Returns `{ attributionCoAuthor: ... }` otherwise. Using a spread
|
|
242
|
+
* helper instead of a value-returning function lets `exactOptionalProperty
|
|
243
|
+
* Types` distinguish "omitted" from "explicitly undefined" — required
|
|
244
|
+
* by the strict tsconfig.
|
|
245
|
+
*/
|
|
246
|
+
function attributionConfigSpread(layered, existing) {
|
|
247
|
+
const preserved = existing?.attributionCoAuthor;
|
|
248
|
+
if (preserved !== undefined) {
|
|
249
|
+
return {
|
|
250
|
+
attributionCoAuthor: {
|
|
251
|
+
...(preserved.enabled !== undefined ? { enabled: preserved.enabled } : {}),
|
|
252
|
+
...(preserved.name !== undefined ? { name: preserved.name } : {}),
|
|
253
|
+
...(preserved.email !== undefined ? { email: preserved.email } : {}),
|
|
254
|
+
...(preserved.skipMerge !== undefined ? { skipMerge: preserved.skipMerge } : {}),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const fromProfile = layered.attribution?.co_author;
|
|
259
|
+
if (fromProfile === undefined)
|
|
260
|
+
return {};
|
|
261
|
+
return {
|
|
262
|
+
attributionCoAuthor: {
|
|
263
|
+
...(fromProfile.enabled !== undefined ? { enabled: fromProfile.enabled } : {}),
|
|
264
|
+
...(fromProfile.name !== undefined && fromProfile.name.length > 0
|
|
265
|
+
? { name: fromProfile.name }
|
|
266
|
+
: {}),
|
|
267
|
+
...(fromProfile.email !== undefined && fromProfile.email.length > 0
|
|
268
|
+
? { email: fromProfile.email }
|
|
269
|
+
: {}),
|
|
270
|
+
...(fromProfile.skip_merge !== undefined ? { skipMerge: fromProfile.skip_merge } : {}),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
228
274
|
/**
|
|
229
275
|
* G6 — Codex install-assist probe.
|
|
230
276
|
*
|
|
@@ -408,6 +454,38 @@ function readExistingPolicyForPreservation(targetDir) {
|
|
|
408
454
|
out.commitHygieneRefuseAtCommits = refuseAt;
|
|
409
455
|
}
|
|
410
456
|
}
|
|
457
|
+
// 0.30.0 attribution augmenter. Preserve every field the operator
|
|
458
|
+
// may have configured so re-running `rea init` doesn't silently
|
|
459
|
+
// revert an opt-in. Block AND inline forms agree at the parser
|
|
460
|
+
// layer.
|
|
461
|
+
const attribution = policy['attribution'];
|
|
462
|
+
if (attribution !== null && typeof attribution === 'object') {
|
|
463
|
+
const attr = attribution;
|
|
464
|
+
const coAuthor = attr['co_author'];
|
|
465
|
+
if (coAuthor !== null && typeof coAuthor === 'object') {
|
|
466
|
+
const ca = coAuthor;
|
|
467
|
+
const preserved = {};
|
|
468
|
+
let any = false;
|
|
469
|
+
if (typeof ca['enabled'] === 'boolean') {
|
|
470
|
+
preserved.enabled = ca['enabled'];
|
|
471
|
+
any = true;
|
|
472
|
+
}
|
|
473
|
+
if (typeof ca['name'] === 'string') {
|
|
474
|
+
preserved.name = ca['name'];
|
|
475
|
+
any = true;
|
|
476
|
+
}
|
|
477
|
+
if (typeof ca['email'] === 'string') {
|
|
478
|
+
preserved.email = ca['email'];
|
|
479
|
+
any = true;
|
|
480
|
+
}
|
|
481
|
+
if (typeof ca['skip_merge'] === 'boolean') {
|
|
482
|
+
preserved.skipMerge = ca['skip_merge'];
|
|
483
|
+
any = true;
|
|
484
|
+
}
|
|
485
|
+
if (any)
|
|
486
|
+
out.attributionCoAuthor = preserved;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
411
489
|
return out;
|
|
412
490
|
}
|
|
413
491
|
function readExistingInstalledAt(policyPath) {
|
|
@@ -484,6 +562,28 @@ function writePolicyYaml(targetDir, config, layered) {
|
|
|
484
562
|
lines.push(` - ${JSON.stringify(p)}`);
|
|
485
563
|
}
|
|
486
564
|
}
|
|
565
|
+
// 0.30.0 attribution augmenter — emit the block whenever the layered
|
|
566
|
+
// profile (or a preserved on-disk policy) declared it. We always emit
|
|
567
|
+
// a fully-explicit `enabled` so an operator reading the file can
|
|
568
|
+
// confirm the current state at a glance without falling back to
|
|
569
|
+
// schema defaults. Identity (name/email) is omitted when empty —
|
|
570
|
+
// operators opt in by hand-editing those two fields, which keeps
|
|
571
|
+
// the policy file diff-clean on profile re-init.
|
|
572
|
+
const attr = config.attributionCoAuthor;
|
|
573
|
+
if (attr !== undefined) {
|
|
574
|
+
lines.push(`attribution:`);
|
|
575
|
+
lines.push(` co_author:`);
|
|
576
|
+
lines.push(` enabled: ${attr.enabled === true ? 'true' : 'false'}`);
|
|
577
|
+
if (attr.name !== undefined && attr.name.length > 0) {
|
|
578
|
+
lines.push(` name: ${JSON.stringify(attr.name)}`);
|
|
579
|
+
}
|
|
580
|
+
if (attr.email !== undefined && attr.email.length > 0) {
|
|
581
|
+
lines.push(` email: ${JSON.stringify(attr.email)}`);
|
|
582
|
+
}
|
|
583
|
+
if (attr.skipMerge !== undefined) {
|
|
584
|
+
lines.push(` skip_merge: ${attr.skipMerge ? 'true' : 'false'}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
487
587
|
// 0.18.1+ helixir #9: emit audit.rotation when the layered profile
|
|
488
588
|
// declared it. Empty `rotation: {}` opts in to documented defaults
|
|
489
589
|
// (50 MiB / 30 days); explicit values override.
|
|
@@ -741,6 +841,10 @@ export async function runInit(options) {
|
|
|
741
841
|
...(existingPolicy?.commitHygieneRefuseAtCommits !== undefined
|
|
742
842
|
? { commitHygieneRefuseAtCommits: existingPolicy.commitHygieneRefuseAtCommits }
|
|
743
843
|
: {}),
|
|
844
|
+
// 0.30.0 attribution augmenter — preserved across re-init OR
|
|
845
|
+
// seeded from the layered profile. Same precedence as the
|
|
846
|
+
// wizard path above. Conditional spread for exact-optional.
|
|
847
|
+
...attributionConfigSpread(layeredBase, existingPolicy),
|
|
744
848
|
fromReagent,
|
|
745
849
|
reagentPolicyPath,
|
|
746
850
|
reagentNotices,
|
|
@@ -772,6 +876,12 @@ export async function runInit(options) {
|
|
|
772
876
|
const mergeResult = mergeSettings(settings, desired);
|
|
773
877
|
await writeSettingsAtomic(settingsPath, mergeResult.merged);
|
|
774
878
|
const commitMsgResult = await installCommitMsgHook(targetDir);
|
|
879
|
+
// 0.30.0 attribution augmenter — install the prepare-commit-msg
|
|
880
|
+
// hook unconditionally. The hook is a no-op when
|
|
881
|
+
// policy.attribution.co_author.enabled !== true, so it is safe to
|
|
882
|
+
// ship under every profile; consumers opt in by editing their
|
|
883
|
+
// .rea/policy.yaml.
|
|
884
|
+
const prepareCommitMsgResult = await installPrepareCommitMsgHook(targetDir);
|
|
775
885
|
const prePushResult = await installPrePushFallback({ targetDir });
|
|
776
886
|
const fragmentInput = {
|
|
777
887
|
policyPath: `.${path.sep}rea${path.sep}policy.yaml`.replace(/\\/g, '/'),
|
|
@@ -800,6 +910,14 @@ export async function runInit(options) {
|
|
|
800
910
|
console.log(` + ${path.relative(targetDir, commitMsgResult.gitHook)}`);
|
|
801
911
|
if (commitMsgResult.huskyHook)
|
|
802
912
|
console.log(` + ${path.relative(targetDir, commitMsgResult.huskyHook)}`);
|
|
913
|
+
if (prepareCommitMsgResult.gitHook) {
|
|
914
|
+
const verb = prepareCommitMsgResult.refreshed === true ? '~' : '+';
|
|
915
|
+
console.log(` ${verb} ${path.relative(targetDir, prepareCommitMsgResult.gitHook)} (attribution augmenter)`);
|
|
916
|
+
}
|
|
917
|
+
if (prepareCommitMsgResult.huskyHook) {
|
|
918
|
+
const verb = prepareCommitMsgResult.refreshed === true ? '~' : '+';
|
|
919
|
+
console.log(` ${verb} ${path.relative(targetDir, prepareCommitMsgResult.huskyHook)} (attribution augmenter)`);
|
|
920
|
+
}
|
|
803
921
|
if (prePushResult.written !== undefined) {
|
|
804
922
|
const verb = prePushResult.decision.action === 'refresh' ? '~' : '+';
|
|
805
923
|
console.log(` ${verb} ${path.relative(targetDir, prePushResult.written)} (pre-push fallback)`);
|
|
@@ -828,6 +946,8 @@ export async function runInit(options) {
|
|
|
828
946
|
}
|
|
829
947
|
for (const w of commitMsgResult.warnings)
|
|
830
948
|
warn(w);
|
|
949
|
+
for (const w of prepareCommitMsgResult.warnings)
|
|
950
|
+
warn(w);
|
|
831
951
|
for (const w of prePushResult.warnings)
|
|
832
952
|
warn(w);
|
|
833
953
|
for (const n of config.reagentNotices)
|
|
@@ -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;
|