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,1374 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse / PostToolUseFailure hook — wicked-garden unified post-tool dispatcher.
|
|
4
|
+
|
|
5
|
+
Consolidates: crew posttool_task, delivery metrics_refresh, mem task_checkpoint,
|
|
6
|
+
search mark_stale, agentic detect_framework,
|
|
7
|
+
observability trace_writer.
|
|
8
|
+
|
|
9
|
+
Dispatches by tool_name from hook payload:
|
|
10
|
+
TaskCreate / TaskUpdate / TodoWrite → mismatch detection on TaskUpdate
|
|
11
|
+
Write / Edit → stale file marking + QE change tracking + MEMORY.md guard
|
|
12
|
+
Task → subagent activity tracking
|
|
13
|
+
Read → large-file-read warning + agentic framework detection
|
|
14
|
+
Bash → activity tracking + claim-sentinel ref-watch
|
|
15
|
+
Grep / Glob → no-op (ambient tips retired; sentinel replaced them)
|
|
16
|
+
PostToolUseFailure (any tool_name) → failure counting + auto issue detection
|
|
17
|
+
|
|
18
|
+
Traces are written to a session-scoped JSONL file in $TMPDIR (local only).
|
|
19
|
+
|
|
20
|
+
Always fails open — any unhandled exception returns {"continue": true}.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Add shared scripts directory to path
|
|
33
|
+
_PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
|
|
34
|
+
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
|
|
35
|
+
|
|
36
|
+
def _resolve_brain_port():
|
|
37
|
+
try:
|
|
38
|
+
from _brain_port import resolve_port
|
|
39
|
+
return resolve_port()
|
|
40
|
+
except Exception:
|
|
41
|
+
return int(os.environ.get("WICKED_BRAIN_PORT", "4242"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Ops logger wrapper — fail-silent, never crashes the hook
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _log(domain, level, event, ok=True, ms=None, detail=None):
|
|
49
|
+
"""Ops logger — fail-silent, never crashes the hook."""
|
|
50
|
+
try:
|
|
51
|
+
from _logger import log
|
|
52
|
+
log(domain, level, event, ok=ok, ms=ms, detail=detail)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Helpers
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _now_iso() -> str:
|
|
62
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sanitize_session_id(raw: str) -> str:
|
|
66
|
+
"""Strip path separators and traversal sequences from session ID."""
|
|
67
|
+
sanitized = raw.replace("/", "_").replace("\\", "_").replace("..", "_")
|
|
68
|
+
return sanitized if sanitized else "default"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_session_id() -> str:
|
|
72
|
+
return _sanitize_session_id(os.environ.get("CLAUDE_SESSION_ID", "default"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _bus_as_truth_flag_on() -> bool:
|
|
76
|
+
"""Return True iff Site 3 bus-as-truth cutover flag is enabled.
|
|
77
|
+
|
|
78
|
+
Site 3 of the bus cutover (#746) gates on ``WG_BUS_AS_TRUTH_REVIEWER_REPORT``.
|
|
79
|
+
This token is in the default-ON set (shipped, PR #776), so the flag is ON
|
|
80
|
+
unless explicitly set to ``"off"`` (case/whitespace normalised).
|
|
81
|
+
|
|
82
|
+
Mirrors the ``_bus_as_truth_enabled(site)`` helper in ``scripts/_bus.py``
|
|
83
|
+
(same normalization + default-map fall-through) but lives in this hook
|
|
84
|
+
module so the hook does not need to import ``_bus`` merely to read the flag.
|
|
85
|
+
|
|
86
|
+
Resolution order (flag-fold PR #777):
|
|
87
|
+
1. Explicit ``"on"`` (case/whitespace normalised) → True.
|
|
88
|
+
2. Explicit ``"off"`` (case/whitespace normalised) → False.
|
|
89
|
+
3. Empty / any other value → default-ON (REVIEWER_REPORT is a shipped site).
|
|
90
|
+
|
|
91
|
+
Reading the env var directly anywhere else in this module is forbidden —
|
|
92
|
+
all reads go through this helper so there is one place to flip and audit.
|
|
93
|
+
"""
|
|
94
|
+
raw = os.environ.get("WG_BUS_AS_TRUTH_REVIEWER_REPORT", "").strip().lower()
|
|
95
|
+
if raw == "on":
|
|
96
|
+
return True
|
|
97
|
+
if raw == "off":
|
|
98
|
+
return False
|
|
99
|
+
# REVIEWER_REPORT is a shipped site — default ON.
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Handler: Grep / Glob (discovery hint for search commands)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _handle_grep_glob(tool_name: str, tool_input: dict) -> dict:
|
|
108
|
+
"""No-op handler. The ambient discovery tips that lived here were retired —
|
|
109
|
+
repetition trained the agent to ignore the channel. Claim-time signals
|
|
110
|
+
(scripts/sentinel/) replaced them."""
|
|
111
|
+
return {"continue": True}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Handler: Write / Edit
|
|
116
|
+
# (stale file marking, QE change tracking)
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def _handle_write_edit(tool_input: dict) -> dict:
|
|
120
|
+
"""Mark file as stale for search index + CLAUDE.md/AGENTS.md sync directive."""
|
|
121
|
+
file_path = tool_input.get("file_path", "")
|
|
122
|
+
if not file_path:
|
|
123
|
+
return {"continue": True}
|
|
124
|
+
|
|
125
|
+
messages = []
|
|
126
|
+
|
|
127
|
+
# 0. CLAUDE.md ↔ AGENTS.md sync directive
|
|
128
|
+
sync_msg = _check_instruction_file_sync(file_path)
|
|
129
|
+
if sync_msg:
|
|
130
|
+
messages.append(sync_msg)
|
|
131
|
+
|
|
132
|
+
# 1. Mark file stale + re-index in brain
|
|
133
|
+
_mark_file_stale(file_path)
|
|
134
|
+
_brain_reindex_file(file_path)
|
|
135
|
+
|
|
136
|
+
# 2. The QE threshold nudge was retired: it recommended test menus off a
|
|
137
|
+
# file-count heuristic (it once suggested unit tests for a README edit).
|
|
138
|
+
# The testing signal now fires at claim time via the sentinel's
|
|
139
|
+
# evidence-freshness invariant (scripts/sentinel/invariants.py).
|
|
140
|
+
|
|
141
|
+
# v9.2.2: dropped the [Scenarios] staleness nudge entirely. The heuristic
|
|
142
|
+
# (any edit under commands/{domain}/ or skills/{domain}/ → "scenarios in
|
|
143
|
+
# scenarios/{domain}/ may need updating") fired multiple times per
|
|
144
|
+
# session with low signal. Users rarely acted on it because most edits
|
|
145
|
+
# don't actually invalidate scenarios. /wg-test stays available on demand.
|
|
146
|
+
|
|
147
|
+
result = {"continue": True}
|
|
148
|
+
if messages:
|
|
149
|
+
result["systemMessage"] = "\n".join(messages)
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_instruction_file_sync(file_path: str) -> str | None:
|
|
154
|
+
"""When CLAUDE.md or AGENTS.md is edited, prompt to sync the counterpart.
|
|
155
|
+
|
|
156
|
+
Both files serve as instruction files for different AI tools and must stay
|
|
157
|
+
consistent. CLAUDE.md is the primary (loaded by Claude Code), AGENTS.md is
|
|
158
|
+
the cross-tool counterpart (Codex, Cursor, Amp, etc.).
|
|
159
|
+
|
|
160
|
+
Only fires once per session to avoid repeated nudges.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
p = Path(file_path)
|
|
164
|
+
name = p.name
|
|
165
|
+
|
|
166
|
+
# Determine which file was edited and which needs syncing
|
|
167
|
+
if name == "CLAUDE.md":
|
|
168
|
+
counterpart_name = "AGENTS.md"
|
|
169
|
+
counterpart = p.parent.parent / "AGENTS.md" if p.parent.name == ".claude" else p.parent / "AGENTS.md"
|
|
170
|
+
elif name == "AGENTS.md":
|
|
171
|
+
counterpart_name = "CLAUDE.md"
|
|
172
|
+
# CLAUDE.md lives in .claude/ subdirectory
|
|
173
|
+
counterpart = p.parent / ".claude" / "CLAUDE.md"
|
|
174
|
+
if not counterpart.exists():
|
|
175
|
+
counterpart = p.parent / "CLAUDE.md"
|
|
176
|
+
else:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# Only fire once per session
|
|
180
|
+
from _session import SessionState
|
|
181
|
+
state = SessionState.load()
|
|
182
|
+
synced_flags = getattr(state, "instruction_sync_fired", None) or []
|
|
183
|
+
if name in synced_flags:
|
|
184
|
+
return None
|
|
185
|
+
synced_flags.append(name)
|
|
186
|
+
state.update(instruction_sync_fired=synced_flags)
|
|
187
|
+
|
|
188
|
+
if not counterpart.exists():
|
|
189
|
+
return (
|
|
190
|
+
f"[Sync] {name} was updated but {counterpart_name} does not exist yet. "
|
|
191
|
+
f"Create {counterpart_name} with the same user-facing instructions so "
|
|
192
|
+
f"other AI tools (Codex, Cursor, Amp) follow the same rules."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
f"[Sync] {name} was updated. Read {counterpart.name} and update it to "
|
|
197
|
+
f"reflect the same user instructions and preferences. Both files must stay "
|
|
198
|
+
f"consistent — {counterpart_name} is used by other AI coding tools."
|
|
199
|
+
)
|
|
200
|
+
except Exception:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _mark_file_stale(file_path: str) -> None:
|
|
205
|
+
"""Accumulate stale file paths in SessionState for incremental re-index."""
|
|
206
|
+
try:
|
|
207
|
+
from _session import SessionState
|
|
208
|
+
state = SessionState.load()
|
|
209
|
+
stale = state.stale_files or []
|
|
210
|
+
if file_path not in stale:
|
|
211
|
+
stale.append(file_path)
|
|
212
|
+
state.update(stale_files=stale)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _brain_reindex_file(file_path: str) -> None:
|
|
218
|
+
"""Re-index a changed file in the brain FTS5 index. Fails silently.
|
|
219
|
+
|
|
220
|
+
Reads the file content and POSTs it to the brain index API so the brain
|
|
221
|
+
stays current as files are edited during the session. Async-safe: uses
|
|
222
|
+
stdlib urllib with a short timeout.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
import urllib.request
|
|
226
|
+
p = Path(file_path)
|
|
227
|
+
if not p.exists() or not p.is_file():
|
|
228
|
+
return
|
|
229
|
+
# Only index text files the brain cares about
|
|
230
|
+
text_ext = {".md", ".txt", ".py", ".js", ".ts", ".jsx", ".tsx", ".sh",
|
|
231
|
+
".json", ".yaml", ".yml", ".toml", ".html", ".css", ".mjs"}
|
|
232
|
+
if p.suffix.lower() not in text_ext:
|
|
233
|
+
return
|
|
234
|
+
# Read content (cap at 50KB to avoid slow indexing on huge files)
|
|
235
|
+
content = p.read_text(encoding="utf-8", errors="replace")[:50000]
|
|
236
|
+
if not content.strip():
|
|
237
|
+
return
|
|
238
|
+
# Build a safe chunk ID from the file path
|
|
239
|
+
safe = file_path.lower().replace("/", "-").replace("\\", "-")
|
|
240
|
+
safe = re.sub(r"[^a-z0-9.-]", "-", safe)
|
|
241
|
+
safe = re.sub(r"-+", "-", safe).strip("-")
|
|
242
|
+
chunk_id = f"chunks/extracted/{safe}/chunk-001.md"
|
|
243
|
+
|
|
244
|
+
port = _resolve_brain_port()
|
|
245
|
+
payload = json.dumps({
|
|
246
|
+
"action": "index",
|
|
247
|
+
"params": {
|
|
248
|
+
"id": chunk_id,
|
|
249
|
+
"path": chunk_id,
|
|
250
|
+
"content": content,
|
|
251
|
+
"brain_id": "wicked-brain",
|
|
252
|
+
},
|
|
253
|
+
}).encode("utf-8")
|
|
254
|
+
req = urllib.request.Request(
|
|
255
|
+
f"http://localhost:{port}/api",
|
|
256
|
+
data=payload,
|
|
257
|
+
headers={"Content-Type": "application/json"},
|
|
258
|
+
method="POST",
|
|
259
|
+
)
|
|
260
|
+
urllib.request.urlopen(req, timeout=2)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass # brain re-index is best-effort, never blocks the hook
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _check_permission_failure(tool_response) -> str | None:
|
|
266
|
+
"""Inspect subagent result text for permission failure signals.
|
|
267
|
+
|
|
268
|
+
Returns a short snippet of the matching text if a permission failure
|
|
269
|
+
pattern is detected, or None if the result looks normal.
|
|
270
|
+
"""
|
|
271
|
+
if not tool_response:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
# tool_response can be a string or a dict with a "result" or "text" field
|
|
275
|
+
if isinstance(tool_response, dict):
|
|
276
|
+
text = (
|
|
277
|
+
tool_response.get("result", "")
|
|
278
|
+
or tool_response.get("text", "")
|
|
279
|
+
or tool_response.get("content", "")
|
|
280
|
+
or tool_response.get("output", "")
|
|
281
|
+
)
|
|
282
|
+
# Also check stringified dict as fallback
|
|
283
|
+
if not text:
|
|
284
|
+
text = json.dumps(tool_response)
|
|
285
|
+
elif isinstance(tool_response, str):
|
|
286
|
+
text = tool_response
|
|
287
|
+
else:
|
|
288
|
+
text = str(tool_response)
|
|
289
|
+
|
|
290
|
+
text_lower = text.lower()
|
|
291
|
+
for pattern in _PERMISSION_FAILURE_PATTERNS:
|
|
292
|
+
idx = text_lower.find(pattern)
|
|
293
|
+
if idx != -1:
|
|
294
|
+
# Extract a short snippet around the match for the system message
|
|
295
|
+
start = max(0, idx - 20)
|
|
296
|
+
end = min(len(text), idx + len(pattern) + 60)
|
|
297
|
+
snippet = text[start:end].strip()
|
|
298
|
+
# Truncate if still too long
|
|
299
|
+
if len(snippet) > 120:
|
|
300
|
+
snippet = snippet[:120] + "..."
|
|
301
|
+
return snippet
|
|
302
|
+
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _handle_task_dispatch(tool_input: dict, tool_response=None) -> dict:
|
|
307
|
+
"""Record subagent activity in session state and detect permission failures."""
|
|
308
|
+
messages = []
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
subagent_type = tool_input.get("subagent_type", "")
|
|
312
|
+
if not subagent_type:
|
|
313
|
+
return {"continue": True}
|
|
314
|
+
|
|
315
|
+
from _session import SessionState
|
|
316
|
+
state = SessionState.load()
|
|
317
|
+
dispatches = getattr(state, "subagent_dispatches", None) or []
|
|
318
|
+
dispatches.append({
|
|
319
|
+
"agent": subagent_type,
|
|
320
|
+
"ts": _now_iso(),
|
|
321
|
+
})
|
|
322
|
+
# Keep last 20 dispatches only
|
|
323
|
+
state.update(subagent_dispatches=dispatches[-20:])
|
|
324
|
+
|
|
325
|
+
# --- Permission failure detection (Issue #318) ---
|
|
326
|
+
snippet = _check_permission_failure(tool_response)
|
|
327
|
+
if snippet:
|
|
328
|
+
failures = (getattr(state, "subagent_permission_failures", None) or 0) + 1
|
|
329
|
+
state.update(subagent_permission_failures=failures)
|
|
330
|
+
|
|
331
|
+
_log(
|
|
332
|
+
"crew", "warn", "subagent.permission_failure",
|
|
333
|
+
ok=False,
|
|
334
|
+
detail={"agent": subagent_type, "snippet": snippet, "count": failures},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
messages.append(
|
|
338
|
+
"[Crew] Subagent completed without work \u2014 permission issue detected. "
|
|
339
|
+
"The subagent '{}' reported: '{}'. "
|
|
340
|
+
"Consider running with broader permissions or executing the task inline.".format(
|
|
341
|
+
subagent_type, snippet
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
state.save()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
result = {"continue": True}
|
|
350
|
+
if messages:
|
|
351
|
+
result["systemMessage"] = "\n".join(messages)
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
# Handler: Read (agentic framework detection)
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
_FRAMEWORK_PATTERNS = {
|
|
360
|
+
"langchain": ["langchain", "langsmith"],
|
|
361
|
+
"langgraph": ["langgraph"],
|
|
362
|
+
"autogen": ["autogen", "pyautogen"],
|
|
363
|
+
"crewai": ["crewai", "crew_ai"],
|
|
364
|
+
"dspy": ["dspy"],
|
|
365
|
+
"llamaindex": ["llama_index", "llama-index"],
|
|
366
|
+
"pydantic-ai": ["pydantic_ai", "pydantic-ai"],
|
|
367
|
+
"anthropic-sdk": ["anthropic"],
|
|
368
|
+
"openai-sdk": ["openai"],
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
_READ_LINE_THRESHOLD = 500
|
|
373
|
+
_READ_CHAR_THRESHOLD = 5000
|
|
374
|
+
_CUMULATIVE_WARN_THRESHOLD = 50000
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _handle_read(tool_input: dict, tool_response=None) -> dict:
|
|
378
|
+
"""Large-file-read warning + path-based agentic framework detection.
|
|
379
|
+
|
|
380
|
+
Checks the Read tool output size and emits a systemMessage warning when
|
|
381
|
+
a single read exceeds _READ_LINE_THRESHOLD lines or _READ_CHAR_THRESHOLD
|
|
382
|
+
chars. Tracks cumulative bytes read for escalating warnings.
|
|
383
|
+
|
|
384
|
+
Optimized (Issue #312): uses a single SessionState load/save for both
|
|
385
|
+
large-file tracking and framework detection, avoiding redundant disk I/O
|
|
386
|
+
on the highest-frequency tool.
|
|
387
|
+
|
|
388
|
+
Always returns {"continue": True} — never blocks reads.
|
|
389
|
+
"""
|
|
390
|
+
file_path = (tool_input.get("file_path") or "")
|
|
391
|
+
messages = []
|
|
392
|
+
|
|
393
|
+
# --- Quick path-based framework check (no I/O needed) ---
|
|
394
|
+
file_path_lower = file_path.lower()
|
|
395
|
+
detected_frameworks = []
|
|
396
|
+
if file_path_lower:
|
|
397
|
+
for framework, patterns in _FRAMEWORK_PATTERNS.items():
|
|
398
|
+
if any(p in file_path_lower for p in patterns):
|
|
399
|
+
detected_frameworks.append(framework)
|
|
400
|
+
|
|
401
|
+
# --- Parse response text for size check ---
|
|
402
|
+
response_text = ""
|
|
403
|
+
try:
|
|
404
|
+
if isinstance(tool_response, str):
|
|
405
|
+
response_text = tool_response
|
|
406
|
+
elif isinstance(tool_response, dict):
|
|
407
|
+
response_text = tool_response.get("content", "") or tool_response.get("output", "") or ""
|
|
408
|
+
elif isinstance(tool_response, list):
|
|
409
|
+
# Some hook payloads wrap content in a list of blocks
|
|
410
|
+
parts = []
|
|
411
|
+
for block in tool_response:
|
|
412
|
+
if isinstance(block, dict):
|
|
413
|
+
parts.append(block.get("text", "") or block.get("content", "") or "")
|
|
414
|
+
elif isinstance(block, str):
|
|
415
|
+
parts.append(block)
|
|
416
|
+
response_text = "\n".join(parts)
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# --- Single SessionState load/save for both concerns ---
|
|
421
|
+
needs_state = bool(response_text) or bool(detected_frameworks)
|
|
422
|
+
if needs_state:
|
|
423
|
+
try:
|
|
424
|
+
from _session import SessionState
|
|
425
|
+
state = SessionState.load()
|
|
426
|
+
state_dirty = False
|
|
427
|
+
|
|
428
|
+
# Large-file-read detection
|
|
429
|
+
if response_text:
|
|
430
|
+
char_count = len(response_text)
|
|
431
|
+
line_count = response_text.count("\n") + 1
|
|
432
|
+
|
|
433
|
+
cumulative = (state.read_bytes_total or 0) + char_count
|
|
434
|
+
warn_count = state.read_large_warn_count or 0
|
|
435
|
+
|
|
436
|
+
if line_count >= _READ_LINE_THRESHOLD or char_count >= _READ_CHAR_THRESHOLD:
|
|
437
|
+
warn_count += 1
|
|
438
|
+
short_path = Path(file_path).name if file_path else "unknown"
|
|
439
|
+
|
|
440
|
+
if cumulative >= _CUMULATIVE_WARN_THRESHOLD and warn_count > 2:
|
|
441
|
+
messages.append(
|
|
442
|
+
f"[Context] Large file read: {short_path} ({line_count} lines, "
|
|
443
|
+
f"{char_count:,} chars). Session total: {cumulative:,} chars read inline. "
|
|
444
|
+
f"Context window pressure is high — delegate remaining reads to "
|
|
445
|
+
f"subagents via Task tool to preserve context."
|
|
446
|
+
)
|
|
447
|
+
else:
|
|
448
|
+
messages.append(
|
|
449
|
+
f"[Context] Large file read detected ({line_count} lines, "
|
|
450
|
+
f"{char_count:,} chars). Consider delegating to a subagent to "
|
|
451
|
+
f"preserve context. Use Task tool with the file path instead of "
|
|
452
|
+
f"reading inline."
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
state.read_bytes_total = cumulative
|
|
456
|
+
state.read_large_warn_count = warn_count
|
|
457
|
+
state_dirty = True
|
|
458
|
+
|
|
459
|
+
# Framework detection (merged into same state load)
|
|
460
|
+
if detected_frameworks:
|
|
461
|
+
existing = state.detected_frameworks or []
|
|
462
|
+
combined = list(set(existing + detected_frameworks))
|
|
463
|
+
if combined != existing:
|
|
464
|
+
state.detected_frameworks = combined
|
|
465
|
+
state_dirty = True
|
|
466
|
+
|
|
467
|
+
if state_dirty:
|
|
468
|
+
state.save()
|
|
469
|
+
except Exception:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
result = {"continue": True}
|
|
473
|
+
if messages:
|
|
474
|
+
result["systemMessage"] = "\n".join(messages)
|
|
475
|
+
return result
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
# Handler: Skill (memory compliance counter reset + pull-model tracking)
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
def _handle_skill(tool_input: dict) -> dict:
|
|
483
|
+
"""Handle Skill PostToolUse: memory compliance reset + pull-model tracking.
|
|
484
|
+
|
|
485
|
+
1. Reset memory_compliance_escalations when a wicked-brain:memory skill
|
|
486
|
+
call succeeds. (mem:store was removed in #603/#604; wicked-brain:memory
|
|
487
|
+
is the canonical memory API as of v9.)
|
|
488
|
+
2. Track wicked-brain:query/search calls for pull-model calibration (Issue #416).
|
|
489
|
+
"""
|
|
490
|
+
skill = (tool_input.get("skill") or "").lower()
|
|
491
|
+
|
|
492
|
+
# Memory compliance reset: zero the escalation counter when the model
|
|
493
|
+
# calls wicked-brain:memory, confirming it acted on the directive.
|
|
494
|
+
# Issue #608: exact match only — substring would false-positive on future
|
|
495
|
+
# skills like wicked-brain:memory-export, wicked-brain:memory-audit, etc.
|
|
496
|
+
if skill != "wicked-brain:memory":
|
|
497
|
+
return {"continue": True}
|
|
498
|
+
try:
|
|
499
|
+
from _session import SessionState
|
|
500
|
+
state = SessionState.load()
|
|
501
|
+
if (state.memory_compliance_escalations or 0) > 0:
|
|
502
|
+
state.update(memory_compliance_escalations=0)
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
return {"continue": True}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
# Handler: Bash async — consensus gate (Issue #368, Tier 2)
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
# This handler is invoked from the ASYNC PostToolUse/Bash hook entry.
|
|
512
|
+
# It fires on every Bash call but fast-exits unless the command is a
|
|
513
|
+
# phase_manager.py approve invocation on a high-complexity project.
|
|
514
|
+
#
|
|
515
|
+
# When matched, it runs the full consensus evaluation synchronously within
|
|
516
|
+
# this process (which itself runs async/non-blocking to the agent).
|
|
517
|
+
# The result is written to phases/{phase}/reviewer-report.md for the
|
|
518
|
+
# PreToolUse Tier 1 hook to check on the NEXT phase's approve call.
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
_REVIEWER_REPORT_HEADER = """\
|
|
522
|
+
---
|
|
523
|
+
verdict: {verdict}
|
|
524
|
+
evidence_items_checked: {evidence_items_checked}
|
|
525
|
+
reviewer: consensus-gate
|
|
526
|
+
reviewed_at: {reviewed_at}
|
|
527
|
+
agreement_ratio: {agreement_ratio}
|
|
528
|
+
findings: {findings}
|
|
529
|
+
conditions: {conditions}
|
|
530
|
+
---
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
_REVIEWER_REPORT_PENDING = """\
|
|
534
|
+
---
|
|
535
|
+
verdict: pending
|
|
536
|
+
evidence_items_checked: 0
|
|
537
|
+
reviewer: consensus-gate
|
|
538
|
+
reviewed_at: {reviewed_at}
|
|
539
|
+
agreement_ratio: 0.0
|
|
540
|
+
findings: []
|
|
541
|
+
conditions: []
|
|
542
|
+
note: "consensus evaluation failed or was skipped — will be re-evaluated on next approve"
|
|
543
|
+
---
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _parse_project_phase_from_command(command: str):
|
|
548
|
+
"""Extract project name and optional --phase from a phase_manager.py approve command.
|
|
549
|
+
|
|
550
|
+
Returns (project_name, phase) tuple. phase may be None if --phase not given.
|
|
551
|
+
Example command:
|
|
552
|
+
sh ...phase_manager.py my-project approve --phase design
|
|
553
|
+
"""
|
|
554
|
+
import shlex
|
|
555
|
+
try:
|
|
556
|
+
tokens = shlex.split(command)
|
|
557
|
+
except ValueError:
|
|
558
|
+
tokens = command.split()
|
|
559
|
+
|
|
560
|
+
project_name = None
|
|
561
|
+
phase = None
|
|
562
|
+
|
|
563
|
+
# Find index of phase_manager.py in tokens
|
|
564
|
+
pm_idx = next(
|
|
565
|
+
(i for i, t in enumerate(tokens) if "phase_manager.py" in t),
|
|
566
|
+
None,
|
|
567
|
+
)
|
|
568
|
+
if pm_idx is None:
|
|
569
|
+
return None, None
|
|
570
|
+
|
|
571
|
+
# Tokens after phase_manager.py: project action [--phase name] [--other flags]
|
|
572
|
+
after = tokens[pm_idx + 1:]
|
|
573
|
+
|
|
574
|
+
# after[0] = project name (positional arg comes before action in phase_manager CLI)
|
|
575
|
+
# Format is: phase_manager.py <project> <action> [flags]
|
|
576
|
+
if len(after) >= 2:
|
|
577
|
+
project_name = after[0]
|
|
578
|
+
# after[1] should be "approve"
|
|
579
|
+
# Look for --phase flag
|
|
580
|
+
for i, tok in enumerate(after[2:], start=2):
|
|
581
|
+
if tok == "--phase" and i + 1 < len(after):
|
|
582
|
+
phase = after[i + 1]
|
|
583
|
+
break
|
|
584
|
+
|
|
585
|
+
# Safety: validate project name
|
|
586
|
+
if project_name:
|
|
587
|
+
import re as _re
|
|
588
|
+
if not _re.match(r'^[a-zA-Z0-9_-]{1,64}$', project_name):
|
|
589
|
+
project_name = None
|
|
590
|
+
|
|
591
|
+
return project_name, phase
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _load_project_for_consensus(project_name: str):
|
|
595
|
+
"""Load project state dict for consensus evaluation.
|
|
596
|
+
|
|
597
|
+
Returns (project_state_dict, project_dir_path) or (None, None) on failure.
|
|
598
|
+
project_state_dict contains at minimum 'complexity_score'.
|
|
599
|
+
"""
|
|
600
|
+
try:
|
|
601
|
+
scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
|
|
602
|
+
if str(scripts_crew) not in sys.path:
|
|
603
|
+
sys.path.insert(0, str(scripts_crew))
|
|
604
|
+
|
|
605
|
+
from phase_manager import load_project_state, get_project_dir
|
|
606
|
+
state = load_project_state(project_name)
|
|
607
|
+
if state is None:
|
|
608
|
+
return None, None
|
|
609
|
+
|
|
610
|
+
project_dir = get_project_dir(project_name)
|
|
611
|
+
|
|
612
|
+
# Convert dataclass to dict (handles both dataclass and plain dict)
|
|
613
|
+
try:
|
|
614
|
+
import dataclasses
|
|
615
|
+
if dataclasses.is_dataclass(state):
|
|
616
|
+
state_dict = dataclasses.asdict(state)
|
|
617
|
+
else:
|
|
618
|
+
state_dict = dict(state)
|
|
619
|
+
except Exception:
|
|
620
|
+
state_dict = {"complexity_score": getattr(state, "complexity_score", 0)}
|
|
621
|
+
|
|
622
|
+
return state_dict, project_dir
|
|
623
|
+
except Exception:
|
|
624
|
+
return None, None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _resolve_phase_for_consensus(project_state_dict: dict, explicit_phase) -> str:
|
|
628
|
+
"""Resolve which phase was just approved.
|
|
629
|
+
|
|
630
|
+
Uses explicit_phase if provided, else current_phase from project state.
|
|
631
|
+
"""
|
|
632
|
+
if explicit_phase:
|
|
633
|
+
return str(explicit_phase)
|
|
634
|
+
|
|
635
|
+
# project_state_dict may have current_phase directly or nested
|
|
636
|
+
phase = project_state_dict.get("current_phase")
|
|
637
|
+
if phase:
|
|
638
|
+
return str(phase)
|
|
639
|
+
|
|
640
|
+
return "unknown"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _load_phases_config() -> dict:
|
|
644
|
+
"""Load phases.json from .claude-plugin/. Returns {} on failure."""
|
|
645
|
+
try:
|
|
646
|
+
phases_file = _PLUGIN_ROOT / ".claude-plugin" / "phases.json"
|
|
647
|
+
if phases_file.exists():
|
|
648
|
+
return json.loads(phases_file.read_text(encoding="utf-8"))
|
|
649
|
+
except Exception:
|
|
650
|
+
pass
|
|
651
|
+
return {}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _build_reviewer_report_yaml(verdict: str, consensus_result: dict) -> str:
|
|
655
|
+
"""Build the YAML frontmatter block for reviewer-report.md from consensus result."""
|
|
656
|
+
agreement_ratio = consensus_result.get("agreement_ratio", 0.0)
|
|
657
|
+
try:
|
|
658
|
+
agreement_ratio = round(float(agreement_ratio), 4)
|
|
659
|
+
except (TypeError, ValueError):
|
|
660
|
+
agreement_ratio = 0.0
|
|
661
|
+
|
|
662
|
+
# Collect findings from consensus_points
|
|
663
|
+
findings_raw = consensus_result.get("consensus_points", [])
|
|
664
|
+
findings_list = []
|
|
665
|
+
for fp in findings_raw[:10]: # cap at 10
|
|
666
|
+
if isinstance(fp, dict):
|
|
667
|
+
text = fp.get("point", fp.get("description", str(fp)))
|
|
668
|
+
else:
|
|
669
|
+
text = str(fp)
|
|
670
|
+
# YAML-safe single-line entry
|
|
671
|
+
findings_list.append(
|
|
672
|
+
' - "' + text[:200].replace('"', "'") + '"'
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# Collect conditions from consensus result
|
|
676
|
+
conditions_raw = consensus_result.get("conditions", []) or []
|
|
677
|
+
conditions_list = []
|
|
678
|
+
for cond in conditions_raw[:10]:
|
|
679
|
+
if isinstance(cond, dict):
|
|
680
|
+
text = cond.get("description", str(cond))
|
|
681
|
+
else:
|
|
682
|
+
text = str(cond)
|
|
683
|
+
conditions_list.append(
|
|
684
|
+
' - "' + text[:200].replace('"', "'") + '"'
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
evidence_count = len(findings_raw) + len(
|
|
688
|
+
consensus_result.get("dissenting_views", []) or []
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
findings_yaml = "[\n" + "\n".join(findings_list) + "\n]" if findings_list else "[]"
|
|
692
|
+
conditions_yaml = "[\n" + "\n".join(conditions_list) + "\n]" if conditions_list else "[]"
|
|
693
|
+
|
|
694
|
+
return _REVIEWER_REPORT_HEADER.format(
|
|
695
|
+
verdict=verdict.lower(),
|
|
696
|
+
evidence_items_checked=evidence_count,
|
|
697
|
+
reviewed_at=_now_iso(),
|
|
698
|
+
agreement_ratio=agreement_ratio,
|
|
699
|
+
findings=findings_yaml,
|
|
700
|
+
conditions=conditions_yaml,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _write_reviewer_report(
|
|
705
|
+
phase_dir: "Path",
|
|
706
|
+
verdict: str,
|
|
707
|
+
consensus_result: dict,
|
|
708
|
+
eval_id: str,
|
|
709
|
+
) -> None:
|
|
710
|
+
"""Emit wicked.consensus.gate_completed — projector materialises the file.
|
|
711
|
+
|
|
712
|
+
Site 3 of bus-cutover (#746, PR #799). Pre-PR-#799 this helper did
|
|
713
|
+
write-then-emit: it read ``report_path.exists()`` to choose
|
|
714
|
+
create-vs-append, wrote the file, then emitted the bus event. PR #799
|
|
715
|
+
deleted the source-side disk writes — the projector handler
|
|
716
|
+
``_consensus_gate_completed`` is now the canonical writer AND owns the
|
|
717
|
+
branch decision (it reads disk state at projection time, eliminating
|
|
718
|
+
the source/projector race).
|
|
719
|
+
|
|
720
|
+
``eval_id`` is threaded from ``_handle_bash_consensus`` so the
|
|
721
|
+
chain_id stays unique across emits in one hook invocation (bus-chain-id
|
|
722
|
+
dedupe gotcha). The ``branch`` payload field is set to ``"append"``
|
|
723
|
+
for mixed-version safety: pre-PR-#799 daemons treat ``"append"`` as
|
|
724
|
+
"append-if-exists, create-if-absent" — exactly the right semantics for
|
|
725
|
+
BOTH the first event (file absent → create) and subsequent events
|
|
726
|
+
(file exists → append). ``"auto"`` or any other value would fall into
|
|
727
|
+
the pre-#799 daemon's create/overwrite path on the second event and
|
|
728
|
+
clobber existing content during a mixed-version rollout. Post-#799
|
|
729
|
+
daemons ignore ``branch`` and decide from disk state at projection time.
|
|
730
|
+
|
|
731
|
+
``_bus.emit_event`` is non-raising — no try/except wrapper needed here.
|
|
732
|
+
"""
|
|
733
|
+
yaml_block = _build_reviewer_report_yaml(verdict, consensus_result)
|
|
734
|
+
|
|
735
|
+
# Extract project_id and phase from phase_dir for payload + chain_id.
|
|
736
|
+
# phase_dir layout: {project_dir}/phases/{phase}
|
|
737
|
+
phase = phase_dir.name
|
|
738
|
+
project_id = phase_dir.parents[1].name
|
|
739
|
+
|
|
740
|
+
if _bus_as_truth_flag_on():
|
|
741
|
+
from _bus import emit_event # noqa: PLC0415 — lazy import per hook pattern
|
|
742
|
+
# raw_payload = yaml_block (just this section). The projector
|
|
743
|
+
# decides create-vs-append by inspecting disk state at projection
|
|
744
|
+
# time (PR #799 — projector self-deciding, eliminates source-side
|
|
745
|
+
# race against the projector).
|
|
746
|
+
emit_event(
|
|
747
|
+
"wicked.consensus.gate_completed",
|
|
748
|
+
{
|
|
749
|
+
"project_id": project_id,
|
|
750
|
+
"phase": phase,
|
|
751
|
+
"verdict": verdict,
|
|
752
|
+
"eval_id": eval_id,
|
|
753
|
+
# ``branch="append"`` is mixed-version safe — pre-#799
|
|
754
|
+
# daemons treat append as "append-if-exists, create-if-
|
|
755
|
+
# absent"; post-#799 daemons ignore the hint entirely
|
|
756
|
+
# and decide from disk state.
|
|
757
|
+
"branch": "append",
|
|
758
|
+
"raw_payload": yaml_block,
|
|
759
|
+
},
|
|
760
|
+
chain_id=f"{project_id}.{phase}.consensus.{eval_id}",
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _write_pending_reviewer_report(phase_dir: "Path", eval_id: str) -> None:
|
|
765
|
+
"""Emit wicked.consensus.gate_pending — projector materialises the file.
|
|
766
|
+
|
|
767
|
+
Fires when consensus evaluation fails. The projector handler
|
|
768
|
+
``_consensus_gate_pending`` materialises the pending placeholder ONLY
|
|
769
|
+
when reviewer-report.md does not already exist — it never overwrites a
|
|
770
|
+
real result (the no-clobber invariant moved into the projector in
|
|
771
|
+
PR #799 along with the legacy direct-write deletion).
|
|
772
|
+
|
|
773
|
+
``eval_id`` MUST be threaded from the caller — no consensus_result is
|
|
774
|
+
available on this failure path, so the caller mints eval_id at the
|
|
775
|
+
entry point and passes it in explicitly (Option A from the pre-impl
|
|
776
|
+
council).
|
|
777
|
+
|
|
778
|
+
``_bus.emit_event`` is non-raising — no try/except wrapper needed here.
|
|
779
|
+
"""
|
|
780
|
+
phase = phase_dir.name
|
|
781
|
+
project_id = phase_dir.parents[1].name
|
|
782
|
+
pending_content = _REVIEWER_REPORT_PENDING.format(reviewed_at=_now_iso())
|
|
783
|
+
|
|
784
|
+
if _bus_as_truth_flag_on():
|
|
785
|
+
from _bus import emit_event # noqa: PLC0415 — lazy import per hook pattern
|
|
786
|
+
# raw_payload = full pending template; projector writes it only if
|
|
787
|
+
# no reviewer-report.md exists yet (no-clobber invariant lives in
|
|
788
|
+
# the projector handler).
|
|
789
|
+
emit_event(
|
|
790
|
+
"wicked.consensus.gate_pending",
|
|
791
|
+
{
|
|
792
|
+
"project_id": project_id,
|
|
793
|
+
"phase": phase,
|
|
794
|
+
"eval_id": eval_id,
|
|
795
|
+
"raw_payload": pending_content,
|
|
796
|
+
},
|
|
797
|
+
chain_id=f"{project_id}.{phase}.consensus.{eval_id}",
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _handle_bash_consensus(tool_input: dict, tool_response) -> dict:
|
|
802
|
+
"""Async consensus gate — fires after phase_manager.py approve on high-complexity projects.
|
|
803
|
+
|
|
804
|
+
FAST EARLY-EXIT: Returns immediately if the Bash command is not a
|
|
805
|
+
phase_manager.py approve call (the vast majority of Bash invocations).
|
|
806
|
+
|
|
807
|
+
On match:
|
|
808
|
+
1. Parse project name and phase from the command.
|
|
809
|
+
2. Load project state. If complexity < 5, skip.
|
|
810
|
+
3. Invoke consensus_gate.evaluate_consensus_gate() directly (this process is async).
|
|
811
|
+
4. Write result to phases/{phase}/reviewer-report.md.
|
|
812
|
+
5. If evaluation fails, write a pending report. Never block.
|
|
813
|
+
|
|
814
|
+
"""
|
|
815
|
+
# --- Fast early-exit: only act on phase_manager.py approve ---
|
|
816
|
+
command = (tool_input.get("command") or "")
|
|
817
|
+
if "phase_manager.py" not in command or "approve" not in command:
|
|
818
|
+
return {"continue": True}
|
|
819
|
+
|
|
820
|
+
# Verify the command contains both keywords in the right order
|
|
821
|
+
# (avoid false matches on e.g. "grep approve phase_manager.py")
|
|
822
|
+
pm_pos = command.find("phase_manager.py")
|
|
823
|
+
approve_pos = command.find("approve", pm_pos)
|
|
824
|
+
if pm_pos < 0 or approve_pos < 0:
|
|
825
|
+
return {"continue": True}
|
|
826
|
+
|
|
827
|
+
# --- Parse project name and phase from command ---
|
|
828
|
+
project_name, explicit_phase = _parse_project_phase_from_command(command)
|
|
829
|
+
if not project_name:
|
|
830
|
+
return {"continue": True}
|
|
831
|
+
|
|
832
|
+
# --- Load project state ---
|
|
833
|
+
project_state_dict, project_dir = _load_project_for_consensus(project_name)
|
|
834
|
+
if project_state_dict is None or project_dir is None:
|
|
835
|
+
return {"continue": True}
|
|
836
|
+
|
|
837
|
+
# --- Complexity gate: skip if complexity < 5 ---
|
|
838
|
+
try:
|
|
839
|
+
complexity = int(project_state_dict.get("complexity_score") or 0)
|
|
840
|
+
except (TypeError, ValueError):
|
|
841
|
+
complexity = 0
|
|
842
|
+
|
|
843
|
+
if complexity < 5:
|
|
844
|
+
return {"continue": True}
|
|
845
|
+
|
|
846
|
+
# --- Determine which phase was just approved ---
|
|
847
|
+
phase = _resolve_phase_for_consensus(project_state_dict, explicit_phase)
|
|
848
|
+
phase_dir = project_dir / "phases" / phase
|
|
849
|
+
|
|
850
|
+
# --- Load phases config and check if consensus is configured for this phase ---
|
|
851
|
+
phases_config = _load_phases_config()
|
|
852
|
+
phase_config = phases_config.get(phase, {})
|
|
853
|
+
proposers = phase_config.get("consensus_proposers", [])
|
|
854
|
+
consensus_threshold = phase_config.get("consensus_threshold")
|
|
855
|
+
|
|
856
|
+
# If phase has no consensus config, check for a generic threshold of 5
|
|
857
|
+
if not proposers:
|
|
858
|
+
# Default proposers for unconfigured phases at high complexity
|
|
859
|
+
proposers = ["engineering", "quality-engineering", "product"]
|
|
860
|
+
if consensus_threshold is None:
|
|
861
|
+
consensus_threshold = 5
|
|
862
|
+
|
|
863
|
+
# Final consensus threshold check
|
|
864
|
+
try:
|
|
865
|
+
if complexity < int(consensus_threshold):
|
|
866
|
+
return {"continue": True}
|
|
867
|
+
except (TypeError, ValueError):
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
# --- Mint eval_id ONCE at the entry point (Finding #2 fix) ---
|
|
871
|
+
# Per the bus-chain-id uniqueness gotcha, every emit in this invocation
|
|
872
|
+
# must carry a per-invocation token. Mint here — before any try block —
|
|
873
|
+
# so the success path, pending path, and exception path all reuse the
|
|
874
|
+
# same value. eval_id is the per-INVOCATION token, not a per-RESULT
|
|
875
|
+
# token; consensus_result.get("eval_id") is NOT preferred here.
|
|
876
|
+
# Width: uuid4().hex[:16] == 64 bits (matches Site 2's PR #762 widening).
|
|
877
|
+
eval_id: str = uuid.uuid4().hex[:16]
|
|
878
|
+
|
|
879
|
+
# --- Run consensus evaluation ---
|
|
880
|
+
try:
|
|
881
|
+
scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
|
|
882
|
+
if str(scripts_crew) not in sys.path:
|
|
883
|
+
sys.path.insert(0, str(scripts_crew))
|
|
884
|
+
|
|
885
|
+
import consensus_gate as _cg
|
|
886
|
+
|
|
887
|
+
# Inject proposers into phases_config if not present (default fallback)
|
|
888
|
+
if phase not in phases_config:
|
|
889
|
+
phases_config[phase] = {
|
|
890
|
+
"consensus_threshold": consensus_threshold,
|
|
891
|
+
"consensus_proposers": proposers,
|
|
892
|
+
"strong_dissent_blocks": True,
|
|
893
|
+
"confidence_threshold": 0.7,
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
consensus_result = _cg.evaluate_consensus_gate(
|
|
897
|
+
project_dir=str(project_dir),
|
|
898
|
+
phase=phase,
|
|
899
|
+
project_state=project_state_dict,
|
|
900
|
+
phases_config=phases_config,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
if consensus_result is None:
|
|
904
|
+
# No gate-result.json to evaluate — not an error, just nothing to do
|
|
905
|
+
return {"continue": True}
|
|
906
|
+
|
|
907
|
+
verdict = consensus_result.get("result", "pending").lower()
|
|
908
|
+
if verdict not in ("approved", "conditional", "rejected"):
|
|
909
|
+
# Map gate result codes to reviewer-report verdicts
|
|
910
|
+
verdict_map = {"approve": "approved", "reject": "rejected",
|
|
911
|
+
"conditional": "conditional"}
|
|
912
|
+
verdict = verdict_map.get(verdict, "pending")
|
|
913
|
+
|
|
914
|
+
_write_reviewer_report(phase_dir, verdict, consensus_result, eval_id)
|
|
915
|
+
_log("crew", "info", "consensus_gate.complete",
|
|
916
|
+
detail={"project": project_name, "phase": phase, "verdict": verdict,
|
|
917
|
+
"complexity": complexity})
|
|
918
|
+
|
|
919
|
+
except Exception as exc:
|
|
920
|
+
# Fail-open: write pending report, log error, never crash.
|
|
921
|
+
# Reuse the eval_id minted at the entry point — never mint a second
|
|
922
|
+
# UUID here. This ensures the exception-path chain_id is consistent
|
|
923
|
+
# with what the success path would have emitted.
|
|
924
|
+
try:
|
|
925
|
+
_write_pending_reviewer_report(phase_dir, eval_id)
|
|
926
|
+
_log("crew", "warn", "consensus_gate.error",
|
|
927
|
+
ok=False, detail={"project": project_name, "phase": phase,
|
|
928
|
+
"error": str(exc)[:200]})
|
|
929
|
+
except Exception:
|
|
930
|
+
pass
|
|
931
|
+
|
|
932
|
+
return {"continue": True}
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ---------------------------------------------------------------------------
|
|
936
|
+
# Handler: Bash (general activity tracking)
|
|
937
|
+
# ---------------------------------------------------------------------------
|
|
938
|
+
|
|
939
|
+
# Theme 9: word-boundary check for search-tool invocations. The discovery
|
|
940
|
+
# hint must NOT fire on benign references like ``echo "use grep instead"``
|
|
941
|
+
# or comments. A real invocation appears at start-of-line/command-start or
|
|
942
|
+
# after a shell separator (``;``, ``&&``, ``||``, ``|``, ``&``, newline).
|
|
943
|
+
#
|
|
944
|
+
# Precision rules:
|
|
945
|
+
# * Allow optional leading whitespace after ``^`` and after each separator,
|
|
946
|
+
# so ``" grep pattern"`` and ``"ls; grep foo"`` both match.
|
|
947
|
+
# * Use ``\b`` before each tool name — without it, ``"ls; tag"`` would
|
|
948
|
+
# false-match ``ag`` inside ``tag``.
|
|
949
|
+
# * Accept multi-char ``&&`` and ``||`` as well as single ``;`` ``|`` ``&``;
|
|
950
|
+
# ``&&`` must precede ``&`` (and ``||`` precede ``|``) in the alternation
|
|
951
|
+
# so the longer separator wins the leftmost-match.
|
|
952
|
+
_SEARCH_INVOCATION_RE = re.compile(
|
|
953
|
+
r"(?:^\s*|[\n;]\s*|&&\s*|\|\|\s*|[|&]\s*)\b(?:grep|rg|ripgrep|ag)\b",
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _handle_bash(tool_input: dict, tool_response) -> dict:
|
|
958
|
+
"""Track bash activity.
|
|
959
|
+
|
|
960
|
+
The claim sentinel used to ref-watch here on every Bash call; it moved to
|
|
961
|
+
the Stop hook where it is claim-gated (fires only when the turn's final
|
|
962
|
+
message asserts done/passing/shipped). See scripts/sentinel/invariants.py
|
|
963
|
+
::claim_tick and hooks/scripts/stop.py. Publish-time protection (an actual
|
|
964
|
+
push to main or a tag) remains the block tier in scripts/sentinel/pre_push.py.
|
|
965
|
+
"""
|
|
966
|
+
try:
|
|
967
|
+
from _session import SessionState
|
|
968
|
+
state = SessionState.load()
|
|
969
|
+
bash_count = (getattr(state, "bash_count", None) or 0) + 1
|
|
970
|
+
state.update(bash_count=bash_count)
|
|
971
|
+
state.save()
|
|
972
|
+
except Exception:
|
|
973
|
+
pass
|
|
974
|
+
return {"continue": True}
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
# ---------------------------------------------------------------------------
|
|
978
|
+
# Handler: PostToolUseFailure
|
|
979
|
+
# (failure counting + threshold alerts)
|
|
980
|
+
# ---------------------------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
_FAILURE_THRESHOLD_DEFAULT = 3
|
|
983
|
+
_MISMATCH_SIGNALS = [
|
|
984
|
+
"failed", "not working", "broken", "error", "couldn't", "unable", "blocked"
|
|
985
|
+
]
|
|
986
|
+
|
|
987
|
+
# Patterns that indicate a missing CLI or Python module
|
|
988
|
+
_MISSING_TOOL_RE = [
|
|
989
|
+
re.compile(r"command not found:\s*(\S+)"),
|
|
990
|
+
re.compile(r"not found:\s*(\S+)"),
|
|
991
|
+
]
|
|
992
|
+
_MISSING_MODULE_RE = [
|
|
993
|
+
re.compile(r"ModuleNotFoundError: No module named ['\"](\S+?)['\"]"),
|
|
994
|
+
re.compile(r"ImportError: No module named ['\"](\S+?)['\"]"),
|
|
995
|
+
]
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _detect_missing_tool(error_text: str) -> str | None:
|
|
999
|
+
"""Check if error indicates a missing tool/module. Returns a prereq-doctor hint or None."""
|
|
1000
|
+
if not error_text:
|
|
1001
|
+
return None
|
|
1002
|
+
try:
|
|
1003
|
+
plugin_root = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", ".")).resolve()
|
|
1004
|
+
doctor = plugin_root / "scripts" / "platform" / "prereq_doctor.py"
|
|
1005
|
+
if not doctor.exists():
|
|
1006
|
+
return None
|
|
1007
|
+
|
|
1008
|
+
import subprocess as _sp
|
|
1009
|
+
proc = _sp.run(
|
|
1010
|
+
[sys.executable, str(doctor), "diagnose", error_text],
|
|
1011
|
+
capture_output=True, text=True, timeout=5,
|
|
1012
|
+
)
|
|
1013
|
+
if proc.returncode != 0:
|
|
1014
|
+
return None
|
|
1015
|
+
diag = json.loads(proc.stdout)
|
|
1016
|
+
if diag.get("error_type") == "unknown":
|
|
1017
|
+
return None
|
|
1018
|
+
|
|
1019
|
+
# Build a concise hint for the model
|
|
1020
|
+
if diag.get("fix") == "uv_sync":
|
|
1021
|
+
return (
|
|
1022
|
+
f"[Prereq Doctor] Missing Python module '{diag.get('module')}'. "
|
|
1023
|
+
f"Fix: run `uv sync` from the plugin root. "
|
|
1024
|
+
f"Or use: Skill(skill=\"wicked-garden:platform:prereq-doctor\", args=\"check uv\")"
|
|
1025
|
+
)
|
|
1026
|
+
if diag.get("status") == "missing":
|
|
1027
|
+
install_cmd = diag.get("install_cmd", "")
|
|
1028
|
+
return (
|
|
1029
|
+
f"[Prereq Doctor] CLI '{diag.get('cli', diag.get('tool'))}' not installed. "
|
|
1030
|
+
f"Install with: `{install_cmd}` — "
|
|
1031
|
+
f"Ask the user before installing. "
|
|
1032
|
+
f"Or use: Skill(skill=\"wicked-garden:platform:prereq-doctor\", args=\"check {diag.get('tool')}\")"
|
|
1033
|
+
)
|
|
1034
|
+
return None
|
|
1035
|
+
except Exception:
|
|
1036
|
+
return None
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _handle_failure(payload: dict) -> dict:
|
|
1040
|
+
"""PostToolUseFailure: detect missing tools, count failures, queue issue at threshold."""
|
|
1041
|
+
tool_name = payload.get("tool_name", "unknown")
|
|
1042
|
+
tool_error = payload.get("tool_error", "") or payload.get("tool_use_error", "")
|
|
1043
|
+
|
|
1044
|
+
# --- Missing tool detection (fires immediately, no threshold) ---
|
|
1045
|
+
prereq_hint = _detect_missing_tool(str(tool_error))
|
|
1046
|
+
if prereq_hint:
|
|
1047
|
+
return {"continue": True, "systemMessage": prereq_hint}
|
|
1048
|
+
|
|
1049
|
+
# --- General failure counting (existing behavior) ---
|
|
1050
|
+
try:
|
|
1051
|
+
threshold = int(os.environ.get("WICKED_ISSUE_THRESHOLD", str(_FAILURE_THRESHOLD_DEFAULT)))
|
|
1052
|
+
except (ValueError, TypeError):
|
|
1053
|
+
threshold = _FAILURE_THRESHOLD_DEFAULT
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
from _session import SessionState
|
|
1057
|
+
state = SessionState.load()
|
|
1058
|
+
counts = state.failure_counts or {}
|
|
1059
|
+
counts[tool_name] = counts.get(tool_name, 0) + 1
|
|
1060
|
+
current_count = counts[tool_name]
|
|
1061
|
+
state.update(failure_counts=counts)
|
|
1062
|
+
|
|
1063
|
+
if current_count >= threshold:
|
|
1064
|
+
record = {
|
|
1065
|
+
"type": "tool_failure",
|
|
1066
|
+
"tool": tool_name,
|
|
1067
|
+
"count": current_count,
|
|
1068
|
+
"last_error": str(tool_error)[:500],
|
|
1069
|
+
"session_id": _get_session_id(),
|
|
1070
|
+
"ts": _now_iso(),
|
|
1071
|
+
}
|
|
1072
|
+
pending = state.pending_issues or []
|
|
1073
|
+
pending.append(record)
|
|
1074
|
+
counts[tool_name] = 0
|
|
1075
|
+
state.update(failure_counts=counts, pending_issues=pending)
|
|
1076
|
+
|
|
1077
|
+
return {
|
|
1078
|
+
"continue": True,
|
|
1079
|
+
"systemMessage": (
|
|
1080
|
+
f"[Issue Reporter] {current_count} {tool_name} failures — issue queued."
|
|
1081
|
+
),
|
|
1082
|
+
}
|
|
1083
|
+
except Exception:
|
|
1084
|
+
pass
|
|
1085
|
+
|
|
1086
|
+
return {"continue": True}
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def _handle_task_update_mismatch(tool_input: dict) -> dict:
|
|
1090
|
+
"""Detect task completion mismatch signals (task marked done but looks blocked)."""
|
|
1091
|
+
if tool_input.get("status") != "completed":
|
|
1092
|
+
return {"continue": True}
|
|
1093
|
+
|
|
1094
|
+
subject = tool_input.get("subject", "") or ""
|
|
1095
|
+
description = tool_input.get("description", "") or ""
|
|
1096
|
+
task_id = tool_input.get("taskId", "") or ""
|
|
1097
|
+
|
|
1098
|
+
combined = (subject + " " + description).lower()
|
|
1099
|
+
found_signal = next((s for s in _MISMATCH_SIGNALS if s in combined), None)
|
|
1100
|
+
|
|
1101
|
+
if found_signal:
|
|
1102
|
+
record = {
|
|
1103
|
+
"type": "task_mismatch",
|
|
1104
|
+
"task_id": task_id,
|
|
1105
|
+
"subject": subject[:200],
|
|
1106
|
+
"signal": found_signal,
|
|
1107
|
+
"detail": description[:300],
|
|
1108
|
+
"ts": _now_iso(),
|
|
1109
|
+
}
|
|
1110
|
+
try:
|
|
1111
|
+
from _session import SessionState
|
|
1112
|
+
state = SessionState.load()
|
|
1113
|
+
pending = state.pending_issues or []
|
|
1114
|
+
pending.append(record)
|
|
1115
|
+
state.update(pending_issues=pending)
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1118
|
+
|
|
1119
|
+
return {"continue": True}
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
# ---------------------------------------------------------------------------
|
|
1123
|
+
# Observability trace writer
|
|
1124
|
+
# ---------------------------------------------------------------------------
|
|
1125
|
+
|
|
1126
|
+
def _write_trace(payload: dict) -> None:
|
|
1127
|
+
"""Write a trace entry to a session-scoped JSONL file in $TMPDIR."""
|
|
1128
|
+
if os.environ.get("WICKED_TRACE_ACTIVE"):
|
|
1129
|
+
return
|
|
1130
|
+
try:
|
|
1131
|
+
os.environ["WICKED_TRACE_ACTIVE"] = "1"
|
|
1132
|
+
session_id = _get_session_id()
|
|
1133
|
+
tool_name = payload.get("tool_name", "")
|
|
1134
|
+
event = payload.get("hook_event_name", "PostToolUse")
|
|
1135
|
+
ts = _now_iso()
|
|
1136
|
+
|
|
1137
|
+
entry = {
|
|
1138
|
+
"ts": ts,
|
|
1139
|
+
"session_id": session_id,
|
|
1140
|
+
"tool": tool_name,
|
|
1141
|
+
"event": event,
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
tmpdir = os.environ.get("TMPDIR") or __import__("tempfile").gettempdir()
|
|
1145
|
+
trace_file = Path(tmpdir) / f"wicked-trace-{session_id}.jsonl"
|
|
1146
|
+
with open(trace_file, "a", encoding="utf-8") as f:
|
|
1147
|
+
f.write(json.dumps(entry) + "\n")
|
|
1148
|
+
except Exception:
|
|
1149
|
+
pass
|
|
1150
|
+
finally:
|
|
1151
|
+
os.environ.pop("WICKED_TRACE_ACTIVE", None)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
# ---------------------------------------------------------------------------
|
|
1155
|
+
# Turn progress visibility (Issue #323)
|
|
1156
|
+
# ---------------------------------------------------------------------------
|
|
1157
|
+
|
|
1158
|
+
_TURN_STATUS_THRESHOLD_SECS = 120 # 2 minutes
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _check_turn_progress(tool_name: str) -> str | None:
|
|
1162
|
+
"""Increment turn tool count and return a status note if the turn is long-running.
|
|
1163
|
+
|
|
1164
|
+
Returns a status string when elapsed time exceeds _TURN_STATUS_THRESHOLD_SECS,
|
|
1165
|
+
or None if the turn is still short, state is unavailable, or the [Status]
|
|
1166
|
+
banner has already fired this turn (v9.2.12 per-turn latch).
|
|
1167
|
+
|
|
1168
|
+
v9.2.12: Pre-fix, the [Status] banner re-fired on every PostToolUse once
|
|
1169
|
+
the 2-minute threshold tripped — meaning every long turn surfaced N
|
|
1170
|
+
redundant "running for X min, Y tool calls so far" banners (one per
|
|
1171
|
+
subsequent tool call). The `last_status_turn` latch (declared on
|
|
1172
|
+
SessionState, written here) suppresses re-emission within the same turn.
|
|
1173
|
+
A new turn (turn_count incremented by prompt_submit) re-arms the banner.
|
|
1174
|
+
"""
|
|
1175
|
+
try:
|
|
1176
|
+
from _session import SessionState
|
|
1177
|
+
state = SessionState.load()
|
|
1178
|
+
|
|
1179
|
+
# Increment tool count
|
|
1180
|
+
new_count = (state.turn_tool_count or 0) + 1
|
|
1181
|
+
state.update(turn_tool_count=new_count)
|
|
1182
|
+
|
|
1183
|
+
# Check elapsed time
|
|
1184
|
+
turn_start = state.turn_start_ts
|
|
1185
|
+
if not turn_start:
|
|
1186
|
+
return None
|
|
1187
|
+
|
|
1188
|
+
start_dt = datetime.fromisoformat(turn_start.replace("Z", "+00:00"))
|
|
1189
|
+
now_dt = datetime.now(timezone.utc)
|
|
1190
|
+
elapsed_secs = (now_dt - start_dt).total_seconds()
|
|
1191
|
+
|
|
1192
|
+
if elapsed_secs < _TURN_STATUS_THRESHOLD_SECS:
|
|
1193
|
+
return None
|
|
1194
|
+
|
|
1195
|
+
# Per-turn latch (v9.2.12): suppress re-emission once shown this turn.
|
|
1196
|
+
current_turn = state.turn_count or 0
|
|
1197
|
+
if current_turn and current_turn == (state.last_status_turn or 0):
|
|
1198
|
+
return None
|
|
1199
|
+
state.update(last_status_turn=current_turn)
|
|
1200
|
+
|
|
1201
|
+
elapsed_mins = int(elapsed_secs / 60)
|
|
1202
|
+
|
|
1203
|
+
# Build status note
|
|
1204
|
+
parts = [
|
|
1205
|
+
f"[Status] This turn has been running for {elapsed_mins} "
|
|
1206
|
+
f"minute{'s' if elapsed_mins != 1 else ''}. "
|
|
1207
|
+
f"{new_count} tool call{'s' if new_count != 1 else ''} so far."
|
|
1208
|
+
]
|
|
1209
|
+
|
|
1210
|
+
# Include active crew phase/specialist if available
|
|
1211
|
+
active_project = state.active_project
|
|
1212
|
+
if active_project and isinstance(active_project, dict):
|
|
1213
|
+
phase = active_project.get("current_phase", "")
|
|
1214
|
+
specialist = active_project.get("current_specialist", "")
|
|
1215
|
+
if phase:
|
|
1216
|
+
detail = f"Active crew phase: {phase}"
|
|
1217
|
+
if specialist:
|
|
1218
|
+
detail += f" ({specialist})"
|
|
1219
|
+
parts.append(detail)
|
|
1220
|
+
|
|
1221
|
+
return " ".join(parts)
|
|
1222
|
+
except Exception:
|
|
1223
|
+
return None
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
# ---------------------------------------------------------------------------
|
|
1227
|
+
# Latency profiling (Issue #312) — cumulative PostToolUse timing in SessionState
|
|
1228
|
+
# ---------------------------------------------------------------------------
|
|
1229
|
+
|
|
1230
|
+
def _record_latency(handler_label: str, handler_ms: int, total_ms: int) -> None:
|
|
1231
|
+
"""Accumulate PostToolUse latency in SessionState for profiling.
|
|
1232
|
+
|
|
1233
|
+
Updates three fields:
|
|
1234
|
+
- post_tool_total_ms: cumulative wall-clock time across all invocations
|
|
1235
|
+
- post_tool_call_count: total invocation count
|
|
1236
|
+
- post_tool_handler_ms: per-handler cumulative ms (keyed by handler_label)
|
|
1237
|
+
|
|
1238
|
+
Fails silently — never crashes the hook.
|
|
1239
|
+
"""
|
|
1240
|
+
try:
|
|
1241
|
+
from _session import SessionState
|
|
1242
|
+
state = SessionState.load()
|
|
1243
|
+
state.post_tool_total_ms = (state.post_tool_total_ms or 0) + total_ms
|
|
1244
|
+
state.post_tool_call_count = (state.post_tool_call_count or 0) + 1
|
|
1245
|
+
handler_breakdown = state.post_tool_handler_ms or {}
|
|
1246
|
+
handler_breakdown[handler_label] = handler_breakdown.get(handler_label, 0) + handler_ms
|
|
1247
|
+
state.post_tool_handler_ms = handler_breakdown
|
|
1248
|
+
state.save()
|
|
1249
|
+
except Exception:
|
|
1250
|
+
pass
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
# ---------------------------------------------------------------------------
|
|
1254
|
+
# Main dispatcher
|
|
1255
|
+
# ---------------------------------------------------------------------------
|
|
1256
|
+
|
|
1257
|
+
def main():
|
|
1258
|
+
_t0 = time.monotonic()
|
|
1259
|
+
|
|
1260
|
+
try:
|
|
1261
|
+
raw = sys.stdin.read()
|
|
1262
|
+
payload = json.loads(raw) if raw.strip() else {}
|
|
1263
|
+
except Exception:
|
|
1264
|
+
print(json.dumps({"continue": True}))
|
|
1265
|
+
return
|
|
1266
|
+
|
|
1267
|
+
try:
|
|
1268
|
+
tool_name = payload.get("tool_name", "")
|
|
1269
|
+
tool_input = payload.get("tool_input", {}) or {}
|
|
1270
|
+
has_error = "tool_error" in payload or "tool_use_error" in payload
|
|
1271
|
+
|
|
1272
|
+
_log("posttool", "debug", "hook.start", detail={"tool": tool_name})
|
|
1273
|
+
|
|
1274
|
+
# Write observability trace (low-priority, always runs)
|
|
1275
|
+
_t_trace = time.monotonic()
|
|
1276
|
+
_write_trace(payload)
|
|
1277
|
+
_trace_ms = int((time.monotonic() - _t_trace) * 1000)
|
|
1278
|
+
|
|
1279
|
+
# --- Route by event type and tool name ---
|
|
1280
|
+
handler_label = tool_name or "unknown"
|
|
1281
|
+
_t_handler = time.monotonic()
|
|
1282
|
+
|
|
1283
|
+
# PostToolUseFailure path
|
|
1284
|
+
if has_error:
|
|
1285
|
+
handler_label = "failure"
|
|
1286
|
+
result = _handle_failure(payload)
|
|
1287
|
+
# Task-management tools — native task store is the source of truth.
|
|
1288
|
+
# PreToolUse validates metadata; PostToolUse only runs the mismatch
|
|
1289
|
+
# detector for TaskUpdate (status/subject/metadata consistency).
|
|
1290
|
+
# v10 Phase 3 (#813 successor): also append a cross-session audit
|
|
1291
|
+
# entry so verify_chain_emission can find tasks created in earlier
|
|
1292
|
+
# sessions. The writer fails-open — any I/O error swallowed.
|
|
1293
|
+
elif tool_name in ("TaskCreate", "TaskUpdate", "TodoWrite"):
|
|
1294
|
+
handler_label = "TaskCreate|TaskUpdate|TodoWrite"
|
|
1295
|
+
if tool_name == "TaskUpdate":
|
|
1296
|
+
_handle_task_update_mismatch(tool_input)
|
|
1297
|
+
if tool_name in ("TaskCreate", "TaskUpdate"):
|
|
1298
|
+
try:
|
|
1299
|
+
from crew._task_audit_writer import append_task_audit
|
|
1300
|
+
append_task_audit(
|
|
1301
|
+
tool_name=tool_name,
|
|
1302
|
+
tool_input=tool_input,
|
|
1303
|
+
tool_response=payload.get("tool_response"),
|
|
1304
|
+
session_id=_get_session_id(),
|
|
1305
|
+
)
|
|
1306
|
+
except Exception:
|
|
1307
|
+
pass # fail-open per Phase 3 contract
|
|
1308
|
+
result = {"continue": True}
|
|
1309
|
+
# Write / Edit tools (async — quick operations only)
|
|
1310
|
+
elif tool_name in ("Write", "Edit"):
|
|
1311
|
+
handler_label = "Write|Edit"
|
|
1312
|
+
result = _handle_write_edit(tool_input)
|
|
1313
|
+
# Task (subagent dispatch + permission failure detection)
|
|
1314
|
+
elif tool_name == "Task":
|
|
1315
|
+
tool_response = payload.get("tool_response", {})
|
|
1316
|
+
result = _handle_task_dispatch(tool_input, tool_response)
|
|
1317
|
+
# Read (large-file warning + framework detection)
|
|
1318
|
+
elif tool_name == "Read":
|
|
1319
|
+
tool_response = payload.get("tool_response", "")
|
|
1320
|
+
result = _handle_read(tool_input, tool_response)
|
|
1321
|
+
# Bash (activity tracking + discovery hints + async consensus gate)
|
|
1322
|
+
elif tool_name == "Bash":
|
|
1323
|
+
tool_response = payload.get("tool_response", {})
|
|
1324
|
+
result = _handle_bash(tool_input, tool_response)
|
|
1325
|
+
# Tier 2 consensus gate (Issue #368) — fast-exits unless phase_manager approve
|
|
1326
|
+
_handle_bash_consensus(tool_input, tool_response)
|
|
1327
|
+
# Skill (memory compliance counter reset + pull-model tracking)
|
|
1328
|
+
elif tool_name == "Skill":
|
|
1329
|
+
result = _handle_skill(tool_input)
|
|
1330
|
+
# Grep / Glob (discovery hints for search commands)
|
|
1331
|
+
elif tool_name in ("Grep", "Glob"):
|
|
1332
|
+
handler_label = "Grep|Glob"
|
|
1333
|
+
result = _handle_grep_glob(tool_name, tool_input)
|
|
1334
|
+
# All other tools — pass through
|
|
1335
|
+
else:
|
|
1336
|
+
handler_label = "passthrough"
|
|
1337
|
+
result = {"continue": True}
|
|
1338
|
+
|
|
1339
|
+
_handler_ms = int((time.monotonic() - _t_handler) * 1000)
|
|
1340
|
+
|
|
1341
|
+
# Turn progress visibility (Issue #323): append status note on long turns
|
|
1342
|
+
_t_turn = time.monotonic()
|
|
1343
|
+
turn_status = _check_turn_progress(tool_name)
|
|
1344
|
+
_turn_ms = int((time.monotonic() - _t_turn) * 1000)
|
|
1345
|
+
if turn_status:
|
|
1346
|
+
existing_msg = result.get("systemMessage", "")
|
|
1347
|
+
if existing_msg:
|
|
1348
|
+
result["systemMessage"] = existing_msg + "\n" + turn_status
|
|
1349
|
+
else:
|
|
1350
|
+
result["systemMessage"] = turn_status
|
|
1351
|
+
|
|
1352
|
+
# --- Latency profiling (Issue #312) ---
|
|
1353
|
+
_total_ms = int((time.monotonic() - _t0) * 1000)
|
|
1354
|
+
_record_latency(handler_label, _handler_ms, _total_ms)
|
|
1355
|
+
|
|
1356
|
+
_log("posttool", "verbose", "hook.latency", ms=_total_ms, detail={
|
|
1357
|
+
"tool": tool_name,
|
|
1358
|
+
"handler": handler_label,
|
|
1359
|
+
"handler_ms": _handler_ms,
|
|
1360
|
+
"trace_ms": _trace_ms,
|
|
1361
|
+
"turn_progress_ms": _turn_ms,
|
|
1362
|
+
"total_ms": _total_ms,
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
_log("posttool", "debug", "hook.end", ms=_total_ms)
|
|
1366
|
+
print(json.dumps(result))
|
|
1367
|
+
|
|
1368
|
+
except Exception as e:
|
|
1369
|
+
print(f"[wicked-garden] post_tool error: {e}", file=sys.stderr)
|
|
1370
|
+
print(json.dumps({"continue": True}))
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
if __name__ == "__main__":
|
|
1374
|
+
main()
|