wicked-garden 12.27.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-plugin/archetypes.json +166 -0
- package/.claude-plugin/components.json +75 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +174 -0
- package/.claude-plugin/specialist.json +48 -0
- package/.claude-plugin/stack-registry.json +96 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +712 -0
- package/CONTRIBUTING.md +99 -0
- package/ETHOS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
- package/agents/agentic/architect.md +449 -0
- package/agents/agentic/performance-analyst.md +702 -0
- package/agents/agentic/safety-reviewer.md +662 -0
- package/agents/crew/implementer.md +163 -0
- package/agents/crew/researcher.md +94 -0
- package/agents/crew/reviewer.md +184 -0
- package/agents/data/data-engineer.md +227 -0
- package/agents/engineering/api-documentarian.md +440 -0
- package/agents/engineering/migration-engineer.md +301 -0
- package/agents/engineering/solution-architect.md +208 -0
- package/agents/jam/brainstorm-facilitator.md +297 -0
- package/agents/jam/council.md +322 -0
- package/agents/jam/quick-facilitator.md +85 -0
- package/agents/persona/persona-agent.md +121 -0
- package/agents/platform/compliance-officer.md +266 -0
- package/agents/platform/privacy-expert.md +496 -0
- package/agents/platform/security-engineer.md +287 -0
- package/agents/product/a11y-expert.md +323 -0
- package/agents/product/requirements-analyst.md +238 -0
- package/agents/product/ui-reviewer.md +180 -0
- package/agents/product/ux-designer.md +199 -0
- package/agents/product/value-strategist.md +269 -0
- package/agents/qe/semantic-reviewer.md +78 -0
- package/commands/agentic/audit.md +24 -0
- package/commands/agentic/design.md +24 -0
- package/commands/agentic/frameworks.md +25 -0
- package/commands/agentic/review.md +27 -0
- package/commands/archetype/build.md +12 -0
- package/commands/archetype/decide.md +12 -0
- package/commands/archetype/explore.md +12 -0
- package/commands/archetype/incident.md +12 -0
- package/commands/archetype/migrate.md +12 -0
- package/commands/archetype/modernize.md +12 -0
- package/commands/archetype/review.md +14 -0
- package/commands/archetype/ship.md +12 -0
- package/commands/archetype/specify.md +12 -0
- package/commands/archetype/triage.md +12 -0
- package/commands/compile.md +37 -0
- package/commands/data/analyze.md +18 -0
- package/commands/data/data.md +21 -0
- package/commands/data/ml.md +18 -0
- package/commands/data/ontology.md +22 -0
- package/commands/data/pipeline.md +17 -0
- package/commands/deliberate.md +181 -0
- package/commands/engineering/add-field.md +77 -0
- package/commands/engineering/apply.md +58 -0
- package/commands/engineering/arch.md +26 -0
- package/commands/engineering/debug.md +27 -0
- package/commands/engineering/docs.md +27 -0
- package/commands/engineering/new-generator.md +257 -0
- package/commands/engineering/patch-plan.md +59 -0
- package/commands/engineering/plan.md +29 -0
- package/commands/engineering/remove.md +48 -0
- package/commands/engineering/rename.md +44 -0
- package/commands/engineering/review.md +32 -0
- package/commands/help.md +128 -0
- package/commands/install.md +118 -0
- package/commands/intent.md +54 -0
- package/commands/jam/brainstorm.md +37 -0
- package/commands/jam/council.md +49 -0
- package/commands/jam/quick.md +21 -0
- package/commands/jam/revisit.md +79 -0
- package/commands/persona/as.md +136 -0
- package/commands/persona/define.md +24 -0
- package/commands/persona/list.md +118 -0
- package/commands/platform/actions.md +20 -0
- package/commands/platform/assert.md +18 -0
- package/commands/platform/audit.md +23 -0
- package/commands/platform/compliance.md +20 -0
- package/commands/platform/gh.md +22 -0
- package/commands/platform/health.md +22 -0
- package/commands/platform/incident.md +23 -0
- package/commands/platform/infra.md +24 -0
- package/commands/platform/peer-health.md +103 -0
- package/commands/platform/plugin-health.md +58 -0
- package/commands/platform/security.md +38 -0
- package/commands/platform/toolchain.md +96 -0
- package/commands/platform/traces.md +20 -0
- package/commands/product/a11y.md +19 -0
- package/commands/product/acceptance.md +19 -0
- package/commands/product/align.md +20 -0
- package/commands/product/analyze.md +23 -0
- package/commands/product/elicit.md +19 -0
- package/commands/product/listen.md +24 -0
- package/commands/product/mockup.md +20 -0
- package/commands/product/screenshot.md +19 -0
- package/commands/product/strategy.md +40 -0
- package/commands/product/synthesize.md +23 -0
- package/commands/product/ux-review.md +42 -0
- package/commands/product/ux.md +20 -0
- package/commands/product/visual-direction.md +34 -0
- package/commands/prove.md +47 -0
- package/commands/report-issue.md +243 -0
- package/commands/reset.md +144 -0
- package/commands/search/blast-radius.md +58 -0
- package/commands/search/hotspots.md +55 -0
- package/commands/search/index.md +38 -0
- package/commands/search/lineage.md +47 -0
- package/commands/search/service-map.md +79 -0
- package/commands/setup.md +363 -0
- package/commands/smaht/briefing.md +200 -0
- package/commands/smaht/events-import.md +95 -0
- package/commands/smaht/state.md +19 -0
- package/commands/where-am-i.md +58 -0
- package/docs/README.md +32 -0
- package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
- package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
- package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
- package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
- package/docs/brain-chunk-format.md +135 -0
- package/docs/compiler.md +95 -0
- package/docs/daemon-api.md +70 -0
- package/docs/domains.md +197 -0
- package/docs/getting-started.md +208 -0
- package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
- package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
- package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
- package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
- package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
- package/docs/required-peers.md +60 -0
- package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
- package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
- package/docs/v11/archetypes.md +231 -0
- package/hooks/hooks.json +275 -0
- package/hooks/scripts/bootstrap.py +1797 -0
- package/hooks/scripts/invoke.py +62 -0
- package/hooks/scripts/notification.py +166 -0
- package/hooks/scripts/permission_request.py +223 -0
- package/hooks/scripts/post_tool.py +1374 -0
- package/hooks/scripts/pre_compact.py +272 -0
- package/hooks/scripts/pre_tool.py +1278 -0
- package/hooks/scripts/prompt_submit.py +1109 -0
- package/hooks/scripts/session_end.py +95 -0
- package/hooks/scripts/stop.py +624 -0
- package/hooks/scripts/subagent_lifecycle.py +464 -0
- package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
- package/hooks/scripts/task_completed.py +322 -0
- package/hooks/scripts/worktree_guard.py +148 -0
- package/hooks/subscriptions/on_gate_decided.json +9 -0
- package/install.mjs +128 -0
- package/package.json +41 -0
- package/pyproject.toml +59 -0
- package/schemas/README.md +32 -0
- package/schemas/assert_contracts.json +37 -0
- package/schemas/health_probe.json +46 -0
- package/schemas/plugin_status.json +37 -0
- package/scripts/_adapters/__init__.py +166 -0
- package/scripts/_adapters/jira_adapter.py +18 -0
- package/scripts/_adapters/linear_adapter.py +18 -0
- package/scripts/_adapters/miro_adapter.py +18 -0
- package/scripts/_adapters/notion_adapter.py +18 -0
- package/scripts/_agents.py +357 -0
- package/scripts/_brain_ingest/__init__.py +2 -0
- package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
- package/scripts/_brain_port.py +163 -0
- package/scripts/_bus.py +1081 -0
- package/scripts/_bus_catalog_gen.py +93 -0
- package/scripts/_bus_consumers.json +46 -0
- package/scripts/_bus_emit.py +43 -0
- package/scripts/_capability_registry.py +334 -0
- package/scripts/_capability_resolver.py +322 -0
- package/scripts/_domain_store.py +559 -0
- package/scripts/_event_log_reader.py +154 -0
- package/scripts/_event_schema.py +305 -0
- package/scripts/_event_store.py +397 -0
- package/scripts/_heavy_cadence.py +324 -0
- package/scripts/_integration_resolver.py +321 -0
- package/scripts/_logger.py +151 -0
- package/scripts/_loom.py +482 -0
- package/scripts/_paths.py +133 -0
- package/scripts/_python.sh +22 -0
- package/scripts/_run.py +137 -0
- package/scripts/_session.py +502 -0
- package/scripts/_validate_registry.py +1042 -0
- package/scripts/_wicked_testing_probe.py +277 -0
- package/scripts/agentic/analyze_agents.py +391 -0
- package/scripts/agentic/detect_framework.py +420 -0
- package/scripts/agentic/issue_taxonomy.py +357 -0
- package/scripts/agentic/pattern_scorer.py +434 -0
- package/scripts/classify/persist.py +169 -0
- package/scripts/compiler/compile.py +552 -0
- package/scripts/compiler/phase0/VERDICT.md +60 -0
- package/scripts/compiler/phase0/detect.py +292 -0
- package/scripts/compiler/phase0/emit.py +228 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
- package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
- package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/verdict.json +104 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
- package/scripts/compiler/phase0/run_probe.py +234 -0
- package/scripts/compiler/phase0/wire_vault.py +310 -0
- package/scripts/crew/__init__.py +0 -0
- package/scripts/crew/_task_audit_writer.py +243 -0
- package/scripts/crew/_task_reader.py +389 -0
- package/scripts/crew/archetypes_v11.py +348 -0
- package/scripts/crew/detectors/__init__.py +12 -0
- package/scripts/crew/flow_compiler.py +121 -0
- package/scripts/crew/phase_manager.py +703 -0
- package/scripts/crew/scope_delta.py +264 -0
- package/scripts/crew/specialist_resolver.py +218 -0
- package/scripts/crew/stack_registry.py +189 -0
- package/scripts/data/data_profiler.py +271 -0
- package/scripts/data/ontology_recommender.py +231 -0
- package/scripts/data/schema_validator.py +267 -0
- package/scripts/delivery/drift.py +739 -0
- package/scripts/delivery/telemetry.py +614 -0
- package/scripts/engineering/patch/codegraph_db.py +125 -0
- package/scripts/engineering/patch/generators/__init__.py +56 -0
- package/scripts/engineering/patch/generators/base.py +373 -0
- package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
- package/scripts/engineering/patch/generators/go_generator.py +430 -0
- package/scripts/engineering/patch/generators/java_generator.py +928 -0
- package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
- package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
- package/scripts/engineering/patch/generators/perl_generator.py +510 -0
- package/scripts/engineering/patch/generators/php_generator.py +644 -0
- package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
- package/scripts/engineering/patch/generators/python_generator.py +487 -0
- package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
- package/scripts/engineering/patch/generators/rust_generator.py +400 -0
- package/scripts/engineering/patch/generators/sql_generator.py +594 -0
- package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
- package/scripts/engineering/patch/patch.py +800 -0
- package/scripts/engineering/patch/safety.py +429 -0
- package/scripts/jam/__init__.py +0 -0
- package/scripts/jam/_bus_consumers.py +271 -0
- package/scripts/jam/agentic_cli_registry.py +703 -0
- package/scripts/jam/consensus.py +737 -0
- package/scripts/jam/detect_clis.py +409 -0
- package/scripts/jam/jam.py +283 -0
- package/scripts/jam/save_transcript.py +171 -0
- package/scripts/loom/__init__.py +22 -0
- package/scripts/loom/cli.py +170 -0
- package/scripts/loom/compose.py +180 -0
- package/scripts/loom/gate.py +120 -0
- package/scripts/loom/manifest.py +140 -0
- package/scripts/loom/resolve.py +59 -0
- package/scripts/persona/__init__.py +0 -0
- package/scripts/persona/registry.py +688 -0
- package/scripts/platform/guard_pipeline.py +1214 -0
- package/scripts/platform/guard_profiles.py +216 -0
- package/scripts/platform/observability/assert_contracts.py +434 -0
- package/scripts/platform/observability/health_probe.py +915 -0
- package/scripts/platform/observability/ops_log_viewer.py +209 -0
- package/scripts/platform/prereq_doctor.py +567 -0
- package/scripts/qe/conditions_manifest.py +260 -0
- package/scripts/qe/content_sanitizer.py +160 -0
- package/scripts/qe/coverage_tracker.py +535 -0
- package/scripts/qe/discover_scenarios.py +243 -0
- package/scripts/qe/evidence_tracker.py +232 -0
- package/scripts/qe/prove.py +196 -0
- package/scripts/qe/registry_lookup.py +80 -0
- package/scripts/qe/registry_store.py +176 -0
- package/scripts/qe/semantic_review.py +1005 -0
- package/scripts/qe/vault_gate.py +382 -0
- package/scripts/qe/verdict_audit.py +175 -0
- package/scripts/qe/verdict_schema.py +335 -0
- package/scripts/reset.py +220 -0
- package/scripts/resolve_path.py +22 -0
- package/scripts/sentinel/invariants.py +402 -0
- package/scripts/sentinel/pre_push.py +73 -0
- package/scripts/setup/__init__.py +1 -0
- package/scripts/setup/detect_state.py +114 -0
- package/scripts/setup/migrate_legacy.py +67 -0
- package/scripts/setup/migrate_v6_projects.py +232 -0
- package/scripts/setup/onboarding.py +114 -0
- package/scripts/smaht/_bus_consumers.py +98 -0
- package/scripts/smaht/adapters/__init__.py +83 -0
- package/scripts/smaht/adapters/brain_adapter.py +254 -0
- package/scripts/smaht/adapters/context7_adapter.py +438 -0
- package/scripts/smaht/adapters/delegation_adapter.py +104 -0
- package/scripts/smaht/adapters/domain_adapter.py +206 -0
- package/scripts/smaht/adapters/events_adapter.py +231 -0
- package/scripts/smaht/adapters/tools_adapter.py +49 -0
- package/scripts/smaht/cheatsheet_store.py +236 -0
- package/scripts/smaht/context_package.py +386 -0
- package/scripts/smaht/knowledge_graph.py +395 -0
- package/scripts/smaht/propose_skills.py +856 -0
- package/scripts/smaht/test_cheatsheet_store.py +428 -0
- package/scripts/statusline.py +151 -0
- package/scripts/where_am_i.py +236 -0
- package/skills/agentic/agentic-patterns/SKILL.md +91 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
- package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
- package/skills/agentic/context-engineering/SKILL.md +142 -0
- package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
- package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
- package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
- package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
- package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
- package/skills/agentic/frameworks/SKILL.md +191 -0
- package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
- package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
- package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
- package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
- package/skills/agentic/refs/audit.md +134 -0
- package/skills/agentic/refs/design.md +137 -0
- package/skills/agentic/refs/frameworks.md +102 -0
- package/skills/agentic/refs/review.md +160 -0
- package/skills/agentic/review-methodology/SKILL.md +186 -0
- package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
- package/skills/agentic/trust-and-safety/SKILL.md +177 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
- package/skills/archetype/SKILL.md +104 -0
- package/skills/archetype/refs/build.md +142 -0
- package/skills/archetype/refs/decide.md +117 -0
- package/skills/archetype/refs/explore.md +66 -0
- package/skills/archetype/refs/incident.md +149 -0
- package/skills/archetype/refs/migrate.md +170 -0
- package/skills/archetype/refs/modernize.md +187 -0
- package/skills/archetype/refs/review.md +145 -0
- package/skills/archetype/refs/ship.md +117 -0
- package/skills/archetype/refs/specify.md +108 -0
- package/skills/archetype/refs/triage.md +62 -0
- package/skills/classify/SKILL.md +157 -0
- package/skills/data/analysis/SKILL.md +125 -0
- package/skills/data/analysis/refs/templates.md +170 -0
- package/skills/data/data/SKILL.md +118 -0
- package/skills/data/data/refs/examples.md +235 -0
- package/skills/data/ml/SKILL.md +125 -0
- package/skills/data/ml/refs/templates.md +172 -0
- package/skills/data/pipeline/SKILL.md +136 -0
- package/skills/data/pipeline/refs/templates.md +175 -0
- package/skills/data/refs/analyze.md +123 -0
- package/skills/data/refs/data.md +95 -0
- package/skills/data/refs/ml.md +118 -0
- package/skills/data/refs/pipeline.md +92 -0
- package/skills/deliberate/SKILL.md +125 -0
- package/skills/deliberate/refs/architecture-lens.md +37 -0
- package/skills/deliberate/refs/assumptions-lens.md +43 -0
- package/skills/deliberate/refs/code-lens.md +38 -0
- package/skills/deliberate/refs/content-lens.md +37 -0
- package/skills/deliberate/refs/data-lens.md +43 -0
- package/skills/deliberate/refs/design-lens.md +37 -0
- package/skills/deliberate/refs/opportunity-patterns.md +107 -0
- package/skills/deliberate/refs/ops-lens.md +43 -0
- package/skills/deliberate/refs/product-lens.md +46 -0
- package/skills/deliberate/refs/rethink-framework.md +77 -0
- package/skills/deliberate/refs/security-lens.md +47 -0
- package/skills/deliberate/refs/testing-lens.md +45 -0
- package/skills/deliberate/refs/timing-lens.md +42 -0
- package/skills/engineering/architecture/SKILL.md +177 -0
- package/skills/engineering/architecture/refs/adr-template.md +268 -0
- package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
- package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
- package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
- package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
- package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
- package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
- package/skills/engineering/backend/SKILL.md +102 -0
- package/skills/engineering/backend/refs/checklists.md +146 -0
- package/skills/engineering/backend/refs/patterns.md +181 -0
- package/skills/engineering/debugging/SKILL.md +99 -0
- package/skills/engineering/debugging/refs/patterns.md +248 -0
- package/skills/engineering/debugging/refs/process.md +203 -0
- package/skills/engineering/docs/SKILL.md +97 -0
- package/skills/engineering/engineering/SKILL.md +88 -0
- package/skills/engineering/engineering/refs/checklists.md +91 -0
- package/skills/engineering/engineering/refs/templates.md +71 -0
- package/skills/engineering/frontend/SKILL.md +92 -0
- package/skills/engineering/frontend/refs/checklists.md +96 -0
- package/skills/engineering/frontend/refs/patterns.md +167 -0
- package/skills/engineering/integration/SKILL.md +183 -0
- package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
- package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
- package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
- package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
- package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
- package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
- package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
- package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
- package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
- package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
- package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
- package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
- package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
- package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
- package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
- package/skills/engineering/integration/refs/grpc-testing.md +49 -0
- package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
- package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
- package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
- package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
- package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
- package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
- package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
- package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
- package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
- package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
- package/skills/engineering/large-scale-migration/SKILL.md +65 -0
- package/skills/engineering/patch/SKILL.md +184 -0
- package/skills/engineering/refs/arch.md +126 -0
- package/skills/engineering/refs/debug.md +142 -0
- package/skills/engineering/refs/docs.md +190 -0
- package/skills/engineering/refs/plan.md +109 -0
- package/skills/engineering/refs/review.md +138 -0
- package/skills/engineering/system-design/SKILL.md +154 -0
- package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
- package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
- package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
- package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
- package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
- package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
- package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
- package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
- package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
- package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
- package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
- package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
- package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
- package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
- package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
- package/skills/engineering/unit-test-quality/SKILL.md +194 -0
- package/skills/ground/SKILL.md +130 -0
- package/skills/integration-discovery/SKILL.md +168 -0
- package/skills/integration-discovery/refs/capabilities.md +282 -0
- package/skills/integration-discovery/refs/cli-detection.md +295 -0
- package/skills/integration-discovery/refs/discovery-sources.md +233 -0
- package/skills/integration-discovery/refs/task-patterns.md +206 -0
- package/skills/jam/SKILL.md +45 -0
- package/skills/jam/refs/facilitation-patterns.md +125 -0
- package/skills/jam/refs/quick.md +64 -0
- package/skills/jam/refs/synthesis-patterns.md +114 -0
- package/skills/multi-model/SKILL.md +168 -0
- package/skills/multi-model/refs/aichat.md +119 -0
- package/skills/multi-model/refs/aider.md +100 -0
- package/skills/multi-model/refs/auditability.md +238 -0
- package/skills/multi-model/refs/codex.md +169 -0
- package/skills/multi-model/refs/context.md +306 -0
- package/skills/multi-model/refs/copilot.md +161 -0
- package/skills/multi-model/refs/examples.md +224 -0
- package/skills/multi-model/refs/gemini.md +185 -0
- package/skills/multi-model/refs/goose.md +112 -0
- package/skills/multi-model/refs/llm.md +124 -0
- package/skills/multi-model/refs/opencode.md +196 -0
- package/skills/multi-model/refs/orchestration.md +188 -0
- package/skills/multi-model/refs/pi.md +208 -0
- package/skills/persona/SKILL.md +144 -0
- package/skills/persona/refs/define.md +133 -0
- package/skills/platform/actions/refs/actions.md +114 -0
- package/skills/platform/audit/SKILL.md +174 -0
- package/skills/platform/audit/refs/audit.md +145 -0
- package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
- package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
- package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
- package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
- package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
- package/skills/platform/compliance/SKILL.md +169 -0
- package/skills/platform/compliance/refs/checklists.md +303 -0
- package/skills/platform/compliance/refs/compliance.md +117 -0
- package/skills/platform/compliance/refs/frameworks.md +282 -0
- package/skills/platform/errors/SKILL.md +183 -0
- package/skills/platform/errors/refs/patterns.md +271 -0
- package/skills/platform/errors/refs/severity.md +264 -0
- package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
- package/skills/platform/gh/refs/gh.md +105 -0
- package/skills/platform/gh-cli/SKILL.md +120 -0
- package/skills/platform/gh-cli/refs/automation.md +63 -0
- package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
- package/skills/platform/gh-cli/refs/patterns.md +132 -0
- package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
- package/skills/platform/github-actions/SKILL.md +159 -0
- package/skills/platform/github-actions/refs/security.md +150 -0
- package/skills/platform/github-actions/refs/templates.md +230 -0
- package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
- package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
- package/skills/platform/gitlab-ci/SKILL.md +140 -0
- package/skills/platform/gitlab-ci/refs/templates.md +223 -0
- package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
- package/skills/platform/glab-cli/SKILL.md +133 -0
- package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
- package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
- package/skills/platform/glab-cli/refs/patterns.md +131 -0
- package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
- package/skills/platform/health/SKILL.md +177 -0
- package/skills/platform/health/refs/health.md +72 -0
- package/skills/platform/health/refs/sources.md +302 -0
- package/skills/platform/incident/refs/incident.md +136 -0
- package/skills/platform/infra/refs/infra.md +104 -0
- package/skills/platform/observability/SKILL.md +128 -0
- package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
- package/skills/platform/prereq-doctor/SKILL.md +57 -0
- package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
- package/skills/platform/traces/refs/traces.md +101 -0
- package/skills/product/acceptance-criteria/SKILL.md +193 -0
- package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
- package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
- package/skills/product/accessibility/SKILL.md +197 -0
- package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
- package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
- package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
- package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
- package/skills/product/accessibility/refs/audit-report-results.md +276 -0
- package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
- package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
- package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
- package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
- package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
- package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
- package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
- package/skills/product/analyze/SKILL.md +164 -0
- package/skills/product/analyze/refs/algorithms.md +238 -0
- package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
- package/skills/product/imagery/SKILL.md +97 -0
- package/skills/product/imagery/alter/SKILL.md +105 -0
- package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
- package/skills/product/imagery/create/SKILL.md +109 -0
- package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
- package/skills/product/imagery/create/refs/provider-reference.md +219 -0
- package/skills/product/imagery/review/SKILL.md +101 -0
- package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
- package/skills/product/imagery/review/refs/quality-gates.md +153 -0
- package/skills/product/imagery/scripts/analyze.sh +23 -0
- package/skills/product/imagery/scripts/edit.sh +14 -0
- package/skills/product/imagery/scripts/generate.sh +13 -0
- package/skills/product/imagery/scripts/provider.py +787 -0
- package/skills/product/listen/SKILL.md +175 -0
- package/skills/product/listen/refs/channels.md +220 -0
- package/skills/product/mockup/SKILL.md +106 -0
- package/skills/product/mockup/refs/templates.md +95 -0
- package/skills/product/refs/a11y.md +109 -0
- package/skills/product/refs/acceptance.md +79 -0
- package/skills/product/refs/align.md +67 -0
- package/skills/product/refs/analyze.md +77 -0
- package/skills/product/refs/elicit.md +67 -0
- package/skills/product/refs/listen.md +58 -0
- package/skills/product/refs/mockup.md +77 -0
- package/skills/product/refs/screenshot.md +59 -0
- package/skills/product/refs/strategy.md +77 -0
- package/skills/product/refs/synthesize.md +54 -0
- package/skills/product/refs/ux-review.md +68 -0
- package/skills/product/refs/ux.md +101 -0
- package/skills/product/requirements-analysis/SKILL.md +136 -0
- package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
- package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
- package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
- package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
- package/skills/product/requirements-graph/SKILL.md +155 -0
- package/skills/product/requirements-graph/refs/examples.md +223 -0
- package/skills/product/requirements-graph/refs/schema.md +162 -0
- package/skills/product/requirements-migrate/SKILL.md +90 -0
- package/skills/product/requirements-navigate/SKILL.md +141 -0
- package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
- package/skills/product/screenshot/SKILL.md +147 -0
- package/skills/product/strategy/SKILL.md +198 -0
- package/skills/product/strategy/refs/frameworks.md +295 -0
- package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
- package/skills/product/strategy/refs/templates-roi-value.md +207 -0
- package/skills/product/synthesize/SKILL.md +116 -0
- package/skills/product/synthesize/refs/journey-mapping.md +212 -0
- package/skills/product/synthesize/refs/prioritization.md +193 -0
- package/skills/product/ux-review/SKILL.md +164 -0
- package/skills/product/visual-direction/SKILL.md +137 -0
- package/skills/product/visual-review/SKILL.md +131 -0
- package/skills/runtime-exec/SKILL.md +138 -0
- package/skills/search/codebase-narrator/SKILL.md +131 -0
- package/skills/search/codebase-narrator/refs/output-template.md +100 -0
- package/skills/smaht/SKILL.md +72 -0
- package/skills/smaht/discovery/SKILL.md +65 -0
- package/skills/smaht/intent/SKILL.md +89 -0
- package/skills/smaht/propose-skills/SKILL.md +142 -0
- package/skills/smaht/refs/state.md +76 -0
- package/skills/swarm/SKILL.md +132 -0
- package/skills/swarm/refs/fan-out.md +104 -0
- package/skills/swarm/refs/independent-verification.md +86 -0
- package/skills/swarm/refs/receipts-and-evidence.md +69 -0
- package/skills/swarm/refs/recon-synthesis.md +87 -0
- package/skills/swarm/refs/ship-discipline.md +90 -0
- package/skills/wickedizer/SKILL.md +147 -0
- package/skills/wickedizer/refs/artifact-types.md +238 -0
- package/skills/wickedizer/refs/complexity.md +278 -0
- package/skills/wickedizer/refs/de-ai-pass.md +140 -0
- package/skills/wickedizer/refs/failure-modes.md +249 -0
- package/skills/wickedizer/refs/patterns.md +282 -0
- package/skills/wickedizer/refs/review.md +221 -0
- package/skills/wickedizer/refs/structure.md +282 -0
- package/skills/wickedizer/refs/voice-profile.md +125 -0
- package/skills/workflow/SKILL.md +132 -0
- package/skills/workflow/refs/archetype-adjustments.md +205 -0
- package/skills/workflow/refs/evidence.md +58 -0
- package/skills/workflow/refs/integration.md +130 -0
- package/skills/workflow/refs/risk-dimension-signals.md +260 -0
- package/skills/workflow/refs/scoring-rubric.md +49 -0
- package/skills/workflow/refs/signal-keywords.md +176 -0
- package/skills/workflow/refs/specialist-routing-rules.md +159 -0
- package/skills/worktrees/SKILL.md +87 -0
- package/skills/worktrees/refs/recipes.md +96 -0
package/scripts/_bus.py
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
_bus.py — wicked-bus integration shim for wicked-garden.
|
|
4
|
+
|
|
5
|
+
Fire-and-forget event emission + poll-on-invoke consumption.
|
|
6
|
+
Bus unavailable = no-op. Never blocks the caller. Never raises.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from _bus import emit_event, poll_pending, BUS_EVENT_MAP
|
|
10
|
+
|
|
11
|
+
# Emit (fire-and-forget, returns immediately)
|
|
12
|
+
emit_event("wicked.phase.transitioned", {
|
|
13
|
+
"project_id": "abc", "phase_from": "clarify", "phase_to": "design",
|
|
14
|
+
}, chain_id="abc123.root")
|
|
15
|
+
|
|
16
|
+
# Poll (returns list of events, acks after processing)
|
|
17
|
+
events = poll_pending(event_type_prefix="wicked.gate.")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("wicked-garden.bus")
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Static event catalog — Phase 1 events only (those with wired consumers).
|
|
33
|
+
# New events are added here when their emit points + consumers ship.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
BUS_EVENT_MAP: Dict[str, Dict[str, str]] = {
|
|
37
|
+
# Crew domain — phase_manager.py + propose-process facilitator skill
|
|
38
|
+
"wicked.project.created": {
|
|
39
|
+
"domain": "wicked-garden",
|
|
40
|
+
"subdomain": "crew.project",
|
|
41
|
+
"description": "New crew project created with complexity scoring",
|
|
42
|
+
},
|
|
43
|
+
"wicked.project.completed": {
|
|
44
|
+
"domain": "wicked-garden",
|
|
45
|
+
"subdomain": "crew.project",
|
|
46
|
+
"description": "Crew project completed (final phase approved)",
|
|
47
|
+
},
|
|
48
|
+
# v11.1.1 archetype lifecycle events. The v11.0.0/v11.1.0 phase_manager
|
|
49
|
+
# rewrote the project state but didn't register any v11 event types,
|
|
50
|
+
# so emit_event(...) was returning early on Unknown event type.
|
|
51
|
+
# These four events let v11 archetype-mode projects flow through the
|
|
52
|
+
# bus exactly the way v6 universal-pipeline projects did, without
|
|
53
|
+
# the v6 gate ceremony.
|
|
54
|
+
"wicked.archetype.created": {
|
|
55
|
+
"domain": "wicked-garden",
|
|
56
|
+
"subdomain": "crew.archetype",
|
|
57
|
+
"description": "v11 archetype-mode project created (carries v11_archetype + initial phase_plan)",
|
|
58
|
+
},
|
|
59
|
+
"wicked.archetype.advanced": {
|
|
60
|
+
"domain": "wicked-garden",
|
|
61
|
+
"subdomain": "crew.archetype",
|
|
62
|
+
"description": "v11 archetype phase approved + (when present) next phase named",
|
|
63
|
+
},
|
|
64
|
+
"wicked.archetype.completed": {
|
|
65
|
+
"domain": "wicked-garden",
|
|
66
|
+
"subdomain": "crew.archetype",
|
|
67
|
+
"description": "v11 archetype final phase approved (project is_complete)",
|
|
68
|
+
},
|
|
69
|
+
"wicked.archetype.hard_gate_passed": {
|
|
70
|
+
"domain": "wicked-garden",
|
|
71
|
+
"subdomain": "crew.archetype",
|
|
72
|
+
"description": "v11 archetype hard gate (cutover/mitigate/etc.) passed with confirmed_by + evidence",
|
|
73
|
+
},
|
|
74
|
+
"wicked.archetype.classified": {
|
|
75
|
+
"domain": "wicked-garden",
|
|
76
|
+
"subdomain": "crew.classify",
|
|
77
|
+
"description": "v11 prompt classified into work-shape archetype set (LLM or regex tier)",
|
|
78
|
+
},
|
|
79
|
+
# modernize archetype — stack_registry.py dispatch. Emitted when the
|
|
80
|
+
# `modernize` discover phase resolves a legacy stack class whose status
|
|
81
|
+
# is planned/none, OR an unknown stack. The reader NEVER fabricates a
|
|
82
|
+
# migration for these — it emits this gap event + returns a gap-task dict
|
|
83
|
+
# for the caller to hand to TaskCreate, then STOPS (fail-closed ETHOS).
|
|
84
|
+
"wicked.modernize.stack_gap": {
|
|
85
|
+
"domain": "wicked-garden",
|
|
86
|
+
"subdomain": "crew.modernize",
|
|
87
|
+
"description": "Legacy stack class is planned/none/unknown — capability-gap task emitted instead of a fabricated migration",
|
|
88
|
+
},
|
|
89
|
+
"wicked.project.complexity_scored": {
|
|
90
|
+
"domain": "wicked-garden",
|
|
91
|
+
"subdomain": "crew.scoring",
|
|
92
|
+
"description": "Complexity score computed for a project",
|
|
93
|
+
},
|
|
94
|
+
"wicked.phase.transitioned": {
|
|
95
|
+
"domain": "wicked-garden",
|
|
96
|
+
"subdomain": "crew.phase",
|
|
97
|
+
"description": "Phase approved and advanced to next",
|
|
98
|
+
},
|
|
99
|
+
"wicked.gate.decided": {
|
|
100
|
+
"domain": "wicked-garden",
|
|
101
|
+
"subdomain": "crew.gate",
|
|
102
|
+
"description": "Gate returned APPROVE, CONDITIONAL, or REJECT",
|
|
103
|
+
},
|
|
104
|
+
"wicked.gate.blocked": {
|
|
105
|
+
"domain": "wicked-garden",
|
|
106
|
+
"subdomain": "crew.gate",
|
|
107
|
+
"description": "Gate returned REJECT — phase advancement blocked",
|
|
108
|
+
},
|
|
109
|
+
"wicked.rework.triggered": {
|
|
110
|
+
"domain": "wicked-garden",
|
|
111
|
+
"subdomain": "crew.rework",
|
|
112
|
+
"description": "Rework initiated after gate REJECT or CONDITIONAL",
|
|
113
|
+
},
|
|
114
|
+
# Crew domain — issue #717 (resolve skill: classify-don't-retry path).
|
|
115
|
+
# Emitted per accepted resolution by `crew:resolve --accept`. The verdict
|
|
116
|
+
# on gate-result.json is NEVER mutated by this event — the resolution is
|
|
117
|
+
# surfaced as a first-class bus event so downstream consumers can audit
|
|
118
|
+
# who accepted what without crawling sidecar files. Pairs with the
|
|
119
|
+
# "bus-as-truth" decision from #732.
|
|
120
|
+
"wicked.gate.condition.resolved": {
|
|
121
|
+
"domain": "wicked-garden",
|
|
122
|
+
"subdomain": "crew.condition",
|
|
123
|
+
"description": "Mechanical CONDITIONAL finding resolved via crew:resolve skill (verdict unchanged)",
|
|
124
|
+
},
|
|
125
|
+
# Site 5 of bus-cutover (#746): condition verification flip.
|
|
126
|
+
# Emitted from conditions_manifest.mark_cleared() BEFORE the disk
|
|
127
|
+
# writes (sidecar + manifest). The projector handler
|
|
128
|
+
# _condition_marked_cleared in daemon/projector.py replays the same
|
|
129
|
+
# atomic two-step write order: sidecar first, then manifest flip.
|
|
130
|
+
# chain_id MUST include condition_id for per-condition idempotency
|
|
131
|
+
# (see ``memory/bus-chain-id-must-include-uniqueness-segment-gotcha.md``).
|
|
132
|
+
"wicked.condition.marked_cleared": {
|
|
133
|
+
"domain": "wicked-garden",
|
|
134
|
+
"subdomain": "crew.condition",
|
|
135
|
+
"description": "Condition verification flipped to verified=True via mark_cleared() (Site 5 cutover)",
|
|
136
|
+
},
|
|
137
|
+
# Site W1 of bus-cutover wave-2 (#787): solo_mode inline-HITL
|
|
138
|
+
# evidence record. Emitted from solo_mode.dispatch_human_inline()
|
|
139
|
+
# BEFORE the disk write at phases/{phase}/inline-review-context.md.
|
|
140
|
+
# Solo-mode also fires wicked.gate.decided for the same gate which
|
|
141
|
+
# carries the verdict + conditions; this event is for the markdown
|
|
142
|
+
# evidence file specifically (separate artifact, separate event).
|
|
143
|
+
# chain_id format: {project}.{phase}.gate (one inline-review-context
|
|
144
|
+
# per gate per phase, no per-condition split).
|
|
145
|
+
"wicked.crew.inline_review_context_recorded": {
|
|
146
|
+
"domain": "wicked-garden",
|
|
147
|
+
"subdomain": "crew.solo_mode",
|
|
148
|
+
"description": "Inline-HITL gate review evidence recorded by solo_mode (Site W1 cutover)",
|
|
149
|
+
},
|
|
150
|
+
# Wave-2 Tranche A summary emits — these are AUDIT MARKERS, not file
|
|
151
|
+
# projection events. The corresponding scripts (adopt_legacy.py,
|
|
152
|
+
# migrate_qe_evaluator_name.py, log_retention.py) are EXEMPT from
|
|
153
|
+
# full bus-cutover per docs/v9/wave-2-cutover-plan.md §W2/W3/W4: the
|
|
154
|
+
# writes themselves are migration/maintenance side-effects (one-shot
|
|
155
|
+
# transformations or rotation) that don't fit the projector replay
|
|
156
|
+
# model. These summary emits give the audit log a marker so future
|
|
157
|
+
# forensics can identify projects that went through legacy adoption
|
|
158
|
+
# / qe-evaluator rename / log rotation. No projector handlers; no
|
|
159
|
+
# entries in _PROJECTION_RESOLVERS.
|
|
160
|
+
"wicked.crew.legacy_adopted": {
|
|
161
|
+
"domain": "wicked-garden",
|
|
162
|
+
"subdomain": "crew.migration",
|
|
163
|
+
"description": "Legacy beta.3 → v6.0 project migration applied via adopt_legacy.py (audit marker)",
|
|
164
|
+
},
|
|
165
|
+
"wicked.crew.qe_evaluator_migrated": {
|
|
166
|
+
"domain": "wicked-garden",
|
|
167
|
+
"subdomain": "crew.migration",
|
|
168
|
+
"description": "qe-evaluator → gate-adjudicator rename applied via migrate_qe_evaluator_name.py (audit marker)",
|
|
169
|
+
},
|
|
170
|
+
"wicked.log.rotated": {
|
|
171
|
+
"domain": "wicked-garden",
|
|
172
|
+
"subdomain": "platform.log_retention",
|
|
173
|
+
"description": "Log file rotated by log_retention.rotate_if_needed (audit marker)",
|
|
174
|
+
},
|
|
175
|
+
# Wave-2 Tranche B (#746): JSONL append-stream cutovers.
|
|
176
|
+
# Each event mirrors a JSONL append into a per-phase log file; the
|
|
177
|
+
# projector handler replays the append from raw_payload. Same shape
|
|
178
|
+
# as wave-1 Site 1 (dispatch-log) but for four additional logs.
|
|
179
|
+
"wicked.amendment.appended": {
|
|
180
|
+
"domain": "wicked-garden",
|
|
181
|
+
"subdomain": "crew.amendment",
|
|
182
|
+
"description": "Phase amendment appended to amendments.jsonl (Site W6 cutover)",
|
|
183
|
+
},
|
|
184
|
+
"wicked.reeval.addendum_appended": {
|
|
185
|
+
"domain": "wicked-garden",
|
|
186
|
+
"subdomain": "crew.reeval",
|
|
187
|
+
"description": "Re-eval addendum appended to per-phase + project-root JSONL logs (Site W7 cutover; dual-file projection)",
|
|
188
|
+
},
|
|
189
|
+
"wicked.convergence.transition_recorded": {
|
|
190
|
+
"domain": "wicked-garden",
|
|
191
|
+
"subdomain": "crew.convergence",
|
|
192
|
+
"description": "Convergence-log transition recorded for an artifact (Site W8 cutover)",
|
|
193
|
+
},
|
|
194
|
+
"wicked.review.semantic_gap_recorded": {
|
|
195
|
+
"domain": "wicked-garden",
|
|
196
|
+
"subdomain": "crew.review",
|
|
197
|
+
"description": "Semantic-gap report persisted at review phase (Site W10a cutover)",
|
|
198
|
+
},
|
|
199
|
+
# Wave-2 Tranche C (#746):
|
|
200
|
+
"wicked.hitl.decision_recorded": {
|
|
201
|
+
"domain": "wicked-garden",
|
|
202
|
+
"subdomain": "crew.hitl",
|
|
203
|
+
"description": "HITL pause-decision evidence recorded by hitl_judge.write_hitl_decision_evidence (Site W5 cutover)",
|
|
204
|
+
},
|
|
205
|
+
"wicked.subagent.engaged": {
|
|
206
|
+
"domain": "wicked-garden",
|
|
207
|
+
"subdomain": "crew.subagent",
|
|
208
|
+
"description": "Specialist subagent engagement recorded by subagent_lifecycle (Site W9b cutover)",
|
|
209
|
+
},
|
|
210
|
+
# Jam domain — jam.py
|
|
211
|
+
"wicked.session.started": {
|
|
212
|
+
"domain": "wicked-garden",
|
|
213
|
+
"subdomain": "jam.session",
|
|
214
|
+
"description": "Brainstorm or council session started",
|
|
215
|
+
},
|
|
216
|
+
"wicked.session.synthesized": {
|
|
217
|
+
"domain": "wicked-garden",
|
|
218
|
+
"subdomain": "jam.session",
|
|
219
|
+
"description": "Session synthesis completed",
|
|
220
|
+
},
|
|
221
|
+
"wicked.session.synthesis_ready": {
|
|
222
|
+
"domain": "wicked-garden",
|
|
223
|
+
"subdomain": "jam.session",
|
|
224
|
+
"description": "All expected Round 1 personas contributed or timeout elapsed — facilitator may synthesize",
|
|
225
|
+
},
|
|
226
|
+
"wicked.council.voted": {
|
|
227
|
+
"domain": "wicked-garden",
|
|
228
|
+
"subdomain": "jam.council",
|
|
229
|
+
"description": "Council evaluation completed with model votes",
|
|
230
|
+
},
|
|
231
|
+
"wicked.persona.contributed": {
|
|
232
|
+
"domain": "wicked-garden",
|
|
233
|
+
"subdomain": "jam.persona",
|
|
234
|
+
"description": "Persona contributed a perspective in a brainstorm round",
|
|
235
|
+
},
|
|
236
|
+
# QE domain
|
|
237
|
+
"wicked.scenario.run": {
|
|
238
|
+
"domain": "wicked-garden",
|
|
239
|
+
"subdomain": "qe.scenario",
|
|
240
|
+
"description": "Test scenario executed with pass/fail result",
|
|
241
|
+
},
|
|
242
|
+
"wicked.coverage.changed": {
|
|
243
|
+
"domain": "wicked-garden",
|
|
244
|
+
"subdomain": "qe.coverage",
|
|
245
|
+
"description": "Test coverage metrics changed",
|
|
246
|
+
},
|
|
247
|
+
# Platform domain
|
|
248
|
+
"wicked.security.finding_raised": {
|
|
249
|
+
"domain": "wicked-garden",
|
|
250
|
+
"subdomain": "platform.security",
|
|
251
|
+
"description": "Security review raised a finding",
|
|
252
|
+
},
|
|
253
|
+
"wicked.guard.findings": {
|
|
254
|
+
"domain": "wicked-garden",
|
|
255
|
+
"subdomain": "platform.guard",
|
|
256
|
+
"description": "Autonomous session-close guard pipeline surfaced findings (Issue #448)",
|
|
257
|
+
},
|
|
258
|
+
"wicked.compliance.passed": {
|
|
259
|
+
"domain": "wicked-garden",
|
|
260
|
+
"subdomain": "platform.compliance",
|
|
261
|
+
"description": "Compliance check passed for a framework",
|
|
262
|
+
},
|
|
263
|
+
"wicked.compliance.failed": {
|
|
264
|
+
"domain": "wicked-garden",
|
|
265
|
+
"subdomain": "platform.compliance",
|
|
266
|
+
"description": "Compliance check failed for a framework",
|
|
267
|
+
},
|
|
268
|
+
# Auto-advance audit event
|
|
269
|
+
"wicked.phase.auto_advanced": {
|
|
270
|
+
"domain": "wicked-garden",
|
|
271
|
+
"subdomain": "crew.phase",
|
|
272
|
+
"description": "Phase auto-advanced for low-complexity project (audit trail)",
|
|
273
|
+
},
|
|
274
|
+
# Yolo scope-increase revoke — emitted by _apply_scope_increase_revoke when
|
|
275
|
+
# an augment/re-tier-up mutation flips yolo_approved_by_user to False.
|
|
276
|
+
"wicked.crew.yolo_revoked": {
|
|
277
|
+
"domain": "wicked-garden",
|
|
278
|
+
"subdomain": "crew.yolo",
|
|
279
|
+
"description": "Yolo auto-approval revoked due to scope-increase mutation (audit + observability)",
|
|
280
|
+
},
|
|
281
|
+
# Smaht domain — fact_extractor.py → brain auto-memorize subscriber
|
|
282
|
+
"wicked.fact.extracted": {
|
|
283
|
+
"domain": "smaht",
|
|
284
|
+
"subdomain": "facts",
|
|
285
|
+
"description": "Structured fact extracted from conversation (consumed by wicked-brain auto-memorize)",
|
|
286
|
+
},
|
|
287
|
+
# wicked-testing integration — verdict events (#549, AC-25)
|
|
288
|
+
"wicked.verdict.recorded": {
|
|
289
|
+
"domain": "wicked-testing",
|
|
290
|
+
"subdomain": "gate.verdict",
|
|
291
|
+
"description": "wicked-testing reviewer recorded a gate verdict (PASS/FAIL/N-A/SKIP)",
|
|
292
|
+
},
|
|
293
|
+
# Delivery domain — telemetry.py + drift.py (Issue #443)
|
|
294
|
+
"wicked.quality.drift_detected": {
|
|
295
|
+
"domain": "wicked-garden",
|
|
296
|
+
"subdomain": "delivery.telemetry",
|
|
297
|
+
"description": "Cross-session quality metric drifted past baseline threshold (special-cause or >=15% drop)",
|
|
298
|
+
},
|
|
299
|
+
# Crew domain — Part C of #734 (bus-as-truth emit additions paired with
|
|
300
|
+
# the load-bearing artifact writes the resume projector + bus-emit lint
|
|
301
|
+
# need to track. See PR #735 audit for the silent-write inventory.)
|
|
302
|
+
"wicked.dispatch.log_entry_appended": {
|
|
303
|
+
"domain": "wicked-garden",
|
|
304
|
+
"subdomain": "crew.dispatch",
|
|
305
|
+
"description": "HMAC-signed dispatch-log.jsonl entry appended (orphan-check sentinel)",
|
|
306
|
+
},
|
|
307
|
+
"wicked.consensus.report_created": {
|
|
308
|
+
"domain": "wicked-garden",
|
|
309
|
+
"subdomain": "crew.consensus",
|
|
310
|
+
"description": "Consensus gate report written to consensus-report.json",
|
|
311
|
+
},
|
|
312
|
+
"wicked.consensus.evidence_recorded": {
|
|
313
|
+
"domain": "wicked-garden",
|
|
314
|
+
"subdomain": "crew.consensus",
|
|
315
|
+
"description": "Consensus rejection evidence written to consensus-evidence.json (audit trail)",
|
|
316
|
+
},
|
|
317
|
+
# Site 3 of bus-cutover (#746): reviewer-report.md write sites
|
|
318
|
+
# (hooks/scripts/post_tool.py lines 963, 970, 984).
|
|
319
|
+
# Two events, not three:
|
|
320
|
+
# gate_completed — emitted after each _write_reviewer_report call
|
|
321
|
+
# (both the append-to-existing and create-new branches)
|
|
322
|
+
# gate_pending — emitted after _write_pending_reviewer_report
|
|
323
|
+
# (failure path: no consensus_result available)
|
|
324
|
+
"wicked.consensus.gate_completed": {
|
|
325
|
+
"domain": "wicked-garden",
|
|
326
|
+
"subdomain": "crew.consensus",
|
|
327
|
+
"description": "Consensus gate verdict written to reviewer-report.md (append or create)",
|
|
328
|
+
},
|
|
329
|
+
"wicked.consensus.gate_pending": {
|
|
330
|
+
"domain": "wicked-garden",
|
|
331
|
+
"subdomain": "crew.consensus",
|
|
332
|
+
"description": "Pending consensus gate placeholder written to reviewer-report.md (evaluation failed)",
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Payload deny-list — these fields must NEVER appear in bus payloads.
|
|
337
|
+
# Enforced by _sanitize_payload() before emission.
|
|
338
|
+
_PAYLOAD_DENY_LIST = frozenset({
|
|
339
|
+
"content", "body", "diff", "patch", "raw_text", "thinking",
|
|
340
|
+
"memory_content", "file_content", "source_code", "prompt",
|
|
341
|
+
"password", "secret", "token", "api_key", "credential",
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
# Per-event-type allow-list overrides — explicit exceptions where a normally
|
|
345
|
+
# denied key is part of the event's contract. Event types listed here get their
|
|
346
|
+
# named fields passed through the deny-list unchanged. Keep this list short and
|
|
347
|
+
# justified: every entry is a deliberate carve-out auditable at review time.
|
|
348
|
+
#
|
|
349
|
+
# wicked.fact.extracted — content is the whole point of the event. The brain
|
|
350
|
+
# auto-memorize subscriber requires payload.content to produce a memory.
|
|
351
|
+
# wicked.dispatch.log_entry_appended — `raw_payload` is the canonical JSONL
|
|
352
|
+
# bytes the projector replays into the `dispatch_log_entries` table under
|
|
353
|
+
# Site 1 of the bus-cutover (#746). Without it the projector cannot
|
|
354
|
+
# reproduce the on-disk line. Audit note in the PR body: this carve-out
|
|
355
|
+
# only ships an already-on-disk dispatch record (HMAC-signed), so payload
|
|
356
|
+
# inspection here is bounded by what the orphan check already trusts.
|
|
357
|
+
_PAYLOAD_ALLOW_OVERRIDES: Dict[str, frozenset] = {
|
|
358
|
+
"wicked.fact.extracted": frozenset({"content"}),
|
|
359
|
+
"wicked.dispatch.log_entry_appended": frozenset({"raw_payload"}),
|
|
360
|
+
# Site 2 of bus-cutover (#746): the consensus report and evidence emits
|
|
361
|
+
# carry the canonical on-disk JSON bytes via `raw_payload` so the
|
|
362
|
+
# projector can reproduce the file byte-for-byte. Without the carve-out
|
|
363
|
+
# the deny-list strips it as if it were generic file content. Council
|
|
364
|
+
# Condition C10 — `raw_payload` is REQUIRED for both emits.
|
|
365
|
+
"wicked.consensus.report_created": frozenset({"raw_payload"}),
|
|
366
|
+
"wicked.consensus.evidence_recorded": frozenset({"raw_payload"}),
|
|
367
|
+
# Site 3 of bus-cutover (#746): reviewer-report.md emits also ship
|
|
368
|
+
# raw_payload so the projector can reproduce the file byte-for-byte.
|
|
369
|
+
"wicked.consensus.gate_completed": frozenset({"raw_payload"}),
|
|
370
|
+
"wicked.consensus.gate_pending": frozenset({"raw_payload"}),
|
|
371
|
+
# Wave-2 Tranche B (#746): JSONL append-stream cutovers carry the
|
|
372
|
+
# canonical JSONL line via `raw_payload` so the projector can replay
|
|
373
|
+
# the append byte-for-byte. Same pattern as Site 1 dispatch-log.
|
|
374
|
+
# Each per-event audit note: the line bytes describe an append-only
|
|
375
|
+
# log entry (already validated by the source module's _validate_*
|
|
376
|
+
# call), so payload inspection is bounded by the schema check upstream.
|
|
377
|
+
"wicked.amendment.appended": frozenset({"raw_payload"}),
|
|
378
|
+
"wicked.reeval.addendum_appended": frozenset({"raw_payload"}),
|
|
379
|
+
"wicked.convergence.transition_recorded": frozenset({"raw_payload"}),
|
|
380
|
+
"wicked.review.semantic_gap_recorded": frozenset({"raw_payload"}),
|
|
381
|
+
# Wave-2 Tranche C (#746): hitl + subagent_engagement carry the
|
|
382
|
+
# canonical bytes via raw_payload so the projector can reproduce
|
|
383
|
+
# the file byte-for-byte. hitl additionally carries `filename`
|
|
384
|
+
# (caller-supplied evidence file name) — the projector validates it
|
|
385
|
+
# against a whitelist before writing to phases/{phase}/{filename}.
|
|
386
|
+
"wicked.hitl.decision_recorded": frozenset({"raw_payload", "filename"}),
|
|
387
|
+
"wicked.subagent.engaged": frozenset({"raw_payload"}),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Emit health counters — module-level, protected by _emit_counter_lock.
|
|
392
|
+
#
|
|
393
|
+
# _EMIT_ATTEMPTED: incremented AFTER _check_available() + BUS_EVENT_MAP
|
|
394
|
+
# validation pass (bus-absent no-ops must NOT inflate the denominator).
|
|
395
|
+
# _EMIT_SUCCEEDED: incremented inside _fire() on returncode == 0.
|
|
396
|
+
#
|
|
397
|
+
# Public accessor: bus_emit_stats() — pure reader, no side effects.
|
|
398
|
+
# Test-teardown helper: _bus_reset_stats() — resets both to 0 atomically.
|
|
399
|
+
#
|
|
400
|
+
# Threshold for the 99.9% SLO lives in .claude-plugin/gate-policy.json
|
|
401
|
+
# under bus_health.emit_success_threshold — NOT as a constant here.
|
|
402
|
+
# See Site 3 cutover decision memory:
|
|
403
|
+
# site-3-threshold-lives-in-gate-policy-user-override-2026-05-02.md
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
_EMIT_ATTEMPTED: int = 0
|
|
407
|
+
_EMIT_SUCCEEDED: int = 0
|
|
408
|
+
_emit_counter_lock: threading.Lock = threading.Lock()
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Availability cache — 60s TTL, reset on failure
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
_bus_available: Optional[bool] = None
|
|
415
|
+
_bus_checked_at: float = 0.0
|
|
416
|
+
_bus_binary: Optional[str] = None
|
|
417
|
+
_CACHE_TTL_SECONDS = 60.0
|
|
418
|
+
_EMIT_TIMEOUT_SECONDS = 5.0
|
|
419
|
+
|
|
420
|
+
# Cursor ID for poll-on-invoke (set on first registration)
|
|
421
|
+
_cursor_id: Optional[str] = None
|
|
422
|
+
_CURSOR_FILE = os.path.join(
|
|
423
|
+
os.path.expanduser("~"),
|
|
424
|
+
".something-wicked", "wicked-garden", "local",
|
|
425
|
+
"wicked-garden", "_bus_cursor.json",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _is_disabled() -> bool:
|
|
430
|
+
"""Check if bus is disabled via env var."""
|
|
431
|
+
return os.environ.get("WICKED_BUS_DISABLED", "").strip() in ("1", "true", "yes")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
# Default-ON set for _bus_as_truth_enabled — shipped cutover sites whose
|
|
436
|
+
# flag falls through to the default map. All five planned sites (1-5) have
|
|
437
|
+
# shipped. Keep this frozenset in sync with PROJECTION_FILE_FLAGS in
|
|
438
|
+
# scripts/crew/reconcile_v2.py.
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
_BUS_AS_TRUTH_DEFAULT_ON: frozenset = frozenset({
|
|
442
|
+
"DISPATCH_LOG", # Site 1 — dispatch-log.jsonl (PR #751)
|
|
443
|
+
"CONSENSUS_REPORT", # Site 2 — consensus-report.json (PR #758)
|
|
444
|
+
"CONSENSUS_EVIDENCE", # Site 2 — consensus-evidence.json (PR #758)
|
|
445
|
+
"REVIEWER_REPORT", # Site 3 — reviewer-report.md (PR #776)
|
|
446
|
+
"GATE_RESULT", # Site 4 — gate-result.json (PR #782 + #784)
|
|
447
|
+
"CONDITIONS_MANIFEST", # Site 5 — conditions-manifest.json (PR #785)
|
|
448
|
+
"INLINE_REVIEW_CONTEXT", # Site W1 — inline-review-context.md (PR #788)
|
|
449
|
+
# Wave-2 Tranche B (PR #790):
|
|
450
|
+
"AMENDMENTS", # Site W6
|
|
451
|
+
"REEVAL_ADDENDUM", # Site W7
|
|
452
|
+
"CONVERGENCE", # Site W8
|
|
453
|
+
"SEMANTIC_GAP", # Site W10a
|
|
454
|
+
# Wave-2 Tranche C (this PR):
|
|
455
|
+
"HITL_DECISION", # Site W5 — phases/{phase}/hitl-*.json + council-*.json
|
|
456
|
+
"SUBAGENT_ENGAGEMENT", # Site W9b — phases/{phase}/specialist-engagement.jsonl
|
|
457
|
+
"SKIPPED_PHASE_STATUS", # Site W10b — phases/{phase}/status.md (SKIPPED only)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _bus_as_truth_enabled(site: str = "DISPATCH_LOG") -> bool:
|
|
462
|
+
"""Resolve flag state for ``site`` token.
|
|
463
|
+
|
|
464
|
+
Site 1 (#751) introduced this helper hardcoded to ``DISPATCH_LOG``.
|
|
465
|
+
Site 2 (#746) added the ``site`` parameter so multiple cutover handlers
|
|
466
|
+
can each gate on their own flag while sharing one helper. Default keeps
|
|
467
|
+
Site 1 callers working unchanged.
|
|
468
|
+
|
|
469
|
+
Reading the env var directly in projector handlers or emitters is
|
|
470
|
+
forbidden — every read MUST go through this helper so we have one place
|
|
471
|
+
to flip and one place to audit. Cutover Sites 3-5 pass their own site
|
|
472
|
+
name (``REVIEWER_REPORT``, ``GATE_RESULT``, ``CONDITIONS_MANIFEST``).
|
|
473
|
+
|
|
474
|
+
Resolution order (flag-fold PR #777):
|
|
475
|
+
1. Explicit ``"on"`` (case/whitespace normalised) → True.
|
|
476
|
+
2. Explicit ``"off"`` (case/whitespace normalised) → False.
|
|
477
|
+
Operator opt-out beats the default map — ``OFF``, ``Off``, `` off ``
|
|
478
|
+
all opt out correctly.
|
|
479
|
+
3. Empty / any other value → default map.
|
|
480
|
+
Shipped sites (``_BUS_AS_TRUTH_DEFAULT_ON``) return True;
|
|
481
|
+
unshipped sites return False.
|
|
482
|
+
|
|
483
|
+
This resolves four findings from the dual-bot review (#777):
|
|
484
|
+
Finding #1 — non-"on"/"off" values now fall through to the default map
|
|
485
|
+
consistently for both shipped and unshipped sites.
|
|
486
|
+
Finding #3 — ``.strip().lower()`` normalises case + whitespace.
|
|
487
|
+
Finding #4 — asymmetric truthy-value handling eliminated; shipped and
|
|
488
|
+
unshipped sites both go through the same fall-through path.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
site: Cutover site identifier (uppercase token). Composed into the
|
|
492
|
+
env var name as ``WG_BUS_AS_TRUTH_<site>``. Site 2 callers pass
|
|
493
|
+
``"CONSENSUS_REPORT"`` and ``"CONSENSUS_EVIDENCE"``.
|
|
494
|
+
"""
|
|
495
|
+
raw = os.environ.get(f"WG_BUS_AS_TRUTH_{site}", "").strip().lower()
|
|
496
|
+
if raw == "on":
|
|
497
|
+
return True
|
|
498
|
+
if raw == "off":
|
|
499
|
+
return False
|
|
500
|
+
return site in _BUS_AS_TRUTH_DEFAULT_ON
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _resolve_binary() -> Optional[str]:
|
|
504
|
+
"""Resolve the wicked-bus binary path once, cache it."""
|
|
505
|
+
global _bus_binary
|
|
506
|
+
if _bus_binary is not None:
|
|
507
|
+
return _bus_binary
|
|
508
|
+
|
|
509
|
+
# Try direct binary first (faster than npx)
|
|
510
|
+
path = shutil.which("wicked-bus")
|
|
511
|
+
if path:
|
|
512
|
+
_bus_binary = path
|
|
513
|
+
return _bus_binary
|
|
514
|
+
|
|
515
|
+
# Fallback: verify npx can find it
|
|
516
|
+
try:
|
|
517
|
+
result = subprocess.run(
|
|
518
|
+
["npx", "wicked-bus", "status", "--json"],
|
|
519
|
+
capture_output=True, text=True,
|
|
520
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
521
|
+
)
|
|
522
|
+
if result.returncode == 0:
|
|
523
|
+
_bus_binary = "npx"
|
|
524
|
+
return _bus_binary
|
|
525
|
+
except Exception:
|
|
526
|
+
pass # fail open
|
|
527
|
+
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _check_available() -> bool:
|
|
532
|
+
"""Check bus availability with 60s TTL cache."""
|
|
533
|
+
global _bus_available, _bus_checked_at
|
|
534
|
+
|
|
535
|
+
now = time.monotonic()
|
|
536
|
+
if _bus_available is not None and (now - _bus_checked_at) < _CACHE_TTL_SECONDS:
|
|
537
|
+
return _bus_available
|
|
538
|
+
|
|
539
|
+
if _is_disabled():
|
|
540
|
+
_bus_available = False
|
|
541
|
+
_bus_checked_at = now
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
binary = _resolve_binary()
|
|
545
|
+
if binary is None:
|
|
546
|
+
_bus_available = False
|
|
547
|
+
_bus_checked_at = now
|
|
548
|
+
return False
|
|
549
|
+
|
|
550
|
+
_bus_available = True
|
|
551
|
+
_bus_checked_at = now
|
|
552
|
+
return True
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _invalidate_cache() -> None:
|
|
556
|
+
"""Reset availability cache on failure."""
|
|
557
|
+
global _bus_available, _bus_checked_at
|
|
558
|
+
_bus_available = None
|
|
559
|
+
_bus_checked_at = 0.0
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _build_cmd(*args: str) -> List[str]:
|
|
563
|
+
"""Build command list using cached binary path."""
|
|
564
|
+
if _bus_binary == "npx":
|
|
565
|
+
return ["npx", "wicked-bus", *args]
|
|
566
|
+
elif _bus_binary:
|
|
567
|
+
return [_bus_binary, *args]
|
|
568
|
+
return ["npx", "wicked-bus", *args]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _sanitize_payload(payload: Dict[str, Any], event_type: str = "") -> Dict[str, Any]:
|
|
572
|
+
"""Remove denied fields from payload. Never send content to the bus.
|
|
573
|
+
|
|
574
|
+
Per-event allow overrides (see _PAYLOAD_ALLOW_OVERRIDES) let specific event
|
|
575
|
+
types pass named denied fields through — these are explicit, audited carve-outs
|
|
576
|
+
for events whose contract requires a denied key (e.g. wicked.fact.extracted
|
|
577
|
+
must ship content to the brain auto-memorize subscriber).
|
|
578
|
+
"""
|
|
579
|
+
allow = _PAYLOAD_ALLOW_OVERRIDES.get(event_type, frozenset())
|
|
580
|
+
return {k: v for k, v in payload.items() if k not in _PAYLOAD_DENY_LIST or k in allow}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
# Emit — fire-and-forget
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
def emit_event(
|
|
588
|
+
event_type: str,
|
|
589
|
+
payload: Dict[str, Any],
|
|
590
|
+
chain_id: Optional[str] = None,
|
|
591
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
592
|
+
) -> None:
|
|
593
|
+
"""Emit an event to wicked-bus. Fire-and-forget — returns immediately.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
event_type: Must be a key in BUS_EVENT_MAP.
|
|
597
|
+
payload: Event data. Denied fields are stripped automatically.
|
|
598
|
+
chain_id: Crew causality chain ID. Added to metadata.
|
|
599
|
+
metadata: Additional operational context (not indexed, not filtered).
|
|
600
|
+
"""
|
|
601
|
+
if not _check_available():
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
event_def = BUS_EVENT_MAP.get(event_type)
|
|
605
|
+
if event_def is None:
|
|
606
|
+
return # Unknown event type — fail silently
|
|
607
|
+
|
|
608
|
+
# Count attempts only AFTER availability + event-map validation pass.
|
|
609
|
+
# Bus-absent no-ops (early returns above) must NOT inflate the denominator.
|
|
610
|
+
global _EMIT_ATTEMPTED, _EMIT_SUCCEEDED
|
|
611
|
+
with _emit_counter_lock:
|
|
612
|
+
_EMIT_ATTEMPTED += 1
|
|
613
|
+
|
|
614
|
+
safe_payload = _sanitize_payload(payload, event_type)
|
|
615
|
+
|
|
616
|
+
cmd = _build_cmd(
|
|
617
|
+
"emit",
|
|
618
|
+
"--type", event_type,
|
|
619
|
+
"--domain", event_def["domain"],
|
|
620
|
+
"--subdomain", event_def["subdomain"],
|
|
621
|
+
"--payload", json.dumps(safe_payload, default=str),
|
|
622
|
+
"--json",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Merge chain_id into metadata
|
|
626
|
+
meta = dict(metadata or {})
|
|
627
|
+
if chain_id:
|
|
628
|
+
meta["chain_id"] = chain_id
|
|
629
|
+
|
|
630
|
+
if meta:
|
|
631
|
+
cmd.extend(["--metadata", json.dumps(meta, default=str)])
|
|
632
|
+
|
|
633
|
+
def _fire() -> None:
|
|
634
|
+
global _EMIT_SUCCEEDED
|
|
635
|
+
try:
|
|
636
|
+
result = subprocess.run(
|
|
637
|
+
cmd,
|
|
638
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
639
|
+
capture_output=True,
|
|
640
|
+
)
|
|
641
|
+
if result.returncode == 0:
|
|
642
|
+
with _emit_counter_lock:
|
|
643
|
+
_EMIT_SUCCEEDED += 1
|
|
644
|
+
except Exception:
|
|
645
|
+
_invalidate_cache()
|
|
646
|
+
|
|
647
|
+
threading.Thread(target=_fire, daemon=True).start()
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def bus_emit_stats() -> Dict[str, Any]:
|
|
651
|
+
"""Return current emit health counters as a plain dict.
|
|
652
|
+
|
|
653
|
+
Pure reader — no logging, no side effects. Callers (e.g.
|
|
654
|
+
wicked-garden:platform:plugin-health) are responsible for alerting
|
|
655
|
+
when ratio falls below the threshold defined in gate-policy.json
|
|
656
|
+
bus_health.emit_success_threshold.
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
{
|
|
660
|
+
"attempted": int, -- emits past availability + BUS_EVENT_MAP check
|
|
661
|
+
"succeeded": int, -- returncode-0 subprocess completions
|
|
662
|
+
"ratio": float, -- succeeded / attempted; 0.0 when attempted == 0
|
|
663
|
+
}
|
|
664
|
+
"""
|
|
665
|
+
with _emit_counter_lock:
|
|
666
|
+
attempted = _EMIT_ATTEMPTED
|
|
667
|
+
succeeded = _EMIT_SUCCEEDED
|
|
668
|
+
ratio = succeeded / attempted if attempted > 0 else 0.0
|
|
669
|
+
return {"attempted": attempted, "succeeded": succeeded, "ratio": ratio}
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _bus_reset_stats() -> None:
|
|
673
|
+
"""Reset emit health counters to zero.
|
|
674
|
+
|
|
675
|
+
TEST-TEARDOWN HELPER ONLY — not for production use. Wired into
|
|
676
|
+
tests/conftest.py autouse fixture so counter isolation is automatic
|
|
677
|
+
across the suite. Resets both counters atomically under the lock.
|
|
678
|
+
"""
|
|
679
|
+
global _EMIT_ATTEMPTED, _EMIT_SUCCEEDED
|
|
680
|
+
with _emit_counter_lock:
|
|
681
|
+
_EMIT_ATTEMPTED = 0
|
|
682
|
+
_EMIT_SUCCEEDED = 0
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# ---------------------------------------------------------------------------
|
|
686
|
+
# Tail — read-only recent-event view (no cursor, no subscriber side effects)
|
|
687
|
+
# ---------------------------------------------------------------------------
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _resolve_bus_db_path() -> Optional[str]:
|
|
691
|
+
"""Return the wicked-bus event-store DB path via ``status --json``.
|
|
692
|
+
|
|
693
|
+
Uses the same binary resolver as every other call in this module so the
|
|
694
|
+
path stays correct regardless of how wicked-bus was installed. Returns
|
|
695
|
+
None when the bus is unavailable or the status payload lacks ``db_path``.
|
|
696
|
+
Never raises.
|
|
697
|
+
"""
|
|
698
|
+
if not _check_available():
|
|
699
|
+
return None
|
|
700
|
+
try:
|
|
701
|
+
result = subprocess.run(
|
|
702
|
+
_build_cmd("status", "--json"),
|
|
703
|
+
capture_output=True, text=True,
|
|
704
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
705
|
+
)
|
|
706
|
+
if result.returncode != 0:
|
|
707
|
+
return None
|
|
708
|
+
data = json.loads(result.stdout)
|
|
709
|
+
except Exception:
|
|
710
|
+
_invalidate_cache()
|
|
711
|
+
return None
|
|
712
|
+
db_path = data.get("db_path")
|
|
713
|
+
return db_path if isinstance(db_path, str) and db_path else None
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def tail_events(limit: int = 10) -> List[Dict[str, Any]]:
|
|
717
|
+
"""Return the most recent ``limit`` bus events, newest first. Read-only.
|
|
718
|
+
|
|
719
|
+
This is a *display* helper (e.g. ``/wicked-garden:smaht:state``). It does
|
|
720
|
+
NOT register a subscriber, advance a cursor, or mutate the bus in any way:
|
|
721
|
+
the wicked-bus ``replay`` command resets a cursor rather than returning
|
|
722
|
+
rows, and ``list`` returns subscribers, so neither yields event rows
|
|
723
|
+
synchronously. Instead we resolve the event-store path via ``status``
|
|
724
|
+
(through the shared binary resolver) and read the ``events`` table in
|
|
725
|
+
read-only mode.
|
|
726
|
+
|
|
727
|
+
Each returned dict carries the canonical bus columns plus a ``data`` alias
|
|
728
|
+
for the parsed ``payload`` (consumers index ``event.get("data", {})``).
|
|
729
|
+
|
|
730
|
+
Fail-open: any error — bus absent, unreadable DB, malformed rows — returns
|
|
731
|
+
``[]``. Never raises.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
limit: Max number of recent events to return (clamped to >= 0).
|
|
735
|
+
"""
|
|
736
|
+
if limit <= 0:
|
|
737
|
+
return []
|
|
738
|
+
|
|
739
|
+
db_path = _resolve_bus_db_path()
|
|
740
|
+
if not db_path or not os.path.exists(db_path):
|
|
741
|
+
return []
|
|
742
|
+
|
|
743
|
+
import sqlite3 # stdlib; imported lazily so the module loads without it
|
|
744
|
+
|
|
745
|
+
events: List[Dict[str, Any]] = []
|
|
746
|
+
conn = None
|
|
747
|
+
try:
|
|
748
|
+
# Read-only URI connection — never creates or writes the DB.
|
|
749
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=2.0)
|
|
750
|
+
conn.row_factory = sqlite3.Row
|
|
751
|
+
rows = conn.execute(
|
|
752
|
+
"SELECT event_id, event_type, domain, subdomain, payload, "
|
|
753
|
+
"metadata, emitted_at "
|
|
754
|
+
"FROM events ORDER BY event_id DESC LIMIT ?",
|
|
755
|
+
(int(limit),),
|
|
756
|
+
).fetchall()
|
|
757
|
+
except Exception:
|
|
758
|
+
return [] # fail open — bus DB unreadable or schema mismatch
|
|
759
|
+
finally:
|
|
760
|
+
if conn is not None:
|
|
761
|
+
try:
|
|
762
|
+
conn.close()
|
|
763
|
+
except Exception:
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
for row in rows:
|
|
767
|
+
try:
|
|
768
|
+
payload_raw = row["payload"]
|
|
769
|
+
payload = json.loads(payload_raw) if payload_raw else {}
|
|
770
|
+
if not isinstance(payload, dict):
|
|
771
|
+
payload = {"value": payload}
|
|
772
|
+
except (json.JSONDecodeError, TypeError):
|
|
773
|
+
payload = {}
|
|
774
|
+
try:
|
|
775
|
+
meta_raw = row["metadata"]
|
|
776
|
+
metadata = json.loads(meta_raw) if meta_raw else {}
|
|
777
|
+
if not isinstance(metadata, dict):
|
|
778
|
+
metadata = {}
|
|
779
|
+
except (json.JSONDecodeError, TypeError):
|
|
780
|
+
metadata = {}
|
|
781
|
+
events.append({
|
|
782
|
+
"event_id": row["event_id"],
|
|
783
|
+
"event_type": row["event_type"],
|
|
784
|
+
"domain": row["domain"],
|
|
785
|
+
"subdomain": row["subdomain"],
|
|
786
|
+
"payload": payload,
|
|
787
|
+
"data": payload, # alias — consumers read event.get("data", {})
|
|
788
|
+
"metadata": metadata,
|
|
789
|
+
"emitted_at": row["emitted_at"],
|
|
790
|
+
})
|
|
791
|
+
return events
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ---------------------------------------------------------------------------
|
|
795
|
+
# Poll — on-invoke consumption
|
|
796
|
+
# ---------------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
def _load_cursor() -> Optional[str]:
|
|
799
|
+
"""Load cursor ID from persistent storage."""
|
|
800
|
+
global _cursor_id
|
|
801
|
+
if _cursor_id:
|
|
802
|
+
return _cursor_id
|
|
803
|
+
try:
|
|
804
|
+
with open(_CURSOR_FILE, "r") as f:
|
|
805
|
+
data = json.load(f)
|
|
806
|
+
_cursor_id = data.get("cursor_id")
|
|
807
|
+
return _cursor_id
|
|
808
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _save_cursor(cursor_id: str) -> None:
|
|
813
|
+
"""Persist cursor ID for cross-session continuity."""
|
|
814
|
+
global _cursor_id
|
|
815
|
+
_cursor_id = cursor_id
|
|
816
|
+
try:
|
|
817
|
+
os.makedirs(os.path.dirname(_CURSOR_FILE), exist_ok=True)
|
|
818
|
+
with open(_CURSOR_FILE, "w") as f:
|
|
819
|
+
json.dump({"cursor_id": cursor_id, "updated_at": time.time()}, f)
|
|
820
|
+
except OSError:
|
|
821
|
+
pass # fail open
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _ensure_registered() -> Optional[str]:
|
|
825
|
+
"""Register wicked-garden as a bus subscriber if not already registered."""
|
|
826
|
+
cursor = _load_cursor()
|
|
827
|
+
if cursor:
|
|
828
|
+
return cursor
|
|
829
|
+
|
|
830
|
+
if not _check_available():
|
|
831
|
+
return None
|
|
832
|
+
|
|
833
|
+
try:
|
|
834
|
+
result = subprocess.run(
|
|
835
|
+
_build_cmd(
|
|
836
|
+
"register",
|
|
837
|
+
"--plugin", "wicked-garden",
|
|
838
|
+
"--role", "subscriber",
|
|
839
|
+
"--filter", "wicked.*",
|
|
840
|
+
"--json",
|
|
841
|
+
),
|
|
842
|
+
capture_output=True, text=True,
|
|
843
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
844
|
+
)
|
|
845
|
+
if result.returncode == 0:
|
|
846
|
+
data = json.loads(result.stdout)
|
|
847
|
+
cursor_id = data.get("cursor_id")
|
|
848
|
+
if cursor_id:
|
|
849
|
+
_save_cursor(cursor_id)
|
|
850
|
+
return cursor_id
|
|
851
|
+
except Exception:
|
|
852
|
+
_invalidate_cache()
|
|
853
|
+
|
|
854
|
+
return None
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def poll_pending(
|
|
858
|
+
event_type_prefix: Optional[str] = None,
|
|
859
|
+
limit: int = 50,
|
|
860
|
+
) -> List[Dict[str, Any]]:
|
|
861
|
+
"""Poll for pending events since last ack. Returns list of event dicts.
|
|
862
|
+
|
|
863
|
+
Call at command startup (poll-on-invoke pattern). Returns empty list
|
|
864
|
+
if bus is unavailable or no events pending.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
event_type_prefix: Filter events by type prefix (e.g., "wicked.gate.").
|
|
868
|
+
limit: Max events to return per poll.
|
|
869
|
+
"""
|
|
870
|
+
if not _check_available():
|
|
871
|
+
return []
|
|
872
|
+
|
|
873
|
+
cursor = _ensure_registered()
|
|
874
|
+
if not cursor:
|
|
875
|
+
return []
|
|
876
|
+
|
|
877
|
+
try:
|
|
878
|
+
# Read cursor's last_event_id from registration
|
|
879
|
+
result = subprocess.run(
|
|
880
|
+
_build_cmd("list", "--json"),
|
|
881
|
+
capture_output=True, text=True,
|
|
882
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
883
|
+
)
|
|
884
|
+
if result.returncode != 0:
|
|
885
|
+
return []
|
|
886
|
+
|
|
887
|
+
subs = json.loads(result.stdout)
|
|
888
|
+
last_event_id = 0
|
|
889
|
+
for sub in subs:
|
|
890
|
+
if sub.get("cursor_id") == cursor:
|
|
891
|
+
last_event_id = sub.get("last_event_id", 0)
|
|
892
|
+
break
|
|
893
|
+
|
|
894
|
+
# Replay from last acked position
|
|
895
|
+
replay_result = subprocess.run(
|
|
896
|
+
_build_cmd(
|
|
897
|
+
"replay",
|
|
898
|
+
"--cursor-id", cursor,
|
|
899
|
+
"--from-event-id", str(last_event_id),
|
|
900
|
+
"--json",
|
|
901
|
+
),
|
|
902
|
+
capture_output=True, text=True,
|
|
903
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
if replay_result.returncode != 0:
|
|
907
|
+
return []
|
|
908
|
+
|
|
909
|
+
data = json.loads(replay_result.stdout)
|
|
910
|
+
events = data.get("events", [])
|
|
911
|
+
|
|
912
|
+
# Filter by event_type prefix if specified
|
|
913
|
+
if event_type_prefix and events:
|
|
914
|
+
events = [e for e in events if e.get("event_type", "").startswith(event_type_prefix)]
|
|
915
|
+
|
|
916
|
+
return events[:limit]
|
|
917
|
+
|
|
918
|
+
except Exception:
|
|
919
|
+
_invalidate_cache()
|
|
920
|
+
return []
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def ack_events(last_event_id: int) -> bool:
|
|
924
|
+
"""Acknowledge events up to last_event_id. Advances the cursor.
|
|
925
|
+
|
|
926
|
+
Call after successfully processing events from poll_pending().
|
|
927
|
+
"""
|
|
928
|
+
cursor = _load_cursor()
|
|
929
|
+
if not cursor or not _check_available():
|
|
930
|
+
return False
|
|
931
|
+
|
|
932
|
+
try:
|
|
933
|
+
result = subprocess.run(
|
|
934
|
+
_build_cmd(
|
|
935
|
+
"ack",
|
|
936
|
+
"--cursor-id", cursor,
|
|
937
|
+
"--last-event-id", str(last_event_id),
|
|
938
|
+
"--json",
|
|
939
|
+
),
|
|
940
|
+
capture_output=True, text=True,
|
|
941
|
+
timeout=_EMIT_TIMEOUT_SECONDS,
|
|
942
|
+
)
|
|
943
|
+
return result.returncode == 0
|
|
944
|
+
except Exception:
|
|
945
|
+
_invalidate_cache()
|
|
946
|
+
return False
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
# ---------------------------------------------------------------------------
|
|
950
|
+
# Idempotency ledger — keyed on (event_type, chain_id)
|
|
951
|
+
# ---------------------------------------------------------------------------
|
|
952
|
+
|
|
953
|
+
_LEDGER_FILE = os.path.join(
|
|
954
|
+
os.path.expanduser("~"),
|
|
955
|
+
".something-wicked", "wicked-garden", "local",
|
|
956
|
+
"wicked-garden", "_bus_processed.json",
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _load_ledger() -> Dict[str, float]:
|
|
961
|
+
"""Load processed events ledger. Returns {key: timestamp}."""
|
|
962
|
+
try:
|
|
963
|
+
with open(_LEDGER_FILE, "r") as f:
|
|
964
|
+
return json.load(f)
|
|
965
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
966
|
+
return {}
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _save_ledger(ledger: Dict[str, float]) -> None:
|
|
970
|
+
"""Persist processed events ledger."""
|
|
971
|
+
try:
|
|
972
|
+
os.makedirs(os.path.dirname(_LEDGER_FILE), exist_ok=True)
|
|
973
|
+
with open(_LEDGER_FILE, "w") as f:
|
|
974
|
+
json.dump(ledger, f)
|
|
975
|
+
except OSError:
|
|
976
|
+
pass # fail open
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def is_processed(event_type: str, chain_id: str) -> bool:
|
|
980
|
+
"""Check if an event has already been processed."""
|
|
981
|
+
key = f"{event_type}:{chain_id}"
|
|
982
|
+
ledger = _load_ledger()
|
|
983
|
+
return key in ledger
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def mark_processed(event_type: str, chain_id: str) -> None:
|
|
987
|
+
"""Mark an event as processed in the idempotency ledger."""
|
|
988
|
+
key = f"{event_type}:{chain_id}"
|
|
989
|
+
ledger = _load_ledger()
|
|
990
|
+
ledger[key] = time.time()
|
|
991
|
+
|
|
992
|
+
# Prune entries older than 7 days to prevent unbounded growth
|
|
993
|
+
cutoff = time.time() - (7 * 86400)
|
|
994
|
+
ledger = {k: v for k, v in ledger.items() if v > cutoff}
|
|
995
|
+
|
|
996
|
+
_save_ledger(ledger)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
# ---------------------------------------------------------------------------
|
|
1000
|
+
# Consumer registry — static manifest at scripts/_bus_consumers.json.
|
|
1001
|
+
# Enforces the max_consumers budget at load time. Does not reject duplicates
|
|
1002
|
+
# (idempotent), logs an error when the registered count exceeds the budget.
|
|
1003
|
+
# ---------------------------------------------------------------------------
|
|
1004
|
+
|
|
1005
|
+
_CONSUMERS_MANIFEST = os.path.join(
|
|
1006
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
1007
|
+
"_bus_consumers.json",
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def load_consumer_registry(
|
|
1012
|
+
manifest_path: Optional[str] = None,
|
|
1013
|
+
) -> Dict[str, Any]:
|
|
1014
|
+
"""Load the bus consumer registry and validate it against max_consumers.
|
|
1015
|
+
|
|
1016
|
+
Returns a dict with keys:
|
|
1017
|
+
consumers: list of consumer entries (may be empty on error)
|
|
1018
|
+
max_consumers: int budget (defaults to 8 if absent)
|
|
1019
|
+
registered: int count of consumer entries
|
|
1020
|
+
over_budget: bool — True when registered > max_consumers
|
|
1021
|
+
error: optional str — set when the manifest is missing / malformed
|
|
1022
|
+
|
|
1023
|
+
Fail-open: never raises. A malformed or missing manifest logs an error
|
|
1024
|
+
and returns a registry dict with empty consumers so the bus still boots.
|
|
1025
|
+
Idempotent at load time — duplicate consumer ids are not rejected.
|
|
1026
|
+
"""
|
|
1027
|
+
path = manifest_path or _CONSUMERS_MANIFEST
|
|
1028
|
+
result: Dict[str, Any] = {
|
|
1029
|
+
"consumers": [],
|
|
1030
|
+
"max_consumers": 8,
|
|
1031
|
+
"registered": 0,
|
|
1032
|
+
"over_budget": False,
|
|
1033
|
+
"error": None,
|
|
1034
|
+
}
|
|
1035
|
+
try:
|
|
1036
|
+
with open(path, "r") as f:
|
|
1037
|
+
data = json.load(f)
|
|
1038
|
+
except FileNotFoundError:
|
|
1039
|
+
result["error"] = f"consumer manifest not found: {path}"
|
|
1040
|
+
logger.debug(result["error"])
|
|
1041
|
+
return result
|
|
1042
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
1043
|
+
result["error"] = f"consumer manifest malformed: {exc}"
|
|
1044
|
+
logger.error("bus consumer registry: %s", result["error"])
|
|
1045
|
+
return result
|
|
1046
|
+
|
|
1047
|
+
if not isinstance(data, dict):
|
|
1048
|
+
result["error"] = "consumer manifest root is not an object"
|
|
1049
|
+
logger.error("bus consumer registry: %s", result["error"])
|
|
1050
|
+
return result
|
|
1051
|
+
|
|
1052
|
+
consumers = data.get("consumers", [])
|
|
1053
|
+
if not isinstance(consumers, list):
|
|
1054
|
+
result["error"] = "consumer manifest 'consumers' is not a list"
|
|
1055
|
+
logger.error("bus consumer registry: %s", result["error"])
|
|
1056
|
+
consumers = []
|
|
1057
|
+
|
|
1058
|
+
max_budget = data.get("max_consumers", 8)
|
|
1059
|
+
try:
|
|
1060
|
+
max_budget = int(max_budget)
|
|
1061
|
+
except (TypeError, ValueError):
|
|
1062
|
+
max_budget = 8
|
|
1063
|
+
|
|
1064
|
+
registered = len(consumers)
|
|
1065
|
+
over_budget = registered > max_budget
|
|
1066
|
+
|
|
1067
|
+
result.update({
|
|
1068
|
+
"consumers": consumers,
|
|
1069
|
+
"max_consumers": max_budget,
|
|
1070
|
+
"registered": registered,
|
|
1071
|
+
"over_budget": over_budget,
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
if over_budget:
|
|
1075
|
+
logger.error(
|
|
1076
|
+
"bus consumer registry over budget: %d consumers registered, max %d. "
|
|
1077
|
+
"Review %s before adding more consumers.",
|
|
1078
|
+
registered, max_budget, path,
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
return result
|