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
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
_validate_registry.py — startup-time validator for wicked-garden in-code allowlists.
|
|
4
|
+
|
|
5
|
+
Catches the "invisible gap" failure mode: hardcoded lists of registered assets
|
|
6
|
+
(phases, reviewer subagent_types, bus event handlers) silently drift from the
|
|
7
|
+
files / catalogs they reference. Probes pass, unrelated traffic is fine, then
|
|
8
|
+
the first request that hits the missing asset blows up with a cryptic error.
|
|
9
|
+
|
|
10
|
+
This validator walks each allowlist source, checks that every target exists
|
|
11
|
+
on disk / in the relevant catalog, and aggregates findings into structured
|
|
12
|
+
categories: ``missing`` / ``malformed`` / ``invalid_id``.
|
|
13
|
+
|
|
14
|
+
Stdlib-only, cross-platform. Two consumers:
|
|
15
|
+
|
|
16
|
+
1. ``hooks/scripts/bootstrap.py`` calls ``run_all_checks()`` once per
|
|
17
|
+
SessionStart and surfaces findings as a session-briefing warning.
|
|
18
|
+
Fail-OPEN: the validator never blocks session start.
|
|
19
|
+
2. The CLI entrypoint runs the same checks and exits non-zero on any
|
|
20
|
+
``missing`` / ``malformed`` / ``invalid_id`` finding for CI use.
|
|
21
|
+
|
|
22
|
+
Allowlists covered (when the source-of-truth file exists):
|
|
23
|
+
|
|
24
|
+
- Phase IDs in ``.claude-plugin/phases.json`` ↔ phase agents in ``agents/``
|
|
25
|
+
(each phase that declares ``fallback_agent`` should have a matching agent
|
|
26
|
+
file or specialist alias).
|
|
27
|
+
- Reviewer ``subagent_type`` strings in ``.claude-plugin/gate-policy.json``
|
|
28
|
+
↔ agent files in ``agents/**/*.md`` (frontmatter ``subagent_type``).
|
|
29
|
+
External plugin reviewers (``wicked-testing:*``) are recognised via a
|
|
30
|
+
configurable allow-prefix list and reported as ``external`` (advisory),
|
|
31
|
+
not ``missing``.
|
|
32
|
+
- Bus event types in ``scripts/_bus.py::BUS_EVENT_MAP`` ↔ projector handlers
|
|
33
|
+
in ``daemon/projector.py::_HANDLERS``. Audit-marker events are explicitly
|
|
34
|
+
excluded from the handler check via ``_AUDIT_MARKER_EVENTS``.
|
|
35
|
+
- Skills referenced from agent frontmatter ``skills:`` lists ↔ ``SKILL.md``
|
|
36
|
+
files present on disk under ``skills/``.
|
|
37
|
+
|
|
38
|
+
Categories:
|
|
39
|
+
|
|
40
|
+
- ``missing`` — a referenced asset exists in an allowlist but the
|
|
41
|
+
target file / handler / row is absent.
|
|
42
|
+
- ``malformed`` — the source-of-truth file is unreadable, invalid JSON,
|
|
43
|
+
or has a structurally broken entry.
|
|
44
|
+
- ``invalid_id`` — the referenced asset has a syntactic problem
|
|
45
|
+
(e.g. empty string, non-string in a string field,
|
|
46
|
+
unexpected colon-form).
|
|
47
|
+
|
|
48
|
+
Out-of-scope (informational only, never elevated to ``missing``):
|
|
49
|
+
|
|
50
|
+
- ``external`` — drop-in plugin reviewers (``wicked-testing:*``,
|
|
51
|
+
``wicked-brain:*``) that legitimately live in another
|
|
52
|
+
plugin. Surfaced for visibility but never failing.
|
|
53
|
+
- ``skipped`` — a check skipped because its source-of-truth file is
|
|
54
|
+
missing on this checkout (e.g. ``daemon/projector.py``
|
|
55
|
+
not present in a slim install). Bootstrap surfaces
|
|
56
|
+
``skipped`` checks as INFO, not WARN.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import json
|
|
62
|
+
import os
|
|
63
|
+
import re
|
|
64
|
+
import sys
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
|
67
|
+
|
|
68
|
+
# Categories. Keep these literal — the bootstrap consumer matches on them.
|
|
69
|
+
CAT_MISSING = "missing"
|
|
70
|
+
CAT_MALFORMED = "malformed"
|
|
71
|
+
CAT_INVALID_ID = "invalid_id"
|
|
72
|
+
CAT_EXTERNAL = "external"
|
|
73
|
+
CAT_SKIPPED = "skipped"
|
|
74
|
+
|
|
75
|
+
# Severity → categories. Bootstrap-warning + CI-failing categories.
|
|
76
|
+
_BLOCKING_CATEGORIES = frozenset({CAT_MISSING, CAT_MALFORMED, CAT_INVALID_ID})
|
|
77
|
+
|
|
78
|
+
# Reviewer subagent_type prefixes that are EXPECTED to live outside this plugin.
|
|
79
|
+
# Anything starting with one of these is downgraded from `missing` to `external`.
|
|
80
|
+
_EXTERNAL_REVIEWER_PREFIXES: Tuple[str, ...] = (
|
|
81
|
+
"wicked-testing:",
|
|
82
|
+
"wicked-brain:",
|
|
83
|
+
"wicked-bus:",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Bus event types that are AUDIT MARKERS — by design they have no projector
|
|
87
|
+
# handler. Source: scripts/_bus.py inline comments around the migration events.
|
|
88
|
+
# Keep this in sync with daemon/projector.py.ARCHITECTURE.md if it changes.
|
|
89
|
+
_AUDIT_MARKER_EVENTS: Tuple[str, ...] = (
|
|
90
|
+
"wicked.crew.legacy_adopted",
|
|
91
|
+
"wicked.crew.qe_evaluator_migrated",
|
|
92
|
+
"wicked.log.rotated",
|
|
93
|
+
# jam events do not project to wicked-garden's projector — their
|
|
94
|
+
# consumers live in jam scripts and the brain auto-memorize subscriber.
|
|
95
|
+
"wicked.session.started",
|
|
96
|
+
"wicked.session.synthesized",
|
|
97
|
+
"wicked.session.synthesis_ready",
|
|
98
|
+
"wicked.council.voted",
|
|
99
|
+
"wicked.persona.contributed",
|
|
100
|
+
# qe / platform / delivery events are observability-only — no projector.
|
|
101
|
+
"wicked.scenario.run",
|
|
102
|
+
"wicked.coverage.changed",
|
|
103
|
+
"wicked.security.finding_raised",
|
|
104
|
+
"wicked.guard.findings",
|
|
105
|
+
"wicked.compliance.passed",
|
|
106
|
+
"wicked.compliance.failed",
|
|
107
|
+
"wicked.rollout.decided",
|
|
108
|
+
"wicked.experiment.concluded",
|
|
109
|
+
"wicked.fact.extracted",
|
|
110
|
+
"wicked.verdict.recorded",
|
|
111
|
+
"wicked.quality.drift_detected",
|
|
112
|
+
# crew condition resolution — observability only, verdict not mutated.
|
|
113
|
+
"wicked.gate.condition.resolved",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Reviewer values in gate-policy.json that are NOT subagent identifiers — they
|
|
117
|
+
# are mode markers / role placeholders consumed by gate_dispatch.py directly.
|
|
118
|
+
_NON_AGENT_REVIEWERS: Tuple[str, ...] = (
|
|
119
|
+
"human",
|
|
120
|
+
"user",
|
|
121
|
+
"self",
|
|
122
|
+
"skip",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Frontmatter regex — minimal YAML reader for ``subagent_type:`` and ``name:``.
|
|
126
|
+
# Stdlib-only constraint precludes a real YAML dependency, and the agent
|
|
127
|
+
# frontmatter is mechanically generated so a regex is sufficient.
|
|
128
|
+
_FRONTMATTER_RE = re.compile(r"^---\s*$(.*?)^---\s*$", re.DOTALL | re.MULTILINE)
|
|
129
|
+
_KEY_LINE_RE = re.compile(r"^\s*([A-Za-z_][\w-]*)\s*:\s*(.*?)\s*$")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _plugin_root() -> Path:
|
|
133
|
+
"""Resolve plugin root from CLAUDE_PLUGIN_ROOT, else this file's parent."""
|
|
134
|
+
env = os.environ.get("CLAUDE_PLUGIN_ROOT")
|
|
135
|
+
if env:
|
|
136
|
+
p = Path(env)
|
|
137
|
+
if p.exists():
|
|
138
|
+
return p
|
|
139
|
+
# scripts/_validate_registry.py → parent.parent
|
|
140
|
+
return Path(__file__).resolve().parent.parent
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _read_json(path: Path) -> Tuple[Optional[Any], Optional[str]]:
|
|
144
|
+
"""Read JSON and return (data, error_message). Either the data or an error
|
|
145
|
+
string — never both."""
|
|
146
|
+
if not path.is_file():
|
|
147
|
+
return None, f"file not found: {path}"
|
|
148
|
+
try:
|
|
149
|
+
return json.loads(path.read_text(encoding="utf-8")), None
|
|
150
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
151
|
+
return None, f"{type(exc).__name__}: {exc}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _parse_agent_frontmatter(path: Path) -> Tuple[Dict[str, str], Optional[str]]:
|
|
155
|
+
"""Extract YAML-style frontmatter keys from an agent .md file.
|
|
156
|
+
|
|
157
|
+
Returns ({key: value, ...}, error_or_None). Only flat string keys are
|
|
158
|
+
parsed — list / nested values are returned as the raw line tail.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
162
|
+
except OSError as exc:
|
|
163
|
+
return {}, f"read error: {exc}"
|
|
164
|
+
m = _FRONTMATTER_RE.search(text)
|
|
165
|
+
if not m:
|
|
166
|
+
return {}, "no frontmatter block"
|
|
167
|
+
out: Dict[str, str] = {}
|
|
168
|
+
for line in m.group(1).splitlines():
|
|
169
|
+
km = _KEY_LINE_RE.match(line)
|
|
170
|
+
if km:
|
|
171
|
+
out[km.group(1)] = km.group(2)
|
|
172
|
+
return out, None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _scan_agents(plugin_root: Path) -> Tuple[Dict[str, Path], List[Dict[str, Any]]]:
|
|
176
|
+
"""Walk agents/**/*.md and return (subagent_type → path map, malformed findings)."""
|
|
177
|
+
findings: List[Dict[str, Any]] = []
|
|
178
|
+
by_subagent: Dict[str, Path] = {}
|
|
179
|
+
by_name: Dict[str, Path] = {}
|
|
180
|
+
|
|
181
|
+
agents_dir = plugin_root / "agents"
|
|
182
|
+
if not agents_dir.is_dir():
|
|
183
|
+
return {}, [
|
|
184
|
+
{
|
|
185
|
+
"category": CAT_SKIPPED,
|
|
186
|
+
"check": "agents.scan",
|
|
187
|
+
"target": str(agents_dir),
|
|
188
|
+
"detail": "agents/ directory not found — slim install?",
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
for md_path in agents_dir.rglob("*.md"):
|
|
193
|
+
if md_path.name.upper() == "README.MD":
|
|
194
|
+
continue
|
|
195
|
+
fm, err = _parse_agent_frontmatter(md_path)
|
|
196
|
+
if err:
|
|
197
|
+
findings.append(
|
|
198
|
+
{
|
|
199
|
+
"category": CAT_MALFORMED,
|
|
200
|
+
"check": "agents.frontmatter",
|
|
201
|
+
"target": str(md_path.relative_to(plugin_root)),
|
|
202
|
+
"detail": err,
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
sub = fm.get("subagent_type", "").strip()
|
|
207
|
+
name = fm.get("name", "").strip()
|
|
208
|
+
if name:
|
|
209
|
+
by_name.setdefault(name, md_path)
|
|
210
|
+
if not sub:
|
|
211
|
+
# Not every agent is required to declare subagent_type
|
|
212
|
+
# (e.g. README-style agents). Skip silently.
|
|
213
|
+
continue
|
|
214
|
+
if ":" in sub and not sub.startswith("wicked-garden:"):
|
|
215
|
+
# Mis-namespaced (e.g. typo'd colon) — flag as invalid_id.
|
|
216
|
+
findings.append(
|
|
217
|
+
{
|
|
218
|
+
"category": CAT_INVALID_ID,
|
|
219
|
+
"check": "agents.frontmatter",
|
|
220
|
+
"target": str(md_path.relative_to(plugin_root)),
|
|
221
|
+
"detail": f"subagent_type does not start with 'wicked-garden:': {sub!r}",
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
continue
|
|
225
|
+
if sub in by_subagent:
|
|
226
|
+
findings.append(
|
|
227
|
+
{
|
|
228
|
+
"category": CAT_INVALID_ID,
|
|
229
|
+
"check": "agents.duplicate",
|
|
230
|
+
"target": sub,
|
|
231
|
+
"detail": f"duplicate subagent_type in {md_path.relative_to(plugin_root)} (also {by_subagent[sub].relative_to(plugin_root)})",
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
continue
|
|
235
|
+
by_subagent[sub] = md_path
|
|
236
|
+
|
|
237
|
+
# Expose name-keyed entries under bare-name keys so gate-policy
|
|
238
|
+
# references like "senior-engineer" resolve. We register under the
|
|
239
|
+
# name AND under the colon-namespaced subagent_type so both forms work.
|
|
240
|
+
for nm, p in by_name.items():
|
|
241
|
+
# Only register if no colon-form already claims that bare name.
|
|
242
|
+
# The gate-policy "fallback" / shorthand reviewers use bare names.
|
|
243
|
+
by_subagent.setdefault(nm, p)
|
|
244
|
+
|
|
245
|
+
return by_subagent, findings
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def check_phases(plugin_root: Path) -> List[Dict[str, Any]]:
|
|
249
|
+
"""Validate phase fallback_agent references against agent files."""
|
|
250
|
+
findings: List[Dict[str, Any]] = []
|
|
251
|
+
phases_path = plugin_root / ".claude-plugin" / "phases.json"
|
|
252
|
+
data, err = _read_json(phases_path)
|
|
253
|
+
if err:
|
|
254
|
+
findings.append(
|
|
255
|
+
{
|
|
256
|
+
"category": CAT_SKIPPED if "not found" in err else CAT_MALFORMED,
|
|
257
|
+
"check": "phases.json",
|
|
258
|
+
"target": str(phases_path),
|
|
259
|
+
"detail": err,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
return findings
|
|
263
|
+
if not isinstance(data, dict):
|
|
264
|
+
findings.append(
|
|
265
|
+
{
|
|
266
|
+
"category": CAT_MALFORMED,
|
|
267
|
+
"check": "phases.json",
|
|
268
|
+
"target": str(phases_path),
|
|
269
|
+
"detail": "top-level value is not an object",
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
return findings
|
|
273
|
+
|
|
274
|
+
phases = data.get("phases")
|
|
275
|
+
if not isinstance(phases, dict):
|
|
276
|
+
findings.append(
|
|
277
|
+
{
|
|
278
|
+
"category": CAT_MALFORMED,
|
|
279
|
+
"check": "phases.json",
|
|
280
|
+
"target": str(phases_path),
|
|
281
|
+
"detail": "missing or non-object 'phases' key",
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
return findings
|
|
285
|
+
|
|
286
|
+
agents_index, agent_findings = _scan_agents(plugin_root)
|
|
287
|
+
findings.extend(agent_findings)
|
|
288
|
+
|
|
289
|
+
# Slim-install short-circuit: if agents/ was missing, _scan_agents
|
|
290
|
+
# already emitted a single `skipped` finding. Don't cascade into N
|
|
291
|
+
# `missing` findings for every fallback_agent — one slim-install
|
|
292
|
+
# signal is enough.
|
|
293
|
+
if not agents_index and any(
|
|
294
|
+
f.get("category") == CAT_SKIPPED and f.get("check") == "agents.scan"
|
|
295
|
+
for f in agent_findings
|
|
296
|
+
):
|
|
297
|
+
return findings
|
|
298
|
+
|
|
299
|
+
for phase_id, body in phases.items():
|
|
300
|
+
if not isinstance(phase_id, str) or not phase_id:
|
|
301
|
+
findings.append(
|
|
302
|
+
{
|
|
303
|
+
"category": CAT_INVALID_ID,
|
|
304
|
+
"check": "phases.id",
|
|
305
|
+
"target": repr(phase_id),
|
|
306
|
+
"detail": "phase id must be a non-empty string",
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
continue
|
|
310
|
+
if not isinstance(body, dict):
|
|
311
|
+
findings.append(
|
|
312
|
+
{
|
|
313
|
+
"category": CAT_MALFORMED,
|
|
314
|
+
"check": "phases.body",
|
|
315
|
+
"target": phase_id,
|
|
316
|
+
"detail": "phase body is not an object",
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
continue
|
|
320
|
+
fb = body.get("fallback_agent")
|
|
321
|
+
if fb in (None, ""):
|
|
322
|
+
continue # fallback_agent is optional
|
|
323
|
+
if not isinstance(fb, str):
|
|
324
|
+
findings.append(
|
|
325
|
+
{
|
|
326
|
+
"category": CAT_INVALID_ID,
|
|
327
|
+
"check": "phases.fallback_agent",
|
|
328
|
+
"target": phase_id,
|
|
329
|
+
"detail": f"fallback_agent is not a string: {fb!r}",
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
continue
|
|
333
|
+
if fb in _NON_AGENT_REVIEWERS:
|
|
334
|
+
continue
|
|
335
|
+
if fb in agents_index:
|
|
336
|
+
continue
|
|
337
|
+
# Bare names without colon — already merged into agents_index by name.
|
|
338
|
+
findings.append(
|
|
339
|
+
{
|
|
340
|
+
"category": CAT_MISSING,
|
|
341
|
+
"check": "phases.fallback_agent",
|
|
342
|
+
"target": f"{phase_id} → {fb}",
|
|
343
|
+
"detail": f"phase '{phase_id}' references fallback_agent '{fb}' but no agent .md file declares it",
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return findings
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def check_gate_policy(plugin_root: Path) -> List[Dict[str, Any]]:
|
|
351
|
+
"""Validate gate-policy reviewer + fallback references against agent files."""
|
|
352
|
+
findings: List[Dict[str, Any]] = []
|
|
353
|
+
gp_path = plugin_root / ".claude-plugin" / "gate-policy.json"
|
|
354
|
+
data, err = _read_json(gp_path)
|
|
355
|
+
if err:
|
|
356
|
+
findings.append(
|
|
357
|
+
{
|
|
358
|
+
"category": CAT_SKIPPED if "not found" in err else CAT_MALFORMED,
|
|
359
|
+
"check": "gate-policy.json",
|
|
360
|
+
"target": str(gp_path),
|
|
361
|
+
"detail": err,
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
return findings
|
|
365
|
+
if not isinstance(data, dict):
|
|
366
|
+
findings.append(
|
|
367
|
+
{
|
|
368
|
+
"category": CAT_MALFORMED,
|
|
369
|
+
"check": "gate-policy.json",
|
|
370
|
+
"target": str(gp_path),
|
|
371
|
+
"detail": "top-level value is not an object",
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
return findings
|
|
375
|
+
|
|
376
|
+
gates = data.get("gates")
|
|
377
|
+
if not isinstance(gates, dict):
|
|
378
|
+
findings.append(
|
|
379
|
+
{
|
|
380
|
+
"category": CAT_MALFORMED,
|
|
381
|
+
"check": "gate-policy.gates",
|
|
382
|
+
"target": str(gp_path),
|
|
383
|
+
"detail": "missing or non-object 'gates' key",
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
return findings
|
|
387
|
+
|
|
388
|
+
agents_index, agent_findings = _scan_agents(plugin_root)
|
|
389
|
+
findings.extend(agent_findings)
|
|
390
|
+
|
|
391
|
+
# Slim-install short-circuit (mirrors check_phases): if agents/ was
|
|
392
|
+
# missing, return after surfacing the single `skipped` finding rather
|
|
393
|
+
# than cascading into per-reviewer `missing` findings.
|
|
394
|
+
if not agents_index and any(
|
|
395
|
+
f.get("category") == CAT_SKIPPED and f.get("check") == "agents.scan"
|
|
396
|
+
for f in agent_findings
|
|
397
|
+
):
|
|
398
|
+
return findings
|
|
399
|
+
|
|
400
|
+
def _resolve(reviewer: str) -> str:
|
|
401
|
+
if not isinstance(reviewer, str) or not reviewer.strip():
|
|
402
|
+
return CAT_INVALID_ID
|
|
403
|
+
if reviewer in _NON_AGENT_REVIEWERS:
|
|
404
|
+
return "ok"
|
|
405
|
+
if any(reviewer.startswith(p) for p in _EXTERNAL_REVIEWER_PREFIXES):
|
|
406
|
+
return CAT_EXTERNAL
|
|
407
|
+
if reviewer in agents_index:
|
|
408
|
+
return "ok"
|
|
409
|
+
return CAT_MISSING
|
|
410
|
+
|
|
411
|
+
for gate_id, tiers in gates.items():
|
|
412
|
+
if not isinstance(tiers, dict):
|
|
413
|
+
findings.append(
|
|
414
|
+
{
|
|
415
|
+
"category": CAT_MALFORMED,
|
|
416
|
+
"check": "gate-policy.gate",
|
|
417
|
+
"target": gate_id,
|
|
418
|
+
"detail": "gate body is not an object",
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
for tier_name, tier_body in tiers.items():
|
|
423
|
+
if not isinstance(tier_body, dict):
|
|
424
|
+
continue # description / non-tier keys
|
|
425
|
+
# Read raw values without falsy-coercion (`or []`) — falsy
|
|
426
|
+
# non-list values like `""`, `0`, `{}` previously slipped through
|
|
427
|
+
# as "no reviewers" instead of being flagged as malformed.
|
|
428
|
+
raw_reviewers = tier_body.get("reviewers")
|
|
429
|
+
if raw_reviewers is None:
|
|
430
|
+
reviewers: List[Any] = []
|
|
431
|
+
elif isinstance(raw_reviewers, list):
|
|
432
|
+
reviewers = raw_reviewers
|
|
433
|
+
else:
|
|
434
|
+
findings.append(
|
|
435
|
+
{
|
|
436
|
+
"category": CAT_MALFORMED,
|
|
437
|
+
"check": "gate-policy.reviewers",
|
|
438
|
+
"target": f"{gate_id}.{tier_name}",
|
|
439
|
+
"detail": (
|
|
440
|
+
"reviewers is not a list "
|
|
441
|
+
f"(got {type(raw_reviewers).__name__}: {raw_reviewers!r})"
|
|
442
|
+
),
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
continue
|
|
446
|
+
raw_fallback = tier_body.get("fallback")
|
|
447
|
+
# `fallback` is a single string (or absent). Reject any other
|
|
448
|
+
# non-string type — the previous `is not None and != ""` check
|
|
449
|
+
# silently accepted dicts / numbers / booleans.
|
|
450
|
+
if raw_fallback is None or raw_fallback == "":
|
|
451
|
+
fallback: Optional[str] = None
|
|
452
|
+
elif isinstance(raw_fallback, str):
|
|
453
|
+
fallback = raw_fallback
|
|
454
|
+
else:
|
|
455
|
+
findings.append(
|
|
456
|
+
{
|
|
457
|
+
"category": CAT_MALFORMED,
|
|
458
|
+
"check": "gate-policy.fallback",
|
|
459
|
+
"target": f"{gate_id}.{tier_name}",
|
|
460
|
+
"detail": (
|
|
461
|
+
"fallback is not a string "
|
|
462
|
+
f"(got {type(raw_fallback).__name__}: {raw_fallback!r})"
|
|
463
|
+
),
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
fallback = None
|
|
467
|
+
for rv in reviewers:
|
|
468
|
+
outcome = _resolve(rv)
|
|
469
|
+
if outcome == "ok":
|
|
470
|
+
continue
|
|
471
|
+
findings.append(
|
|
472
|
+
{
|
|
473
|
+
"category": outcome,
|
|
474
|
+
"check": "gate-policy.reviewer",
|
|
475
|
+
"target": f"{gate_id}.{tier_name} → {rv}",
|
|
476
|
+
"detail": (
|
|
477
|
+
"external plugin reviewer (advisory only)"
|
|
478
|
+
if outcome == CAT_EXTERNAL
|
|
479
|
+
else f"reviewer '{rv}' has no matching agent .md file"
|
|
480
|
+
if outcome == CAT_MISSING
|
|
481
|
+
else f"reviewer is not a non-empty string: {rv!r}"
|
|
482
|
+
),
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
if fallback is not None:
|
|
486
|
+
outcome = _resolve(fallback)
|
|
487
|
+
if outcome == "ok":
|
|
488
|
+
continue
|
|
489
|
+
findings.append(
|
|
490
|
+
{
|
|
491
|
+
"category": outcome,
|
|
492
|
+
"check": "gate-policy.fallback",
|
|
493
|
+
"target": f"{gate_id}.{tier_name} → {fallback}",
|
|
494
|
+
"detail": (
|
|
495
|
+
"external plugin fallback (advisory only)"
|
|
496
|
+
if outcome == CAT_EXTERNAL
|
|
497
|
+
else f"fallback '{fallback}' has no matching agent .md file"
|
|
498
|
+
if outcome == CAT_MISSING
|
|
499
|
+
else f"fallback is not a non-empty string: {fallback!r}"
|
|
500
|
+
),
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return findings
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _extract_string_keys_from_dict_literal(text: str, var_name: str) -> Optional[Set[str]]:
|
|
508
|
+
"""Best-effort string-key extraction from a Python dict literal.
|
|
509
|
+
|
|
510
|
+
Stdlib-only — we cannot import the bus module at validation time without
|
|
511
|
+
pulling its deps, so we parse the source text. Looks for:
|
|
512
|
+
|
|
513
|
+
VAR_NAME ... = {
|
|
514
|
+
"key1": ...,
|
|
515
|
+
"key2": ...,
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
Returns the set of keys, or None if the variable cannot be located.
|
|
519
|
+
"""
|
|
520
|
+
# Match the assignment up to the opening brace
|
|
521
|
+
pat = re.compile(
|
|
522
|
+
r"^\s*" + re.escape(var_name) + r"\s*(?::[^=]+)?=\s*\{",
|
|
523
|
+
re.MULTILINE,
|
|
524
|
+
)
|
|
525
|
+
m = pat.search(text)
|
|
526
|
+
if not m:
|
|
527
|
+
return None
|
|
528
|
+
start = m.end() - 1 # position of '{'
|
|
529
|
+
depth = 0
|
|
530
|
+
end = -1
|
|
531
|
+
in_string: Optional[str] = None
|
|
532
|
+
i = start
|
|
533
|
+
while i < len(text):
|
|
534
|
+
ch = text[i]
|
|
535
|
+
if in_string:
|
|
536
|
+
if ch == "\\":
|
|
537
|
+
i += 2
|
|
538
|
+
continue
|
|
539
|
+
if ch == in_string:
|
|
540
|
+
in_string = None
|
|
541
|
+
i += 1
|
|
542
|
+
continue
|
|
543
|
+
# Skip Python line comments ('#' to end of line) — but ONLY when
|
|
544
|
+
# we are NOT inside a string. A '}' inside a comment must not
|
|
545
|
+
# decrement depth or we terminate parsing prematurely.
|
|
546
|
+
if ch == "#":
|
|
547
|
+
nl = text.find("\n", i)
|
|
548
|
+
if nl < 0:
|
|
549
|
+
# Comment runs to EOF — no closer can follow.
|
|
550
|
+
break
|
|
551
|
+
i = nl + 1
|
|
552
|
+
continue
|
|
553
|
+
if ch in ("'", '"'):
|
|
554
|
+
in_string = ch
|
|
555
|
+
i += 1
|
|
556
|
+
continue
|
|
557
|
+
if ch == "{":
|
|
558
|
+
depth += 1
|
|
559
|
+
elif ch == "}":
|
|
560
|
+
depth -= 1
|
|
561
|
+
if depth == 0:
|
|
562
|
+
end = i
|
|
563
|
+
break
|
|
564
|
+
i += 1
|
|
565
|
+
if end < 0:
|
|
566
|
+
return None
|
|
567
|
+
# Walk the dict body character-by-character, tracking brace/bracket depth
|
|
568
|
+
# AND string state, so we can collect ONLY the top-level (depth-0) keys.
|
|
569
|
+
# The regex-per-line approach picks up inner dict keys like "description"
|
|
570
|
+
# from each event record, which is wrong.
|
|
571
|
+
body = text[start + 1 : end]
|
|
572
|
+
keys: Set[str] = set()
|
|
573
|
+
depth_b = 0 # braces
|
|
574
|
+
depth_s = 0 # square brackets
|
|
575
|
+
depth_p = 0 # parens
|
|
576
|
+
in_str: Optional[str] = None
|
|
577
|
+
str_buf: List[str] = []
|
|
578
|
+
str_start_at_zero = False
|
|
579
|
+
j = 0
|
|
580
|
+
n = len(body)
|
|
581
|
+
while j < n:
|
|
582
|
+
ch = body[j]
|
|
583
|
+
if in_str:
|
|
584
|
+
if ch == "\\":
|
|
585
|
+
str_buf.append(body[j : j + 2])
|
|
586
|
+
j += 2
|
|
587
|
+
continue
|
|
588
|
+
if ch == in_str:
|
|
589
|
+
# Closing quote — peek ahead for ':'
|
|
590
|
+
literal = "".join(str_buf)
|
|
591
|
+
in_str = None
|
|
592
|
+
str_buf = []
|
|
593
|
+
# Skip whitespace and check for ':' — Python permits a
|
|
594
|
+
# newline between the closing quote and the colon, so accept
|
|
595
|
+
# CR/LF here in addition to space/tab.
|
|
596
|
+
k = j + 1
|
|
597
|
+
while k < n and body[k] in " \t\n\r":
|
|
598
|
+
k += 1
|
|
599
|
+
if (
|
|
600
|
+
str_start_at_zero
|
|
601
|
+
and k < n
|
|
602
|
+
and body[k] == ":"
|
|
603
|
+
and re.fullmatch(r"[A-Za-z0-9_.\-]+", literal)
|
|
604
|
+
):
|
|
605
|
+
keys.add(literal)
|
|
606
|
+
str_start_at_zero = False
|
|
607
|
+
j += 1
|
|
608
|
+
continue
|
|
609
|
+
str_buf.append(ch)
|
|
610
|
+
j += 1
|
|
611
|
+
continue
|
|
612
|
+
if ch == "#":
|
|
613
|
+
# Skip line comment
|
|
614
|
+
nl = body.find("\n", j)
|
|
615
|
+
if nl < 0:
|
|
616
|
+
break
|
|
617
|
+
j = nl + 1
|
|
618
|
+
continue
|
|
619
|
+
if ch in ("'", '"'):
|
|
620
|
+
in_str = ch
|
|
621
|
+
str_buf = []
|
|
622
|
+
str_start_at_zero = depth_b == 0 and depth_s == 0 and depth_p == 0
|
|
623
|
+
j += 1
|
|
624
|
+
continue
|
|
625
|
+
if ch == "{":
|
|
626
|
+
depth_b += 1
|
|
627
|
+
elif ch == "}":
|
|
628
|
+
depth_b -= 1
|
|
629
|
+
elif ch == "[":
|
|
630
|
+
depth_s += 1
|
|
631
|
+
elif ch == "]":
|
|
632
|
+
depth_s -= 1
|
|
633
|
+
elif ch == "(":
|
|
634
|
+
depth_p += 1
|
|
635
|
+
elif ch == ")":
|
|
636
|
+
depth_p -= 1
|
|
637
|
+
j += 1
|
|
638
|
+
return keys
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def check_bus_handlers(plugin_root: Path) -> List[Dict[str, Any]]:
|
|
642
|
+
"""Validate every BUS_EVENT_MAP entry that needs a projector has a handler."""
|
|
643
|
+
findings: List[Dict[str, Any]] = []
|
|
644
|
+
bus_path = plugin_root / "scripts" / "_bus.py"
|
|
645
|
+
proj_path = plugin_root / "daemon" / "projector.py"
|
|
646
|
+
|
|
647
|
+
if not bus_path.is_file():
|
|
648
|
+
findings.append(
|
|
649
|
+
{
|
|
650
|
+
"category": CAT_SKIPPED,
|
|
651
|
+
"check": "bus.event_map",
|
|
652
|
+
"target": str(bus_path),
|
|
653
|
+
"detail": "_bus.py not found — bus catalog check skipped",
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
return findings
|
|
657
|
+
|
|
658
|
+
if not proj_path.is_file():
|
|
659
|
+
findings.append(
|
|
660
|
+
{
|
|
661
|
+
"category": CAT_SKIPPED,
|
|
662
|
+
"check": "bus.handlers",
|
|
663
|
+
"target": str(proj_path),
|
|
664
|
+
"detail": "daemon/projector.py not found — handler check skipped",
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
return findings
|
|
668
|
+
|
|
669
|
+
try:
|
|
670
|
+
bus_text = bus_path.read_text(encoding="utf-8", errors="replace")
|
|
671
|
+
except OSError as exc:
|
|
672
|
+
findings.append(
|
|
673
|
+
{
|
|
674
|
+
"category": CAT_MALFORMED,
|
|
675
|
+
"check": "bus.event_map",
|
|
676
|
+
"target": str(bus_path),
|
|
677
|
+
"detail": f"read error: {exc}",
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
return findings
|
|
681
|
+
try:
|
|
682
|
+
proj_text = proj_path.read_text(encoding="utf-8", errors="replace")
|
|
683
|
+
except OSError as exc:
|
|
684
|
+
findings.append(
|
|
685
|
+
{
|
|
686
|
+
"category": CAT_MALFORMED,
|
|
687
|
+
"check": "bus.handlers",
|
|
688
|
+
"target": str(proj_path),
|
|
689
|
+
"detail": f"read error: {exc}",
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
return findings
|
|
693
|
+
|
|
694
|
+
event_keys = _extract_string_keys_from_dict_literal(bus_text, "BUS_EVENT_MAP")
|
|
695
|
+
handler_keys = _extract_string_keys_from_dict_literal(proj_text, "_HANDLERS")
|
|
696
|
+
|
|
697
|
+
if event_keys is None:
|
|
698
|
+
findings.append(
|
|
699
|
+
{
|
|
700
|
+
"category": CAT_MALFORMED,
|
|
701
|
+
"check": "bus.event_map",
|
|
702
|
+
"target": str(bus_path),
|
|
703
|
+
"detail": "could not locate BUS_EVENT_MAP literal",
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
return findings
|
|
707
|
+
if handler_keys is None:
|
|
708
|
+
findings.append(
|
|
709
|
+
{
|
|
710
|
+
"category": CAT_MALFORMED,
|
|
711
|
+
"check": "bus.handlers",
|
|
712
|
+
"target": str(proj_path),
|
|
713
|
+
"detail": "could not locate _HANDLERS literal",
|
|
714
|
+
}
|
|
715
|
+
)
|
|
716
|
+
return findings
|
|
717
|
+
|
|
718
|
+
audit_marker = set(_AUDIT_MARKER_EVENTS)
|
|
719
|
+
for evt in sorted(event_keys):
|
|
720
|
+
if evt in audit_marker:
|
|
721
|
+
continue
|
|
722
|
+
if evt in handler_keys:
|
|
723
|
+
continue
|
|
724
|
+
findings.append(
|
|
725
|
+
{
|
|
726
|
+
"category": CAT_MISSING,
|
|
727
|
+
"check": "bus.handlers",
|
|
728
|
+
"target": evt,
|
|
729
|
+
"detail": (
|
|
730
|
+
"event in BUS_EVENT_MAP has no projector handler in "
|
|
731
|
+
"daemon/projector.py::_HANDLERS and is not in the "
|
|
732
|
+
"audit-marker exclusion list"
|
|
733
|
+
),
|
|
734
|
+
}
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Reverse direction: handler defined for an unknown event.
|
|
738
|
+
# This is informational — events may be emitted via callsites that bypass
|
|
739
|
+
# BUS_EVENT_MAP (e.g. legacy direct-emit paths kept for bus-as-truth
|
|
740
|
+
# cutover compat). Surface as `external` so it shows up in audit but does
|
|
741
|
+
# not fail the gate.
|
|
742
|
+
for h in sorted(handler_keys):
|
|
743
|
+
if h in event_keys:
|
|
744
|
+
continue
|
|
745
|
+
findings.append(
|
|
746
|
+
{
|
|
747
|
+
"category": CAT_EXTERNAL,
|
|
748
|
+
"check": "bus.handlers.orphan",
|
|
749
|
+
"target": h,
|
|
750
|
+
"detail": (
|
|
751
|
+
"_HANDLERS dispatches an event_type that is not "
|
|
752
|
+
"registered in BUS_EVENT_MAP — likely emitted via a "
|
|
753
|
+
"legacy or direct-emit callsite (advisory only)"
|
|
754
|
+
),
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
return findings
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def check_skill_refs(plugin_root: Path) -> List[Dict[str, Any]]:
|
|
762
|
+
"""Validate that any agent frontmatter ``skills:`` reference resolves
|
|
763
|
+
to a SKILL.md on disk under ``skills/``.
|
|
764
|
+
|
|
765
|
+
Frontmatter shape varies:
|
|
766
|
+
|
|
767
|
+
skills: foo, bar
|
|
768
|
+
skills: [foo, bar]
|
|
769
|
+
skills:
|
|
770
|
+
- foo
|
|
771
|
+
- bar
|
|
772
|
+
|
|
773
|
+
We accept all three forms by stripping brackets / dashes and splitting
|
|
774
|
+
on commas / newlines.
|
|
775
|
+
"""
|
|
776
|
+
findings: List[Dict[str, Any]] = []
|
|
777
|
+
agents_dir = plugin_root / "agents"
|
|
778
|
+
skills_dir = plugin_root / "skills"
|
|
779
|
+
if not agents_dir.is_dir() or not skills_dir.is_dir():
|
|
780
|
+
findings.append(
|
|
781
|
+
{
|
|
782
|
+
"category": CAT_SKIPPED,
|
|
783
|
+
"check": "skills.refs",
|
|
784
|
+
"target": str(agents_dir if not agents_dir.is_dir() else skills_dir),
|
|
785
|
+
"detail": "agents/ or skills/ directory missing — skill-ref check skipped",
|
|
786
|
+
}
|
|
787
|
+
)
|
|
788
|
+
return findings
|
|
789
|
+
|
|
790
|
+
# Build the set of skill identifiers present on disk.
|
|
791
|
+
# A skill is any directory containing SKILL.md, OR a domain dir with
|
|
792
|
+
# SKILL.md at its root (single-skill domains). We index by:
|
|
793
|
+
# - bare name (last path segment of the dir holding SKILL.md)
|
|
794
|
+
# - "wicked-garden:{domain}:{skill-name}" colon-form
|
|
795
|
+
available: Set[str] = set()
|
|
796
|
+
for skill_md in skills_dir.rglob("SKILL.md"):
|
|
797
|
+
try:
|
|
798
|
+
rel = skill_md.relative_to(skills_dir)
|
|
799
|
+
except ValueError:
|
|
800
|
+
continue
|
|
801
|
+
parts = rel.parts[:-1] # drop SKILL.md
|
|
802
|
+
if not parts:
|
|
803
|
+
continue
|
|
804
|
+
# Bare leaf-name form
|
|
805
|
+
available.add(parts[-1])
|
|
806
|
+
# Colon-namespaced form for multi-skill domains
|
|
807
|
+
if len(parts) == 1:
|
|
808
|
+
available.add(f"wicked-garden:{parts[0]}")
|
|
809
|
+
elif len(parts) >= 2:
|
|
810
|
+
available.add(f"wicked-garden:{parts[0]}:{parts[-1]}")
|
|
811
|
+
|
|
812
|
+
# Walk agents
|
|
813
|
+
for md_path in agents_dir.rglob("*.md"):
|
|
814
|
+
if md_path.name.upper() == "README.MD":
|
|
815
|
+
continue
|
|
816
|
+
try:
|
|
817
|
+
text = md_path.read_text(encoding="utf-8", errors="replace")
|
|
818
|
+
except OSError:
|
|
819
|
+
continue
|
|
820
|
+
m = _FRONTMATTER_RE.search(text)
|
|
821
|
+
if not m:
|
|
822
|
+
continue
|
|
823
|
+
# Find the ``skills:`` block
|
|
824
|
+
block = m.group(1)
|
|
825
|
+
sk_match = re.search(
|
|
826
|
+
r"^\s*skills\s*:\s*(.*?)(?=^\s*[A-Za-z_][\w-]*\s*:|\Z)",
|
|
827
|
+
block,
|
|
828
|
+
re.DOTALL | re.MULTILINE,
|
|
829
|
+
)
|
|
830
|
+
if not sk_match:
|
|
831
|
+
continue
|
|
832
|
+
raw = sk_match.group(1)
|
|
833
|
+
# Normalise inline / list / multiline forms
|
|
834
|
+
cleaned = raw.replace("[", " ").replace("]", " ")
|
|
835
|
+
cleaned = re.sub(r"^\s*-\s*", "", cleaned, flags=re.MULTILINE)
|
|
836
|
+
tokens = [
|
|
837
|
+
t.strip().strip("'\"")
|
|
838
|
+
for piece in cleaned.split(",")
|
|
839
|
+
for t in piece.splitlines()
|
|
840
|
+
]
|
|
841
|
+
for tok in tokens:
|
|
842
|
+
if not tok:
|
|
843
|
+
continue
|
|
844
|
+
if tok in available:
|
|
845
|
+
continue
|
|
846
|
+
# Only flag if it LOOKS like a skill ref (no spaces, plausible chars)
|
|
847
|
+
if " " in tok or len(tok) > 80:
|
|
848
|
+
continue
|
|
849
|
+
findings.append(
|
|
850
|
+
{
|
|
851
|
+
"category": CAT_MISSING,
|
|
852
|
+
"check": "skills.refs",
|
|
853
|
+
"target": f"{md_path.relative_to(plugin_root)} → {tok}",
|
|
854
|
+
"detail": (
|
|
855
|
+
f"agent declares skills include '{tok}' but no "
|
|
856
|
+
"SKILL.md under skills/ matches that id"
|
|
857
|
+
),
|
|
858
|
+
}
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
return findings
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def run_all_checks(plugin_root: Optional[Path] = None) -> Dict[str, Any]:
|
|
865
|
+
"""Run every check and return a structured report.
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
{
|
|
869
|
+
"ok": bool, # False if any blocking finding
|
|
870
|
+
"checks_run": [name, ...],
|
|
871
|
+
"findings": [ {category, check, target, detail}, ... ],
|
|
872
|
+
"summary": {category: count, ...},
|
|
873
|
+
}
|
|
874
|
+
"""
|
|
875
|
+
root = plugin_root or _plugin_root()
|
|
876
|
+
findings: List[Dict[str, Any]] = []
|
|
877
|
+
checks_run: List[str] = []
|
|
878
|
+
|
|
879
|
+
for name, fn in (
|
|
880
|
+
("phases", check_phases),
|
|
881
|
+
("gate_policy", check_gate_policy),
|
|
882
|
+
("bus_handlers", check_bus_handlers),
|
|
883
|
+
("skill_refs", check_skill_refs),
|
|
884
|
+
):
|
|
885
|
+
try:
|
|
886
|
+
results = fn(root)
|
|
887
|
+
except Exception as exc: # fail-open per individual check
|
|
888
|
+
results = [
|
|
889
|
+
{
|
|
890
|
+
"category": CAT_MALFORMED,
|
|
891
|
+
"check": f"{name}.exception",
|
|
892
|
+
"target": name,
|
|
893
|
+
"detail": f"{type(exc).__name__}: {exc}",
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
checks_run.append(name)
|
|
897
|
+
findings.extend(results)
|
|
898
|
+
|
|
899
|
+
summary: Dict[str, int] = {}
|
|
900
|
+
for f in findings:
|
|
901
|
+
summary[f["category"]] = summary.get(f["category"], 0) + 1
|
|
902
|
+
|
|
903
|
+
blocking = sum(summary.get(c, 0) for c in _BLOCKING_CATEGORIES)
|
|
904
|
+
return {
|
|
905
|
+
"ok": blocking == 0,
|
|
906
|
+
"checks_run": checks_run,
|
|
907
|
+
"findings": findings,
|
|
908
|
+
"summary": summary,
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def format_briefing(
|
|
913
|
+
report: Dict[str, Any],
|
|
914
|
+
max_lines: int = 8,
|
|
915
|
+
include_advisory: bool = False,
|
|
916
|
+
) -> Optional[str]:
|
|
917
|
+
"""Format a session-briefing string for bootstrap.py.
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
- None when there is nothing worth surfacing in the SessionStart
|
|
921
|
+
briefing (no blocking findings AND advisory-only findings are
|
|
922
|
+
either absent or suppressed by `include_advisory=False`).
|
|
923
|
+
- A short multi-line block otherwise.
|
|
924
|
+
|
|
925
|
+
The default ``include_advisory=False`` keeps the briefing quiet during
|
|
926
|
+
normal operation: drop-in plugin references (`wicked-testing:*`,
|
|
927
|
+
`wicked-brain:*`) and known orphan handlers populate the advisory
|
|
928
|
+
bucket and would otherwise add 10-15 lines to every SessionStart.
|
|
929
|
+
The CLI runner shows ALL findings unconditionally — this filter only
|
|
930
|
+
affects the bootstrap surface.
|
|
931
|
+
"""
|
|
932
|
+
findings = report.get("findings", []) or []
|
|
933
|
+
if not findings:
|
|
934
|
+
return None
|
|
935
|
+
summary = report.get("summary") or {}
|
|
936
|
+
blocking = {
|
|
937
|
+
c: n for c, n in summary.items() if c in _BLOCKING_CATEGORIES and n > 0
|
|
938
|
+
}
|
|
939
|
+
advisory = {
|
|
940
|
+
c: n
|
|
941
|
+
for c, n in summary.items()
|
|
942
|
+
if c not in _BLOCKING_CATEGORIES and n > 0
|
|
943
|
+
}
|
|
944
|
+
# Bootstrap default: quiet unless something is broken. CI runs the
|
|
945
|
+
# CLI which always shows everything.
|
|
946
|
+
if not blocking and not include_advisory:
|
|
947
|
+
return None
|
|
948
|
+
if not blocking and not advisory:
|
|
949
|
+
return None
|
|
950
|
+
head_parts = []
|
|
951
|
+
if blocking:
|
|
952
|
+
head_parts.append(
|
|
953
|
+
", ".join(f"{c}={n}" for c, n in sorted(blocking.items()))
|
|
954
|
+
)
|
|
955
|
+
if advisory and include_advisory:
|
|
956
|
+
head_parts.append(
|
|
957
|
+
"advisory: "
|
|
958
|
+
+ ", ".join(f"{c}={n}" for c, n in sorted(advisory.items()))
|
|
959
|
+
)
|
|
960
|
+
head = "[Registry] startup-time validation surfaced findings — " + "; ".join(
|
|
961
|
+
head_parts
|
|
962
|
+
)
|
|
963
|
+
lines = [head]
|
|
964
|
+
# Show blocking findings first, capped, then advisory.
|
|
965
|
+
blocking_list = [f for f in findings if f["category"] in _BLOCKING_CATEGORIES]
|
|
966
|
+
if include_advisory:
|
|
967
|
+
advisory_list = [
|
|
968
|
+
f for f in findings if f["category"] not in _BLOCKING_CATEGORIES
|
|
969
|
+
]
|
|
970
|
+
else:
|
|
971
|
+
advisory_list = []
|
|
972
|
+
shown = 0
|
|
973
|
+
for f in blocking_list + advisory_list:
|
|
974
|
+
if shown >= max_lines:
|
|
975
|
+
break
|
|
976
|
+
lines.append(
|
|
977
|
+
f" - [{f['category']}] {f['check']}: {f['target']} — {f['detail']}"
|
|
978
|
+
)
|
|
979
|
+
shown += 1
|
|
980
|
+
surfaced_total = len(blocking_list) + len(advisory_list)
|
|
981
|
+
leftover = surfaced_total - shown
|
|
982
|
+
if leftover > 0:
|
|
983
|
+
lines.append(
|
|
984
|
+
f" ... and {leftover} more (run scripts/_validate_registry.py for full list)"
|
|
985
|
+
)
|
|
986
|
+
return "\n".join(lines)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
# ---------------------------------------------------------------------------
|
|
990
|
+
# CLI
|
|
991
|
+
# ---------------------------------------------------------------------------
|
|
992
|
+
|
|
993
|
+
def _emit(report: Dict[str, Any], as_json: bool) -> None:
|
|
994
|
+
if as_json:
|
|
995
|
+
# Use sys.stdout.write + json.dumps to honour the cross-platform
|
|
996
|
+
# rule (no `printf` ambiguity); print() in Python is line-buffered
|
|
997
|
+
# and adds a single newline which is fine on every platform.
|
|
998
|
+
sys.stdout.write(json.dumps(report, indent=2))
|
|
999
|
+
sys.stdout.write("\n")
|
|
1000
|
+
return
|
|
1001
|
+
summary = report.get("summary") or {}
|
|
1002
|
+
summary_str = ", ".join(f"{c}={n}" for c, n in sorted(summary.items())) or "clean"
|
|
1003
|
+
sys.stdout.write(
|
|
1004
|
+
f"wicked-garden registry validator — checks: "
|
|
1005
|
+
f"{', '.join(report.get('checks_run', []))}\n"
|
|
1006
|
+
)
|
|
1007
|
+
sys.stdout.write(f" summary: {summary_str}\n")
|
|
1008
|
+
for f in report.get("findings", []):
|
|
1009
|
+
sys.stdout.write(
|
|
1010
|
+
f" [{f['category']}] {f['check']}: {f['target']} — {f['detail']}\n"
|
|
1011
|
+
)
|
|
1012
|
+
if report.get("ok"):
|
|
1013
|
+
sys.stdout.write("OK\n")
|
|
1014
|
+
else:
|
|
1015
|
+
sys.stdout.write("FAIL\n")
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
1019
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
1020
|
+
as_json = "--json" in argv
|
|
1021
|
+
if "--help" in argv or "-h" in argv:
|
|
1022
|
+
sys.stdout.write(
|
|
1023
|
+
"Usage: _validate_registry.py [--json]\n\n"
|
|
1024
|
+
"Validate that wicked-garden in-code allowlists "
|
|
1025
|
+
"(phases, reviewers, bus handlers, skill refs) point to "
|
|
1026
|
+
"assets that actually exist on disk / in catalog.\n\n"
|
|
1027
|
+
"Exit code: 0 = clean, 1 = blocking findings, 2 = unexpected error.\n"
|
|
1028
|
+
)
|
|
1029
|
+
return 0
|
|
1030
|
+
try:
|
|
1031
|
+
report = run_all_checks()
|
|
1032
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
1033
|
+
sys.stderr.write(
|
|
1034
|
+
f"_validate_registry: unexpected error: {type(exc).__name__}: {exc}\n"
|
|
1035
|
+
)
|
|
1036
|
+
return 2
|
|
1037
|
+
_emit(report, as_json=as_json)
|
|
1038
|
+
return 0 if report.get("ok") else 1
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
if __name__ == "__main__":
|
|
1042
|
+
sys.exit(main())
|