ceo-orchestration 1.0.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/.claude/adr/ADR-001-runtime-state-directory.md +164 -0
- package/.claude/adr/ADR-002-hooks-package-layout.md +228 -0
- package/.claude/adr/ADR-003-branch-protection-replaces-skill-signing.md +266 -0
- package/.claude/adr/ADR-004-defer-bash-legacy-removal.md +171 -0
- package/.claude/adr/ADR-005-event-stream-v2.md +153 -0
- package/.claude/adr/ADR-006-registry-derived-manifests.md +145 -0
- package/.claude/adr/ADR-007-spec-v1-semver-rc-policy.md +159 -0
- package/.claude/adr/ADR-008-hook-adapter-layer.md +169 -0
- package/.claude/adr/ADR-009-squad-contract.md +167 -0
- package/.claude/adr/ADR-010-canonical-edit-sentinel.md +181 -0
- package/.claude/adr/ADR-011-event-stream-v2.1-injection-flag.md +150 -0
- package/.claude/adr/ADR-012-cross-adapter-golden-fixtures.md +182 -0
- package/.claude/adr/ADR-013-squad-trading-hft.md +135 -0
- package/.claude/adr/ADR-014-hook-migration-batch-policy.md +197 -0
- package/.claude/adr/ADR-015-reflexion-v2-outcome-loop.md +248 -0
- package/.claude/adr/ADR-016-spawn-token-tracking.md +179 -0
- package/.claude/adr/ADR-017-lesson-pruning-policy.md +193 -0
- package/.claude/adr/ADR-018-claim-grammar.md +302 -0
- package/.claude/adr/ADR-019-AMEND-1-confidence-gate-block-mode-lifecycle.md +128 -0
- package/.claude/adr/ADR-019-AMEND-2-CLASS-SHA_EXISTS-promote-to-high-confidence-block.md +67 -0
- package/.claude/adr/ADR-019-confidence-gate-enforcement-lifecycle.md +221 -0
- package/.claude/adr/ADR-020-lesson-pruning-policy-v2.md +171 -0
- package/.claude/adr/ADR-021-e2e-harness-contract.md +189 -0
- package/.claude/adr/ADR-022-reserved-slot.md +52 -0
- package/.claude/adr/ADR-023-docs-freshness-lifecycle.md +184 -0
- package/.claude/adr/ADR-024-perf-baseline-policy.md +222 -0
- package/.claude/adr/ADR-025-squad-edtech.md +236 -0
- package/.claude/adr/ADR-026-squad-government.md +263 -0
- package/.claude/adr/ADR-027-unified-agent-state-backend.md +266 -0
- package/.claude/adr/ADR-028-multi-llm-canonical-parity.md +244 -0
- package/.claude/adr/ADR-029-lexical-tfidf-retrieval.md +205 -0
- package/.claude/adr/ADR-030-llm-as-judge-methodology.md +336 -0
- package/.claude/adr/ADR-031-self-improving-skills.md +221 -0
- package/.claude/adr/ADR-032-interactive-debate-protocol.md +337 -0
- package/.claude/adr/ADR-033-cost-budget-enforcement.md +275 -0
- package/.claude/adr/ADR-034-shared-working-memory.md +233 -0
- package/.claude/adr/ADR-035-otel-export.md +242 -0
- package/.claude/adr/ADR-036-output-safety.md +263 -0
- package/.claude/adr/ADR-037-chaos-testing-methodology.md +289 -0
- package/.claude/adr/ADR-038-session-graph-continuity.md +243 -0
- package/.claude/adr/ADR-039-skill-marketplace-protocol.md +170 -0
- package/.claude/adr/ADR-040-AMEND-2-credential-blocking.md +390 -0
- package/.claude/adr/ADR-040-live-adapter-activation-contract.md +285 -0
- package/.claude/adr/ADR-041-transition-log-convention.md +272 -0
- package/.claude/adr/ADR-042-AMEND-1-read-only-mcp-tools-expansion.md +214 -0
- package/.claude/adr/ADR-042-mcp-server-contract.md +727 -0
- package/.claude/adr/ADR-043-soc2-audit-trail-mapping.md +503 -0
- package/.claude/adr/ADR-044-formal-verification-pilot.md +505 -0
- package/.claude/adr/ADR-045-policy-as-code-engine.md +705 -0
- package/.claude/adr/ADR-046-deterministic-replay.md +167 -0
- package/.claude/adr/ADR-047-predictive-budgeting.md +213 -0
- package/.claude/adr/ADR-048-cross-plan-memory.md +227 -0
- package/.claude/adr/ADR-049-policy-engine-dual-path-deprecation.md +96 -0
- package/.claude/adr/ADR-049a-worktree-orchestration-policy.md +414 -0
- package/.claude/adr/ADR-050-native-subagents-dual-rail.md +165 -0
- package/.claude/adr/ADR-051-skill-reference-expanded-trust-boundary.md +282 -0
- package/.claude/adr/ADR-052-multi-model-dispatch-by-role.md +444 -0
- package/.claude/adr/ADR-053-sentinel-hmac-deferred.md +227 -0
- package/.claude/adr/ADR-054-AMEND-1-anthropic-admin-key-tier.md +131 -0
- package/.claude/adr/ADR-054-github-token-rotation.md +111 -0
- package/.claude/adr/ADR-055-AMEND-1-spool-writer-async-drain.md +170 -0
- package/.claude/adr/ADR-055-AMEND-2-chain-reset-marker.md +126 -0
- package/.claude/adr/ADR-055-AMEND-3-opportunistic-drain-nonblocking.md +183 -0
- package/.claude/adr/ADR-055-audit-log-hmac-chain.md +264 -0
- package/.claude/adr/ADR-056-hook-lifecycle-expansion.md +261 -0
- package/.claude/adr/ADR-057-output-scan-redaction.md +268 -0
- package/.claude/adr/ADR-058-brainstorm-gate-and-two-pass-review.md +240 -0
- package/.claude/adr/ADR-059-skill-bootstrap-env-knob.md +204 -0
- package/.claude/adr/ADR-060-curated-skill-import-pipeline.md +464 -0
- package/.claude/adr/ADR-061-runtime-cost-streaming.md +171 -0
- package/.claude/adr/ADR-062-AMEND-1-rag-conditional-default-on-supersedes-opt-in.md +232 -0
- package/.claude/adr/ADR-062-rag-sidecar-mcp-opt-in.md +231 -0
- package/.claude/adr/ADR-063-agent-eval-empirical-dispatch-validation.md +609 -0
- package/.claude/adr/ADR-064-dynamic-tier-policy-learned-dispatch.md +288 -0
- package/.claude/adr/ADR-065-audit-event-naming-convention.md +185 -0
- package/.claude/adr/ADR-066-context-mode-orthogonal-to-manifest.md +92 -0
- package/.claude/adr/ADR-067-ceo-model-downshift-static-routing.md +219 -0
- package/.claude/adr/ADR-069-wondelai-skills-import-refused.md +183 -0
- package/.claude/adr/ADR-070-audit-emit-package-layout.md +228 -0
- package/.claude/adr/ADR-071-benchmark-comparison-methodology.md +209 -0
- package/.claude/adr/ADR-072-test-discovery-via-conftest.md +184 -0
- package/.claude/adr/ADR-073-semver-bump-criteria-sprint-32.md +209 -0
- package/.claude/adr/ADR-074-sprint-32-phase-3-b1-refused.md +320 -0
- package/.claude/adr/ADR-075-sprint-32-phase-5-b5-benchmark-refused.md +250 -0
- package/.claude/adr/ADR-076-sprint-32-final-closure.md +218 -0
- package/.claude/adr/ADR-077-2026-04-24-webfetch-injection-incident.md +203 -0
- package/.claude/adr/ADR-078-sentinel-cosign-clarification.md +295 -0
- package/.claude/adr/ADR-079-prompt-sha-salt-hmac-impact.md +221 -0
- package/.claude/adr/ADR-080-rail-anomaly-h4-defense-in-depth.md +1143 -0
- package/.claude/adr/ADR-081-token-as-time-unit.md +272 -0
- package/.claude/adr/ADR-082-l7c-mitigation-default-on.md +240 -0
- package/.claude/adr/ADR-083-mcp-injection-scanner.md +225 -0
- package/.claude/adr/ADR-084-multi-adapter-refused-claude-only.md +152 -0
- package/.claude/adr/ADR-085-framework-landscape-claude-only.md +183 -0
- package/.claude/adr/ADR-086-checkpointing-refused.md +124 -0
- package/.claude/adr/ADR-087-AMEND-1-otel-consume-native-opt-in.md +217 -0
- package/.claude/adr/ADR-087-otel-emit-refused.md +136 -0
- package/.claude/adr/ADR-088-guardrails-library-refused.md +128 -0
- package/.claude/adr/ADR-089-sec-cluster-disposition.md +182 -0
- package/.claude/adr/ADR-090-framework-activation-defaults.md +217 -0
- package/.claude/adr/ADR-091-dogfood-validation-deferred.md +128 -0
- package/.claude/adr/ADR-092-plan-closure-honest-deferral.md +165 -0
- package/.claude/adr/ADR-093-refused-adr-moratorium.md +181 -0
- package/.claude/adr/ADR-094-claude-sdk-compat-version-pinning.md +160 -0
- package/.claude/adr/ADR-095-calendar-gate-retraction.md +202 -0
- package/.claude/adr/ADR-096-vibecoder-only-by-design.md +215 -0
- package/.claude/adr/ADR-097-function-length-advisory-permanent.md +186 -0
- package/.claude/adr/ADR-098-ceo-boot-audit-emit-register.md +251 -0
- package/.claude/adr/ADR-099-changesets-adoption.md +245 -0
- package/.claude/adr/ADR-100-trusted-dependencies-re-affirm.md +208 -0
- package/.claude/adr/ADR-101-replay-redact-helper.md +106 -0
- package/.claude/adr/ADR-102-mcp-introspection-extends-042.md +165 -0
- package/.claude/adr/ADR-103-calendar-gate-final-purge.md +121 -0
- package/.claude/adr/ADR-104-AMEND-1-aek-dated-promotion-criteria.md +338 -0
- package/.claude/adr/ADR-104-adaptive-execution-kernel-advisory.md +210 -0
- package/.claude/adr/ADR-105-multi-llm-coordinated-supersede.md +126 -0
- package/.claude/adr/ADR-106-codex-mcp-adapter-contract.md +153 -0
- package/.claude/adr/ADR-107-pair-rail-mandatory-l2-plus.md +189 -0
- package/.claude/adr/ADR-108-cross-llm-veto-floor.md +129 -0
- package/.claude/adr/ADR-109-codex-skill-rehash-protocol.md +104 -0
- package/.claude/adr/ADR-110-codex-pretool-enforcement.md +94 -0
- package/.claude/adr/ADR-111-locked-corpus-governance.md +191 -0
- package/.claude/adr/ADR-112-grandfather-cap-scope-clarification.md +192 -0
- package/.claude/adr/ADR-113-plan-084-canonical-guard-extension.md +59 -0
- package/.claude/adr/ADR-114-codex-egress-redaction-symmetry.md +72 -0
- package/.claude/adr/ADR-115-post-sota-maintenance-mode.md +152 -0
- package/.claude/adr/ADR-116-AMEND-1-kernel-extension-v2.md +640 -0
- package/.claude/adr/ADR-116-kernel-hard-deny-tier-0-extension.md +465 -0
- package/.claude/adr/ADR-117-adr-id-collision-rename-policy.md +279 -0
- package/.claude/adr/ADR-118-AMEND-1-phase-c-enforcing-flip.md +191 -0
- package/.claude/adr/ADR-118-god-mode-auto-usable-state.md +338 -0
- package/.claude/adr/ADR-119-sentinel-unlock-contract.md +133 -0
- package/.claude/adr/ADR-120-pii-core-promotion.md +280 -0
- package/.claude/adr/ADR-121-sentinel-signers-rotation-policy.md +434 -0
- package/.claude/adr/ADR-122-dpop-mcp-bearer-replay-defense.md +232 -0
- package/.claude/adr/ADR-123-streaming-adapter-canonical-source.md +130 -0
- package/.claude/adr/ADR-124-post-audit-sota-execution-mode.md +362 -0
- package/.claude/adr/ADR-125-risk-tiered-defaulting-doctrine.md +355 -0
- package/.claude/adr/ADR-126-governed-sidecar-capability-model.md +509 -0
- package/.claude/adr/ADR-127-pair-rail-advisory-promotion.md +218 -0
- package/.claude/adr/ADR-128-c2-vector-memory-capability-class.md +380 -0
- package/.claude/adr/ADR-129-AMEND-1-key-floor-waiver-lift.md +249 -0
- package/.claude/adr/ADR-129-c1-crypto-capability-class.md +289 -0
- package/.claude/adr/ADR-131-c5-dev-tools-capability-class.md +215 -0
- package/.claude/adr/ADR-132-goap-advisory-planning-doctrine.md +333 -0
- package/.claude/adr/ADR-133-autonomous-loop-opt-in-capability-doctrine.md +440 -0
- package/.claude/adr/ADR-135-AMEND-1-write-mode-trust-boundary.md +457 -0
- package/.claude/adr/ADR-135-AMEND-2-write-mode-activation.md +175 -0
- package/.claude/adr/ADR-135-federation-contract-mvp.md +253 -0
- package/.claude/adr/ADR-136-AMEND-1-workflow-primitive-adoption.md +139 -0
- package/.claude/adr/ADR-136-workflow-engine-doctrine.md +155 -0
- package/.claude/adr/ADR-137-skill-priority-stack-decision.md +162 -0
- package/.claude/adr/ADR-138-ac-format-priority-and-story-anchor.md +149 -0
- package/.claude/adr/ADR-139-coverage-doctrine-tiered.md +133 -0
- package/.claude/adr/ADR-140-receiving-review-doctrine.md +136 -0
- package/.claude/adr/ADR-141-reduce-protocol.md +124 -0
- package/.claude/adr/ADR-142-opus-4-8-model-bump.md +116 -0
- package/.claude/adr/ADR-143-git-hook-bypass-guard.md +166 -0
- package/.claude/adr/ADR-144-subagent-model-tiering-frontmatter.md +111 -0
- package/.claude/adr/ADR-145-cross-model-review-persona-demand-modality.md +103 -0
- package/.claude/adr/ADR-146-adversary-review-hook.md +122 -0
- package/.claude/adr/ADR-147-eval-harness-doctrine.md +109 -0
- package/.claude/adr/ADR-148-canonical-pricing-source.md +123 -0
- package/.claude/adr/ADR-149-model-id-allowlist.md +196 -0
- package/.claude/adr/ADR-150-commit-signing-policy.md +12 -0
- package/.claude/adr/ADR-151-fan-plan-advisory-bridge.md +178 -0
- package/.claude/adr/ADR-152-claude-md-decomposition.md +262 -0
- package/.claude/adr/ADR-153-compaction-continuity.md +141 -0
- package/.claude/adr/ADR-154-updatedinput-single-rewriter.md +68 -0
- package/.claude/adr/ADR-155-install-baseline-manifest.md +66 -0
- package/.claude/adr/ADR-156-constitution-sync-cascade.md +122 -0
- package/.claude/adr/README.md +392 -0
- package/.claude/adversary.md +116 -0
- package/.claude/agent-metrics.md +101 -0
- package/.claude/agents/_dispatch.md +30 -0
- package/.claude/agents/_probe_architect.md +45 -0
- package/.claude/agents/_probe_canonical_edit.md +46 -0
- package/.claude/agents/_probe_missing_skill.md +42 -0
- package/.claude/agents/code-reviewer.md +166 -0
- package/.claude/agents/devops.md +114 -0
- package/.claude/agents/identity-trust-architect.md +234 -0
- package/.claude/agents/incident-commander.md +285 -0
- package/.claude/agents/llm-finops-architect.md +265 -0
- package/.claude/agents/performance-engineer.md +148 -0
- package/.claude/agents/qa-architect.md +167 -0
- package/.claude/agents/security-engineer.md +192 -0
- package/.claude/agents/threat-detection-engineer.md +238 -0
- package/.claude/benchmarks/_schemas/judge-prompt.md +26 -0
- package/.claude/benchmarks/_schemas/judge-rubric-example.json +11 -0
- package/.claude/benchmarks/_schemas/judge-rubric.yaml +39 -0
- package/.claude/benchmarks/calibration-grades.jsonl +6 -0
- package/.claude/benchmarks/human-sample-calibration.md +232 -0
- package/.claude/benchmarks/judge-rotation-schedule.md +61 -0
- package/.claude/benchmarks/retrieval-judgment-set.yaml +194 -0
- package/.claude/benchmarks/tests/test_retrieval_recall_gate.py +330 -0
- package/.claude/commands/agent-budget.md +105 -0
- package/.claude/commands/architect.md +130 -0
- package/.claude/commands/audit-page.md +149 -0
- package/.claude/commands/audit-tokens.md +89 -0
- package/.claude/commands/ceo-boot.md +118 -0
- package/.claude/commands/ceo-info.md +71 -0
- package/.claude/commands/debate.md +258 -0
- package/.claude/commands/effort.md +99 -0
- package/.claude/commands/fan-plan.md +129 -0
- package/.claude/commands/goap.md +163 -0
- package/.claude/commands/lesson-review.md +66 -0
- package/.claude/commands/memory-scratchpad.md +100 -0
- package/.claude/commands/onboard.md +204 -0
- package/.claude/commands/pitfall.md +54 -0
- package/.claude/commands/resume.md +90 -0
- package/.claude/commands/self-test.md +83 -0
- package/.claude/commands/skill-review.md +102 -0
- package/.claude/commands/spawn.md +212 -0
- package/.claude/commands/squad-install.md +94 -0
- package/.claude/commands/status.md +177 -0
- package/.claude/commands/terse.md +81 -0
- package/.claude/commands/veto-check.md +63 -0
- package/.claude/data/audit-registry.golden.txt +306 -0
- package/.claude/data/canonical_models.json +1030 -0
- package/.claude/data/confidence-gate-class-tiers.json +24 -0
- package/.claude/data/cookbook_patterns.json +139 -0
- package/.claude/data/federation/enabled.md +34 -0
- package/.claude/data/federation/lan-enabled.md +38 -0
- package/.claude/data/federation/peers.example.yaml +89 -0
- package/.claude/data/goap/action-cost-baseline.json +29 -0
- package/.claude/dispatcher/disable_predicate_eval.py +630 -0
- package/.claude/dispatcher/routing-matrix-loader.py +874 -0
- package/.claude/dispatcher/routing-matrix.yaml +343 -0
- package/.claude/dispatcher/tests/conftest.py +11 -0
- package/.claude/dispatcher/tests/test_disable_predicate_eval.py +424 -0
- package/.claude/dispatcher/tests/test_routing_matrix_loader.py +461 -0
- package/.claude/docs/dpop-scope.md +79 -0
- package/.claude/docs/sentinel-signers-rotation-DRAFT.md +117 -0
- package/.claude/eval/README.md +73 -0
- package/.claude/eval/reporter.py +109 -0
- package/.claude/eval/runner.py +532 -0
- package/.claude/eval/self_test.yaml +57 -0
- package/.claude/eval/tasks/__init__.py +185 -0
- package/.claude/eval/tasks/t01_fix_off_by_one.py +52 -0
- package/.claude/eval/tasks/t02_implement_fizzbuzz.py +65 -0
- package/.claude/eval/tasks/t03_json_config_parse.py +80 -0
- package/.claude/eval/tasks/t04_refactor_dedupe.py +71 -0
- package/.claude/eval/tasks/t05_add_unit_test.py +77 -0
- package/.claude/eval/tasks/t06_palindrome.py +58 -0
- package/.claude/eval/tasks/t07_sql_param_fix.py +69 -0
- package/.claude/eval/tasks/t08_word_count.py +53 -0
- package/.claude/eval/tasks/t09_readme_doc.py +64 -0
- package/.claude/eval/tasks/t10_binary_search.py +58 -0
- package/.claude/frontend-team.md +202 -0
- package/.claude/governance/README.md +37 -0
- package/.claude/governance/audit_tokens_allowlist.json +37 -0
- package/.claude/governance/codex-cli-binary-sha256.txt +32 -0
- package/.claude/governance/codex-cli-pin.txt +26 -0
- package/.claude/governance/function-length-grandfather.yaml +2095 -0
- package/.claude/governance/governance-waivers.yaml +28 -0
- package/.claude/governance/pair-rail-inputs-hash-manifest.txt +32 -0
- package/.claude/governance/pair-rail-verdict-template.md +58 -0
- package/.claude/governance/pair-rail-verdict-v1.16.0-rc.1.md +120 -0
- package/.claude/governance/pair-rail-verdict-v1.16.0.md +64 -0
- package/.claude/gpg-revocations.jsonl +1 -0
- package/.claude/hooks/SessionEnd.py +353 -0
- package/.claude/hooks/SessionStart.py +345 -0
- package/.claude/hooks/Stop.py +195 -0
- package/.claude/hooks/UserPromptSubmit.py +329 -0
- package/.claude/hooks/_lib/EXECUTION-CONTEXT-DEFERRED.md +82 -0
- package/.claude/hooks/_lib/__init__.py +26 -0
- package/.claude/hooks/_lib/action_required.py +592 -0
- package/.claude/hooks/_lib/adapters/__init__.py +87 -0
- package/.claude/hooks/_lib/adapters/_constants.py +127 -0
- package/.claude/hooks/_lib/adapters/claude.py +167 -0
- package/.claude/hooks/_lib/adapters/codex.py +754 -0
- package/.claude/hooks/_lib/adapters/live/__init__.py +378 -0
- package/.claude/hooks/_lib/adapters/live/_breaker.py +309 -0
- package/.claude/hooks/_lib/adapters/live/_cost.py +389 -0
- package/.claude/hooks/_lib/adapters/live/_policy.py +319 -0
- package/.claude/hooks/_lib/adapters/live/_result.py +206 -0
- package/.claude/hooks/_lib/adapters/live/_transport.py +681 -0
- package/.claude/hooks/_lib/adapters/live/claude.py +1027 -0
- package/.claude/hooks/_lib/adapters/live/claude_batch.py +652 -0
- package/.claude/hooks/_lib/adapters/live/gemini.py +270 -0
- package/.claude/hooks/_lib/adapters/live/local.py +195 -0
- package/.claude/hooks/_lib/adapters/live/openai.py +371 -0
- package/.claude/hooks/_lib/adversary_rules.py +196 -0
- package/.claude/hooks/_lib/agent_frontmatter.py +288 -0
- package/.claude/hooks/_lib/audit_emit.py +11746 -0
- package/.claude/hooks/_lib/audit_emit_dispatch.py +179 -0
- package/.claude/hooks/_lib/audit_hmac.py +1146 -0
- package/.claude/hooks/_lib/audit_rotation.py +101 -0
- package/.claude/hooks/_lib/canonical_json.py +145 -0
- package/.claude/hooks/_lib/codex_cli_shape.py +502 -0
- package/.claude/hooks/_lib/codex_egress_redact.py +185 -0
- package/.claude/hooks/_lib/confidence_labels.py +338 -0
- package/.claude/hooks/_lib/contract.py +254 -0
- package/.claude/hooks/_lib/cookbook_patterns.py +136 -0
- package/.claude/hooks/_lib/cost_envelope.py +719 -0
- package/.claude/hooks/_lib/credentials.py +188 -0
- package/.claude/hooks/_lib/effective_config.py +767 -0
- package/.claude/hooks/_lib/egress_taxonomy.py +448 -0
- package/.claude/hooks/_lib/embeddings.py +322 -0
- package/.claude/hooks/_lib/env_guard.py +353 -0
- package/.claude/hooks/_lib/env_persist_allowlist.py +147 -0
- package/.claude/hooks/_lib/escalation_signals.py +335 -0
- package/.claude/hooks/_lib/estimation/__init__.py +12 -0
- package/.claude/hooks/_lib/estimation/bayesian.py +147 -0
- package/.claude/hooks/_lib/estimation/pipeline.py +209 -0
- package/.claude/hooks/_lib/exceptions.py +101 -0
- package/.claude/hooks/_lib/execution_context.py +208 -0
- package/.claude/hooks/_lib/federation/__init__.py +104 -0
- package/.claude/hooks/_lib/federation/audit_chain.py +118 -0
- package/.claude/hooks/_lib/federation/audit_chain_ext.py +408 -0
- package/.claude/hooks/_lib/federation/cert_inspector.py +573 -0
- package/.claude/hooks/_lib/federation/client.py +327 -0
- package/.claude/hooks/_lib/federation/handlers/__init__.py +30 -0
- package/.claude/hooks/_lib/federation/handlers/audit_event_batch.py +346 -0
- package/.claude/hooks/_lib/federation/handlers/audit_event_push.py +395 -0
- package/.claude/hooks/_lib/federation/handlers/peer_register.py +484 -0
- package/.claude/hooks/_lib/federation/handlers/peer_revoke.py +356 -0
- package/.claude/hooks/_lib/federation/identity.py +1056 -0
- package/.claude/hooks/_lib/federation/rate_limit.py +476 -0
- package/.claude/hooks/_lib/federation/replay.py +284 -0
- package/.claude/hooks/_lib/federation/scopes.py +168 -0
- package/.claude/hooks/_lib/federation/server.py +2218 -0
- package/.claude/hooks/_lib/file_walker.py +145 -0
- package/.claude/hooks/_lib/filelock.py +191 -0
- package/.claude/hooks/_lib/frontmatter.py +124 -0
- package/.claude/hooks/_lib/git_bypass.py +971 -0
- package/.claude/hooks/_lib/gpg_verify.py +356 -0
- package/.claude/hooks/_lib/guardrail_validator.py +478 -0
- package/.claude/hooks/_lib/injection_patterns.py +252 -0
- package/.claude/hooks/_lib/injection_salt.py +160 -0
- package/.claude/hooks/_lib/mcp/__init__.py +5 -0
- package/.claude/hooks/_lib/mcp/bearer_replay.py +279 -0
- package/.claude/hooks/_lib/mcp/canonical_guard.py +1140 -0
- package/.claude/hooks/_lib/mcp_bearer_friction.py +475 -0
- package/.claude/hooks/_lib/mcp_injection_scan.py +250 -0
- package/.claude/hooks/_lib/mcp_routing.py +151 -0
- package/.claude/hooks/_lib/memory_shared.py +592 -0
- package/.claude/hooks/_lib/metrics.py +241 -0
- package/.claude/hooks/_lib/model_routing.py +227 -0
- package/.claude/hooks/_lib/otel/__init__.py +34 -0
- package/.claude/hooks/_lib/otel/bounded_exporter.py +373 -0
- package/.claude/hooks/_lib/otel/hook_bridge.py +53 -0
- package/.claude/hooks/_lib/otel/queue.py +229 -0
- package/.claude/hooks/_lib/otel_emit.py +604 -0
- package/.claude/hooks/_lib/output_scan.py +1062 -0
- package/.claude/hooks/_lib/output_scan_dedup.py +379 -0
- package/.claude/hooks/_lib/pair_rail_decide.py +244 -0
- package/.claude/hooks/_lib/payload.py +195 -0
- package/.claude/hooks/_lib/persona_routing.py +244 -0
- package/.claude/hooks/_lib/pii_patterns.py +851 -0
- package/.claude/hooks/_lib/plan_frontmatter.py +166 -0
- package/.claude/hooks/_lib/policy.py +1527 -0
- package/.claude/hooks/_lib/policy_preprocessors.py +462 -0
- package/.claude/hooks/_lib/rag_bridge.py +624 -0
- package/.claude/hooks/_lib/rag_events.py +171 -0
- package/.claude/hooks/_lib/rag_router.py +253 -0
- package/.claude/hooks/_lib/redact.py +228 -0
- package/.claude/hooks/_lib/replay_redact.py +511 -0
- package/.claude/hooks/_lib/scratchpad_lib.py +225 -0
- package/.claude/hooks/_lib/secret_patterns.py +905 -0
- package/.claude/hooks/_lib/sentinel_signers.py +740 -0
- package/.claude/hooks/_lib/spec_context_sanitizer.py +258 -0
- package/.claude/hooks/_lib/spool_writer.py +2613 -0
- package/.claude/hooks/_lib/state_store.py +476 -0
- package/.claude/hooks/_lib/subagent_dispatch.py +244 -0
- package/.claude/hooks/_lib/swarm_circuit_breaker.py +203 -0
- package/.claude/hooks/_lib/swarm_enable_gate.py +152 -0
- package/.claude/hooks/_lib/team.py +128 -0
- package/.claude/hooks/_lib/test_isolation.py +352 -0
- package/.claude/hooks/_lib/testing.py +351 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_attack_surface.py +251 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_audit_stitching.py +135 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_identity.py +234 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_replay.py +204 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_sentinel_stage2.py +214 -0
- package/.claude/hooks/_lib/tests/federation/test_federation_server.py +385 -0
- package/.claude/hooks/_lib/tests/test_confidence_gate_class_block.py +313 -0
- package/.claude/hooks/_lib/tests/test_cost_envelope.py +759 -0
- package/.claude/hooks/_lib/tests/test_execution_context.py +254 -0
- package/.claude/hooks/_lib/tests/test_goap_advisory_invariant.py +134 -0
- package/.claude/hooks/_lib/tests/test_goap_planner.py +368 -0
- package/.claude/hooks/_lib/tests/test_plan104_audit_emit.py +324 -0
- package/.claude/hooks/_lib/tests/test_plan104_demand_resolver.py +584 -0
- package/.claude/hooks/_lib/tests/test_plan104_demand_scan.py +164 -0
- package/.claude/hooks/_lib/tests/test_plan104_microbench.py +109 -0
- package/.claude/hooks/_lib/tests/test_plan104_waive_parser.py +113 -0
- package/.claude/hooks/_lib/tests/test_plan105_audit_emit.py +259 -0
- package/.claude/hooks/_lib/tests/test_plan105_check_roadmap_binding.py +68 -0
- package/.claude/hooks/_lib/tests/test_plan105_goap_planner.py +158 -0
- package/.claude/hooks/_lib/tests/test_plan105_spawn_outcome.py +234 -0
- package/.claude/hooks/_lib/tests/test_rag_dead_code_disposition.py +262 -0
- package/.claude/hooks/_lib/tests/test_rag_router.py +209 -0
- package/.claude/hooks/_lib/tests/test_swarm_circuit_breaker.py +278 -0
- package/.claude/hooks/_lib/tests/test_swarm_kill_switch_chain.py +360 -0
- package/.claude/hooks/_lib/tier_policy/__init__.py +123 -0
- package/.claude/hooks/_lib/tier_policy/_agent_frontmatter.py +509 -0
- package/.claude/hooks/_lib/tier_policy/_constants.py +376 -0
- package/.claude/hooks/_lib/tier_policy/_types.py +355 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/baseline.json +17 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/oversize_64kib.json +1 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/prototype_pollution_attack.yaml +14 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/schema_v1_sample.json +5 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/schema_v2_sample.json +17 -0
- package/.claude/hooks/_lib/tier_policy/fixtures/yaml_bomb_attack.yaml +20 -0
- package/.claude/hooks/_lib/tier_policy/loader.py +476 -0
- package/.claude/hooks/_lib/tokens.py +136 -0
- package/.claude/hooks/_lib/tool_lifecycle.py +488 -0
- package/.claude/hooks/_lib/trusted_env.py +77 -0
- package/.claude/hooks/_python-hook.sh +242 -0
- package/.claude/hooks/accel_dispatch.py +172 -0
- package/.claude/hooks/adequacy_gate.py +424 -0
- package/.claude/hooks/audit_log.py +1352 -0
- package/.claude/hooks/auto_boot.py +518 -0
- package/.claude/hooks/check_adversary.py +273 -0
- package/.claude/hooks/check_agent_spawn.py +2696 -0
- package/.claude/hooks/check_anti_ceo_overhead.py +786 -0
- package/.claude/hooks/check_arbitration_kernel.py +544 -0
- package/.claude/hooks/check_bash_canonical_forensic.py +180 -0
- package/.claude/hooks/check_bash_safety.py +1483 -0
- package/.claude/hooks/check_budget.py +916 -0
- package/.claude/hooks/check_canonical_edit.py +1197 -0
- package/.claude/hooks/check_closeout_guard.py +154 -0
- package/.claude/hooks/check_codex_filewrite.py +366 -0
- package/.claude/hooks/check_codex_response.py +403 -0
- package/.claude/hooks/check_confidence_gate.py +545 -0
- package/.claude/hooks/check_config_change.py +346 -0
- package/.claude/hooks/check_config_protection.py +381 -0
- package/.claude/hooks/check_cost_envelope.py +286 -0
- package/.claude/hooks/check_fluency_nudge.py +747 -0
- package/.claude/hooks/check_mcp_response.py +234 -0
- package/.claude/hooks/check_output_safety.py +237 -0
- package/.claude/hooks/check_output_secrets.py +518 -0
- package/.claude/hooks/check_pair_rail.py +1700 -0
- package/.claude/hooks/check_plan_edit.py +905 -0
- package/.claude/hooks/check_postcompact_reinject.py +265 -0
- package/.claude/hooks/check_precompact_continuity.py +379 -0
- package/.claude/hooks/check_protocol_semver_cascade.py +401 -0
- package/.claude/hooks/check_read_injection.py +366 -0
- package/.claude/hooks/check_scratchpad_access.py +228 -0
- package/.claude/hooks/check_setup_verification.py +297 -0
- package/.claude/hooks/check_skill_bootstrap_post.py +339 -0
- package/.claude/hooks/check_skill_patch_sentinel.py +413 -0
- package/.claude/hooks/check_skill_reference_read.py +518 -0
- package/.claude/hooks/check_subagent_fabrication.py +45 -0
- package/.claude/hooks/check_subagent_start.py +232 -0
- package/.claude/hooks/check_tier_policy.py +211 -0
- package/.claude/hooks/check_tier_policy_misrouting_24h.py +187 -0
- package/.claude/hooks/check_webfetch_injection.py +277 -0
- package/.claude/hooks/check_worktree_writer.py +773 -0
- package/.claude/hooks/codex_review_user_code.py +304 -0
- package/.claude/hooks/emit_architect_outcome.py +232 -0
- package/.claude/hooks/latency_report.py +343 -0
- package/.claude/hooks/policy_dispatch.py +168 -0
- package/.claude/hooks/review_loop.py +560 -0
- package/.claude/hooks/route.py +115 -0
- package/.claude/hooks/tests/_agent_fixture.py +153 -0
- package/.claude/hooks/tests/adapters/__init__.py +0 -0
- package/.claude/hooks/tests/adapters/live/__init__.py +0 -0
- package/.claude/hooks/tests/adapters/live/test_adapters.py +488 -0
- package/.claude/hooks/tests/adapters/live/test_audit_wiring.py +81 -0
- package/.claude/hooks/tests/adapters/live/test_breaker.py +272 -0
- package/.claude/hooks/tests/adapters/live/test_cost.py +191 -0
- package/.claude/hooks/tests/adapters/live/test_o7_modernization.py +670 -0
- package/.claude/hooks/tests/adapters/live/test_policy.py +168 -0
- package/.claude/hooks/tests/conftest.py +139 -0
- package/.claude/hooks/tests/fixtures/adapters/claude/in/agent_spawn_compliant.json +9 -0
- package/.claude/hooks/tests/fixtures/adapters/claude/in/bash_safe_command.json +8 -0
- package/.claude/hooks/tests/fixtures/adapters/claude/in/post_audit_event.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/claude/out/allow.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/claude/out/block_with_reason.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/codex/in/.gitkeep +1 -0
- package/.claude/hooks/tests/fixtures/adapters/codex/out/.gitkeep +1 -0
- package/.claude/hooks/tests/fixtures/adapters/gemini/GAPS.md +46 -0
- package/.claude/hooks/tests/fixtures/adapters/gemini/in/agent_spawn_minimal.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/gemini/in/bash_minimal.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/gemini/out/allow.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/local/in/agent_spawn_ollama.json +19 -0
- package/.claude/hooks/tests/fixtures/adapters/local/in/bash_minimal.json +8 -0
- package/.claude/hooks/tests/fixtures/adapters/local/out/allow.json +1 -0
- package/.claude/hooks/tests/fixtures/adapters/openai/in/agent_spawn_chat_completions.json +13 -0
- package/.claude/hooks/tests/fixtures/adapters/openai/in/bash_responses_api.json +9 -0
- package/.claude/hooks/tests/fixtures/adapters/openai/out/allow.json +1 -0
- package/.claude/hooks/tests/fixtures/anti_ceo_overhead/should-NOT-block-on-Y.ndjson +13 -0
- package/.claude/hooks/tests/fixtures/anti_ceo_overhead/should-block-on-X.ndjson +9 -0
- package/.claude/hooks/tests/fixtures/byte_identity/__init__.py +5 -0
- package/.claude/hooks/tests/fixtures/byte_identity/bash_safety_fuzzer.py +287 -0
- package/.claude/hooks/tests/fixtures/byte_identity/plan_edit_fuzzer.py +364 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/aws-iam-policy-arn-id-25.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/blog-paragraph-18.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/boilerplate-26.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/cdn-cache-key-12.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/certificate-fingerprint-10.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/changelog-19.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/commit-sha-01.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/django-csrf-token-24.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/docker-image-04.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/docs-example-22.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/haiku-20.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/hex-placeholder-15.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/hex-short-23.txt +5 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/image-thumbnail-09.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/jwt-payload-decoded-08.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/kubernetes-uid-06.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/md5-hash-02.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/phone-number-16.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/postgres-uuid-05.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/redis-cluster-node-13.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/session-token-11.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/sha256-checksum-03.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/short-token-21.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/software-license-14.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/telemetry-trace-07.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/negative/zip-postal-17.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/binance-api-key-alnum-03.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/binance-api-key-hex-01.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/binance-api-key-hex-02.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bip39-mnemonic-12-31.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bip39-mnemonic-12-33.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bip39-mnemonic-24-32.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bitfinex-api-key-11.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bitfinex-api-key-12.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bitfinex-api-key-13.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bitstamp-api-key-30.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bitstamp-customer-id-29.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bybit-api-key-18.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bybit-api-key-19.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bybit-api-secret-20.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/bybit-combined-21.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/coinbase-api-key-uuid-04.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/coinbase-api-secret-b64-05.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/coinbase-combined-07.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/coinbase-passphrase-06.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/evm-private-key-34.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/evm-private-key-35.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/evm-private-key-36.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/generic-api-key-37.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/generic-api-key-38.txt +3 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/generic-api-key-39.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kraken-api-key-08.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kraken-api-secret-09.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kraken-combined-10.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kucoin-api-key-uuid-26.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kucoin-api-secret-uuid-27.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/kucoin-passphrase-28.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/okx-api-key-uuid-22.txt +1 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/okx-api-secret-23.txt +2 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/okx-combined-25.txt +4 -0
- package/.claude/hooks/tests/fixtures/exchange_keys/positive/okx-passphrase-24.txt +1 -0
- package/.claude/hooks/tests/fixtures/hooks/audit_log/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/audit_log/out.json +0 -0
- package/.claude/hooks/tests/fixtures/hooks/check_agent_spawn/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_agent_spawn/out.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_bash_safety/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_bash_safety/out.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_canonical_edit/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_canonical_edit/out.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_confidence_gate/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_confidence_gate/out.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_plan_edit/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_plan_edit/out.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_read_injection/in.json +1 -0
- package/.claude/hooks/tests/fixtures/hooks/check_read_injection/out.json +1 -0
- package/.claude/hooks/tests/fixtures/lifecycle/concurrent_interleaved.json +36 -0
- package/.claude/hooks/tests/fixtures/lifecycle/orphaned_pre.json +8 -0
- package/.claude/hooks/tests/fixtures/lifecycle/paired_bash_post.json +8 -0
- package/.claude/hooks/tests/fixtures/lifecycle/paired_bash_pre.json +9 -0
- package/.claude/hooks/tests/fixtures/normalized/agent_spawn_chat_completions.json +36 -0
- package/.claude/hooks/tests/fixtures/normalized/agent_spawn_compliant.json +24 -0
- package/.claude/hooks/tests/fixtures/normalized/agent_spawn_minimal.json +24 -0
- package/.claude/hooks/tests/fixtures/normalized/agent_spawn_ollama.json +42 -0
- package/.claude/hooks/tests/fixtures/normalized/bash_minimal.json +23 -0
- package/.claude/hooks/tests/fixtures/normalized/bash_responses_api.json +32 -0
- package/.claude/hooks/tests/fixtures/normalized/bash_safe_command.json +23 -0
- package/.claude/hooks/tests/fixtures/normalized/post_audit_event.json +31 -0
- package/.claude/hooks/tests/fixtures/output_safety/control/01_random_hash_log.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/control/02_docs_mention_email_no_address.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/control/03_partial_jwt_two_segments.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/control/04_random_11_digits_no_cpf_context.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/control/05_credit_card_shape_invalid_luhn.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/01_api_key_anthropic.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/02_api_key_github_pat_classic.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/03_api_key_github_fine_grained.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/04_api_key_aws_access_key.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/05_api_key_aws_secret_assignment.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/06_jwt.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/07_bearer.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/08_cpf_with_context.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/09_cnpj_with_context.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/10_credit_card_luhn_valid.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/11_email_in_login_context.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/12_nfkc_full_width.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/13_zero_width_evasion.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/14_bidi_evasion.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_safety/positive/15_base64_encoded_secret.txt +1 -0
- package/.claude/hooks/tests/fixtures/output_scan/scenarios.jsonl +45 -0
- package/.claude/hooks/tests/fixtures/sample_payload_clean.json +13 -0
- package/.claude/hooks/tests/fixtures/sample_payload_with_secrets.json +12 -0
- package/.claude/hooks/tests/mutations/README.md +86 -0
- package/.claude/hooks/tests/mutations/__init__.py +14 -0
- package/.claude/hooks/tests/mutations/engine_mutations/__init__.py +15 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_01_parser_accepts_anchor.py +51 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_02_parser_skip_depth_limit.py +38 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_03_parser_accept_multi_doc.py +47 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_04_parser_accepts_bom.py +41 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_05_parser_scalar_len_off_by_one.py +61 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_06_parser_accepts_python_tag.py +50 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_07_parser_accepts_tab_indent.py +56 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_08_compiler_skip_regex_compile.py +45 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_09_compiler_regex_pattern_cap_off.py +31 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_10_compiler_accept_unknown_form.py +42 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_11_compiler_missing_predicate_tolerated.py +79 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_12_compiler_duplicate_rule_id_tolerated.py +66 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_13_compiler_missing_top_level_key_tolerated.py +46 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_14_compiler_schema_version_passthrough.py +43 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_15_evaluator_any_empty_returns_true.py +41 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_16_evaluator_all_empty_returns_true.py +37 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_17_evaluator_not_passthrough.py +37 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_18_evaluator_eq_true_on_type_mismatch.py +51 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_19_evaluator_regex_match_only.py +43 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_20_evaluator_path_under_no_realpath.py +48 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_21_evaluator_in_accepts_any.py +37 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_22_evaluator_length_off_by_one.py +45 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_23_evaluator_first_match_becomes_last.py +66 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_24_error_model_wrong_kind_on_parse.py +39 -0
- package/.claude/hooks/tests/mutations/engine_mutations/mutation_25_error_model_fail_open_on_load.py +42 -0
- package/.claude/hooks/tests/mutations/policy_mutations/__init__.py +16 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_01_remove_credential_leak.py +49 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_02_remove_rm_rf.py +44 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_03_remove_git_reset_hard.py +44 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_04_remove_git_push_force.py +44 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_05_reorder_rules.py +59 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_06_change_reason_enum.py +54 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_07_default_flipped_to_block.py +56 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_bash_08_flip_rm_rf_to_allow.py +49 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_01_remove_illegal_transition.py +79 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_02_remove_illegal_status.py +80 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_03_remove_missing_reviewed_at.py +80 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_04_remove_missing_completed_at.py +80 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_05_remove_missing_related_commits.py +79 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_06_remove_missing_abandonment_reason.py +80 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_07_scope_guard_inverted.py +93 -0
- package/.claude/hooks/tests/mutations/policy_mutations/mutation_plan_08_default_block.py +90 -0
- package/.claude/hooks/tests/probes/test_architect_probe.py +286 -0
- package/.claude/hooks/tests/probes/test_canonical_edit_probe.py +190 -0
- package/.claude/hooks/tests/probes/test_skill_content_probe.py +219 -0
- package/.claude/hooks/tests/test_SessionEnd.py +59 -0
- package/.claude/hooks/tests/test_SessionStart.py +42 -0
- package/.claude/hooks/tests/test_UserPromptSubmit.py +47 -0
- package/.claude/hooks/tests/test_accel_dispatch.py +96 -0
- package/.claude/hooks/tests/test_action_required_invariants.py +274 -0
- package/.claude/hooks/tests/test_adapter_drift_detector.py +254 -0
- package/.claude/hooks/tests/test_adapter_golden.py +198 -0
- package/.claude/hooks/tests/test_adequacy_gate.py +86 -0
- package/.claude/hooks/tests/test_adr_052_role_to_model_coverage.py +112 -0
- package/.claude/hooks/tests/test_adr_058_brainstorm_structure.py +280 -0
- package/.claude/hooks/tests/test_adversary_rules_live.py +400 -0
- package/.claude/hooks/tests/test_agent_frontmatter.py +377 -0
- package/.claude/hooks/tests/test_anti_ceo_overhead.py +591 -0
- package/.claude/hooks/tests/test_audit_emit.py +1707 -0
- package/.claude/hooks/tests/test_audit_emit_api_contract.py +693 -0
- package/.claude/hooks/tests/test_audit_emit_async_flush.py +563 -0
- package/.claude/hooks/tests/test_audit_emit_backpressure.py +138 -0
- package/.claude/hooks/tests/test_audit_emit_callsite_coverage_matrix.py +101 -0
- package/.claude/hooks/tests/test_audit_emit_chain_length.py +357 -0
- package/.claude/hooks/tests/test_audit_emit_coverage.py +2679 -0
- package/.claude/hooks/tests/test_audit_emit_ghost_action_guard.py +447 -0
- package/.claude/hooks/tests/test_audit_emit_plan088_canonical13.py +323 -0
- package/.claude/hooks/tests/test_audit_emit_rotation.py +218 -0
- package/.claude/hooks/tests/test_audit_emit_veto_v214.py +202 -0
- package/.claude/hooks/tests/test_audit_emit_wire_audit.py +699 -0
- package/.claude/hooks/tests/test_audit_hmac.py +334 -0
- package/.claude/hooks/tests/test_audit_hmac_branch_coverage.py +212 -0
- package/.claude/hooks/tests/test_audit_hmac_chain_monotonicity_property.py +136 -0
- package/.claude/hooks/tests/test_audit_hmac_coverage_v214.py +358 -0
- package/.claude/hooks/tests/test_audit_hmac_hardening.py +302 -0
- package/.claude/hooks/tests/test_audit_hmac_rotation_scenarios.py +231 -0
- package/.claude/hooks/tests/test_audit_hmac_verify_chain.py +443 -0
- package/.claude/hooks/tests/test_audit_log.py +280 -0
- package/.claude/hooks/tests/test_audit_log_coverage.py +173 -0
- package/.claude/hooks/tests/test_audit_log_path_d.py +516 -0
- package/.claude/hooks/tests/test_audit_log_phase1.py +358 -0
- package/.claude/hooks/tests/test_audit_log_schema_consistency.py +97 -0
- package/.claude/hooks/tests/test_audit_log_security.py +289 -0
- package/.claude/hooks/tests/test_audit_log_tokens.py +92 -0
- package/.claude/hooks/tests/test_audit_log_v2_7.py +378 -0
- package/.claude/hooks/tests/test_audit_log_v2_8_model.py +201 -0
- package/.claude/hooks/tests/test_audit_rotation.py +158 -0
- package/.claude/hooks/tests/test_audit_stream_verbose_protection.py +86 -0
- package/.claude/hooks/tests/test_audit_tokens_content_ban.py +512 -0
- package/.claude/hooks/tests/test_auto_boot.py +28 -0
- package/.claude/hooks/tests/test_available_models_mirror.py +226 -0
- package/.claude/hooks/tests/test_bash_canonical_forensic.py +74 -0
- package/.claude/hooks/tests/test_bash_canonical_interceptor.py +79 -0
- package/.claude/hooks/tests/test_brotli_passthrough.py +145 -0
- package/.claude/hooks/tests/test_byte_identity_fuzzer.py +185 -0
- package/.claude/hooks/tests/test_byte_identity_harness.py +953 -0
- package/.claude/hooks/tests/test_canonical_guard_typed_exceptions.py +117 -0
- package/.claude/hooks/tests/test_canonical_json.py +153 -0
- package/.claude/hooks/tests/test_chain_invariants_property.py +132 -0
- package/.claude/hooks/tests/test_check_adversary_live.py +149 -0
- package/.claude/hooks/tests/test_check_agent_spawn.py +1084 -0
- package/.claude/hooks/tests/test_check_agent_spawn_coverage.py +277 -0
- package/.claude/hooks/tests/test_check_agent_spawn_effort_token.py +74 -0
- package/.claude/hooks/tests/test_check_agent_spawn_import_isolation.py +82 -0
- package/.claude/hooks/tests/test_check_agent_spawn_model_routing_mode.py +245 -0
- package/.claude/hooks/tests/test_check_agent_spawn_reference_bypass.py +385 -0
- package/.claude/hooks/tests/test_check_agent_spawn_routing_promotion.py +302 -0
- package/.claude/hooks/tests/test_check_agent_spawn_skill_reference.py +336 -0
- package/.claude/hooks/tests/test_check_arbitration_kernel.py +472 -0
- package/.claude/hooks/tests/test_check_arbitration_kernel_v214.py +157 -0
- package/.claude/hooks/tests/test_check_bash_safety.py +546 -0
- package/.claude/hooks/tests/test_check_bash_safety_canonical_matrix.py +336 -0
- package/.claude/hooks/tests/test_check_bash_safety_cp_chaining.py +120 -0
- package/.claude/hooks/tests/test_check_bash_safety_h5_rewrite.py +462 -0
- package/.claude/hooks/tests/test_check_budget.py +580 -0
- package/.claude/hooks/tests/test_check_budget_max_tokens.py +397 -0
- package/.claude/hooks/tests/test_check_budget_quota_hint.py +115 -0
- package/.claude/hooks/tests/test_check_canonical_edit.py +302 -0
- package/.claude/hooks/tests/test_check_canonical_edit_coverage.py +370 -0
- package/.claude/hooks/tests/test_check_canonical_edit_kernel_v2.py +401 -0
- package/.claude/hooks/tests/test_check_canonical_edit_markers.py +473 -0
- package/.claude/hooks/tests/test_check_canonical_edit_mcp.py +401 -0
- package/.claude/hooks/tests/test_check_canonical_edit_session67_format.py +245 -0
- package/.claude/hooks/tests/test_check_codex_filewrite.py +964 -0
- package/.claude/hooks/tests/test_check_codex_response.py +419 -0
- package/.claude/hooks/tests/test_check_compaction_continuity.py +450 -0
- package/.claude/hooks/tests/test_check_confidence_gate.py +326 -0
- package/.claude/hooks/tests/test_check_config_change.py +369 -0
- package/.claude/hooks/tests/test_check_config_protection.py +364 -0
- package/.claude/hooks/tests/test_check_fluency_nudge.py +321 -0
- package/.claude/hooks/tests/test_check_mcp_response.py +261 -0
- package/.claude/hooks/tests/test_check_output_safety.py +314 -0
- package/.claude/hooks/tests/test_check_output_secrets.py +488 -0
- package/.claude/hooks/tests/test_check_output_secrets_coverage.py +321 -0
- package/.claude/hooks/tests/test_check_pair_rail.py +897 -0
- package/.claude/hooks/tests/test_check_pair_rail_decide_canonical.py +297 -0
- package/.claude/hooks/tests/test_check_pair_rail_golden.py +362 -0
- package/.claude/hooks/tests/test_check_pair_rail_hook_integration.py +120 -0
- package/.claude/hooks/tests/test_check_pair_rail_matrix.py +1077 -0
- package/.claude/hooks/tests/test_check_plan_edit.py +679 -0
- package/.claude/hooks/tests/test_check_plan_edit_stranded.py +310 -0
- package/.claude/hooks/tests/test_check_protocol_semver_cascade.py +141 -0
- package/.claude/hooks/tests/test_check_protocol_semver_cascade_settings_wired.py +297 -0
- package/.claude/hooks/tests/test_check_protocol_semver_cascade_synccascade.py +365 -0
- package/.claude/hooks/tests/test_check_read_injection.py +143 -0
- package/.claude/hooks/tests/test_check_read_injection_coverage.py +237 -0
- package/.claude/hooks/tests/test_check_read_injection_pathbound.py +153 -0
- package/.claude/hooks/tests/test_check_scratchpad_access.py +244 -0
- package/.claude/hooks/tests/test_check_skill_bootstrap_post.py +256 -0
- package/.claude/hooks/tests/test_check_skill_patch_sentinel.py +439 -0
- package/.claude/hooks/tests/test_check_skill_reference_read.py +170 -0
- package/.claude/hooks/tests/test_check_skill_reference_read_v2.py +388 -0
- package/.claude/hooks/tests/test_check_subagent_fabrication.py +54 -0
- package/.claude/hooks/tests/test_check_subagent_start.py +505 -0
- package/.claude/hooks/tests/test_check_tier_policy.py +48 -0
- package/.claude/hooks/tests/test_check_tier_policy_misrouting_24h.py +294 -0
- package/.claude/hooks/tests/test_check_webfetch_injection.py +49 -0
- package/.claude/hooks/tests/test_claim_producer_pair_end_to_end_loop_perf.py +227 -0
- package/.claude/hooks/tests/test_claude_adapter_thinking.py +731 -0
- package/.claude/hooks/tests/test_claude_batch_adapter.py +672 -0
- package/.claude/hooks/tests/test_closeout_guard.py +184 -0
- package/.claude/hooks/tests/test_codex_adapter.py +777 -0
- package/.claude/hooks/tests/test_codex_cli_shape.py +217 -0
- package/.claude/hooks/tests/test_codex_egress_proof_telemetry.py +214 -0
- package/.claude/hooks/tests/test_codex_egress_redact.py +342 -0
- package/.claude/hooks/tests/test_codex_egress_redact_outgoing.py +236 -0
- package/.claude/hooks/tests/test_codex_reply_multi_turn.py +72 -0
- package/.claude/hooks/tests/test_codex_review_user_code.py +44 -0
- package/.claude/hooks/tests/test_codex_strict_json.py +123 -0
- package/.claude/hooks/tests/test_confidence_gate_producer_pair.py +522 -0
- package/.claude/hooks/tests/test_confidence_labels.py +362 -0
- package/.claude/hooks/tests/test_contract.py +237 -0
- package/.claude/hooks/tests/test_cookbook_advisor_hook.py +208 -0
- package/.claude/hooks/tests/test_credentials.py +195 -0
- package/.claude/hooks/tests/test_detect_repo_profile_branches.py +116 -0
- package/.claude/hooks/tests/test_e2e_hook_chain.py +184 -0
- package/.claude/hooks/tests/test_effective_config.py +648 -0
- package/.claude/hooks/tests/test_emit_architect_outcome.py +175 -0
- package/.claude/hooks/tests/test_env_persist_allowlist.py +365 -0
- package/.claude/hooks/tests/test_escalation_signals.py +357 -0
- package/.claude/hooks/tests/test_estimation_bayesian_pipeline.py +140 -0
- package/.claude/hooks/tests/test_execution_context_deferral.py +222 -0
- package/.claude/hooks/tests/test_fail_open_contract.py +118 -0
- package/.claude/hooks/tests/test_file_walker.py +332 -0
- package/.claude/hooks/tests/test_filelock.py +131 -0
- package/.claude/hooks/tests/test_filelock_contract.py +172 -0
- package/.claude/hooks/tests/test_find_sentinels_pattern_matrix.py +114 -0
- package/.claude/hooks/tests/test_flip_closures.py +219 -0
- package/.claude/hooks/tests/test_frontmatter.py +139 -0
- package/.claude/hooks/tests/test_git_bypass_guard.py +1095 -0
- package/.claude/hooks/tests/test_gpg_verify.py +578 -0
- package/.claude/hooks/tests/test_hook_byte_fidelity.py +113 -0
- package/.claude/hooks/tests/test_hook_latency.py +245 -0
- package/.claude/hooks/tests/test_hook_latency_import.py +178 -0
- package/.claude/hooks/tests/test_injection_patterns.py +276 -0
- package/.claude/hooks/tests/test_injection_patterns_bypass.py +276 -0
- package/.claude/hooks/tests/test_injection_salt.py +191 -0
- package/.claude/hooks/tests/test_kernel_subsumes_security_critical_lib.py +88 -0
- package/.claude/hooks/tests/test_kill_switch_godmode_enforcing.py +101 -0
- package/.claude/hooks/tests/test_latency_report.py +28 -0
- package/.claude/hooks/tests/test_lib_canonical_import.py +355 -0
- package/.claude/hooks/tests/test_lifecycle_edge_cases.py +565 -0
- package/.claude/hooks/tests/test_live_adapters.py +463 -0
- package/.claude/hooks/tests/test_live_audit_isolation.py +357 -0
- package/.claude/hooks/tests/test_mcp_bearer_friction_buffer.py +276 -0
- package/.claude/hooks/tests/test_mcp_bearer_friction_emit.py +117 -0
- package/.claude/hooks/tests/test_mcp_canonical_guard.py +1989 -0
- package/.claude/hooks/tests/test_mcp_injection_repro_harness.py +437 -0
- package/.claude/hooks/tests/test_mcp_injection_scan.py +228 -0
- package/.claude/hooks/tests/test_mcp_routing_resolve.py +246 -0
- package/.claude/hooks/tests/test_memory_shared.py +412 -0
- package/.claude/hooks/tests/test_metrics.py +115 -0
- package/.claude/hooks/tests/test_migrated_hooks_fixtures.py +121 -0
- package/.claude/hooks/tests/test_model_routing.py +175 -0
- package/.claude/hooks/tests/test_model_routing_resolve.py +97 -0
- package/.claude/hooks/tests/test_model_routing_resolve_full.py +318 -0
- package/.claude/hooks/tests/test_otel_bounded_exporter.py +521 -0
- package/.claude/hooks/tests/test_otel_emit.py +243 -0
- package/.claude/hooks/tests/test_otel_queue.py +334 -0
- package/.claude/hooks/tests/test_otel_wire_defaultoff.py +392 -0
- package/.claude/hooks/tests/test_output_scan.py +1119 -0
- package/.claude/hooks/tests/test_output_scan_dedup.py +329 -0
- package/.claude/hooks/tests/test_output_scan_fixtures.py +136 -0
- package/.claude/hooks/tests/test_pair_rail_decide.py +141 -0
- package/.claude/hooks/tests/test_payload.py +89 -0
- package/.claude/hooks/tests/test_persona_coverage_wire.py +376 -0
- package/.claude/hooks/tests/test_persona_routing_enforcing.py +119 -0
- package/.claude/hooks/tests/test_phase_c_advisory_audit.py +75 -0
- package/.claude/hooks/tests/test_pii_patterns.py +558 -0
- package/.claude/hooks/tests/test_plan114_wires.py +468 -0
- package/.claude/hooks/tests/test_plan128_emit_wiring.py +74 -0
- package/.claude/hooks/tests/test_plan132_codex_review_observe.py +99 -0
- package/.claude/hooks/tests/test_plan133_a1_env_guard.py +221 -0
- package/.claude/hooks/tests/test_plan133_a2_canonical_skill_unicode.py +359 -0
- package/.claude/hooks/tests/test_plan133_a2_invisible_unicode.py +239 -0
- package/.claude/hooks/tests/test_plan133_a3_egress_taxonomy.py +221 -0
- package/.claude/hooks/tests/test_plan133_e1_adversary.py +360 -0
- package/.claude/hooks/tests/test_plan_085_wave_c_callsites_preserved.py +147 -0
- package/.claude/hooks/tests/test_plan_091_expected_callsites.py +206 -0
- package/.claude/hooks/tests/test_plan_frontmatter.py +217 -0
- package/.claude/hooks/tests/test_policy_coverage_residual_session73.py +597 -0
- package/.claude/hooks/tests/test_policy_coverage_v214.py +1099 -0
- package/.claude/hooks/tests/test_policy_dispatch.py +454 -0
- package/.claude/hooks/tests/test_policy_engine.py +791 -0
- package/.claude/hooks/tests/test_policy_fuzz_bomb.py +356 -0
- package/.claude/hooks/tests/test_policy_golden_error_kinds.py +287 -0
- package/.claude/hooks/tests/test_policy_mutations.py +359 -0
- package/.claude/hooks/tests/test_policy_preprocessors.py +514 -0
- package/.claude/hooks/tests/test_policy_redos_guards.py +393 -0
- package/.claude/hooks/tests/test_rag_bridge.py +675 -0
- package/.claude/hooks/tests/test_rag_events.py +202 -0
- package/.claude/hooks/tests/test_red_team_fixtures.py +427 -0
- package/.claude/hooks/tests/test_redact.py +506 -0
- package/.claude/hooks/tests/test_redact_redos.py +254 -0
- package/.claude/hooks/tests/test_redact_secrets_parity.py +334 -0
- package/.claude/hooks/tests/test_replay_determinism.py +263 -0
- package/.claude/hooks/tests/test_review_loop.py +28 -0
- package/.claude/hooks/tests/test_review_loop_wiring.py +206 -0
- package/.claude/hooks/tests/test_route.py +36 -0
- package/.claude/hooks/tests/test_rubric_catalogue.py +359 -0
- package/.claude/hooks/tests/test_scratchpad_lib.py +259 -0
- package/.claude/hooks/tests/test_secret_patterns.py +680 -0
- package/.claude/hooks/tests/test_secret_patterns_provenance.py +82 -0
- package/.claude/hooks/tests/test_sentinel_session_cache.py +324 -0
- package/.claude/hooks/tests/test_sentinel_session_cache_tier1.py +205 -0
- package/.claude/hooks/tests/test_sentinel_signers.py +641 -0
- package/.claude/hooks/tests/test_session_75_kernel_findings.py +180 -0
- package/.claude/hooks/tests/test_session_76_audit_v3_findings.py +493 -0
- package/.claude/hooks/tests/test_session_77_audit_v3_backlog_findings.py +644 -0
- package/.claude/hooks/tests/test_session_77_round_2_findings.py +135 -0
- package/.claude/hooks/tests/test_session_77_round_3_findings.py +159 -0
- package/.claude/hooks/tests/test_session_77_round_4_findings.py +120 -0
- package/.claude/hooks/tests/test_session_end.py +113 -0
- package/.claude/hooks/tests/test_session_start.py +293 -0
- package/.claude/hooks/tests/test_skill_unknown_ratio_path_d.py +249 -0
- package/.claude/hooks/tests/test_smart_loading_resolver_caching.py +140 -0
- package/.claude/hooks/tests/test_spec_context_sanitizer.py +179 -0
- package/.claude/hooks/tests/test_spool_drain_contended_skip.py +249 -0
- package/.claude/hooks/tests/test_spool_drain_rotation_property_b.py +227 -0
- package/.claude/hooks/tests/test_spool_drain_rotation_race.py +395 -0
- package/.claude/hooks/tests/test_spool_writer_cache.py +463 -0
- package/.claude/hooks/tests/test_state_store.py +302 -0
- package/.claude/hooks/tests/test_stop.py +133 -0
- package/.claude/hooks/tests/test_streaming_rate_cap.py +108 -0
- package/.claude/hooks/tests/test_subagent_dispatch.py +248 -0
- package/.claude/hooks/tests/test_subagent_model_override_removed.py +108 -0
- package/.claude/hooks/tests/test_team.py +95 -0
- package/.claude/hooks/tests/test_template_dogfood_parity.py +106 -0
- package/.claude/hooks/tests/test_terminal_compress.py +135 -0
- package/.claude/hooks/tests/test_test_env_context_agent_binding.py +140 -0
- package/.claude/hooks/tests/test_testing_helper.py +53 -0
- package/.claude/hooks/tests/test_thinking_budget_command.py +229 -0
- package/.claude/hooks/tests/test_tier_policy_agent_frontmatter.py +421 -0
- package/.claude/hooks/tests/test_tier_policy_agent_frontmatter_disposition.py +175 -0
- package/.claude/hooks/tests/test_tier_policy_constants.py +336 -0
- package/.claude/hooks/tests/test_tier_policy_loader.py +544 -0
- package/.claude/hooks/tests/test_tier_policy_loader_fallback_observed.py +169 -0
- package/.claude/hooks/tests/test_tier_policy_types.py +270 -0
- package/.claude/hooks/tests/test_tokens_lib.py +118 -0
- package/.claude/hooks/tests/test_tool_lifecycle.py +598 -0
- package/.claude/hooks/tests/test_tool_lifecycle_perf.py +110 -0
- package/.claude/hooks/tests/test_turbo_profile.py +28 -0
- package/.claude/hooks/tests/test_turbo_sessionstart.py +79 -0
- package/.claude/hooks/tests/test_two_writer_chain.py +175 -0
- package/.claude/hooks/tests/test_upgrade_retry.py +346 -0
- package/.claude/hooks/tests/test_user_prompt_submit.py +254 -0
- package/.claude/hooks/tests/test_user_prompt_submit_salt.py +204 -0
- package/.claude/hooks/tests/test_verify_after_edit.py +100 -0
- package/.claude/hooks/tests/test_veto_floor_bijection.py +174 -0
- package/.claude/hooks/tests/test_w5_cookbook_remediation.py +712 -0
- package/.claude/hooks/tests/test_w5_scrub_enforcement.py +371 -0
- package/.claude/hooks/tests/test_webfetch_injection.py +280 -0
- package/.claude/hooks/tests/test_wiredeadmod_estimation_wiring.py +283 -0
- package/.claude/hooks/tests/test_wiredeadmod_spawn_wiring.py +303 -0
- package/.claude/hooks/tests/test_worktree_writer.py +509 -0
- package/.claude/hooks/turbo_profile.py +554 -0
- package/.claude/hooks/turbo_sessionstart.py +472 -0
- package/.claude/hooks/verify_after_edit.py +281 -0
- package/.claude/pitfalls-catalog.yaml +150 -0
- package/.claude/plans/AUDIT-LOG-SCHEMA.md +548 -0
- package/.claude/plans/DEBATE-SCHEMA.md +539 -0
- package/.claude/plans/PLAN-128/AB-PROTOCOL.md +121 -0
- package/.claude/plans/PLAN-128/measure-state.sh +101 -0
- package/.claude/plans/PLAN-139-canonical-invariants-and-debt-ledger.md +253 -0
- package/.claude/plans/PLAN-140/architect/round-1/approved.md +40 -0
- package/.claude/plans/PLAN-140-compaction-hook-origin-dropfix.md +95 -0
- package/.claude/plans/PLAN-141/architect/round-1/approved.md +28 -0
- package/.claude/plans/PLAN-141-mcp-smoke-staging-ruff-tolerance.md +72 -0
- package/.claude/plans/PLAN-142/architect/round-1/anonymization-map.md +11 -0
- package/.claude/plans/PLAN-142/architect/round-1/consensus.md +95 -0
- package/.claude/plans/PLAN-142/architect/round-1/devops-engineer.md +57 -0
- package/.claude/plans/PLAN-142/architect/round-1/proposal.md +57 -0
- package/.claude/plans/PLAN-142/architect/round-1/security-engineer.md +55 -0
- package/.claude/plans/PLAN-142/architect/round-1/vp-engineering.md +58 -0
- package/.claude/plans/PLAN-142/architect/round-2/anonymization-map.md +11 -0
- package/.claude/plans/PLAN-142/architect/round-2/approved.md +65 -0
- package/.claude/plans/PLAN-142/architect/round-2/consensus.md +78 -0
- package/.claude/plans/PLAN-142/architect/round-2/devops-engineer.md +58 -0
- package/.claude/plans/PLAN-142/architect/round-2/security-engineer.md +56 -0
- package/.claude/plans/PLAN-142/architect/round-2/vp-engineering.md +54 -0
- package/.claude/plans/PLAN-142/staging/EXECUTION-RUNBOOK.md +74 -0
- package/.claude/plans/PLAN-142/staging/STAGING-NOTES.md +63 -0
- package/.claude/plans/PLAN-142/staging/check_pair_rail__invoke_and_consume.py.txt +644 -0
- package/.claude/plans/PLAN-142/staging/codex_adapter_parsers.py.txt +677 -0
- package/.claude/plans/PLAN-142/staging/codex_cli_shape.py +433 -0
- package/.claude/plans/PLAN-142-codex-cli-0139-adapter-migration.md +224 -0
- package/.claude/plans/PLAN-143/architect/round-1/anonymization-map.md +22 -0
- package/.claude/plans/PLAN-143/architect/round-1/consensus.md +108 -0
- package/.claude/plans/PLAN-143/architect/round-1/devops-engineer.md +228 -0
- package/.claude/plans/PLAN-143/architect/round-1/proposal.md +48 -0
- package/.claude/plans/PLAN-143/architect/round-1/security-engineer.md +224 -0
- package/.claude/plans/PLAN-143/architect/round-1/vp-engineering.md +166 -0
- package/.claude/plans/PLAN-143/patches/PLAN143-item1-env-inventory.NOTE.md +106 -0
- package/.claude/plans/PLAN-143/patches/PLAN143-item2-spool-writer-rotate-guard.patch +41 -0
- package/.claude/plans/PLAN-143/patches/PLAN143-item3-audit-emit-exit-code.patch +32 -0
- package/.claude/plans/PLAN-143-repo-hygiene-debt.md +201 -0
- package/.claude/plans/PLAN-SCHEMA.md +870 -0
- package/.claude/plans/README.md +208 -0
- package/.claude/plans/examples/debate-round-1/consensus.md +166 -0
- package/.claude/plans/examples/debate-round-1/devops-engineer.md +133 -0
- package/.claude/plans/examples/debate-round-1/proposal.md +66 -0
- package/.claude/plans/examples/debate-round-1/security-engineer.md +109 -0
- package/.claude/plans/examples/debate-round-1/vp-engineering.md +110 -0
- package/.claude/policies/.drift-manifest.json +16 -0
- package/.claude/policies/bash-safety.policy.yaml +37 -0
- package/.claude/policies/fixtures/.gitkeep +0 -0
- package/.claude/policies/fixtures/bash-safety.fixtures.jsonl +46 -0
- package/.claude/policies/fixtures/plan-edit.fixtures.jsonl +36 -0
- package/.claude/policies/grandfather-cap.policy.yaml +85 -0
- package/.claude/policies/plan-edit.policy.yaml +152 -0
- package/.claude/policies/rubric-violation-catalogue.yaml +187 -0
- package/.claude/policies/schemas/repo-profile-skill-binding.schema.json +126 -0
- package/.claude/policies/schemas/repo-profile.schema.json +83 -0
- package/.claude/policies/schemas/squad-bundle-frontmatter.schema.json +152 -0
- package/.claude/policies/secret-patterns-exchange.yaml +368 -0
- package/.claude/policies/smart-loading-cap-table.yaml +34 -0
- package/.claude/proposals/.gitkeep +0 -0
- package/.claude/proposals/README.md +42 -0
- package/.claude/proposals/SP-001-code-review-checklist-2026-04-20.md +65 -0
- package/.claude/proposals/SP-001-code-review-checklist-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-002-security-and-auth-2026-04-20.md +74 -0
- package/.claude/proposals/SP-002-security-and-auth-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-003-design-system-and-components-2026-04-20.md +67 -0
- package/.claude/proposals/SP-003-design-system-and-components-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-004-accessibility-and-wcag-2026-04-20.md +68 -0
- package/.claude/proposals/SP-004-accessibility-and-wcag-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-005-ux-and-user-journeys-2026-04-20.md +63 -0
- package/.claude/proposals/SP-005-ux-and-user-journeys-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-006-chaos-and-resilience-2026-04-20.md +79 -0
- package/.claude/proposals/SP-006-chaos-and-resilience-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-007-ai-llm-orchestration-2026-04-20.md +76 -0
- package/.claude/proposals/SP-007-ai-llm-orchestration-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-008-performance-engineering-2026-04-20.md +82 -0
- package/.claude/proposals/SP-008-performance-engineering-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-009-code-review-checklist-2026-04-20.md +76 -0
- package/.claude/proposals/SP-009-code-review-checklist-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-010-accessibility-and-wcag-adopter-note-2026-04-20.md +77 -0
- package/.claude/proposals/SP-010-accessibility-and-wcag-adopter-note-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-011-design-system-and-components-adopter-note-2026-04-20.md +79 -0
- package/.claude/proposals/SP-011-design-system-and-components-adopter-note-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-012-ux-and-user-journeys-adopter-note-2026-04-20.md +83 -0
- package/.claude/proposals/SP-012-ux-and-user-journeys-adopter-note-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-013-frontend-performance-optimization-2026-04-20.md +82 -0
- package/.claude/proposals/SP-013-frontend-performance-optimization-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-014-observability-and-ops-2026-04-20.md +80 -0
- package/.claude/proposals/SP-014-observability-and-ops-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-015-testing-strategy-2026-04-20.md +87 -0
- package/.claude/proposals/SP-015-testing-strategy-2026-04-20.md.asc +8 -0
- package/.claude/proposals/SP-016-code-review-checklist-fluency-rubric-2026-04-28.md +111 -0
- package/.claude/proposals/SP-016-code-review-checklist-fluency-rubric-2026-04-28.md.asc +8 -0
- package/.claude/proposals/SP-017-chaos-and-resilience-adopter-note-2026-04-28.md +87 -0
- package/.claude/proposals/SP-017-chaos-and-resilience-adopter-note-2026-04-28.md.asc +8 -0
- package/.claude/proposals/SP-018-ceo-orchestration-inventory-regen-2026-04-21.md +64 -0
- package/.claude/proposals/SP-018-ceo-orchestration-inventory-regen-2026-04-21.md.asc +8 -0
- package/.claude/proposals/SP-019-terse-mode-2026-04-21.md +107 -0
- package/.claude/proposals/SP-019-terse-mode-2026-04-21.md.asc +8 -0
- package/.claude/proposals/SP-020-ceo-orchestration-audit-tokens-2026-04-21.md +74 -0
- package/.claude/proposals/SP-020-ceo-orchestration-audit-tokens-2026-04-21.md.asc +8 -0
- package/.claude/proposals/SP-021-ceo-orchestration-autonomous-loop-2026-04-21.md +71 -0
- package/.claude/proposals/SP-021-ceo-orchestration-autonomous-loop-2026-04-21.md.asc +8 -0
- package/.claude/rag/_index_core.py +344 -0
- package/.claude/rag/indexignore +101 -0
- package/.claude/rag/install-sidecar.sh +275 -0
- package/.claude/rag/models.manifest.json +19 -0
- package/.claude/rag/requirements.lock +40 -0
- package/.claude/rag/sidecar-config.template.json +53 -0
- package/.claude/rag/tests/test_index_core.py +262 -0
- package/.claude/rag/tests/test_install_sidecar.sh +132 -0
- package/.claude/scripts/.known_actions_floor.lock +0 -0
- package/.claude/scripts/admin-invite.py +199 -0
- package/.claude/scripts/adopter-metrics.py +712 -0
- package/.claude/scripts/aek-calibration-c2.py +253 -0
- package/.claude/scripts/aek-calibration-c3.py +382 -0
- package/.claude/scripts/aggregate-changesets.py +350 -0
- package/.claude/scripts/architect-bundle-validate.py +227 -0
- package/.claude/scripts/audit-dashboard.py +1320 -0
- package/.claude/scripts/audit-log-labels.jsonl +0 -0
- package/.claude/scripts/audit-log-retain.py +404 -0
- package/.claude/scripts/audit-query.py +3333 -0
- package/.claude/scripts/audit-telemetry.py +337 -0
- package/.claude/scripts/audit-tokens.py +502 -0
- package/.claude/scripts/audit-verify-chain.py +537 -0
- package/.claude/scripts/backup-audit.py +247 -0
- package/.claude/scripts/benchmark/plan-071-import-floor/README.md +194 -0
- package/.claude/scripts/benchmark/plan-071-import-floor/fixtures/baseline.json +1 -0
- package/.claude/scripts/benchmark/plan-071-import-floor/fixtures/expected_quantiles.json +11 -0
- package/.claude/scripts/benchmark/plan-071-import-floor/import_floor_bench.py +791 -0
- package/.claude/scripts/benchmark/plan-071-import-floor/run_bench.sh +180 -0
- package/.claude/scripts/benchmark-fallback-scorer.py +254 -0
- package/.claude/scripts/benchmark-judge.py +621 -0
- package/.claude/scripts/budget-summary.py +946 -0
- package/.claude/scripts/build-canonical-models.py +645 -0
- package/.claude/scripts/calibration-kappa.py +262 -0
- package/.claude/scripts/cc-analytics-pull.py +393 -0
- package/.claude/scripts/ceo-backup.sh +307 -0
- package/.claude/scripts/ceo-boot.py +3017 -0
- package/.claude/scripts/ceo-cost.py +1116 -0
- package/.claude/scripts/ceo-diagnose.py +486 -0
- package/.claude/scripts/ceo-escalation-detector.py +743 -0
- package/.claude/scripts/ceo-health.py +584 -0
- package/.claude/scripts/ceo-info.py +1001 -0
- package/.claude/scripts/ceo-restore.sh +215 -0
- package/.claude/scripts/chaos-inject.py +439 -0
- package/.claude/scripts/check-action-sha-drift.py +275 -0
- package/.claude/scripts/check-active-hooks-executable.py +119 -0
- package/.claude/scripts/check-adr-chain.py +617 -0
- package/.claude/scripts/check-audit-action-name-convention.py +221 -0
- package/.claude/scripts/check-audit-hmac-null.py +253 -0
- package/.claude/scripts/check-audit-read-api-stable.py +239 -0
- package/.claude/scripts/check-audit-registry-coverage.py +999 -0
- package/.claude/scripts/check-auto-activation-flags.py +180 -0
- package/.claude/scripts/check-canonical-doc-freshness.py +222 -0
- package/.claude/scripts/check-claude-md-claims.py +346 -0
- package/.claude/scripts/check-confidence-gate-drift.py +295 -0
- package/.claude/scripts/check-conformance-harness-mapping.py +503 -0
- package/.claude/scripts/check-contamination.sh +25 -0
- package/.claude/scripts/check-creative-rewrite.py +596 -0
- package/.claude/scripts/check-debate-round-lifecycle.py +185 -0
- package/.claude/scripts/check-debt-ledger.py +305 -0
- package/.claude/scripts/check-docs-drift.py +259 -0
- package/.claude/scripts/check-docs-freshness.py +487 -0
- package/.claude/scripts/check-flip-criteria-drift.py +426 -0
- package/.claude/scripts/check-flip-release-gate-consistency.py +134 -0
- package/.claude/scripts/check-framework-updates.sh +239 -0
- package/.claude/scripts/check-function-length.py +426 -0
- package/.claude/scripts/check-model-deprecations.py +377 -0
- package/.claude/scripts/check-originator-residue.py +248 -0
- package/.claude/scripts/check-pitfall-regression.sh +153 -0
- package/.claude/scripts/check-policy-drift.py +74 -0
- package/.claude/scripts/check-roadmap-binding.py +170 -0
- package/.claude/scripts/check-rule-invariants.py +385 -0
- package/.claude/scripts/check-sdk-compat.sh +76 -0
- package/.claude/scripts/check-secret-pattern-coverage.py +175 -0
- package/.claude/scripts/check-sidecar-manifest.py +493 -0
- package/.claude/scripts/check-skill-activation-mode.py +41 -0
- package/.claude/scripts/check-skill-health.sh +179 -0
- package/.claude/scripts/check-spec-drift.py +147 -0
- package/.claude/scripts/check-staleness.py +506 -0
- package/.claude/scripts/check-stdlib-only.py +373 -0
- package/.claude/scripts/check-substrate-watch.py +285 -0
- package/.claude/scripts/check-swarm-harness-mapping.py +380 -0
- package/.claude/scripts/check-test-audit-isolation.py +622 -0
- package/.claude/scripts/check-test-env-hygiene.py +509 -0
- package/.claude/scripts/check-threat-model-freshness.py +313 -0
- package/.claude/scripts/check-tier-boundaries.py +233 -0
- package/.claude/scripts/check-tla-schema-drift.py +272 -0
- package/.claude/scripts/check_atlas_fpr.py +595 -0
- package/.claude/scripts/check_contamination.py +337 -0
- package/.claude/scripts/check_known_actions_floor.py +155 -0
- package/.claude/scripts/check_threat_model_coverage.py +214 -0
- package/.claude/scripts/check_translations_drift.py +199 -0
- package/.claude/scripts/codex_invoke.py +436 -0
- package/.claude/scripts/compare-adopters.py +549 -0
- package/.claude/scripts/confidence-gate-backfill.py +261 -0
- package/.claude/scripts/confidence_gate.py +736 -0
- package/.claude/scripts/context-budget.py +1887 -0
- package/.claude/scripts/contextual-recommender.py +815 -0
- package/.claude/scripts/cost-table.yaml +99 -0
- package/.claude/scripts/debate-converge.py +335 -0
- package/.claude/scripts/debate-emit.py +132 -0
- package/.claude/scripts/debate-orchestrate.py +972 -0
- package/.claude/scripts/detect-repo-profile.py +1280 -0
- package/.claude/scripts/detectors/__init__.py +19 -0
- package/.claude/scripts/detectors/looping.py +127 -0
- package/.claude/scripts/detectors/overpowered.py +96 -0
- package/.claude/scripts/detectors/retry_churn.py +119 -0
- package/.claude/scripts/detectors/schema.py +94 -0
- package/.claude/scripts/detectors/tests/__init__.py +0 -0
- package/.claude/scripts/detectors/tests/fixtures.py +420 -0
- package/.claude/scripts/detectors/tests/test_looping.py +124 -0
- package/.claude/scripts/detectors/tests/test_overpowered.py +114 -0
- package/.claude/scripts/detectors/tests/test_retry_churn.py +101 -0
- package/.claude/scripts/detectors/tests/test_schema.py +109 -0
- package/.claude/scripts/detectors/tests/test_tool_cascade.py +131 -0
- package/.claude/scripts/detectors/tests/test_wasteful_thinking.py +112 -0
- package/.claude/scripts/detectors/tests/test_weak_model.py +104 -0
- package/.claude/scripts/detectors/tool_cascade.py +127 -0
- package/.claude/scripts/detectors/wasteful_thinking.py +99 -0
- package/.claude/scripts/detectors/weak_model.py +92 -0
- package/.claude/scripts/env-inventory-check.py +268 -0
- package/.claude/scripts/env-inventory.json +3305 -0
- package/.claude/scripts/extract-skill.py +456 -0
- package/.claude/scripts/fan-plan-parser.py +370 -0
- package/.claude/scripts/find-orphan-sentinels.py +89 -0
- package/.claude/scripts/first-run-wizard.py +1151 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/.env.example +1 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/exchanges/binance.py +3 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/exchanges/coinbase.py +3 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/package.json +5 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/strategies/grid.py +3 -0
- package/.claude/scripts/fixtures/cloned-trading-repo/strategies/pairs.py +3 -0
- package/.claude/scripts/fixtures/missing-package-manifest/README.md +3 -0
- package/.claude/scripts/fixtures/missing-package-manifest/src/main.py +1 -0
- package/.claude/scripts/fixtures/mixed-frontend-backend/package.json +9 -0
- package/.claude/scripts/fixtures/mixed-frontend-backend/requirements.txt +2 -0
- package/.claude/scripts/fixtures/mixed-frontend-backend/src/api/handler.py +2 -0
- package/.claude/scripts/fixtures/mixed-frontend-backend/src/pages/index.tsx +1 -0
- package/.claude/scripts/fixtures/monorepo/apps/app-a/README.md +1 -0
- package/.claude/scripts/fixtures/monorepo/apps/app-b/index.ts +1 -0
- package/.claude/scripts/fixtures/monorepo/package.json +5 -0
- package/.claude/scripts/fixtures/monorepo/packages/lib-a/index.js +1 -0
- package/.claude/scripts/fixtures/monorepo/packages/lib-b/index.js +1 -0
- package/.claude/scripts/fixtures/monorepo/pnpm-workspace.yaml +3 -0
- package/.claude/scripts/fixtures/persona-coverage-expected-thresholds.yaml +20 -0
- package/.claude/scripts/flip-criteria-drift-allowlist.txt +31 -0
- package/.claude/scripts/generate-adr-index.py +339 -0
- package/.claude/scripts/generate-available-models.py +280 -0
- package/.claude/scripts/generate-dispatch.py +430 -0
- package/.claude/scripts/generate-sbom.py +287 -0
- package/.claude/scripts/generate-skill-inventory.sh +193 -0
- package/.claude/scripts/github-api-client.py +297 -0
- package/.claude/scripts/goap-planner.py +742 -0
- package/.claude/scripts/hook-profiler.py +671 -0
- package/.claude/scripts/import-skill.py +569 -0
- package/.claude/scripts/import_ui_ux_pro_max.py +137 -0
- package/.claude/scripts/inject-agent-context.sh +948 -0
- package/.claude/scripts/k-calibration.py +456 -0
- package/.claude/scripts/key-hygiene.py +511 -0
- package/.claude/scripts/lesson-restore.py +171 -0
- package/.claude/scripts/lesson_ranker.py +100 -0
- package/.claude/scripts/lessons.py +883 -0
- package/.claude/scripts/lint-skills.py +555 -0
- package/.claude/scripts/local/README.md +280 -0
- package/.claude/scripts/local/check-doc-skill-paths.sh +124 -0
- package/.claude/scripts/local/dependency-graph.py +684 -0
- package/.claude/scripts/local/estimate-calibrator.py +240 -0
- package/.claude/scripts/local/findings-pretty-print.py +78 -0
- package/.claude/scripts/local/generate-ceremony.sh +558 -0
- package/.claude/scripts/local/pair-rail-gate.sh +156 -0
- package/.claude/scripts/local/release-dry-run.py +853 -0
- package/.claude/scripts/local/tests/test_dependency_graph.py +364 -0
- package/.claude/scripts/local/tests/test_generate_ceremony.sh +144 -0
- package/.claude/scripts/local/tests/test_release_dry_run.py +743 -0
- package/.claude/scripts/local/validate-findings.py +168 -0
- package/.claude/scripts/local/validate-saved-workflows.js +69 -0
- package/.claude/scripts/local/verify-counts.sh +420 -0
- package/.claude/scripts/local/verify-scope-coverage.py +205 -0
- package/.claude/scripts/local/verify-staging-manifest.py +188 -0
- package/.claude/scripts/local/wave-readonly-monitor.py +271 -0
- package/.claude/scripts/log-friction.sh +290 -0
- package/.claude/scripts/mcp/code_nav_bridge.py +259 -0
- package/.claude/scripts/mcp-server/__init__.py +16 -0
- package/.claude/scripts/mcp-server/auth.py +333 -0
- package/.claude/scripts/mcp-server/cost.py +108 -0
- package/.claude/scripts/mcp-server/dispatch.py +853 -0
- package/.claude/scripts/mcp-server/handlers/__init__.py +16 -0
- package/.claude/scripts/mcp-server/handlers/audit_query.py +384 -0
- package/.claude/scripts/mcp-server/handlers/get_audit_log.py +163 -0
- package/.claude/scripts/mcp-server/handlers/get_cost_budget.py +130 -0
- package/.claude/scripts/mcp-server/handlers/get_debate_state.py +207 -0
- package/.claude/scripts/mcp-server/handlers/get_skill.py +199 -0
- package/.claude/scripts/mcp-server/handlers/list_agents.py +236 -0
- package/.claude/scripts/mcp-server/handlers/list_pitfalls.py +192 -0
- package/.claude/scripts/mcp-server/handlers/list_skills.py +197 -0
- package/.claude/scripts/mcp-server/handlers/plan_status.py +489 -0
- package/.claude/scripts/mcp-server/handlers/server_capabilities.py +127 -0
- package/.claude/scripts/mcp-server/handlers/spawn_agent.py +274 -0
- package/.claude/scripts/mcp-server/http_transport.py +373 -0
- package/.claude/scripts/mcp-server/rate_limit.py +345 -0
- package/.claude/scripts/mcp-server/server.py +212 -0
- package/.claude/scripts/mcp-server/start-mcp-server.sh +111 -0
- package/.claude/scripts/mcp-server/stdio_transport.py +150 -0
- package/.claude/scripts/mcp-server/tests/__init__.py +1 -0
- package/.claude/scripts/mcp-server/tests/test_auth.py +454 -0
- package/.claude/scripts/mcp-server/tests/test_cost.py +122 -0
- package/.claude/scripts/mcp-server/tests/test_dispatch.py +448 -0
- package/.claude/scripts/mcp-server/tests/test_dispatch_bearer_replay_wire.py +358 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_get_audit_log.py +107 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_get_skill.py +108 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_list_agents.py +92 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_list_pitfalls.py +103 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_list_skills.py +121 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_server_capabilities.py +128 -0
- package/.claude/scripts/mcp-server/tests/test_handlers_spawn_agent.py +275 -0
- package/.claude/scripts/mcp-server/tests/test_http_transport.py +418 -0
- package/.claude/scripts/mcp-server/tests/test_rate_limit.py +239 -0
- package/.claude/scripts/mcp-server/tests/test_server.py +125 -0
- package/.claude/scripts/mcp-server/tests/test_stdio_transport.py +196 -0
- package/.claude/scripts/mcp-soak-monitor.py +224 -0
- package/.claude/scripts/memory-prioritize.py +516 -0
- package/.claude/scripts/migrate-grandfather-to-sha256.py +384 -0
- package/.claude/scripts/model-deprecations.json +165 -0
- package/.claude/scripts/morning-ceremony.py +266 -0
- package/.claude/scripts/morning_ledger.py +446 -0
- package/.claude/scripts/mutation-floors.yaml +51 -0
- package/.claude/scripts/mutation-test.py +506 -0
- package/.claude/scripts/nightly-proposals.py +210 -0
- package/.claude/scripts/optimizer/__init__.py +46 -0
- package/.claude/scripts/optimizer/_codex_redaction.py +101 -0
- package/.claude/scripts/optimizer/_skeleton.py +137 -0
- package/.claude/scripts/optimizer/codex_phase_gate.py +257 -0
- package/.claude/scripts/optimizer/complexity_gate.py +208 -0
- package/.claude/scripts/optimizer/fanout.py +249 -0
- package/.claude/scripts/optimizer/model_choice.py +151 -0
- package/.claude/scripts/optimizer/model_normalize.py +118 -0
- package/.claude/scripts/optimizer/rag_recommender.py +110 -0
- package/.claude/scripts/optimizer/recommender.py +213 -0
- package/.claude/scripts/optimizer/tests/__init__.py +0 -0
- package/.claude/scripts/optimizer/tests/test_codex_phase_gate.py +314 -0
- package/.claude/scripts/optimizer/tests/test_codex_review_invoked_emission.py +225 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_complexity_gate.py +122 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_fanout.py +134 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_model_choice.py +124 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_model_normalize.py +155 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_rag_recommender.py +190 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_recommender.py +131 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_skeleton.py +117 -0
- package/.claude/scripts/optimizer/tests/test_optimizer_types.py +53 -0
- package/.claude/scripts/optimizer/types.py +122 -0
- package/.claude/scripts/osv_check.py +559 -0
- package/.claude/scripts/otel-export.py +329 -0
- package/.claude/scripts/otel-local-sink.py +470 -0
- package/.claude/scripts/persona_demand_resolver.py +658 -0
- package/.claude/scripts/persona_demand_scan.py +382 -0
- package/.claude/scripts/persona_waive_parser.py +127 -0
- package/.claude/scripts/pitfall-query.py +218 -0
- package/.claude/scripts/plan-tokens.py +843 -0
- package/.claude/scripts/policy-shadow-runner.py +445 -0
- package/.claude/scripts/predict-budget/predict-plan-cost.py +581 -0
- package/.claude/scripts/predict-budget/tests/test_predict_plan_cost.py +375 -0
- package/.claude/scripts/profile-opus-4-7.py +557 -0
- package/.claude/scripts/prune-lessons.py +453 -0
- package/.claude/scripts/rate-card-calibrate.py +283 -0
- package/.claude/scripts/rate-card-fixtures.json +18 -0
- package/.claude/scripts/reality-ledger.py +2175 -0
- package/.claude/scripts/red-team-corpus/.byte-identity-check.txt +86 -0
- package/.claude/scripts/red-team-corpus/README.md +132 -0
- package/.claude/scripts/red-team-corpus/external/EXT-001-prompt-inject.md +24 -0
- package/.claude/scripts/red-team-corpus/external/EXT-002-hackaprompt.md +25 -0
- package/.claude/scripts/red-team-corpus/external/EXT-003-gcg.md +31 -0
- package/.claude/scripts/red-team-corpus/external/EXT-004-tap.md +23 -0
- package/.claude/scripts/red-team-corpus/external/EXT-005-cybersecurity-eval.md +30 -0
- package/.claude/scripts/red-team-corpus/external/EXT-006-anthropic-samples.md +26 -0
- package/.claude/scripts/red-team-corpus/external/EXT-007-trojan-source.md +26 -0
- package/.claude/scripts/red-team-corpus/external/EXT-008-owasp-llm-top10.md +33 -0
- package/.claude/scripts/red-team-corpus/external/EXT-009-jailbreak-bench.md +24 -0
- package/.claude/scripts/red-team-corpus/external/EXT-010-advbench.md +22 -0
- package/.claude/scripts/red-team-corpus/external/EXT-011-mitre-atlas.md +25 -0
- package/.claude/scripts/red-team-corpus/external/EXT-012-npm-typosquat.md +23 -0
- package/.claude/scripts/red-team-corpus/external/EXT-013-log-tamper-poc.md +25 -0
- package/.claude/scripts/red-team-corpus/external/EXT-014-cwe-798-credentials.md +24 -0
- package/.claude/scripts/red-team-corpus/external/EXT-015-garak.md +28 -0
- package/.claude/scripts/red-team-corpus/external/EXT-016-skill-content-injection-via-markdown.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-017-persona-impersonation-ceo.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-018-file-assignment-wildcard-escape.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-019-veto-bypass-force-proceed.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-020-canonical-edit-circumvent-settings.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-021-spawn-without-agent-profile.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-022-hidden-unicode-in-skill-name.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-023-mcp-spawn-governance-bypass.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-024-adapter-credential-in-error-trace.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-025-sandbox-escape-nested-subshell.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-026-plan-edit-without-debate.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-027-audit-log-rotation-race.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-028-npm-dependency-confusion.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-029-output-safety-unicode-confusable.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-030-adapter-retry-storm-dos.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-031-team-md-direct-edit.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-032-sandbox-env-var-exfil.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-033-mcp-rate-limit-bypass-headers.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-034-otel-span-attribute-leak.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-035-skill-patch-polyglot-payload.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-036-output-safety-base64-triple-wrap.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-037-plan-id-cross-plan-memory-read.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-038-npm-slsa-provenance-strip.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-039-adapter-exfil-streaming-chunk.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/EXT-040-sandbox-symlink-to-secrets.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/external/README.md +63 -0
- package/.claude/scripts/red-team-corpus/flake-budget.yaml +244 -0
- package/.claude/scripts/red-team-corpus/provenance.md +74 -0
- package/.claude/scripts/red-team-corpus/regression/REG-001-s3-audit-emission-gap.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-002-audit-registry-miss.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-003-breaker-provider-kwarg-missing.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-004-canonical-edit-conftest-block.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-005-mcp-dispatch-oversized-handler.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-006-audit-registry-false-orphan.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-007-spec-count-undercount.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-008-adr-reserved-slot-phantom.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-009-tlc-pending-placeholder.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-010-mutation-kill-rate-fake.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-011-byte-identity-governance-persona.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-012-conformance-mapping-partial-path.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-013-l1-fairness-lazy-fire.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-014-mcp-path-traversal-skill.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/regression/REG-015-mcp-hmac-timestamp-skew.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-001-skill-patch-bidi-trojan.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-002-skill-patch-zero-width-smuggle.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-003-skill-patch-exec-smuggled-fence.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-004-skill-patch-oversized-diff.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-005-audit-log-byte-rewrite.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-006-audit-log-truncation.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-007-audit-log-lock-race.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-008-plan-id-env-spoof.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-009-plan-id-frontmatter-hijack.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-010-plan-id-cross-plan-read.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-011-sandbox-escape-curl-exfil.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-012-sandbox-escape-env-dump.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-013-sandbox-escape-symlink-plant.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-014-mcp-handler-governance-bypass.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-015-mcp-handler-acl-enumeration.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-016-mcp-handler-rate-limit-evasion.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-017-adapter-exfil-via-error-message.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-018-adapter-exfil-otel-attr.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-019-adapter-exfil-retry-replay.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-020-output-safety-nfkc-bypass.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-021-output-safety-base64-double-wrap.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-022-output-safety-entropy-below-threshold.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-023-output-safety-regex-obfuscation.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-024-output-safety-luhn-partial.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-025-npm-tamper-supply-chain.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-026-npm-tamper-typo-squat.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/synthetic/SYN-027-npm-tamper-unsigned-slsa.jsonl +1 -0
- package/.claude/scripts/red-team-corpus/v1/fixtures.jsonl +67 -0
- package/.claude/scripts/red-team-corpus/v1/fixtures.jsonl.sha256 +1 -0
- package/.claude/scripts/red-team-corpus/v1/labels.json +88 -0
- package/.claude/scripts/red-team-eval.py +1099 -0
- package/.claude/scripts/registry.py +438 -0
- package/.claude/scripts/replay/__init__.py +0 -0
- package/.claude/scripts/replay/replay-session.py +1232 -0
- package/.claude/scripts/replay/tests/__init__.py +0 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/api-key-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/cpf-cnpj-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/email-in-log-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/homoglyph-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/jwt-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/os-path-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-01-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-02-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-03-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-04-positive.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-05-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-06-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-07-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/fixtures/pan-08-negative.jsonl +1 -0
- package/.claude/scripts/replay/tests/test_replay_redact_lib.py +971 -0
- package/.claude/scripts/replay/tests/test_replay_session.py +396 -0
- package/.claude/scripts/replay/tests/test_replay_session_capture.py +522 -0
- package/.claude/scripts/repo-profile.schema.json +83 -0
- package/.claude/scripts/run-promotion-gate.py +631 -0
- package/.claude/scripts/run-skill-benchmark.py +1276 -0
- package/.claude/scripts/scan-injection-strict.sh +162 -0
- package/.claude/scripts/scan-injection.py +305 -0
- package/.claude/scripts/scan-upstream-injection.py +663 -0
- package/.claude/scripts/scratchpad.py +427 -0
- package/.claude/scripts/self_test.py +602 -0
- package/.claude/scripts/session-graph-build.py +728 -0
- package/.claude/scripts/session-resume.py +363 -0
- package/.claude/scripts/set-quality-profile.sh +229 -0
- package/.claude/scripts/skill-budget-generator.py +599 -0
- package/.claude/scripts/skill-import-rubric.py +368 -0
- package/.claude/scripts/skill-index-build.py +534 -0
- package/.claude/scripts/skill-patch-apply.py +1088 -0
- package/.claude/scripts/skill-patch-propose.py +690 -0
- package/.claude/scripts/skill-retrieve.py +522 -0
- package/.claude/scripts/skill_grandfather_parser.py +295 -0
- package/.claude/scripts/smart-loading-resolver.py +994 -0
- package/.claude/scripts/spot-check-findings.py +211 -0
- package/.claude/scripts/squad-export.py +437 -0
- package/.claude/scripts/squad-import.py +741 -0
- package/.claude/scripts/status.py +315 -0
- package/.claude/scripts/statusline-ceo.py +597 -0
- package/.claude/scripts/substrate-watch.json +54 -0
- package/.claude/scripts/success-receipt.py +1038 -0
- package/.claude/scripts/swarm/__init__.py +42 -0
- package/.claude/scripts/swarm/_benchmark_replay.py +259 -0
- package/.claude/scripts/swarm/_child_isolation.py +113 -0
- package/.claude/scripts/swarm/_coordinator_sim.py +293 -0
- package/.claude/scripts/swarm/_governors.py +277 -0
- package/.claude/scripts/swarm/_integration.py +547 -0
- package/.claude/scripts/swarm/_parent_death.py +176 -0
- package/.claude/scripts/swarm/_process_group.py +250 -0
- package/.claude/scripts/swarm/_replay_tournament.py +214 -0
- package/.claude/scripts/swarm/_spawn_gate.py +292 -0
- package/.claude/scripts/swarm/_subagent_fabrication.py +444 -0
- package/.claude/scripts/swarm/_worktree_pool.py +276 -0
- package/.claude/scripts/swarm/coordinator.py +543 -0
- package/.claude/scripts/swarm/file_assignment.py +111 -0
- package/.claude/scripts/swarm/fixtures/mcp_corpus.json +111 -0
- package/.claude/scripts/swarm/kill_switch.py +260 -0
- package/.claude/scripts/swarm/loop_runner.py +486 -0
- package/.claude/scripts/swarm/recovery.py +178 -0
- package/.claude/scripts/swarm/test_mcp_injection_repro.py +518 -0
- package/.claude/scripts/swarm/test_rail_anomaly_repro.py +586 -0
- package/.claude/scripts/swarm/tests/__init__.py +1 -0
- package/.claude/scripts/swarm/tests/test_benchmark_manifest_schema.py +227 -0
- package/.claude/scripts/swarm/tests/test_benchmark_replay.py +248 -0
- package/.claude/scripts/swarm/tests/test_child_isolation.py +138 -0
- package/.claude/scripts/swarm/tests/test_coordinator.py +289 -0
- package/.claude/scripts/swarm/tests/test_coordinator_production_integration.py +434 -0
- package/.claude/scripts/swarm/tests/test_coordinator_sim.py +192 -0
- package/.claude/scripts/swarm/tests/test_coordinator_tick.py +165 -0
- package/.claude/scripts/swarm/tests/test_file_assignment.py +100 -0
- package/.claude/scripts/swarm/tests/test_governors.py +269 -0
- package/.claude/scripts/swarm/tests/test_integration.py +344 -0
- package/.claude/scripts/swarm/tests/test_kill_switch.py +307 -0
- package/.claude/scripts/swarm/tests/test_loop_runner.py +168 -0
- package/.claude/scripts/swarm/tests/test_loop_runner_circuit_breaker.py +555 -0
- package/.claude/scripts/swarm/tests/test_loop_runner_gate_enforcement.py +304 -0
- package/.claude/scripts/swarm/tests/test_loop_runner_gate_kill_switch.py +147 -0
- package/.claude/scripts/swarm/tests/test_loop_runner_sentinel_revocation_slo.py +112 -0
- package/.claude/scripts/swarm/tests/test_optimizer_killswitch.py +205 -0
- package/.claude/scripts/swarm/tests/test_parent_death.py +128 -0
- package/.claude/scripts/swarm/tests/test_parent_death_integration.py +305 -0
- package/.claude/scripts/swarm/tests/test_process_group.py +132 -0
- package/.claude/scripts/swarm/tests/test_process_group_reap.py +212 -0
- package/.claude/scripts/swarm/tests/test_rail_anomaly_repro.py +516 -0
- package/.claude/scripts/swarm/tests/test_recovery.py +165 -0
- package/.claude/scripts/swarm/tests/test_replay_tournament.py +284 -0
- package/.claude/scripts/swarm/tests/test_spawn_gate.py +265 -0
- package/.claude/scripts/swarm/tests/test_subagent_fabrication.py +824 -0
- package/.claude/scripts/swarm/tests/test_swarm_activation_smoke.py +112 -0
- package/.claude/scripts/swarm/tests/test_tournament.py +195 -0
- package/.claude/scripts/swarm/tests/test_worktree_pool.py +252 -0
- package/.claude/scripts/swarm/tournament.py +261 -0
- package/.claude/scripts/task-route.py +807 -0
- package/.claude/scripts/test-env-hygiene-allowlist.yaml +1093 -0
- package/.claude/scripts/tests/DEFERRED.md +99 -0
- package/.claude/scripts/tests/conftest.py +42 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/bad-type.md +4 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/missing-frontmatter.md +1 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/multidoc.md +6 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/sample-CHANGELOG.md +29 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/second-minor.md +4 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/single-patch.md +4 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/third-major.md +4 -0
- package/.claude/scripts/tests/fixtures/aggregate-changesets/unknown-key.md +6 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/bidi_override.md +12 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/fenced_python.md +19 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/homoglyph.md +11 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/injection.md +11 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/long_line.md +9 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/oversized.md +261 -0
- package/.claude/scripts/tests/fixtures/bad_lessons/zero_width.md +11 -0
- package/.claude/scripts/tests/fixtures/budget_summary/generate_fixtures.py +368 -0
- package/.claude/scripts/tests/fixtures/claims/README.md +21 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/neg-missing.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/neg-no-file.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/pos-extract.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/pos-main.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/pos-verify.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/function_exists/quoted-colon-path.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/import_resolves/codeblock-skipped.txt +8 -0
- package/.claude/scripts/tests/fixtures/claims/import_resolves/neg-blocked-os.txt +6 -0
- package/.claude/scripts/tests/fixtures/claims/import_resolves/neg-relative.txt +5 -0
- package/.claude/scripts/tests/fixtures/claims/import_resolves/pos-dotted.txt +6 -0
- package/.claude/scripts/tests/fixtures/claims/import_resolves/pos-stdlib-like.txt +5 -0
- package/.claude/scripts/tests/fixtures/claims/line_range/neg-missing-file.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/line_range/neg-too-long.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/line_range/pos-large.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/line_range/pos-small.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/line_range/quoted-path.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/codeblock-skipped.txt +7 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-absolute-outside.txt +6 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-dotdot-escape.txt +7 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-imaginary.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-proc-self.txt +6 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-symlink-escape.txt +8 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/neg-typo.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/pos-claude.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/pos-readme.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/path_exists/pos-self.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/sha_exists/neg-fake.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/sha_exists/neg-not-sha.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/sha_exists/pos-head.txt +4 -0
- package/.claude/scripts/tests/fixtures/claims/sha_exists/pos-root.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/sha_exists/pos-short.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/neg-missing-file.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/neg-wrong-test.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/pos-audit-emit.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/pos-extra.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/pos-file.txt +1 -0
- package/.claude/scripts/tests/fixtures/claims/test_passes/quoted-pytest-selector.txt +1 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/converged-pair-1/round-1/a.md +39 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/converged-pair-1/round-1/b.md +36 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/converged-pair-1/round-2/a.md +36 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/converged-pair-1/round-2/b.md +36 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/not-converged-pair-1/round-1/a.md +35 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/not-converged-pair-1/round-1/b.md +34 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/not-converged-pair-1/round-2/a.md +35 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/not-converged-pair-1/round-2/b.md +34 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/partial-overlap/round-1/a.md +35 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/partial-overlap/round-2/a.md +36 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/with-secret/round-1/a.md +36 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/with-secret/round-1/b.md +33 -0
- package/.claude/scripts/tests/fixtures/debate_convergence/with-secret/round-2/a.md +34 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_anchor_only.md +10 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_broken.md +5 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_external_url.md +9 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_in_fenced_code.md +18 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_in_frontmatter.md +10 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_in_html_comment.md +10 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_in_inline_code.md +7 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_in_table.md +6 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_relative_parent.md +7 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/link_url_encoded.md +5 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/real_target.md +3 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/sub/dir.md +3 -0
- package/.claude/scripts/tests/fixtures/docs_freshness/with%20space.md +3 -0
- package/.claude/scripts/tests/fixtures/good_lessons/clean_auth.md +11 -0
- package/.claude/scripts/tests/fixtures/good_lessons/clean_logging.md +11 -0
- package/.claude/scripts/tests/fixtures/good_lessons/clean_retry.md +11 -0
- package/.claude/scripts/tests/fixtures/gpg-keyring-fixture.py +209 -0
- package/.claude/scripts/tests/fixtures/injection/benign-01.txt +8 -0
- package/.claude/scripts/tests/fixtures/injection/benign-02.txt +5 -0
- package/.claude/scripts/tests/fixtures/injection/benign-03.txt +7 -0
- package/.claude/scripts/tests/fixtures/injection/benign-04.txt +9 -0
- package/.claude/scripts/tests/fixtures/injection/benign-05.txt +7 -0
- package/.claude/scripts/tests/fixtures/injection/benign-06.txt +7 -0
- package/.claude/scripts/tests/fixtures/injection/benign-07.txt +11 -0
- package/.claude/scripts/tests/fixtures/injection/benign-08.txt +4 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-01.txt +4 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-02.txt +2 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-03.txt +4 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-04.txt +2 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-05.txt +2 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-06.txt +5 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-07.txt +5 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-08.txt +2 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-09.txt +3 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-10.txt +2 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-11.txt +3 -0
- package/.claude/scripts/tests/fixtures/injection/malicious-12.txt +5 -0
- package/.claude/scripts/tests/fixtures/plan-tokens-calibration/manifest.json +49 -0
- package/.claude/scripts/tests/fixtures/plan-tokens-calibration/plan-051.md +36 -0
- package/.claude/scripts/tests/fixtures/plan-tokens-calibration/plan-052.md +32 -0
- package/.claude/scripts/tests/fixtures/plan-tokens-calibration/plan-058.md +31 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-1-boundary/docs/SAMPLE.md +8 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-1-negative/.claude/scripts/sample.py +12 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-1-negative/docs/SAMPLE.md +4 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-1-positive/.claude/scripts/sample.py +12 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-1-positive/docs/SAMPLE.md +9 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-2-boundary/README.md +4 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-2-negative/.claude/rag/requirements.lock +4 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-2-positive/.claude/rag/requirements.lock +2 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-3-boundary/.claude/agents/devops.md +8 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-3-negative/.claude/agents/devops.md +5 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-3-negative/audit-log.jsonl +2 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-3-positive/.claude/agents/devops.md +7 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-3-positive/audit-log.jsonl +4 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-4-boundary/.claude/adr/ADR-997-fixture-superseded.md +8 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-4-negative/.claude/adr/ADR-998-fixture-negative.md +16 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-4-positive/.claude/adr/ADR-999-fixture-positive.md +15 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-boundary/.claude/hooks/_lib/.do-not-import-from-here +15 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-boundary/.claude/hooks/_lib/audit_emit.py +8 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-boundary/.claude/scripts/dynamic_action.py +12 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-negative/.claude/hooks/_lib/.do-not-import-from-here +15 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-negative/.claude/hooks/_lib/audit_emit.py +11 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-negative/.claude/scripts/registered_emitter.py +8 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-positive/.claude/hooks/_lib/.do-not-import-from-here +15 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-positive/.claude/hooks/_lib/audit_emit.py +12 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/detector-6-positive/.claude/scripts/phantom_emitter.py +13 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/issue-body-template.md +47 -0
- package/.claude/scripts/tests/fixtures/reality-ledger/redaction/_test_corpus.py +7 -0
- package/.claude/scripts/tests/fixtures/repo_profile/cloned-trading-repo/.env.example +5 -0
- package/.claude/scripts/tests/fixtures/repo_profile/cloned-trading-repo/Cargo.toml +9 -0
- package/.claude/scripts/tests/fixtures/repo_profile/cloned-trading-repo/README.md +6 -0
- package/.claude/scripts/tests/fixtures/repo_profile/cloned-trading-repo/exchanges/binance.py +6 -0
- package/.claude/scripts/tests/fixtures/repo_profile/cloned-trading-repo/strategies/triangular.py +4 -0
- package/.claude/scripts/tests/fixtures/repo_profile/missing-package-manifest/README.md +7 -0
- package/.claude/scripts/tests/fixtures/repo_profile/missing-package-manifest/notes.md +1 -0
- package/.claude/scripts/tests/fixtures/repo_profile/mixed-frontend-backend/README.md +6 -0
- package/.claude/scripts/tests/fixtures/repo_profile/mixed-frontend-backend/api/server.js +4 -0
- package/.claude/scripts/tests/fixtures/repo_profile/mixed-frontend-backend/package.json +15 -0
- package/.claude/scripts/tests/fixtures/repo_profile/mixed-frontend-backend/pages/index.tsx +3 -0
- package/.claude/scripts/tests/fixtures/repo_profile/monorepo/README.md +6 -0
- package/.claude/scripts/tests/fixtures/repo_profile/monorepo/apps/backend/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/repo_profile/monorepo/apps/frontend/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/repo_profile/monorepo/package.json +5 -0
- package/.claude/scripts/tests/fixtures/repo_profile/monorepo/packages/shared/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/sample_audit_log.jsonl +50 -0
- package/.claude/scripts/tests/fixtures/siem/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-engine.yaml +8 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-fail-closed.yaml +7 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-fintech.yaml +9 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-frontend.yaml +9 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-generic.yaml +8 -0
- package/.claude/scripts/tests/fixtures/smart_loading/profile-trading-readonly.yaml +9 -0
- package/.claude/scripts/tests/fixtures/smart_loading/synthetic-skill-catalog.yaml +186 -0
- package/.claude/scripts/tests/fixtures/squad_marketplace/.gitkeep +4 -0
- package/.claude/scripts/tests/fixtures/task-route/calibration-holdout.json +49 -0
- package/.claude/scripts/tests/fixtures/task-route/calibration-train.json +174 -0
- package/.claude/scripts/tests/perf/__init__.py +3 -0
- package/.claude/scripts/tests/perf/perf_utils.py +134 -0
- package/.claude/scripts/tests/perf/test_kernel_hard_deny_microbench.py +149 -0
- package/.claude/scripts/tests/perf/test_optimizer_complexity_gate_p99.py +145 -0
- package/.claude/scripts/tests/perf/test_wave_c_canonical_json.py +132 -0
- package/.claude/scripts/tests/perf/test_wave_c_filelock_mkdir.py +71 -0
- package/.claude/scripts/tests/perf/test_wave_c_plan_glob_cache.py +84 -0
- package/.claude/scripts/tests/perf/test_wave_c_preview_collapse.py +98 -0
- package/.claude/scripts/tests/perf/test_wave_c_sys_modules.py +104 -0
- package/.claude/scripts/tests/test_a4_pricing_doctrine.py +127 -0
- package/.claude/scripts/tests/test_admin_invite.py +173 -0
- package/.claude/scripts/tests/test_adopter_metrics.py +723 -0
- package/.claude/scripts/tests/test_aek_calibration_c2.py +107 -0
- package/.claude/scripts/tests/test_aek_calibration_c3.py +192 -0
- package/.claude/scripts/tests/test_aek_state_machine.py +385 -0
- package/.claude/scripts/tests/test_aggregate_changesets.py +646 -0
- package/.claude/scripts/tests/test_architect_bundle_validate.py +159 -0
- package/.claude/scripts/tests/test_audit_dashboard.py +822 -0
- package/.claude/scripts/tests/test_audit_log_dispatch_hint.py +91 -0
- package/.claude/scripts/tests/test_audit_log_retain.py +394 -0
- package/.claude/scripts/tests/test_audit_query.py +1177 -0
- package/.claude/scripts/tests/test_audit_query_by_domain.py +576 -0
- package/.claude/scripts/tests/test_audit_query_claims.py +92 -0
- package/.claude/scripts/tests/test_audit_query_critical.py +267 -0
- package/.claude/scripts/tests/test_audit_query_tokens.py +106 -0
- package/.claude/scripts/tests/test_audit_telemetry.py +214 -0
- package/.claude/scripts/tests/test_audit_tokens.py +255 -0
- package/.claude/scripts/tests/test_audit_verify_chain.py +189 -0
- package/.claude/scripts/tests/test_backup_audit.py +295 -0
- package/.claude/scripts/tests/test_benchmark_fallback_scorer.py +299 -0
- package/.claude/scripts/tests/test_benchmark_judge.py +569 -0
- package/.claude/scripts/tests/test_benchmarks_replay.py +313 -0
- package/.claude/scripts/tests/test_budget_summary.py +628 -0
- package/.claude/scripts/tests/test_build_canonical_models.py +349 -0
- package/.claude/scripts/tests/test_calibration_kappa.py +234 -0
- package/.claude/scripts/tests/test_cc_analytics_pull.py +296 -0
- package/.claude/scripts/tests/test_ceo_backup.py +318 -0
- package/.claude/scripts/tests/test_ceo_boot.py +643 -0
- package/.claude/scripts/tests/test_ceo_boot_audit_emit.py +484 -0
- package/.claude/scripts/tests/test_ceo_boot_enhanced.py +706 -0
- package/.claude/scripts/tests/test_ceo_boot_persona_cadence.py +392 -0
- package/.claude/scripts/tests/test_ceo_boot_plan_082.py +365 -0
- package/.claude/scripts/tests/test_ceo_boot_tamper_tripwires.py +556 -0
- package/.claude/scripts/tests/test_ceo_boot_task_candidate.py +868 -0
- package/.claude/scripts/tests/test_ceo_cost.py +221 -0
- package/.claude/scripts/tests/test_ceo_cost_stream.py +1076 -0
- package/.claude/scripts/tests/test_ceo_diagnose.py +314 -0
- package/.claude/scripts/tests/test_ceo_escalation_detector.py +591 -0
- package/.claude/scripts/tests/test_ceo_health.py +202 -0
- package/.claude/scripts/tests/test_ceo_info.py +542 -0
- package/.claude/scripts/tests/test_chaos_inject_lockdown.py +384 -0
- package/.claude/scripts/tests/test_check_action_sha_drift.py +174 -0
- package/.claude/scripts/tests/test_check_active_hooks_executable.py +79 -0
- package/.claude/scripts/tests/test_check_adr_chain.py +665 -0
- package/.claude/scripts/tests/test_check_audit_hmac_null.py +178 -0
- package/.claude/scripts/tests/test_check_audit_read_api_stable.py +176 -0
- package/.claude/scripts/tests/test_check_audit_registry_coverage.py +744 -0
- package/.claude/scripts/tests/test_check_auto_activation_flags.py +140 -0
- package/.claude/scripts/tests/test_check_canonical_doc_freshness.py +149 -0
- package/.claude/scripts/tests/test_check_claude_md_claims.py +223 -0
- package/.claude/scripts/tests/test_check_conformance_harness_mapping.py +243 -0
- package/.claude/scripts/tests/test_check_contamination.py +161 -0
- package/.claude/scripts/tests/test_check_creative_rewrite.py +183 -0
- package/.claude/scripts/tests/test_check_debate_round_lifecycle.py +162 -0
- package/.claude/scripts/tests/test_check_debt_ledger.py +227 -0
- package/.claude/scripts/tests/test_check_doc_skill_paths.py +99 -0
- package/.claude/scripts/tests/test_check_docs_freshness.py +224 -0
- package/.claude/scripts/tests/test_check_flip_criteria_drift.py +343 -0
- package/.claude/scripts/tests/test_check_flip_release_gate_consistency.py +195 -0
- package/.claude/scripts/tests/test_check_function_length.py +519 -0
- package/.claude/scripts/tests/test_check_model_deprecations.py +368 -0
- package/.claude/scripts/tests/test_check_originator_residue.py +165 -0
- package/.claude/scripts/tests/test_check_rule_invariants.py +327 -0
- package/.claude/scripts/tests/test_check_sdk_compat.py +88 -0
- package/.claude/scripts/tests/test_check_sidecar_manifest_sbom_sync.py +177 -0
- package/.claude/scripts/tests/test_check_spec_drift.py +358 -0
- package/.claude/scripts/tests/test_check_staleness.py +128 -0
- package/.claude/scripts/tests/test_check_stdlib_only_exceptions.py +91 -0
- package/.claude/scripts/tests/test_check_substrate_watch.py +234 -0
- package/.claude/scripts/tests/test_check_test_audit_isolation.py +322 -0
- package/.claude/scripts/tests/test_check_test_env_hygiene.py +432 -0
- package/.claude/scripts/tests/test_check_threat_model_coverage.py +251 -0
- package/.claude/scripts/tests/test_check_threat_model_freshness.py +235 -0
- package/.claude/scripts/tests/test_check_tier_boundaries.py +225 -0
- package/.claude/scripts/tests/test_check_tla_schema_drift.py +246 -0
- package/.claude/scripts/tests/test_check_translations_drift.py +262 -0
- package/.claude/scripts/tests/test_code_nav_bridge.py +192 -0
- package/.claude/scripts/tests/test_compaction_template.py +163 -0
- package/.claude/scripts/tests/test_compare_adopters.py +646 -0
- package/.claude/scripts/tests/test_confidence_gate.py +611 -0
- package/.claude/scripts/tests/test_confidence_gate_backfill.py +212 -0
- package/.claude/scripts/tests/test_context_budget.py +1400 -0
- package/.claude/scripts/tests/test_contextual_recommender.py +723 -0
- package/.claude/scripts/tests/test_coverage_audit_marker.py +109 -0
- package/.claude/scripts/tests/test_debate_converge.py +399 -0
- package/.claude/scripts/tests/test_debate_emit_cli.py +153 -0
- package/.claude/scripts/tests/test_debate_orchestrate.py +575 -0
- package/.claude/scripts/tests/test_detect_repo_profile.py +434 -0
- package/.claude/scripts/tests/test_discover_foreign_context.py +208 -0
- package/.claude/scripts/tests/test_dispatch_archetype_hint.py +429 -0
- package/.claude/scripts/tests/test_dispatch_frontmatter_validation.py +274 -0
- package/.claude/scripts/tests/test_drift_wire.py +259 -0
- package/.claude/scripts/tests/test_embeddings.py +249 -0
- package/.claude/scripts/tests/test_env_inventory_check.py +197 -0
- package/.claude/scripts/tests/test_eval_c3.py +474 -0
- package/.claude/scripts/tests/test_extract_skill.py +572 -0
- package/.claude/scripts/tests/test_fan_plan_parser.py +213 -0
- package/.claude/scripts/tests/test_find_orphan_sentinels.py +62 -0
- package/.claude/scripts/tests/test_first_run_wizard.py +634 -0
- package/.claude/scripts/tests/test_generate_adr_index.py +146 -0
- package/.claude/scripts/tests/test_generate_available_models.py +209 -0
- package/.claude/scripts/tests/test_generate_dispatch.py +90 -0
- package/.claude/scripts/tests/test_generate_skill_inventory.py +76 -0
- package/.claude/scripts/tests/test_github_api_client.py +146 -0
- package/.claude/scripts/tests/test_governance_waivers_gate.py +176 -0
- package/.claude/scripts/tests/test_hook_profiler.py +426 -0
- package/.claude/scripts/tests/test_import_skill.py +927 -0
- package/.claude/scripts/tests/test_import_skill_skip_rubric_auth.py +198 -0
- package/.claude/scripts/tests/test_inject_agent_context_mitigated_dispatch.py +266 -0
- package/.claude/scripts/tests/test_inject_agent_context_reference_mode.py +105 -0
- package/.claude/scripts/tests/test_inspired_by_validator.py +307 -0
- package/.claude/scripts/tests/test_install_dispatcher_present_maintainer.py +76 -0
- package/.claude/scripts/tests/test_install_maintainer_unchanged.py +86 -0
- package/.claude/scripts/tests/test_install_npm_sha256.py +113 -0
- package/.claude/scripts/tests/test_install_sh_placeholders.py +268 -0
- package/.claude/scripts/tests/test_install_sh_self_sha.py +244 -0
- package/.claude/scripts/tests/test_install_sh_session_75_flags.py +147 -0
- package/.claude/scripts/tests/test_install_user_dispatcher_present.py +75 -0
- package/.claude/scripts/tests/test_install_user_no_writes_outside_claude.py +75 -0
- package/.claude/scripts/tests/test_install_user_passes_validate_governance.py +73 -0
- package/.claude/scripts/tests/test_install_user_preserves_existing_repo.py +135 -0
- package/.claude/scripts/tests/test_install_user_skips_governance_hooks.py +102 -0
- package/.claude/scripts/tests/test_k_calibration.py +415 -0
- package/.claude/scripts/tests/test_key_hygiene.py +372 -0
- package/.claude/scripts/tests/test_lesson_ranker.py +82 -0
- package/.claude/scripts/tests/test_lesson_restore.py +91 -0
- package/.claude/scripts/tests/test_lessons.py +278 -0
- package/.claude/scripts/tests/test_lessons_concurrency.py +118 -0
- package/.claude/scripts/tests/test_lessons_emit.py +114 -0
- package/.claude/scripts/tests/test_lessons_inject.py +144 -0
- package/.claude/scripts/tests/test_lessons_v2.py +264 -0
- package/.claude/scripts/tests/test_lint_skills.py +525 -0
- package/.claude/scripts/tests/test_log_friction.py +436 -0
- package/.claude/scripts/tests/test_memory_prioritize.py +315 -0
- package/.claude/scripts/tests/test_morning_ledger.py +415 -0
- package/.claude/scripts/tests/test_mutation_test.py +144 -0
- package/.claude/scripts/tests/test_npm_rebuild.py +154 -0
- package/.claude/scripts/tests/test_osv_check.py +411 -0
- package/.claude/scripts/tests/test_otel_export.py +613 -0
- package/.claude/scripts/tests/test_otel_local_sink.py +262 -0
- package/.claude/scripts/tests/test_owasp_llm_top_10_benchmark.py +235 -0
- package/.claude/scripts/tests/test_parse_coverage_tier1.py +107 -0
- package/.claude/scripts/tests/test_pitfall_query.py +148 -0
- package/.claude/scripts/tests/test_plan_frontmatter_status.py +217 -0
- package/.claude/scripts/tests/test_plan_id_uniqueness.py +133 -0
- package/.claude/scripts/tests/test_plan_schema_enforcement.py +251 -0
- package/.claude/scripts/tests/test_plan_tokens.py +513 -0
- package/.claude/scripts/tests/test_plan_vcheck_gate.py +257 -0
- package/.claude/scripts/tests/test_policy_shadow_runner.py +312 -0
- package/.claude/scripts/tests/test_prune_lessons.py +341 -0
- package/.claude/scripts/tests/test_quality_profile.py +392 -0
- package/.claude/scripts/tests/test_rate_card_calibrate.py +185 -0
- package/.claude/scripts/tests/test_reality_ledger.py +1723 -0
- package/.claude/scripts/tests/test_red_team_eval.py +566 -0
- package/.claude/scripts/tests/test_red_team_eval_sha.py +260 -0
- package/.claude/scripts/tests/test_registry.py +290 -0
- package/.claude/scripts/tests/test_run_benchmark.py +639 -0
- package/.claude/scripts/tests/test_run_skill_benchmark_emit.py +195 -0
- package/.claude/scripts/tests/test_run_skill_benchmark_judge_mode.py +306 -0
- package/.claude/scripts/tests/test_scan_injection.py +191 -0
- package/.claude/scripts/tests/test_scan_injection_strict.sh +201 -0
- package/.claude/scripts/tests/test_scratchpad_cli.py +317 -0
- package/.claude/scripts/tests/test_self_test.py +369 -0
- package/.claude/scripts/tests/test_session_graph.py +511 -0
- package/.claude/scripts/tests/test_session_resume.py +306 -0
- package/.claude/scripts/tests/test_siem_rule_fixtures_have_paired_positive_negative.py +112 -0
- package/.claude/scripts/tests/test_skill_budget_generator.py +329 -0
- package/.claude/scripts/tests/test_skill_grandfather_parser.py +314 -0
- package/.claude/scripts/tests/test_skill_import_rubric.py +497 -0
- package/.claude/scripts/tests/test_skill_patch_apply_create_new_skill.py +459 -0
- package/.claude/scripts/tests/test_skill_patch_propose.py +294 -0
- package/.claude/scripts/tests/test_skill_patch_shadow_race.py +271 -0
- package/.claude/scripts/tests/test_skill_retrieval.py +486 -0
- package/.claude/scripts/tests/test_skill_retrieve_rag_wire.py +747 -0
- package/.claude/scripts/tests/test_smart_loading_resolver.py +808 -0
- package/.claude/scripts/tests/test_squad_export.py +265 -0
- package/.claude/scripts/tests/test_squad_grandfather_cap.py +434 -0
- package/.claude/scripts/tests/test_squad_import.py +905 -0
- package/.claude/scripts/tests/test_statusline_ceo.py +543 -0
- package/.claude/scripts/tests/test_success_receipt.py +448 -0
- package/.claude/scripts/tests/test_task_route.py +456 -0
- package/.claude/scripts/tests/test_token_budget_guard.py +418 -0
- package/.claude/scripts/tests/test_token_estimator.py +395 -0
- package/.claude/scripts/tests/test_trading_readonly.py +705 -0
- package/.claude/scripts/tests/test_ui_ux_imports.py +223 -0
- package/.claude/scripts/tests/test_validate_skill_frontmatter_pii_core.py +630 -0
- package/.claude/scripts/tests/test_validate_spec_context.py +128 -0
- package/.claude/scripts/tests/test_validate_squad_contract.py +221 -0
- package/.claude/scripts/tests/test_value_dashboard.py +593 -0
- package/.claude/scripts/tests/test_verify_adr_118_rationale.py +183 -0
- package/.claude/scripts/tests/test_verify_atlas_binding.py +159 -0
- package/.claude/scripts/tests/test_verify_counts.py +138 -0
- package/.claude/scripts/tests/test_verify_counts_remediation.py +258 -0
- package/.claude/scripts/tests/test_verify_persona_coverage.py +576 -0
- package/.claude/scripts/tests/test_veto_check.py +171 -0
- package/.claude/scripts/tests/test_workflow_devops_p2.py +229 -0
- package/.claude/scripts/tier_policy_cli/__init__.py +43 -0
- package/.claude/scripts/tier_policy_cli/_agent_frontmatter.py +196 -0
- package/.claude/scripts/tier_policy_cli/_constants.py +92 -0
- package/.claude/scripts/tier_policy_cli/_types.py +228 -0
- package/.claude/scripts/tier_policy_cli/apply.py +1139 -0
- package/.claude/scripts/tier_policy_cli/cli.py +795 -0
- package/.claude/scripts/tier_policy_cli/learn.py +846 -0
- package/.claude/scripts/tier_policy_cli/loader.py +535 -0
- package/.claude/scripts/tier_policy_cli/setup.py +33 -0
- package/.claude/scripts/tier_policy_cli/tests/__init__.py +0 -0
- package/.claude/scripts/tier_policy_cli/tests/test_adversarial.py +605 -0
- package/.claude/scripts/tier_policy_cli/tests/test_agent_frontmatter.py +231 -0
- package/.claude/scripts/tier_policy_cli/tests/test_apply.py +698 -0
- package/.claude/scripts/tier_policy_cli/tests/test_check_tier_policy_hook.py +187 -0
- package/.claude/scripts/tier_policy_cli/tests/test_cli.py +434 -0
- package/.claude/scripts/tier_policy_cli/tests/test_constants.py +113 -0
- package/.claude/scripts/tier_policy_cli/tests/test_learn.py +1380 -0
- package/.claude/scripts/tier_policy_cli/tests/test_learn_mutation.py +549 -0
- package/.claude/scripts/tier_policy_cli/tests/test_loader.py +368 -0
- package/.claude/scripts/tier_policy_cli/tests/test_types.py +152 -0
- package/.claude/scripts/token-budget-guard.py +657 -0
- package/.claude/scripts/token-estimator.py +957 -0
- package/.claude/scripts/tournament/__init__.py +22 -0
- package/.claude/scripts/tournament/check_fixture.py +271 -0
- package/.claude/scripts/tournament/fixtures/CORPUS_SHA256.txt +10 -0
- package/.claude/scripts/tournament/fixtures/code-review.jsonl +10 -0
- package/.claude/scripts/tournament/fixtures/docs-writing.jsonl +10 -0
- package/.claude/scripts/tournament/fixtures/performance-triage.jsonl +10 -0
- package/.claude/scripts/tournament/fixtures/security-review.jsonl +10 -0
- package/.claude/scripts/tournament/fixtures/test-design.jsonl +10 -0
- package/.claude/scripts/tournament/judge.py +269 -0
- package/.claude/scripts/tournament/loader.py +262 -0
- package/.claude/scripts/tournament/regen_corpus_sha.py +93 -0
- package/.claude/scripts/tournament/reporter.py +328 -0
- package/.claude/scripts/tournament/runner.py +707 -0
- package/.claude/scripts/tournament/scorer.py +118 -0
- package/.claude/scripts/tournament/tests/__init__.py +0 -0
- package/.claude/scripts/tournament/tests/_fake_dispatcher.py +233 -0
- package/.claude/scripts/tournament/tests/golden/strict_report_seed42.jsonl +6 -0
- package/.claude/scripts/tournament/tests/test_fixture_envelope.py +106 -0
- package/.claude/scripts/tournament/tests/test_fixture_security.py +227 -0
- package/.claude/scripts/tournament/tests/test_judge.py +299 -0
- package/.claude/scripts/tournament/tests/test_loader.py +223 -0
- package/.claude/scripts/tournament/tests/test_model_id_parity.py +136 -0
- package/.claude/scripts/tournament/tests/test_reporter.py +450 -0
- package/.claude/scripts/tournament/tests/test_reporter_golden.py +182 -0
- package/.claude/scripts/tournament/tests/test_runner.py +313 -0
- package/.claude/scripts/tournament/tests/test_runner_fail_open.py +204 -0
- package/.claude/scripts/tournament/tests/test_scorer.py +138 -0
- package/.claude/scripts/tournament/tests/test_tournament_e2e_smoke.py +147 -0
- package/.claude/scripts/tournament/tests/test_tournament_properties.py +181 -0
- package/.claude/scripts/trading-readonly-escape-hatch.sh +244 -0
- package/.claude/scripts/trading-readonly-guardrails.py +1136 -0
- package/.claude/scripts/translations-pairs.yaml +60 -0
- package/.claude/scripts/validate-findings.py +243 -0
- package/.claude/scripts/validate-governance.sh +1238 -0
- package/.claude/scripts/validate-skill-frontmatter.py +679 -0
- package/.claude/scripts/validate-spec-context.py +146 -0
- package/.claude/scripts/validate-squad-contract.py +318 -0
- package/.claude/scripts/validate_governance_fast.py +555 -0
- package/.claude/scripts/value-dashboard.py +851 -0
- package/.claude/scripts/verify-adr-118-rationale.py +285 -0
- package/.claude/scripts/verify-atlas-binding.py +331 -0
- package/.claude/scripts/verify-persona-coverage.py +531 -0
- package/.claude/scripts/verify-sprint3-invariants.sh +133 -0
- package/.claude/scripts/veto-check.py +218 -0
- package/.claude/security/README.md +200 -0
- package/.claude/security/sentinel-signers-registry.yaml +60 -0
- package/.claude/sentinel-signers.txt +24 -0
- package/.claude/settings.json +786 -0
- package/.claude/sidecars/c1-crypto/cryptography-mvp/README.md +89 -0
- package/.claude/sidecars/c1-crypto/cryptography-mvp/boundary_test.py +114 -0
- package/.claude/sidecars/c1-crypto/cryptography-mvp/install.sh +45 -0
- package/.claude/sidecars/c1-crypto/cryptography-mvp/manifest.json +52 -0
- package/.claude/sidecars/c1-crypto/cryptography-mvp/sidecar_code/cert_inspector.py +775 -0
- package/.claude/sidecars/c1-crypto/stdlib-ssl-mvp/boundary_test.py +318 -0
- package/.claude/sidecars/c1-crypto/stdlib-ssl-mvp/install.sh +57 -0
- package/.claude/sidecars/c1-crypto/stdlib-ssl-mvp/manifest.json +48 -0
- package/.claude/sidecars/c2-vector-memory/lightrag-mvp/README.md +88 -0
- package/.claude/sidecars/c2-vector-memory/lightrag-mvp/boundary_test.py +221 -0
- package/.claude/sidecars/c2-vector-memory/lightrag-mvp/install.sh +33 -0
- package/.claude/sidecars/c2-vector-memory/lightrag-mvp/manifest.json +59 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/boundary_test.py +142 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/install.sh +46 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/manifest.json +52 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/tests/__init__.py +0 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/tests/test_audit_emit_known_actions_property.py +123 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/tests/test_canonical_guard_symmetry_property.py +67 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/tests/test_payload_roundtrip_property.py +73 -0
- package/.claude/sidecars/c5-dev-tools/hypothesis/tests/test_redact_idempotence_property.py +68 -0
- package/.claude/skill-governance-grandfather.yaml +39 -0
- package/.claude/skill-patch-signers.txt +19 -0
- package/.claude/skills/core/agent-architect/SKILL.md +126 -0
- package/.claude/skills/core/ai-llm-orchestration/SKILL.md +620 -0
- package/.claude/skills/core/ai-llm-orchestration/SKILL.md.shadow.md +121 -0
- package/.claude/skills/core/architecture-decisions/SKILL.md +364 -0
- package/.claude/skills/core/architecture-decisions/benchmarks/architecture-decisions.yaml +257 -0
- package/.claude/skills/core/ceo-orchestration/SKILL-frontend.md +117 -0
- package/.claude/skills/core/ceo-orchestration/SKILL.md +700 -0
- package/.claude/skills/core/chaos-and-resilience/SKILL.md +568 -0
- package/.claude/skills/core/chaos-and-resilience/SKILL.md.shadow.md +553 -0
- package/.claude/skills/core/code-intelligence-lsp/SKILL.md +375 -0
- package/.claude/skills/core/code-review-checklist/SKILL.md +675 -0
- package/.claude/skills/core/code-review-checklist/SKILL.md.shadow.md +337 -0
- package/.claude/skills/core/code-review-checklist/benchmarks/code-review-checklist.yaml +444 -0
- package/.claude/skills/core/codebase-onboarding/SKILL.md +515 -0
- package/.claude/skills/core/compliance-lgpd/SKILL-frontend.md +513 -0
- package/.claude/skills/core/compliance-lgpd/SKILL.md +817 -0
- package/.claude/skills/core/consent-lifecycle/SKILL.md +149 -0
- package/.claude/skills/core/cookbook-advisor/SKILL.md +191 -0
- package/.claude/skills/core/coverage-audit/SKILL.md +116 -0
- package/.claude/skills/core/cross-llm-pair-review/SKILL.md +212 -0
- package/.claude/skills/core/data-schema-design/SKILL.md +933 -0
- package/.claude/skills/core/devops-ci-cd/SKILL.md +659 -0
- package/.claude/skills/core/dpo-reporting/SKILL.md +187 -0
- package/.claude/skills/core/evidence-based-qa/SKILL.md +565 -0
- package/.claude/skills/core/git-workflow-discipline/SKILL.md +600 -0
- package/.claude/skills/core/growth-and-launch/SKILL-frontend.md +800 -0
- package/.claude/skills/core/growth-and-launch/SKILL.md +903 -0
- package/.claude/skills/core/help-me/SKILL.md +177 -0
- package/.claude/skills/core/help-me/tests/test_help_me_skill.py +490 -0
- package/.claude/skills/core/identity-and-trust-architecture/SKILL.md +1062 -0
- package/.claude/skills/core/incident-management/SKILL.md +421 -0
- package/.claude/skills/core/incremental-refactoring/SKILL-frontend.md +210 -0
- package/.claude/skills/core/incremental-refactoring/SKILL.md +226 -0
- package/.claude/skills/core/llm-routing-and-finops/SKILL.md +828 -0
- package/.claude/skills/core/mcp-server-authoring/SKILL.md +685 -0
- package/.claude/skills/core/minimal-change-discipline/SKILL.md +545 -0
- package/.claude/skills/core/monetization-and-billing/SKILL-frontend.md +562 -0
- package/.claude/skills/core/monetization-and-billing/SKILL.md +585 -0
- package/.claude/skills/core/observability-and-ops/SKILL-frontend.md +290 -0
- package/.claude/skills/core/observability-and-ops/SKILL.md +612 -0
- package/.claude/skills/core/observability-and-ops/SKILL.md.shadow.md +324 -0
- package/.claude/skills/core/parallelization-by-default/SKILL.md +176 -0
- package/.claude/skills/core/parallelization-by-default/tests/test_parallelization_skill.py +490 -0
- package/.claude/skills/core/performance-engineering/SKILL.md +219 -0
- package/.claude/skills/core/performance-engineering/SKILL.md.shadow.md +204 -0
- package/.claude/skills/core/pii-data-flow/SKILL.md +166 -0
- package/.claude/skills/core/pre-plan-brainstorm/CHECKLIST.md +87 -0
- package/.claude/skills/core/pre-plan-brainstorm/SKILL.md +186 -0
- package/.claude/skills/core/product-conversion-readiness/SKILL-frontend.md +668 -0
- package/.claude/skills/core/product-conversion-readiness/SKILL.md +941 -0
- package/.claude/skills/core/public-api-design/SKILL.md +603 -0
- package/.claude/skills/core/public-api-design/benchmarks/public-api-design.yaml +261 -0
- package/.claude/skills/core/receiving-review/SKILL.md +131 -0
- package/.claude/skills/core/receiving-review/benchmarks/receiving-review.yaml +254 -0
- package/.claude/skills/core/requirement-quality-checklist/SKILL.md +97 -0
- package/.claude/skills/core/security-and-auth/SKILL.md +868 -0
- package/.claude/skills/core/security-and-auth/SKILL.md.shadow.md +500 -0
- package/.claude/skills/core/security-and-auth/benchmarks/owasp-basics.yaml +491 -0
- package/.claude/skills/core/security-and-auth/benchmarks/owasp-llm-top-10.yaml +769 -0
- package/.claude/skills/core/spec-clarify/SKILL.md +120 -0
- package/.claude/skills/core/state-machines-and-invariants/SKILL.md +288 -0
- package/.claude/skills/core/technical-writing/SKILL.md +432 -0
- package/.claude/skills/core/terse-mode/SKILL.md +80 -0
- package/.claude/skills/core/terse-mode/SKILL.md.shadow.md +65 -0
- package/.claude/skills/core/testing-strategy/SKILL.md +1026 -0
- package/.claude/skills/core/testing-strategy/SKILL.md.shadow.md +983 -0
- package/.claude/skills/domains/academic-humanities/examples/PLAN-EXAMPLE-ACH.md +126 -0
- package/.claude/skills/domains/academic-humanities/pitfalls.yaml +68 -0
- package/.claude/skills/domains/academic-humanities/skills/anthropologist/SKILL.md +394 -0
- package/.claude/skills/domains/academic-humanities/skills/geographer/SKILL.md +453 -0
- package/.claude/skills/domains/academic-humanities/skills/historian/SKILL.md +255 -0
- package/.claude/skills/domains/academic-humanities/skills/narratologist/SKILL.md +398 -0
- package/.claude/skills/domains/academic-humanities/skills/psychologist/SKILL.md +271 -0
- package/.claude/skills/domains/academic-humanities/task-chains.yaml +125 -0
- package/.claude/skills/domains/academic-humanities/team-personas.md +278 -0
- package/.claude/skills/domains/business-support/examples/PLAN-EXAMPLE-BSP.md +115 -0
- package/.claude/skills/domains/business-support/pitfalls.yaml +69 -0
- package/.claude/skills/domains/business-support/skills/analytics-reporter/SKILL.md +339 -0
- package/.claude/skills/domains/business-support/skills/executive-summary/SKILL.md +268 -0
- package/.claude/skills/domains/business-support/skills/finance-tracker/SKILL.md +321 -0
- package/.claude/skills/domains/business-support/skills/support-responder/SKILL.md +341 -0
- package/.claude/skills/domains/business-support/task-chains.yaml +118 -0
- package/.claude/skills/domains/business-support/team-personas.md +259 -0
- package/.claude/skills/domains/civil-engineering/skills/civil-engineer/SKILL.md +275 -0
- package/.claude/skills/domains/community/NOTICE.md +83 -0
- package/.claude/skills/domains/community/skills/advanced-evaluation/SKILL.md +463 -0
- package/.claude/skills/domains/community/skills/agent-evaluation/SKILL.md +400 -0
- package/.claude/skills/domains/community/skills/agentic-actions-auditor/SKILL.md +410 -0
- package/.claude/skills/domains/community/team-personas.md +41 -0
- package/.claude/skills/domains/devrel/examples/api-deprecation-comms.md +180 -0
- package/.claude/skills/domains/devrel/pitfalls.yaml +74 -0
- package/.claude/skills/domains/devrel/skills/developer-advocate/SKILL.md +382 -0
- package/.claude/skills/domains/devrel/task-chains.yaml +129 -0
- package/.claude/skills/domains/devrel/team-personas.md +260 -0
- package/.claude/skills/domains/edtech/examples/PLAN-EXAMPLE.md +89 -0
- package/.claude/skills/domains/edtech/pitfalls.yaml +98 -0
- package/.claude/skills/domains/edtech/skills/assessment-integrity/SKILL.md +208 -0
- package/.claude/skills/domains/edtech/skills/learning-analytics/SKILL.md +212 -0
- package/.claude/skills/domains/edtech/skills/student-data-privacy/SKILL.md +197 -0
- package/.claude/skills/domains/edtech/skills/study-abroad-advisory/SKILL.md +582 -0
- package/.claude/skills/domains/edtech/task-chains.yaml +122 -0
- package/.claude/skills/domains/edtech/team-personas.md +252 -0
- package/.claude/skills/domains/embedded/skills/embedded-firmware/SKILL.md +471 -0
- package/.claude/skills/domains/finance-accounting/examples/new-subscription-revenue.md +135 -0
- package/.claude/skills/domains/finance-accounting/pitfalls.yaml +74 -0
- package/.claude/skills/domains/finance-accounting/skills/bookkeeper-controller/SKILL.md +427 -0
- package/.claude/skills/domains/finance-accounting/skills/financial-analyst/SKILL.md +348 -0
- package/.claude/skills/domains/finance-accounting/skills/fpa-analyst/SKILL.md +366 -0
- package/.claude/skills/domains/finance-accounting/skills/tax-strategist/SKILL.md +358 -0
- package/.claude/skills/domains/finance-accounting/task-chains.yaml +90 -0
- package/.claude/skills/domains/finance-accounting/team-personas.md +281 -0
- package/.claude/skills/domains/fintech/ORG_CHART.md +167 -0
- package/.claude/skills/domains/fintech/commands/audit-ai.md +124 -0
- package/.claude/skills/domains/fintech/commands/deploy.md +15 -0
- package/.claude/skills/domains/fintech/commands/status.md +13 -0
- package/.claude/skills/domains/fintech/frontend-team-personas.md +503 -0
- package/.claude/skills/domains/fintech/pitfalls.yaml +58 -0
- package/.claude/skills/domains/fintech/scripts/check-pitfall-regression.sh +80 -0
- package/.claude/skills/domains/fintech/scripts/check-type-sync.sh +110 -0
- package/.claude/skills/domains/fintech/skills/blockchain-security-audit/SKILL.md +492 -0
- package/.claude/skills/domains/fintech/skills/equity-research/SKILL.md +459 -0
- package/.claude/skills/domains/fintech/skills/exchange-api-integration/SKILL.md +315 -0
- package/.claude/skills/domains/fintech/skills/exchange-onboarding-playbook/SKILL.md +527 -0
- package/.claude/skills/domains/fintech/skills/financial-correctness-and-math/SKILL-frontend.md +308 -0
- package/.claude/skills/domains/fintech/skills/financial-correctness-and-math/SKILL.md +340 -0
- package/.claude/skills/domains/fintech/skills/financial-display/SKILL.md +193 -0
- package/.claude/skills/domains/fintech/skills/frontend-data-layer/SKILL.md +206 -0
- package/.claude/skills/domains/fintech/skills/frontend-patterns/SKILL.md +387 -0
- package/.claude/skills/domains/fintech/skills/prediction-markets/SKILL.md +139 -0
- package/.claude/skills/domains/fintech/skills/real-time-market-systems/SKILL.md +315 -0
- package/.claude/skills/domains/fintech/skills/solidity-smart-contracts/SKILL.md +356 -0
- package/.claude/skills/domains/fintech/skills/trading-execution/SKILL.md +126 -0
- package/.claude/skills/domains/fintech/task-chains.yaml +46 -0
- package/.claude/skills/domains/fintech/team-personas.md +773 -0
- package/.claude/skills/domains/government/examples/PLAN-EXAMPLE.md +158 -0
- package/.claude/skills/domains/government/pitfalls.yaml +114 -0
- package/.claude/skills/domains/government/skills/accessibility-section-508/SKILL.md +183 -0
- package/.claude/skills/domains/government/skills/digital-presales/SKILL.md +359 -0
- package/.claude/skills/domains/government/skills/foia-and-records/SKILL.md +211 -0
- package/.claude/skills/domains/government/skills/public-procurement/SKILL.md +264 -0
- package/.claude/skills/domains/government/task-chains.yaml +88 -0
- package/.claude/skills/domains/government/team-personas.md +296 -0
- package/.claude/skills/domains/healthcare/examples/patient-portal-symptom-checker.md +130 -0
- package/.claude/skills/domains/healthcare/pitfalls.yaml +74 -0
- package/.claude/skills/domains/healthcare/skills/healthcare-customer-service/SKILL.md +369 -0
- package/.claude/skills/domains/healthcare/skills/marketing-compliance/SKILL.md +367 -0
- package/.claude/skills/domains/healthcare/task-chains.yaml +87 -0
- package/.claude/skills/domains/healthcare/team-personas.md +273 -0
- package/.claude/skills/domains/hospitality/skills/guest-services/SKILL.md +417 -0
- package/.claude/skills/domains/hr/examples/attrition-model-launch.md +128 -0
- package/.claude/skills/domains/hr/pitfalls.yaml +74 -0
- package/.claude/skills/domains/hr/skills/hr-onboarding/SKILL.md +435 -0
- package/.claude/skills/domains/hr/skills/recruitment-specialist/SKILL.md +400 -0
- package/.claude/skills/domains/hr/task-chains.yaml +91 -0
- package/.claude/skills/domains/hr/team-personas.md +251 -0
- package/.claude/skills/domains/i18n-business/examples/PLAN-EXAMPLE-I18N.md +115 -0
- package/.claude/skills/domains/i18n-business/pitfalls.yaml +68 -0
- package/.claude/skills/domains/i18n-business/skills/cultural-intelligence/SKILL.md +448 -0
- package/.claude/skills/domains/i18n-business/skills/french-consulting/SKILL.md +347 -0
- package/.claude/skills/domains/i18n-business/skills/korean-business/SKILL.md +360 -0
- package/.claude/skills/domains/i18n-business/skills/language-translator/SKILL.md +389 -0
- package/.claude/skills/domains/i18n-business/task-chains.yaml +117 -0
- package/.claude/skills/domains/i18n-business/team-personas.md +258 -0
- package/.claude/skills/domains/identity-systems/examples/passkey-rollout.md +137 -0
- package/.claude/skills/domains/identity-systems/pitfalls.yaml +74 -0
- package/.claude/skills/domains/identity-systems/skills/identity-graph-operator/SKILL.md +353 -0
- package/.claude/skills/domains/identity-systems/task-chains.yaml +90 -0
- package/.claude/skills/domains/identity-systems/team-personas.md +233 -0
- package/.claude/skills/domains/legal/examples/client-intake-pii-flow.md +177 -0
- package/.claude/skills/domains/legal/pitfalls.yaml +77 -0
- package/.claude/skills/domains/legal/skills/client-intake/SKILL.md +407 -0
- package/.claude/skills/domains/legal/skills/document-review/SKILL.md +373 -0
- package/.claude/skills/domains/legal/skills/legal-billing/SKILL.md +331 -0
- package/.claude/skills/domains/legal/task-chains.yaml +131 -0
- package/.claude/skills/domains/legal/team-personas.md +260 -0
- package/.claude/skills/domains/lgpd-heavy-saas/examples/PLAN-EXAMPLE.md +120 -0
- package/.claude/skills/domains/lgpd-heavy-saas/pitfalls.yaml +90 -0
- package/.claude/skills/domains/lgpd-heavy-saas/task-chains.yaml +83 -0
- package/.claude/skills/domains/lgpd-heavy-saas/team-personas.md +159 -0
- package/.claude/skills/domains/marketing-global/skills/agentic-search-optimizer/SKILL.md +391 -0
- package/.claude/skills/domains/marketing-global/skills/ai-citation-strategist/SKILL.md +343 -0
- package/.claude/skills/domains/marketing-global/skills/app-store-optimizer/SKILL.md +495 -0
- package/.claude/skills/domains/marketing-global/skills/book-co-author/SKILL.md +220 -0
- package/.claude/skills/domains/marketing-global/skills/carousel-growth-engine/SKILL.md +393 -0
- package/.claude/skills/domains/marketing-global/skills/content-creator/SKILL.md +416 -0
- package/.claude/skills/domains/marketing-global/skills/growth-hacker/SKILL.md +495 -0
- package/.claude/skills/domains/marketing-global/skills/instagram-curator/SKILL.md +419 -0
- package/.claude/skills/domains/marketing-global/skills/linkedin-content-creator/SKILL.md +291 -0
- package/.claude/skills/domains/marketing-global/skills/podcast-strategist/SKILL.md +408 -0
- package/.claude/skills/domains/marketing-global/skills/reddit-community-builder/SKILL.md +295 -0
- package/.claude/skills/domains/marketing-global/skills/seo-specialist/SKILL.md +352 -0
- package/.claude/skills/domains/marketing-global/skills/social-media-strategist/SKILL.md +349 -0
- package/.claude/skills/domains/marketing-global/skills/tiktok-strategist/SKILL.md +329 -0
- package/.claude/skills/domains/marketing-global/skills/twitter-engager/SKILL.md +382 -0
- package/.claude/skills/domains/marketing-global/skills/video-optimization-specialist/SKILL.md +386 -0
- package/.claude/skills/domains/mobile/examples/PLAN-EXAMPLE-MOB.md +129 -0
- package/.claude/skills/domains/mobile/pitfalls.yaml +69 -0
- package/.claude/skills/domains/mobile/skills/mobile-app-builder/SKILL.md +446 -0
- package/.claude/skills/domains/mobile/task-chains.yaml +126 -0
- package/.claude/skills/domains/mobile/team-personas.md +292 -0
- package/.claude/skills/domains/paid-media/examples/new-channel-launch.md +122 -0
- package/.claude/skills/domains/paid-media/pitfalls.yaml +79 -0
- package/.claude/skills/domains/paid-media/skills/auditor/SKILL.md +362 -0
- package/.claude/skills/domains/paid-media/skills/creative-strategist/SKILL.md +457 -0
- package/.claude/skills/domains/paid-media/skills/paid-social-strategist/SKILL.md +493 -0
- package/.claude/skills/domains/paid-media/skills/ppc-strategist/SKILL.md +450 -0
- package/.claude/skills/domains/paid-media/skills/programmatic-buyer/SKILL.md +396 -0
- package/.claude/skills/domains/paid-media/skills/search-query-analyst/SKILL.md +336 -0
- package/.claude/skills/domains/paid-media/skills/tracking-specialist/SKILL.md +457 -0
- package/.claude/skills/domains/paid-media/task-chains.yaml +121 -0
- package/.claude/skills/domains/paid-media/team-personas.md +251 -0
- package/.claude/skills/domains/project-management/examples/PLAN-EXAMPLE-PMG.md +117 -0
- package/.claude/skills/domains/project-management/pitfalls.yaml +68 -0
- package/.claude/skills/domains/project-management/skills/experiment-tracker/SKILL.md +293 -0
- package/.claude/skills/domains/project-management/skills/project-shepherd/SKILL.md +312 -0
- package/.claude/skills/domains/project-management/skills/studio-operations/SKILL.md +333 -0
- package/.claude/skills/domains/project-management/skills/studio-producer/SKILL.md +329 -0
- package/.claude/skills/domains/project-management/task-chains.yaml +118 -0
- package/.claude/skills/domains/project-management/team-personas.md +264 -0
- package/.claude/skills/domains/real-estate-finance/examples/PLAN-EXAMPLE-REF.md +129 -0
- package/.claude/skills/domains/real-estate-finance/pitfalls.yaml +68 -0
- package/.claude/skills/domains/real-estate-finance/skills/buyer-seller-agent/SKILL.md +410 -0
- package/.claude/skills/domains/real-estate-finance/skills/loan-officer-assistant/SKILL.md +415 -0
- package/.claude/skills/domains/real-estate-finance/task-chains.yaml +123 -0
- package/.claude/skills/domains/real-estate-finance/team-personas.md +287 -0
- package/.claude/skills/domains/retail/skills/customer-returns/SKILL.md +363 -0
- package/.claude/skills/domains/saas-platforms/examples/enterprise-tier-isolation.md +147 -0
- package/.claude/skills/domains/saas-platforms/pitfalls.yaml +74 -0
- package/.claude/skills/domains/saas-platforms/skills/cms-developer/SKILL.md +377 -0
- package/.claude/skills/domains/saas-platforms/skills/filament-specialist/SKILL.md +316 -0
- package/.claude/skills/domains/saas-platforms/skills/salesforce-architect/SKILL.md +369 -0
- package/.claude/skills/domains/saas-platforms/task-chains.yaml +90 -0
- package/.claude/skills/domains/saas-platforms/team-personas.md +283 -0
- package/.claude/skills/domains/sales/examples/qbr-revenue-forecast.md +158 -0
- package/.claude/skills/domains/sales/pitfalls.yaml +73 -0
- package/.claude/skills/domains/sales/skills/account-strategist/SKILL.md +408 -0
- package/.claude/skills/domains/sales/skills/deal-strategist/SKILL.md +292 -0
- package/.claude/skills/domains/sales/skills/discovery-coach/SKILL.md +257 -0
- package/.claude/skills/domains/sales/skills/outbound-strategist/SKILL.md +262 -0
- package/.claude/skills/domains/sales/skills/pipeline-analyst/SKILL.md +317 -0
- package/.claude/skills/domains/sales/skills/proposal-strategist/SKILL.md +288 -0
- package/.claude/skills/domains/sales/skills/sales-coach/SKILL.md +306 -0
- package/.claude/skills/domains/sales/skills/sales-engineer/SKILL.md +272 -0
- package/.claude/skills/domains/sales/skills/sales-outreach/SKILL.md +338 -0
- package/.claude/skills/domains/sales/task-chains.yaml +123 -0
- package/.claude/skills/domains/sales/team-personas.md +249 -0
- package/.claude/skills/domains/supply-chain/skills/supply-chain-strategist/SKILL.md +340 -0
- package/.claude/skills/domains/trading-hft/examples/PLAN-EXAMPLE.md +145 -0
- package/.claude/skills/domains/trading-hft/pitfalls.yaml +99 -0
- package/.claude/skills/domains/trading-hft/skills/kill-switches/SKILL.md +128 -0
- package/.claude/skills/domains/trading-hft/skills/latency-budgets/SKILL.md +117 -0
- package/.claude/skills/domains/trading-hft/skills/order-routing/SKILL.md +97 -0
- package/.claude/skills/domains/trading-hft/task-chains.yaml +97 -0
- package/.claude/skills/domains/trading-hft/team-personas.md +155 -0
- package/.claude/skills/domains/training-l-and-d/skills/corporate-training-designer/SKILL.md +268 -0
- package/.claude/skills/domains/voice-ai/skills/voice-ai-integration/SKILL.md +405 -0
- package/.claude/skills/frontend/NOTICE.md +80 -0
- package/.claude/skills/frontend/accessibility-and-wcag/SKILL.md +395 -0
- package/.claude/skills/frontend/accessibility-and-wcag/SKILL.md.shadow.md +181 -0
- package/.claude/skills/frontend/accessibility-and-wcag/benchmarks/accessibility-and-wcag.yaml +420 -0
- package/.claude/skills/frontend/accessibility-and-wcag/reference/charts-accessibility.yaml +357 -0
- package/.claude/skills/frontend/code-quality-and-typescript/SKILL.md +167 -0
- package/.claude/skills/frontend/design-system-and-components/SKILL.md +155 -0
- package/.claude/skills/frontend/design-system-and-components/SKILL.md.shadow.md +138 -0
- package/.claude/skills/frontend/design-system-and-components/reference/fonts.yaml +811 -0
- package/.claude/skills/frontend/design-system-and-components/reference/palettes.yaml +3066 -0
- package/.claude/skills/frontend/frontend-accessibility/SKILL.md +213 -0
- package/.claude/skills/frontend/frontend-data-layer/SKILL.md +310 -0
- package/.claude/skills/frontend/frontend-patterns/SKILL.md +771 -0
- package/.claude/skills/frontend/frontend-performance-optimization/SKILL.md +228 -0
- package/.claude/skills/frontend/frontend-performance-optimization/SKILL.md.shadow.md +213 -0
- package/.claude/skills/frontend/ux-and-user-journeys/SKILL.md +153 -0
- package/.claude/skills/frontend/ux-and-user-journeys/SKILL.md.shadow.md +138 -0
- package/.claude/skills/frontend/ux-and-user-journeys/reference/guidelines.yaml +997 -0
- package/.claude/squad-revocations.jsonl +5 -0
- package/.claude/task-chains.yaml +151 -0
- package/.claude/team.md +825 -0
- package/.claude/templates/squad-bundle/README.md +208 -0
- package/.claude/templates/squad-bundle/conftest.py +27 -0
- package/.claude/templates/squad-bundle/examples/template-example.md.template +94 -0
- package/.claude/templates/squad-bundle/pitfalls.yaml.template +88 -0
- package/.claude/templates/squad-bundle/task-chains.yaml.template +92 -0
- package/.claude/templates/squad-bundle/team-personas.md.template +161 -0
- package/.claude/trust/README.md +89 -0
- package/.claude/trust/owner.asc +11 -0
- package/.claude/workflows/README.md +124 -0
- package/.claude/workflows/audit-fanout.js +204 -0
- package/.claude/workflows/eval-baseline-n20.js +330 -0
- package/.claude/workflows/nightly-hygiene.js +176 -0
- package/LICENSE +21 -0
- package/PROTOCOL.md +597 -0
- package/README.md +167 -0
- package/SPEC/v1/README.md +181 -0
- package/SPEC/v1/adapters.schema.md +272 -0
- package/SPEC/v1/audit-log.schema.md +1514 -0
- package/SPEC/v1/audit-query.schema.md +152 -0
- package/SPEC/v1/benchmarks.schema.md +166 -0
- package/SPEC/v1/claude-sdk-compat.md +123 -0
- package/SPEC/v1/debate.schema.md +35 -0
- package/SPEC/v1/hook-io.schema.md +94 -0
- package/SPEC/v1/install-cli.md +234 -0
- package/SPEC/v1/judge-payload.schema.md +98 -0
- package/SPEC/v1/live-adapters-policy.schema.md +118 -0
- package/SPEC/v1/mcp-server.schema.md +558 -0
- package/SPEC/v1/memory-shared.schema.md +365 -0
- package/SPEC/v1/normalized_envelope.schema.md +183 -0
- package/SPEC/v1/npm-shim.md +95 -0
- package/SPEC/v1/plan.schema.md +34 -0
- package/SPEC/v1/policy-dsl.schema.md +466 -0
- package/SPEC/v1/predict-budget.schema.md +289 -0
- package/SPEC/v1/rag-sidecar.schema.md +222 -0
- package/SPEC/v1/red-team-corpus.schema.md +186 -0
- package/SPEC/v1/replay.schema.md +272 -0
- package/SPEC/v1/scratchpad.schema.md +172 -0
- package/SPEC/v1/sentinel-format.schema.md +306 -0
- package/SPEC/v1/session-graph.schema.md +236 -0
- package/SPEC/v1/skill-frontmatter.schema.md +83 -0
- package/SPEC/v1/skill-index.schema.md +197 -0
- package/SPEC/v1/skill-proposals.schema.md +175 -0
- package/SPEC/v1/soc2-control-map.schema.md +797 -0
- package/SPEC/v1/squad-manifest.schema.md +157 -0
- package/SPEC/v1/state-stores.schema.md +146 -0
- package/SPEC/v1/tier-policy.schema.md +264 -0
- package/SPEC/v1/tournament-report.schema.md +156 -0
- package/VERSION +1 -0
- package/bin/ceo-orch-init.js +55 -0
- package/package.json +42 -0
- package/scripts/_framework_manifest_set.sh +237 -0
- package/scripts/_hash_lib.sh +92 -0
- package/scripts/build-plugin.py +351 -0
- package/scripts/discover_foreign_context.py +151 -0
- package/scripts/install-accelerators.sh +166 -0
- package/scripts/install-npm.sh +254 -0
- package/scripts/install.sh +1932 -0
- package/scripts/local/OWNER-CEREMONY-PLAN-094-WAVE-A.sh +648 -0
- package/scripts/local/OWNER-CEREMONY-S82-V1120.sh +169 -0
- package/scripts/local/plan-093-apply-kernel-edits.py +496 -0
- package/scripts/local/plan-093-execute-ceremony.sh +118 -0
- package/scripts/local/plan-093-kernel-override-restart.sh +115 -0
- package/scripts/local/plan-093-ship-v1.26.0.sh +226 -0
- package/scripts/local/plan-094-apply-wave-a-c-e.py +398 -0
- package/scripts/local/smoke-install-parity.sh +168 -0
- package/scripts/local/trading-readonly-escape-hatch.sh +244 -0
- package/scripts/measure-repo-size.sh +98 -0
- package/scripts/npm-rebuild.sh +172 -0
- package/scripts/publish-plugin.sh +144 -0
- package/scripts/tests/smoke-install.sh +260 -0
- package/scripts/tests/test-install-sandbox-merge.sh +137 -0
- package/scripts/tests/test_install_baseline_manifest.sh +392 -0
- package/scripts/uninstall.sh +282 -0
- package/scripts/upgrade.sh +1260 -0
- package/templates/.claude/tier-policy.json +35 -0
- package/templates/.claude/tier-policy.json.sigchain +1 -0
- package/templates/.env.example +134 -0
- package/templates/.github/CODEOWNERS.template +33 -0
- package/templates/.github/workflows/benchmarks.yml.template +145 -0
- package/templates/.github/workflows/validate.yml.template +226 -0
- package/templates/.mcp.json +13 -0
- package/templates/CLAUDE.md +125 -0
- package/templates/MEMORY.md +36 -0
- package/templates/README.md +46 -0
- package/templates/compaction.md +130 -0
- package/templates/docs/BRANCH-PROTECTION.md +203 -0
- package/templates/docs/rotation-log.md +18 -0
- package/templates/oidc-proxy/README.md +141 -0
- package/templates/oidc-proxy/broker.config.example.json +29 -0
- package/templates/oidc-proxy/oidc_key_broker.py +361 -0
- package/templates/oidc-proxy/tests/test_oidc_key_broker.py +361 -0
- package/templates/scripts/statusline-ceo.py +597 -0
- package/templates/settings/settings.base.json +708 -0
- package/templates/settings/settings.stack.node.json +19 -0
- package/templates/settings/settings.stack.otel.json +25 -0
- package/templates/settings/settings.stack.sandbox.json +57 -0
- package/templates/settings/settings.user.json +265 -0
- package/templates/team-personas-reference.md +269 -0
|
@@ -0,0 +1,2613 @@
|
|
|
1
|
+
# PLAN-094 Wave A — spool_writer (canonical promotion of spool_writer_DRAFT.py)
|
|
2
|
+
"""_lib/spool_writer.py — durable spool + drain-on-next-invoke (PLAN-094 Wave A).
|
|
3
|
+
|
|
4
|
+
Implements ADR-055-AMEND-1: 5-phase atomic drain protocol, 4-tuple total
|
|
5
|
+
order across concurrent producers, K_MAX/K_TAIL_WINDOW idempotent skip,
|
|
6
|
+
canonical-tail prev_hmac reconstruction, per-PID spool + per-PID journal,
|
|
7
|
+
atomic split-and-delete for K_MAX deferral, header/body uuid sentinel.
|
|
8
|
+
|
|
9
|
+
Stdlib-only (ADR-002). Fail-open invariant — never raise to caller; any
|
|
10
|
+
infra failure emits a breadcrumb and returns safely.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import atexit
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import signal
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
|
|
28
|
+
|
|
29
|
+
_HOOKS_DIR = Path(__file__).resolve().parent.parent
|
|
30
|
+
if str(_HOOKS_DIR) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(_HOOKS_DIR))
|
|
32
|
+
|
|
33
|
+
from _lib.filelock import FileLock, FileLockTimeout # noqa: E402
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from _lib import audit_hmac as _audit_hmac # noqa: E402
|
|
37
|
+
_HMAC_AVAILABLE = True
|
|
38
|
+
except Exception: # pragma: no cover
|
|
39
|
+
_audit_hmac = None # type: ignore[assignment]
|
|
40
|
+
_HMAC_AVAILABLE = False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from _lib import canonical_json as _canonical_json # noqa: E402
|
|
44
|
+
except Exception: # pragma: no cover
|
|
45
|
+
_canonical_json = None # type: ignore[assignment]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Constants (ADR-055-AMEND-1 §3 wave A spec)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
K_MAX = 100
|
|
53
|
+
K_TAIL_WINDOW = 200
|
|
54
|
+
SPOOL_HEADER_VERSION = 1
|
|
55
|
+
# PLAN-111 Wave B-alt4 (debate consensus + cProfile data-driven): trigger
|
|
56
|
+
# raised 50 -> 100. Halves drain cascade count (40 -> 20 across 5-trial
|
|
57
|
+
# emit_pair benchmark, confirmed by cprofile-post-b-darwin.txt). Per-trial
|
|
58
|
+
# wall-clock 174ms -> 170ms (-2%); cumulative drain_now cumtime 364ms -> 307ms.
|
|
59
|
+
# Combined with Wave A cache: per-emit 580us -> 425us (-27%). AC-C1 strict
|
|
60
|
+
# <=360us NOT hit alone; Wave C.1 RELAX path engaged (200->300ms test
|
|
61
|
+
# budget) with ubuntu projection ~255ms still well under.
|
|
62
|
+
DRAIN_TRIGGER_SIZE = 100
|
|
63
|
+
DRAIN_TRIGGER_MTIME_MS = 100
|
|
64
|
+
STALE_SPOOL_TTL_DAYS = 7
|
|
65
|
+
SPOOL_LOCK_TIMEOUT = 2.5
|
|
66
|
+
|
|
67
|
+
_SPOOL_PREFIX = "audit-spool"
|
|
68
|
+
_JOURNAL_PREFIX = "audit-pending"
|
|
69
|
+
_DRAINING_SUFFIX_TOKEN = ".draining."
|
|
70
|
+
_MALFORMED_SUFFIX_TOKEN = ".malformed."
|
|
71
|
+
_QUARANTINED_SUFFIX_TOKEN = ".quarantined."
|
|
72
|
+
_TMP_SUFFIX_TOKEN = ".tmp."
|
|
73
|
+
# PLAN-119 WS-D1 — test-origin spool quarantine suffix. A spool minted under a
|
|
74
|
+
# test signal is stamped ``_origin:"test"`` in its header; if such a spool is
|
|
75
|
+
# ever drained while the canonical destination IS the live chain, it is renamed
|
|
76
|
+
# out of the drain path with this token so its entries never reach the live
|
|
77
|
+
# canonical append.
|
|
78
|
+
_TEST_ORIGIN_SUFFIX_TOKEN = ".test-origin."
|
|
79
|
+
# PLAN-119 WS-D1 — the live-log path snapshot env var. Set by the WS-A session
|
|
80
|
+
# fixture (``_lib/test_isolation``) BEFORE it redirects the env, so the drainer —
|
|
81
|
+
# which post-redirect cannot infer the real live path from the environment — can
|
|
82
|
+
# compare the canonical destination against it. The literal is DUPLICATED here
|
|
83
|
+
# (NOT imported) because this kernel module must not depend on the test helper;
|
|
84
|
+
# the two MUST stay in sync (see ``_lib/test_isolation.LIVE_LOG_SNAPSHOT_VAR``).
|
|
85
|
+
_LIVE_LOG_SNAPSHOT_VAR = "CEO_AUDIT_LIVE_LOG_PATH_SNAPSHOT"
|
|
86
|
+
|
|
87
|
+
_SPOOL_UUID_HEX_LEN = 16 # secrets.token_hex(8) -> 16 hex chars
|
|
88
|
+
_DRAIN_EPOCH_HEX_LEN = 8 # secrets.token_hex(4) -> 8 hex chars
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Public dataclasses
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class DrainStats:
|
|
98
|
+
"""Summary of a single drain_now() invocation.
|
|
99
|
+
|
|
100
|
+
iter3-P2-1: `intentionally_deleted` counts entries silently removed from
|
|
101
|
+
spool during drain (duplicate-tuple rejection — paired with the
|
|
102
|
+
audit_spool_intentionally_deleted forensic emit). Surfaced into
|
|
103
|
+
JournalReconciliation.intentionally_deleted for AC10 visibility.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
appended: int = 0
|
|
107
|
+
skipped_idempotent: int = 0
|
|
108
|
+
rejected_duplicate_tuple: int = 0
|
|
109
|
+
intentionally_deleted: int = 0
|
|
110
|
+
quarantined_files: int = 0
|
|
111
|
+
# PLAN-119 WS-D1 — count of _origin:"test" spools refused at a live-chain drain.
|
|
112
|
+
test_origin_quarantined: int = 0
|
|
113
|
+
partial_lines_discarded: int = 0
|
|
114
|
+
files_consumed_fully: int = 0
|
|
115
|
+
files_split_remainder: int = 0
|
|
116
|
+
in_recovery_mode: bool = False
|
|
117
|
+
drain_epoch: Optional[str] = None
|
|
118
|
+
ok: bool = True
|
|
119
|
+
error: Optional[str] = None
|
|
120
|
+
# (ADR-055-AMEND-3) — True when an OPPORTUNISTIC (force=False)
|
|
121
|
+
# drain yielded the canonical lock to another drainer without blocking.
|
|
122
|
+
# `ok` stays True (not an error): the lock holder's global sweep plus the
|
|
123
|
+
# yielding process's own later drain cover its events. NEVER persisted to
|
|
124
|
+
# the audit log (in-process DrainStats only; no Sec MF-3 surface).
|
|
125
|
+
contended_skip: bool = False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class JournalReconciliation:
|
|
130
|
+
"""Counters emitted to audit_flush_dropped_count at session-start."""
|
|
131
|
+
|
|
132
|
+
begin_no_commit: int = 0
|
|
133
|
+
commit_no_drained: int = 0
|
|
134
|
+
recovered: int = 0
|
|
135
|
+
truly_lost: int = 0
|
|
136
|
+
tamper_rejected: int = 0
|
|
137
|
+
intentionally_deleted: int = 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Path helpers — mirror audit_emit conventions
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# PLAN-111 Wave A — single-slot cache for _project_dir_from_env + _state_dir.
|
|
146
|
+
# Keyed on env-tuple (CEO_AUDIT_LOG_DIR, HOME); replace-on-miss; explicit
|
|
147
|
+
# reset via _reset_caches_for_test() bound to TestEnvContext.setUp/tearDown.
|
|
148
|
+
# Single-threaded contract: concurrent os.environ mutation = UB; future
|
|
149
|
+
# threading plans must add per-thread cache or thread-local env snapshot.
|
|
150
|
+
# Debate Round 1: SA-K1 (single-slot), SA-K7 (single-threaded), SA-K10
|
|
151
|
+
# (permission re-assertion on cache MISS), AC-A2a (store-on-mkdir-success
|
|
152
|
+
# only; no cached-as-unusable Path).
|
|
153
|
+
_PROJECT_DIR_CACHE: "Optional[Tuple[Tuple[Optional[str], Optional[str]], Path]]" = None
|
|
154
|
+
_STATE_DIR_CACHE: "Optional[Tuple[Tuple[Optional[str], Optional[str]], Path]]" = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _reset_caches_for_test() -> None:
|
|
158
|
+
"""Clear _project_dir_from_env + _state_dir caches.
|
|
159
|
+
|
|
160
|
+
PLAN-111 Wave A.5: bound to TestEnvContext.setUp/tearDown to avoid
|
|
161
|
+
cross-test leakage when CEO_AUDIT_LOG_DIR or HOME mutate. Also bound
|
|
162
|
+
via unittest.addModuleCleanup in test_lifecycle_edge_cases.py for
|
|
163
|
+
bare-TestCase classes that don't inherit TestEnvContext.
|
|
164
|
+
|
|
165
|
+
PRODUCTION CODE MUST NOT CALL THIS — it's a test-only API. Calling
|
|
166
|
+
mid-flight flushes the cache and re-pays the mkdir + lstat cost on
|
|
167
|
+
next emit (correctness preserved; perf regressed for one call).
|
|
168
|
+
"""
|
|
169
|
+
global _PROJECT_DIR_CACHE, _STATE_DIR_CACHE
|
|
170
|
+
_PROJECT_DIR_CACHE = None
|
|
171
|
+
_STATE_DIR_CACHE = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _project_dir_from_env() -> Path:
|
|
175
|
+
"""Return the audit project dir (BYTE-IDENTICAL to audit_emit._audit_dir).
|
|
176
|
+
|
|
177
|
+
P1-5: must mirror audit_emit._audit_dir() exactly — only CEO_AUDIT_LOG_DIR
|
|
178
|
+
+ HOME. CEO_AUDIT_LOG_PATH is the FILE path, not the dir path, so we do
|
|
179
|
+
NOT derive the dir from it (audit_emit doesn't either).
|
|
180
|
+
|
|
181
|
+
PLAN-111 Wave A: single-slot cache keyed on (CEO_AUDIT_LOG_DIR, HOME).
|
|
182
|
+
Cache HIT skips re-construction of Path object (saves ~7us/call x
|
|
183
|
+
5.7x/emit cumulative). Cache MISS replaces the slot atomically.
|
|
184
|
+
|
|
185
|
+
Single-threaded contract: concurrent os.environ mutation = UB.
|
|
186
|
+
"""
|
|
187
|
+
global _PROJECT_DIR_CACHE
|
|
188
|
+
env_dir = os.environ.get("CEO_AUDIT_LOG_DIR")
|
|
189
|
+
env_home = os.environ.get("HOME")
|
|
190
|
+
env_key = (env_dir, env_home)
|
|
191
|
+
cached = _PROJECT_DIR_CACHE
|
|
192
|
+
if cached is not None and cached[0] == env_key:
|
|
193
|
+
return cached[1]
|
|
194
|
+
if env_dir:
|
|
195
|
+
p = Path(env_dir)
|
|
196
|
+
else:
|
|
197
|
+
home = env_home or str(Path.home())
|
|
198
|
+
p = Path(home) / ".claude" / "projects" / "ceo-orchestration"
|
|
199
|
+
_PROJECT_DIR_CACHE = (env_key, p)
|
|
200
|
+
return p
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _state_dir() -> Path:
|
|
204
|
+
"""Return <audit_dir>/state/ ensuring parents exist (0700).
|
|
205
|
+
|
|
206
|
+
P1-5: state dir is unconditionally a child of audit_emit._audit_dir();
|
|
207
|
+
no CEO_PROJECT_STATE_DIR override (audit_emit doesn't expose one).
|
|
208
|
+
|
|
209
|
+
PLAN-111 Wave A: single-slot cache keyed on (CEO_AUDIT_LOG_DIR, HOME).
|
|
210
|
+
Cache HIT skips mkdir syscall + Path construction (saves ~17us/call x
|
|
211
|
+
5.7x/emit cumulative ~= 100us/emit recovery).
|
|
212
|
+
|
|
213
|
+
Cache MISS semantics (AC-A2a + SA-K10 permission re-assertion +
|
|
214
|
+
PLAN-113 W4-SEC mode-mismatch self-heal):
|
|
215
|
+
1. Resolve target Path (uses _project_dir_from_env cache implicitly).
|
|
216
|
+
2. Attempt mkdir(parents=True, exist_ok=True, mode=0o700).
|
|
217
|
+
3. If mkdir FAILS: breadcrumb + DO NOT cache (next call retries).
|
|
218
|
+
4. If mkdir SUCCEEDS: validate via lstat() that dir is not a symlink,
|
|
219
|
+
is owned by os.getuid(), and has mode 0o700.
|
|
220
|
+
- symlink / wrong-owner mismatch: FAIL-CLOSED (raise) — we never
|
|
221
|
+
chmod-and-trust an attacker-controlled or other-owned dir.
|
|
222
|
+
- mode-only mismatch (e.g. a pre-existing state/ at 0o755 created
|
|
223
|
+
by an older path; exist_ok=True does NOT relax perms): attempt
|
|
224
|
+
SELF-HEAL via os.chmod(d, 0o700) then RE-lstat + re-validate the
|
|
225
|
+
full invariant. If it is now a non-symlink, owned by os.getuid(),
|
|
226
|
+
AND 0o700 → PROCEED (cache + return). Otherwise keep the SA-K10
|
|
227
|
+
fail-CLOSED.
|
|
228
|
+
5. Only if validation (incl. any self-heal) succeeds: cache the Path.
|
|
229
|
+
|
|
230
|
+
Single-threaded contract: concurrent os.environ mutation = UB.
|
|
231
|
+
"""
|
|
232
|
+
global _STATE_DIR_CACHE
|
|
233
|
+
env_dir = os.environ.get("CEO_AUDIT_LOG_DIR")
|
|
234
|
+
env_home = os.environ.get("HOME")
|
|
235
|
+
env_key = (env_dir, env_home)
|
|
236
|
+
cached = _STATE_DIR_CACHE
|
|
237
|
+
if cached is not None and cached[0] == env_key:
|
|
238
|
+
return cached[1]
|
|
239
|
+
d = _project_dir_from_env() / "state"
|
|
240
|
+
mkdir_ok = False
|
|
241
|
+
try:
|
|
242
|
+
d.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
243
|
+
mkdir_ok = True
|
|
244
|
+
except OSError as e:
|
|
245
|
+
_breadcrumb(f"_state_dir mkdir failed (no cache): "
|
|
246
|
+
f"{type(e).__name__}: {e}")
|
|
247
|
+
if mkdir_ok:
|
|
248
|
+
try:
|
|
249
|
+
import stat as _stat_mod
|
|
250
|
+
|
|
251
|
+
def _classify(st_mode: int, st_uid: int) -> "Optional[str]":
|
|
252
|
+
"""Return a mismatch reason, or None when state/ is healthy."""
|
|
253
|
+
if _stat_mod.S_ISLNK(st_mode):
|
|
254
|
+
return f"is_symlink: {d}"
|
|
255
|
+
if st_uid != os.getuid():
|
|
256
|
+
return f"uid_mismatch: {st_uid} != {os.getuid()}"
|
|
257
|
+
if (st_mode & 0o777) != 0o700:
|
|
258
|
+
return f"mode_mismatch: {oct(st_mode & 0o777)} != 0o700"
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
st = os.lstat(str(d))
|
|
262
|
+
mismatch_reason: "Optional[str]" = _classify(st.st_mode, st.st_uid)
|
|
263
|
+
|
|
264
|
+
# PLAN-113 W4-SEC self-heal: a pre-existing state/ dir at a mode
|
|
265
|
+
# other than 0o700 (most commonly 0o755 from an older code path —
|
|
266
|
+
# exist_ok=True does NOT relax perms on an existing dir)
|
|
267
|
+
# previously fail-CLOSED on EVERY call, dropping the audit event
|
|
268
|
+
# and flooding the spool. Self-heal ONLY the mode-mismatch case
|
|
269
|
+
# (NEVER symlink / wrong-owner — chmod-and-trusting those is
|
|
270
|
+
# exactly the attack we fail closed on): chmod 0o700, then
|
|
271
|
+
# RE-lstat and re-validate the FULL invariant from scratch.
|
|
272
|
+
if (mismatch_reason is not None
|
|
273
|
+
and not _stat_mod.S_ISLNK(st.st_mode)
|
|
274
|
+
and st.st_uid == os.getuid()):
|
|
275
|
+
healed = False
|
|
276
|
+
fd = None
|
|
277
|
+
try:
|
|
278
|
+
# PLAN-113 Codex B3 P2 — close the chmod TOCTOU: a path-based
|
|
279
|
+
# os.chmod follows a symlink if state/ is swapped between the
|
|
280
|
+
# initial lstat and the chmod, applying 0o700 to the attacker's
|
|
281
|
+
# target. Open with O_NOFOLLOW (refuse a symlinked final
|
|
282
|
+
# component) | O_DIRECTORY (refuse a non-dir), then fchmod +
|
|
283
|
+
# fstat the FD — no path re-resolution, so no race window.
|
|
284
|
+
fd = os.open(
|
|
285
|
+
str(d), os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY
|
|
286
|
+
)
|
|
287
|
+
os.fchmod(fd, 0o700)
|
|
288
|
+
fst = os.fstat(fd)
|
|
289
|
+
mismatch_reason = _classify(fst.st_mode, fst.st_uid)
|
|
290
|
+
if mismatch_reason is None:
|
|
291
|
+
st = fst
|
|
292
|
+
healed = True
|
|
293
|
+
except OSError as e:
|
|
294
|
+
_breadcrumb(
|
|
295
|
+
f"_state_dir self-heal (O_NOFOLLOW fchmod) failed "
|
|
296
|
+
f"(FAIL-CLOSED): {type(e).__name__}: {e}"
|
|
297
|
+
)
|
|
298
|
+
finally:
|
|
299
|
+
if fd is not None:
|
|
300
|
+
try:
|
|
301
|
+
os.close(fd)
|
|
302
|
+
except OSError:
|
|
303
|
+
pass
|
|
304
|
+
if healed:
|
|
305
|
+
_breadcrumb(
|
|
306
|
+
f"_state_dir mode self-healed to 0o700: {d}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if mismatch_reason is not None:
|
|
310
|
+
# PLAN-111 Wave A SA-K10 fail-CLOSED (Codex R2 P0 fix):
|
|
311
|
+
# detecting + not-caching is fake-security; caller would
|
|
312
|
+
# still write to the attacker-controlled symlink target.
|
|
313
|
+
# Raise OSError so callers' fail-open path catches it
|
|
314
|
+
# (matches existing _state_dir fail-open semantics) and
|
|
315
|
+
# diverts the write to fallback path. PLAN-113 W4-SEC: we
|
|
316
|
+
# only reach here for symlink / wrong-owner, OR a mode
|
|
317
|
+
# mismatch the chmod self-heal could not resolve.
|
|
318
|
+
_breadcrumb(
|
|
319
|
+
f"_state_dir SECURITY mismatch (FAIL-CLOSED): "
|
|
320
|
+
f"{mismatch_reason}"
|
|
321
|
+
)
|
|
322
|
+
raise PermissionError(
|
|
323
|
+
f"_state_dir permission re-assertion failed: "
|
|
324
|
+
f"{mismatch_reason}"
|
|
325
|
+
)
|
|
326
|
+
_STATE_DIR_CACHE = (env_key, d)
|
|
327
|
+
except PermissionError:
|
|
328
|
+
# Re-raise the SA-K10 fail-CLOSED so caller handles.
|
|
329
|
+
raise
|
|
330
|
+
except OSError as e:
|
|
331
|
+
_breadcrumb(f"_state_dir lstat failed (no cache): "
|
|
332
|
+
f"{type(e).__name__}: {e}")
|
|
333
|
+
return d
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _canonical_log_path() -> Path:
|
|
337
|
+
"""Mirror audit_emit._log_path() — canonical audit-log.jsonl."""
|
|
338
|
+
env = os.environ.get("CEO_AUDIT_LOG_PATH")
|
|
339
|
+
if env:
|
|
340
|
+
return Path(env)
|
|
341
|
+
return _project_dir_from_env() / "audit-log.jsonl"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _canonical_log_lock() -> Path:
|
|
345
|
+
"""Mirror audit_emit._lock_path() — sibling .lock file."""
|
|
346
|
+
env = os.environ.get("CEO_AUDIT_LOG_LOCK")
|
|
347
|
+
if env:
|
|
348
|
+
return Path(env)
|
|
349
|
+
return _project_dir_from_env() / "audit-log.lock"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _errors_path() -> Path:
|
|
353
|
+
"""Breadcrumb file for fail-open infra errors."""
|
|
354
|
+
env = os.environ.get("CEO_AUDIT_LOG_ERR")
|
|
355
|
+
if env:
|
|
356
|
+
return Path(env)
|
|
357
|
+
return _project_dir_from_env() / "audit-log.errors"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _spool_path(pid: int) -> Path:
|
|
361
|
+
"""Active spool path for a given PID (header + body)."""
|
|
362
|
+
return _state_dir() / f"{_SPOOL_PREFIX}.{pid}.jsonl"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _journal_path(pid: int) -> Path:
|
|
366
|
+
"""Per-PID journal path."""
|
|
367
|
+
return _state_dir() / f"{_JOURNAL_PREFIX}.{pid}.journal"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _aggregate_journal_path() -> Path:
|
|
371
|
+
"""Aggregate journal for swept dead-PID envelopes."""
|
|
372
|
+
return _state_dir() / f"{_JOURNAL_PREFIX}.journal"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _aggregate_journal_lock_path() -> Path:
|
|
376
|
+
"""flock for aggregation sweep at session-start."""
|
|
377
|
+
return _state_dir() / f"{_JOURNAL_PREFIX}.journal.aggregation.lock"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _spool_flock_path(pid: int) -> Path:
|
|
381
|
+
"""Per-PID spool flock sibling (NOT same fd as the spool file)."""
|
|
382
|
+
return _state_dir() / f"{_SPOOL_PREFIX}.{pid}.jsonl.lock"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _journal_flock_path(pid: int) -> Path:
|
|
386
|
+
"""Per-PID journal flock sibling."""
|
|
387
|
+
return _state_dir() / f"{_JOURNAL_PREFIX}.{pid}.journal.lock"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# P0-1: single helper for the .draining.<epoch> filename construction.
|
|
391
|
+
# Bug fix — old code did `f"{spool_path}draining.{epoch}"` (missing dot)
|
|
392
|
+
# producing audit-spool.<pid>.jsonldraining.<epoch>; glob sweep failed.
|
|
393
|
+
def _draining_path(spool_path: Path, epoch: str) -> Path:
|
|
394
|
+
"""audit-spool.<pid>.jsonl → audit-spool.<pid>.draining.<epoch>."""
|
|
395
|
+
stem = spool_path.stem # 'audit-spool.<pid>' (strips trailing .jsonl)
|
|
396
|
+
return spool_path.with_name(f"{stem}.draining.{epoch}")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# P2-2: validated _spool_uuid regex (16 lowercase hex chars, secrets.token_hex(8))
|
|
400
|
+
_SPOOL_UUID_RE = re.compile(r"^[0-9a-f]{16}$")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _validate_spool_header_strict(header: Any) -> bool:
|
|
404
|
+
"""Strict 4-field header invariant — same predicate used by Phase 2
|
|
405
|
+
quarantine and `_ensure_spool_header` PID-reuse path.
|
|
406
|
+
|
|
407
|
+
iter5-P1 closure: factored out so writer-side reuse cannot accept a
|
|
408
|
+
header that the drainer will later reject. Mismatch between writer
|
|
409
|
+
and drainer validation = silent data loss for events appended after
|
|
410
|
+
the lax reuse but before Phase 2 quarantine.
|
|
411
|
+
|
|
412
|
+
Returns True only when ALL of:
|
|
413
|
+
- dict with `_spool_header: True`
|
|
414
|
+
- `_spool_uuid: str` of length 16 and matching `^[0-9a-f]{16}$`
|
|
415
|
+
- `_pid: int`
|
|
416
|
+
- `_created_wall_ns: int`
|
|
417
|
+
- `_created_monotonic_ns: int`
|
|
418
|
+
"""
|
|
419
|
+
if not isinstance(header, dict):
|
|
420
|
+
return False
|
|
421
|
+
if header.get("_spool_header") is not True:
|
|
422
|
+
return False
|
|
423
|
+
spool_uuid = header.get("_spool_uuid")
|
|
424
|
+
if (not isinstance(spool_uuid, str)
|
|
425
|
+
or len(spool_uuid) != _SPOOL_UUID_HEX_LEN
|
|
426
|
+
or _SPOOL_UUID_RE.match(spool_uuid) is None):
|
|
427
|
+
return False
|
|
428
|
+
if not isinstance(header.get("_pid"), int):
|
|
429
|
+
return False
|
|
430
|
+
if not isinstance(header.get("_created_wall_ns"), int):
|
|
431
|
+
return False
|
|
432
|
+
if not isinstance(header.get("_created_monotonic_ns"), int):
|
|
433
|
+
return False
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _recover_ordinal_counter(spool_path: Path) -> int:
|
|
438
|
+
"""Return ordinal_within_file counter from existing spool body.
|
|
439
|
+
|
|
440
|
+
iter4-P1-1: replaces prior `\n`-counting approach. A complete body
|
|
441
|
+
line WITHOUT trailing `\n` (writer fsync'd then crashed before
|
|
442
|
+
terminator) was previously counted as zero entries → ordinal 0
|
|
443
|
+
re-issued → Phase 3 monotonicity quarantine. Now: parse each line
|
|
444
|
+
(terminated or not), return max(ordinal)+1. Returns 0 on missing/
|
|
445
|
+
empty/header-only/no-valid-ordinals.
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
with open(str(spool_path), "rb") as f:
|
|
449
|
+
content = f.read()
|
|
450
|
+
except OSError:
|
|
451
|
+
return 0
|
|
452
|
+
if b"\n" not in content:
|
|
453
|
+
return 0
|
|
454
|
+
_header, body = content.split(b"\n", 1)
|
|
455
|
+
if not body:
|
|
456
|
+
return 0
|
|
457
|
+
max_ord = -1
|
|
458
|
+
for line_bytes in body.split(b"\n"):
|
|
459
|
+
if not line_bytes.strip():
|
|
460
|
+
continue
|
|
461
|
+
try:
|
|
462
|
+
entry = json.loads(line_bytes)
|
|
463
|
+
except (json.JSONDecodeError, ValueError):
|
|
464
|
+
continue
|
|
465
|
+
if not isinstance(entry, dict):
|
|
466
|
+
continue
|
|
467
|
+
o = entry.get("ordinal_within_file")
|
|
468
|
+
if isinstance(o, int) and o > max_ord:
|
|
469
|
+
max_ord = o
|
|
470
|
+
return max_ord + 1 if max_ord >= 0 else 0
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class _SpoolHeaderUnrecoverable(OSError):
|
|
474
|
+
"""Raised when an existing spool has an invalid header AND quarantine
|
|
475
|
+
rename fails. Caller (`spool_append`) MUST abort fail-open — minting
|
|
476
|
+
a fresh header over the still-present corrupt file would silently
|
|
477
|
+
lose the new event (Phase 2 quarantines the whole file later).
|
|
478
|
+
iter6-P1 closure.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _quarantine_corrupt_active_spool(spool_path: Path, pid: int) -> bool:
|
|
483
|
+
"""Rename malformed-header active spool aside; forensic emit.
|
|
484
|
+
|
|
485
|
+
iter4-P1-2: caller MUST hold `_spool_flock_path(pid)`. We must NOT
|
|
486
|
+
call drain_now() here (Phase 2 re-acquires this same flock →
|
|
487
|
+
self-timeout). _forensic() has its own re-entry guard so the emit
|
|
488
|
+
path won't recurse into spool_append.
|
|
489
|
+
|
|
490
|
+
iter6-P1: returns True on successful rename, False on rename failure.
|
|
491
|
+
Caller MUST honor False by aborting the current append fail-open
|
|
492
|
+
(NOT minting a fresh header over the still-present corrupt file —
|
|
493
|
+
that would let new events land behind a header Phase 2 will quarantine).
|
|
494
|
+
"""
|
|
495
|
+
epoch = secrets.token_hex(4)
|
|
496
|
+
corrupt_path = spool_path.with_name(
|
|
497
|
+
f"{spool_path.stem}.corrupt-header.{epoch}"
|
|
498
|
+
)
|
|
499
|
+
try:
|
|
500
|
+
os.rename(str(spool_path), str(corrupt_path))
|
|
501
|
+
except OSError as e:
|
|
502
|
+
_breadcrumb(
|
|
503
|
+
f"quarantine_corrupt_active_spool rename failed pid={pid}: "
|
|
504
|
+
f"{type(e).__name__}: {e}"
|
|
505
|
+
)
|
|
506
|
+
return False
|
|
507
|
+
_forensic("audit_spool_tamper_detected", {
|
|
508
|
+
"mismatch_kind": "malformed_active_spool_header",
|
|
509
|
+
"spool_pid": pid,
|
|
510
|
+
"corrupt_path": str(corrupt_path),
|
|
511
|
+
"drain_epoch": epoch,
|
|
512
|
+
})
|
|
513
|
+
return True
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
# Fail-open breadcrumb
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _breadcrumb(message: str) -> None:
|
|
522
|
+
"""Append a one-line forensic crumb; NEVER raise."""
|
|
523
|
+
try:
|
|
524
|
+
p = _errors_path()
|
|
525
|
+
p.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
526
|
+
ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
527
|
+
with p.open("a", encoding="utf-8") as f:
|
|
528
|
+
f.write(f"{ts} spool_writer: {message}\n")
|
|
529
|
+
except Exception: # pragma: no cover
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
# Kill-switch + exit-handler state
|
|
535
|
+
# ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def is_sync_mode() -> bool:
|
|
539
|
+
"""CEO_AUDIT_SYNC_MODE=1 reverts to pre-Wave-A synchronous behavior."""
|
|
540
|
+
return os.environ.get("CEO_AUDIT_SYNC_MODE", "") == "1"
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
_EXIT_HANDLER_INSTALLED = False
|
|
544
|
+
_PREV_SIGTERM_HANDLER: Any = None
|
|
545
|
+
_PREV_SIGINT_HANDLER: Any = None
|
|
546
|
+
|
|
547
|
+
# Per-PID spool header cache — populated on first append; mirrors what the
|
|
548
|
+
# spool file's header line carries. We keep an in-process counter for the
|
|
549
|
+
# strictly-monotonic ordinal_within_file.
|
|
550
|
+
_SPOOL_HEADER_CACHE: Dict[int, Dict[str, Any]] = {}
|
|
551
|
+
_ORDINAL_COUNTER: Dict[int, int] = {}
|
|
552
|
+
|
|
553
|
+
# Callback wired by audit_emit so we can emit forensic events without
|
|
554
|
+
# importing audit_emit (circular dep). audit_emit.install() passes a
|
|
555
|
+
# function with signature (action: str, fields: Dict[str, Any]) -> None.
|
|
556
|
+
_FORENSIC_EMIT: Optional[Callable[[str, Dict[str, Any]], None]] = None
|
|
557
|
+
|
|
558
|
+
# P1-4 reentrancy guard: prevent the cycle
|
|
559
|
+
# spool_append → drain → _forensic → emit_generic → spool_append → ...
|
|
560
|
+
# Module-level flag set during _forensic() execution; if re-entered, write
|
|
561
|
+
# a breadcrumb and bail (no spool, no canonical).
|
|
562
|
+
_IN_FORENSIC_EMIT = False
|
|
563
|
+
|
|
564
|
+
# P1-1 reentrancy guard: prevent signal handler calling drain_now() from
|
|
565
|
+
# deadlocking against a writer mid-spool_append in the same PID. The signal
|
|
566
|
+
# handler checks this and bails (the partial spool entry is picked up by
|
|
567
|
+
# next-session reconciliation).
|
|
568
|
+
_IN_SPOOL_APPEND = False
|
|
569
|
+
|
|
570
|
+
# PLAN-094-FOLLOWUP Wave A.3-fail-open (option B) — module-level durability
|
|
571
|
+
# indicator for the last `spool_append()` call. audit_emit wire-in checks
|
|
572
|
+
# `last_append_succeeded()` after each call; on False, falls through to the
|
|
573
|
+
# sync canonical write path. Preserves the existing fail-open invariant
|
|
574
|
+
# (`spool_append` never raises to caller) while restoring durability defense
|
|
575
|
+
# depth on per-PID spool flock alloc / encode / OSError / unexpected paths.
|
|
576
|
+
_LAST_APPEND_SUCCEEDED = True
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def last_append_succeeded() -> bool:
|
|
580
|
+
"""Return True if the most recent spool_append() persisted durably.
|
|
581
|
+
|
|
582
|
+
audit_emit wire-in uses this to decide whether to fall back to the
|
|
583
|
+
sync canonical write path (False = silent-drop on spool side; sync
|
|
584
|
+
is now responsible for durability of THIS event).
|
|
585
|
+
"""
|
|
586
|
+
return _LAST_APPEND_SUCCEEDED
|
|
587
|
+
|
|
588
|
+
# P1-2 journal buffer state (per-PID amortized fsync).
|
|
589
|
+
# Buffer envelopes in memory; flush + fsync at: drain boundary, signal,
|
|
590
|
+
# atexit, or every _JOURNAL_FLUSH_EVERY writes. ADR-055-AMEND-1 §3 line 96:
|
|
591
|
+
# "journal fsync every 100ms OR 10 emits".
|
|
592
|
+
_JOURNAL_FLUSH_EVERY = 10
|
|
593
|
+
_JOURNAL_BUFFER: Dict[int, List[bytes]] = {}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def set_forensic_emitter(emit_fn: Callable[[str, Dict[str, Any]], None]) -> None:
|
|
597
|
+
"""Wire the forensic emit callback (called by audit_emit on import)."""
|
|
598
|
+
global _FORENSIC_EMIT
|
|
599
|
+
_FORENSIC_EMIT = emit_fn
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _forensic(action: str, fields: Dict[str, Any]) -> None:
|
|
603
|
+
"""Emit a forensic audit event via wired callback; swallow errors.
|
|
604
|
+
|
|
605
|
+
P1-4: recursion guard — if we're already inside a _forensic() emit,
|
|
606
|
+
write a breadcrumb and return without re-entering spool_append.
|
|
607
|
+
"""
|
|
608
|
+
global _IN_FORENSIC_EMIT
|
|
609
|
+
cb = _FORENSIC_EMIT
|
|
610
|
+
if cb is None:
|
|
611
|
+
return
|
|
612
|
+
if _IN_FORENSIC_EMIT:
|
|
613
|
+
_breadcrumb(f"forensic re-entry blocked: {action}")
|
|
614
|
+
return
|
|
615
|
+
_IN_FORENSIC_EMIT = True
|
|
616
|
+
try:
|
|
617
|
+
cb(action, fields)
|
|
618
|
+
except Exception as e: # pragma: no cover
|
|
619
|
+
_breadcrumb(f"forensic emit failed: {action}: {type(e).__name__}: {e}")
|
|
620
|
+
finally:
|
|
621
|
+
_IN_FORENSIC_EMIT = False
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
# Spool header / append (Phase 0 — writer-side)
|
|
626
|
+
# ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _origin_for_new_spool() -> str:
|
|
630
|
+
"""PLAN-119 WS-D1 — write-time origin stamp for a freshly-minted spool header.
|
|
631
|
+
|
|
632
|
+
Returns ``"test"`` when a test signal is present AT WRITE TIME
|
|
633
|
+
(``CEO_TEST_HARNESS=1`` or ``PYTEST_CURRENT_TEST`` set), else ``"live"``.
|
|
634
|
+
The writer knows the truth; the drainer cannot infer it later, so the stamp
|
|
635
|
+
is minted ONCE per spool (header) and is sticky to that PID's file. A real
|
|
636
|
+
session writes ``"live"`` and is never quarantined; legacy spool with no
|
|
637
|
+
``_origin`` defaults to ``"live"`` at drain time (fail-safe toward never
|
|
638
|
+
dropping a real event).
|
|
639
|
+
"""
|
|
640
|
+
if (os.environ.get("CEO_TEST_HARNESS") == "1"
|
|
641
|
+
or os.environ.get("PYTEST_CURRENT_TEST")):
|
|
642
|
+
return "test"
|
|
643
|
+
return "live"
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _ensure_spool_header(pid: int, _retry_depth: int = 0) -> Dict[str, Any]:
|
|
647
|
+
"""Return the header dict for the current spool, creating it if absent.
|
|
648
|
+
|
|
649
|
+
Header is written exactly once per spool file (first line). Fresh
|
|
650
|
+
_spool_uuid minted on creation so body[i].spool_uuid sentinel matches.
|
|
651
|
+
|
|
652
|
+
iter2-P1-4: existing nonempty spool with empty header cache (PID reuse,
|
|
653
|
+
module reload, pytest reset) → parse existing header + reuse uuid;
|
|
654
|
+
minting a new one would produce header_body_uuid_mismatch quarantine.
|
|
655
|
+
iter4-P1-1: ordinal recovery via body-line parse (max(ordinal)+1), NOT
|
|
656
|
+
`\n` count — robust against unterminated final line from crashed writer.
|
|
657
|
+
iter4-P1-2: malformed-header → quarantine active spool via direct
|
|
658
|
+
rename (NOT drain_now() — Phase 2 self-deadlocks on our flock) +
|
|
659
|
+
fresh-mint retry (bounded depth=1).
|
|
660
|
+
"""
|
|
661
|
+
header = _SPOOL_HEADER_CACHE.get(pid)
|
|
662
|
+
spool_p = _spool_path(pid)
|
|
663
|
+
|
|
664
|
+
if header is not None and spool_p.exists():
|
|
665
|
+
return header
|
|
666
|
+
|
|
667
|
+
# iter2-P1-4: existing nonempty spool with no in-process header cache.
|
|
668
|
+
# Read the existing header instead of minting a colliding new one.
|
|
669
|
+
# iter5-P1: header reuse MUST apply the same strict 4-field validation
|
|
670
|
+
# that Phase 2 applies (_validate_spool_header_strict). Accepting a
|
|
671
|
+
# semantically incomplete header here lets new appends ride a file
|
|
672
|
+
# that Phase 2 will later quarantine → silent loss of the new events.
|
|
673
|
+
if spool_p.exists():
|
|
674
|
+
try:
|
|
675
|
+
st = spool_p.stat()
|
|
676
|
+
except OSError:
|
|
677
|
+
st = None
|
|
678
|
+
if st is not None and st.st_size > 0:
|
|
679
|
+
header_parsed_ok = False
|
|
680
|
+
try:
|
|
681
|
+
with spool_p.open("rb") as fh:
|
|
682
|
+
first = fh.readline()
|
|
683
|
+
existing: Any = json.loads(first.decode("utf-8"))
|
|
684
|
+
if _validate_spool_header_strict(existing):
|
|
685
|
+
_SPOOL_HEADER_CACHE[pid] = existing
|
|
686
|
+
# iter4-P1-1: ordinal recovery via body parse (NOT
|
|
687
|
+
# `\n`-count) — robust against unterminated final line.
|
|
688
|
+
_ORDINAL_COUNTER[pid] = _recover_ordinal_counter(spool_p)
|
|
689
|
+
header_parsed_ok = True
|
|
690
|
+
return existing
|
|
691
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
692
|
+
_breadcrumb(
|
|
693
|
+
f"existing spool header unparsable pid={pid}: "
|
|
694
|
+
f"{type(e).__name__}: {e}"
|
|
695
|
+
)
|
|
696
|
+
if not header_parsed_ok:
|
|
697
|
+
# iter4-P1-2: do NOT call drain_now from inside the spool
|
|
698
|
+
# flock (Phase 2 re-acquires this flock → self-timeout →
|
|
699
|
+
# bail → caller appends to malformed spool → quarantine
|
|
700
|
+
# silently loses the new event). Quarantine the file
|
|
701
|
+
# directly (rename to `.corrupt-header.<epoch>`) and mint
|
|
702
|
+
# a fresh header. Bounded retry depth = 1.
|
|
703
|
+
#
|
|
704
|
+
# iter6-P1: if quarantine FAILS (rename OSError), we MUST
|
|
705
|
+
# NOT mint over the still-present corrupt file — that
|
|
706
|
+
# lets new events land behind a header Phase 2 will
|
|
707
|
+
# quarantine (silent data loss). Raise a specific
|
|
708
|
+
# exception so caller (`spool_append`) aborts fail-open.
|
|
709
|
+
quarantined = _quarantine_corrupt_active_spool(spool_p, pid)
|
|
710
|
+
# Drop any stale in-memory state for this pid.
|
|
711
|
+
_SPOOL_HEADER_CACHE.pop(pid, None)
|
|
712
|
+
_ORDINAL_COUNTER.pop(pid, None)
|
|
713
|
+
if not quarantined:
|
|
714
|
+
if _retry_depth >= 1:
|
|
715
|
+
# iter6-P1: exhausted retry AND quarantine still
|
|
716
|
+
# failing → abort current append fail-open.
|
|
717
|
+
# spool_append's outer try/except catches OSError
|
|
718
|
+
# and breadcrumbs. The corrupt file remains on
|
|
719
|
+
# disk for operator forensic recovery; Phase 2
|
|
720
|
+
# may eventually quarantine via its own path
|
|
721
|
+
# (header still fails _validate_spool_header_strict).
|
|
722
|
+
raise _SpoolHeaderUnrecoverable(
|
|
723
|
+
f"pid={pid}: quarantine of corrupt active spool "
|
|
724
|
+
f"failed after {_retry_depth + 1} attempt(s); "
|
|
725
|
+
f"aborting append fail-open"
|
|
726
|
+
)
|
|
727
|
+
return _ensure_spool_header(pid, _retry_depth=_retry_depth + 1)
|
|
728
|
+
# Quarantine succeeded → fall through to fresh-mint path.
|
|
729
|
+
|
|
730
|
+
header = {
|
|
731
|
+
"_spool_header": True,
|
|
732
|
+
"_spool_uuid": secrets.token_hex(8),
|
|
733
|
+
"_pid": pid,
|
|
734
|
+
"_created_wall_ns": time.time_ns(),
|
|
735
|
+
"_created_monotonic_ns": time.monotonic_ns(),
|
|
736
|
+
"_version": SPOOL_HEADER_VERSION,
|
|
737
|
+
# PLAN-119 WS-D1 — sticky write-time origin stamp (minted once per PID
|
|
738
|
+
# file). The drainer refuses to drain "test" spool into the LIVE chain.
|
|
739
|
+
"_origin": _origin_for_new_spool(),
|
|
740
|
+
}
|
|
741
|
+
spool_p.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
742
|
+
needs_header = (not spool_p.exists()) or spool_p.stat().st_size == 0
|
|
743
|
+
if needs_header:
|
|
744
|
+
line = json.dumps(header, separators=(",", ":"), ensure_ascii=False) + "\n"
|
|
745
|
+
# O_CREAT|O_WRONLY|O_APPEND with 0600 perms — owner-only.
|
|
746
|
+
fd = os.open(str(spool_p), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
|
|
747
|
+
try:
|
|
748
|
+
os.write(fd, line.encode("utf-8"))
|
|
749
|
+
os.fsync(fd)
|
|
750
|
+
finally:
|
|
751
|
+
os.close(fd)
|
|
752
|
+
_SPOOL_HEADER_CACHE[pid] = header
|
|
753
|
+
_ORDINAL_COUNTER.setdefault(pid, 0)
|
|
754
|
+
return header
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _next_ordinal(pid: int) -> int:
|
|
758
|
+
"""Return the next strictly-monotonic ordinal for a given pid."""
|
|
759
|
+
n = _ORDINAL_COUNTER.get(pid, 0)
|
|
760
|
+
_ORDINAL_COUNTER[pid] = n + 1
|
|
761
|
+
return n
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _ensure_spool_ready_for_append(pid: int) -> None:
|
|
765
|
+
"""iter3-P1-1: isolate any unterminated final line before appending.
|
|
766
|
+
|
|
767
|
+
If a prior writer died mid-write leaving no trailing newline, the next
|
|
768
|
+
append concatenates onto the partial line and the new event is lost.
|
|
769
|
+
Append a separator newline under the held spool flock to isolate the
|
|
770
|
+
partial fragment (Phase 3 will discard it). No-op when missing/empty/
|
|
771
|
+
already-newline-terminated. Caller MUST hold _spool_flock_path(pid).
|
|
772
|
+
Fail-open: breadcrumb on any OSError.
|
|
773
|
+
"""
|
|
774
|
+
spool_p = _spool_path(pid)
|
|
775
|
+
try:
|
|
776
|
+
if not spool_p.exists():
|
|
777
|
+
return
|
|
778
|
+
st = spool_p.stat()
|
|
779
|
+
if st.st_size == 0:
|
|
780
|
+
return
|
|
781
|
+
with open(str(spool_p), "rb") as f:
|
|
782
|
+
f.seek(-1, os.SEEK_END)
|
|
783
|
+
last = f.read(1)
|
|
784
|
+
if last == b"\n":
|
|
785
|
+
return
|
|
786
|
+
fd = os.open(str(spool_p), os.O_WRONLY | os.O_APPEND, 0o600)
|
|
787
|
+
try:
|
|
788
|
+
os.write(fd, b"\n")
|
|
789
|
+
os.fsync(fd)
|
|
790
|
+
finally:
|
|
791
|
+
os.close(fd)
|
|
792
|
+
_breadcrumb(
|
|
793
|
+
f"spool partial-final-line isolated pid={pid} size={st.st_size}"
|
|
794
|
+
)
|
|
795
|
+
except OSError as e: # pragma: no cover
|
|
796
|
+
_breadcrumb(
|
|
797
|
+
f"spool ready-for-append check failed pid={pid}: "
|
|
798
|
+
f"{type(e).__name__}: {e}"
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _sha256_of_canonical_json(entry_clean: Dict[str, Any]) -> str:
|
|
803
|
+
"""sha256 over the canonical_json bytes (deterministic idempotence marker).
|
|
804
|
+
|
|
805
|
+
entry_clean MUST exclude internal _drain_* metadata + hmac field. The
|
|
806
|
+
canonical_json encoder is the same one ADR-055 HMAC chain uses, so the
|
|
807
|
+
digest is reproducible across re-drains.
|
|
808
|
+
"""
|
|
809
|
+
if _canonical_json is None:
|
|
810
|
+
raw = json.dumps(entry_clean, sort_keys=True, separators=(",", ":"),
|
|
811
|
+
ensure_ascii=False).encode("utf-8")
|
|
812
|
+
else:
|
|
813
|
+
raw = _canonical_json.encode(entry_clean)
|
|
814
|
+
return hashlib.sha256(raw).hexdigest()
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _write_journal_envelope(
|
|
818
|
+
pid: int,
|
|
819
|
+
record_id: str,
|
|
820
|
+
spool_uuid: str,
|
|
821
|
+
ordinal: int,
|
|
822
|
+
sha256_of_line: str,
|
|
823
|
+
op: str,
|
|
824
|
+
drain_epoch: Optional[str] = None,
|
|
825
|
+
) -> None:
|
|
826
|
+
"""Buffer + amortized-fsync journal envelope. Best-effort fail-open.
|
|
827
|
+
|
|
828
|
+
P1-2: NOT a hot-path fsync. Envelopes are buffered in
|
|
829
|
+
_JOURNAL_BUFFER[pid]; flushed + fsync'd at drain boundary, signal
|
|
830
|
+
handler, atexit, or every _JOURNAL_FLUSH_EVERY writes (per ADR-055-
|
|
831
|
+
AMEND-1 §3 line 96: "journal fsync every 100ms OR 10 emits").
|
|
832
|
+
"""
|
|
833
|
+
env: Dict[str, Any] = {
|
|
834
|
+
"record_id": record_id,
|
|
835
|
+
"spool_uuid": spool_uuid,
|
|
836
|
+
"ordinal_within_file": ordinal,
|
|
837
|
+
"sha256_of_line": sha256_of_line,
|
|
838
|
+
"op": op,
|
|
839
|
+
"wall_ns": time.time_ns(),
|
|
840
|
+
}
|
|
841
|
+
if drain_epoch is not None:
|
|
842
|
+
env["drain_epoch_at_commit"] = drain_epoch
|
|
843
|
+
try:
|
|
844
|
+
line = (json.dumps(env, separators=(",", ":"), ensure_ascii=False)
|
|
845
|
+
+ "\n").encode("utf-8")
|
|
846
|
+
except (TypeError, ValueError) as e:
|
|
847
|
+
_breadcrumb(f"journal encode failed: {type(e).__name__}: {e}")
|
|
848
|
+
return
|
|
849
|
+
buf = _JOURNAL_BUFFER.setdefault(pid, [])
|
|
850
|
+
buf.append(line)
|
|
851
|
+
if len(buf) >= _JOURNAL_FLUSH_EVERY:
|
|
852
|
+
_flush_journal_buffer(pid)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _flush_journal_buffer(pid: int) -> None:
|
|
856
|
+
"""Flush buffered journal envelopes for PID with a single fsync.
|
|
857
|
+
|
|
858
|
+
P1-2: invoked at drain boundary, signal handler, atexit, or when
|
|
859
|
+
buffer hits _JOURNAL_FLUSH_EVERY. Best-effort fail-open. Acquires
|
|
860
|
+
the per-PID journal flock around the append+fsync window.
|
|
861
|
+
"""
|
|
862
|
+
buf = _JOURNAL_BUFFER.get(pid)
|
|
863
|
+
if not buf:
|
|
864
|
+
return
|
|
865
|
+
p = _journal_path(pid)
|
|
866
|
+
try:
|
|
867
|
+
p.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
868
|
+
except OSError as e:
|
|
869
|
+
_breadcrumb(f"journal mkdir failed: {type(e).__name__}: {e}")
|
|
870
|
+
return
|
|
871
|
+
payload = b"".join(buf)
|
|
872
|
+
try:
|
|
873
|
+
with FileLock(_journal_flock_path(pid), timeout=SPOOL_LOCK_TIMEOUT):
|
|
874
|
+
fd = os.open(
|
|
875
|
+
str(p), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600,
|
|
876
|
+
)
|
|
877
|
+
try:
|
|
878
|
+
os.write(fd, payload)
|
|
879
|
+
os.fsync(fd)
|
|
880
|
+
finally:
|
|
881
|
+
os.close(fd)
|
|
882
|
+
# Only clear on successful write; on failure the buffer retains
|
|
883
|
+
# the envelopes for the next flush attempt (best-effort durability).
|
|
884
|
+
_JOURNAL_BUFFER[pid] = []
|
|
885
|
+
except FileLockTimeout:
|
|
886
|
+
_breadcrumb(f"journal flock timeout pid={pid}")
|
|
887
|
+
except OSError as e:
|
|
888
|
+
_breadcrumb(f"journal flush failed: {type(e).__name__}: {e}")
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def spool_append(entry: Dict[str, Any]) -> None:
|
|
892
|
+
"""Write entry to per-PID spool with 4-tuple metadata + fsync.
|
|
893
|
+
|
|
894
|
+
Stamps wall_ns, pid, spool_uuid, ordinal_within_file, record_id.
|
|
895
|
+
Wraps spool write in begin/commit journal envelopes for crash
|
|
896
|
+
forensics. Fail-open: any infra error writes a breadcrumb and
|
|
897
|
+
returns silently.
|
|
898
|
+
|
|
899
|
+
P1-1: acquires per-PID spool flock for the header-ensure + body
|
|
900
|
+
append window so same-PID concurrent emit and signal-triggered drain
|
|
901
|
+
don't race. Module-level _IN_SPOOL_APPEND latch lets signal handler
|
|
902
|
+
detect a writer in progress and bail.
|
|
903
|
+
|
|
904
|
+
P1-3: stamps `record_id` onto the spool entry so the drain Phase 5
|
|
905
|
+
`op:"drained"` envelope can carry it for session-start reconciliation.
|
|
906
|
+
"""
|
|
907
|
+
global _IN_SPOOL_APPEND, _LAST_APPEND_SUCCEEDED
|
|
908
|
+
_LAST_APPEND_SUCCEEDED = False
|
|
909
|
+
try:
|
|
910
|
+
pid = os.getpid()
|
|
911
|
+
record_id = uuid.uuid4().hex
|
|
912
|
+
spool_p = _spool_path(pid)
|
|
913
|
+
# Per-PID spool flock — ADR-055-AMEND-1 §4 Phase 1: writers acquire
|
|
914
|
+
# only their own spool's flock + journal's flock (never canonical).
|
|
915
|
+
try:
|
|
916
|
+
spool_flock = FileLock(
|
|
917
|
+
_spool_flock_path(pid), timeout=SPOOL_LOCK_TIMEOUT,
|
|
918
|
+
)
|
|
919
|
+
except OSError as e:
|
|
920
|
+
_breadcrumb(f"spool flock alloc failed: {type(e).__name__}: {e}")
|
|
921
|
+
return
|
|
922
|
+
try:
|
|
923
|
+
with spool_flock:
|
|
924
|
+
_IN_SPOOL_APPEND = True
|
|
925
|
+
try:
|
|
926
|
+
header = _ensure_spool_header(pid)
|
|
927
|
+
# iter3-P1-1: isolate any unterminated final line from a
|
|
928
|
+
# prior crashed writer BEFORE we mint our ordinal so the
|
|
929
|
+
# new entry can't be silently lost via concatenation.
|
|
930
|
+
_ensure_spool_ready_for_append(pid)
|
|
931
|
+
spool_uuid = header["_spool_uuid"]
|
|
932
|
+
ordinal = _next_ordinal(pid)
|
|
933
|
+
|
|
934
|
+
stamped: Dict[str, Any] = dict(entry)
|
|
935
|
+
stamped.setdefault("wall_ns", time.time_ns())
|
|
936
|
+
stamped["pid"] = pid
|
|
937
|
+
stamped["spool_uuid"] = spool_uuid
|
|
938
|
+
stamped["ordinal_within_file"] = ordinal
|
|
939
|
+
# P1-3: carry record_id through the spool so Phase 5
|
|
940
|
+
# can emit op:"drained" with matching id.
|
|
941
|
+
stamped["record_id"] = record_id
|
|
942
|
+
|
|
943
|
+
try:
|
|
944
|
+
line_bytes = json.dumps(
|
|
945
|
+
stamped, separators=(",", ":"),
|
|
946
|
+
ensure_ascii=False,
|
|
947
|
+
).encode("utf-8") + b"\n"
|
|
948
|
+
except (TypeError, ValueError) as e:
|
|
949
|
+
_breadcrumb(
|
|
950
|
+
f"spool encode failed: {type(e).__name__}: {e}"
|
|
951
|
+
)
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
sha = hashlib.sha256(line_bytes).hexdigest()
|
|
955
|
+
|
|
956
|
+
_write_journal_envelope(
|
|
957
|
+
pid, record_id, spool_uuid, ordinal, sha, "begin",
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
try:
|
|
961
|
+
fd = os.open(
|
|
962
|
+
str(spool_p),
|
|
963
|
+
os.O_WRONLY | os.O_CREAT | os.O_APPEND,
|
|
964
|
+
0o600,
|
|
965
|
+
)
|
|
966
|
+
try:
|
|
967
|
+
os.write(fd, line_bytes)
|
|
968
|
+
os.fsync(fd)
|
|
969
|
+
finally:
|
|
970
|
+
os.close(fd)
|
|
971
|
+
except OSError as e:
|
|
972
|
+
_breadcrumb(
|
|
973
|
+
f"spool append failed: {type(e).__name__}: {e}"
|
|
974
|
+
)
|
|
975
|
+
return
|
|
976
|
+
|
|
977
|
+
_write_journal_envelope(
|
|
978
|
+
pid, record_id, spool_uuid, ordinal, sha, "commit",
|
|
979
|
+
)
|
|
980
|
+
_LAST_APPEND_SUCCEEDED = True
|
|
981
|
+
finally:
|
|
982
|
+
_IN_SPOOL_APPEND = False
|
|
983
|
+
except FileLockTimeout:
|
|
984
|
+
_IN_SPOOL_APPEND = False
|
|
985
|
+
_breadcrumb(f"spool flock timeout pid={pid}")
|
|
986
|
+
return
|
|
987
|
+
except Exception as e: # fail-open invariant
|
|
988
|
+
_IN_SPOOL_APPEND = False
|
|
989
|
+
_breadcrumb(f"spool_append unexpected: {type(e).__name__}: {e}")
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
# ---------------------------------------------------------------------------
|
|
993
|
+
# Drain trigger
|
|
994
|
+
# ---------------------------------------------------------------------------
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def should_drain() -> bool:
|
|
998
|
+
"""Return True if current spool meets event-count or staleness trigger.
|
|
999
|
+
|
|
1000
|
+
P2-1: event-count trigger uses an actual newline-count over the spool
|
|
1001
|
+
body (capped at DRAIN_TRIGGER_SIZE + 2 to keep the probe cheap), NOT
|
|
1002
|
+
a byte-size proxy. Header is 1 line; we trigger when body lines >=
|
|
1003
|
+
DRAIN_TRIGGER_SIZE.
|
|
1004
|
+
"""
|
|
1005
|
+
try:
|
|
1006
|
+
pid = os.getpid()
|
|
1007
|
+
p = _spool_path(pid)
|
|
1008
|
+
if not p.exists():
|
|
1009
|
+
return False
|
|
1010
|
+
st = p.stat()
|
|
1011
|
+
if st.st_size == 0:
|
|
1012
|
+
return False
|
|
1013
|
+
# Staleness trigger first (cheap)
|
|
1014
|
+
age_ms = (time.time() - st.st_mtime) * 1000.0
|
|
1015
|
+
if age_ms > DRAIN_TRIGGER_MTIME_MS:
|
|
1016
|
+
return True
|
|
1017
|
+
# P2-1: honest newline count, capped at DRAIN_TRIGGER_SIZE + 2
|
|
1018
|
+
# (1 for header + 1 to early-exit once we cross the threshold).
|
|
1019
|
+
cap = DRAIN_TRIGGER_SIZE + 2
|
|
1020
|
+
count = 0
|
|
1021
|
+
try:
|
|
1022
|
+
with p.open("rb") as f:
|
|
1023
|
+
while count < cap:
|
|
1024
|
+
chunk = f.read(8192)
|
|
1025
|
+
if not chunk:
|
|
1026
|
+
break
|
|
1027
|
+
count += chunk.count(b"\n")
|
|
1028
|
+
if count >= cap:
|
|
1029
|
+
break
|
|
1030
|
+
except OSError:
|
|
1031
|
+
return False
|
|
1032
|
+
# Body lines = count - 1 (header is line 1). Trigger when
|
|
1033
|
+
# body_lines >= DRAIN_TRIGGER_SIZE → count >= DRAIN_TRIGGER_SIZE+1.
|
|
1034
|
+
return count >= (DRAIN_TRIGGER_SIZE + 1)
|
|
1035
|
+
except Exception:
|
|
1036
|
+
return False
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
# ---------------------------------------------------------------------------
|
|
1040
|
+
# Tail read of canonical log (Phase 4 prev_hmac + _drain_sha256 set)
|
|
1041
|
+
# ---------------------------------------------------------------------------
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _read_canonical_tail(
|
|
1045
|
+
log_path: Path, k_lines: int
|
|
1046
|
+
) -> Tuple[Optional[bytes], List[Dict[str, Any]]]:
|
|
1047
|
+
"""Return (prev_hmac_bytes_or_None, last_k_entries) from canonical log.
|
|
1048
|
+
|
|
1049
|
+
Reads up to k_lines from EOF using a backward chunked scan. Returns the
|
|
1050
|
+
last entry's hmac (raw bytes) plus the parsed dicts for the last K
|
|
1051
|
+
well-formed JSON lines. Empty / missing log → (None, []).
|
|
1052
|
+
"""
|
|
1053
|
+
if not log_path.exists():
|
|
1054
|
+
return None, []
|
|
1055
|
+
try:
|
|
1056
|
+
size = log_path.stat().st_size
|
|
1057
|
+
except OSError:
|
|
1058
|
+
return None, []
|
|
1059
|
+
if size == 0:
|
|
1060
|
+
return None, []
|
|
1061
|
+
|
|
1062
|
+
chunk = 8192
|
|
1063
|
+
buf = b""
|
|
1064
|
+
lines: List[bytes] = []
|
|
1065
|
+
try:
|
|
1066
|
+
with log_path.open("rb") as f:
|
|
1067
|
+
pos = size
|
|
1068
|
+
while pos > 0 and len(lines) <= k_lines:
|
|
1069
|
+
read = min(chunk, pos)
|
|
1070
|
+
pos -= read
|
|
1071
|
+
f.seek(pos)
|
|
1072
|
+
buf = f.read(read) + buf
|
|
1073
|
+
parts = buf.split(b"\n")
|
|
1074
|
+
# First element may be a partial fragment until we read
|
|
1075
|
+
# further; keep it in buf, take the tail-complete lines.
|
|
1076
|
+
buf = parts[0]
|
|
1077
|
+
# parts[1:] are complete lines (after the first newline).
|
|
1078
|
+
# Reverse so we accumulate newest-first.
|
|
1079
|
+
for ln in reversed(parts[1:]):
|
|
1080
|
+
if ln:
|
|
1081
|
+
lines.append(ln)
|
|
1082
|
+
if len(lines) >= k_lines:
|
|
1083
|
+
break
|
|
1084
|
+
if pos == 0 and buf:
|
|
1085
|
+
if len(lines) < k_lines:
|
|
1086
|
+
lines.append(buf)
|
|
1087
|
+
except OSError as e:
|
|
1088
|
+
_breadcrumb(f"canonical tail read failed: {type(e).__name__}: {e}")
|
|
1089
|
+
return None, []
|
|
1090
|
+
|
|
1091
|
+
# lines is newest-first; reverse to chronological order
|
|
1092
|
+
lines.reverse()
|
|
1093
|
+
entries: List[Dict[str, Any]] = []
|
|
1094
|
+
for raw in lines[-k_lines:]:
|
|
1095
|
+
try:
|
|
1096
|
+
obj = json.loads(raw.decode("utf-8"))
|
|
1097
|
+
if isinstance(obj, dict):
|
|
1098
|
+
entries.append(obj)
|
|
1099
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1100
|
+
continue
|
|
1101
|
+
|
|
1102
|
+
prev_hmac: Optional[bytes] = None
|
|
1103
|
+
for obj in reversed(entries):
|
|
1104
|
+
hx = obj.get("hmac")
|
|
1105
|
+
if isinstance(hx, str) and len(hx) == 64:
|
|
1106
|
+
try:
|
|
1107
|
+
prev_hmac = bytes.fromhex(hx)
|
|
1108
|
+
break
|
|
1109
|
+
except ValueError:
|
|
1110
|
+
continue
|
|
1111
|
+
return prev_hmac, entries
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
# ---------------------------------------------------------------------------
|
|
1115
|
+
# Phase 2 — sweep + atomic rename + header validation
|
|
1116
|
+
# ---------------------------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
@dataclass(eq=False)
|
|
1120
|
+
class _DrainingFile:
|
|
1121
|
+
"""One file under drain in the current cycle.
|
|
1122
|
+
|
|
1123
|
+
P0-2: declared eq=False so default object identity hash applies —
|
|
1124
|
+
instances are usable as Dict keys without TypeError. (We still key
|
|
1125
|
+
per_file_consumed on str(path) per Option B in the review for
|
|
1126
|
+
clarity; the eq=False guard is defense-in-depth.)
|
|
1127
|
+
|
|
1128
|
+
P2-4: header_raw stores the verbatim bytes (incl. trailing newline)
|
|
1129
|
+
of the header line, so Phase 5 atomic-split writes the byte-identical
|
|
1130
|
+
header to the new .draining.<new_epoch> file (no re-encode drift).
|
|
1131
|
+
|
|
1132
|
+
iter2-P0-3: `quarantined` flag — set by `_quarantine()` after mid-file
|
|
1133
|
+
tamper rename so Phase 5 cleanup SKIPS the entry (the file is already
|
|
1134
|
+
moved aside as .malformed.<epoch>; trying to split would touch a
|
|
1135
|
+
non-existent path or re-sweep the quarantined file).
|
|
1136
|
+
"""
|
|
1137
|
+
|
|
1138
|
+
path: Path
|
|
1139
|
+
pid: int
|
|
1140
|
+
drain_epoch: str
|
|
1141
|
+
header: Optional[Dict[str, Any]] = None
|
|
1142
|
+
header_raw: bytes = b""
|
|
1143
|
+
body_lines: List[bytes] = field(default_factory=list)
|
|
1144
|
+
consumed_count: int = 0
|
|
1145
|
+
quarantined: bool = False
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _is_alive_pid(pid: int) -> bool:
|
|
1149
|
+
"""Return True if a process with PID is alive in current namespace."""
|
|
1150
|
+
if pid <= 0:
|
|
1151
|
+
return False
|
|
1152
|
+
try:
|
|
1153
|
+
os.kill(pid, 0)
|
|
1154
|
+
except ProcessLookupError:
|
|
1155
|
+
return False
|
|
1156
|
+
except PermissionError:
|
|
1157
|
+
return True
|
|
1158
|
+
except OSError:
|
|
1159
|
+
return False
|
|
1160
|
+
return True
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _parse_spool_pid(name: str) -> Optional[int]:
|
|
1164
|
+
"""Extract PID from audit-spool.<pid>.* filename forms."""
|
|
1165
|
+
if not name.startswith(_SPOOL_PREFIX + "."):
|
|
1166
|
+
return None
|
|
1167
|
+
rest = name[len(_SPOOL_PREFIX) + 1:]
|
|
1168
|
+
pid_str, _, _ = rest.partition(".")
|
|
1169
|
+
try:
|
|
1170
|
+
return int(pid_str)
|
|
1171
|
+
except ValueError:
|
|
1172
|
+
return None
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def _phase2_sweep_and_rename(
|
|
1176
|
+
state_dir: Path, drain_epoch: str, our_pid: int
|
|
1177
|
+
) -> Tuple[List[_DrainingFile], bool]:
|
|
1178
|
+
"""Sweep state dir for spool/draining files; atomic-rename into batch.
|
|
1179
|
+
|
|
1180
|
+
Returns (list of draining files to process, in_recovery_mode_flag).
|
|
1181
|
+
in_recovery_mode is True iff any pre-existing .draining.* was swept
|
|
1182
|
+
(severity branching for unexpected_skip — ADR-055-AMEND-1 Phase 4).
|
|
1183
|
+
"""
|
|
1184
|
+
files: List[_DrainingFile] = []
|
|
1185
|
+
in_recovery = False
|
|
1186
|
+
|
|
1187
|
+
try:
|
|
1188
|
+
names = sorted(os.listdir(str(state_dir)))
|
|
1189
|
+
except OSError:
|
|
1190
|
+
return [], False
|
|
1191
|
+
|
|
1192
|
+
# 1) Pre-existing .draining.* — these are stale from prior crashed drains
|
|
1193
|
+
# iter3-P2-2: wire STALE_SPOOL_TTL_DAYS into the sweep. A .draining file
|
|
1194
|
+
# owned by a dead PID (or even a live one) whose mtime is older than
|
|
1195
|
+
# the TTL is forensically interesting — emit an audit_spool_stale_recovered
|
|
1196
|
+
# advisory BEFORE processing so SOC has trace of the long-deferred
|
|
1197
|
+
# recovery. Processing still proceeds; the TTL is advisory-only.
|
|
1198
|
+
stale_ttl_ns = STALE_SPOOL_TTL_DAYS * 86400 * 1_000_000_000
|
|
1199
|
+
now_ns = time.time_ns()
|
|
1200
|
+
for name in names:
|
|
1201
|
+
if _DRAINING_SUFFIX_TOKEN not in name:
|
|
1202
|
+
continue
|
|
1203
|
+
if not name.startswith(_SPOOL_PREFIX + "."):
|
|
1204
|
+
continue
|
|
1205
|
+
pid = _parse_spool_pid(name)
|
|
1206
|
+
if pid is None:
|
|
1207
|
+
continue
|
|
1208
|
+
# Parse the existing drain_epoch from the suffix; merge in as-is.
|
|
1209
|
+
try:
|
|
1210
|
+
old_epoch = name.rsplit(_DRAINING_SUFFIX_TOKEN, 1)[1]
|
|
1211
|
+
except IndexError:
|
|
1212
|
+
old_epoch = drain_epoch
|
|
1213
|
+
full_path = state_dir / name
|
|
1214
|
+
# iter3-P2-2: stale-recovery advisory if age > TTL.
|
|
1215
|
+
try:
|
|
1216
|
+
st_mtime_ns = full_path.stat().st_mtime_ns
|
|
1217
|
+
file_age_ns = now_ns - st_mtime_ns
|
|
1218
|
+
if file_age_ns > stale_ttl_ns:
|
|
1219
|
+
file_age_hours = file_age_ns // (3600 * 1_000_000_000)
|
|
1220
|
+
_forensic("audit_spool_stale_recovered", {
|
|
1221
|
+
"original_pid": pid,
|
|
1222
|
+
"file_age_hours": int(file_age_hours),
|
|
1223
|
+
"stale_ttl_days": STALE_SPOOL_TTL_DAYS,
|
|
1224
|
+
"drain_epoch": drain_epoch,
|
|
1225
|
+
"prior_drain_epoch": old_epoch,
|
|
1226
|
+
})
|
|
1227
|
+
except OSError:
|
|
1228
|
+
pass
|
|
1229
|
+
files.append(_DrainingFile(
|
|
1230
|
+
path=full_path, pid=pid, drain_epoch=old_epoch,
|
|
1231
|
+
))
|
|
1232
|
+
in_recovery = True
|
|
1233
|
+
|
|
1234
|
+
# 2) Active spool files — atomically rename to .draining.<drain_epoch>.
|
|
1235
|
+
# Skip our own active spool (we hold it open for writes); also skip
|
|
1236
|
+
# spools of live PIDs other than ours when force-drain is not set —
|
|
1237
|
+
# let the owning process drain them. (Conservative: live-PID spools
|
|
1238
|
+
# of OTHER processes will be reaped on next aggregate sweep.)
|
|
1239
|
+
for name in names:
|
|
1240
|
+
if not name.startswith(_SPOOL_PREFIX + "."):
|
|
1241
|
+
continue
|
|
1242
|
+
if not name.endswith(".jsonl"):
|
|
1243
|
+
continue
|
|
1244
|
+
pid = _parse_spool_pid(name)
|
|
1245
|
+
if pid is None:
|
|
1246
|
+
continue
|
|
1247
|
+
src = state_dir / name
|
|
1248
|
+
try:
|
|
1249
|
+
src_stat = src.stat()
|
|
1250
|
+
except OSError:
|
|
1251
|
+
continue
|
|
1252
|
+
if src_stat.st_size == 0:
|
|
1253
|
+
continue
|
|
1254
|
+
# Only rename our own spool OR clearly-dead PID spools. Live-PID
|
|
1255
|
+
# spools belong to peers — leave them for their own drain trigger
|
|
1256
|
+
# to avoid stealing in-flight writes (writer flock would block us
|
|
1257
|
+
# anyway; explicit check is the cheap path).
|
|
1258
|
+
if pid != our_pid and _is_alive_pid(pid):
|
|
1259
|
+
continue
|
|
1260
|
+
# iter4-P2-1: stale-recovery advisory ALSO covers active spool files
|
|
1261
|
+
# whose owning PID is dead AND mtime is older than TTL (ADR-055-
|
|
1262
|
+
# AMEND-1 §A.4.5 covers both .draining.* AND orphaned active
|
|
1263
|
+
# spools). Emit BEFORE the rename so SOC trace carries the
|
|
1264
|
+
# forensic correlation between the orphan and the drain epoch.
|
|
1265
|
+
if pid != our_pid:
|
|
1266
|
+
try:
|
|
1267
|
+
file_age_ns = now_ns - src_stat.st_mtime_ns
|
|
1268
|
+
if file_age_ns > stale_ttl_ns:
|
|
1269
|
+
file_age_hours = file_age_ns // (3600 * 1_000_000_000)
|
|
1270
|
+
_forensic("audit_spool_stale_recovered", {
|
|
1271
|
+
"original_pid": pid,
|
|
1272
|
+
"file_age_hours": int(file_age_hours),
|
|
1273
|
+
"stale_ttl_days": STALE_SPOOL_TTL_DAYS,
|
|
1274
|
+
"drain_epoch": drain_epoch,
|
|
1275
|
+
"source": "active_spool_orphan",
|
|
1276
|
+
})
|
|
1277
|
+
except OSError:
|
|
1278
|
+
pass
|
|
1279
|
+
# P0-1: use _draining_path helper (was: missing dot before draining)
|
|
1280
|
+
dst = _draining_path(src, drain_epoch)
|
|
1281
|
+
# iter3-P1-2: acquire the per-PID spool flock around rename so a
|
|
1282
|
+
# concurrent writer mid-spool_append in the same PID (e.g. signal-
|
|
1283
|
+
# handler-triggered drain racing the writer's own fd) is forced to
|
|
1284
|
+
# complete before we steal the file. Writers hold the flock for
|
|
1285
|
+
# ~10µs per emit; a short timeout (0.5s) is sufficient. If we time
|
|
1286
|
+
# out, we skip this PID this cycle — the spool gets picked up at
|
|
1287
|
+
# next drain trigger. Either path is correctness-preserving.
|
|
1288
|
+
try:
|
|
1289
|
+
with FileLock(_spool_flock_path(pid), timeout=0.5):
|
|
1290
|
+
# Re-check existence under lock (writer may have unlinked
|
|
1291
|
+
# during contention, or another concurrent drain in our PID
|
|
1292
|
+
# could have renamed it first).
|
|
1293
|
+
try:
|
|
1294
|
+
if not src.exists() or src.stat().st_size == 0:
|
|
1295
|
+
continue
|
|
1296
|
+
except OSError:
|
|
1297
|
+
continue
|
|
1298
|
+
try:
|
|
1299
|
+
os.rename(str(src), str(dst))
|
|
1300
|
+
except OSError as e:
|
|
1301
|
+
_breadcrumb(
|
|
1302
|
+
f"phase2 rename failed for {name}: "
|
|
1303
|
+
f"{type(e).__name__}: {e}"
|
|
1304
|
+
)
|
|
1305
|
+
continue
|
|
1306
|
+
except FileLockTimeout:
|
|
1307
|
+
# Writer is mid-append on this spool; defer to next drain
|
|
1308
|
+
# cycle. The spool size/mtime trigger will catch it.
|
|
1309
|
+
_breadcrumb(
|
|
1310
|
+
f"phase2 spool flock timeout pid={pid} (writer mid-append)"
|
|
1311
|
+
)
|
|
1312
|
+
continue
|
|
1313
|
+
files.append(_DrainingFile(
|
|
1314
|
+
path=dst, pid=pid, drain_epoch=drain_epoch,
|
|
1315
|
+
))
|
|
1316
|
+
# If the renamed-away spool belonged to our pid, reset our header
|
|
1317
|
+
# cache so subsequent spool_append mints a fresh _spool_uuid.
|
|
1318
|
+
if pid == our_pid:
|
|
1319
|
+
_SPOOL_HEADER_CACHE.pop(pid, None)
|
|
1320
|
+
_ORDINAL_COUNTER.pop(pid, None)
|
|
1321
|
+
|
|
1322
|
+
return files, in_recovery
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def _quarantine(
|
|
1326
|
+
file: _DrainingFile, mismatch_kind: str, drain_epoch: str
|
|
1327
|
+
) -> None:
|
|
1328
|
+
"""Rename a malformed/tampered spool to .malformed.<drain_epoch>; emit.
|
|
1329
|
+
|
|
1330
|
+
iter2-P0-3: sets `file.quarantined = True` so Phase 5 cleanup skips the
|
|
1331
|
+
entry (the source .draining.<epoch> no longer exists at its original
|
|
1332
|
+
path — trying to split or unlink would either fail or, worse, re-sweep
|
|
1333
|
+
the renamed .malformed.<epoch> file on a subsequent drain.
|
|
1334
|
+
"""
|
|
1335
|
+
try:
|
|
1336
|
+
new_name = file.path.name.replace(
|
|
1337
|
+
_DRAINING_SUFFIX_TOKEN[1:] + file.drain_epoch,
|
|
1338
|
+
_MALFORMED_SUFFIX_TOKEN[1:] + drain_epoch,
|
|
1339
|
+
1,
|
|
1340
|
+
)
|
|
1341
|
+
new_path = file.path.with_name(new_name)
|
|
1342
|
+
os.rename(str(file.path), str(new_path))
|
|
1343
|
+
except OSError as e:
|
|
1344
|
+
_breadcrumb(f"quarantine rename failed: {type(e).__name__}: {e}")
|
|
1345
|
+
# iter2-P0-3: latch quarantined flag for Phase 5 cleanup skip.
|
|
1346
|
+
file.quarantined = True
|
|
1347
|
+
_forensic("audit_spool_tamper_detected", {
|
|
1348
|
+
"mismatch_kind": mismatch_kind,
|
|
1349
|
+
"spool_pid": file.pid,
|
|
1350
|
+
"drain_epoch": drain_epoch,
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _should_quarantine_test_origin(
|
|
1355
|
+
file: "_DrainingFile", canonical_log_path: Path
|
|
1356
|
+
) -> bool:
|
|
1357
|
+
"""PLAN-119 WS-D1 — True iff this spool is ``_origin:"test"`` AND the
|
|
1358
|
+
canonical drain destination IS the live chain.
|
|
1359
|
+
|
|
1360
|
+
Conjunction-gated (Codex R2 P0-1): a redirected pytest/session tmp dir is
|
|
1361
|
+
NOT the live chain, so a ``"test"`` spool drains NORMALLY there (isolated
|
|
1362
|
+
drain-behavior tests keep working). Fail-SAFE to NO quarantine (Codex R3
|
|
1363
|
+
P1-1) when the live-log snapshot is absent/unreadable — never risk dropping a
|
|
1364
|
+
real event. Legacy spool with no ``_origin`` (or ``_origin:"live"``) is never
|
|
1365
|
+
quarantined.
|
|
1366
|
+
"""
|
|
1367
|
+
header = file.header
|
|
1368
|
+
if not isinstance(header, dict) or header.get("_origin") != "test":
|
|
1369
|
+
return False
|
|
1370
|
+
snapshot = os.environ.get(_LIVE_LOG_SNAPSHOT_VAR)
|
|
1371
|
+
if not snapshot:
|
|
1372
|
+
return False # cannot confirm destination is live → drain normally
|
|
1373
|
+
try:
|
|
1374
|
+
return Path(canonical_log_path).resolve() == Path(snapshot).resolve()
|
|
1375
|
+
except (OSError, ValueError):
|
|
1376
|
+
return False
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _quarantine_test_origin(file: "_DrainingFile", drain_epoch: str) -> None:
|
|
1380
|
+
"""PLAN-119 WS-D1 — rename an ``_origin:"test"`` spool out of the drain path
|
|
1381
|
+
so its entries never reach the live canonical append.
|
|
1382
|
+
|
|
1383
|
+
Breadcrumb-only — no new audit action (the canonical ``_KNOWN_ACTIONS`` is a
|
|
1384
|
+
kernel-HARD-DENY surface; the rename + the ``DrainStats`` counter are the
|
|
1385
|
+
durable trace). Mirrors ``_quarantine``'s rename discipline (latches
|
|
1386
|
+
``file.quarantined`` so Phase 5 cleanup skips the moved file).
|
|
1387
|
+
"""
|
|
1388
|
+
try:
|
|
1389
|
+
new_name = file.path.name.replace(
|
|
1390
|
+
_DRAINING_SUFFIX_TOKEN[1:] + file.drain_epoch,
|
|
1391
|
+
_TEST_ORIGIN_SUFFIX_TOKEN[1:] + drain_epoch,
|
|
1392
|
+
1,
|
|
1393
|
+
)
|
|
1394
|
+
new_path = file.path.with_name(new_name)
|
|
1395
|
+
os.rename(str(file.path), str(new_path))
|
|
1396
|
+
except OSError as e:
|
|
1397
|
+
_breadcrumb(
|
|
1398
|
+
f"test-origin quarantine rename failed: {type(e).__name__}: {e}"
|
|
1399
|
+
)
|
|
1400
|
+
file.quarantined = True
|
|
1401
|
+
# Codex P1 — compact the quarantined spool's journal records so session-start
|
|
1402
|
+
# reconciliation does NOT later count them as inflight/lost and emit a spurious
|
|
1403
|
+
# ``audit_flush_dropped_count``. A deliberate test-origin quarantine is a
|
|
1404
|
+
# COMPLETION (the entries are intentionally not drained to the live chain),
|
|
1405
|
+
# not a real-event loss. ``file.body_lines`` is populated by Phase 2.
|
|
1406
|
+
try:
|
|
1407
|
+
rec_ids = []
|
|
1408
|
+
for raw in (file.body_lines or []):
|
|
1409
|
+
try:
|
|
1410
|
+
entry = json.loads(raw.decode("utf-8"))
|
|
1411
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1412
|
+
continue
|
|
1413
|
+
rid = entry.get("record_id") if isinstance(entry, dict) else None
|
|
1414
|
+
if isinstance(rid, str) and rid:
|
|
1415
|
+
rec_ids.append(rid)
|
|
1416
|
+
if rec_ids:
|
|
1417
|
+
_journal_compact_drained(file.pid, rec_ids, drain_epoch)
|
|
1418
|
+
except Exception as e: # pragma: no cover — journal hygiene, never blocks
|
|
1419
|
+
_breadcrumb(
|
|
1420
|
+
f"test-origin journal compaction failed: {type(e).__name__}: {e}"
|
|
1421
|
+
)
|
|
1422
|
+
_breadcrumb(
|
|
1423
|
+
f"test-origin spool refused at live-chain drain: pid={file.pid} "
|
|
1424
|
+
f"epoch={drain_epoch}"
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _phase2_validate_header(
|
|
1429
|
+
file: _DrainingFile, drain_epoch: str
|
|
1430
|
+
) -> bool:
|
|
1431
|
+
"""Read header + first body line; validate body[0].spool_uuid sentinel.
|
|
1432
|
+
|
|
1433
|
+
Returns True iff header is well-formed AND header._spool_uuid matches
|
|
1434
|
+
body[0].spool_uuid (when body is non-empty). Quarantines on mismatch.
|
|
1435
|
+
Loads body_lines into file as a side effect.
|
|
1436
|
+
"""
|
|
1437
|
+
try:
|
|
1438
|
+
raw_lines = file.path.read_bytes().split(b"\n")
|
|
1439
|
+
except OSError as e:
|
|
1440
|
+
_breadcrumb(f"phase2 read failed: {type(e).__name__}: {e}")
|
|
1441
|
+
return False
|
|
1442
|
+
|
|
1443
|
+
# Strip trailing empty fragment from final newline
|
|
1444
|
+
if raw_lines and raw_lines[-1] == b"":
|
|
1445
|
+
raw_lines = raw_lines[:-1]
|
|
1446
|
+
|
|
1447
|
+
if not raw_lines:
|
|
1448
|
+
# Empty file; nothing to do — caller treats as fully-consumed
|
|
1449
|
+
file.header = None
|
|
1450
|
+
file.body_lines = []
|
|
1451
|
+
return True
|
|
1452
|
+
|
|
1453
|
+
# Parse header
|
|
1454
|
+
try:
|
|
1455
|
+
header = json.loads(raw_lines[0].decode("utf-8"))
|
|
1456
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1457
|
+
_quarantine(file, "malformed_spool_header", drain_epoch)
|
|
1458
|
+
return False
|
|
1459
|
+
# iter5-P1: use the shared strict predicate (same one used by
|
|
1460
|
+
# _ensure_spool_header for writer-side reuse) so writer and drainer
|
|
1461
|
+
# validation are byte-equivalent.
|
|
1462
|
+
if not _validate_spool_header_strict(header):
|
|
1463
|
+
_quarantine(file, "malformed_spool_header", drain_epoch)
|
|
1464
|
+
return False
|
|
1465
|
+
|
|
1466
|
+
file.header = header
|
|
1467
|
+
# P2-4: verbatim header bytes for Phase 5 atomic split
|
|
1468
|
+
file.header_raw = raw_lines[0] + b"\n"
|
|
1469
|
+
file.body_lines = raw_lines[1:]
|
|
1470
|
+
|
|
1471
|
+
if not file.body_lines:
|
|
1472
|
+
return True
|
|
1473
|
+
|
|
1474
|
+
# body[0] sentinel
|
|
1475
|
+
body0_raw = file.body_lines[0]
|
|
1476
|
+
try:
|
|
1477
|
+
body0 = json.loads(body0_raw.decode("utf-8"))
|
|
1478
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1479
|
+
# body[0] malformed → if it's the ONLY body line, treat as partial-
|
|
1480
|
+
# line crash (Phase 3 discard). Otherwise quarantine (mid-file
|
|
1481
|
+
# malformed is suspicious).
|
|
1482
|
+
if len(file.body_lines) == 1:
|
|
1483
|
+
return True
|
|
1484
|
+
_quarantine(file, "malformed_spool_header", drain_epoch)
|
|
1485
|
+
return False
|
|
1486
|
+
# PLAN-094-FOLLOWUP bug fix: extract spool_uuid from validated header
|
|
1487
|
+
# (was UnboundLocalError NameError — refactor regression from
|
|
1488
|
+
# _validate_spool_header_strict extraction; caught by Wave A.7r.1 test).
|
|
1489
|
+
spool_uuid = header["_spool_uuid"]
|
|
1490
|
+
if not isinstance(body0, dict) or body0.get("spool_uuid") != spool_uuid:
|
|
1491
|
+
_quarantine(file, "header_body_uuid_mismatch", drain_epoch)
|
|
1492
|
+
return False
|
|
1493
|
+
return True
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
# ---------------------------------------------------------------------------
|
|
1497
|
+
# Phase 3 — sort + 4-tuple uniqueness
|
|
1498
|
+
# ---------------------------------------------------------------------------
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def _phase3_collect_and_sort(
|
|
1502
|
+
files: List[_DrainingFile], drain_epoch: str, stats: DrainStats,
|
|
1503
|
+
) -> Tuple[
|
|
1504
|
+
List[Tuple[Tuple[int, int, str, int], Dict[str, Any], _DrainingFile, int]],
|
|
1505
|
+
Dict[str, Set[int]],
|
|
1506
|
+
int,
|
|
1507
|
+
]:
|
|
1508
|
+
"""Parse body lines from each draining file; sort by 4-tuple; de-dup.
|
|
1509
|
+
|
|
1510
|
+
Returns (deduped, partial_discard_consumed_indices_by_path,
|
|
1511
|
+
duplicate_consumed_count).
|
|
1512
|
+
|
|
1513
|
+
iter4-P2-2: returns the count of duplicate-tuple entries marked
|
|
1514
|
+
consumed in this cycle so Phase 4 can carry it as the starting
|
|
1515
|
+
`processed` value. Without this, a duplicate storm (e.g. corrupted
|
|
1516
|
+
spool with N identical 4-tuples) consumes/deletes more than K_MAX
|
|
1517
|
+
entries in one drain cycle, softening the K_MAX hard-cap perf
|
|
1518
|
+
contract (correctness preserved — duplicates ARE discarded).
|
|
1519
|
+
|
|
1520
|
+
iter2-P0-1: partial-discard tracking is now a Set[int] of body indices
|
|
1521
|
+
(not a prefix-count). Phase 5 splits the file using the COMPLEMENT of
|
|
1522
|
+
the set, so out-of-order index consumption (later index handled before
|
|
1523
|
+
earlier index) is safe.
|
|
1524
|
+
|
|
1525
|
+
iter2-P0-2: duplicate 4-tuple is marked CONSUMED (added to the per-file
|
|
1526
|
+
set) so Phase 5 cleanup actually removes it from disk; without this,
|
|
1527
|
+
the rejected entry survives split → re-detected as duplicate forever
|
|
1528
|
+
(livelock). audit_spool_intentionally_deleted advisory emits.
|
|
1529
|
+
|
|
1530
|
+
iter2-P2-2: per-file ordinal monotonicity check — within each spool
|
|
1531
|
+
file, `ordinal_within_file` of entry N+1 must be > entry N (the writer
|
|
1532
|
+
issues strictly-monotonic ordinals via _next_ordinal()). Violation →
|
|
1533
|
+
quarantine source with mismatch_kind="ordinal_monotonicity_violation".
|
|
1534
|
+
|
|
1535
|
+
Tamper / malformed handling:
|
|
1536
|
+
- JSON-parse fail on the LAST line of a file → partial-line discard
|
|
1537
|
+
(writer SIGKILL mid-write); emit audit_spool_partial_line_discarded
|
|
1538
|
+
- JSON-parse fail on any OTHER line → tamper; quarantine the file and
|
|
1539
|
+
drop ALL its remaining unconsumed body entries.
|
|
1540
|
+
- Duplicate full 4-tuple → reject with audit_spool_duplicate_tuple_rejected;
|
|
1541
|
+
mark consumed (iter2-P0-2).
|
|
1542
|
+
- Ordinal monotonicity violation → quarantine (iter2-P2-2).
|
|
1543
|
+
"""
|
|
1544
|
+
collected: List[Tuple[Tuple[int, int, str, int], Dict[str, Any], _DrainingFile, int]] = []
|
|
1545
|
+
partial_consumed: Dict[str, Set[int]] = {}
|
|
1546
|
+
|
|
1547
|
+
def _mark(path_str: str, idx: int) -> None:
|
|
1548
|
+
partial_consumed.setdefault(path_str, set()).add(idx)
|
|
1549
|
+
|
|
1550
|
+
for f in files:
|
|
1551
|
+
if not f.body_lines:
|
|
1552
|
+
continue
|
|
1553
|
+
last_idx = len(f.body_lines) - 1
|
|
1554
|
+
path_key = str(f.path)
|
|
1555
|
+
# iter2-P2-2: per-file ordinal monotonicity tracking.
|
|
1556
|
+
prev_ordinal: Optional[int] = None
|
|
1557
|
+
for idx, raw in enumerate(f.body_lines):
|
|
1558
|
+
if not raw:
|
|
1559
|
+
# Empty line (double-newline) — discard silently but mark
|
|
1560
|
+
# consumed so Phase 5 doesn't perpetually re-split it.
|
|
1561
|
+
_mark(path_key, idx)
|
|
1562
|
+
continue
|
|
1563
|
+
try:
|
|
1564
|
+
obj = json.loads(raw.decode("utf-8"))
|
|
1565
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
1566
|
+
if idx == last_idx:
|
|
1567
|
+
stats.partial_lines_discarded += 1
|
|
1568
|
+
_forensic("audit_spool_partial_line_discarded", {
|
|
1569
|
+
"spool_pid": f.pid,
|
|
1570
|
+
"drain_epoch": drain_epoch,
|
|
1571
|
+
"body_index": idx,
|
|
1572
|
+
})
|
|
1573
|
+
# iter2-P0-1: mark this index consumed so Phase 5
|
|
1574
|
+
# split treats the partial line as handled.
|
|
1575
|
+
_mark(path_key, idx)
|
|
1576
|
+
continue
|
|
1577
|
+
# Mid-file malformed — quarantine file and stop collecting
|
|
1578
|
+
# from it (subsequent entries are suspect)
|
|
1579
|
+
_quarantine(f, "malformed_spool_header", drain_epoch)
|
|
1580
|
+
stats.quarantined_files += 1
|
|
1581
|
+
# Drop any entries already collected from this file:
|
|
1582
|
+
collected = [c for c in collected if c[2] is not f]
|
|
1583
|
+
partial_consumed.pop(path_key, None)
|
|
1584
|
+
break
|
|
1585
|
+
if not isinstance(obj, dict):
|
|
1586
|
+
if idx == last_idx:
|
|
1587
|
+
stats.partial_lines_discarded += 1
|
|
1588
|
+
_mark(path_key, idx) # iter2-P0-1
|
|
1589
|
+
continue
|
|
1590
|
+
_quarantine(f, "malformed_spool_header", drain_epoch)
|
|
1591
|
+
stats.quarantined_files += 1
|
|
1592
|
+
collected = [c for c in collected if c[2] is not f]
|
|
1593
|
+
partial_consumed.pop(path_key, None)
|
|
1594
|
+
break
|
|
1595
|
+
try:
|
|
1596
|
+
wall_ns = int(obj["wall_ns"])
|
|
1597
|
+
pid = int(obj["pid"])
|
|
1598
|
+
spool_uuid = str(obj["spool_uuid"])
|
|
1599
|
+
ordinal = int(obj["ordinal_within_file"])
|
|
1600
|
+
except (KeyError, TypeError, ValueError):
|
|
1601
|
+
# Missing 4-tuple field — treat as tamper (would also break
|
|
1602
|
+
# HMAC since spool_uuid participates in canonical_json)
|
|
1603
|
+
_quarantine(f, "malformed_spool_header", drain_epoch)
|
|
1604
|
+
stats.quarantined_files += 1
|
|
1605
|
+
collected = [c for c in collected if c[2] is not f]
|
|
1606
|
+
partial_consumed.pop(path_key, None)
|
|
1607
|
+
break
|
|
1608
|
+
# iter2-P2-2: per-file ordinal monotonicity (strictly increasing).
|
|
1609
|
+
if prev_ordinal is not None and ordinal <= prev_ordinal:
|
|
1610
|
+
_quarantine(f, "ordinal_monotonicity_violation", drain_epoch)
|
|
1611
|
+
stats.quarantined_files += 1
|
|
1612
|
+
collected = [c for c in collected if c[2] is not f]
|
|
1613
|
+
partial_consumed.pop(path_key, None)
|
|
1614
|
+
break
|
|
1615
|
+
prev_ordinal = ordinal
|
|
1616
|
+
key = (wall_ns, pid, spool_uuid, ordinal)
|
|
1617
|
+
collected.append((key, obj, f, idx))
|
|
1618
|
+
|
|
1619
|
+
collected.sort(key=lambda x: x[0])
|
|
1620
|
+
|
|
1621
|
+
deduped: List[Tuple[Tuple[int, int, str, int], Dict[str, Any], _DrainingFile, int]] = []
|
|
1622
|
+
seen: set = set()
|
|
1623
|
+
# iter4-P2-2: bound duplicate-cleanup to K_MAX so a storm can't exceed
|
|
1624
|
+
# the per-cycle perf cap. Unmarked duplicates survive split → next
|
|
1625
|
+
# drain re-detects → bounded amortization (correctness preserved; no
|
|
1626
|
+
# canonical append happens for duplicates either way).
|
|
1627
|
+
duplicate_consumed = 0
|
|
1628
|
+
for key, obj, f, idx in collected:
|
|
1629
|
+
if key in seen:
|
|
1630
|
+
if duplicate_consumed >= K_MAX:
|
|
1631
|
+
# Still record the forensic so SOC sees the rejection,
|
|
1632
|
+
# but DO NOT _mark() consumed (defer to next cycle).
|
|
1633
|
+
stats.rejected_duplicate_tuple += 1
|
|
1634
|
+
_forensic("audit_spool_duplicate_tuple_rejected", {
|
|
1635
|
+
"wall_ns": key[0],
|
|
1636
|
+
"pid": key[1],
|
|
1637
|
+
"spool_uuid": key[2],
|
|
1638
|
+
"ordinal_within_file": key[3],
|
|
1639
|
+
"drain_epoch": drain_epoch,
|
|
1640
|
+
"deferred_to_next_cycle": True,
|
|
1641
|
+
})
|
|
1642
|
+
continue
|
|
1643
|
+
stats.rejected_duplicate_tuple += 1
|
|
1644
|
+
_forensic("audit_spool_duplicate_tuple_rejected", {
|
|
1645
|
+
"wall_ns": key[0],
|
|
1646
|
+
"pid": key[1],
|
|
1647
|
+
"spool_uuid": key[2],
|
|
1648
|
+
"ordinal_within_file": key[3],
|
|
1649
|
+
"drain_epoch": drain_epoch,
|
|
1650
|
+
})
|
|
1651
|
+
# iter2-P0-2: mark the duplicate's body index CONSUMED so Phase 5
|
|
1652
|
+
# actually removes it from disk (otherwise the rejected entry
|
|
1653
|
+
# survives split → re-detected forever = livelock). Emit a
|
|
1654
|
+
# paired advisory so SOC has trace of the silent deletion.
|
|
1655
|
+
_mark(str(f.path), idx)
|
|
1656
|
+
# iter3-P2-1: surface count into DrainStats for AC10 visibility
|
|
1657
|
+
# (JournalReconciliation aggregates this at session-start).
|
|
1658
|
+
stats.intentionally_deleted += 1
|
|
1659
|
+
duplicate_consumed += 1
|
|
1660
|
+
_forensic("audit_spool_intentionally_deleted", {
|
|
1661
|
+
"reason": "duplicate_tuple",
|
|
1662
|
+
"spool_pid": key[1],
|
|
1663
|
+
"spool_uuid": key[2],
|
|
1664
|
+
"ordinal_within_file": key[3],
|
|
1665
|
+
"drain_epoch": drain_epoch,
|
|
1666
|
+
})
|
|
1667
|
+
continue
|
|
1668
|
+
seen.add(key)
|
|
1669
|
+
deduped.append((key, obj, f, idx))
|
|
1670
|
+
return deduped, partial_consumed, duplicate_consumed
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
# ---------------------------------------------------------------------------
|
|
1674
|
+
# Phase 4 — idempotent chain reconstruction
|
|
1675
|
+
# ---------------------------------------------------------------------------
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _phase4_build_batch(
|
|
1679
|
+
deduped: List[Tuple[Tuple[int, int, str, int], Dict[str, Any], _DrainingFile, int]],
|
|
1680
|
+
drain_epoch: str,
|
|
1681
|
+
in_recovery: bool,
|
|
1682
|
+
stats: DrainStats,
|
|
1683
|
+
starting_processed: int = 0,
|
|
1684
|
+
) -> Tuple[List[bytes], List[_DrainingFile], Dict[str, Set[int]], Optional[bytes], List[Tuple[str, int]]]:
|
|
1685
|
+
"""Compute HMAC chain; produce canonical-log batch lines.
|
|
1686
|
+
|
|
1687
|
+
Returns (batch_line_bytes, fully_consumed_files,
|
|
1688
|
+
per_file_consumed_indices, last_hmac_bytes, drained_record_ids_by_pid).
|
|
1689
|
+
|
|
1690
|
+
iter2-P0-1: per_file_consumed is now Dict[str, Set[int]] (body indices,
|
|
1691
|
+
not a prefix-count). Phase 5 splits the remainder using the COMPLEMENT
|
|
1692
|
+
of the set — out-of-order index consumption (e.g. clock-skew rollback
|
|
1693
|
+
where a LATER body index sorts ahead of an EARLIER one and is cut by
|
|
1694
|
+
K_MAX) no longer trashes unprocessed earlier lines.
|
|
1695
|
+
|
|
1696
|
+
iter2-P1-1: idempotent-skip branch now ALSO appends (record_id, pid)
|
|
1697
|
+
to drained_ids and marks the index consumed — without this, the matching
|
|
1698
|
+
journal `commit` envelope stays as commit_no_drained forever (phantom
|
|
1699
|
+
inflight count in AC10).
|
|
1700
|
+
|
|
1701
|
+
iter2-P2-1: canonical output honors the SOURCE file's drain_epoch
|
|
1702
|
+
(src_file.drain_epoch) — important for forensic correlation when an
|
|
1703
|
+
in-recovery drain re-emits entries from a prior-cycle .draining file.
|
|
1704
|
+
|
|
1705
|
+
iter3-P0-1: K_MAX counts TOTAL PROCESSED entries (appended + idempotent
|
|
1706
|
+
skipped + intentionally consumed via tamper/duplicate paths), NOT just
|
|
1707
|
+
appended. Without this, repeated partial-crash cycles where each crash
|
|
1708
|
+
leaves K_MAX entries in .draining and the next drain hits all K_MAX as
|
|
1709
|
+
skipped + processes K_MAX fresh can accumulate >K_TAIL_WINDOW processed
|
|
1710
|
+
entries per cycle → entries fall out of the skip-guard tail window on
|
|
1711
|
+
a subsequent crash cycle → re-appended duplicates in canonical log.
|
|
1712
|
+
Capping TOTAL processed bounds disk-write + idempotent work per drain
|
|
1713
|
+
cycle so window-overflow cannot occur.
|
|
1714
|
+
|
|
1715
|
+
Honors K_MAX cap; remaining un-consumed entries are left for the next
|
|
1716
|
+
drain cycle via Phase 5 atomic split.
|
|
1717
|
+
"""
|
|
1718
|
+
log_path = _canonical_log_path()
|
|
1719
|
+
|
|
1720
|
+
# PLAN-112-FOLLOWUP-hmac-tamper-fix Wave B.1 — Path B variant 1 hoist.
|
|
1721
|
+
# Probe rotation BEFORE reading canonical tail (and therefore before
|
|
1722
|
+
# computing prev_hmac), so this batch always anchors against the
|
|
1723
|
+
# CURRENT canonical file (post-rotation if applicable).
|
|
1724
|
+
#
|
|
1725
|
+
# Prior bug: Phase 4 read prev_hmac from the about-to-rotate file;
|
|
1726
|
+
# Phase 5 detected rotation but batch_lines were already HMAC-chained
|
|
1727
|
+
# against the old chain → verifier (resets at file boundary per
|
|
1728
|
+
# ADR-055 §2) saw STATUS_TAMPER at line 1 of the new file.
|
|
1729
|
+
# Per F-7.7 (PLAN-112 wave-c-bundles/C10) + D3 confirmation.
|
|
1730
|
+
# Fail-open: any rotation exception is breadcrumbed and the read
|
|
1731
|
+
# proceeds against whatever log path is current.
|
|
1732
|
+
try:
|
|
1733
|
+
from _lib import audit_emit as _audit_emit_lazy
|
|
1734
|
+
from _lib import audit_hmac as _audit_hmac_lazy
|
|
1735
|
+
# PLAN-143 item-2 (audit-errors-02): guard the rotation probe.
|
|
1736
|
+
# Under test/shim wiring the lazily-imported object can be an
|
|
1737
|
+
# _EmitCapture double that lacks _rotate_if_needed_safe; calling
|
|
1738
|
+
# it raised AttributeError into the fail-open except below, which
|
|
1739
|
+
# breadcrumbed cosmetic audit-log.errors noise and silently
|
|
1740
|
+
# skipped the probe. getattr-guard -> no attribute means "no
|
|
1741
|
+
# rotation" (None), taking the same intended branch without noise.
|
|
1742
|
+
_rotate_probe = getattr(
|
|
1743
|
+
_audit_emit_lazy, "_rotate_if_needed_safe", None
|
|
1744
|
+
)
|
|
1745
|
+
rotated_to_phase4 = (
|
|
1746
|
+
_rotate_probe(log_path) if callable(_rotate_probe) else None
|
|
1747
|
+
)
|
|
1748
|
+
if rotated_to_phase4 is not None and not _audit_hmac_lazy.is_disabled():
|
|
1749
|
+
try:
|
|
1750
|
+
_audit_hmac_lazy.reset_chain_on_rotation()
|
|
1751
|
+
except Exception as re:
|
|
1752
|
+
_breadcrumb(
|
|
1753
|
+
f"phase4 rotation HMAC reset failed: "
|
|
1754
|
+
f"{type(re).__name__}: {re}"
|
|
1755
|
+
)
|
|
1756
|
+
# PLAN-112-FOLLOWUP-hmac-tamper-fix Wave B.3 — emit
|
|
1757
|
+
# chain_reset_marker as line 1 of new file + manifest. Fail-open
|
|
1758
|
+
# (marker absence → verifier legacy mode, not tamper signal).
|
|
1759
|
+
try:
|
|
1760
|
+
_audit_emit_lazy._emit_chain_reset_marker_under_lock(
|
|
1761
|
+
log=log_path,
|
|
1762
|
+
previous_archive_path=str(rotated_to_phase4),
|
|
1763
|
+
rotation_trigger="size_threshold",
|
|
1764
|
+
)
|
|
1765
|
+
except Exception as me:
|
|
1766
|
+
_breadcrumb(
|
|
1767
|
+
f"phase4 chain_reset_marker emit failed: "
|
|
1768
|
+
f"{type(me).__name__}: {me}"
|
|
1769
|
+
)
|
|
1770
|
+
except Exception as e:
|
|
1771
|
+
# Fail-open: rotation probe never blocks the canonical read.
|
|
1772
|
+
_breadcrumb(f"phase4 rotation probe failed: {type(e).__name__}: {e}")
|
|
1773
|
+
|
|
1774
|
+
# AFTER rotation probe: read tail from (now-current) file.
|
|
1775
|
+
prev_hmac, tail_entries = _read_canonical_tail(log_path, K_TAIL_WINDOW)
|
|
1776
|
+
drain_sha_set: set = set()
|
|
1777
|
+
for e in tail_entries:
|
|
1778
|
+
ds = e.get("_drain_sha256")
|
|
1779
|
+
if isinstance(ds, str) and len(ds) == 64:
|
|
1780
|
+
drain_sha_set.add(ds)
|
|
1781
|
+
|
|
1782
|
+
if _HMAC_AVAILABLE and _audit_hmac is not None and prev_hmac is None:
|
|
1783
|
+
# Empty log + HMAC available → genesis (which is correct
|
|
1784
|
+
# post-rotation since the new file is empty).
|
|
1785
|
+
prev_hmac = _audit_hmac.GENESIS_PREV
|
|
1786
|
+
|
|
1787
|
+
batch_lines: List[bytes] = []
|
|
1788
|
+
# iter2-P0-1: set-of-indices, not prefix-count.
|
|
1789
|
+
per_file_consumed: Dict[str, Set[int]] = {}
|
|
1790
|
+
drained_ids: List[Tuple[str, int]] = [] # P1-3: (record_id, pid)
|
|
1791
|
+
last_hmac: Optional[bytes] = prev_hmac
|
|
1792
|
+
|
|
1793
|
+
def _mark(path_str: str, idx: int) -> None:
|
|
1794
|
+
per_file_consumed.setdefault(path_str, set()).add(idx)
|
|
1795
|
+
|
|
1796
|
+
# iter3-P0-1: cap on TOTAL processed (appended + idempotent_skipped +
|
|
1797
|
+
# tamper-consumed); breaking on appended-only allowed cumulative
|
|
1798
|
+
# processed > K_TAIL_WINDOW across recovery cycles → window overflow.
|
|
1799
|
+
# iter4-P2-2: `starting_processed` carries duplicate-cleanup count from
|
|
1800
|
+
# Phase 3 so combined Phase-3 + Phase-4 work ≤ K_MAX per drain cycle.
|
|
1801
|
+
processed = starting_processed
|
|
1802
|
+
for key, entry, src_file, body_idx in deduped:
|
|
1803
|
+
if processed >= K_MAX:
|
|
1804
|
+
break
|
|
1805
|
+
path_key = str(src_file.path)
|
|
1806
|
+
|
|
1807
|
+
# P1-3: pull record_id off the spool entry BEFORE stripping internal
|
|
1808
|
+
# fields so we can emit op:"drained" with the matching id.
|
|
1809
|
+
rec_id = entry.get("record_id")
|
|
1810
|
+
|
|
1811
|
+
entry_clean = {
|
|
1812
|
+
k: v for k, v in entry.items()
|
|
1813
|
+
if not (isinstance(k, str) and k.startswith("_drain_"))
|
|
1814
|
+
and k != "hmac" and k != "hmac_error"
|
|
1815
|
+
}
|
|
1816
|
+
try:
|
|
1817
|
+
sha = _sha256_of_canonical_json(entry_clean)
|
|
1818
|
+
except Exception as e:
|
|
1819
|
+
_breadcrumb(f"sha compute failed: {type(e).__name__}: {e}")
|
|
1820
|
+
# Treat unencodable entry like a tamper — skip + count
|
|
1821
|
+
stats.partial_lines_discarded += 1
|
|
1822
|
+
_mark(path_key, body_idx)
|
|
1823
|
+
# iter3-P0-1: tamper-consumed counts toward K_MAX budget
|
|
1824
|
+
processed += 1
|
|
1825
|
+
continue
|
|
1826
|
+
|
|
1827
|
+
if sha in drain_sha_set:
|
|
1828
|
+
stats.skipped_idempotent += 1
|
|
1829
|
+
severity = "INFORMATIONAL" if in_recovery else "ALARM"
|
|
1830
|
+
_forensic("audit_spool_unexpected_skip", {
|
|
1831
|
+
"drain_epoch": drain_epoch,
|
|
1832
|
+
"spool_uuid": key[2],
|
|
1833
|
+
"ordinal_within_file": key[3],
|
|
1834
|
+
"skipped_sha256": sha,
|
|
1835
|
+
"drain_in_recovery_mode": in_recovery,
|
|
1836
|
+
"severity": severity,
|
|
1837
|
+
})
|
|
1838
|
+
_mark(path_key, body_idx)
|
|
1839
|
+
# iter2-P1-1: also commit drained_id so the journal compactor
|
|
1840
|
+
# emits op:"drained" for this record. Without this, the matching
|
|
1841
|
+
# `commit` envelope stays as commit_no_drained forever in the
|
|
1842
|
+
# journal (AC10 phantom inflight). The canonical batch line was
|
|
1843
|
+
# already appended on the prior crashed drain — this entry is
|
|
1844
|
+
# "drained" from the journal's perspective even though we are
|
|
1845
|
+
# not re-appending the bytes this cycle.
|
|
1846
|
+
if isinstance(rec_id, str) and rec_id:
|
|
1847
|
+
drained_ids.append((rec_id, src_file.pid))
|
|
1848
|
+
# iter3-P0-1: idempotent-skip counts toward K_MAX budget so
|
|
1849
|
+
# window-overflow on multi-crash cycles is structurally
|
|
1850
|
+
# impossible (processed ≤ K_MAX < K_TAIL_WINDOW).
|
|
1851
|
+
processed += 1
|
|
1852
|
+
continue
|
|
1853
|
+
|
|
1854
|
+
chained = dict(entry_clean)
|
|
1855
|
+
chained["_drain_sha256"] = sha
|
|
1856
|
+
# iter2-P2-1: honor source file's drain_epoch for forensic
|
|
1857
|
+
# correlation (this entry was first staged in that cycle).
|
|
1858
|
+
chained["_drain_epoch"] = src_file.drain_epoch
|
|
1859
|
+
|
|
1860
|
+
if (_HMAC_AVAILABLE and _audit_hmac is not None
|
|
1861
|
+
and not _audit_hmac.is_disabled()):
|
|
1862
|
+
try:
|
|
1863
|
+
key_bytes = _audit_hmac.get_or_create_key()
|
|
1864
|
+
digest = _audit_hmac.compute_entry_hmac(
|
|
1865
|
+
key_bytes,
|
|
1866
|
+
last_hmac if last_hmac is not None else _audit_hmac.GENESIS_PREV,
|
|
1867
|
+
chained,
|
|
1868
|
+
)
|
|
1869
|
+
chained["hmac"] = digest.hex()
|
|
1870
|
+
last_hmac = digest
|
|
1871
|
+
except _audit_hmac.AuditProducerPathPollutionError as ppe:
|
|
1872
|
+
# PLAN-118 AC-B4 chokepoint 4 / recursion-safety case 5 —
|
|
1873
|
+
# the spool fast-path (audit_emit._write_event line 1760)
|
|
1874
|
+
# appends to spool BEFORE local HMAC computation; the
|
|
1875
|
+
# HMAC is then computed HERE at drain time. On
|
|
1876
|
+
# canonical-resolution mismatch at drain: refuse to
|
|
1877
|
+
# compute HMAC for the batch entry; normalize the entry
|
|
1878
|
+
# to hmac:null + closed-enum hmac_error BEFORE the entry
|
|
1879
|
+
# is appended to the chain (so polluted HMACs never enter
|
|
1880
|
+
# the chain at all). Emit AC-B5 breadcrumb with
|
|
1881
|
+
# chokepoint=spool_drain (safe — the typed emitter's own
|
|
1882
|
+
# _write_event call will also trigger this same
|
|
1883
|
+
# exception and recurse-fail-OPEN through the same
|
|
1884
|
+
# channel; the breadcrumb's purpose is exactly that
|
|
1885
|
+
# forensic signal).
|
|
1886
|
+
chained["hmac"] = None
|
|
1887
|
+
chained["hmac_error"] = "producer_path_pollution_detected"
|
|
1888
|
+
_breadcrumb(
|
|
1889
|
+
f"hmac_error: producer_path_pollution_detected "
|
|
1890
|
+
f"(chokepoint=spool_drain): {ppe}"
|
|
1891
|
+
)
|
|
1892
|
+
# Parse the exception payload for the AC-B5 breadcrumb.
|
|
1893
|
+
try:
|
|
1894
|
+
_msg = str(ppe)
|
|
1895
|
+
_rc = "audit_emit_path_pollution"
|
|
1896
|
+
_psp = "00000000"
|
|
1897
|
+
_ecp = "00000000"
|
|
1898
|
+
_valid_rcs = (
|
|
1899
|
+
"audit_emit_path_pollution",
|
|
1900
|
+
"canonical_json_path_pollution",
|
|
1901
|
+
"audit_hmac_path_pollution",
|
|
1902
|
+
)
|
|
1903
|
+
for _tok in _msg.split():
|
|
1904
|
+
if _tok.startswith("reason_code="):
|
|
1905
|
+
_cand = _tok.split("=", 1)[1]
|
|
1906
|
+
if _cand in _valid_rcs:
|
|
1907
|
+
_rc = _cand
|
|
1908
|
+
elif _tok.startswith("path_sha256_prefix="):
|
|
1909
|
+
_cand = _tok.split("=", 1)[1]
|
|
1910
|
+
if len(_cand) == 8 and all(c in "0123456789abcdef" for c in _cand):
|
|
1911
|
+
_psp = _cand
|
|
1912
|
+
elif _tok.startswith("expected_canonical_prefix="):
|
|
1913
|
+
_cand = _tok.split("=", 1)[1]
|
|
1914
|
+
if len(_cand) == 8 and all(c in "0123456789abcdef" for c in _cand):
|
|
1915
|
+
_ecp = _cand
|
|
1916
|
+
# Lazy-import the typed emitter to avoid circular import
|
|
1917
|
+
# at module load (spool_writer is imported from audit_emit).
|
|
1918
|
+
from _lib import audit_emit as _audit_emit
|
|
1919
|
+
_audit_emit.emit_audit_producer_path_pollution_detected(
|
|
1920
|
+
chokepoint="spool_drain",
|
|
1921
|
+
reason_code=_rc,
|
|
1922
|
+
path_sha256_prefix=_psp,
|
|
1923
|
+
expected_canonical_prefix=_ecp,
|
|
1924
|
+
)
|
|
1925
|
+
except Exception as _be: # pragma: no cover
|
|
1926
|
+
_breadcrumb(
|
|
1927
|
+
f"AC-B5 spool_drain breadcrumb emit failed: "
|
|
1928
|
+
f"{type(_be).__name__}: {_be}"
|
|
1929
|
+
)
|
|
1930
|
+
except Exception as e:
|
|
1931
|
+
chained["hmac"] = None
|
|
1932
|
+
chained["hmac_error"] = f"{type(e).__name__}: {e}"
|
|
1933
|
+
_breadcrumb(f"hmac compute failed: {type(e).__name__}: {e}")
|
|
1934
|
+
|
|
1935
|
+
try:
|
|
1936
|
+
line = json.dumps(
|
|
1937
|
+
chained, separators=(",", ":"), ensure_ascii=False,
|
|
1938
|
+
).encode("utf-8") + b"\n"
|
|
1939
|
+
except (TypeError, ValueError) as e:
|
|
1940
|
+
_breadcrumb(f"phase4 encode failed: {type(e).__name__}: {e}")
|
|
1941
|
+
_mark(path_key, body_idx)
|
|
1942
|
+
# iter3-P0-1: encode-failure consumed counts toward K_MAX.
|
|
1943
|
+
processed += 1
|
|
1944
|
+
continue
|
|
1945
|
+
batch_lines.append(line)
|
|
1946
|
+
_mark(path_key, body_idx)
|
|
1947
|
+
# P1-3: track for op:"drained" envelope emission
|
|
1948
|
+
if isinstance(rec_id, str) and rec_id:
|
|
1949
|
+
drained_ids.append((rec_id, src_file.pid))
|
|
1950
|
+
# iter3-P0-1: append counts toward K_MAX (was: `appended += 1`).
|
|
1951
|
+
processed += 1
|
|
1952
|
+
|
|
1953
|
+
fully_consumed: List[_DrainingFile] = []
|
|
1954
|
+
return batch_lines, fully_consumed, per_file_consumed, last_hmac, drained_ids
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
# ---------------------------------------------------------------------------
|
|
1958
|
+
# Phase 5 — atomic append + split + cleanup
|
|
1959
|
+
# ---------------------------------------------------------------------------
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
def _phase5_append_canonical(
|
|
1963
|
+
batch_lines: List[bytes], last_hmac: Optional[bytes]
|
|
1964
|
+
) -> int:
|
|
1965
|
+
"""Append batch to canonical log; single fsync; update sidecar cache.
|
|
1966
|
+
|
|
1967
|
+
iter2-P2-3: chain_length sidecar increment counts HMAC-bearing lines
|
|
1968
|
+
only — entries that failed HMAC compute (hmac=null + hmac_error set)
|
|
1969
|
+
don't extend the chain even though their bytes are appended. The
|
|
1970
|
+
sidecar is a cache for chain-verify performance; counting null-hmac
|
|
1971
|
+
lines would diverge from the true HMAC chain length.
|
|
1972
|
+
"""
|
|
1973
|
+
if not batch_lines:
|
|
1974
|
+
return 0
|
|
1975
|
+
log_path = _canonical_log_path()
|
|
1976
|
+
log_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
1977
|
+
|
|
1978
|
+
# PLAN-094-FOLLOWUP Wave A.3-rotation — probe rotation BEFORE the
|
|
1979
|
+
# canonical append so spool-drained writes never accumulate past the
|
|
1980
|
+
# configured threshold. Lazy-import audit_emit (circular dep at
|
|
1981
|
+
# module load time) + audit_hmac for chain reset.
|
|
1982
|
+
try:
|
|
1983
|
+
from _lib import audit_emit as _audit_emit_lazy
|
|
1984
|
+
from _lib import audit_hmac as _audit_hmac_lazy
|
|
1985
|
+
# PLAN-143 item-2 (audit-errors-02): guard the rotation probe — a
|
|
1986
|
+
# capture-shim object may lack _rotate_if_needed_safe (see the
|
|
1987
|
+
# phase-4 probe above). getattr-guard -> missing attribute means
|
|
1988
|
+
# "no rotation" (None), no AttributeError into the fail-open path.
|
|
1989
|
+
_rotate_probe = getattr(
|
|
1990
|
+
_audit_emit_lazy, "_rotate_if_needed_safe", None
|
|
1991
|
+
)
|
|
1992
|
+
rotated_to = (
|
|
1993
|
+
_rotate_probe(log_path) if callable(_rotate_probe) else None
|
|
1994
|
+
)
|
|
1995
|
+
if rotated_to is not None and not _audit_hmac_lazy.is_disabled():
|
|
1996
|
+
try:
|
|
1997
|
+
_audit_hmac_lazy.reset_chain_on_rotation()
|
|
1998
|
+
# Reset last_hmac so this batch re-anchors at genesis.
|
|
1999
|
+
last_hmac = None
|
|
2000
|
+
except Exception as re:
|
|
2001
|
+
_breadcrumb(
|
|
2002
|
+
f"phase5 rotation HMAC reset failed: "
|
|
2003
|
+
f"{type(re).__name__}: {re}"
|
|
2004
|
+
)
|
|
2005
|
+
except Exception as e:
|
|
2006
|
+
# Fail-open: rotation probe never blocks the canonical append.
|
|
2007
|
+
_breadcrumb(f"phase5 rotation probe failed: {type(e).__name__}: {e}")
|
|
2008
|
+
|
|
2009
|
+
payload = b"".join(batch_lines)
|
|
2010
|
+
fd = os.open(str(log_path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
|
|
2011
|
+
try:
|
|
2012
|
+
os.write(fd, payload)
|
|
2013
|
+
os.fsync(fd)
|
|
2014
|
+
finally:
|
|
2015
|
+
os.close(fd)
|
|
2016
|
+
|
|
2017
|
+
if (_HMAC_AVAILABLE and _audit_hmac is not None
|
|
2018
|
+
and not _audit_hmac.is_disabled()
|
|
2019
|
+
and last_hmac is not None
|
|
2020
|
+
and len(last_hmac) == _audit_hmac.HMAC_BYTES):
|
|
2021
|
+
try:
|
|
2022
|
+
_audit_hmac.write_last_hmac(last_hmac)
|
|
2023
|
+
except Exception as e:
|
|
2024
|
+
_breadcrumb(f"write_last_hmac failed: {type(e).__name__}: {e}")
|
|
2025
|
+
# iter2-P2-3: count HMAC-bearing lines only.
|
|
2026
|
+
hmac_bearing = 0
|
|
2027
|
+
for line in batch_lines:
|
|
2028
|
+
try:
|
|
2029
|
+
obj = json.loads(line.rstrip(b"\n").decode("utf-8"))
|
|
2030
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
2031
|
+
continue
|
|
2032
|
+
if not isinstance(obj, dict):
|
|
2033
|
+
continue
|
|
2034
|
+
hx = obj.get("hmac")
|
|
2035
|
+
if isinstance(hx, str) and len(hx) == 64:
|
|
2036
|
+
hmac_bearing += 1
|
|
2037
|
+
try:
|
|
2038
|
+
current = _audit_hmac.read_chain_length()
|
|
2039
|
+
_audit_hmac.write_chain_length(current + hmac_bearing)
|
|
2040
|
+
except Exception as e:
|
|
2041
|
+
_breadcrumb(f"chain_length update failed: {type(e).__name__}: {e}")
|
|
2042
|
+
return len(batch_lines)
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
def _phase5_split_and_cleanup(
|
|
2046
|
+
files: List[_DrainingFile],
|
|
2047
|
+
per_file_consumed: Dict[str, Set[int]],
|
|
2048
|
+
drain_epoch: str,
|
|
2049
|
+
stats: DrainStats,
|
|
2050
|
+
) -> None:
|
|
2051
|
+
"""For each draining file: unlink if fully consumed; else atomic-split.
|
|
2052
|
+
|
|
2053
|
+
iter2-P0-1: per_file_consumed is a Set[int] of body INDICES. The
|
|
2054
|
+
remainder file is built from the COMPLEMENT (all body indices NOT in
|
|
2055
|
+
the set). This correctly handles out-of-order index consumption (e.g.
|
|
2056
|
+
wall_ns clock-skew rollback where a later body index sorts BEFORE an
|
|
2057
|
+
earlier one and is cut by K_MAX) — prefix-based cleanup would have
|
|
2058
|
+
discarded unprocessed earlier lines.
|
|
2059
|
+
|
|
2060
|
+
iter2-P0-3: SKIP any _DrainingFile whose .quarantined flag is set —
|
|
2061
|
+
the file is already renamed to .malformed.<epoch>; trying to split
|
|
2062
|
+
or unlink the original path would either fail (ENOENT) or, worse,
|
|
2063
|
+
re-sweep the renamed quarantined file on a subsequent drain.
|
|
2064
|
+
|
|
2065
|
+
iter2-P1-3: when splitting, mint a NEW drain_epoch for the destination
|
|
2066
|
+
so dst path != src path. This closes the `.split.tmp` two-step window
|
|
2067
|
+
(old code reused src epoch → dst==src → os.rename(src,dst) no-op AND
|
|
2068
|
+
the temp suffix dance remained). Now: tmp → fsync → rename(tmp,
|
|
2069
|
+
new_dst) atomic; unlink(src). Single window if rename succeeds.
|
|
2070
|
+
|
|
2071
|
+
P2-4: header is written VERBATIM from file.header_raw — no JSON
|
|
2072
|
+
re-encode (preserves byte identity of _spool_uuid sentinel chain).
|
|
2073
|
+
"""
|
|
2074
|
+
for f in files:
|
|
2075
|
+
# iter2-P0-3: skip quarantined files — original path doesn't exist
|
|
2076
|
+
# at .draining.<epoch> anymore; it was renamed to .malformed.<epoch>.
|
|
2077
|
+
if f.quarantined:
|
|
2078
|
+
continue
|
|
2079
|
+
path_key = str(f.path)
|
|
2080
|
+
if f.header is None and not f.body_lines:
|
|
2081
|
+
# Already empty (no body) — just unlink the (header-only) file.
|
|
2082
|
+
try:
|
|
2083
|
+
if f.path.exists():
|
|
2084
|
+
os.unlink(str(f.path))
|
|
2085
|
+
except OSError:
|
|
2086
|
+
pass
|
|
2087
|
+
continue
|
|
2088
|
+
consumed_set = per_file_consumed.get(path_key, set())
|
|
2089
|
+
total = len(f.body_lines)
|
|
2090
|
+
# iter2-P0-1: fully consumed iff every body index is in the set.
|
|
2091
|
+
if total == 0 or all(i in consumed_set for i in range(total)):
|
|
2092
|
+
try:
|
|
2093
|
+
os.unlink(str(f.path))
|
|
2094
|
+
stats.files_consumed_fully += 1
|
|
2095
|
+
except OSError as e:
|
|
2096
|
+
_breadcrumb(f"phase5 unlink failed: {type(e).__name__}: {e}")
|
|
2097
|
+
continue
|
|
2098
|
+
|
|
2099
|
+
# iter2-P1-3: mint a NEW epoch for the split destination so
|
|
2100
|
+
# dst_path differs from src_path; this collapses the prior
|
|
2101
|
+
# two-step `.split.tmp` rename dance into a single rename+unlink.
|
|
2102
|
+
new_epoch = secrets.token_hex(4)
|
|
2103
|
+
tmp_token = secrets.token_hex(4)
|
|
2104
|
+
# tmp is a sibling .tmp.<token> for atomic create+rename within same dir
|
|
2105
|
+
tmp_path = f.path.with_name(f.path.name + _TMP_SUFFIX_TOKEN + tmp_token)
|
|
2106
|
+
try:
|
|
2107
|
+
# P2-4: verbatim header bytes — no JSON re-encode
|
|
2108
|
+
header_line = f.header_raw if f.header_raw else (
|
|
2109
|
+
json.dumps(
|
|
2110
|
+
f.header, separators=(",", ":"), ensure_ascii=False,
|
|
2111
|
+
).encode("utf-8") + b"\n"
|
|
2112
|
+
)
|
|
2113
|
+
# iter2-P0-1: rebuild remainder using COMPLEMENT of consumed_set
|
|
2114
|
+
# (NOT a prefix slice). Empty lines are preserved as empty bytes
|
|
2115
|
+
# in body_lines and re-emitted with the newline separator below.
|
|
2116
|
+
remainder_lines = [
|
|
2117
|
+
f.body_lines[i] for i in range(total) if i not in consumed_set
|
|
2118
|
+
]
|
|
2119
|
+
body_remainder = b"\n".join(remainder_lines)
|
|
2120
|
+
if body_remainder and not body_remainder.endswith(b"\n"):
|
|
2121
|
+
body_remainder += b"\n"
|
|
2122
|
+
fd = os.open(
|
|
2123
|
+
str(tmp_path),
|
|
2124
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC,
|
|
2125
|
+
0o600,
|
|
2126
|
+
)
|
|
2127
|
+
try:
|
|
2128
|
+
os.write(fd, header_line)
|
|
2129
|
+
if body_remainder:
|
|
2130
|
+
os.write(fd, body_remainder)
|
|
2131
|
+
os.fsync(fd)
|
|
2132
|
+
finally:
|
|
2133
|
+
os.close(fd)
|
|
2134
|
+
# iter2-P1-3: dst is .draining.<new_epoch> — distinct from src.
|
|
2135
|
+
dst_path = _draining_path(_spool_path(f.pid), new_epoch)
|
|
2136
|
+
# Single atomic rename + unlink source. If rename fails, the
|
|
2137
|
+
# tmp file is cleaned up in the except branch.
|
|
2138
|
+
os.rename(str(tmp_path), str(dst_path))
|
|
2139
|
+
try:
|
|
2140
|
+
os.unlink(str(f.path))
|
|
2141
|
+
except OSError as e: # pragma: no cover
|
|
2142
|
+
# Source gone (race / already cleaned); rename succeeded so
|
|
2143
|
+
# the remainder file is correctly in place.
|
|
2144
|
+
_breadcrumb(
|
|
2145
|
+
f"phase5 src unlink after split failed: "
|
|
2146
|
+
f"{type(e).__name__}: {e}"
|
|
2147
|
+
)
|
|
2148
|
+
stats.files_split_remainder += 1
|
|
2149
|
+
except OSError as e:
|
|
2150
|
+
_breadcrumb(f"phase5 split failed: {type(e).__name__}: {e}")
|
|
2151
|
+
try:
|
|
2152
|
+
if tmp_path.exists():
|
|
2153
|
+
os.unlink(str(tmp_path))
|
|
2154
|
+
except OSError:
|
|
2155
|
+
pass
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
# ---------------------------------------------------------------------------
|
|
2159
|
+
# Journal post-drain compaction
|
|
2160
|
+
# ---------------------------------------------------------------------------
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def _journal_compact_drained(
|
|
2164
|
+
pid: int,
|
|
2165
|
+
drained_record_ids: Iterable[str],
|
|
2166
|
+
drain_epoch: str,
|
|
2167
|
+
) -> None:
|
|
2168
|
+
"""Append op=drained envelopes; rewrite journal dropping fully-completed triples.
|
|
2169
|
+
|
|
2170
|
+
P1-3: emits op:"drained" envelopes with the actual record_id so
|
|
2171
|
+
session-start reconciliation can distinguish recovered from inflight.
|
|
2172
|
+
"""
|
|
2173
|
+
drained_set = {r for r in drained_record_ids if isinstance(r, str) and r}
|
|
2174
|
+
if not drained_set:
|
|
2175
|
+
return
|
|
2176
|
+
# Flush any pending in-memory journal envelopes for this PID FIRST so
|
|
2177
|
+
# the compaction read sees a consistent on-disk view.
|
|
2178
|
+
_flush_journal_buffer(pid)
|
|
2179
|
+
journal = _journal_path(pid)
|
|
2180
|
+
if not journal.exists():
|
|
2181
|
+
return
|
|
2182
|
+
# 1) Append drained envelopes via the buffer (then flush)
|
|
2183
|
+
for rid in drained_set:
|
|
2184
|
+
_write_journal_envelope(
|
|
2185
|
+
pid, rid, "", -1, "", "drained", drain_epoch=drain_epoch,
|
|
2186
|
+
)
|
|
2187
|
+
_flush_journal_buffer(pid)
|
|
2188
|
+
# 2) Rewrite journal under per-PID journal flock dropping rows whose
|
|
2189
|
+
# record_id is in drained_set (begin/commit/drained triples collapse).
|
|
2190
|
+
try:
|
|
2191
|
+
with FileLock(_journal_flock_path(pid), timeout=SPOOL_LOCK_TIMEOUT):
|
|
2192
|
+
with journal.open("r", encoding="utf-8") as f:
|
|
2193
|
+
keep_lines: List[str] = []
|
|
2194
|
+
for raw in f:
|
|
2195
|
+
stripped = raw.strip()
|
|
2196
|
+
if not stripped:
|
|
2197
|
+
continue
|
|
2198
|
+
try:
|
|
2199
|
+
env = json.loads(stripped)
|
|
2200
|
+
except json.JSONDecodeError:
|
|
2201
|
+
keep_lines.append(raw)
|
|
2202
|
+
continue
|
|
2203
|
+
if (isinstance(env, dict)
|
|
2204
|
+
and env.get("record_id") in drained_set):
|
|
2205
|
+
continue
|
|
2206
|
+
keep_lines.append(raw)
|
|
2207
|
+
tmp = journal.with_name(journal.name + ".compact.tmp")
|
|
2208
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
2209
|
+
f.writelines(keep_lines)
|
|
2210
|
+
f.flush()
|
|
2211
|
+
os.fsync(f.fileno())
|
|
2212
|
+
os.replace(str(tmp), str(journal))
|
|
2213
|
+
except FileLockTimeout:
|
|
2214
|
+
_breadcrumb(f"journal compact flock timeout pid={pid}")
|
|
2215
|
+
except OSError as e:
|
|
2216
|
+
_breadcrumb(f"journal compact failed: {type(e).__name__}: {e}")
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
# ---------------------------------------------------------------------------
|
|
2220
|
+
# drain_now — orchestration
|
|
2221
|
+
# ---------------------------------------------------------------------------
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
def _own_spool_stale_past_trigger(pid: int) -> bool:
|
|
2225
|
+
"""True iff our own spool's mtime age exceeds DRAIN_TRIGGER_MTIME_MS.
|
|
2226
|
+
|
|
2227
|
+
(ADR-055-AMEND-3) SEC veto-floor MF-1 gate. When an opportunistic
|
|
2228
|
+
(force=False) drain yields the canonical lock AND our own spool is already
|
|
2229
|
+
stale past the staleness trigger, the lock holder is not keeping up — that
|
|
2230
|
+
is genuine drain starvation (a wedged/contended holder), distinct from
|
|
2231
|
+
benign single-winner contention (a fresh spool). OSError-safe: a missing or
|
|
2232
|
+
unreadable spool returns False (fail-quiet, never raises).
|
|
2233
|
+
"""
|
|
2234
|
+
try:
|
|
2235
|
+
st = _spool_path(pid).stat()
|
|
2236
|
+
except OSError:
|
|
2237
|
+
return False
|
|
2238
|
+
if st.st_size == 0:
|
|
2239
|
+
return False
|
|
2240
|
+
age_ms = (time.time() - st.st_mtime) * 1000.0
|
|
2241
|
+
return age_ms > DRAIN_TRIGGER_MTIME_MS
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
def drain_now(*, force: bool = False) -> DrainStats:
|
|
2245
|
+
"""Execute 5-phase atomic drain. Bounded ≤K_MAX entries per call.
|
|
2246
|
+
|
|
2247
|
+
Fail-open invariant — any error sets stats.ok=False and is captured in
|
|
2248
|
+
stats.error. NEVER raises to caller.
|
|
2249
|
+
"""
|
|
2250
|
+
stats = DrainStats()
|
|
2251
|
+
if is_sync_mode() and not force:
|
|
2252
|
+
return stats
|
|
2253
|
+
try:
|
|
2254
|
+
if not force and not should_drain():
|
|
2255
|
+
return stats
|
|
2256
|
+
|
|
2257
|
+
drain_epoch = secrets.token_hex(4)
|
|
2258
|
+
stats.drain_epoch = drain_epoch
|
|
2259
|
+
state_dir = _state_dir()
|
|
2260
|
+
our_pid = os.getpid()
|
|
2261
|
+
log_path = _canonical_log_path()
|
|
2262
|
+
log_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
2263
|
+
lock_path = _canonical_log_lock()
|
|
2264
|
+
|
|
2265
|
+
# P1-2: flush any buffered journal envelopes BEFORE drain begins so
|
|
2266
|
+
# the journal reflects all begin/commit events up to this point
|
|
2267
|
+
# (op:"drained" envelopes are appended after canonical append).
|
|
2268
|
+
_flush_journal_buffer(our_pid)
|
|
2269
|
+
|
|
2270
|
+
# Phase 1 — canonical lock (deadlock-free; writers hold only their
|
|
2271
|
+
# own per-PID flocks, never the canonical lock).
|
|
2272
|
+
#
|
|
2273
|
+
# (ADR-055-AMEND-3) — opportunistic/forced split. A FORCED
|
|
2274
|
+
# drain (recovery / exit-handler / session-start) must complete, so it
|
|
2275
|
+
# blocks up to SPOOL_LOCK_TIMEOUT and a timeout there is anomalous. An
|
|
2276
|
+
# OPPORTUNISTIC drain (force=False, the per-emit hot path) is
|
|
2277
|
+
# best-effort: the lock holder plus the loser's own later drain cover
|
|
2278
|
+
# its events, so it acquires NON-BLOCKING (timeout=0 — a clean one-shot
|
|
2279
|
+
# try-lock, one flock(LOCK_NB) with no sleep) and yields silently on
|
|
2280
|
+
# contention instead of blocking the hook subprocess for 2.5s.
|
|
2281
|
+
canonical_timeout = SPOOL_LOCK_TIMEOUT if force else 0.0
|
|
2282
|
+
try:
|
|
2283
|
+
with FileLock(lock_path, timeout=canonical_timeout):
|
|
2284
|
+
files, in_recovery = _phase2_sweep_and_rename(
|
|
2285
|
+
state_dir, drain_epoch, our_pid,
|
|
2286
|
+
)
|
|
2287
|
+
stats.in_recovery_mode = in_recovery
|
|
2288
|
+
|
|
2289
|
+
valid_files: List[_DrainingFile] = []
|
|
2290
|
+
for f in files:
|
|
2291
|
+
if _phase2_validate_header(f, drain_epoch):
|
|
2292
|
+
# PLAN-119 WS-D1 — refuse _origin:"test" spool ONLY when
|
|
2293
|
+
# the canonical destination IS the live chain; applied
|
|
2294
|
+
# AFTER header validation and BEFORE phase 3, so test
|
|
2295
|
+
# entries never reach batch_lines / the canonical append.
|
|
2296
|
+
if _should_quarantine_test_origin(f, log_path):
|
|
2297
|
+
_quarantine_test_origin(f, drain_epoch)
|
|
2298
|
+
stats.test_origin_quarantined += 1
|
|
2299
|
+
else:
|
|
2300
|
+
valid_files.append(f)
|
|
2301
|
+
else:
|
|
2302
|
+
stats.quarantined_files += 1
|
|
2303
|
+
|
|
2304
|
+
# iter2-P0-1 + iter4-P2-2: phase 3 returns
|
|
2305
|
+
# (deduped, Dict[str, Set[int]], duplicate_consumed_count)
|
|
2306
|
+
deduped, partial_consumed, dup_consumed = (
|
|
2307
|
+
_phase3_collect_and_sort(
|
|
2308
|
+
valid_files, drain_epoch, stats,
|
|
2309
|
+
)
|
|
2310
|
+
)
|
|
2311
|
+
|
|
2312
|
+
# P0-2 + P1-3 + iter2-P0-1: phase 4 returns
|
|
2313
|
+
# (batch_lines, _, per_file_consumed_set, last_hmac, ids)
|
|
2314
|
+
# iter4-P2-2: pass dup_consumed as starting_processed so
|
|
2315
|
+
# combined Phase-3 + Phase-4 work bounded by K_MAX.
|
|
2316
|
+
(batch_lines, _, per_file_consumed, last_hmac,
|
|
2317
|
+
drained_ids) = _phase4_build_batch(
|
|
2318
|
+
deduped, drain_epoch, in_recovery, stats,
|
|
2319
|
+
starting_processed=dup_consumed,
|
|
2320
|
+
)
|
|
2321
|
+
|
|
2322
|
+
# iter2-P0-1: merge partial/duplicate-rejection consumed
|
|
2323
|
+
# indices (Set[int] union) into the Phase-4 per-file map so
|
|
2324
|
+
# Phase 5 split honors the COMPLEMENT for remainder lines.
|
|
2325
|
+
for path_key, idx_set in partial_consumed.items():
|
|
2326
|
+
if path_key in per_file_consumed:
|
|
2327
|
+
per_file_consumed[path_key] |= idx_set
|
|
2328
|
+
else:
|
|
2329
|
+
per_file_consumed[path_key] = set(idx_set)
|
|
2330
|
+
|
|
2331
|
+
stats.appended = _phase5_append_canonical(batch_lines, last_hmac)
|
|
2332
|
+
|
|
2333
|
+
# P1-3: emit op:"drained" envelopes grouped by pid + flush.
|
|
2334
|
+
drained_by_pid: Dict[int, List[str]] = {}
|
|
2335
|
+
for rec_id, pid in drained_ids:
|
|
2336
|
+
drained_by_pid.setdefault(pid, []).append(rec_id)
|
|
2337
|
+
for pid, ids in drained_by_pid.items():
|
|
2338
|
+
_journal_compact_drained(pid, ids, drain_epoch)
|
|
2339
|
+
|
|
2340
|
+
_phase5_split_and_cleanup(
|
|
2341
|
+
valid_files, per_file_consumed, drain_epoch, stats,
|
|
2342
|
+
)
|
|
2343
|
+
|
|
2344
|
+
# P1-2: drain-boundary journal flush for our own PID
|
|
2345
|
+
_flush_journal_buffer(our_pid)
|
|
2346
|
+
except FileLockTimeout:
|
|
2347
|
+
if force:
|
|
2348
|
+
# Forced drain could not complete — genuinely anomalous.
|
|
2349
|
+
stats.ok = False
|
|
2350
|
+
stats.error = "canonical_lock_timeout"
|
|
2351
|
+
_breadcrumb("drain canonical lock timeout")
|
|
2352
|
+
else:
|
|
2353
|
+
# Opportunistic yield — expected under concurrency, NOT an
|
|
2354
|
+
# error. Another drainer holds the lock; its global sweep plus
|
|
2355
|
+
# our own later drain cover our events, so ok stays True.
|
|
2356
|
+
stats.contended_skip = True
|
|
2357
|
+
# SEC veto-floor MF-1 (ADR-052): keep a genuinely wedged holder
|
|
2358
|
+
# observable. Emit a DISTINCT, gated breadcrumb ONLY when our
|
|
2359
|
+
# own spool is already stale past the staleness trigger (holder
|
|
2360
|
+
# not keeping up = real starvation). Benign single-winner
|
|
2361
|
+
# contention (fresh spool) stays silent, so the existing
|
|
2362
|
+
# audit-log.errors line-count detectors (ceo-diagnose.py /
|
|
2363
|
+
# status.py) still surface a wedge without the benign volume.
|
|
2364
|
+
if _own_spool_stale_past_trigger(our_pid):
|
|
2365
|
+
_breadcrumb(
|
|
2366
|
+
"drain canonical lock STARVED: own spool stale past "
|
|
2367
|
+
"trigger while opportunistic drain yielded"
|
|
2368
|
+
)
|
|
2369
|
+
return stats
|
|
2370
|
+
except Exception as e:
|
|
2371
|
+
stats.ok = False
|
|
2372
|
+
stats.error = f"{type(e).__name__}: {e}"
|
|
2373
|
+
_breadcrumb(f"drain_now unexpected: {type(e).__name__}: {e}")
|
|
2374
|
+
return stats
|
|
2375
|
+
|
|
2376
|
+
|
|
2377
|
+
# ---------------------------------------------------------------------------
|
|
2378
|
+
# Session-start reconciliation
|
|
2379
|
+
# ---------------------------------------------------------------------------
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def reconcile_journal_at_session_start() -> JournalReconciliation:
|
|
2383
|
+
"""Walk audit-pending.*.journal entries; classify counts; emit forensic.
|
|
2384
|
+
|
|
2385
|
+
Returns the JournalReconciliation populated with counts. Emits
|
|
2386
|
+
audit_flush_dropped_count via wired forensic callback (caller in
|
|
2387
|
+
audit_emit wires the callback before invoking this).
|
|
2388
|
+
"""
|
|
2389
|
+
rec = JournalReconciliation()
|
|
2390
|
+
state_dir = _state_dir()
|
|
2391
|
+
try:
|
|
2392
|
+
if not state_dir.exists():
|
|
2393
|
+
_forensic("audit_flush_dropped_count", _journal_rec_dict(rec))
|
|
2394
|
+
return rec
|
|
2395
|
+
names = sorted(os.listdir(str(state_dir)))
|
|
2396
|
+
except OSError:
|
|
2397
|
+
_forensic("audit_flush_dropped_count", _journal_rec_dict(rec))
|
|
2398
|
+
return rec
|
|
2399
|
+
|
|
2400
|
+
by_record: Dict[str, Dict[str, bool]] = {}
|
|
2401
|
+
for name in names:
|
|
2402
|
+
if not name.startswith(_JOURNAL_PREFIX + "."):
|
|
2403
|
+
continue
|
|
2404
|
+
if not name.endswith(".journal"):
|
|
2405
|
+
continue
|
|
2406
|
+
if name == _aggregate_journal_path().name:
|
|
2407
|
+
continue
|
|
2408
|
+
# Parse <pid> from "audit-pending.<pid>.journal"
|
|
2409
|
+
rest = name[len(_JOURNAL_PREFIX) + 1:]
|
|
2410
|
+
pid_str = rest[:-len(".journal")]
|
|
2411
|
+
try:
|
|
2412
|
+
pid = int(pid_str)
|
|
2413
|
+
except ValueError:
|
|
2414
|
+
continue
|
|
2415
|
+
if pid == os.getpid():
|
|
2416
|
+
continue
|
|
2417
|
+
if _is_alive_pid(pid):
|
|
2418
|
+
continue
|
|
2419
|
+
path = state_dir / name
|
|
2420
|
+
try:
|
|
2421
|
+
with path.open("r", encoding="utf-8") as f:
|
|
2422
|
+
for raw in f:
|
|
2423
|
+
stripped = raw.strip()
|
|
2424
|
+
if not stripped:
|
|
2425
|
+
continue
|
|
2426
|
+
try:
|
|
2427
|
+
env = json.loads(stripped)
|
|
2428
|
+
except json.JSONDecodeError:
|
|
2429
|
+
continue
|
|
2430
|
+
if not isinstance(env, dict):
|
|
2431
|
+
continue
|
|
2432
|
+
rid = env.get("record_id")
|
|
2433
|
+
op = env.get("op")
|
|
2434
|
+
if not isinstance(rid, str) or not isinstance(op, str):
|
|
2435
|
+
continue
|
|
2436
|
+
slot = by_record.setdefault(
|
|
2437
|
+
rid, {"begin": False, "commit": False, "drained": False},
|
|
2438
|
+
)
|
|
2439
|
+
if op in slot:
|
|
2440
|
+
slot[op] = True
|
|
2441
|
+
except OSError:
|
|
2442
|
+
continue
|
|
2443
|
+
|
|
2444
|
+
for rid, ops in by_record.items():
|
|
2445
|
+
if ops["begin"] and not ops["commit"]:
|
|
2446
|
+
rec.begin_no_commit += 1
|
|
2447
|
+
elif ops["commit"] and not ops["drained"]:
|
|
2448
|
+
rec.commit_no_drained += 1
|
|
2449
|
+
elif ops["commit"] and ops["drained"]:
|
|
2450
|
+
rec.recovered += 1
|
|
2451
|
+
|
|
2452
|
+
# Trigger a recovery drain to sweep commit_no_drained envelopes.
|
|
2453
|
+
if rec.commit_no_drained > 0:
|
|
2454
|
+
recov = drain_now(force=True)
|
|
2455
|
+
if recov.ok:
|
|
2456
|
+
# iter2-P1-2: canonical-append-before-unlink crash recovery
|
|
2457
|
+
# produces mostly `skipped_idempotent` (the entries are already
|
|
2458
|
+
# in the canonical log; next drain reads same bytes from
|
|
2459
|
+
# .draining, sees them in K_TAIL_WINDOW, skips). Sum both
|
|
2460
|
+
# counters so AC10 reports actual recovery count, not just the
|
|
2461
|
+
# rare appended-on-recovery subset.
|
|
2462
|
+
rec.recovered += recov.appended + recov.skipped_idempotent
|
|
2463
|
+
# iter3-P2-1: surface intentionally_deleted (duplicate-tuple
|
|
2464
|
+
# rejections during recovery) into the reconciliation counter
|
|
2465
|
+
# so the audit_flush_dropped_count emit carries it.
|
|
2466
|
+
rec.intentionally_deleted += recov.intentionally_deleted
|
|
2467
|
+
|
|
2468
|
+
_forensic("audit_flush_dropped_count", _journal_rec_dict(rec))
|
|
2469
|
+
return rec
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
def _journal_rec_dict(rec: JournalReconciliation) -> Dict[str, Any]:
|
|
2473
|
+
return {
|
|
2474
|
+
"begin_no_commit": rec.begin_no_commit,
|
|
2475
|
+
"commit_no_drained": rec.commit_no_drained,
|
|
2476
|
+
"recovered": rec.recovered,
|
|
2477
|
+
"truly_lost": rec.truly_lost,
|
|
2478
|
+
"tamper_rejected": rec.tamper_rejected,
|
|
2479
|
+
"intentionally_deleted": rec.intentionally_deleted,
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
|
|
2483
|
+
# ---------------------------------------------------------------------------
|
|
2484
|
+
# Exit handlers
|
|
2485
|
+
# ---------------------------------------------------------------------------
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
def _atexit_drain() -> None:
|
|
2489
|
+
"""atexit hook — best-effort final drain. Swallow all errors.
|
|
2490
|
+
|
|
2491
|
+
P1-2: also flushes the in-memory journal buffer for our PID so any
|
|
2492
|
+
begin/commit envelopes that hadn't hit the amortization threshold
|
|
2493
|
+
are durable before process exit.
|
|
2494
|
+
"""
|
|
2495
|
+
try:
|
|
2496
|
+
drain_now(force=True)
|
|
2497
|
+
except Exception:
|
|
2498
|
+
pass
|
|
2499
|
+
try:
|
|
2500
|
+
_flush_journal_buffer(os.getpid())
|
|
2501
|
+
except Exception:
|
|
2502
|
+
pass
|
|
2503
|
+
|
|
2504
|
+
|
|
2505
|
+
def _signal_drain_handler(signum: int, frame: Any) -> None:
|
|
2506
|
+
"""SIGTERM/SIGINT — force-drain then re-raise to default handler.
|
|
2507
|
+
|
|
2508
|
+
P1-1: if a writer is currently inside spool_append (holding the spool
|
|
2509
|
+
flock in our PID), bail without forcing a drain — the in-flight entry
|
|
2510
|
+
will be picked up by next-session reconciliation.
|
|
2511
|
+
|
|
2512
|
+
P1-2: flushes the journal buffer for our PID so begin/commit envelopes
|
|
2513
|
+
survive signal-driven termination.
|
|
2514
|
+
|
|
2515
|
+
P2-5: if the previous handler was signal.SIG_IGN, preserve it (the
|
|
2516
|
+
user explicitly ignored this signal; default termination would be a
|
|
2517
|
+
behavior change). atexit does NOT run after default signal termination
|
|
2518
|
+
so our drain call here IS the final flush opportunity for SIG_DFL paths.
|
|
2519
|
+
"""
|
|
2520
|
+
if _IN_SPOOL_APPEND:
|
|
2521
|
+
# Writer in progress; bail rather than deadlock against ourself.
|
|
2522
|
+
_breadcrumb(f"signal {signum} arrived during spool_append; bail")
|
|
2523
|
+
return
|
|
2524
|
+
try:
|
|
2525
|
+
drain_now(force=True)
|
|
2526
|
+
except Exception:
|
|
2527
|
+
pass
|
|
2528
|
+
try:
|
|
2529
|
+
_flush_journal_buffer(os.getpid())
|
|
2530
|
+
except Exception:
|
|
2531
|
+
pass
|
|
2532
|
+
# Resolve the previous handler.
|
|
2533
|
+
if signum == signal.SIGTERM:
|
|
2534
|
+
prev = _PREV_SIGTERM_HANDLER
|
|
2535
|
+
elif signum == signal.SIGINT:
|
|
2536
|
+
prev = _PREV_SIGINT_HANDLER
|
|
2537
|
+
else:
|
|
2538
|
+
prev = signal.SIG_DFL
|
|
2539
|
+
|
|
2540
|
+
# P2-5: distinguish SIG_IGN vs SIG_DFL vs callable.
|
|
2541
|
+
if prev is signal.SIG_IGN:
|
|
2542
|
+
# Restore IGN and do NOT re-raise — the user wanted this signal
|
|
2543
|
+
# ignored; honoring that is correctness, not failure.
|
|
2544
|
+
try:
|
|
2545
|
+
signal.signal(signum, signal.SIG_IGN)
|
|
2546
|
+
except (ValueError, OSError): # pragma: no cover
|
|
2547
|
+
pass
|
|
2548
|
+
return
|
|
2549
|
+
if callable(prev):
|
|
2550
|
+
# Chain to user handler.
|
|
2551
|
+
try:
|
|
2552
|
+
signal.signal(signum, prev)
|
|
2553
|
+
except (ValueError, OSError): # pragma: no cover
|
|
2554
|
+
pass
|
|
2555
|
+
try:
|
|
2556
|
+
prev(signum, frame)
|
|
2557
|
+
except Exception: # pragma: no cover
|
|
2558
|
+
pass
|
|
2559
|
+
return
|
|
2560
|
+
# Default: restore SIG_DFL and re-raise. atexit does NOT run after
|
|
2561
|
+
# default-signal termination → the drain_now(force=True) above is the
|
|
2562
|
+
# final flush opportunity.
|
|
2563
|
+
try:
|
|
2564
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
2565
|
+
except (ValueError, OSError): # pragma: no cover
|
|
2566
|
+
pass
|
|
2567
|
+
try:
|
|
2568
|
+
os.kill(os.getpid(), signum)
|
|
2569
|
+
except OSError: # pragma: no cover
|
|
2570
|
+
pass
|
|
2571
|
+
|
|
2572
|
+
|
|
2573
|
+
def install_exit_handlers() -> None:
|
|
2574
|
+
"""Register atexit + SIGTERM/SIGINT drain handlers (idempotent one-shot)."""
|
|
2575
|
+
global _EXIT_HANDLER_INSTALLED
|
|
2576
|
+
global _PREV_SIGTERM_HANDLER, _PREV_SIGINT_HANDLER
|
|
2577
|
+
if _EXIT_HANDLER_INSTALLED:
|
|
2578
|
+
return
|
|
2579
|
+
try:
|
|
2580
|
+
atexit.register(_atexit_drain)
|
|
2581
|
+
except Exception as e: # pragma: no cover
|
|
2582
|
+
_breadcrumb(f"atexit register failed: {type(e).__name__}: {e}")
|
|
2583
|
+
try:
|
|
2584
|
+
_PREV_SIGTERM_HANDLER = signal.signal(
|
|
2585
|
+
signal.SIGTERM, _signal_drain_handler,
|
|
2586
|
+
)
|
|
2587
|
+
except (ValueError, OSError) as e:
|
|
2588
|
+
# Signal handlers only work on main thread; in worker threads
|
|
2589
|
+
# signal.signal raises ValueError. Fail-open.
|
|
2590
|
+
_breadcrumb(f"SIGTERM install skipped: {type(e).__name__}: {e}")
|
|
2591
|
+
try:
|
|
2592
|
+
_PREV_SIGINT_HANDLER = signal.signal(
|
|
2593
|
+
signal.SIGINT, _signal_drain_handler,
|
|
2594
|
+
)
|
|
2595
|
+
except (ValueError, OSError) as e:
|
|
2596
|
+
_breadcrumb(f"SIGINT install skipped: {type(e).__name__}: {e}")
|
|
2597
|
+
_EXIT_HANDLER_INSTALLED = True
|
|
2598
|
+
|
|
2599
|
+
|
|
2600
|
+
# ---------------------------------------------------------------------------
|
|
2601
|
+
# Test helpers (NOT public API — prefixed _; harness uses these)
|
|
2602
|
+
# ---------------------------------------------------------------------------
|
|
2603
|
+
|
|
2604
|
+
|
|
2605
|
+
def _reset_for_test() -> None:
|
|
2606
|
+
"""Clear all in-process spool state (caches + handler latch + buffer)."""
|
|
2607
|
+
global _EXIT_HANDLER_INSTALLED, _IN_FORENSIC_EMIT, _IN_SPOOL_APPEND
|
|
2608
|
+
_SPOOL_HEADER_CACHE.clear()
|
|
2609
|
+
_ORDINAL_COUNTER.clear()
|
|
2610
|
+
_JOURNAL_BUFFER.clear()
|
|
2611
|
+
_EXIT_HANDLER_INSTALLED = False
|
|
2612
|
+
_IN_FORENSIC_EMIT = False
|
|
2613
|
+
_IN_SPOOL_APPEND = False
|