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,1278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook — wicked-garden unified pre-tool dispatcher.
|
|
4
|
+
|
|
5
|
+
Consolidates: crew pretool_taskcreate, crew pretool_planmode, mem block_memory_md,
|
|
6
|
+
crew gate preflight (Bash approve calls).
|
|
7
|
+
|
|
8
|
+
Dispatches by tool_name from hook payload:
|
|
9
|
+
TaskCreate → validate event metadata envelope + crew initiative injection
|
|
10
|
+
TaskUpdate → validate event metadata envelope on updates
|
|
11
|
+
EnterPlanMode → deny and redirect to crew workflow
|
|
12
|
+
Write / Edit → MEMORY.md write guard (AGENTS.md writes allowed, synced via PostToolUse)
|
|
13
|
+
Bash → crew gate preflight (phase_manager.py approve calls) +
|
|
14
|
+
worktree cwd-leak guard (#878, hooks/scripts/worktree_guard.py)
|
|
15
|
+
|
|
16
|
+
Always fails open — any unhandled exception returns permissionDecision: "allow".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Add shared scripts directory to path
|
|
27
|
+
_PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
|
|
28
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
|
|
29
|
+
# Allow importing sibling hook modules (e.g. worktree_guard) in any context.
|
|
30
|
+
sys.path.append(str(Path(__file__).resolve().parent))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Ops logger wrapper — fail-silent, never crashes the hook
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def _log(domain, level, event, ok=True, ms=None, detail=None):
|
|
38
|
+
"""Ops logger — fail-silent, never crashes the hook."""
|
|
39
|
+
try:
|
|
40
|
+
from _logger import log
|
|
41
|
+
log(domain, level, event, ok=ok, ms=ms, detail=detail)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Response helpers
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def _allow(updated_input=None, system_message=None) -> str:
|
|
51
|
+
output = {
|
|
52
|
+
"hookSpecificOutput": {
|
|
53
|
+
"hookEventName": "PreToolUse",
|
|
54
|
+
"permissionDecision": "allow",
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if updated_input is not None:
|
|
58
|
+
output["hookSpecificOutput"]["updatedInput"] = updated_input
|
|
59
|
+
if system_message:
|
|
60
|
+
output["systemMessage"] = system_message
|
|
61
|
+
return json.dumps(output)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _deny(reason: str) -> str:
|
|
65
|
+
return json.dumps({
|
|
66
|
+
"hookSpecificOutput": {
|
|
67
|
+
"hookEventName": "PreToolUse",
|
|
68
|
+
"permissionDecision": "deny",
|
|
69
|
+
"permissionDecisionReason": reason,
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Handler: TaskCreate
|
|
76
|
+
# (inject crew initiative metadata, one-time crew suggestion)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def _find_active_crew_project():
|
|
80
|
+
"""Return (project_data_dict, project_name, initiative_name).
|
|
81
|
+
|
|
82
|
+
Scoped to the current workspace (CLAUDE_PROJECT_NAME or cwd basename).
|
|
83
|
+
Only returns projects whose ``workspace`` field matches.
|
|
84
|
+
Uses DomainStore exclusively — operates local-only in hook mode.
|
|
85
|
+
|
|
86
|
+
``initiative_name`` is the project name (used to tag task metadata
|
|
87
|
+
with a coarse grouping). Custom display names can be set via the
|
|
88
|
+
optional ``initiative`` field on project.json.
|
|
89
|
+
"""
|
|
90
|
+
workspace = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
|
|
91
|
+
try:
|
|
92
|
+
from _domain_store import DomainStore
|
|
93
|
+
ds = DomainStore("wicked-crew", hook_mode=True)
|
|
94
|
+
projects = ds.list("projects") or []
|
|
95
|
+
for p in sorted(projects, key=lambda x: x.get("updated_at", x.get("created_at", "")), reverse=True):
|
|
96
|
+
if p.get("archived"):
|
|
97
|
+
continue
|
|
98
|
+
# Workspace scoping — skip projects from other workspaces
|
|
99
|
+
if workspace and p.get("workspace", "") != workspace:
|
|
100
|
+
continue
|
|
101
|
+
phase = p.get("current_phase", "")
|
|
102
|
+
if phase and phase not in ("complete", "done", ""):
|
|
103
|
+
name = p.get("name", "")
|
|
104
|
+
initiative = p.get("initiative") or name
|
|
105
|
+
return p, name, initiative
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return None, None, None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_event_metadata(tool_input: dict) -> "str | None":
|
|
113
|
+
"""Validate the metadata envelope on TaskCreate / TaskUpdate.
|
|
114
|
+
|
|
115
|
+
Returns an error string on failure, or None on pass. Loads phases.json
|
|
116
|
+
for phase enum validation. Fail-silent on import errors — an
|
|
117
|
+
un-importable schema module must never break task creation.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
from _event_schema import validate_metadata
|
|
121
|
+
except Exception:
|
|
122
|
+
return None # schema module unavailable; fail open
|
|
123
|
+
|
|
124
|
+
metadata = tool_input.get("metadata") or {}
|
|
125
|
+
phases = set(_load_phases_config_hook().keys())
|
|
126
|
+
status = tool_input.get("status")
|
|
127
|
+
return validate_metadata(
|
|
128
|
+
metadata, valid_phases=phases or None, status=status,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _task_metadata_mode() -> str:
|
|
133
|
+
"""``off`` | ``warn`` | ``strict`` — mirrors gate enforcement convention."""
|
|
134
|
+
mode = (os.environ.get("WG_TASK_METADATA") or "warn").strip().lower()
|
|
135
|
+
return mode if mode in ("off", "warn", "strict") else "warn"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _handle_task_create(tool_input: dict) -> str:
|
|
139
|
+
"""Inject crew initiative metadata and validate the event envelope."""
|
|
140
|
+
mode = _task_metadata_mode()
|
|
141
|
+
err = None if mode == "off" else _validate_event_metadata(tool_input)
|
|
142
|
+
|
|
143
|
+
if err and mode == "strict":
|
|
144
|
+
_log("pretool", "info", "task.metadata.blocked", detail={"reason": err})
|
|
145
|
+
return _deny(f"[wicked-garden] task metadata invalid: {err}")
|
|
146
|
+
|
|
147
|
+
_data, project_name, initiative_name = _find_active_crew_project()
|
|
148
|
+
|
|
149
|
+
if project_name:
|
|
150
|
+
metadata = tool_input.get("metadata") or {}
|
|
151
|
+
if not metadata.get("initiative"):
|
|
152
|
+
metadata["initiative"] = initiative_name
|
|
153
|
+
tool_input["metadata"] = metadata
|
|
154
|
+
system_message = None
|
|
155
|
+
if err and mode == "warn":
|
|
156
|
+
_log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
|
|
157
|
+
system_message = f"[wicked-garden] DEPRECATION: task metadata: {err}"
|
|
158
|
+
return _allow(updated_input=tool_input, system_message=system_message)
|
|
159
|
+
|
|
160
|
+
# No active project — show one-time suggestion via SessionState
|
|
161
|
+
system_message = None
|
|
162
|
+
if err and mode == "warn":
|
|
163
|
+
_log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
|
|
164
|
+
system_message = f"[wicked-garden] DEPRECATION: task metadata: {err}"
|
|
165
|
+
try:
|
|
166
|
+
from _session import SessionState
|
|
167
|
+
state = SessionState.load()
|
|
168
|
+
if not state.task_suggest_shown:
|
|
169
|
+
state.update(task_suggest_shown=True)
|
|
170
|
+
suggest = "Creating tasks? Consider `/wicked-garden:crew:start` for quality gates."
|
|
171
|
+
return _allow(
|
|
172
|
+
system_message=(system_message + "\n" + suggest) if system_message else suggest
|
|
173
|
+
)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
return _allow(system_message=system_message)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _handle_task_update(tool_input: dict) -> str:
|
|
181
|
+
"""Validate the event envelope on TaskUpdate (metadata merges).
|
|
182
|
+
|
|
183
|
+
TaskUpdate metadata is a merge patch — an empty/absent metadata is valid
|
|
184
|
+
(the caller is updating status or subject only). Validation only fires
|
|
185
|
+
when metadata is present and contains an event_type claim.
|
|
186
|
+
"""
|
|
187
|
+
mode = _task_metadata_mode()
|
|
188
|
+
if mode == "off":
|
|
189
|
+
return _allow()
|
|
190
|
+
|
|
191
|
+
metadata = tool_input.get("metadata") or {}
|
|
192
|
+
if not metadata.get("event_type"):
|
|
193
|
+
return _allow() # pure status/subject update
|
|
194
|
+
|
|
195
|
+
err = _validate_event_metadata(tool_input)
|
|
196
|
+
if err and mode == "strict":
|
|
197
|
+
_log("pretool", "info", "task.metadata.blocked", detail={"reason": err})
|
|
198
|
+
return _deny(f"[wicked-garden] task metadata invalid: {err}")
|
|
199
|
+
if err and mode == "warn":
|
|
200
|
+
_log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
|
|
201
|
+
return _allow(
|
|
202
|
+
system_message=f"[wicked-garden] DEPRECATION: task metadata: {err}"
|
|
203
|
+
)
|
|
204
|
+
return _allow()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Handler: EnterPlanMode
|
|
209
|
+
# (deny native plan mode, redirect to crew workflow)
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def _handle_enter_plan_mode(tool_input: dict) -> str:
|
|
213
|
+
"""Block native plan mode — redirect to crew workflow instead.
|
|
214
|
+
|
|
215
|
+
wicked-garden uses crew projects for planning, not Claude's built-in
|
|
216
|
+
plan mode. Always deny and point to /wicked-garden:crew:start.
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
data, project_name, _ = _find_active_crew_project()
|
|
220
|
+
if project_name and data:
|
|
221
|
+
current_phase = data.get("current_phase", "")
|
|
222
|
+
return _deny(
|
|
223
|
+
f"Do not use native plan mode. A crew project '{project_name}' is already active "
|
|
224
|
+
f"(phase: {current_phase}). Continue working within the crew workflow. "
|
|
225
|
+
f"Use `/wicked-garden:crew:execute` to proceed or `/wicked-garden:crew:status` to check progress."
|
|
226
|
+
)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
return _deny(
|
|
231
|
+
"Do not use native plan mode. This project uses wicked-garden crew workflows for planning. "
|
|
232
|
+
"Use `/wicked-garden:crew:start` to create a new crew project with outcome clarification, "
|
|
233
|
+
"phased execution, and quality gates."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Handler: Write / Edit
|
|
239
|
+
# (MEMORY.md and AGENTS.md write guard)
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
# Claude Code auto-memory directory pattern
|
|
243
|
+
_AUTO_MEMORY_MARKER = ".claude/projects/"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _handle_write_guard(tool_input: dict) -> str:
|
|
247
|
+
"""Block direct writes to MEMORY.md, auto-memory directory, and AGENTS.md.
|
|
248
|
+
|
|
249
|
+
Also warns when an active crew project is in build or review phase and the
|
|
250
|
+
file path is not on the orchestrator allowlist. This enforces the
|
|
251
|
+
orchestrator-only principle (Issue #251): orchestrators should write only
|
|
252
|
+
to project state files, not directly produce implementation artifacts.
|
|
253
|
+
|
|
254
|
+
Issue #442: Also blocks Write/Edit during build phase when the
|
|
255
|
+
challenge-artifacts gate has not cleared (complexity >= 4). Respects
|
|
256
|
+
``WG_CHALLENGE_GATE=off`` as a scoped bypass. (The legacy global
|
|
257
|
+
``CREW_GATE_ENFORCEMENT=legacy`` switch was removed in #466 — v6 gate
|
|
258
|
+
enforcement is unconditional; use ``git revert`` to roll back.)
|
|
259
|
+
|
|
260
|
+
TODO (Issue #329): When Claude Code supports updatedInput for PreToolUse hooks
|
|
261
|
+
to redirect tool calls, change the MEMORY.md deny into an updatedInput redirect
|
|
262
|
+
that rewrites the Write/Edit call into a wicked-brain:memory invocation instead.
|
|
263
|
+
This would be less disruptive than a hard deny — the intent to persist data would
|
|
264
|
+
still succeed, just through the correct channel.
|
|
265
|
+
"""
|
|
266
|
+
file_path = tool_input.get("file_path", "")
|
|
267
|
+
|
|
268
|
+
# AGENTS.md is a cross-tool instruction file shared with Codex, Cursor, Amp, etc.
|
|
269
|
+
# Writes are allowed — a PostToolUse hook keeps CLAUDE.md and AGENTS.md in sync.
|
|
270
|
+
|
|
271
|
+
# Theme 9: bare endswith("MEMORY.md") would match e.g. "notMEMORY.md".
|
|
272
|
+
# Use Path().name for a true basename check, which is platform-agnostic
|
|
273
|
+
# (works for both POSIX and Windows separators).
|
|
274
|
+
try:
|
|
275
|
+
_basename = Path(file_path).name
|
|
276
|
+
except (TypeError, ValueError):
|
|
277
|
+
_basename = ""
|
|
278
|
+
is_memory_md = _basename == "MEMORY.md"
|
|
279
|
+
is_auto_memory = _AUTO_MEMORY_MARKER in file_path and "/memory/" in file_path
|
|
280
|
+
|
|
281
|
+
if is_memory_md or is_auto_memory:
|
|
282
|
+
return _deny(
|
|
283
|
+
"Do not write to MEMORY.md or the auto memory directory. "
|
|
284
|
+
"This project uses wicked-garden memory for persistence. "
|
|
285
|
+
"Use wicked-brain:memory to save decisions, patterns, and gotchas instead."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Issue #442: challenge-artifacts gate for build-phase writes
|
|
289
|
+
challenge_block = _check_challenge_gate(file_path)
|
|
290
|
+
if challenge_block:
|
|
291
|
+
return _deny(challenge_block)
|
|
292
|
+
|
|
293
|
+
# Issue #734 Part B: bus-emit lint for orphan writes to load-bearing
|
|
294
|
+
# state artifacts. WG_BUS_EMIT_LINT=warn (default) emits a systemMessage;
|
|
295
|
+
# =strict denies the write; =off disables. Fail-open on any error.
|
|
296
|
+
lint_action, lint_msg = _check_bus_emit_lint(file_path)
|
|
297
|
+
if lint_action == "deny":
|
|
298
|
+
return _deny(lint_msg)
|
|
299
|
+
|
|
300
|
+
# Orchestrator-only warning (Issue #251): warn when writing outside allowlist
|
|
301
|
+
# during build or review phases. Fail open — always allow, but emit systemMessage.
|
|
302
|
+
warning = _check_orchestrator_write(file_path)
|
|
303
|
+
|
|
304
|
+
# Combine lint warn + orchestrator warn into a single systemMessage
|
|
305
|
+
# rather than letting the second overwrite the first.
|
|
306
|
+
messages: list[str] = []
|
|
307
|
+
if lint_action == "warn" and lint_msg:
|
|
308
|
+
messages.append(lint_msg)
|
|
309
|
+
if warning:
|
|
310
|
+
messages.append(warning)
|
|
311
|
+
if messages:
|
|
312
|
+
return _allow(system_message="\n\n".join(messages))
|
|
313
|
+
|
|
314
|
+
return _allow()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Challenge gate (Issue #442)
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
def _challenge_gate_bypassed() -> bool:
|
|
322
|
+
"""Respect ``WG_CHALLENGE_GATE=off`` as a scoped bypass.
|
|
323
|
+
|
|
324
|
+
Only the scoped flag disables the challenge gate. The global
|
|
325
|
+
``CREW_GATE_ENFORCEMENT=legacy`` switch was removed in #466 — v6 gate
|
|
326
|
+
enforcement is unconditional. Rollback is a ``git revert``, not a
|
|
327
|
+
runtime toggle.
|
|
328
|
+
"""
|
|
329
|
+
if (os.environ.get("WG_CHALLENGE_GATE") or "").strip().lower() == "off":
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _check_challenge_gate(file_path: str) -> str:
|
|
335
|
+
"""Return a deny-reason if the challenge gate blocks this Write/Edit.
|
|
336
|
+
|
|
337
|
+
Only fires when ALL of the following are true:
|
|
338
|
+
* an active crew project exists in the current workspace
|
|
339
|
+
* the project is in the ``build`` phase
|
|
340
|
+
* the project complexity_score is >= the configured threshold
|
|
341
|
+
(``WG_CHALLENGE_MIN_COMPLEXITY`` env var, default 4)
|
|
342
|
+
* ``phases/design/challenge-artifacts.md`` is missing or invalid
|
|
343
|
+
* the file being written is NOT on the orchestrator allowlist
|
|
344
|
+
(we still allow orchestrator state writes so the project can
|
|
345
|
+
self-recover — the contrarian itself needs to be able to write
|
|
346
|
+
the artifact)
|
|
347
|
+
|
|
348
|
+
Any unexpected error → returns "" (fail-open). This mirrors the
|
|
349
|
+
always-fail-open contract the rest of this file upholds.
|
|
350
|
+
"""
|
|
351
|
+
if _challenge_gate_bypassed():
|
|
352
|
+
return ""
|
|
353
|
+
|
|
354
|
+
# Contrarian must be able to write the challenge artifact itself — never
|
|
355
|
+
# block on the artifact's own path. Also never block state writes.
|
|
356
|
+
if "challenge-artifacts.md" in file_path:
|
|
357
|
+
return ""
|
|
358
|
+
if _is_allowlisted(file_path):
|
|
359
|
+
return ""
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
data, project_name, _ = _find_active_crew_project()
|
|
363
|
+
if not project_name or not data:
|
|
364
|
+
return ""
|
|
365
|
+
|
|
366
|
+
current_phase = (data.get("current_phase") or "").lower()
|
|
367
|
+
if current_phase != "build":
|
|
368
|
+
return ""
|
|
369
|
+
|
|
370
|
+
complexity = int(data.get("complexity_score", 0) or 0)
|
|
371
|
+
|
|
372
|
+
# Import here so pre_tool.py stays robust if the crew module is
|
|
373
|
+
# missing / malformed. Fail-open on any import or attribute error.
|
|
374
|
+
try:
|
|
375
|
+
scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
|
|
376
|
+
if str(scripts_crew) not in sys.path:
|
|
377
|
+
sys.path.insert(0, str(scripts_crew))
|
|
378
|
+
from challenge_manifest import ( # type: ignore
|
|
379
|
+
is_required,
|
|
380
|
+
artifact_satisfies_gate,
|
|
381
|
+
)
|
|
382
|
+
except Exception:
|
|
383
|
+
return ""
|
|
384
|
+
|
|
385
|
+
if not is_required(complexity, phase="build"):
|
|
386
|
+
return ""
|
|
387
|
+
|
|
388
|
+
# Resolve the project directory to look up the artifact
|
|
389
|
+
try:
|
|
390
|
+
from _paths import get_local_path
|
|
391
|
+
base = get_local_path("wicked-crew", "projects")
|
|
392
|
+
project_id = data.get("id") or project_name
|
|
393
|
+
project_dir = base / project_id
|
|
394
|
+
except Exception:
|
|
395
|
+
return ""
|
|
396
|
+
|
|
397
|
+
if not (project_dir / "project.json").exists():
|
|
398
|
+
return ""
|
|
399
|
+
|
|
400
|
+
ok, reason = artifact_satisfies_gate(project_dir, phase="design")
|
|
401
|
+
if ok:
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
_log(
|
|
405
|
+
"pretool",
|
|
406
|
+
"info",
|
|
407
|
+
"challenge.gate.blocked",
|
|
408
|
+
detail={"project": project_name, "reason": reason},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
f"[wicked-garden] Challenge gate is unresolved for crew project "
|
|
413
|
+
f"'{project_name}' (complexity={complexity}). {reason} "
|
|
414
|
+
f"Invoke the contrarian specialist to produce and clear "
|
|
415
|
+
f"phases/design/challenge-artifacts.md before continuing build. "
|
|
416
|
+
f"Run `wicked-garden:crew:contrarian` or dispatch Task("
|
|
417
|
+
f"subagent_type='wicked-garden:crew:contrarian'). "
|
|
418
|
+
f"To bypass temporarily, set WG_CHALLENGE_GATE=off."
|
|
419
|
+
)
|
|
420
|
+
except Exception:
|
|
421
|
+
return ""
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Orchestrator allowlist (Issue #251)
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
# Paths that the orchestrator is permitted to write directly.
|
|
429
|
+
# .something-wicked/ contains project state, status.md files are orchestrator output.
|
|
430
|
+
_ORCHESTRATOR_ALLOWLIST = [
|
|
431
|
+
".something-wicked/",
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
_ORCHESTRATOR_ALLOWLIST_SUFFIXES = [
|
|
435
|
+
"status.md",
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
_ORCHESTRATOR_PHASES = {"build", "review"}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _is_allowlisted(file_path: str) -> bool:
|
|
442
|
+
"""Return True if file_path is on the orchestrator write allowlist.
|
|
443
|
+
|
|
444
|
+
Theme 9: ``endswith("status.md")`` would false-match ``bogusstatus.md``.
|
|
445
|
+
Suffix entries match against the basename (Path.name) so the check
|
|
446
|
+
requires the exact filename, not a tail-substring.
|
|
447
|
+
"""
|
|
448
|
+
for fragment in _ORCHESTRATOR_ALLOWLIST:
|
|
449
|
+
if fragment in file_path:
|
|
450
|
+
return True
|
|
451
|
+
try:
|
|
452
|
+
basename = Path(file_path).name
|
|
453
|
+
except (TypeError, ValueError):
|
|
454
|
+
return False
|
|
455
|
+
for suffix in _ORCHESTRATOR_ALLOWLIST_SUFFIXES:
|
|
456
|
+
if basename == suffix:
|
|
457
|
+
return True
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _check_orchestrator_write(file_path: str) -> str:
|
|
462
|
+
"""Return a warning message if the write appears to be orchestrator inline work.
|
|
463
|
+
|
|
464
|
+
Only warns — never denies. Returns empty string if no warning needed.
|
|
465
|
+
"""
|
|
466
|
+
if _is_allowlisted(file_path):
|
|
467
|
+
return ""
|
|
468
|
+
|
|
469
|
+
# Check if there is an active crew project in build or review phase
|
|
470
|
+
try:
|
|
471
|
+
data, project_name, _ = _find_active_crew_project()
|
|
472
|
+
if not project_name or not data:
|
|
473
|
+
return ""
|
|
474
|
+
|
|
475
|
+
current_phase = data.get("current_phase", "")
|
|
476
|
+
if current_phase.lower() not in _ORCHESTRATOR_PHASES:
|
|
477
|
+
return ""
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
f"[wicked-garden] Orchestrator principle: The active crew project "
|
|
481
|
+
f"'{project_name}' is in '{current_phase}' phase. "
|
|
482
|
+
f"Orchestrators should not write implementation files directly. "
|
|
483
|
+
f"Delegate to implementer, researcher, or specialist subagents via Task(). "
|
|
484
|
+
f"Allowed direct writes: .something-wicked/ paths and status.md files."
|
|
485
|
+
)
|
|
486
|
+
except Exception:
|
|
487
|
+
return ""
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
# Handler: Bash
|
|
492
|
+
# (crew gate preflight — intercepts phase_manager.py approve calls)
|
|
493
|
+
# ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
# Regex to extract project and phase from phase_manager.py approve invocations.
|
|
496
|
+
# Matches forms like:
|
|
497
|
+
# phase_manager.py myproject approve --phase build
|
|
498
|
+
# phase_manager.py myproject approve --phase build --override-gate
|
|
499
|
+
_APPROVE_RE = re.compile(
|
|
500
|
+
r"phase_manager\.py\s+(\S+)\s+approve(?:\s+.*?)?(?:--phase\s+(\S+))?",
|
|
501
|
+
re.DOTALL,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------------------------------------
|
|
506
|
+
# Bus-emit lint (Issue #734 Part B)
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
#
|
|
509
|
+
# Detects orphan writes to load-bearing crew state artifacts that are not
|
|
510
|
+
# accompanied by a recent ``wicked-bus`` event on the project's chain. The
|
|
511
|
+
# resume projector (#734 Part A) and any future bus-as-truth subscriber
|
|
512
|
+
# rely on emits to stay current — silent writes leave projections stale
|
|
513
|
+
# and force callers back to file polling.
|
|
514
|
+
#
|
|
515
|
+
# Configuration:
|
|
516
|
+
# WG_BUS_EMIT_LINT=warn|strict|off (default: warn)
|
|
517
|
+
# WG_BUS_EMIT_LINT_WINDOW_SEC=<int> (default: 60)
|
|
518
|
+
#
|
|
519
|
+
# Detection mechanism: query the daemon's projections.db ``event_log`` table
|
|
520
|
+
# for any event ingested in the last N seconds whose chain_id starts with
|
|
521
|
+
# ``{active_project_id}.``. Read-only sqlite URI mode keeps the projector
|
|
522
|
+
# untouched. ANY error fail-opens — the lint is advisory unless strict.
|
|
523
|
+
|
|
524
|
+
#: Filenames (basename match, platform-agnostic) that mark a path as
|
|
525
|
+
#: load-bearing crew state. Each is a post-audit-#735 high-priority gap.
|
|
526
|
+
#: Basename matching avoids the "Windows backslash vs POSIX forward-slash"
|
|
527
|
+
#: bug — Path(file_path).name is the same on every platform.
|
|
528
|
+
_BUS_EMIT_LINT_TARGET_BASENAMES = frozenset({
|
|
529
|
+
"gate-result.json",
|
|
530
|
+
"dispatch-log.jsonl",
|
|
531
|
+
"conditions-manifest.json",
|
|
532
|
+
"reviewer-report.md",
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
#: A file with a target basename only counts as crew state if its parent
|
|
536
|
+
#: chain contains a ``phases`` directory — the wicked-crew artifact
|
|
537
|
+
#: convention. This prevents a scratch file like
|
|
538
|
+
#: ``/tmp/notes/gate-result.json`` from triggering the lint.
|
|
539
|
+
_BUS_EMIT_LINT_PARENT_MARKERS = ("phases",)
|
|
540
|
+
|
|
541
|
+
#: Event types that semantically pair with the target artifacts. A "recent
|
|
542
|
+
#: emit" only counts if its event_type is in this set — otherwise an
|
|
543
|
+
#: unrelated ``wicked.project.created`` at session start would buy 60s of
|
|
544
|
+
#: silent orphan-write passes, which is exactly the false-negative the
|
|
545
|
+
#: lint exists to catch.
|
|
546
|
+
#:
|
|
547
|
+
#: Pairing rationale per target:
|
|
548
|
+
#: gate-result.json ← wicked.gate.decided
|
|
549
|
+
#: conditions-manifest.json ← wicked.gate.decided (CONDITIONAL branch)
|
|
550
|
+
#: dispatch-log.jsonl ← wicked.dispatch.log_entry_appended (Part C)
|
|
551
|
+
#: reviewer-report.md ← wicked.consensus.report_created /
|
|
552
|
+
#: wicked.consensus.evidence_recorded (Part C)
|
|
553
|
+
#:
|
|
554
|
+
#: Phase + project lifecycle events stay in the set because callers may
|
|
555
|
+
#: write multiple state files in the wake of a single phase/project event.
|
|
556
|
+
_BUS_EMIT_LINT_PAIR_EVENTS = (
|
|
557
|
+
"wicked.gate.decided",
|
|
558
|
+
"wicked.gate.blocked",
|
|
559
|
+
"wicked.rework.triggered",
|
|
560
|
+
"wicked.phase.transitioned",
|
|
561
|
+
"wicked.project.completed",
|
|
562
|
+
# Part C of #734 — new emits paired with their target artifact writes
|
|
563
|
+
"wicked.dispatch.log_entry_appended",
|
|
564
|
+
"wicked.consensus.report_created",
|
|
565
|
+
"wicked.consensus.evidence_recorded",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _bus_emit_lint_mode() -> str:
|
|
570
|
+
"""Return ``'off' | 'warn' | 'strict'`` (default ``'warn'``)."""
|
|
571
|
+
raw = (os.environ.get("WG_BUS_EMIT_LINT") or "warn").strip().lower()
|
|
572
|
+
return raw if raw in ("off", "warn", "strict") else "warn"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _bus_emit_lint_window_sec() -> int:
|
|
576
|
+
"""Return the recent-emit window in seconds (default 60)."""
|
|
577
|
+
try:
|
|
578
|
+
return max(1, int(os.environ.get("WG_BUS_EMIT_LINT_WINDOW_SEC") or "60"))
|
|
579
|
+
except (TypeError, ValueError):
|
|
580
|
+
return 60
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _is_bus_emit_lint_target(file_path: str) -> bool:
|
|
584
|
+
"""True iff file_path's basename matches a target AND it lives under a
|
|
585
|
+
crew-style ``phases/`` parent chain.
|
|
586
|
+
|
|
587
|
+
Basename-based to be Windows-safe (``C:\\proj\\phases\\build\\gate-result.json``
|
|
588
|
+
matches the same as the POSIX form). The parent-marker check prevents a
|
|
589
|
+
scratch file like ``/tmp/notes/gate-result.json`` from triggering the
|
|
590
|
+
lint just because the basename collides.
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
p = Path(file_path)
|
|
594
|
+
except (TypeError, ValueError):
|
|
595
|
+
return False
|
|
596
|
+
if p.name not in _BUS_EMIT_LINT_TARGET_BASENAMES:
|
|
597
|
+
return False
|
|
598
|
+
# Walk parent components looking for a wicked-crew style marker. Use
|
|
599
|
+
# ``parts`` (Path.parts) so the comparison is OS-native.
|
|
600
|
+
parts = p.parts
|
|
601
|
+
return any(marker in parts for marker in _BUS_EMIT_LINT_PARENT_MARKERS)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _resolve_daemon_db_path() -> "str | None":
|
|
605
|
+
"""Find the daemon DB. Honors ``WG_DAEMON_DB`` first (matches daemon/db.py)."""
|
|
606
|
+
candidates = [
|
|
607
|
+
os.environ.get("WG_DAEMON_DB"),
|
|
608
|
+
os.environ.get("WG_PROJECTOR_DB_PATH"), # legacy alias
|
|
609
|
+
str(Path.home() / ".something-wicked"
|
|
610
|
+
/ "wicked-garden-daemon" / "projections.db"),
|
|
611
|
+
]
|
|
612
|
+
for c in candidates:
|
|
613
|
+
if c and Path(c).is_file():
|
|
614
|
+
return c
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _has_recent_bus_emit(project_id: str, window_sec: int) -> bool:
|
|
619
|
+
"""Query event_log for a recent SEMANTICALLY-PAIRED emit on this project's chain.
|
|
620
|
+
|
|
621
|
+
Returns ``True`` on any error or missing DB — fail-open per this file's
|
|
622
|
+
convention (mirrors ``_check_challenge_gate``'s "any unexpected error
|
|
623
|
+
→ return '' (fail-open)" docstring). The lint stays silent when we
|
|
624
|
+
cannot verify; users running without the daemon don't get false
|
|
625
|
+
positives. The lint only fires when we CAN verify and find no
|
|
626
|
+
matching emit.
|
|
627
|
+
|
|
628
|
+
"Semantically paired" means the event_type is in
|
|
629
|
+
:data:`_BUS_EMIT_LINT_PAIR_EVENTS` — gate / phase / project lifecycle
|
|
630
|
+
events. An unrelated ``wicked.project.created`` at session start MUST
|
|
631
|
+
NOT buy a 60-second window of silent orphan-write passes; that would
|
|
632
|
+
be the exact false-negative the lint exists to catch.
|
|
633
|
+
|
|
634
|
+
Caveat — daemon projection race: ``emit_event()`` is fire-and-forget
|
|
635
|
+
and the daemon's consumer ingests into ``event_log`` on its polling
|
|
636
|
+
loop. A caller that emits and then writes within milliseconds may be
|
|
637
|
+
flagged because the projection has not yet caught up. The 60s default
|
|
638
|
+
window is sized to absorb realistic poll intervals; production users
|
|
639
|
+
can widen via ``WG_BUS_EMIT_LINT_WINDOW_SEC`` if their daemon polls
|
|
640
|
+
more slowly.
|
|
641
|
+
|
|
642
|
+
Read-only URI mode keeps us honest about not mutating the projector.
|
|
643
|
+
"""
|
|
644
|
+
db_path = _resolve_daemon_db_path()
|
|
645
|
+
if not db_path:
|
|
646
|
+
return True # fail-open — daemon not running / no projector
|
|
647
|
+
try:
|
|
648
|
+
import sqlite3
|
|
649
|
+
import time as _time
|
|
650
|
+
uri = Path(db_path).absolute().as_uri() + "?mode=ro"
|
|
651
|
+
conn = sqlite3.connect(uri, uri=True)
|
|
652
|
+
try:
|
|
653
|
+
since = int(_time.time()) - window_sec
|
|
654
|
+
# LIKE escape: '_' and '%' in project_id would otherwise match
|
|
655
|
+
# unrelated chains. Same idiom as scripts/crew/resume_projector.py
|
|
656
|
+
# — see brain memory "sqlite-like-wildcard-escape-gotcha".
|
|
657
|
+
escaped = (
|
|
658
|
+
project_id
|
|
659
|
+
.replace("!", "!!")
|
|
660
|
+
.replace("_", "!_")
|
|
661
|
+
.replace("%", "!%")
|
|
662
|
+
)
|
|
663
|
+
placeholders = ",".join("?" for _ in _BUS_EMIT_LINT_PAIR_EVENTS)
|
|
664
|
+
params = (
|
|
665
|
+
f"{escaped}.%",
|
|
666
|
+
since,
|
|
667
|
+
*_BUS_EMIT_LINT_PAIR_EVENTS,
|
|
668
|
+
)
|
|
669
|
+
row = conn.execute(
|
|
670
|
+
f"""SELECT 1 FROM event_log
|
|
671
|
+
WHERE chain_id LIKE ? ESCAPE '!'
|
|
672
|
+
AND ingested_at >= ?
|
|
673
|
+
AND event_type IN ({placeholders})
|
|
674
|
+
LIMIT 1""",
|
|
675
|
+
params,
|
|
676
|
+
).fetchone()
|
|
677
|
+
return row is not None
|
|
678
|
+
finally:
|
|
679
|
+
conn.close()
|
|
680
|
+
except Exception:
|
|
681
|
+
# sqlite3.Error, OSError on connect, anything unexpected — same
|
|
682
|
+
# fail-open convention as the missing-DB case above.
|
|
683
|
+
return True
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _check_bus_emit_lint(file_path: str) -> "tuple[str, str]":
|
|
687
|
+
"""Return ``(action, message)``.
|
|
688
|
+
|
|
689
|
+
``action``:
|
|
690
|
+
* ``'allow'`` — no message; lint disabled, not a target, no active
|
|
691
|
+
project, or recent emit found
|
|
692
|
+
* ``'warn'`` — caller should attach ``message`` as systemMessage
|
|
693
|
+
* ``'deny'`` — strict mode, no recent emit; caller should deny
|
|
694
|
+
|
|
695
|
+
Fail-open on any unexpected error.
|
|
696
|
+
"""
|
|
697
|
+
try:
|
|
698
|
+
mode = _bus_emit_lint_mode()
|
|
699
|
+
if mode == "off":
|
|
700
|
+
return "allow", ""
|
|
701
|
+
if not _is_bus_emit_lint_target(file_path):
|
|
702
|
+
return "allow", ""
|
|
703
|
+
|
|
704
|
+
data, project_name, _ = _find_active_crew_project()
|
|
705
|
+
if not data or not project_name:
|
|
706
|
+
return "allow", ""
|
|
707
|
+
|
|
708
|
+
project_id = data.get("id") or project_name
|
|
709
|
+
window = _bus_emit_lint_window_sec()
|
|
710
|
+
if _has_recent_bus_emit(project_id, window):
|
|
711
|
+
return "allow", ""
|
|
712
|
+
|
|
713
|
+
msg = (
|
|
714
|
+
f"[bus-emit lint] writing to {file_path} but no wicked-bus "
|
|
715
|
+
f"event was emitted on chain '{project_id}.*' in the last "
|
|
716
|
+
f"{window}s. The resume projector (#734) and future bus-as-truth "
|
|
717
|
+
f"subscribers (#732) rely on emits to track state — silent "
|
|
718
|
+
f"writes leave projections stale. "
|
|
719
|
+
f"Bypass: WG_BUS_EMIT_LINT=off | "
|
|
720
|
+
f"adjust window: WG_BUS_EMIT_LINT_WINDOW_SEC=<sec>"
|
|
721
|
+
)
|
|
722
|
+
if mode == "strict":
|
|
723
|
+
return "deny", msg
|
|
724
|
+
return "warn", msg
|
|
725
|
+
except Exception:
|
|
726
|
+
return "allow", ""
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _parse_frontmatter(text: str) -> dict:
|
|
730
|
+
"""Extract YAML frontmatter fields using simple regex — no YAML library.
|
|
731
|
+
|
|
732
|
+
Parses only the first ``---`` block. Returns a dict of key → raw string
|
|
733
|
+
values. Multi-line values and lists are NOT parsed; only flat scalar
|
|
734
|
+
``key: value`` pairs are extracted.
|
|
735
|
+
"""
|
|
736
|
+
fm: dict = {}
|
|
737
|
+
match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
|
|
738
|
+
if not match:
|
|
739
|
+
return fm
|
|
740
|
+
block = match.group(1)
|
|
741
|
+
for line in block.splitlines():
|
|
742
|
+
kv = re.match(r"^(\w[\w_-]*):\s*(.*)", line)
|
|
743
|
+
if kv:
|
|
744
|
+
fm[kv.group(1)] = kv.group(2).strip()
|
|
745
|
+
return fm
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _safe_read_text(path: Path) -> str:
|
|
749
|
+
"""Read file text; return '' on any error."""
|
|
750
|
+
try:
|
|
751
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
752
|
+
except Exception:
|
|
753
|
+
return ""
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _safe_read_json(path: Path):
|
|
757
|
+
"""Read and parse JSON; return None on any error."""
|
|
758
|
+
try:
|
|
759
|
+
return json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
760
|
+
except Exception:
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _load_phases_config_hook() -> dict:
|
|
765
|
+
"""Load phases.json from .claude-plugin/ relative to plugin root.
|
|
766
|
+
|
|
767
|
+
Returns the ``phases`` sub-dict, or an empty dict on any error.
|
|
768
|
+
This is a local copy of the logic in phase_manager.py so the hook
|
|
769
|
+
stays stdlib-only and avoids importing phase_manager (which has
|
|
770
|
+
side-effects at import time like reading env vars at module level).
|
|
771
|
+
"""
|
|
772
|
+
try:
|
|
773
|
+
config_path = _PLUGIN_ROOT / ".claude-plugin" / "phases.json"
|
|
774
|
+
if config_path.exists():
|
|
775
|
+
data = _safe_read_json(config_path)
|
|
776
|
+
if isinstance(data, dict):
|
|
777
|
+
return data.get("phases", {})
|
|
778
|
+
except Exception:
|
|
779
|
+
pass
|
|
780
|
+
return {}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _get_project_id_for(project_arg: str) -> str:
|
|
784
|
+
"""Resolve project ID from a project name / slug argument.
|
|
785
|
+
|
|
786
|
+
The phase_manager CLI accepts a project name or ID. We look inside the
|
|
787
|
+
wicked-crew projects directory for a project.json whose ``name`` or ``id``
|
|
788
|
+
matches ``project_arg``. Falls back to using ``project_arg`` verbatim as
|
|
789
|
+
the directory name (which works when the CLI was given the raw ID).
|
|
790
|
+
"""
|
|
791
|
+
try:
|
|
792
|
+
from _paths import get_local_path
|
|
793
|
+
base = get_local_path("wicked-crew", "projects")
|
|
794
|
+
# Validate before using as a path component
|
|
795
|
+
if re.match(r'^[a-zA-Z0-9_-]{1,64}$', project_arg):
|
|
796
|
+
direct = base / project_arg
|
|
797
|
+
if direct.is_dir():
|
|
798
|
+
return project_arg
|
|
799
|
+
|
|
800
|
+
# Scan all project directories for a name match
|
|
801
|
+
if base.exists():
|
|
802
|
+
for p in base.iterdir():
|
|
803
|
+
if not p.is_dir():
|
|
804
|
+
continue
|
|
805
|
+
pjson = _safe_read_json(p / "project.json")
|
|
806
|
+
if pjson and (pjson.get("name") == project_arg or pjson.get("id") == project_arg):
|
|
807
|
+
return p.name
|
|
808
|
+
except Exception:
|
|
809
|
+
pass
|
|
810
|
+
return project_arg
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _get_phase_plan(project_dir: Path) -> list:
|
|
814
|
+
"""Return the phase_plan list from project.json, or [] on failure."""
|
|
815
|
+
pjson = _safe_read_json(project_dir / "project.json")
|
|
816
|
+
if isinstance(pjson, dict):
|
|
817
|
+
return [str(p) for p in pjson.get("phase_plan", [])]
|
|
818
|
+
return []
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _get_project_complexity(project_dir: Path) -> int:
|
|
822
|
+
"""Return complexity_score from project.json, or 0 on failure."""
|
|
823
|
+
pjson = _safe_read_json(project_dir / "project.json")
|
|
824
|
+
if isinstance(pjson, dict):
|
|
825
|
+
return int(pjson.get("complexity_score", 0))
|
|
826
|
+
return 0
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _check_skipped_phases(phase_dir_base: Path, phase_plan: list, current_phase: str) -> str:
|
|
830
|
+
"""Return error message if any planned phase before current_phase has no directory.
|
|
831
|
+
|
|
832
|
+
A missing phase directory means the phase was silently skipped without
|
|
833
|
+
going through the proper skip flow (which records a skip reason).
|
|
834
|
+
"""
|
|
835
|
+
try:
|
|
836
|
+
idx = phase_plan.index(current_phase)
|
|
837
|
+
except ValueError:
|
|
838
|
+
return "" # current phase not in plan — can't assess
|
|
839
|
+
|
|
840
|
+
for prev in phase_plan[:idx]:
|
|
841
|
+
if not (phase_dir_base / prev).is_dir():
|
|
842
|
+
return (
|
|
843
|
+
f"phase '{prev}' has no execution directory — it appears to have been "
|
|
844
|
+
f"silently skipped without a recorded skip reason. "
|
|
845
|
+
f"Use /wicked-garden:crew:approve on '{prev}' first, or record a valid "
|
|
846
|
+
f"skip reason via phase_manager.py {prev} skip."
|
|
847
|
+
)
|
|
848
|
+
return ""
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _check_deliverables(phase_dir: Path, phase_name: str, phases_config: dict) -> str:
|
|
852
|
+
"""Return error message if any required deliverable is missing or too small."""
|
|
853
|
+
phase = phases_config.get(phase_name, {})
|
|
854
|
+
raw_deliverables = phase.get("required_deliverables", [])
|
|
855
|
+
|
|
856
|
+
for entry in raw_deliverables:
|
|
857
|
+
if isinstance(entry, str):
|
|
858
|
+
entry = {"file": entry, "min_bytes": 100, "frontmatter": []}
|
|
859
|
+
elif not isinstance(entry, dict):
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
fname = entry.get("file", "")
|
|
863
|
+
min_bytes = int(entry.get("min_bytes", 100))
|
|
864
|
+
required_fm = entry.get("frontmatter", [])
|
|
865
|
+
|
|
866
|
+
if not fname:
|
|
867
|
+
continue
|
|
868
|
+
|
|
869
|
+
fpath = phase_dir / fname
|
|
870
|
+
if not fpath.exists():
|
|
871
|
+
return (
|
|
872
|
+
f"required deliverable '{fname}' is missing from phases/{phase_name}/. "
|
|
873
|
+
f"Complete the phase work before approving."
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
size = fpath.stat().st_size
|
|
877
|
+
if size < min_bytes:
|
|
878
|
+
return (
|
|
879
|
+
f"required deliverable '{fname}' is too small ({size} bytes, "
|
|
880
|
+
f"minimum {min_bytes} bytes). The file may be a stub or incomplete."
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Check frontmatter fields
|
|
884
|
+
if required_fm:
|
|
885
|
+
text = _safe_read_text(fpath)
|
|
886
|
+
fm = _parse_frontmatter(text)
|
|
887
|
+
for field in required_fm:
|
|
888
|
+
if field not in fm or not fm[field]:
|
|
889
|
+
return (
|
|
890
|
+
f"required deliverable '{fname}' is missing frontmatter field "
|
|
891
|
+
f"'{field}'. Add the field to the YAML frontmatter block."
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
return ""
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _check_test_coverage(phase_dir_base: Path, phase_name: str, phases_config: dict) -> str:
|
|
898
|
+
"""Return error message if test coverage ratio is below threshold."""
|
|
899
|
+
phase = phases_config.get(phase_name, {})
|
|
900
|
+
min_coverage = phase.get("min_test_coverage")
|
|
901
|
+
|
|
902
|
+
# Only enforce for phases that declare a coverage requirement
|
|
903
|
+
if min_coverage is None:
|
|
904
|
+
return ""
|
|
905
|
+
|
|
906
|
+
# --- Find the planned count from test-strategy ---
|
|
907
|
+
planned = 0
|
|
908
|
+
for strategy_fname in ("test-plan.md", "test-strategy.md"):
|
|
909
|
+
strategy_path = phase_dir_base / "test-strategy" / strategy_fname
|
|
910
|
+
if strategy_path.exists():
|
|
911
|
+
fm = _parse_frontmatter(_safe_read_text(strategy_path))
|
|
912
|
+
raw = fm.get("case_count", "")
|
|
913
|
+
if raw:
|
|
914
|
+
try:
|
|
915
|
+
planned = int(re.sub(r"[^\d]", "", raw))
|
|
916
|
+
except ValueError:
|
|
917
|
+
pass
|
|
918
|
+
if planned:
|
|
919
|
+
break
|
|
920
|
+
|
|
921
|
+
if not planned:
|
|
922
|
+
return "" # Can't assess without a planned count
|
|
923
|
+
|
|
924
|
+
# --- Find the executed count from test-results ---
|
|
925
|
+
executed = 0
|
|
926
|
+
results_path = phase_dir_base / phase_name / "test-results.md"
|
|
927
|
+
if results_path.exists():
|
|
928
|
+
text = _safe_read_text(results_path)
|
|
929
|
+
fm = _parse_frontmatter(text)
|
|
930
|
+
|
|
931
|
+
# Try frontmatter fields first
|
|
932
|
+
for key in ("executed", "pass_count", "total_count", "test_count"):
|
|
933
|
+
if key in fm:
|
|
934
|
+
try:
|
|
935
|
+
executed = int(re.sub(r"[^\d]", "", fm[key]))
|
|
936
|
+
break
|
|
937
|
+
except ValueError:
|
|
938
|
+
pass
|
|
939
|
+
|
|
940
|
+
# Fallback: count test entries in markdown body (lines starting with - [ ] or - [x])
|
|
941
|
+
if not executed:
|
|
942
|
+
executed = len(re.findall(r"- \[[ xX]\]", text))
|
|
943
|
+
|
|
944
|
+
if not executed:
|
|
945
|
+
return "" # Can't assess without an executed count
|
|
946
|
+
|
|
947
|
+
ratio = executed / planned
|
|
948
|
+
if ratio < min_coverage:
|
|
949
|
+
pct_actual = int(ratio * 100)
|
|
950
|
+
pct_required = int(min_coverage * 100)
|
|
951
|
+
return (
|
|
952
|
+
f"test coverage is {pct_actual}% ({executed}/{planned} tests executed), "
|
|
953
|
+
f"but {pct_required}% is required for phase '{phase_name}'. "
|
|
954
|
+
f"Run the missing tests and update test-results.md before approving."
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
return ""
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _check_conditions_resolved(phase_dir_base: Path, phase_plan: list, current_phase: str,
|
|
961
|
+
phases_config: dict) -> str:
|
|
962
|
+
"""Return error message if any prior phase has unresolved conditions."""
|
|
963
|
+
try:
|
|
964
|
+
idx = phase_plan.index(current_phase)
|
|
965
|
+
except ValueError:
|
|
966
|
+
return ""
|
|
967
|
+
|
|
968
|
+
for prev in phase_plan[:idx]:
|
|
969
|
+
prev_cfg = phases_config.get(prev, {})
|
|
970
|
+
if not prev_cfg.get("conditions_manifest_required", False):
|
|
971
|
+
continue
|
|
972
|
+
|
|
973
|
+
manifest_path = phase_dir_base / prev / "conditions-manifest.json"
|
|
974
|
+
if not manifest_path.exists():
|
|
975
|
+
continue # No manifest = no conditional gate was issued
|
|
976
|
+
|
|
977
|
+
manifest = _safe_read_json(manifest_path)
|
|
978
|
+
if not isinstance(manifest, (list, dict)):
|
|
979
|
+
continue
|
|
980
|
+
|
|
981
|
+
conditions = manifest if isinstance(manifest, list) else manifest.get("conditions", [])
|
|
982
|
+
unresolved = [c for c in conditions if isinstance(c, dict) and not c.get("resolved_at")]
|
|
983
|
+
if unresolved:
|
|
984
|
+
labels = [c.get("id") or c.get("description", "unknown") for c in unresolved[:3]]
|
|
985
|
+
extra = f" (and {len(unresolved) - 3} more)" if len(unresolved) > 3 else ""
|
|
986
|
+
return (
|
|
987
|
+
f"phase '{prev}' has {len(unresolved)} unresolved condition(s): "
|
|
988
|
+
f"{', '.join(labels)}{extra}. "
|
|
989
|
+
f"Resolve all conditions in phases/{prev}/conditions-manifest.json "
|
|
990
|
+
f"before approving '{current_phase}'."
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
return ""
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _check_specialist_engagement(phase_dir: Path, phase_name: str, phases_config: dict,
|
|
997
|
+
complexity: int) -> str:
|
|
998
|
+
"""Return error message if required specialists are missing and complexity >= 5."""
|
|
999
|
+
phase = phases_config.get(phase_name, {})
|
|
1000
|
+
required = phase.get("required_specialists", [])
|
|
1001
|
+
|
|
1002
|
+
if not required or complexity < 5:
|
|
1003
|
+
return ""
|
|
1004
|
+
|
|
1005
|
+
# Site W9 of bus-cutover wave-2 (#746): the engagement file
|
|
1006
|
+
# was refactored from JSON-array (read-modify-write) to JSONL
|
|
1007
|
+
# (append-only) in W9a. Read the JSONL form preferentially; fall
|
|
1008
|
+
# back to the legacy .json form for production projects that
|
|
1009
|
+
# haven't yet had their first post-W9a engagement recorded. The
|
|
1010
|
+
# next engagement append starts the .jsonl file fresh; both readers
|
|
1011
|
+
# eventually converge on JSONL as projects exercise the hook.
|
|
1012
|
+
engaged_domains: set = set()
|
|
1013
|
+
engagement_jsonl = phase_dir / "specialist-engagement.jsonl"
|
|
1014
|
+
engagement_json = phase_dir / "specialist-engagement.json"
|
|
1015
|
+
if engagement_jsonl.exists():
|
|
1016
|
+
try:
|
|
1017
|
+
for line in engagement_jsonl.read_text(encoding="utf-8").splitlines():
|
|
1018
|
+
line = line.strip()
|
|
1019
|
+
if not line:
|
|
1020
|
+
continue
|
|
1021
|
+
try:
|
|
1022
|
+
entry = json.loads(line)
|
|
1023
|
+
except json.JSONDecodeError:
|
|
1024
|
+
continue
|
|
1025
|
+
if isinstance(entry, dict):
|
|
1026
|
+
engaged_domains.add(entry.get("domain", ""))
|
|
1027
|
+
except OSError:
|
|
1028
|
+
pass # fail-open: file unreadable → treat as no engagement
|
|
1029
|
+
elif engagement_json.exists():
|
|
1030
|
+
# Legacy backward-compat read for pre-W9a projects.
|
|
1031
|
+
entries = _safe_read_json(engagement_json)
|
|
1032
|
+
if isinstance(entries, list):
|
|
1033
|
+
engaged_domains = {
|
|
1034
|
+
e.get("domain", "") for e in entries if isinstance(e, dict)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
missing = [s for s in required if s not in engaged_domains]
|
|
1038
|
+
if missing:
|
|
1039
|
+
return (
|
|
1040
|
+
f"required specialist(s) did not engage for phase '{phase_name}': "
|
|
1041
|
+
f"{', '.join(missing)}. "
|
|
1042
|
+
f"This project has complexity {complexity} (>= 5), so specialist engagement "
|
|
1043
|
+
f"is mandatory. Invoke the missing specialists via Task() before approving."
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
return ""
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _warn_reviewer_report(phase_dir: Path, phase_name: str, complexity: int) -> str:
|
|
1050
|
+
"""Return a warning (non-blocking) if reviewer-report.md is missing for high-complexity phases."""
|
|
1051
|
+
if complexity < 5:
|
|
1052
|
+
return ""
|
|
1053
|
+
report_path = phase_dir / "reviewer-report.md"
|
|
1054
|
+
if not report_path.exists():
|
|
1055
|
+
return (
|
|
1056
|
+
f"[wicked-garden] Warning: phases/{phase_name}/reviewer-report.md is missing "
|
|
1057
|
+
f"for a complexity-{complexity} project. The async reviewer (Tier 2) should "
|
|
1058
|
+
f"have written this. Proceeding with approve, but consider running "
|
|
1059
|
+
f"/wicked-garden:crew:gate to generate a reviewer report."
|
|
1060
|
+
)
|
|
1061
|
+
return ""
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _crew_gate_preflight(command: str) -> dict:
|
|
1065
|
+
"""Run deterministic file-based gate checks before phase_manager.py approve executes.
|
|
1066
|
+
|
|
1067
|
+
Returns {"ok": True} to allow, or {"ok": False, "reason": "..."} to block.
|
|
1068
|
+
All checks complete in <100ms (file stat + small reads only).
|
|
1069
|
+
Fails open on any unexpected error.
|
|
1070
|
+
"""
|
|
1071
|
+
# ------------------------------------------------------------------ #
|
|
1072
|
+
# FAST EARLY-EXIT: skip if not an approve call #
|
|
1073
|
+
# ------------------------------------------------------------------ #
|
|
1074
|
+
if "phase_manager.py" not in command or "approve" not in command:
|
|
1075
|
+
return {"ok": True}
|
|
1076
|
+
|
|
1077
|
+
# ------------------------------------------------------------------ #
|
|
1078
|
+
# v6.0: strict enforcement always active (D3 — no legacy bypass) #
|
|
1079
|
+
# ------------------------------------------------------------------ #
|
|
1080
|
+
|
|
1081
|
+
# ------------------------------------------------------------------ #
|
|
1082
|
+
# Parse project and phase from command #
|
|
1083
|
+
# ------------------------------------------------------------------ #
|
|
1084
|
+
m = _APPROVE_RE.search(command)
|
|
1085
|
+
if not m:
|
|
1086
|
+
return {"ok": True} # Unrecognised form — fail open
|
|
1087
|
+
|
|
1088
|
+
project_arg = m.group(1)
|
|
1089
|
+
phase_name = m.group(2)
|
|
1090
|
+
|
|
1091
|
+
if not project_arg:
|
|
1092
|
+
return {"ok": True}
|
|
1093
|
+
|
|
1094
|
+
# If --phase was not explicit, try to derive from session state
|
|
1095
|
+
if not phase_name:
|
|
1096
|
+
try:
|
|
1097
|
+
from _session import SessionState
|
|
1098
|
+
state = SessionState.load()
|
|
1099
|
+
if state.active_project and isinstance(state.active_project, dict):
|
|
1100
|
+
phase_name = state.active_project.get("current_phase")
|
|
1101
|
+
except Exception:
|
|
1102
|
+
pass
|
|
1103
|
+
|
|
1104
|
+
if not phase_name:
|
|
1105
|
+
return {"ok": True} # Can't validate without knowing the phase
|
|
1106
|
+
|
|
1107
|
+
# ------------------------------------------------------------------ #
|
|
1108
|
+
# Resolve paths #
|
|
1109
|
+
# ------------------------------------------------------------------ #
|
|
1110
|
+
try:
|
|
1111
|
+
project_id = _get_project_id_for(project_arg)
|
|
1112
|
+
from _paths import get_local_path
|
|
1113
|
+
base = get_local_path("wicked-crew", "projects")
|
|
1114
|
+
project_dir = base / project_id
|
|
1115
|
+
phase_dir = project_dir / "phases" / phase_name
|
|
1116
|
+
phase_dir_base = project_dir / "phases"
|
|
1117
|
+
except Exception:
|
|
1118
|
+
return {"ok": True} # Path resolution failure — fail open
|
|
1119
|
+
|
|
1120
|
+
# If project.json doesn't exist, we have no ground truth — fail open.
|
|
1121
|
+
# This handles the case where phase_manager.py is invoked on a project
|
|
1122
|
+
# that doesn't yet have local state (e.g. first run, or different workspace).
|
|
1123
|
+
if not (project_dir / "project.json").exists():
|
|
1124
|
+
return {"ok": True}
|
|
1125
|
+
|
|
1126
|
+
phases_config = _load_phases_config_hook()
|
|
1127
|
+
phase_plan = _get_phase_plan(project_dir)
|
|
1128
|
+
complexity = _get_project_complexity(project_dir)
|
|
1129
|
+
|
|
1130
|
+
# ------------------------------------------------------------------ #
|
|
1131
|
+
# Check a: Skipped phase detection #
|
|
1132
|
+
# ------------------------------------------------------------------ #
|
|
1133
|
+
if phase_plan:
|
|
1134
|
+
msg = _check_skipped_phases(phase_dir_base, phase_plan, phase_name)
|
|
1135
|
+
if msg:
|
|
1136
|
+
return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
|
|
1137
|
+
|
|
1138
|
+
# ------------------------------------------------------------------ #
|
|
1139
|
+
# Check b: Phase directory exists #
|
|
1140
|
+
# ------------------------------------------------------------------ #
|
|
1141
|
+
if not phase_dir.is_dir():
|
|
1142
|
+
return {
|
|
1143
|
+
"ok": False,
|
|
1144
|
+
"reason": (
|
|
1145
|
+
f"Cannot approve {phase_name}: phases/{phase_name}/ directory does not exist. "
|
|
1146
|
+
f"The phase has not produced any output. Execute the phase before approving."
|
|
1147
|
+
),
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
# ------------------------------------------------------------------ #
|
|
1151
|
+
# Check c: Required deliverables present + frontmatter valid #
|
|
1152
|
+
# ------------------------------------------------------------------ #
|
|
1153
|
+
msg = _check_deliverables(phase_dir, phase_name, phases_config)
|
|
1154
|
+
if msg:
|
|
1155
|
+
return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
|
|
1156
|
+
|
|
1157
|
+
# ------------------------------------------------------------------ #
|
|
1158
|
+
# Check d: Test coverage ratio #
|
|
1159
|
+
# ------------------------------------------------------------------ #
|
|
1160
|
+
msg = _check_test_coverage(phase_dir_base, phase_name, phases_config)
|
|
1161
|
+
if msg:
|
|
1162
|
+
return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
|
|
1163
|
+
|
|
1164
|
+
# ------------------------------------------------------------------ #
|
|
1165
|
+
# Check e: Conditions resolved in prior phases #
|
|
1166
|
+
# ------------------------------------------------------------------ #
|
|
1167
|
+
if phase_plan:
|
|
1168
|
+
msg = _check_conditions_resolved(phase_dir_base, phase_plan, phase_name, phases_config)
|
|
1169
|
+
if msg:
|
|
1170
|
+
return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
|
|
1171
|
+
|
|
1172
|
+
# ------------------------------------------------------------------ #
|
|
1173
|
+
# Check f: Specialist engagement (blocks if complexity >= 5) #
|
|
1174
|
+
# ------------------------------------------------------------------ #
|
|
1175
|
+
msg = _check_specialist_engagement(phase_dir, phase_name, phases_config, complexity)
|
|
1176
|
+
if msg:
|
|
1177
|
+
return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
|
|
1178
|
+
|
|
1179
|
+
# ------------------------------------------------------------------ #
|
|
1180
|
+
# Check g: Reviewer report (warn only) #
|
|
1181
|
+
# ------------------------------------------------------------------ #
|
|
1182
|
+
warning = _warn_reviewer_report(phase_dir, phase_name, complexity)
|
|
1183
|
+
|
|
1184
|
+
return {"ok": True, "_warning": warning}
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def _handle_bash(tool_input: dict, cwd: str = "") -> str:
|
|
1188
|
+
"""Bash preflight: crew gate check + worktree cwd-leak guard (#878)."""
|
|
1189
|
+
command = tool_input.get("command", "") or ""
|
|
1190
|
+
|
|
1191
|
+
# Crew gate preflight first — a hard deny here blocks regardless of cwd.
|
|
1192
|
+
# Fast path — non-approve Bash commands take <1ms.
|
|
1193
|
+
result = _crew_gate_preflight(command)
|
|
1194
|
+
if not result.get("ok"):
|
|
1195
|
+
return _deny(result.get("reason", "Gate preflight check failed."))
|
|
1196
|
+
warning = result.get("_warning", "") or None
|
|
1197
|
+
|
|
1198
|
+
# Worktree cwd-leak guard (#878): when an isolated-worktree agent runs a
|
|
1199
|
+
# Bash command, anchor it in the worktree (cd <root> && ...) so a leaked
|
|
1200
|
+
# persistent cwd cannot land commits in the main repo. Fail-open by design.
|
|
1201
|
+
try:
|
|
1202
|
+
from worktree_guard import evaluate as _wt_evaluate
|
|
1203
|
+
decision = _wt_evaluate(command, cwd)
|
|
1204
|
+
except Exception:
|
|
1205
|
+
decision = None
|
|
1206
|
+
if decision:
|
|
1207
|
+
if decision.get("action") == "deny":
|
|
1208
|
+
return _deny(decision["reason"])
|
|
1209
|
+
if decision.get("action") == "rewrite":
|
|
1210
|
+
updated = dict(tool_input)
|
|
1211
|
+
updated["command"] = decision["command"]
|
|
1212
|
+
return _allow(updated_input=updated, system_message=warning)
|
|
1213
|
+
|
|
1214
|
+
return _allow(system_message=warning)
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
# ---------------------------------------------------------------------------
|
|
1218
|
+
# Main dispatcher
|
|
1219
|
+
# ---------------------------------------------------------------------------
|
|
1220
|
+
|
|
1221
|
+
def main():
|
|
1222
|
+
_t0 = time.monotonic()
|
|
1223
|
+
|
|
1224
|
+
try:
|
|
1225
|
+
raw = sys.stdin.read()
|
|
1226
|
+
input_data = json.loads(raw) if raw.strip() else {}
|
|
1227
|
+
except Exception:
|
|
1228
|
+
print(_allow())
|
|
1229
|
+
return
|
|
1230
|
+
|
|
1231
|
+
try:
|
|
1232
|
+
tool_name = input_data.get("tool_name", "")
|
|
1233
|
+
tool_input = input_data.get("tool_input", {}) or {}
|
|
1234
|
+
|
|
1235
|
+
_log("pretool", "debug", "hook.start", detail={"tool": tool_name})
|
|
1236
|
+
|
|
1237
|
+
if tool_name == "TaskCreate":
|
|
1238
|
+
result = _handle_task_create(tool_input)
|
|
1239
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1240
|
+
print(result)
|
|
1241
|
+
return
|
|
1242
|
+
|
|
1243
|
+
if tool_name == "TaskUpdate":
|
|
1244
|
+
result = _handle_task_update(tool_input)
|
|
1245
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1246
|
+
print(result)
|
|
1247
|
+
return
|
|
1248
|
+
|
|
1249
|
+
if tool_name == "EnterPlanMode":
|
|
1250
|
+
result = _handle_enter_plan_mode(tool_input)
|
|
1251
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1252
|
+
print(result)
|
|
1253
|
+
return
|
|
1254
|
+
|
|
1255
|
+
if tool_name in ("Write", "Edit"):
|
|
1256
|
+
result = _handle_write_guard(tool_input)
|
|
1257
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1258
|
+
print(result)
|
|
1259
|
+
return
|
|
1260
|
+
|
|
1261
|
+
if tool_name == "Bash":
|
|
1262
|
+
result = _handle_bash(tool_input, input_data.get("cwd", "") or "")
|
|
1263
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1264
|
+
print(result)
|
|
1265
|
+
return
|
|
1266
|
+
|
|
1267
|
+
# All other tools — allow
|
|
1268
|
+
_log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1269
|
+
print(_allow())
|
|
1270
|
+
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
print(f"[wicked-garden] pre_tool error: {e}", file=sys.stderr)
|
|
1273
|
+
# Always fail open on error
|
|
1274
|
+
print(_allow())
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
if __name__ == "__main__":
|
|
1278
|
+
main()
|