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,1797 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SessionStart hook — wicked-garden unified bootstrap.
|
|
4
|
+
|
|
5
|
+
Consolidates: crew session_start, mem session_start,
|
|
6
|
+
search session_start, smaht session_start.
|
|
7
|
+
|
|
8
|
+
Flow:
|
|
9
|
+
1. Read config from ~/.something-wicked/wicked-garden/config.json
|
|
10
|
+
2. If missing or setup_complete=false: emit setup instructions, return
|
|
11
|
+
3. Load dynamic agents
|
|
12
|
+
4. Load active crew project
|
|
13
|
+
5. Run memory decay
|
|
14
|
+
6. Check onboarding status → imperative directive if needed
|
|
15
|
+
7. Assemble session briefing
|
|
16
|
+
8. Return {"continue": true, "systemMessage": "<briefing>"}
|
|
17
|
+
|
|
18
|
+
Always fails open — any unhandled exception returns {"continue": true}.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
# Add shared scripts directory to path so hook can import shared modules
|
|
30
|
+
_PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
|
|
31
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
|
|
32
|
+
|
|
33
|
+
from _brain_port import resolve_port
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Ops logger wrapper — fail-silent, never crashes the hook
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _log(domain, level, event, ok=True, ms=None, detail=None):
|
|
40
|
+
"""Ops logger — fail-silent, never crashes the hook."""
|
|
41
|
+
try:
|
|
42
|
+
from _logger import log
|
|
43
|
+
log(domain, level, event, ok=ok, ms=ms, detail=detail)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Shared module imports — each wrapped so missing modules degrade gracefully
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def _load_session_state():
|
|
53
|
+
try:
|
|
54
|
+
from _session import SessionState
|
|
55
|
+
return SessionState.load()
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _save_session_state(state):
|
|
61
|
+
try:
|
|
62
|
+
state.save()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _check_prior_guard_findings():
|
|
68
|
+
"""Load the most recent guard-pipeline findings and render a briefing note.
|
|
69
|
+
|
|
70
|
+
Reads ${TMPDIR}/wicked-guard/*/findings.json files written by stop.py's
|
|
71
|
+
guard pipeline (Issue #448). Returns a briefing string or None.
|
|
72
|
+
|
|
73
|
+
Fail-open — never blocks bootstrap.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
import tempfile
|
|
77
|
+
guard_root = Path(tempfile.gettempdir()) / "wicked-guard"
|
|
78
|
+
if not guard_root.is_dir():
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
newest = None # type: ignore
|
|
82
|
+
newest_mtime = 0.0
|
|
83
|
+
for session_dir in guard_root.iterdir():
|
|
84
|
+
if not session_dir.is_dir():
|
|
85
|
+
continue
|
|
86
|
+
f = session_dir / "findings.json"
|
|
87
|
+
if not f.is_file():
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
mt = f.stat().st_mtime
|
|
91
|
+
except OSError:
|
|
92
|
+
continue
|
|
93
|
+
if mt > newest_mtime:
|
|
94
|
+
newest_mtime = mt
|
|
95
|
+
newest = f
|
|
96
|
+
|
|
97
|
+
if newest is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Only surface if report is recent (<24h) — stale reports are noise.
|
|
101
|
+
if (time.time() - newest_mtime) > 86400:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
with open(newest, "r", encoding="utf-8") as fh:
|
|
105
|
+
report = json.load(fh)
|
|
106
|
+
|
|
107
|
+
total = int(report.get("total_findings", 0) or 0)
|
|
108
|
+
if total == 0:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
by_sev = report.get("findings_by_severity", {}) or {}
|
|
112
|
+
profile = report.get("profile", "?")
|
|
113
|
+
lines = [
|
|
114
|
+
f"[Guard] Last session's {profile} guard pipeline surfaced {total} finding(s) "
|
|
115
|
+
f"(block={by_sev.get('block', 0)} warn={by_sev.get('warn', 0)} info={by_sev.get('info', 0)}). "
|
|
116
|
+
f"Report: {newest}"
|
|
117
|
+
]
|
|
118
|
+
# Show up to 5 non-info findings for visibility
|
|
119
|
+
shown = 0
|
|
120
|
+
for check in report.get("checks", []) or []:
|
|
121
|
+
for f in check.get("findings", []) or []:
|
|
122
|
+
if shown >= 5:
|
|
123
|
+
break
|
|
124
|
+
sev = f.get("severity", "info")
|
|
125
|
+
if sev == "info":
|
|
126
|
+
continue
|
|
127
|
+
msg = f.get("message", "")[:140]
|
|
128
|
+
loc = ""
|
|
129
|
+
if f.get("file"):
|
|
130
|
+
loc = f" @ {f['file']}"
|
|
131
|
+
if f.get("line"):
|
|
132
|
+
loc += f":{f['line']}"
|
|
133
|
+
lines.append(f" - [{sev}] {f.get('rule_id', '?')}: {msg}{loc}")
|
|
134
|
+
shown += 1
|
|
135
|
+
if shown >= 5:
|
|
136
|
+
break
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
except Exception:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _probe_plugin_readiness():
|
|
143
|
+
"""Dynamically discover installed plugins and probe their hook readiness.
|
|
144
|
+
|
|
145
|
+
Scans the plugin cache for installed plugins (excluding ourselves), finds
|
|
146
|
+
their hooks.json, and runs a lightweight test of each hook command.
|
|
147
|
+
Returns a list of dicts for plugins whose hooks fail, e.g.:
|
|
148
|
+
[{"name": "semgrep", "hook": "PostToolUse", "error": "No SEMGREP_APP_TOKEN found"}]
|
|
149
|
+
|
|
150
|
+
Runs with a tight timeout (2s per probe) and fails open — never blocks bootstrap.
|
|
151
|
+
"""
|
|
152
|
+
unready = []
|
|
153
|
+
try:
|
|
154
|
+
# Discover plugin cache directories
|
|
155
|
+
cache_dirs = [
|
|
156
|
+
Path.home() / ".claude" / "plugins" / "cache",
|
|
157
|
+
Path(os.environ.get("CLAUDE_CONFIG_DIR", "")) / "plugins" / "cache" if os.environ.get("CLAUDE_CONFIG_DIR") else None,
|
|
158
|
+
]
|
|
159
|
+
# Also check alt-configs location
|
|
160
|
+
alt_config = Path.home() / "alt-configs" / ".claude" / "plugins" / "cache"
|
|
161
|
+
if alt_config.exists():
|
|
162
|
+
cache_dirs.append(alt_config)
|
|
163
|
+
|
|
164
|
+
our_name = "wicked-garden"
|
|
165
|
+
seen_plugins = set()
|
|
166
|
+
|
|
167
|
+
for cache_dir in cache_dirs:
|
|
168
|
+
if cache_dir is None or not cache_dir.exists():
|
|
169
|
+
continue
|
|
170
|
+
# Walk plugin org/name directories
|
|
171
|
+
for org_dir in cache_dir.iterdir():
|
|
172
|
+
if not org_dir.is_dir():
|
|
173
|
+
continue
|
|
174
|
+
for plugin_dir in org_dir.iterdir():
|
|
175
|
+
if not plugin_dir.is_dir():
|
|
176
|
+
continue
|
|
177
|
+
# Find the actual plugin root (may be versioned)
|
|
178
|
+
hooks_candidates = list(plugin_dir.rglob("hooks/hooks.json"))
|
|
179
|
+
for hooks_file in hooks_candidates:
|
|
180
|
+
plugin_root = hooks_file.parent.parent
|
|
181
|
+
# Read plugin.json to get name
|
|
182
|
+
pj = plugin_root / ".claude-plugin" / "plugin.json"
|
|
183
|
+
if not pj.exists():
|
|
184
|
+
pj = plugin_root / "plugin.json"
|
|
185
|
+
plugin_name = plugin_dir.name
|
|
186
|
+
if pj.exists():
|
|
187
|
+
try:
|
|
188
|
+
plugin_name = json.loads(pj.read_text()).get("name", plugin_dir.name)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Skip ourselves and already-checked plugins
|
|
193
|
+
if plugin_name == our_name or plugin_name in seen_plugins:
|
|
194
|
+
continue
|
|
195
|
+
seen_plugins.add(plugin_name)
|
|
196
|
+
|
|
197
|
+
# Read their hooks.json
|
|
198
|
+
try:
|
|
199
|
+
hooks_data = json.loads(hooks_file.read_text())
|
|
200
|
+
except Exception:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Probe each hook command with a dry-run (empty stdin, 2s timeout)
|
|
204
|
+
for event, entries in hooks_data.get("hooks", {}).items():
|
|
205
|
+
for entry in entries:
|
|
206
|
+
for hook in entry.get("hooks", []):
|
|
207
|
+
cmd = hook.get("command", "")
|
|
208
|
+
if not cmd or hook.get("type") != "command":
|
|
209
|
+
continue
|
|
210
|
+
# Extract the base binary name for the probe
|
|
211
|
+
parts = cmd.split()
|
|
212
|
+
if not parts:
|
|
213
|
+
continue
|
|
214
|
+
binary = parts[0]
|
|
215
|
+
# Skip if binary references plugin root (needs env var)
|
|
216
|
+
if "${CLAUDE_PLUGIN_ROOT}" in cmd:
|
|
217
|
+
continue
|
|
218
|
+
# Quick probe: just check if the binary is available and
|
|
219
|
+
# runs without error when given empty input
|
|
220
|
+
try:
|
|
221
|
+
result = subprocess.run(
|
|
222
|
+
parts + ["--help"] if len(parts) == 1 else parts[:2] + ["--version"],
|
|
223
|
+
capture_output=True, text=True, timeout=2,
|
|
224
|
+
input="",
|
|
225
|
+
)
|
|
226
|
+
# If the command itself isn't found, skip — that's a
|
|
227
|
+
# different problem (missing install, not unready)
|
|
228
|
+
except FileNotFoundError:
|
|
229
|
+
continue # Binary not installed — not our problem
|
|
230
|
+
except subprocess.TimeoutExpired:
|
|
231
|
+
continue # Timed out — assume it works but is slow
|
|
232
|
+
except Exception:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# For env-var-gated tools, check common auth patterns
|
|
236
|
+
# by looking at stderr for auth/token messages
|
|
237
|
+
if result.returncode != 0:
|
|
238
|
+
stderr = result.stderr.lower()
|
|
239
|
+
auth_signals = ["token", "login", "auth", "credential", "api_key", "api key"]
|
|
240
|
+
if any(sig in stderr for sig in auth_signals):
|
|
241
|
+
# Extract a short error message
|
|
242
|
+
err_lines = [l.strip() for l in result.stderr.strip().split("\n") if l.strip()]
|
|
243
|
+
short_err = err_lines[-1][:120] if err_lines else "auth required"
|
|
244
|
+
unready.append({
|
|
245
|
+
"name": plugin_name,
|
|
246
|
+
"hook": event,
|
|
247
|
+
"command": cmd[:80],
|
|
248
|
+
"error": short_err,
|
|
249
|
+
})
|
|
250
|
+
break # One failure per plugin is enough
|
|
251
|
+
break # Only test first hook per event
|
|
252
|
+
if any(u["name"] == plugin_name for u in unready):
|
|
253
|
+
break # Already flagged this plugin
|
|
254
|
+
except Exception:
|
|
255
|
+
pass # Fail open — probe errors never block bootstrap
|
|
256
|
+
return unready
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _suggest_auth_fix(probe_result):
|
|
260
|
+
"""Dynamically extract or infer an auth fix command from a probe failure.
|
|
261
|
+
|
|
262
|
+
Looks for common patterns in the error message (e.g., "please login", "run X login",
|
|
263
|
+
"set SOME_TOKEN") and returns the fix command. No hardcoded tool list — works by
|
|
264
|
+
parsing the error text that the tool itself produced.
|
|
265
|
+
|
|
266
|
+
Returns a fix command string, or None if no fix can be inferred.
|
|
267
|
+
"""
|
|
268
|
+
error = probe_result.get("error", "")
|
|
269
|
+
name = probe_result.get("name", "")
|
|
270
|
+
cmd_base = probe_result.get("command", "").split()[0] if probe_result.get("command") else name
|
|
271
|
+
|
|
272
|
+
# Pattern 1: error says "run '<tool> login'" or "please login"
|
|
273
|
+
# e.g., "please login to Semgrep" → "semgrep login"
|
|
274
|
+
import re
|
|
275
|
+
login_match = re.search(r"(?:run|please)\s+['\"]?(\w[\w-]*\s+(?:login|auth\s+login))['\"]?", error, re.IGNORECASE)
|
|
276
|
+
if login_match:
|
|
277
|
+
return login_match.group(1)
|
|
278
|
+
|
|
279
|
+
# Pattern 2: error mentions a specific env var
|
|
280
|
+
# e.g., "No SEMGREP_APP_TOKEN found" → "export SEMGREP_APP_TOKEN=<your-token>"
|
|
281
|
+
env_match = re.search(r"(?:no|missing|set|need)\s+(\w+_(?:TOKEN|KEY|SECRET|API_KEY))\b", error, re.IGNORECASE)
|
|
282
|
+
if env_match:
|
|
283
|
+
var = env_match.group(1)
|
|
284
|
+
return f"export {var}=<your-token> # or: {cmd_base} login"
|
|
285
|
+
|
|
286
|
+
# Pattern 3: generic "login" or "auth" mention
|
|
287
|
+
if "login" in error.lower() or "auth" in error.lower():
|
|
288
|
+
return f"{cmd_base} login"
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _load_agents():
|
|
294
|
+
"""Load dynamic agents from disk. Returns (agent_count, agents_dict)."""
|
|
295
|
+
try:
|
|
296
|
+
from _agents import AgentLoader
|
|
297
|
+
loader = AgentLoader()
|
|
298
|
+
agents = loader.load_disk_agents(_PLUGIN_ROOT / "agents")
|
|
299
|
+
return len(agents), agents
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"[wicked-garden] agent load error: {e}", file=sys.stderr)
|
|
302
|
+
return 0, {}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _resolve_capabilities(agents, config=None):
|
|
306
|
+
"""Run capability resolution for pre-loaded agents.
|
|
307
|
+
|
|
308
|
+
Returns dict[agent_name, tool_list] or empty dict on any error.
|
|
309
|
+
Fails open — never blocks bootstrap.
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
from _capability_resolver import resolve_all_agents, discover_mcp_servers
|
|
313
|
+
|
|
314
|
+
mcp_servers = discover_mcp_servers()
|
|
315
|
+
return resolve_all_agents(agents, config, mcp_servers)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print(f"[wicked-garden] capability resolution error: {e}", file=sys.stderr)
|
|
318
|
+
return {}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _find_active_crew_project(workspace: str = ""):
|
|
322
|
+
"""Return (project_data, project_name) for most recently updated active crew project
|
|
323
|
+
scoped to the given workspace.
|
|
324
|
+
|
|
325
|
+
Only returns projects whose ``workspace`` field matches the current folder.
|
|
326
|
+
Projects without a workspace field (legacy) are never auto-selected.
|
|
327
|
+
|
|
328
|
+
Uses DomainStore for consistent data access. Falls back to filesystem
|
|
329
|
+
scan if DomainStore fails (legacy compat for one release).
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
from _domain_store import DomainStore
|
|
333
|
+
ds = DomainStore("wicked-crew", hook_mode=True)
|
|
334
|
+
projects = ds.list("projects") or []
|
|
335
|
+
# Sort by updated_at descending, return first non-archived, non-complete
|
|
336
|
+
# that matches the current workspace
|
|
337
|
+
for p in sorted(projects, key=lambda x: x.get("updated_at", ""), reverse=True):
|
|
338
|
+
if p.get("archived"):
|
|
339
|
+
continue
|
|
340
|
+
# Workspace scoping — skip projects from other workspaces
|
|
341
|
+
if workspace and p.get("workspace", "") != workspace:
|
|
342
|
+
continue
|
|
343
|
+
phase = p.get("current_phase", "")
|
|
344
|
+
if phase and phase != "complete":
|
|
345
|
+
return p, p.get("name")
|
|
346
|
+
return None, None
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
# Legacy fallback: direct filesystem scan (remove after one release)
|
|
351
|
+
return _find_active_crew_project_legacy(workspace)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _find_active_crew_project_legacy(workspace: str = ""):
|
|
355
|
+
"""Filesystem-based crew project scan. Legacy fallback."""
|
|
356
|
+
projects_dir = Path.home() / ".something-wicked" / "wicked-crew" / "projects"
|
|
357
|
+
if not projects_dir.exists():
|
|
358
|
+
return None, None
|
|
359
|
+
|
|
360
|
+
for pf in sorted(
|
|
361
|
+
projects_dir.glob("*/project.json"),
|
|
362
|
+
key=lambda p: p.stat().st_mtime,
|
|
363
|
+
reverse=True,
|
|
364
|
+
):
|
|
365
|
+
try:
|
|
366
|
+
data = json.loads(pf.read_text())
|
|
367
|
+
if data.get("archived"):
|
|
368
|
+
continue
|
|
369
|
+
# Workspace scoping — skip projects from other workspaces
|
|
370
|
+
if workspace and data.get("workspace", "") != workspace:
|
|
371
|
+
continue
|
|
372
|
+
phase = data.get("current_phase", "")
|
|
373
|
+
if phase and phase != "complete":
|
|
374
|
+
return data, data.get("name", pf.parent.name)
|
|
375
|
+
except (json.JSONDecodeError, OSError):
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
return None, None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _brain_api(action, params=None, timeout=3):
|
|
382
|
+
"""Call brain API. Returns parsed JSON or None."""
|
|
383
|
+
try:
|
|
384
|
+
import urllib.request
|
|
385
|
+
port = resolve_port()
|
|
386
|
+
payload = json.dumps({"action": action, "params": params or {}}).encode("utf-8")
|
|
387
|
+
req = urllib.request.Request(
|
|
388
|
+
f"http://localhost:{port}/api",
|
|
389
|
+
data=payload,
|
|
390
|
+
headers={"Content-Type": "application/json"},
|
|
391
|
+
method="POST",
|
|
392
|
+
)
|
|
393
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosem: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- URL is f"http://localhost:{port}/api" with int-validated port from resolve_port()
|
|
394
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
395
|
+
except Exception:
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _run_memory_decay():
|
|
400
|
+
"""Run memory decay maintenance via brain lint. Returns a summary string or None."""
|
|
401
|
+
try:
|
|
402
|
+
result = _brain_api("lint", {}, timeout=5)
|
|
403
|
+
if result and (result.get("archived", 0) > 0 or result.get("deleted", 0) > 0):
|
|
404
|
+
return f"Memory: {result.get('archived', 0)} archived, {result.get('deleted', 0)} cleaned"
|
|
405
|
+
return None
|
|
406
|
+
except Exception:
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _check_search_staleness():
|
|
411
|
+
"""Check brain index health. Returns a briefing note or None.
|
|
412
|
+
|
|
413
|
+
Brain is the unified knowledge layer — no legacy SQLite fallback.
|
|
414
|
+
Fails open — any exception returns None so the session always continues.
|
|
415
|
+
"""
|
|
416
|
+
stats = _brain_api("stats", {}, timeout=2)
|
|
417
|
+
if stats is None:
|
|
418
|
+
# Deterministic auto-start — server lifecycle is a hook job, not a
|
|
419
|
+
# model directive. ensure_server is lock-safe and fails open.
|
|
420
|
+
try:
|
|
421
|
+
from _brain_port import ensure_server
|
|
422
|
+
if ensure_server():
|
|
423
|
+
stats = _brain_api("stats", {}, timeout=2)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
if stats is None:
|
|
427
|
+
return (
|
|
428
|
+
"Brain server unreachable and auto-start failed — run "
|
|
429
|
+
"`wicked-brain-call --start` to see the cause. Brain skills "
|
|
430
|
+
"auto-start the server on every call; do NOT skip brain usage."
|
|
431
|
+
)
|
|
432
|
+
if stats.get("total", 0) == 0:
|
|
433
|
+
return "Brain index is empty — run `wicked-brain:ingest` to index your codebase"
|
|
434
|
+
return None # Brain is healthy
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _check_onboarding_status():
|
|
438
|
+
"""Check if the current project has been onboarded (search index + memories).
|
|
439
|
+
|
|
440
|
+
Returns (has_index, has_memories, directive_or_none).
|
|
441
|
+
Fails open when the brain is unreachable: transient API errors must not be
|
|
442
|
+
interpreted as "this project was never onboarded". Per-install setup state
|
|
443
|
+
is enforced separately by _check_setup_gate in prompt_submit.py — that
|
|
444
|
+
flag is user-level (~/.something-wicked/wicked-garden/config.json), not
|
|
445
|
+
per-project, so it cannot answer the per-project onboarding question here.
|
|
446
|
+
"""
|
|
447
|
+
project = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
|
|
448
|
+
cwd = str(Path.cwd())
|
|
449
|
+
|
|
450
|
+
has_index = False
|
|
451
|
+
has_memories = False # default: assume not onboarded (fail-closed for new projects)
|
|
452
|
+
|
|
453
|
+
# Check search index — brain is the knowledge layer
|
|
454
|
+
stats = _brain_api("stats", {}, timeout=2)
|
|
455
|
+
brain_reachable = stats is not None
|
|
456
|
+
if stats and stats.get("total", 0) > 0:
|
|
457
|
+
has_index = True
|
|
458
|
+
|
|
459
|
+
# If brain is unreachable, we cannot determine onboarding state — fail open.
|
|
460
|
+
# Brain unavailability is surfaced separately by _check_brain_dependency.
|
|
461
|
+
if not brain_reachable:
|
|
462
|
+
return False, True, None
|
|
463
|
+
|
|
464
|
+
# Check for any stored memories (broad: any brain result, not just /mem- paths).
|
|
465
|
+
# If brain has an index with content (has_index=True), that alone is sufficient
|
|
466
|
+
# evidence that the project has been set up — skip the memory search.
|
|
467
|
+
if has_index:
|
|
468
|
+
has_memories = True # indexed brain content ≡ project was onboarded
|
|
469
|
+
else:
|
|
470
|
+
result = _brain_api("search", {"query": "project memory onboarding", "limit": 5}, timeout=3)
|
|
471
|
+
if result is None:
|
|
472
|
+
# Brain went unreachable between calls — fail open.
|
|
473
|
+
return False, True, None
|
|
474
|
+
if isinstance(result, list) and result:
|
|
475
|
+
has_memories = True
|
|
476
|
+
elif isinstance(result, dict) and result.get("results"):
|
|
477
|
+
has_memories = True
|
|
478
|
+
|
|
479
|
+
directive = None
|
|
480
|
+
if not has_index and not has_memories:
|
|
481
|
+
# Neither index nor memories → first-time setup required
|
|
482
|
+
directive = (
|
|
483
|
+
f"[Action Required] Project '{project}' has not been onboarded.\n"
|
|
484
|
+
"You MUST immediately invoke the Skill tool with skill='wicked-garden:setup' "
|
|
485
|
+
"to launch the interactive onboarding wizard.\n"
|
|
486
|
+
"Do NOT ask the user for confirmation — invoke the skill now.\n"
|
|
487
|
+
"Do NOT respond with text first — invoke the skill as your first action."
|
|
488
|
+
)
|
|
489
|
+
elif not has_index and has_memories:
|
|
490
|
+
# Memories exist but brain index is gone → rebuild only
|
|
491
|
+
directive = (
|
|
492
|
+
f"[Onboarding] IMPORTANT: Project '{project}' has memories but no search index. "
|
|
493
|
+
"You MUST run /wicked-garden:search:index now to build the search index."
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return has_index, has_memories, directive
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _read_config():
|
|
500
|
+
"""Read config from ~/.something-wicked/wicked-garden/config.json."""
|
|
501
|
+
config_path = Path.home() / ".something-wicked" / "wicked-garden" / "config.json"
|
|
502
|
+
if not config_path.exists():
|
|
503
|
+
return None
|
|
504
|
+
try:
|
|
505
|
+
return json.loads(config_path.read_text())
|
|
506
|
+
except (json.JSONDecodeError, OSError):
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# v9.2.11 deleted _MEMORY_INSTRUCTIONS. v9.2.10 stopped APPENDING it to the
|
|
511
|
+
# briefing (CLAUDE.md "Memory Management" section already overrides the
|
|
512
|
+
# system-level auto-memory instructions); v9.2.11 confirmed zero imports
|
|
513
|
+
# across hooks/, scripts/, tests/, commands/ and removed the dead constant.
|
|
514
|
+
# Same cleanup pattern as the v9.2.10 yolo test deletion — a constant kept
|
|
515
|
+
# alive only by a test that asserts it exists is a tautology.
|
|
516
|
+
|
|
517
|
+
_DANGEROUS_MODE_WARNING = (
|
|
518
|
+
"[Question Mode] Dangerous mode is active (skipDangerousModePermissionPrompt). "
|
|
519
|
+
"AskUserQuestion is BROKEN in this mode — it auto-completes with empty answers. "
|
|
520
|
+
"You MUST use plain text questions instead: present numbered options, "
|
|
521
|
+
"then STOP and wait for the user to reply before proceeding."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# Dangerous mode detection
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
def _detect_dangerous_mode():
|
|
530
|
+
"""Check if Claude Code is running in dangerous mode.
|
|
531
|
+
|
|
532
|
+
Dangerous mode (skipDangerousModePermissionPrompt=true) breaks
|
|
533
|
+
AskUserQuestion by auto-completing it with empty answers.
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
settings_path = Path.home() / ".claude" / "settings.json"
|
|
537
|
+
if settings_path.exists():
|
|
538
|
+
settings = json.loads(settings_path.read_text())
|
|
539
|
+
return bool(settings.get("skipDangerousModePermissionPrompt"))
|
|
540
|
+
except Exception:
|
|
541
|
+
pass
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
# wicked-brain dependency check
|
|
548
|
+
# ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
def _check_brain_dependency():
|
|
551
|
+
"""Check whether the wicked-brain plugin is installed.
|
|
552
|
+
|
|
553
|
+
Scans ~/.claude/settings.json and .claude/settings.json (project-level)
|
|
554
|
+
for 'wicked-brain' in their enabledPlugins keys.
|
|
555
|
+
|
|
556
|
+
If found:
|
|
557
|
+
- Returns (True, None) — no briefing note needed.
|
|
558
|
+
- Also attempts a lightweight server health probe on the default port
|
|
559
|
+
and returns (True, note) where note describes the offline state if
|
|
560
|
+
the server is not reachable.
|
|
561
|
+
|
|
562
|
+
If NOT found:
|
|
563
|
+
- Returns (False, note) — note contains the install instruction.
|
|
564
|
+
|
|
565
|
+
Always fails open — any exception returns (None, None) so the session
|
|
566
|
+
is never blocked.
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
# Settings file locations to check (global first, then project-local)
|
|
570
|
+
settings_candidates = [
|
|
571
|
+
Path.home() / ".claude" / "settings.json",
|
|
572
|
+
Path(".claude") / "settings.json",
|
|
573
|
+
]
|
|
574
|
+
|
|
575
|
+
# Also honour CLAUDE_CONFIG_DIR if set
|
|
576
|
+
config_dir_env = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
577
|
+
if config_dir_env:
|
|
578
|
+
settings_candidates.append(Path(config_dir_env) / "settings.json")
|
|
579
|
+
|
|
580
|
+
brain_installed = False
|
|
581
|
+
|
|
582
|
+
# Check 1: enabledPlugins in settings.json (plugin-style install)
|
|
583
|
+
for settings_path in settings_candidates:
|
|
584
|
+
try:
|
|
585
|
+
if not settings_path.exists():
|
|
586
|
+
continue
|
|
587
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
588
|
+
enabled = settings.get("enabledPlugins", {})
|
|
589
|
+
if isinstance(enabled, dict) and "wicked-brain" in enabled:
|
|
590
|
+
brain_installed = True
|
|
591
|
+
break
|
|
592
|
+
if isinstance(enabled, list) and "wicked-brain" in enabled:
|
|
593
|
+
brain_installed = True
|
|
594
|
+
break
|
|
595
|
+
except (json.JSONDecodeError, OSError):
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
# Check 2: loose skill installs at ~/.claude/skills/wicked-brain-*
|
|
599
|
+
# Users can install wicked-brain skills directly without the plugin wrapper.
|
|
600
|
+
if not brain_installed:
|
|
601
|
+
try:
|
|
602
|
+
skills_dir = Path.home() / ".claude" / "skills"
|
|
603
|
+
if skills_dir.exists():
|
|
604
|
+
for entry in skills_dir.iterdir():
|
|
605
|
+
if entry.is_dir() and entry.name.startswith("wicked-brain"):
|
|
606
|
+
brain_installed = True
|
|
607
|
+
break
|
|
608
|
+
except OSError:
|
|
609
|
+
pass # fail open
|
|
610
|
+
|
|
611
|
+
# Check 3: CLAUDE_CONFIG_DIR skills directory
|
|
612
|
+
if not brain_installed and config_dir_env:
|
|
613
|
+
try:
|
|
614
|
+
alt_skills_dir = Path(config_dir_env) / "skills"
|
|
615
|
+
if alt_skills_dir.exists():
|
|
616
|
+
for entry in alt_skills_dir.iterdir():
|
|
617
|
+
if entry.is_dir() and entry.name.startswith("wicked-brain"):
|
|
618
|
+
brain_installed = True
|
|
619
|
+
break
|
|
620
|
+
except OSError:
|
|
621
|
+
pass # fail open
|
|
622
|
+
|
|
623
|
+
if not brain_installed:
|
|
624
|
+
note = (
|
|
625
|
+
"[wicked-brain] optional layer not installed.\n"
|
|
626
|
+
"Add it for cross-session memory + cited search: claude plugin install wicked-brain --scope project\n"
|
|
627
|
+
"The toolkit works without it — you lose the memory/context layer "
|
|
628
|
+
"(cross-session recall, brain-backed search, smaht:briefing)."
|
|
629
|
+
)
|
|
630
|
+
return False, note
|
|
631
|
+
|
|
632
|
+
# wicked-brain is installed — probe server health and auto-start on
|
|
633
|
+
# miss. _brain_api returns None on any error (server not running,
|
|
634
|
+
# connection refused, timeout, parse error); ensure_server is the
|
|
635
|
+
# deterministic fix for the "not running" case and fails open.
|
|
636
|
+
server_note = None
|
|
637
|
+
if _brain_api("health", {}, timeout=1) is None:
|
|
638
|
+
started = False
|
|
639
|
+
try:
|
|
640
|
+
from _brain_port import ensure_server
|
|
641
|
+
started = ensure_server()
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
if started:
|
|
645
|
+
server_note = "[wicked-brain] Server was down — auto-started for this session."
|
|
646
|
+
else:
|
|
647
|
+
server_note = (
|
|
648
|
+
"[wicked-brain] Plugin installed but the server could not be "
|
|
649
|
+
"auto-started. Diagnose with `wicked-brain-call --start` "
|
|
650
|
+
"(log: {brain}/_meta/server.log). Brain skills auto-start the "
|
|
651
|
+
"server on every call — do NOT skip brain usage."
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return True, server_note
|
|
655
|
+
|
|
656
|
+
except Exception:
|
|
657
|
+
return None, None # Fail open — never block session start
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
# ---------------------------------------------------------------------------
|
|
661
|
+
# wicked-vault dependency check (required evidence backend)
|
|
662
|
+
# ---------------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
def _check_vault_dependency():
|
|
665
|
+
"""Return a briefing note if wicked-vault is not resolvable, else None.
|
|
666
|
+
|
|
667
|
+
wicked-vault is a required peer (sibling to wicked-bus / wicked-brain /
|
|
668
|
+
wicked-testing): every archetype produces-gate re-derives against it via
|
|
669
|
+
``scripts/qe/vault_gate.py``. When it is absent those gates fail closed,
|
|
670
|
+
so we surface a one-line install pointer at SessionStart.
|
|
671
|
+
|
|
672
|
+
Fast + stdlib-only (no subprocess to npx): checks PATH and loose skill
|
|
673
|
+
installs only. Always fails open — never blocks the session.
|
|
674
|
+
"""
|
|
675
|
+
try:
|
|
676
|
+
import shutil
|
|
677
|
+
|
|
678
|
+
# WICKED_VAULT_BIN explicitly set (even empty kill-switch) → operator
|
|
679
|
+
# is driving resolution deliberately; don't nag.
|
|
680
|
+
if "WICKED_VAULT_BIN" in os.environ:
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
if shutil.which("wicked-vault"):
|
|
684
|
+
return None
|
|
685
|
+
|
|
686
|
+
# Loose skill installs land as skills/wicked-vault-*/ under each CLI
|
|
687
|
+
# config root (npx wicked-vault-install). PATH miss + skills present
|
|
688
|
+
# still means the CLI runs via npx, so treat skills as "installed".
|
|
689
|
+
skill_roots = [Path.home() / ".claude" / "skills"]
|
|
690
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
691
|
+
if cfg:
|
|
692
|
+
skill_roots.append(Path(cfg) / "skills")
|
|
693
|
+
for root in skill_roots:
|
|
694
|
+
try:
|
|
695
|
+
if root.exists():
|
|
696
|
+
for entry in root.iterdir():
|
|
697
|
+
if entry.is_dir() and entry.name.startswith("wicked-vault"):
|
|
698
|
+
return None
|
|
699
|
+
except OSError:
|
|
700
|
+
continue # fail open
|
|
701
|
+
|
|
702
|
+
return (
|
|
703
|
+
"[wicked-vault] REQUIRED but not installed.\n"
|
|
704
|
+
"Install now: npx wicked-vault-install (or: npm i -g wicked-vault)\n"
|
|
705
|
+
"wicked-vault is the evidence backend wicked-loom re-runs every "
|
|
706
|
+
"gate's verifier against — without it, produces-gates fail closed."
|
|
707
|
+
)
|
|
708
|
+
except Exception:
|
|
709
|
+
return None # Fail open — never block session start
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _check_loom_dependency():
|
|
713
|
+
"""Return a briefing note if loom peer-resolution is unavailable, else None.
|
|
714
|
+
|
|
715
|
+
After the Phase B absorption (ECOSYSTEM-RATIONALIZATION.md §5a), the
|
|
716
|
+
wicked-loom peer-resolution surface (resolve/doctor/compose/gate) is
|
|
717
|
+
internal to wicked-garden — absorbed into scripts/loom/. External
|
|
718
|
+
installation of the standalone wicked-loom npm package is no longer
|
|
719
|
+
required.
|
|
720
|
+
|
|
721
|
+
This function returns None (no warning) when the absorbed internal module
|
|
722
|
+
is present, which is always the case in a properly installed wicked-garden.
|
|
723
|
+
The legacy external-binary checks are retained as a fallback for
|
|
724
|
+
environments that may be running a partial or development install.
|
|
725
|
+
|
|
726
|
+
Always fails open — never blocks the session start.
|
|
727
|
+
"""
|
|
728
|
+
try:
|
|
729
|
+
# Primary check: internal absorbed module (scripts/loom/).
|
|
730
|
+
# In a properly installed wicked-garden this always resolves.
|
|
731
|
+
_scripts_dir = Path(__file__).resolve().parents[2] / "scripts"
|
|
732
|
+
if (_scripts_dir / "loom" / "__init__.py").exists():
|
|
733
|
+
return None # Absorbed internally — no external install needed.
|
|
734
|
+
|
|
735
|
+
# Kill-switch / operator override: don't nag when deliberately managed.
|
|
736
|
+
if "WICKED_LOOM_BIN" in os.environ:
|
|
737
|
+
return None
|
|
738
|
+
if os.environ.get("WICKED_LOOM_CUTOVER", "").strip().lower() == "off":
|
|
739
|
+
return None
|
|
740
|
+
|
|
741
|
+
# Legacy external-binary check (development / partial installs only).
|
|
742
|
+
if shutil.which("wicked-loom"):
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
skill_roots = [Path.home() / ".claude" / "skills"]
|
|
746
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
747
|
+
if cfg:
|
|
748
|
+
skill_roots.append(Path(cfg) / "skills")
|
|
749
|
+
for root in skill_roots:
|
|
750
|
+
try:
|
|
751
|
+
if root.exists():
|
|
752
|
+
for entry in root.iterdir():
|
|
753
|
+
if entry.is_dir() and entry.name.startswith("wicked-loom"):
|
|
754
|
+
return None
|
|
755
|
+
except OSError:
|
|
756
|
+
continue # fail open
|
|
757
|
+
|
|
758
|
+
# No loom surface found — surface a note (never a block).
|
|
759
|
+
return (
|
|
760
|
+
"[loom] Peer-resolution runtime not found.\n"
|
|
761
|
+
"wicked-loom is now absorbed into wicked-garden — reinstall "
|
|
762
|
+
"wicked-garden to restore the peer-resolution surface.\n"
|
|
763
|
+
"Without it the produces-gate fails closed."
|
|
764
|
+
)
|
|
765
|
+
except Exception:
|
|
766
|
+
return None # Fail open — never block session start
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
# ---------------------------------------------------------------------------
|
|
770
|
+
# wicked-bus dependency check (required event backbone)
|
|
771
|
+
# ---------------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
def _check_bus_dependency():
|
|
774
|
+
"""Return a briefing note if wicked-bus is not installed, else None.
|
|
775
|
+
|
|
776
|
+
wicked-bus is a required peer (sibling to wicked-brain / wicked-vault /
|
|
777
|
+
wicked-testing): the garden's archetype events flow through it. When it
|
|
778
|
+
is absent, cross-plugin event wiring is silently dropped, so we surface a
|
|
779
|
+
one-line install pointer at SessionStart.
|
|
780
|
+
|
|
781
|
+
Detection mirrors ``_check_brain_dependency`` — wicked-bus installs as a
|
|
782
|
+
Claude Code *plugin*, not an npx CLI, so we verify by presence:
|
|
783
|
+
1. ``enabledPlugins`` in ~/.claude/settings.json, project-local
|
|
784
|
+
.claude/settings.json, and CLAUDE_CONFIG_DIR/settings.json.
|
|
785
|
+
2. Loose skill installs at ``skills/wicked-bus-*`` under the home and
|
|
786
|
+
CLAUDE_CONFIG_DIR skills directories.
|
|
787
|
+
|
|
788
|
+
Fast + stdlib-only (no subprocess). Always fails open — never blocks the
|
|
789
|
+
session.
|
|
790
|
+
"""
|
|
791
|
+
try:
|
|
792
|
+
# Settings file locations to check (global first, then project-local)
|
|
793
|
+
settings_candidates = [
|
|
794
|
+
Path.home() / ".claude" / "settings.json",
|
|
795
|
+
Path(".claude") / "settings.json",
|
|
796
|
+
]
|
|
797
|
+
|
|
798
|
+
# Also honour CLAUDE_CONFIG_DIR if set
|
|
799
|
+
config_dir_env = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
800
|
+
if config_dir_env:
|
|
801
|
+
settings_candidates.append(Path(config_dir_env) / "settings.json")
|
|
802
|
+
|
|
803
|
+
bus_installed = False
|
|
804
|
+
|
|
805
|
+
# Check 1: enabledPlugins in settings.json (plugin-style install)
|
|
806
|
+
for settings_path in settings_candidates:
|
|
807
|
+
try:
|
|
808
|
+
if not settings_path.exists():
|
|
809
|
+
continue
|
|
810
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
811
|
+
enabled = settings.get("enabledPlugins", {})
|
|
812
|
+
if isinstance(enabled, dict) and "wicked-bus" in enabled:
|
|
813
|
+
bus_installed = True
|
|
814
|
+
break
|
|
815
|
+
if isinstance(enabled, list) and "wicked-bus" in enabled:
|
|
816
|
+
bus_installed = True
|
|
817
|
+
break
|
|
818
|
+
except (json.JSONDecodeError, OSError):
|
|
819
|
+
continue
|
|
820
|
+
|
|
821
|
+
# Check 2: loose skill installs at skills/wicked-bus-* under the home
|
|
822
|
+
# and CLAUDE_CONFIG_DIR skills directories.
|
|
823
|
+
if not bus_installed:
|
|
824
|
+
skill_roots = [Path.home() / ".claude" / "skills"]
|
|
825
|
+
if config_dir_env:
|
|
826
|
+
skill_roots.append(Path(config_dir_env) / "skills")
|
|
827
|
+
for root in skill_roots:
|
|
828
|
+
try:
|
|
829
|
+
if root.exists():
|
|
830
|
+
for entry in root.iterdir():
|
|
831
|
+
if entry.is_dir() and entry.name.startswith("wicked-bus"):
|
|
832
|
+
bus_installed = True
|
|
833
|
+
break
|
|
834
|
+
except OSError:
|
|
835
|
+
continue # fail open
|
|
836
|
+
if bus_installed:
|
|
837
|
+
break
|
|
838
|
+
|
|
839
|
+
if not bus_installed:
|
|
840
|
+
return (
|
|
841
|
+
"[wicked-bus] optional layer not installed.\n"
|
|
842
|
+
"Add it for the cross-session audit trail: /plugin install wicked-bus\n"
|
|
843
|
+
"The toolkit works without it — events are simply not recorded "
|
|
844
|
+
"(emission is already fire-and-forget / fail-open)."
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
return None
|
|
848
|
+
except Exception:
|
|
849
|
+
return None # Fail open — never block session start
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
# ---------------------------------------------------------------------------
|
|
853
|
+
# Discovery: contextual command suggestions based on project type (Issue #322)
|
|
854
|
+
# ---------------------------------------------------------------------------
|
|
855
|
+
|
|
856
|
+
def _check_facilitator_context(project_name: str | None) -> str | None:
|
|
857
|
+
"""Return a briefing line for unresolved action items aged >= 2 sessions.
|
|
858
|
+
|
|
859
|
+
Lazily imports ``scripts.delivery.process_memory`` so environments that
|
|
860
|
+
do not ship that module (pre-#447 plugin installs) continue to boot.
|
|
861
|
+
Returns a short ``[ProcessMemory]`` line when the project has any
|
|
862
|
+
aging AIs, or ``None`` when:
|
|
863
|
+
|
|
864
|
+
- No active project was detected (nothing to surface for).
|
|
865
|
+
- ``delivery.process_memory`` is not importable (fail-open).
|
|
866
|
+
- The project has no unresolved action items at the aging threshold.
|
|
867
|
+
- Any exception is raised while reading the memory file.
|
|
868
|
+
|
|
869
|
+
Matches the fail-open pattern used by ``_run_quality_telemetry`` and
|
|
870
|
+
the guard pipeline in ``stop.py``.
|
|
871
|
+
"""
|
|
872
|
+
if not project_name:
|
|
873
|
+
return None
|
|
874
|
+
try:
|
|
875
|
+
# Lazy import — absence must NOT break bootstrap on legacy installs.
|
|
876
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts" / "delivery"))
|
|
877
|
+
try:
|
|
878
|
+
import process_memory as _pm # type: ignore
|
|
879
|
+
except ImportError as exc:
|
|
880
|
+
print(
|
|
881
|
+
f"[wicked-garden] process_memory not importable ({exc!r}); "
|
|
882
|
+
"skipping facilitator context",
|
|
883
|
+
file=sys.stderr,
|
|
884
|
+
)
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
ctx = _pm.facilitator_context(project_name)
|
|
888
|
+
aging = ctx.get("aging_action_items", []) if isinstance(ctx, dict) else []
|
|
889
|
+
if not aging:
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
# Show up to 3 AIs inline so the line stays readable; rest are
|
|
893
|
+
# countable via the summary prefix and fully visible via
|
|
894
|
+
# process-memory.md on disk.
|
|
895
|
+
shown = aging[:3]
|
|
896
|
+
preview = ", ".join(
|
|
897
|
+
f"{ai.get('id', '?')} \"{(ai.get('title') or '')[:60]}\""
|
|
898
|
+
for ai in shown
|
|
899
|
+
)
|
|
900
|
+
more = f" (+{len(aging) - len(shown)} more)" if len(aging) > len(shown) else ""
|
|
901
|
+
return (
|
|
902
|
+
f"[ProcessMemory] {len(aging)} unresolved action item(s) "
|
|
903
|
+
f">=2 sessions old: {preview}{more}"
|
|
904
|
+
)
|
|
905
|
+
except Exception as exc:
|
|
906
|
+
# Fail-open: never block session start on process-memory errors.
|
|
907
|
+
print(
|
|
908
|
+
f"[wicked-garden] facilitator_context error: {exc!r}",
|
|
909
|
+
file=sys.stderr,
|
|
910
|
+
)
|
|
911
|
+
return None
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _probe_onedrive_path() -> str | None:
|
|
915
|
+
"""Detect if cwd is under a OneDrive or CloudStorage path with spaces.
|
|
916
|
+
|
|
917
|
+
On macOS, OneDrive-synced directories live under paths like:
|
|
918
|
+
~/Library/CloudStorage/OneDrive - CompanyName/...
|
|
919
|
+
The spaces cause failures when paths are not properly quoted.
|
|
920
|
+
|
|
921
|
+
Returns the resolved canonical base path (via Path.resolve()), or None
|
|
922
|
+
if cwd is not under a OneDrive/CloudStorage directory.
|
|
923
|
+
Fails open — any error returns None.
|
|
924
|
+
"""
|
|
925
|
+
try:
|
|
926
|
+
cwd = Path.cwd()
|
|
927
|
+
cwd_str = str(cwd)
|
|
928
|
+
if "OneDrive" in cwd_str or "CloudStorage" in cwd_str:
|
|
929
|
+
resolved = cwd.resolve()
|
|
930
|
+
return str(resolved)
|
|
931
|
+
except Exception:
|
|
932
|
+
pass
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
# Critical v6 skills — if these files exist on disk under the plugin root
|
|
937
|
+
# but a Skill() call fails with "Unknown skill", the plugin cache is stale
|
|
938
|
+
# and needs `/reload-plugins`. Issue #434.
|
|
939
|
+
_CRITICAL_V11_SKILLS = (
|
|
940
|
+
"skills/archetype/SKILL.md",
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
# v9.2.10 — first-session-per-project gating for informational briefing notices.
|
|
945
|
+
# Both `[Setup]` and `[Quick Start]` are useful exactly once per project; on
|
|
946
|
+
# every subsequent session they are pure noise. The sentinel file lives under
|
|
947
|
+
# the wicked-garden local store (per-project automatically because
|
|
948
|
+
# `get_local_path` resolves under the active project's slug), so a fresh
|
|
949
|
+
# project sees the notices on its first session and never again.
|
|
950
|
+
|
|
951
|
+
_BOOTSTRAP_NOTICE_FILE = "shown.json"
|
|
952
|
+
_NOTICE_SETUP = "setup_reconfigure"
|
|
953
|
+
_NOTICE_QUICK_START = "quick_start"
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _notice_path() -> "Path | None":
|
|
957
|
+
"""Resolve the per-project sentinel file path. None on resolution failure."""
|
|
958
|
+
try:
|
|
959
|
+
# Lazy import — keeps bootstrap fail-open if scripts/ is unreachable.
|
|
960
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
|
|
961
|
+
from _domain_store import get_local_path # type: ignore
|
|
962
|
+
return get_local_path("wicked-garden", "bootstrap-notices") / _BOOTSTRAP_NOTICE_FILE
|
|
963
|
+
except Exception:
|
|
964
|
+
return None
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _notice_already_shown(notice_id: str) -> bool:
|
|
968
|
+
"""Return True if `notice_id` has been emitted for this project before."""
|
|
969
|
+
p = _notice_path()
|
|
970
|
+
if p is None or not p.exists():
|
|
971
|
+
return False
|
|
972
|
+
try:
|
|
973
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
974
|
+
return notice_id in data
|
|
975
|
+
except (json.JSONDecodeError, OSError):
|
|
976
|
+
return False
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _record_notice_shown(notice_id: str) -> None:
|
|
980
|
+
"""Record `notice_id` as shown. Best-effort; failures don't block bootstrap."""
|
|
981
|
+
p = _notice_path()
|
|
982
|
+
if p is None:
|
|
983
|
+
return
|
|
984
|
+
try:
|
|
985
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
986
|
+
data = {}
|
|
987
|
+
if p.exists():
|
|
988
|
+
try:
|
|
989
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
990
|
+
except (json.JSONDecodeError, OSError):
|
|
991
|
+
data = {}
|
|
992
|
+
data[notice_id] = {"shown_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
|
|
993
|
+
p.write_text(json.dumps(data), encoding="utf-8")
|
|
994
|
+
except OSError:
|
|
995
|
+
pass # fail open
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _check_critical_skills() -> str | None:
|
|
999
|
+
"""Verify critical v6 skill files are present in the plugin root.
|
|
1000
|
+
|
|
1001
|
+
Claude Code's skill registry is cached at plugin-install time. When the
|
|
1002
|
+
plugin ships a new skill mid-version, the cache lags until the user runs
|
|
1003
|
+
`/reload-plugins`. We can't query the live registry from a hook — but we
|
|
1004
|
+
can check the files exist on disk and emit a diagnostic directive so an
|
|
1005
|
+
"Unknown skill" error surfaces a fix path instead of being cryptic.
|
|
1006
|
+
|
|
1007
|
+
Returns a directive string ONLY when one or more critical skills are
|
|
1008
|
+
missing from disk (a real install problem). The previous "happy path"
|
|
1009
|
+
message — "skills are present; if Skill() fails, run /reload-plugins" —
|
|
1010
|
+
fired every session even with a clean install and was chronic noise. A
|
|
1011
|
+
user who has hit a cache-stale failure once knows the fix; emitting it
|
|
1012
|
+
on every healthy session adds no signal. v9.2.10 dropped that branch.
|
|
1013
|
+
|
|
1014
|
+
Fails open on errors.
|
|
1015
|
+
"""
|
|
1016
|
+
try:
|
|
1017
|
+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
|
|
1018
|
+
if not plugin_root:
|
|
1019
|
+
return None
|
|
1020
|
+
root = Path(plugin_root)
|
|
1021
|
+
missing = [s for s in _CRITICAL_V11_SKILLS if not (root / s).exists()]
|
|
1022
|
+
if not missing:
|
|
1023
|
+
return None # healthy install — nothing to say
|
|
1024
|
+
return (
|
|
1025
|
+
"[Skills] Critical v6 skill files are missing from the plugin root:\n"
|
|
1026
|
+
+ "\n".join(f" - {s}" for s in missing)
|
|
1027
|
+
+ "\nThe plugin install is incomplete. Reinstall with "
|
|
1028
|
+
"`claude plugin install wicked-garden --scope project`."
|
|
1029
|
+
)
|
|
1030
|
+
except Exception:
|
|
1031
|
+
return None # fail open — never block bootstrap
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _suggest_commands_for_project() -> str | None:
|
|
1035
|
+
"""Detect project type from files in cwd and suggest 2-3 relevant commands.
|
|
1036
|
+
|
|
1037
|
+
Lightweight: checks only for a few marker files (no directory walks).
|
|
1038
|
+
Returns a formatted suggestion string, or None if no suggestions apply.
|
|
1039
|
+
"""
|
|
1040
|
+
try:
|
|
1041
|
+
cwd = Path.cwd()
|
|
1042
|
+
suggestions = []
|
|
1043
|
+
|
|
1044
|
+
# Detect project signals from marker files
|
|
1045
|
+
has_package_json = (cwd / "package.json").exists()
|
|
1046
|
+
has_pyproject = (cwd / "pyproject.toml").exists() or (cwd / "setup.py").exists()
|
|
1047
|
+
has_dockerfile = (cwd / "Dockerfile").exists() or (cwd / "docker-compose.yml").exists()
|
|
1048
|
+
has_terraform = (cwd / "main.tf").exists() or (cwd / "terraform").is_dir()
|
|
1049
|
+
has_tests = (
|
|
1050
|
+
(cwd / "tests").is_dir() or (cwd / "test").is_dir()
|
|
1051
|
+
or (cwd / "__tests__").is_dir() or (cwd / "spec").is_dir()
|
|
1052
|
+
)
|
|
1053
|
+
has_csv_data = any(cwd.glob("*.csv")) or (cwd / "data").is_dir()
|
|
1054
|
+
has_ci = (cwd / ".github" / "workflows").is_dir()
|
|
1055
|
+
|
|
1056
|
+
# Always useful
|
|
1057
|
+
suggestions.append("`wicked-brain:search` — semantic code search")
|
|
1058
|
+
suggestions.append("`/wicked-garden:engineering:review` — structured code review")
|
|
1059
|
+
|
|
1060
|
+
# Project-type-specific
|
|
1061
|
+
if has_tests:
|
|
1062
|
+
suggestions.append("`/wicked-testing:plan` — generate test plan and scenarios")
|
|
1063
|
+
if has_dockerfile or has_terraform:
|
|
1064
|
+
suggestions.append("`/wicked-garden:platform:security` — security review")
|
|
1065
|
+
if has_csv_data:
|
|
1066
|
+
suggestions.append("`/wicked-garden:data:analyze` — interactive data analysis with DuckDB")
|
|
1067
|
+
if has_ci:
|
|
1068
|
+
suggestions.append("`/wicked-garden:platform:actions` — GitHub Actions optimization")
|
|
1069
|
+
|
|
1070
|
+
# Cap at 3 suggestions
|
|
1071
|
+
suggestions = suggestions[:3]
|
|
1072
|
+
|
|
1073
|
+
if not suggestions:
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
return (
|
|
1077
|
+
"[Quick Start] Available commands for this project:\n"
|
|
1078
|
+
+ "\n".join(f" - {s}" for s in suggestions)
|
|
1079
|
+
)
|
|
1080
|
+
except Exception:
|
|
1081
|
+
return None
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
# ---------------------------------------------------------------------------
|
|
1085
|
+
# #506 — WG_GATE_RESULT_STRICT_AFTER pre-flip monitoring
|
|
1086
|
+
#
|
|
1087
|
+
# Surfaces a visible warning as the strict-mode cutover approaches so
|
|
1088
|
+
# operators aren't surprised when orphan gate-results start rejecting.
|
|
1089
|
+
# Thresholds (T = days until flip, computed in UTC):
|
|
1090
|
+
# T > 7 silent
|
|
1091
|
+
# 7 >= T >= 1 stderr WARN, every SessionStart
|
|
1092
|
+
# T == 0 stderr INFO, one-time per session (latched via
|
|
1093
|
+
# SessionState.strict_mode_active_announced)
|
|
1094
|
+
# T < 0 stderr INFO, same one-time latch (post-flip banner)
|
|
1095
|
+
#
|
|
1096
|
+
# Fail-open: malformed dates fall back to the module default and the
|
|
1097
|
+
# signal is skipped silently rather than blocking bootstrap.
|
|
1098
|
+
# ---------------------------------------------------------------------------
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _check_pre_flip_notice(state, today=None) -> None:
|
|
1102
|
+
"""Emit pre-flip / post-flip monitoring signals on stderr.
|
|
1103
|
+
|
|
1104
|
+
Arguments:
|
|
1105
|
+
state: The loaded SessionState (or None). When None, the
|
|
1106
|
+
post-flip banner still fires but won't latch — next
|
|
1107
|
+
SessionStart in the same session may re-fire, which is
|
|
1108
|
+
the expected degrade.
|
|
1109
|
+
today: ``datetime.date`` override for deterministic tests.
|
|
1110
|
+
Production callers pass ``None`` and the helper uses
|
|
1111
|
+
today's UTC date.
|
|
1112
|
+
"""
|
|
1113
|
+
try:
|
|
1114
|
+
# Lazy import — keeps hook cheap; dispatch_log is in the crew
|
|
1115
|
+
# scripts dir which is already on sys.path via the hook bootstrap.
|
|
1116
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts" / "crew"))
|
|
1117
|
+
from dispatch_log import ( # type: ignore
|
|
1118
|
+
DEFAULT_STRICT_AFTER,
|
|
1119
|
+
_get_strict_after_date,
|
|
1120
|
+
)
|
|
1121
|
+
except Exception:
|
|
1122
|
+
return # fail-open: module unavailable, skip the signal
|
|
1123
|
+
|
|
1124
|
+
try:
|
|
1125
|
+
from datetime import datetime, timezone
|
|
1126
|
+
|
|
1127
|
+
flip_date = _get_strict_after_date()
|
|
1128
|
+
if today is None:
|
|
1129
|
+
today = datetime.now(timezone.utc).date()
|
|
1130
|
+
delta_days = (flip_date - today).days
|
|
1131
|
+
raw_env_val = os.environ.get("WG_GATE_RESULT_STRICT_AFTER", "").strip()
|
|
1132
|
+
# Display the user-supplied value when it parsed; otherwise show
|
|
1133
|
+
# the effective default. A malformed env-var produces the fallback
|
|
1134
|
+
# WARN in ``_get_strict_after_date`` already — we don't duplicate.
|
|
1135
|
+
if raw_env_val and flip_date.isoformat() == raw_env_val:
|
|
1136
|
+
raw_env = raw_env_val
|
|
1137
|
+
else:
|
|
1138
|
+
raw_env = flip_date.isoformat()
|
|
1139
|
+
|
|
1140
|
+
if delta_days > 7:
|
|
1141
|
+
return # silent
|
|
1142
|
+
|
|
1143
|
+
if 1 <= delta_days <= 7:
|
|
1144
|
+
sys.stderr.write(
|
|
1145
|
+
f"[PreFlipNotice] Strict-mode activation in {delta_days} "
|
|
1146
|
+
f"days (WG_GATE_RESULT_STRICT_AFTER={raw_env}). "
|
|
1147
|
+
"Orphan gate-result files will start rejecting.\n"
|
|
1148
|
+
)
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# delta_days <= 0 → post-flip; emit one-time-per-session INFO.
|
|
1152
|
+
already = bool(getattr(state, "strict_mode_active_announced", False)) \
|
|
1153
|
+
if state is not None else False
|
|
1154
|
+
if already:
|
|
1155
|
+
return
|
|
1156
|
+
sys.stderr.write(
|
|
1157
|
+
f"[StrictMode] Strict-mode active (flipped on "
|
|
1158
|
+
f"{flip_date.isoformat()}). Any orphan gate-result now rejects.\n"
|
|
1159
|
+
)
|
|
1160
|
+
if state is not None:
|
|
1161
|
+
try:
|
|
1162
|
+
state.update(strict_mode_active_announced=True)
|
|
1163
|
+
except Exception: # pragma: no cover — defensive
|
|
1164
|
+
pass # session-state write failure: banner may re-fire, acceptable
|
|
1165
|
+
except Exception:
|
|
1166
|
+
# Signal computation errors must never block bootstrap.
|
|
1167
|
+
return
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
# ---------------------------------------------------------------------------
|
|
1171
|
+
# #544 — wicked-testing peer-plugin probe (AC-1 through AC-6)
|
|
1172
|
+
# ---------------------------------------------------------------------------
|
|
1173
|
+
|
|
1174
|
+
# Module-level list of blocking notices to inject into session briefing.
|
|
1175
|
+
# Populated by _probe_wicked_testing(); consumed by main() before building
|
|
1176
|
+
# the briefing. At most one entry per session (the probe runs once).
|
|
1177
|
+
_wt_blocking_notices: list = []
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _probe_wicked_testing(state) -> None:
|
|
1181
|
+
"""Run the wicked-testing availability probe once per session.
|
|
1182
|
+
|
|
1183
|
+
Caches the result in SessionState.extras["wicked_testing_probe"] so
|
|
1184
|
+
crew_command_gate() can read it without re-running the subprocess.
|
|
1185
|
+
|
|
1186
|
+
Session cache semantics:
|
|
1187
|
+
- Probe runs exactly once at SessionStart (first call after fresh state).
|
|
1188
|
+
- SessionState is cleared on session reset (session_ended path above),
|
|
1189
|
+
so a new session always re-probes. No cross-session persistence.
|
|
1190
|
+
- Re-entrant guard: if the probe key is already present, return early.
|
|
1191
|
+
|
|
1192
|
+
Fail-open boundary: exceptions that escape this function are caught by
|
|
1193
|
+
the calling try/except in main() which returns {"continue": true}.
|
|
1194
|
+
This matches every other probe in bootstrap.py.
|
|
1195
|
+
|
|
1196
|
+
CH-02 hardening: the exception handler logs actionable detail, NOT just
|
|
1197
|
+
the bare exception string. crew_command_gate() closes the fail-open gap
|
|
1198
|
+
at the crew layer by treating an absent probe key as missing.
|
|
1199
|
+
"""
|
|
1200
|
+
try:
|
|
1201
|
+
# Re-entrant guard: probe runs at most once per session.
|
|
1202
|
+
extras = getattr(state, "extras", None) or {}
|
|
1203
|
+
if "wicked_testing_probe" in extras:
|
|
1204
|
+
return
|
|
1205
|
+
|
|
1206
|
+
# Import the probe from the scripts/ directory (already on sys.path
|
|
1207
|
+
# from the top of bootstrap.py).
|
|
1208
|
+
from _wicked_testing_probe import probe
|
|
1209
|
+
|
|
1210
|
+
result = probe(state)
|
|
1211
|
+
|
|
1212
|
+
missing = result["status"] != "ok"
|
|
1213
|
+
new_extras = {**extras, "wicked_testing_probe": result}
|
|
1214
|
+
|
|
1215
|
+
if state is not None:
|
|
1216
|
+
state.update(
|
|
1217
|
+
wicked_testing_missing=missing,
|
|
1218
|
+
extras=new_extras,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
# Emit a blocking notice into the session briefing (once per session).
|
|
1222
|
+
# crew_command_gate() only emits a pointer back, suppressing the full
|
|
1223
|
+
# notice on repeat invocations within the same session (AC-2).
|
|
1224
|
+
if result["status"] == "missing":
|
|
1225
|
+
_wt_blocking_notices.append(
|
|
1226
|
+
"[wicked-testing] wicked-testing is not installed.\n"
|
|
1227
|
+
"wicked-testing required — run: npx wicked-testing install\n"
|
|
1228
|
+
"Crew commands (start, execute, just-finish) are blocked until "
|
|
1229
|
+
"wicked-testing is installed."
|
|
1230
|
+
)
|
|
1231
|
+
elif result["status"] == "out-of-range":
|
|
1232
|
+
ver = result.get("version", "unknown")
|
|
1233
|
+
pin = result.get("pin", "unknown")
|
|
1234
|
+
_wt_blocking_notices.append(
|
|
1235
|
+
f"[wicked-testing] wicked-testing {ver} does not satisfy required range {pin}.\n"
|
|
1236
|
+
"wicked-testing required — run: npx wicked-testing install\n"
|
|
1237
|
+
"Crew commands (start, execute, just-finish) are blocked until "
|
|
1238
|
+
"the correct version is installed."
|
|
1239
|
+
)
|
|
1240
|
+
elif result["status"] == "error":
|
|
1241
|
+
err = result.get("error", "unknown error")
|
|
1242
|
+
_wt_blocking_notices.append(
|
|
1243
|
+
f"[wicked-testing] probe error: {err}\n"
|
|
1244
|
+
"Crew commands are blocked. If this is a transient error, "
|
|
1245
|
+
"restart the session. Use WG_SKIP_WICKED_TESTING_CHECK=1 for "
|
|
1246
|
+
"offline dev mode (CONTRIBUTING.md)."
|
|
1247
|
+
)
|
|
1248
|
+
# "ok" and "skipped" → silent (AC-6, AC-5)
|
|
1249
|
+
|
|
1250
|
+
except Exception as exc:
|
|
1251
|
+
# CH-02 hardening: log actionable context so operators can diagnose.
|
|
1252
|
+
# Include what failed and where (not just the bare exception string).
|
|
1253
|
+
print(
|
|
1254
|
+
f"[wicked-garden] wicked-testing probe failed: "
|
|
1255
|
+
f"exception={exc!r} "
|
|
1256
|
+
f"probe_module='_wicked_testing_probe' "
|
|
1257
|
+
f"action='npx wicked-testing --version' "
|
|
1258
|
+
f"note='crew_command_gate will treat absent probe key as missing'",
|
|
1259
|
+
file=sys.stderr,
|
|
1260
|
+
)
|
|
1261
|
+
# Fail-open at this boundary: do NOT block bootstrap.
|
|
1262
|
+
# crew_command_gate() is fail-closed for the absent-key case (CH-02).
|
|
1263
|
+
if state is not None:
|
|
1264
|
+
try:
|
|
1265
|
+
state.update(wicked_testing_missing=False)
|
|
1266
|
+
except Exception:
|
|
1267
|
+
pass # session-state write failure — acceptable
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
# ---------------------------------------------------------------------------
|
|
1271
|
+
# Registry allowlist validation
|
|
1272
|
+
#
|
|
1273
|
+
# Walks in-code allowlists (phase fallback agents, gate-policy reviewers,
|
|
1274
|
+
# bus event handlers, agent skill references) and confirms each referenced
|
|
1275
|
+
# asset exists on disk / in catalog. Catches the "invisible gap" failure
|
|
1276
|
+
# mode where probe traffic passes but the first request that hits the
|
|
1277
|
+
# missing asset blows up with a cryptic error.
|
|
1278
|
+
#
|
|
1279
|
+
# Fail-OPEN per the hook contract: any exception, any timeout, any malformed
|
|
1280
|
+
# allowlist surfaces as a session-briefing warning — never blocks bootstrap.
|
|
1281
|
+
# Findings categorised as `missing` / `malformed` / `invalid_id` are blocking
|
|
1282
|
+
# (CI-fail) but the bootstrap path only WARNs. External (drop-in plugin)
|
|
1283
|
+
# and skipped checks are advisory.
|
|
1284
|
+
# ---------------------------------------------------------------------------
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _run_registry_validation() -> str | None:
|
|
1288
|
+
"""Run the aggregated registry validator and return a briefing line.
|
|
1289
|
+
|
|
1290
|
+
Returns a multi-line string when findings exist, or None when the
|
|
1291
|
+
registry is fully clean (or the validator is unavailable). Caller
|
|
1292
|
+
is expected to append the result to `mode_notes` so it surfaces as
|
|
1293
|
+
a SessionStart [Registry] warning.
|
|
1294
|
+
|
|
1295
|
+
Fail-open boundary: any exception returns None. Validator timing
|
|
1296
|
+
is bounded by file IO (no subprocess, no network), so we trust it
|
|
1297
|
+
to complete inside the bootstrap budget.
|
|
1298
|
+
"""
|
|
1299
|
+
try:
|
|
1300
|
+
# _validate_registry lives in scripts/ which is already on sys.path
|
|
1301
|
+
# via the top-of-file insertion.
|
|
1302
|
+
from _validate_registry import run_all_checks, format_briefing
|
|
1303
|
+
|
|
1304
|
+
report = run_all_checks()
|
|
1305
|
+
return format_briefing(report)
|
|
1306
|
+
except Exception as exc:
|
|
1307
|
+
# Fail-open: validator import or run failure must NOT block bootstrap.
|
|
1308
|
+
# Log to stderr so operators can see why the [Registry] line is absent
|
|
1309
|
+
# but otherwise carry on.
|
|
1310
|
+
print(
|
|
1311
|
+
f"[wicked-garden] registry validation skipped: "
|
|
1312
|
+
f"{type(exc).__name__}: {exc}",
|
|
1313
|
+
file=sys.stderr,
|
|
1314
|
+
)
|
|
1315
|
+
return None
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
# ---------------------------------------------------------------------------
|
|
1319
|
+
# CH-02: SessionStart legacy reeval-log scan
|
|
1320
|
+
#
|
|
1321
|
+
# Scans known crew project directories for reeval-log.jsonl and amendments.jsonl
|
|
1322
|
+
# files containing legacy 'qe-evaluator' reviewer names or trigger prefixes.
|
|
1323
|
+
# Advisory only — does NOT block session startup.
|
|
1324
|
+
# If legacy entries are found, adds a structured migration notice to mode_notes.
|
|
1325
|
+
# ---------------------------------------------------------------------------
|
|
1326
|
+
|
|
1327
|
+
def _scan_for_legacy_reeval_entries() -> str | None:
|
|
1328
|
+
"""Scan crew project dirs for reeval-log.jsonl / amendments.jsonl with legacy qe-evaluator entries.
|
|
1329
|
+
|
|
1330
|
+
Light scan — greps for literal strings; does not parse full JSON.
|
|
1331
|
+
Fails open — any exception returns None so session is never blocked.
|
|
1332
|
+
|
|
1333
|
+
Returns a formatted migration notice string when legacy entries are found,
|
|
1334
|
+
or None when the tree is clean or unreadable.
|
|
1335
|
+
"""
|
|
1336
|
+
try:
|
|
1337
|
+
projects_root = Path.home() / ".something-wicked" / "wicked-garden" / "projects"
|
|
1338
|
+
if not projects_root.exists():
|
|
1339
|
+
return None
|
|
1340
|
+
|
|
1341
|
+
_LEGACY_STRINGS = ('"reviewer": "qe-evaluator"', '"trigger": "qe-evaluator:')
|
|
1342
|
+
dirty_files: list[str] = []
|
|
1343
|
+
|
|
1344
|
+
for log_file in projects_root.rglob("phases/*/reeval-log.jsonl"):
|
|
1345
|
+
try:
|
|
1346
|
+
text = log_file.read_text(encoding="utf-8", errors="replace")
|
|
1347
|
+
if any(s in text for s in _LEGACY_STRINGS):
|
|
1348
|
+
dirty_files.append(str(log_file))
|
|
1349
|
+
except OSError:
|
|
1350
|
+
continue
|
|
1351
|
+
|
|
1352
|
+
for log_file in projects_root.rglob("phases/*/amendments.jsonl"):
|
|
1353
|
+
try:
|
|
1354
|
+
text = log_file.read_text(encoding="utf-8", errors="replace")
|
|
1355
|
+
if any(s in text for s in _LEGACY_STRINGS):
|
|
1356
|
+
dirty_files.append(str(log_file))
|
|
1357
|
+
except OSError:
|
|
1358
|
+
continue
|
|
1359
|
+
|
|
1360
|
+
if not dirty_files:
|
|
1361
|
+
return None
|
|
1362
|
+
|
|
1363
|
+
file_list = "\n".join(f" - {f}" for f in dirty_files[:10])
|
|
1364
|
+
more = f"\n ... and {len(dirty_files) - 10} more" if len(dirty_files) > 10 else ""
|
|
1365
|
+
return (
|
|
1366
|
+
"[Migration Required] Legacy 'qe-evaluator' entries detected in reeval-log.jsonl "
|
|
1367
|
+
"or amendments.jsonl. These will cause errors when crew gates run.\n"
|
|
1368
|
+
f"Affected files:\n{file_list}{more}\n"
|
|
1369
|
+
"Run the migration script before starting a crew workflow:\n"
|
|
1370
|
+
" python scripts/crew/migrate_qe_evaluator_name.py --project-dir PATH/TO/PROJECT\n"
|
|
1371
|
+
"See docs/MIGRATION-v7.md for full guidance."
|
|
1372
|
+
)
|
|
1373
|
+
except Exception:
|
|
1374
|
+
return None # fail open — never block session start
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
# ---------------------------------------------------------------------------
|
|
1378
|
+
# Main
|
|
1379
|
+
# ---------------------------------------------------------------------------
|
|
1380
|
+
|
|
1381
|
+
def main():
|
|
1382
|
+
_t0 = time.monotonic()
|
|
1383
|
+
_log("bootstrap", "debug", "hook.start")
|
|
1384
|
+
|
|
1385
|
+
try:
|
|
1386
|
+
sys.stdin.read() # consume hook payload (not needed for SessionStart)
|
|
1387
|
+
except Exception:
|
|
1388
|
+
pass
|
|
1389
|
+
|
|
1390
|
+
# Export CLAUDE_PLUGIN_ROOT to CLAUDE_ENV_FILE so Bash tool calls can access it.
|
|
1391
|
+
# Hooks receive CLAUDE_PLUGIN_ROOT from the plugin runtime, but Bash tool calls
|
|
1392
|
+
# do not inherit it. Writing to CLAUDE_ENV_FILE bridges the gap.
|
|
1393
|
+
env_file = os.environ.get("CLAUDE_ENV_FILE")
|
|
1394
|
+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
|
|
1395
|
+
if env_file and plugin_root:
|
|
1396
|
+
try:
|
|
1397
|
+
with open(env_file, "a") as f:
|
|
1398
|
+
f.write(f'export CLAUDE_PLUGIN_ROOT="{plugin_root}"\n')
|
|
1399
|
+
except OSError:
|
|
1400
|
+
pass
|
|
1401
|
+
|
|
1402
|
+
try:
|
|
1403
|
+
# 1. Read config
|
|
1404
|
+
config = _read_config()
|
|
1405
|
+
|
|
1406
|
+
if config is None or not config.get("setup_complete", False):
|
|
1407
|
+
# Clear stale session state before early return. Without this,
|
|
1408
|
+
# setup_in_progress=True from a crashed/killed previous session
|
|
1409
|
+
# survives and lets prompt_submit bypass the setup gate.
|
|
1410
|
+
_stale = _load_session_state()
|
|
1411
|
+
if _stale is not None:
|
|
1412
|
+
if _stale.session_ended:
|
|
1413
|
+
_stale.delete()
|
|
1414
|
+
_stale = _load_session_state()
|
|
1415
|
+
if _stale is not None:
|
|
1416
|
+
_stale.update(
|
|
1417
|
+
setup_in_progress=False,
|
|
1418
|
+
setup_complete=False,
|
|
1419
|
+
setup_confirmed=False,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
print(json.dumps({
|
|
1423
|
+
"hookSpecificOutput": {
|
|
1424
|
+
"hookEventName": "SessionStart",
|
|
1425
|
+
"additionalContext": (
|
|
1426
|
+
"[Action Required] wicked-garden is not configured for this project.\n"
|
|
1427
|
+
"You MUST immediately invoke the Skill tool with skill='wicked-garden:setup' "
|
|
1428
|
+
"to launch the interactive setup wizard.\n"
|
|
1429
|
+
"Do NOT ask the user for confirmation — invoke the skill now.\n"
|
|
1430
|
+
"Do NOT respond with text first — invoke the skill as your first action."
|
|
1431
|
+
),
|
|
1432
|
+
},
|
|
1433
|
+
}))
|
|
1434
|
+
return
|
|
1435
|
+
|
|
1436
|
+
# Clear one-time task suggestion flag from crew
|
|
1437
|
+
flag = Path.home() / ".something-wicked" / "wicked-crew" / ".task_suggest_shown"
|
|
1438
|
+
flag.unlink(missing_ok=True)
|
|
1439
|
+
|
|
1440
|
+
# 2. Load session state — if stale from a previous session, start fresh.
|
|
1441
|
+
state = _load_session_state()
|
|
1442
|
+
if state is not None and state.session_ended:
|
|
1443
|
+
state.delete()
|
|
1444
|
+
state = _load_session_state() # fresh defaults
|
|
1445
|
+
if state is not None:
|
|
1446
|
+
state.update(
|
|
1447
|
+
setup_complete=True,
|
|
1448
|
+
turn_count=0,
|
|
1449
|
+
session_ended=False,
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
# 2a. Probe OneDrive / spaces-in-paths (Issue #321)
|
|
1453
|
+
onedrive_path = _probe_onedrive_path()
|
|
1454
|
+
if onedrive_path and state is not None:
|
|
1455
|
+
state.update(onedrive_base_path=onedrive_path)
|
|
1456
|
+
|
|
1457
|
+
_log("bootstrap", "normal", "storage.local", ok=True)
|
|
1458
|
+
|
|
1459
|
+
# 2b. Initialize event store schema (fire-and-forget)
|
|
1460
|
+
try:
|
|
1461
|
+
from _event_store import EventStore
|
|
1462
|
+
EventStore.ensure_schema()
|
|
1463
|
+
_log("bootstrap", "debug", "event_store.schema", ok=True)
|
|
1464
|
+
except Exception:
|
|
1465
|
+
pass # event store is optional — never block session start
|
|
1466
|
+
|
|
1467
|
+
# 2c. #506 — pre-flip monitoring signal for WG_GATE_RESULT_STRICT_AFTER.
|
|
1468
|
+
# Fail-open: never blocks bootstrap. Emits stderr WARN at T-7..T-1,
|
|
1469
|
+
# stderr INFO once per session post-flip.
|
|
1470
|
+
_check_pre_flip_notice(state)
|
|
1471
|
+
|
|
1472
|
+
# 2d. Probe wicked-testing availability (AC-1 through AC-6, #544).
|
|
1473
|
+
# Runs once per session; result cached in SessionState.extras.
|
|
1474
|
+
# Fail-open: any unhandled exception in _probe_wicked_testing() returns
|
|
1475
|
+
# gracefully so bootstrap continues. The crew layer (crew_command_gate)
|
|
1476
|
+
# is fail-closed — it treats an absent probe key as missing (CH-02).
|
|
1477
|
+
_probe_wicked_testing(state)
|
|
1478
|
+
|
|
1479
|
+
# 2e. CH-02 safeguard: scan for legacy qe-evaluator entries in reeval-log.jsonl
|
|
1480
|
+
# and amendments.jsonl. Advisory only — does NOT block session startup.
|
|
1481
|
+
# Emits a migration notice when legacy entries are found so the user is
|
|
1482
|
+
# informed at a clean session boundary (not mid-gate).
|
|
1483
|
+
_legacy_reeval_notice = _scan_for_legacy_reeval_entries()
|
|
1484
|
+
if _legacy_reeval_notice and state is not None:
|
|
1485
|
+
state.update(legacy_reeval_entries_detected=True)
|
|
1486
|
+
|
|
1487
|
+
# 3. Load dynamic agents
|
|
1488
|
+
agents_loaded, agents_dict = _load_agents()
|
|
1489
|
+
if state is not None:
|
|
1490
|
+
state.update(agents_loaded=agents_loaded)
|
|
1491
|
+
|
|
1492
|
+
# 3b. Resolve tool-capabilities for agents that declare them
|
|
1493
|
+
resolutions = _resolve_capabilities(agents_dict, config)
|
|
1494
|
+
if resolutions and state is not None:
|
|
1495
|
+
state.update(resolved_capabilities=resolutions)
|
|
1496
|
+
|
|
1497
|
+
# 4. Load last crew project for this workspace
|
|
1498
|
+
workspace = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
|
|
1499
|
+
project_data, project_name = _find_active_crew_project(workspace)
|
|
1500
|
+
|
|
1501
|
+
# 6. Run memory decay
|
|
1502
|
+
decay_summary = _run_memory_decay()
|
|
1503
|
+
|
|
1504
|
+
# 6b. Check search index staleness and auto-reindex if needed
|
|
1505
|
+
search_staleness_note = _check_search_staleness()
|
|
1506
|
+
mode_notes = []
|
|
1507
|
+
# Inject wicked-testing blocking notices first (high-priority, AC-2).
|
|
1508
|
+
# These were populated by _probe_wicked_testing() in step 2d above.
|
|
1509
|
+
for _wt_notice in _wt_blocking_notices:
|
|
1510
|
+
mode_notes.append(_wt_notice)
|
|
1511
|
+
# CH-02: inject legacy reeval-log migration notice when legacy entries are found.
|
|
1512
|
+
if _legacy_reeval_notice:
|
|
1513
|
+
mode_notes.append(_legacy_reeval_notice)
|
|
1514
|
+
# Required-peer check: wicked-vault (the evidence backend for gates).
|
|
1515
|
+
_vault_note = _check_vault_dependency()
|
|
1516
|
+
if _vault_note:
|
|
1517
|
+
mode_notes.append(_vault_note)
|
|
1518
|
+
# Required-peer check: wicked-bus (the event backbone for archetype events).
|
|
1519
|
+
_bus_note = _check_bus_dependency()
|
|
1520
|
+
if _bus_note:
|
|
1521
|
+
mode_notes.append(_bus_note)
|
|
1522
|
+
# Required-peer check: wicked-loom (the orchestration runtime garden drives).
|
|
1523
|
+
_loom_note = _check_loom_dependency()
|
|
1524
|
+
if _loom_note:
|
|
1525
|
+
mode_notes.append(_loom_note)
|
|
1526
|
+
if onedrive_path:
|
|
1527
|
+
mode_notes.append(
|
|
1528
|
+
f"[Path] OneDrive directory detected. Resolved base: {onedrive_path}. "
|
|
1529
|
+
"Always quote paths with spaces."
|
|
1530
|
+
)
|
|
1531
|
+
if search_staleness_note:
|
|
1532
|
+
mode_notes.append(f"[Search] {search_staleness_note}")
|
|
1533
|
+
|
|
1534
|
+
# 6c. Surface prior session's guard-pipeline findings (Issue #448)
|
|
1535
|
+
guard_note = _check_prior_guard_findings()
|
|
1536
|
+
if guard_note:
|
|
1537
|
+
mode_notes.append(guard_note)
|
|
1538
|
+
|
|
1539
|
+
# 7. Check onboarding status (search index + memories)
|
|
1540
|
+
has_index, has_memories, onboarding_directive = _check_onboarding_status()
|
|
1541
|
+
_log("bootstrap", "normal", "onboarding.status",
|
|
1542
|
+
ok=(has_memories and has_index),
|
|
1543
|
+
detail={"has_memories": has_memories, "has_index": has_index})
|
|
1544
|
+
|
|
1545
|
+
# Persist onboarding gate flags so prompt_submit can enforce them
|
|
1546
|
+
if state is not None:
|
|
1547
|
+
if not has_memories:
|
|
1548
|
+
state.update(
|
|
1549
|
+
needs_onboarding=True,
|
|
1550
|
+
onboarding_complete=False,
|
|
1551
|
+
)
|
|
1552
|
+
else:
|
|
1553
|
+
state.update(
|
|
1554
|
+
needs_onboarding=False,
|
|
1555
|
+
onboarding_complete=True,
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
# 7b. Probe installed plugin hooks for readiness
|
|
1559
|
+
unready_plugins = _probe_plugin_readiness()
|
|
1560
|
+
if unready_plugins and state is not None:
|
|
1561
|
+
state.update(unready_plugins=[p["name"] for p in unready_plugins])
|
|
1562
|
+
if unready_plugins:
|
|
1563
|
+
plugin_lines = []
|
|
1564
|
+
for p in unready_plugins:
|
|
1565
|
+
fix_cmd = _suggest_auth_fix(p)
|
|
1566
|
+
plugin_lines.append(f" - **{p['name']}**: {p['error']}")
|
|
1567
|
+
if fix_cmd:
|
|
1568
|
+
plugin_lines.append(f" Fix: `{fix_cmd}`")
|
|
1569
|
+
mode_notes.append(
|
|
1570
|
+
f"[Plugins] {len(unready_plugins)} plugin(s) installed but not ready:\n"
|
|
1571
|
+
+ "\n".join(plugin_lines)
|
|
1572
|
+
+ "\n After authenticating, run `/wicked-garden:platform:health` to re-probe."
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
# 7c. Detect dangerous mode (AskUserQuestion broken)
|
|
1576
|
+
dangerous_mode = _detect_dangerous_mode()
|
|
1577
|
+
if state is not None:
|
|
1578
|
+
state.update(dangerous_mode=dangerous_mode)
|
|
1579
|
+
|
|
1580
|
+
# 7c.1. Critical v6 skill smoke-test (issue #434)
|
|
1581
|
+
skills_note = _check_critical_skills()
|
|
1582
|
+
if skills_note:
|
|
1583
|
+
mode_notes.append(skills_note)
|
|
1584
|
+
|
|
1585
|
+
# 7c.2. Registry allowlist validation — surface drift between
|
|
1586
|
+
# in-code allowlists (phases, reviewers, bus handlers, skill refs)
|
|
1587
|
+
# and the assets they reference. Fail-OPEN: validator errors are
|
|
1588
|
+
# swallowed inside _run_registry_validation, returning None so the
|
|
1589
|
+
# briefing simply omits the [Registry] line on failure. Findings
|
|
1590
|
+
# are advisory at the session-start surface — they do NOT block.
|
|
1591
|
+
registry_note = _run_registry_validation()
|
|
1592
|
+
if registry_note:
|
|
1593
|
+
mode_notes.append(registry_note)
|
|
1594
|
+
|
|
1595
|
+
# 7d. Check wicked-brain dependency (opt-in memory/context layer)
|
|
1596
|
+
brain_available, brain_note = _check_brain_dependency()
|
|
1597
|
+
if state is not None and brain_available is not None:
|
|
1598
|
+
state.update(brain_available=brain_available)
|
|
1599
|
+
if brain_note:
|
|
1600
|
+
# brain is an opt-in toolkit layer, not a hard requirement — surface
|
|
1601
|
+
# the install pointer informationally, never block the session.
|
|
1602
|
+
mode_notes.append(brain_note)
|
|
1603
|
+
|
|
1604
|
+
# 7e. Brain auto-init and auto-ingest
|
|
1605
|
+
# If brain is installed but has no content for this project, emit
|
|
1606
|
+
# a directive so the agent auto-runs init + ingest on first use.
|
|
1607
|
+
# If the server isn't reachable, emit a start directive instead.
|
|
1608
|
+
if brain_available:
|
|
1609
|
+
stats = _brain_api("stats", {}, timeout=2)
|
|
1610
|
+
if stats is None:
|
|
1611
|
+
# The dependency check above already attempted a deterministic
|
|
1612
|
+
# auto-start; re-probe once in case readiness landed late.
|
|
1613
|
+
try:
|
|
1614
|
+
from _brain_port import ensure_server
|
|
1615
|
+
if ensure_server():
|
|
1616
|
+
stats = _brain_api("stats", {}, timeout=2)
|
|
1617
|
+
except Exception:
|
|
1618
|
+
pass
|
|
1619
|
+
if stats is None:
|
|
1620
|
+
mode_notes.append(
|
|
1621
|
+
"[wicked-brain] Plugin installed but the server did not respond "
|
|
1622
|
+
"after auto-start. Diagnose with `wicked-brain-call --start`, "
|
|
1623
|
+
"then run the brain pipeline: ingest → retag → compile."
|
|
1624
|
+
)
|
|
1625
|
+
elif stats.get("total", 0) == 0:
|
|
1626
|
+
mode_notes.append(
|
|
1627
|
+
"[wicked-brain] Brain is empty for this project. "
|
|
1628
|
+
"You MUST run the full brain pipeline now as background agents:\n"
|
|
1629
|
+
" 1. `wicked-brain:init` — initialize the brain if needed\n"
|
|
1630
|
+
" 2. `wicked-brain:ingest` with the current working directory\n"
|
|
1631
|
+
" 3. `wicked-brain:retag` — expand keywords into semantic tags\n"
|
|
1632
|
+
" 4. `wicked-brain:compile` — synthesize wiki articles from chunks\n"
|
|
1633
|
+
"Run these sequentially in a background agent. "
|
|
1634
|
+
"Do NOT wait for completion before responding to the user."
|
|
1635
|
+
)
|
|
1636
|
+
|
|
1637
|
+
# 8. Assemble session briefing
|
|
1638
|
+
# --- Status block (user-facing) ---
|
|
1639
|
+
project = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
|
|
1640
|
+
|
|
1641
|
+
onboarding_parts = []
|
|
1642
|
+
if has_index:
|
|
1643
|
+
onboarding_parts.append("search index")
|
|
1644
|
+
if has_memories:
|
|
1645
|
+
onboarding_parts.append("memories")
|
|
1646
|
+
if onboarding_parts:
|
|
1647
|
+
onboarding_status = "Complete (" + " + ".join(onboarding_parts) + ")"
|
|
1648
|
+
else:
|
|
1649
|
+
onboarding_status = "Not started"
|
|
1650
|
+
|
|
1651
|
+
status_lines = [
|
|
1652
|
+
f"wicked-garden | {project}",
|
|
1653
|
+
" Storage: local",
|
|
1654
|
+
f" Onboarding: {onboarding_status}",
|
|
1655
|
+
]
|
|
1656
|
+
|
|
1657
|
+
# Show last crew project for this workspace as informational only.
|
|
1658
|
+
# Do NOT auto-activate — the user must explicitly start/resume a project.
|
|
1659
|
+
if project_data:
|
|
1660
|
+
name = project_data.get("name", project_name or "unknown")
|
|
1661
|
+
phase = project_data.get("current_phase", "unknown")
|
|
1662
|
+
status_lines.append(f" Last crew: {name} ({phase})")
|
|
1663
|
+
|
|
1664
|
+
# Store as reference info only — NOT as active_project
|
|
1665
|
+
if state is not None:
|
|
1666
|
+
# active_project_id: only set when project is in a non-complete, non-skipped phase
|
|
1667
|
+
active_phase = project_data.get("current_phase", "")
|
|
1668
|
+
is_active = active_phase not in ("", "complete", "done", "archived")
|
|
1669
|
+
state.update(active_project_id=project_name if is_active else None)
|
|
1670
|
+
|
|
1671
|
+
# v9.2.4: also write active_chain_id so smaht's chain-aware
|
|
1672
|
+
# event scoring (CLAUDE.md "Chain-aware smaht scoring") actually
|
|
1673
|
+
# works. Field declared in v9.2.3, producer was missing — events
|
|
1674
|
+
# got the 0.1 baseline instead of the documented 0.8+ chain
|
|
1675
|
+
# boost. Format mirrors the chain_id convention in CLAUDE.md
|
|
1676
|
+
# "Native Tasks as Dual-Purpose Event Queue":
|
|
1677
|
+
# {slug}.root for the root chain
|
|
1678
|
+
# {slug}.{phase} per active phase
|
|
1679
|
+
# Cleared (set None) when the project is not active so events
|
|
1680
|
+
# in unrelated sessions don't accidentally inherit the chain.
|
|
1681
|
+
if is_active:
|
|
1682
|
+
chain_id = f"{project_name}.{active_phase}" if active_phase != "clarify" else f"{project_name}.root"
|
|
1683
|
+
state.update(active_chain_id=chain_id)
|
|
1684
|
+
else:
|
|
1685
|
+
state.update(active_chain_id=None)
|
|
1686
|
+
|
|
1687
|
+
# v9.2.5: also write active_project (the dict snapshot, distinct
|
|
1688
|
+
# from active_project_id which is the slug). Read by:
|
|
1689
|
+
# - post_tool.py:1428-1431 (tracks current_phase + current_specialist
|
|
1690
|
+
# for QE change-tracking nudge)
|
|
1691
|
+
# - pre_compact.py (WIP recovery dump shows "Active Project" section)
|
|
1692
|
+
# Producer was missing — both consumers got None and silently fell
|
|
1693
|
+
# through their default branches. Same bug class as active_chain_id
|
|
1694
|
+
# (declared, read, never written). Same fix: write in bootstrap when
|
|
1695
|
+
# the project is active.
|
|
1696
|
+
if is_active:
|
|
1697
|
+
state.update(active_project=dict(project_data))
|
|
1698
|
+
else:
|
|
1699
|
+
state.update(active_project=None)
|
|
1700
|
+
|
|
1701
|
+
# #459 telemetry producer: anchor complexity_at_session_open
|
|
1702
|
+
# from the active project's complexity_score so session-close
|
|
1703
|
+
# drift capture has a delta baseline. Only set when not
|
|
1704
|
+
# already populated this session (first SessionStart wins).
|
|
1705
|
+
try:
|
|
1706
|
+
raw_complexity = project_data.get("complexity_score", 0)
|
|
1707
|
+
complexity_val = int(raw_complexity) if raw_complexity is not None else 0
|
|
1708
|
+
updates = {"complexity_score": complexity_val}
|
|
1709
|
+
if getattr(state, "complexity_at_session_open", None) is None:
|
|
1710
|
+
updates["complexity_at_session_open"] = complexity_val
|
|
1711
|
+
state.update(**updates)
|
|
1712
|
+
except (TypeError, ValueError):
|
|
1713
|
+
# Malformed complexity_score — fail-open, don't block bootstrap.
|
|
1714
|
+
pass
|
|
1715
|
+
|
|
1716
|
+
# Enable memory compliance directives for crew sessions.
|
|
1717
|
+
# task_completed.py reads this flag to decide whether to emit
|
|
1718
|
+
# per-task memory prompts; stop.py reads it for session summary.
|
|
1719
|
+
if state is not None:
|
|
1720
|
+
state.update(memory_compliance_required=True)
|
|
1721
|
+
|
|
1722
|
+
briefing_parts = ["\n".join(status_lines)]
|
|
1723
|
+
|
|
1724
|
+
# --- Solo-mode HITL banner (#651) ---
|
|
1725
|
+
# Emit once per session when the active crew project has solo_mode: true.
|
|
1726
|
+
if project_data and project_data.get("solo_mode"):
|
|
1727
|
+
briefing_parts.append(
|
|
1728
|
+
"[SOLO MODE ACTIVE] Council gates replaced with inline human review "
|
|
1729
|
+
"for this project. Gate artifacts are preserved. "
|
|
1730
|
+
"Merge-gate council pattern is unchanged."
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
# --- Mode-specific warnings/notes ---
|
|
1734
|
+
for note in mode_notes:
|
|
1735
|
+
briefing_parts.append(note)
|
|
1736
|
+
|
|
1737
|
+
if decay_summary:
|
|
1738
|
+
briefing_parts.append(f"[Memory] {decay_summary}")
|
|
1739
|
+
|
|
1740
|
+
# NOTE (v9.2.10): the per-session _MEMORY_INSTRUCTIONS append was
|
|
1741
|
+
# removed. CLAUDE.md's "Memory Management" section already covers
|
|
1742
|
+
# what the briefing was telling Claude. v9.2.11 deleted the constant
|
|
1743
|
+
# itself after confirming zero imports.
|
|
1744
|
+
|
|
1745
|
+
if dangerous_mode:
|
|
1746
|
+
briefing_parts.append(_DANGEROUS_MODE_WARNING)
|
|
1747
|
+
|
|
1748
|
+
# [Setup] and [Quick Start] are first-session-per-project notices.
|
|
1749
|
+
# Once shown for a project, they are recorded in the per-project
|
|
1750
|
+
# sentinel file and suppressed on every subsequent bootstrap. v9.2.10.
|
|
1751
|
+
if not onboarding_directive:
|
|
1752
|
+
if not _notice_already_shown(_NOTICE_SETUP):
|
|
1753
|
+
briefing_parts.append(
|
|
1754
|
+
"[Setup] Run `/wicked-garden:setup --reconfigure` to change connection or re-onboard."
|
|
1755
|
+
)
|
|
1756
|
+
_record_notice_shown(_NOTICE_SETUP)
|
|
1757
|
+
|
|
1758
|
+
# Discovery: show 2-3 contextual command suggestions based on project files.
|
|
1759
|
+
# Also gated on first-session-per-project since the suggestions don't change
|
|
1760
|
+
# session-to-session.
|
|
1761
|
+
if not onboarding_directive and has_memories:
|
|
1762
|
+
if not _notice_already_shown(_NOTICE_QUICK_START):
|
|
1763
|
+
discovery_lines = _suggest_commands_for_project()
|
|
1764
|
+
if discovery_lines:
|
|
1765
|
+
briefing_parts.append(discovery_lines)
|
|
1766
|
+
_record_notice_shown(_NOTICE_QUICK_START)
|
|
1767
|
+
|
|
1768
|
+
# Facilitator context: surface aging action items from process_memory.
|
|
1769
|
+
# Additive briefing line (issue #461) — runs last of the informational
|
|
1770
|
+
# notes and is independent of onboarding or setup state.
|
|
1771
|
+
fc_note = _check_facilitator_context(project_name)
|
|
1772
|
+
if fc_note:
|
|
1773
|
+
briefing_parts.append(fc_note)
|
|
1774
|
+
|
|
1775
|
+
# Onboarding directive goes LAST for highest priority
|
|
1776
|
+
if onboarding_directive:
|
|
1777
|
+
briefing_parts.append(onboarding_directive)
|
|
1778
|
+
|
|
1779
|
+
# 9. Persist final session state
|
|
1780
|
+
if state is not None:
|
|
1781
|
+
_save_session_state(state)
|
|
1782
|
+
|
|
1783
|
+
_log("bootstrap", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
|
|
1784
|
+
print(json.dumps({
|
|
1785
|
+
"hookSpecificOutput": {
|
|
1786
|
+
"hookEventName": "SessionStart",
|
|
1787
|
+
"additionalContext": "\n".join(briefing_parts),
|
|
1788
|
+
},
|
|
1789
|
+
}))
|
|
1790
|
+
|
|
1791
|
+
except Exception as e:
|
|
1792
|
+
print(f"[wicked-garden] bootstrap error: {e}", file=sys.stderr)
|
|
1793
|
+
print(json.dumps({}))
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
if __name__ == "__main__":
|
|
1797
|
+
main()
|