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.
Files changed (640) hide show
  1. package/.claude-plugin/archetypes.json +166 -0
  2. package/.claude-plugin/components.json +75 -0
  3. package/.claude-plugin/marketplace.json +15 -0
  4. package/.claude-plugin/plugin.json +174 -0
  5. package/.claude-plugin/specialist.json +48 -0
  6. package/.claude-plugin/stack-registry.json +96 -0
  7. package/AGENTS.md +13 -0
  8. package/CHANGELOG.md +712 -0
  9. package/CONTRIBUTING.md +99 -0
  10. package/ETHOS.md +83 -0
  11. package/LICENSE +21 -0
  12. package/README.md +115 -0
  13. package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
  14. package/agents/agentic/architect.md +449 -0
  15. package/agents/agentic/performance-analyst.md +702 -0
  16. package/agents/agentic/safety-reviewer.md +662 -0
  17. package/agents/crew/implementer.md +163 -0
  18. package/agents/crew/researcher.md +94 -0
  19. package/agents/crew/reviewer.md +184 -0
  20. package/agents/data/data-engineer.md +227 -0
  21. package/agents/engineering/api-documentarian.md +440 -0
  22. package/agents/engineering/migration-engineer.md +301 -0
  23. package/agents/engineering/solution-architect.md +208 -0
  24. package/agents/jam/brainstorm-facilitator.md +297 -0
  25. package/agents/jam/council.md +322 -0
  26. package/agents/jam/quick-facilitator.md +85 -0
  27. package/agents/persona/persona-agent.md +121 -0
  28. package/agents/platform/compliance-officer.md +266 -0
  29. package/agents/platform/privacy-expert.md +496 -0
  30. package/agents/platform/security-engineer.md +287 -0
  31. package/agents/product/a11y-expert.md +323 -0
  32. package/agents/product/requirements-analyst.md +238 -0
  33. package/agents/product/ui-reviewer.md +180 -0
  34. package/agents/product/ux-designer.md +199 -0
  35. package/agents/product/value-strategist.md +269 -0
  36. package/agents/qe/semantic-reviewer.md +78 -0
  37. package/commands/agentic/audit.md +24 -0
  38. package/commands/agentic/design.md +24 -0
  39. package/commands/agentic/frameworks.md +25 -0
  40. package/commands/agentic/review.md +27 -0
  41. package/commands/archetype/build.md +12 -0
  42. package/commands/archetype/decide.md +12 -0
  43. package/commands/archetype/explore.md +12 -0
  44. package/commands/archetype/incident.md +12 -0
  45. package/commands/archetype/migrate.md +12 -0
  46. package/commands/archetype/modernize.md +12 -0
  47. package/commands/archetype/review.md +14 -0
  48. package/commands/archetype/ship.md +12 -0
  49. package/commands/archetype/specify.md +12 -0
  50. package/commands/archetype/triage.md +12 -0
  51. package/commands/compile.md +37 -0
  52. package/commands/data/analyze.md +18 -0
  53. package/commands/data/data.md +21 -0
  54. package/commands/data/ml.md +18 -0
  55. package/commands/data/ontology.md +22 -0
  56. package/commands/data/pipeline.md +17 -0
  57. package/commands/deliberate.md +181 -0
  58. package/commands/engineering/add-field.md +77 -0
  59. package/commands/engineering/apply.md +58 -0
  60. package/commands/engineering/arch.md +26 -0
  61. package/commands/engineering/debug.md +27 -0
  62. package/commands/engineering/docs.md +27 -0
  63. package/commands/engineering/new-generator.md +257 -0
  64. package/commands/engineering/patch-plan.md +59 -0
  65. package/commands/engineering/plan.md +29 -0
  66. package/commands/engineering/remove.md +48 -0
  67. package/commands/engineering/rename.md +44 -0
  68. package/commands/engineering/review.md +32 -0
  69. package/commands/help.md +128 -0
  70. package/commands/install.md +118 -0
  71. package/commands/intent.md +54 -0
  72. package/commands/jam/brainstorm.md +37 -0
  73. package/commands/jam/council.md +49 -0
  74. package/commands/jam/quick.md +21 -0
  75. package/commands/jam/revisit.md +79 -0
  76. package/commands/persona/as.md +136 -0
  77. package/commands/persona/define.md +24 -0
  78. package/commands/persona/list.md +118 -0
  79. package/commands/platform/actions.md +20 -0
  80. package/commands/platform/assert.md +18 -0
  81. package/commands/platform/audit.md +23 -0
  82. package/commands/platform/compliance.md +20 -0
  83. package/commands/platform/gh.md +22 -0
  84. package/commands/platform/health.md +22 -0
  85. package/commands/platform/incident.md +23 -0
  86. package/commands/platform/infra.md +24 -0
  87. package/commands/platform/peer-health.md +103 -0
  88. package/commands/platform/plugin-health.md +58 -0
  89. package/commands/platform/security.md +38 -0
  90. package/commands/platform/toolchain.md +96 -0
  91. package/commands/platform/traces.md +20 -0
  92. package/commands/product/a11y.md +19 -0
  93. package/commands/product/acceptance.md +19 -0
  94. package/commands/product/align.md +20 -0
  95. package/commands/product/analyze.md +23 -0
  96. package/commands/product/elicit.md +19 -0
  97. package/commands/product/listen.md +24 -0
  98. package/commands/product/mockup.md +20 -0
  99. package/commands/product/screenshot.md +19 -0
  100. package/commands/product/strategy.md +40 -0
  101. package/commands/product/synthesize.md +23 -0
  102. package/commands/product/ux-review.md +42 -0
  103. package/commands/product/ux.md +20 -0
  104. package/commands/product/visual-direction.md +34 -0
  105. package/commands/prove.md +47 -0
  106. package/commands/report-issue.md +243 -0
  107. package/commands/reset.md +144 -0
  108. package/commands/search/blast-radius.md +58 -0
  109. package/commands/search/hotspots.md +55 -0
  110. package/commands/search/index.md +38 -0
  111. package/commands/search/lineage.md +47 -0
  112. package/commands/search/service-map.md +79 -0
  113. package/commands/setup.md +363 -0
  114. package/commands/smaht/briefing.md +200 -0
  115. package/commands/smaht/events-import.md +95 -0
  116. package/commands/smaht/state.md +19 -0
  117. package/commands/where-am-i.md +58 -0
  118. package/docs/README.md +32 -0
  119. package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
  120. package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
  121. package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
  122. package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
  123. package/docs/brain-chunk-format.md +135 -0
  124. package/docs/compiler.md +95 -0
  125. package/docs/daemon-api.md +70 -0
  126. package/docs/domains.md +197 -0
  127. package/docs/getting-started.md +208 -0
  128. package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
  129. package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
  130. package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
  131. package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
  132. package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
  133. package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
  134. package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
  135. package/docs/required-peers.md +60 -0
  136. package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
  137. package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
  138. package/docs/v11/archetypes.md +231 -0
  139. package/hooks/hooks.json +275 -0
  140. package/hooks/scripts/bootstrap.py +1797 -0
  141. package/hooks/scripts/invoke.py +62 -0
  142. package/hooks/scripts/notification.py +166 -0
  143. package/hooks/scripts/permission_request.py +223 -0
  144. package/hooks/scripts/post_tool.py +1374 -0
  145. package/hooks/scripts/pre_compact.py +272 -0
  146. package/hooks/scripts/pre_tool.py +1278 -0
  147. package/hooks/scripts/prompt_submit.py +1109 -0
  148. package/hooks/scripts/session_end.py +95 -0
  149. package/hooks/scripts/stop.py +624 -0
  150. package/hooks/scripts/subagent_lifecycle.py +464 -0
  151. package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
  152. package/hooks/scripts/task_completed.py +322 -0
  153. package/hooks/scripts/worktree_guard.py +148 -0
  154. package/hooks/subscriptions/on_gate_decided.json +9 -0
  155. package/install.mjs +128 -0
  156. package/package.json +41 -0
  157. package/pyproject.toml +59 -0
  158. package/schemas/README.md +32 -0
  159. package/schemas/assert_contracts.json +37 -0
  160. package/schemas/health_probe.json +46 -0
  161. package/schemas/plugin_status.json +37 -0
  162. package/scripts/_adapters/__init__.py +166 -0
  163. package/scripts/_adapters/jira_adapter.py +18 -0
  164. package/scripts/_adapters/linear_adapter.py +18 -0
  165. package/scripts/_adapters/miro_adapter.py +18 -0
  166. package/scripts/_adapters/notion_adapter.py +18 -0
  167. package/scripts/_agents.py +357 -0
  168. package/scripts/_brain_ingest/__init__.py +2 -0
  169. package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
  170. package/scripts/_brain_port.py +163 -0
  171. package/scripts/_bus.py +1081 -0
  172. package/scripts/_bus_catalog_gen.py +93 -0
  173. package/scripts/_bus_consumers.json +46 -0
  174. package/scripts/_bus_emit.py +43 -0
  175. package/scripts/_capability_registry.py +334 -0
  176. package/scripts/_capability_resolver.py +322 -0
  177. package/scripts/_domain_store.py +559 -0
  178. package/scripts/_event_log_reader.py +154 -0
  179. package/scripts/_event_schema.py +305 -0
  180. package/scripts/_event_store.py +397 -0
  181. package/scripts/_heavy_cadence.py +324 -0
  182. package/scripts/_integration_resolver.py +321 -0
  183. package/scripts/_logger.py +151 -0
  184. package/scripts/_loom.py +482 -0
  185. package/scripts/_paths.py +133 -0
  186. package/scripts/_python.sh +22 -0
  187. package/scripts/_run.py +137 -0
  188. package/scripts/_session.py +502 -0
  189. package/scripts/_validate_registry.py +1042 -0
  190. package/scripts/_wicked_testing_probe.py +277 -0
  191. package/scripts/agentic/analyze_agents.py +391 -0
  192. package/scripts/agentic/detect_framework.py +420 -0
  193. package/scripts/agentic/issue_taxonomy.py +357 -0
  194. package/scripts/agentic/pattern_scorer.py +434 -0
  195. package/scripts/classify/persist.py +169 -0
  196. package/scripts/compiler/compile.py +552 -0
  197. package/scripts/compiler/phase0/VERDICT.md +60 -0
  198. package/scripts/compiler/phase0/detect.py +292 -0
  199. package/scripts/compiler/phase0/emit.py +228 -0
  200. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
  201. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
  202. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
  203. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
  204. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
  205. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
  206. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
  207. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
  208. package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
  209. package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
  210. package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
  211. package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
  212. package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
  213. package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
  214. package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
  215. package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
  216. package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
  217. package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
  218. package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
  219. package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
  220. package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
  221. package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
  222. package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
  223. package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
  224. package/scripts/compiler/phase0/evidence/verdict.json +104 -0
  225. package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
  226. package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
  227. package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
  228. package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
  229. package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
  230. package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
  231. package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
  232. package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
  233. package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
  234. package/scripts/compiler/phase0/run_probe.py +234 -0
  235. package/scripts/compiler/phase0/wire_vault.py +310 -0
  236. package/scripts/crew/__init__.py +0 -0
  237. package/scripts/crew/_task_audit_writer.py +243 -0
  238. package/scripts/crew/_task_reader.py +389 -0
  239. package/scripts/crew/archetypes_v11.py +348 -0
  240. package/scripts/crew/detectors/__init__.py +12 -0
  241. package/scripts/crew/flow_compiler.py +121 -0
  242. package/scripts/crew/phase_manager.py +703 -0
  243. package/scripts/crew/scope_delta.py +264 -0
  244. package/scripts/crew/specialist_resolver.py +218 -0
  245. package/scripts/crew/stack_registry.py +189 -0
  246. package/scripts/data/data_profiler.py +271 -0
  247. package/scripts/data/ontology_recommender.py +231 -0
  248. package/scripts/data/schema_validator.py +267 -0
  249. package/scripts/delivery/drift.py +739 -0
  250. package/scripts/delivery/telemetry.py +614 -0
  251. package/scripts/engineering/patch/codegraph_db.py +125 -0
  252. package/scripts/engineering/patch/generators/__init__.py +56 -0
  253. package/scripts/engineering/patch/generators/base.py +373 -0
  254. package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
  255. package/scripts/engineering/patch/generators/go_generator.py +430 -0
  256. package/scripts/engineering/patch/generators/java_generator.py +928 -0
  257. package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
  258. package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
  259. package/scripts/engineering/patch/generators/perl_generator.py +510 -0
  260. package/scripts/engineering/patch/generators/php_generator.py +644 -0
  261. package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
  262. package/scripts/engineering/patch/generators/python_generator.py +487 -0
  263. package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
  264. package/scripts/engineering/patch/generators/rust_generator.py +400 -0
  265. package/scripts/engineering/patch/generators/sql_generator.py +594 -0
  266. package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
  267. package/scripts/engineering/patch/patch.py +800 -0
  268. package/scripts/engineering/patch/safety.py +429 -0
  269. package/scripts/jam/__init__.py +0 -0
  270. package/scripts/jam/_bus_consumers.py +271 -0
  271. package/scripts/jam/agentic_cli_registry.py +703 -0
  272. package/scripts/jam/consensus.py +737 -0
  273. package/scripts/jam/detect_clis.py +409 -0
  274. package/scripts/jam/jam.py +283 -0
  275. package/scripts/jam/save_transcript.py +171 -0
  276. package/scripts/loom/__init__.py +22 -0
  277. package/scripts/loom/cli.py +170 -0
  278. package/scripts/loom/compose.py +180 -0
  279. package/scripts/loom/gate.py +120 -0
  280. package/scripts/loom/manifest.py +140 -0
  281. package/scripts/loom/resolve.py +59 -0
  282. package/scripts/persona/__init__.py +0 -0
  283. package/scripts/persona/registry.py +688 -0
  284. package/scripts/platform/guard_pipeline.py +1214 -0
  285. package/scripts/platform/guard_profiles.py +216 -0
  286. package/scripts/platform/observability/assert_contracts.py +434 -0
  287. package/scripts/platform/observability/health_probe.py +915 -0
  288. package/scripts/platform/observability/ops_log_viewer.py +209 -0
  289. package/scripts/platform/prereq_doctor.py +567 -0
  290. package/scripts/qe/conditions_manifest.py +260 -0
  291. package/scripts/qe/content_sanitizer.py +160 -0
  292. package/scripts/qe/coverage_tracker.py +535 -0
  293. package/scripts/qe/discover_scenarios.py +243 -0
  294. package/scripts/qe/evidence_tracker.py +232 -0
  295. package/scripts/qe/prove.py +196 -0
  296. package/scripts/qe/registry_lookup.py +80 -0
  297. package/scripts/qe/registry_store.py +176 -0
  298. package/scripts/qe/semantic_review.py +1005 -0
  299. package/scripts/qe/vault_gate.py +382 -0
  300. package/scripts/qe/verdict_audit.py +175 -0
  301. package/scripts/qe/verdict_schema.py +335 -0
  302. package/scripts/reset.py +220 -0
  303. package/scripts/resolve_path.py +22 -0
  304. package/scripts/sentinel/invariants.py +402 -0
  305. package/scripts/sentinel/pre_push.py +73 -0
  306. package/scripts/setup/__init__.py +1 -0
  307. package/scripts/setup/detect_state.py +114 -0
  308. package/scripts/setup/migrate_legacy.py +67 -0
  309. package/scripts/setup/migrate_v6_projects.py +232 -0
  310. package/scripts/setup/onboarding.py +114 -0
  311. package/scripts/smaht/_bus_consumers.py +98 -0
  312. package/scripts/smaht/adapters/__init__.py +83 -0
  313. package/scripts/smaht/adapters/brain_adapter.py +254 -0
  314. package/scripts/smaht/adapters/context7_adapter.py +438 -0
  315. package/scripts/smaht/adapters/delegation_adapter.py +104 -0
  316. package/scripts/smaht/adapters/domain_adapter.py +206 -0
  317. package/scripts/smaht/adapters/events_adapter.py +231 -0
  318. package/scripts/smaht/adapters/tools_adapter.py +49 -0
  319. package/scripts/smaht/cheatsheet_store.py +236 -0
  320. package/scripts/smaht/context_package.py +386 -0
  321. package/scripts/smaht/knowledge_graph.py +395 -0
  322. package/scripts/smaht/propose_skills.py +856 -0
  323. package/scripts/smaht/test_cheatsheet_store.py +428 -0
  324. package/scripts/statusline.py +151 -0
  325. package/scripts/where_am_i.py +236 -0
  326. package/skills/agentic/agentic-patterns/SKILL.md +91 -0
  327. package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
  328. package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
  329. package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
  330. package/skills/agentic/context-engineering/SKILL.md +142 -0
  331. package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
  332. package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
  333. package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
  334. package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
  335. package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
  336. package/skills/agentic/frameworks/SKILL.md +191 -0
  337. package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
  338. package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
  339. package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
  340. package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
  341. package/skills/agentic/refs/audit.md +134 -0
  342. package/skills/agentic/refs/design.md +137 -0
  343. package/skills/agentic/refs/frameworks.md +102 -0
  344. package/skills/agentic/refs/review.md +160 -0
  345. package/skills/agentic/review-methodology/SKILL.md +186 -0
  346. package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
  347. package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
  348. package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
  349. package/skills/agentic/trust-and-safety/SKILL.md +177 -0
  350. package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
  351. package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
  352. package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
  353. package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
  354. package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
  355. package/skills/archetype/SKILL.md +104 -0
  356. package/skills/archetype/refs/build.md +142 -0
  357. package/skills/archetype/refs/decide.md +117 -0
  358. package/skills/archetype/refs/explore.md +66 -0
  359. package/skills/archetype/refs/incident.md +149 -0
  360. package/skills/archetype/refs/migrate.md +170 -0
  361. package/skills/archetype/refs/modernize.md +187 -0
  362. package/skills/archetype/refs/review.md +145 -0
  363. package/skills/archetype/refs/ship.md +117 -0
  364. package/skills/archetype/refs/specify.md +108 -0
  365. package/skills/archetype/refs/triage.md +62 -0
  366. package/skills/classify/SKILL.md +157 -0
  367. package/skills/data/analysis/SKILL.md +125 -0
  368. package/skills/data/analysis/refs/templates.md +170 -0
  369. package/skills/data/data/SKILL.md +118 -0
  370. package/skills/data/data/refs/examples.md +235 -0
  371. package/skills/data/ml/SKILL.md +125 -0
  372. package/skills/data/ml/refs/templates.md +172 -0
  373. package/skills/data/pipeline/SKILL.md +136 -0
  374. package/skills/data/pipeline/refs/templates.md +175 -0
  375. package/skills/data/refs/analyze.md +123 -0
  376. package/skills/data/refs/data.md +95 -0
  377. package/skills/data/refs/ml.md +118 -0
  378. package/skills/data/refs/pipeline.md +92 -0
  379. package/skills/deliberate/SKILL.md +125 -0
  380. package/skills/deliberate/refs/architecture-lens.md +37 -0
  381. package/skills/deliberate/refs/assumptions-lens.md +43 -0
  382. package/skills/deliberate/refs/code-lens.md +38 -0
  383. package/skills/deliberate/refs/content-lens.md +37 -0
  384. package/skills/deliberate/refs/data-lens.md +43 -0
  385. package/skills/deliberate/refs/design-lens.md +37 -0
  386. package/skills/deliberate/refs/opportunity-patterns.md +107 -0
  387. package/skills/deliberate/refs/ops-lens.md +43 -0
  388. package/skills/deliberate/refs/product-lens.md +46 -0
  389. package/skills/deliberate/refs/rethink-framework.md +77 -0
  390. package/skills/deliberate/refs/security-lens.md +47 -0
  391. package/skills/deliberate/refs/testing-lens.md +45 -0
  392. package/skills/deliberate/refs/timing-lens.md +42 -0
  393. package/skills/engineering/architecture/SKILL.md +177 -0
  394. package/skills/engineering/architecture/refs/adr-template.md +268 -0
  395. package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
  396. package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
  397. package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
  398. package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
  399. package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
  400. package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
  401. package/skills/engineering/backend/SKILL.md +102 -0
  402. package/skills/engineering/backend/refs/checklists.md +146 -0
  403. package/skills/engineering/backend/refs/patterns.md +181 -0
  404. package/skills/engineering/debugging/SKILL.md +99 -0
  405. package/skills/engineering/debugging/refs/patterns.md +248 -0
  406. package/skills/engineering/debugging/refs/process.md +203 -0
  407. package/skills/engineering/docs/SKILL.md +97 -0
  408. package/skills/engineering/engineering/SKILL.md +88 -0
  409. package/skills/engineering/engineering/refs/checklists.md +91 -0
  410. package/skills/engineering/engineering/refs/templates.md +71 -0
  411. package/skills/engineering/frontend/SKILL.md +92 -0
  412. package/skills/engineering/frontend/refs/checklists.md +96 -0
  413. package/skills/engineering/frontend/refs/patterns.md +167 -0
  414. package/skills/engineering/integration/SKILL.md +183 -0
  415. package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
  416. package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
  417. package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
  418. package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
  419. package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
  420. package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
  421. package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
  422. package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
  423. package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
  424. package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
  425. package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
  426. package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
  427. package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
  428. package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
  429. package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
  430. package/skills/engineering/integration/refs/grpc-testing.md +49 -0
  431. package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
  432. package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
  433. package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
  434. package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
  435. package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
  436. package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
  437. package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
  438. package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
  439. package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
  440. package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
  441. package/skills/engineering/large-scale-migration/SKILL.md +65 -0
  442. package/skills/engineering/patch/SKILL.md +184 -0
  443. package/skills/engineering/refs/arch.md +126 -0
  444. package/skills/engineering/refs/debug.md +142 -0
  445. package/skills/engineering/refs/docs.md +190 -0
  446. package/skills/engineering/refs/plan.md +109 -0
  447. package/skills/engineering/refs/review.md +138 -0
  448. package/skills/engineering/system-design/SKILL.md +154 -0
  449. package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
  450. package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
  451. package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
  452. package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
  453. package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
  454. package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
  455. package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
  456. package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
  457. package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
  458. package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
  459. package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
  460. package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
  461. package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
  462. package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
  463. package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
  464. package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
  465. package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
  466. package/skills/engineering/unit-test-quality/SKILL.md +194 -0
  467. package/skills/ground/SKILL.md +130 -0
  468. package/skills/integration-discovery/SKILL.md +168 -0
  469. package/skills/integration-discovery/refs/capabilities.md +282 -0
  470. package/skills/integration-discovery/refs/cli-detection.md +295 -0
  471. package/skills/integration-discovery/refs/discovery-sources.md +233 -0
  472. package/skills/integration-discovery/refs/task-patterns.md +206 -0
  473. package/skills/jam/SKILL.md +45 -0
  474. package/skills/jam/refs/facilitation-patterns.md +125 -0
  475. package/skills/jam/refs/quick.md +64 -0
  476. package/skills/jam/refs/synthesis-patterns.md +114 -0
  477. package/skills/multi-model/SKILL.md +168 -0
  478. package/skills/multi-model/refs/aichat.md +119 -0
  479. package/skills/multi-model/refs/aider.md +100 -0
  480. package/skills/multi-model/refs/auditability.md +238 -0
  481. package/skills/multi-model/refs/codex.md +169 -0
  482. package/skills/multi-model/refs/context.md +306 -0
  483. package/skills/multi-model/refs/copilot.md +161 -0
  484. package/skills/multi-model/refs/examples.md +224 -0
  485. package/skills/multi-model/refs/gemini.md +185 -0
  486. package/skills/multi-model/refs/goose.md +112 -0
  487. package/skills/multi-model/refs/llm.md +124 -0
  488. package/skills/multi-model/refs/opencode.md +196 -0
  489. package/skills/multi-model/refs/orchestration.md +188 -0
  490. package/skills/multi-model/refs/pi.md +208 -0
  491. package/skills/persona/SKILL.md +144 -0
  492. package/skills/persona/refs/define.md +133 -0
  493. package/skills/platform/actions/refs/actions.md +114 -0
  494. package/skills/platform/audit/SKILL.md +174 -0
  495. package/skills/platform/audit/refs/audit.md +145 -0
  496. package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
  497. package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
  498. package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
  499. package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
  500. package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
  501. package/skills/platform/compliance/SKILL.md +169 -0
  502. package/skills/platform/compliance/refs/checklists.md +303 -0
  503. package/skills/platform/compliance/refs/compliance.md +117 -0
  504. package/skills/platform/compliance/refs/frameworks.md +282 -0
  505. package/skills/platform/errors/SKILL.md +183 -0
  506. package/skills/platform/errors/refs/patterns.md +271 -0
  507. package/skills/platform/errors/refs/severity.md +264 -0
  508. package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
  509. package/skills/platform/gh/refs/gh.md +105 -0
  510. package/skills/platform/gh-cli/SKILL.md +120 -0
  511. package/skills/platform/gh-cli/refs/automation.md +63 -0
  512. package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
  513. package/skills/platform/gh-cli/refs/patterns.md +132 -0
  514. package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
  515. package/skills/platform/github-actions/SKILL.md +159 -0
  516. package/skills/platform/github-actions/refs/security.md +150 -0
  517. package/skills/platform/github-actions/refs/templates.md +230 -0
  518. package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
  519. package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
  520. package/skills/platform/gitlab-ci/SKILL.md +140 -0
  521. package/skills/platform/gitlab-ci/refs/templates.md +223 -0
  522. package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
  523. package/skills/platform/glab-cli/SKILL.md +133 -0
  524. package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
  525. package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
  526. package/skills/platform/glab-cli/refs/patterns.md +131 -0
  527. package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
  528. package/skills/platform/health/SKILL.md +177 -0
  529. package/skills/platform/health/refs/health.md +72 -0
  530. package/skills/platform/health/refs/sources.md +302 -0
  531. package/skills/platform/incident/refs/incident.md +136 -0
  532. package/skills/platform/infra/refs/infra.md +104 -0
  533. package/skills/platform/observability/SKILL.md +128 -0
  534. package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
  535. package/skills/platform/prereq-doctor/SKILL.md +57 -0
  536. package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
  537. package/skills/platform/traces/refs/traces.md +101 -0
  538. package/skills/product/acceptance-criteria/SKILL.md +193 -0
  539. package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
  540. package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
  541. package/skills/product/accessibility/SKILL.md +197 -0
  542. package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
  543. package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
  544. package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
  545. package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
  546. package/skills/product/accessibility/refs/audit-report-results.md +276 -0
  547. package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
  548. package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
  549. package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
  550. package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
  551. package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
  552. package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
  553. package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
  554. package/skills/product/analyze/SKILL.md +164 -0
  555. package/skills/product/analyze/refs/algorithms.md +238 -0
  556. package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
  557. package/skills/product/imagery/SKILL.md +97 -0
  558. package/skills/product/imagery/alter/SKILL.md +105 -0
  559. package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
  560. package/skills/product/imagery/create/SKILL.md +109 -0
  561. package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
  562. package/skills/product/imagery/create/refs/provider-reference.md +219 -0
  563. package/skills/product/imagery/review/SKILL.md +101 -0
  564. package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
  565. package/skills/product/imagery/review/refs/quality-gates.md +153 -0
  566. package/skills/product/imagery/scripts/analyze.sh +23 -0
  567. package/skills/product/imagery/scripts/edit.sh +14 -0
  568. package/skills/product/imagery/scripts/generate.sh +13 -0
  569. package/skills/product/imagery/scripts/provider.py +787 -0
  570. package/skills/product/listen/SKILL.md +175 -0
  571. package/skills/product/listen/refs/channels.md +220 -0
  572. package/skills/product/mockup/SKILL.md +106 -0
  573. package/skills/product/mockup/refs/templates.md +95 -0
  574. package/skills/product/refs/a11y.md +109 -0
  575. package/skills/product/refs/acceptance.md +79 -0
  576. package/skills/product/refs/align.md +67 -0
  577. package/skills/product/refs/analyze.md +77 -0
  578. package/skills/product/refs/elicit.md +67 -0
  579. package/skills/product/refs/listen.md +58 -0
  580. package/skills/product/refs/mockup.md +77 -0
  581. package/skills/product/refs/screenshot.md +59 -0
  582. package/skills/product/refs/strategy.md +77 -0
  583. package/skills/product/refs/synthesize.md +54 -0
  584. package/skills/product/refs/ux-review.md +68 -0
  585. package/skills/product/refs/ux.md +101 -0
  586. package/skills/product/requirements-analysis/SKILL.md +136 -0
  587. package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
  588. package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
  589. package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
  590. package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
  591. package/skills/product/requirements-graph/SKILL.md +155 -0
  592. package/skills/product/requirements-graph/refs/examples.md +223 -0
  593. package/skills/product/requirements-graph/refs/schema.md +162 -0
  594. package/skills/product/requirements-migrate/SKILL.md +90 -0
  595. package/skills/product/requirements-navigate/SKILL.md +141 -0
  596. package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
  597. package/skills/product/screenshot/SKILL.md +147 -0
  598. package/skills/product/strategy/SKILL.md +198 -0
  599. package/skills/product/strategy/refs/frameworks.md +295 -0
  600. package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
  601. package/skills/product/strategy/refs/templates-roi-value.md +207 -0
  602. package/skills/product/synthesize/SKILL.md +116 -0
  603. package/skills/product/synthesize/refs/journey-mapping.md +212 -0
  604. package/skills/product/synthesize/refs/prioritization.md +193 -0
  605. package/skills/product/ux-review/SKILL.md +164 -0
  606. package/skills/product/visual-direction/SKILL.md +137 -0
  607. package/skills/product/visual-review/SKILL.md +131 -0
  608. package/skills/runtime-exec/SKILL.md +138 -0
  609. package/skills/search/codebase-narrator/SKILL.md +131 -0
  610. package/skills/search/codebase-narrator/refs/output-template.md +100 -0
  611. package/skills/smaht/SKILL.md +72 -0
  612. package/skills/smaht/discovery/SKILL.md +65 -0
  613. package/skills/smaht/intent/SKILL.md +89 -0
  614. package/skills/smaht/propose-skills/SKILL.md +142 -0
  615. package/skills/smaht/refs/state.md +76 -0
  616. package/skills/swarm/SKILL.md +132 -0
  617. package/skills/swarm/refs/fan-out.md +104 -0
  618. package/skills/swarm/refs/independent-verification.md +86 -0
  619. package/skills/swarm/refs/receipts-and-evidence.md +69 -0
  620. package/skills/swarm/refs/recon-synthesis.md +87 -0
  621. package/skills/swarm/refs/ship-discipline.md +90 -0
  622. package/skills/wickedizer/SKILL.md +147 -0
  623. package/skills/wickedizer/refs/artifact-types.md +238 -0
  624. package/skills/wickedizer/refs/complexity.md +278 -0
  625. package/skills/wickedizer/refs/de-ai-pass.md +140 -0
  626. package/skills/wickedizer/refs/failure-modes.md +249 -0
  627. package/skills/wickedizer/refs/patterns.md +282 -0
  628. package/skills/wickedizer/refs/review.md +221 -0
  629. package/skills/wickedizer/refs/structure.md +282 -0
  630. package/skills/wickedizer/refs/voice-profile.md +125 -0
  631. package/skills/workflow/SKILL.md +132 -0
  632. package/skills/workflow/refs/archetype-adjustments.md +205 -0
  633. package/skills/workflow/refs/evidence.md +58 -0
  634. package/skills/workflow/refs/integration.md +130 -0
  635. package/skills/workflow/refs/risk-dimension-signals.md +260 -0
  636. package/skills/workflow/refs/scoring-rubric.md +49 -0
  637. package/skills/workflow/refs/signal-keywords.md +176 -0
  638. package/skills/workflow/refs/specialist-routing-rules.md +159 -0
  639. package/skills/worktrees/SKILL.md +87 -0
  640. package/skills/worktrees/refs/recipes.md +96 -0
@@ -0,0 +1,1042 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ _validate_registry.py — startup-time validator for wicked-garden in-code allowlists.
4
+
5
+ Catches the "invisible gap" failure mode: hardcoded lists of registered assets
6
+ (phases, reviewer subagent_types, bus event handlers) silently drift from the
7
+ files / catalogs they reference. Probes pass, unrelated traffic is fine, then
8
+ the first request that hits the missing asset blows up with a cryptic error.
9
+
10
+ This validator walks each allowlist source, checks that every target exists
11
+ on disk / in the relevant catalog, and aggregates findings into structured
12
+ categories: ``missing`` / ``malformed`` / ``invalid_id``.
13
+
14
+ Stdlib-only, cross-platform. Two consumers:
15
+
16
+ 1. ``hooks/scripts/bootstrap.py`` calls ``run_all_checks()`` once per
17
+ SessionStart and surfaces findings as a session-briefing warning.
18
+ Fail-OPEN: the validator never blocks session start.
19
+ 2. The CLI entrypoint runs the same checks and exits non-zero on any
20
+ ``missing`` / ``malformed`` / ``invalid_id`` finding for CI use.
21
+
22
+ Allowlists covered (when the source-of-truth file exists):
23
+
24
+ - Phase IDs in ``.claude-plugin/phases.json`` ↔ phase agents in ``agents/``
25
+ (each phase that declares ``fallback_agent`` should have a matching agent
26
+ file or specialist alias).
27
+ - Reviewer ``subagent_type`` strings in ``.claude-plugin/gate-policy.json``
28
+ ↔ agent files in ``agents/**/*.md`` (frontmatter ``subagent_type``).
29
+ External plugin reviewers (``wicked-testing:*``) are recognised via a
30
+ configurable allow-prefix list and reported as ``external`` (advisory),
31
+ not ``missing``.
32
+ - Bus event types in ``scripts/_bus.py::BUS_EVENT_MAP`` ↔ projector handlers
33
+ in ``daemon/projector.py::_HANDLERS``. Audit-marker events are explicitly
34
+ excluded from the handler check via ``_AUDIT_MARKER_EVENTS``.
35
+ - Skills referenced from agent frontmatter ``skills:`` lists ↔ ``SKILL.md``
36
+ files present on disk under ``skills/``.
37
+
38
+ Categories:
39
+
40
+ - ``missing`` — a referenced asset exists in an allowlist but the
41
+ target file / handler / row is absent.
42
+ - ``malformed`` — the source-of-truth file is unreadable, invalid JSON,
43
+ or has a structurally broken entry.
44
+ - ``invalid_id`` — the referenced asset has a syntactic problem
45
+ (e.g. empty string, non-string in a string field,
46
+ unexpected colon-form).
47
+
48
+ Out-of-scope (informational only, never elevated to ``missing``):
49
+
50
+ - ``external`` — drop-in plugin reviewers (``wicked-testing:*``,
51
+ ``wicked-brain:*``) that legitimately live in another
52
+ plugin. Surfaced for visibility but never failing.
53
+ - ``skipped`` — a check skipped because its source-of-truth file is
54
+ missing on this checkout (e.g. ``daemon/projector.py``
55
+ not present in a slim install). Bootstrap surfaces
56
+ ``skipped`` checks as INFO, not WARN.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import json
62
+ import os
63
+ import re
64
+ import sys
65
+ from pathlib import Path
66
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
67
+
68
+ # Categories. Keep these literal — the bootstrap consumer matches on them.
69
+ CAT_MISSING = "missing"
70
+ CAT_MALFORMED = "malformed"
71
+ CAT_INVALID_ID = "invalid_id"
72
+ CAT_EXTERNAL = "external"
73
+ CAT_SKIPPED = "skipped"
74
+
75
+ # Severity → categories. Bootstrap-warning + CI-failing categories.
76
+ _BLOCKING_CATEGORIES = frozenset({CAT_MISSING, CAT_MALFORMED, CAT_INVALID_ID})
77
+
78
+ # Reviewer subagent_type prefixes that are EXPECTED to live outside this plugin.
79
+ # Anything starting with one of these is downgraded from `missing` to `external`.
80
+ _EXTERNAL_REVIEWER_PREFIXES: Tuple[str, ...] = (
81
+ "wicked-testing:",
82
+ "wicked-brain:",
83
+ "wicked-bus:",
84
+ )
85
+
86
+ # Bus event types that are AUDIT MARKERS — by design they have no projector
87
+ # handler. Source: scripts/_bus.py inline comments around the migration events.
88
+ # Keep this in sync with daemon/projector.py.ARCHITECTURE.md if it changes.
89
+ _AUDIT_MARKER_EVENTS: Tuple[str, ...] = (
90
+ "wicked.crew.legacy_adopted",
91
+ "wicked.crew.qe_evaluator_migrated",
92
+ "wicked.log.rotated",
93
+ # jam events do not project to wicked-garden's projector — their
94
+ # consumers live in jam scripts and the brain auto-memorize subscriber.
95
+ "wicked.session.started",
96
+ "wicked.session.synthesized",
97
+ "wicked.session.synthesis_ready",
98
+ "wicked.council.voted",
99
+ "wicked.persona.contributed",
100
+ # qe / platform / delivery events are observability-only — no projector.
101
+ "wicked.scenario.run",
102
+ "wicked.coverage.changed",
103
+ "wicked.security.finding_raised",
104
+ "wicked.guard.findings",
105
+ "wicked.compliance.passed",
106
+ "wicked.compliance.failed",
107
+ "wicked.rollout.decided",
108
+ "wicked.experiment.concluded",
109
+ "wicked.fact.extracted",
110
+ "wicked.verdict.recorded",
111
+ "wicked.quality.drift_detected",
112
+ # crew condition resolution — observability only, verdict not mutated.
113
+ "wicked.gate.condition.resolved",
114
+ )
115
+
116
+ # Reviewer values in gate-policy.json that are NOT subagent identifiers — they
117
+ # are mode markers / role placeholders consumed by gate_dispatch.py directly.
118
+ _NON_AGENT_REVIEWERS: Tuple[str, ...] = (
119
+ "human",
120
+ "user",
121
+ "self",
122
+ "skip",
123
+ )
124
+
125
+ # Frontmatter regex — minimal YAML reader for ``subagent_type:`` and ``name:``.
126
+ # Stdlib-only constraint precludes a real YAML dependency, and the agent
127
+ # frontmatter is mechanically generated so a regex is sufficient.
128
+ _FRONTMATTER_RE = re.compile(r"^---\s*$(.*?)^---\s*$", re.DOTALL | re.MULTILINE)
129
+ _KEY_LINE_RE = re.compile(r"^\s*([A-Za-z_][\w-]*)\s*:\s*(.*?)\s*$")
130
+
131
+
132
+ def _plugin_root() -> Path:
133
+ """Resolve plugin root from CLAUDE_PLUGIN_ROOT, else this file's parent."""
134
+ env = os.environ.get("CLAUDE_PLUGIN_ROOT")
135
+ if env:
136
+ p = Path(env)
137
+ if p.exists():
138
+ return p
139
+ # scripts/_validate_registry.py → parent.parent
140
+ return Path(__file__).resolve().parent.parent
141
+
142
+
143
+ def _read_json(path: Path) -> Tuple[Optional[Any], Optional[str]]:
144
+ """Read JSON and return (data, error_message). Either the data or an error
145
+ string — never both."""
146
+ if not path.is_file():
147
+ return None, f"file not found: {path}"
148
+ try:
149
+ return json.loads(path.read_text(encoding="utf-8")), None
150
+ except (json.JSONDecodeError, OSError) as exc:
151
+ return None, f"{type(exc).__name__}: {exc}"
152
+
153
+
154
+ def _parse_agent_frontmatter(path: Path) -> Tuple[Dict[str, str], Optional[str]]:
155
+ """Extract YAML-style frontmatter keys from an agent .md file.
156
+
157
+ Returns ({key: value, ...}, error_or_None). Only flat string keys are
158
+ parsed — list / nested values are returned as the raw line tail.
159
+ """
160
+ try:
161
+ text = path.read_text(encoding="utf-8", errors="replace")
162
+ except OSError as exc:
163
+ return {}, f"read error: {exc}"
164
+ m = _FRONTMATTER_RE.search(text)
165
+ if not m:
166
+ return {}, "no frontmatter block"
167
+ out: Dict[str, str] = {}
168
+ for line in m.group(1).splitlines():
169
+ km = _KEY_LINE_RE.match(line)
170
+ if km:
171
+ out[km.group(1)] = km.group(2)
172
+ return out, None
173
+
174
+
175
+ def _scan_agents(plugin_root: Path) -> Tuple[Dict[str, Path], List[Dict[str, Any]]]:
176
+ """Walk agents/**/*.md and return (subagent_type → path map, malformed findings)."""
177
+ findings: List[Dict[str, Any]] = []
178
+ by_subagent: Dict[str, Path] = {}
179
+ by_name: Dict[str, Path] = {}
180
+
181
+ agents_dir = plugin_root / "agents"
182
+ if not agents_dir.is_dir():
183
+ return {}, [
184
+ {
185
+ "category": CAT_SKIPPED,
186
+ "check": "agents.scan",
187
+ "target": str(agents_dir),
188
+ "detail": "agents/ directory not found — slim install?",
189
+ }
190
+ ]
191
+
192
+ for md_path in agents_dir.rglob("*.md"):
193
+ if md_path.name.upper() == "README.MD":
194
+ continue
195
+ fm, err = _parse_agent_frontmatter(md_path)
196
+ if err:
197
+ findings.append(
198
+ {
199
+ "category": CAT_MALFORMED,
200
+ "check": "agents.frontmatter",
201
+ "target": str(md_path.relative_to(plugin_root)),
202
+ "detail": err,
203
+ }
204
+ )
205
+ continue
206
+ sub = fm.get("subagent_type", "").strip()
207
+ name = fm.get("name", "").strip()
208
+ if name:
209
+ by_name.setdefault(name, md_path)
210
+ if not sub:
211
+ # Not every agent is required to declare subagent_type
212
+ # (e.g. README-style agents). Skip silently.
213
+ continue
214
+ if ":" in sub and not sub.startswith("wicked-garden:"):
215
+ # Mis-namespaced (e.g. typo'd colon) — flag as invalid_id.
216
+ findings.append(
217
+ {
218
+ "category": CAT_INVALID_ID,
219
+ "check": "agents.frontmatter",
220
+ "target": str(md_path.relative_to(plugin_root)),
221
+ "detail": f"subagent_type does not start with 'wicked-garden:': {sub!r}",
222
+ }
223
+ )
224
+ continue
225
+ if sub in by_subagent:
226
+ findings.append(
227
+ {
228
+ "category": CAT_INVALID_ID,
229
+ "check": "agents.duplicate",
230
+ "target": sub,
231
+ "detail": f"duplicate subagent_type in {md_path.relative_to(plugin_root)} (also {by_subagent[sub].relative_to(plugin_root)})",
232
+ }
233
+ )
234
+ continue
235
+ by_subagent[sub] = md_path
236
+
237
+ # Expose name-keyed entries under bare-name keys so gate-policy
238
+ # references like "senior-engineer" resolve. We register under the
239
+ # name AND under the colon-namespaced subagent_type so both forms work.
240
+ for nm, p in by_name.items():
241
+ # Only register if no colon-form already claims that bare name.
242
+ # The gate-policy "fallback" / shorthand reviewers use bare names.
243
+ by_subagent.setdefault(nm, p)
244
+
245
+ return by_subagent, findings
246
+
247
+
248
+ def check_phases(plugin_root: Path) -> List[Dict[str, Any]]:
249
+ """Validate phase fallback_agent references against agent files."""
250
+ findings: List[Dict[str, Any]] = []
251
+ phases_path = plugin_root / ".claude-plugin" / "phases.json"
252
+ data, err = _read_json(phases_path)
253
+ if err:
254
+ findings.append(
255
+ {
256
+ "category": CAT_SKIPPED if "not found" in err else CAT_MALFORMED,
257
+ "check": "phases.json",
258
+ "target": str(phases_path),
259
+ "detail": err,
260
+ }
261
+ )
262
+ return findings
263
+ if not isinstance(data, dict):
264
+ findings.append(
265
+ {
266
+ "category": CAT_MALFORMED,
267
+ "check": "phases.json",
268
+ "target": str(phases_path),
269
+ "detail": "top-level value is not an object",
270
+ }
271
+ )
272
+ return findings
273
+
274
+ phases = data.get("phases")
275
+ if not isinstance(phases, dict):
276
+ findings.append(
277
+ {
278
+ "category": CAT_MALFORMED,
279
+ "check": "phases.json",
280
+ "target": str(phases_path),
281
+ "detail": "missing or non-object 'phases' key",
282
+ }
283
+ )
284
+ return findings
285
+
286
+ agents_index, agent_findings = _scan_agents(plugin_root)
287
+ findings.extend(agent_findings)
288
+
289
+ # Slim-install short-circuit: if agents/ was missing, _scan_agents
290
+ # already emitted a single `skipped` finding. Don't cascade into N
291
+ # `missing` findings for every fallback_agent — one slim-install
292
+ # signal is enough.
293
+ if not agents_index and any(
294
+ f.get("category") == CAT_SKIPPED and f.get("check") == "agents.scan"
295
+ for f in agent_findings
296
+ ):
297
+ return findings
298
+
299
+ for phase_id, body in phases.items():
300
+ if not isinstance(phase_id, str) or not phase_id:
301
+ findings.append(
302
+ {
303
+ "category": CAT_INVALID_ID,
304
+ "check": "phases.id",
305
+ "target": repr(phase_id),
306
+ "detail": "phase id must be a non-empty string",
307
+ }
308
+ )
309
+ continue
310
+ if not isinstance(body, dict):
311
+ findings.append(
312
+ {
313
+ "category": CAT_MALFORMED,
314
+ "check": "phases.body",
315
+ "target": phase_id,
316
+ "detail": "phase body is not an object",
317
+ }
318
+ )
319
+ continue
320
+ fb = body.get("fallback_agent")
321
+ if fb in (None, ""):
322
+ continue # fallback_agent is optional
323
+ if not isinstance(fb, str):
324
+ findings.append(
325
+ {
326
+ "category": CAT_INVALID_ID,
327
+ "check": "phases.fallback_agent",
328
+ "target": phase_id,
329
+ "detail": f"fallback_agent is not a string: {fb!r}",
330
+ }
331
+ )
332
+ continue
333
+ if fb in _NON_AGENT_REVIEWERS:
334
+ continue
335
+ if fb in agents_index:
336
+ continue
337
+ # Bare names without colon — already merged into agents_index by name.
338
+ findings.append(
339
+ {
340
+ "category": CAT_MISSING,
341
+ "check": "phases.fallback_agent",
342
+ "target": f"{phase_id} → {fb}",
343
+ "detail": f"phase '{phase_id}' references fallback_agent '{fb}' but no agent .md file declares it",
344
+ }
345
+ )
346
+
347
+ return findings
348
+
349
+
350
+ def check_gate_policy(plugin_root: Path) -> List[Dict[str, Any]]:
351
+ """Validate gate-policy reviewer + fallback references against agent files."""
352
+ findings: List[Dict[str, Any]] = []
353
+ gp_path = plugin_root / ".claude-plugin" / "gate-policy.json"
354
+ data, err = _read_json(gp_path)
355
+ if err:
356
+ findings.append(
357
+ {
358
+ "category": CAT_SKIPPED if "not found" in err else CAT_MALFORMED,
359
+ "check": "gate-policy.json",
360
+ "target": str(gp_path),
361
+ "detail": err,
362
+ }
363
+ )
364
+ return findings
365
+ if not isinstance(data, dict):
366
+ findings.append(
367
+ {
368
+ "category": CAT_MALFORMED,
369
+ "check": "gate-policy.json",
370
+ "target": str(gp_path),
371
+ "detail": "top-level value is not an object",
372
+ }
373
+ )
374
+ return findings
375
+
376
+ gates = data.get("gates")
377
+ if not isinstance(gates, dict):
378
+ findings.append(
379
+ {
380
+ "category": CAT_MALFORMED,
381
+ "check": "gate-policy.gates",
382
+ "target": str(gp_path),
383
+ "detail": "missing or non-object 'gates' key",
384
+ }
385
+ )
386
+ return findings
387
+
388
+ agents_index, agent_findings = _scan_agents(plugin_root)
389
+ findings.extend(agent_findings)
390
+
391
+ # Slim-install short-circuit (mirrors check_phases): if agents/ was
392
+ # missing, return after surfacing the single `skipped` finding rather
393
+ # than cascading into per-reviewer `missing` findings.
394
+ if not agents_index and any(
395
+ f.get("category") == CAT_SKIPPED and f.get("check") == "agents.scan"
396
+ for f in agent_findings
397
+ ):
398
+ return findings
399
+
400
+ def _resolve(reviewer: str) -> str:
401
+ if not isinstance(reviewer, str) or not reviewer.strip():
402
+ return CAT_INVALID_ID
403
+ if reviewer in _NON_AGENT_REVIEWERS:
404
+ return "ok"
405
+ if any(reviewer.startswith(p) for p in _EXTERNAL_REVIEWER_PREFIXES):
406
+ return CAT_EXTERNAL
407
+ if reviewer in agents_index:
408
+ return "ok"
409
+ return CAT_MISSING
410
+
411
+ for gate_id, tiers in gates.items():
412
+ if not isinstance(tiers, dict):
413
+ findings.append(
414
+ {
415
+ "category": CAT_MALFORMED,
416
+ "check": "gate-policy.gate",
417
+ "target": gate_id,
418
+ "detail": "gate body is not an object",
419
+ }
420
+ )
421
+ continue
422
+ for tier_name, tier_body in tiers.items():
423
+ if not isinstance(tier_body, dict):
424
+ continue # description / non-tier keys
425
+ # Read raw values without falsy-coercion (`or []`) — falsy
426
+ # non-list values like `""`, `0`, `{}` previously slipped through
427
+ # as "no reviewers" instead of being flagged as malformed.
428
+ raw_reviewers = tier_body.get("reviewers")
429
+ if raw_reviewers is None:
430
+ reviewers: List[Any] = []
431
+ elif isinstance(raw_reviewers, list):
432
+ reviewers = raw_reviewers
433
+ else:
434
+ findings.append(
435
+ {
436
+ "category": CAT_MALFORMED,
437
+ "check": "gate-policy.reviewers",
438
+ "target": f"{gate_id}.{tier_name}",
439
+ "detail": (
440
+ "reviewers is not a list "
441
+ f"(got {type(raw_reviewers).__name__}: {raw_reviewers!r})"
442
+ ),
443
+ }
444
+ )
445
+ continue
446
+ raw_fallback = tier_body.get("fallback")
447
+ # `fallback` is a single string (or absent). Reject any other
448
+ # non-string type — the previous `is not None and != ""` check
449
+ # silently accepted dicts / numbers / booleans.
450
+ if raw_fallback is None or raw_fallback == "":
451
+ fallback: Optional[str] = None
452
+ elif isinstance(raw_fallback, str):
453
+ fallback = raw_fallback
454
+ else:
455
+ findings.append(
456
+ {
457
+ "category": CAT_MALFORMED,
458
+ "check": "gate-policy.fallback",
459
+ "target": f"{gate_id}.{tier_name}",
460
+ "detail": (
461
+ "fallback is not a string "
462
+ f"(got {type(raw_fallback).__name__}: {raw_fallback!r})"
463
+ ),
464
+ }
465
+ )
466
+ fallback = None
467
+ for rv in reviewers:
468
+ outcome = _resolve(rv)
469
+ if outcome == "ok":
470
+ continue
471
+ findings.append(
472
+ {
473
+ "category": outcome,
474
+ "check": "gate-policy.reviewer",
475
+ "target": f"{gate_id}.{tier_name} → {rv}",
476
+ "detail": (
477
+ "external plugin reviewer (advisory only)"
478
+ if outcome == CAT_EXTERNAL
479
+ else f"reviewer '{rv}' has no matching agent .md file"
480
+ if outcome == CAT_MISSING
481
+ else f"reviewer is not a non-empty string: {rv!r}"
482
+ ),
483
+ }
484
+ )
485
+ if fallback is not None:
486
+ outcome = _resolve(fallback)
487
+ if outcome == "ok":
488
+ continue
489
+ findings.append(
490
+ {
491
+ "category": outcome,
492
+ "check": "gate-policy.fallback",
493
+ "target": f"{gate_id}.{tier_name} → {fallback}",
494
+ "detail": (
495
+ "external plugin fallback (advisory only)"
496
+ if outcome == CAT_EXTERNAL
497
+ else f"fallback '{fallback}' has no matching agent .md file"
498
+ if outcome == CAT_MISSING
499
+ else f"fallback is not a non-empty string: {fallback!r}"
500
+ ),
501
+ }
502
+ )
503
+
504
+ return findings
505
+
506
+
507
+ def _extract_string_keys_from_dict_literal(text: str, var_name: str) -> Optional[Set[str]]:
508
+ """Best-effort string-key extraction from a Python dict literal.
509
+
510
+ Stdlib-only — we cannot import the bus module at validation time without
511
+ pulling its deps, so we parse the source text. Looks for:
512
+
513
+ VAR_NAME ... = {
514
+ "key1": ...,
515
+ "key2": ...,
516
+ }
517
+
518
+ Returns the set of keys, or None if the variable cannot be located.
519
+ """
520
+ # Match the assignment up to the opening brace
521
+ pat = re.compile(
522
+ r"^\s*" + re.escape(var_name) + r"\s*(?::[^=]+)?=\s*\{",
523
+ re.MULTILINE,
524
+ )
525
+ m = pat.search(text)
526
+ if not m:
527
+ return None
528
+ start = m.end() - 1 # position of '{'
529
+ depth = 0
530
+ end = -1
531
+ in_string: Optional[str] = None
532
+ i = start
533
+ while i < len(text):
534
+ ch = text[i]
535
+ if in_string:
536
+ if ch == "\\":
537
+ i += 2
538
+ continue
539
+ if ch == in_string:
540
+ in_string = None
541
+ i += 1
542
+ continue
543
+ # Skip Python line comments ('#' to end of line) — but ONLY when
544
+ # we are NOT inside a string. A '}' inside a comment must not
545
+ # decrement depth or we terminate parsing prematurely.
546
+ if ch == "#":
547
+ nl = text.find("\n", i)
548
+ if nl < 0:
549
+ # Comment runs to EOF — no closer can follow.
550
+ break
551
+ i = nl + 1
552
+ continue
553
+ if ch in ("'", '"'):
554
+ in_string = ch
555
+ i += 1
556
+ continue
557
+ if ch == "{":
558
+ depth += 1
559
+ elif ch == "}":
560
+ depth -= 1
561
+ if depth == 0:
562
+ end = i
563
+ break
564
+ i += 1
565
+ if end < 0:
566
+ return None
567
+ # Walk the dict body character-by-character, tracking brace/bracket depth
568
+ # AND string state, so we can collect ONLY the top-level (depth-0) keys.
569
+ # The regex-per-line approach picks up inner dict keys like "description"
570
+ # from each event record, which is wrong.
571
+ body = text[start + 1 : end]
572
+ keys: Set[str] = set()
573
+ depth_b = 0 # braces
574
+ depth_s = 0 # square brackets
575
+ depth_p = 0 # parens
576
+ in_str: Optional[str] = None
577
+ str_buf: List[str] = []
578
+ str_start_at_zero = False
579
+ j = 0
580
+ n = len(body)
581
+ while j < n:
582
+ ch = body[j]
583
+ if in_str:
584
+ if ch == "\\":
585
+ str_buf.append(body[j : j + 2])
586
+ j += 2
587
+ continue
588
+ if ch == in_str:
589
+ # Closing quote — peek ahead for ':'
590
+ literal = "".join(str_buf)
591
+ in_str = None
592
+ str_buf = []
593
+ # Skip whitespace and check for ':' — Python permits a
594
+ # newline between the closing quote and the colon, so accept
595
+ # CR/LF here in addition to space/tab.
596
+ k = j + 1
597
+ while k < n and body[k] in " \t\n\r":
598
+ k += 1
599
+ if (
600
+ str_start_at_zero
601
+ and k < n
602
+ and body[k] == ":"
603
+ and re.fullmatch(r"[A-Za-z0-9_.\-]+", literal)
604
+ ):
605
+ keys.add(literal)
606
+ str_start_at_zero = False
607
+ j += 1
608
+ continue
609
+ str_buf.append(ch)
610
+ j += 1
611
+ continue
612
+ if ch == "#":
613
+ # Skip line comment
614
+ nl = body.find("\n", j)
615
+ if nl < 0:
616
+ break
617
+ j = nl + 1
618
+ continue
619
+ if ch in ("'", '"'):
620
+ in_str = ch
621
+ str_buf = []
622
+ str_start_at_zero = depth_b == 0 and depth_s == 0 and depth_p == 0
623
+ j += 1
624
+ continue
625
+ if ch == "{":
626
+ depth_b += 1
627
+ elif ch == "}":
628
+ depth_b -= 1
629
+ elif ch == "[":
630
+ depth_s += 1
631
+ elif ch == "]":
632
+ depth_s -= 1
633
+ elif ch == "(":
634
+ depth_p += 1
635
+ elif ch == ")":
636
+ depth_p -= 1
637
+ j += 1
638
+ return keys
639
+
640
+
641
+ def check_bus_handlers(plugin_root: Path) -> List[Dict[str, Any]]:
642
+ """Validate every BUS_EVENT_MAP entry that needs a projector has a handler."""
643
+ findings: List[Dict[str, Any]] = []
644
+ bus_path = plugin_root / "scripts" / "_bus.py"
645
+ proj_path = plugin_root / "daemon" / "projector.py"
646
+
647
+ if not bus_path.is_file():
648
+ findings.append(
649
+ {
650
+ "category": CAT_SKIPPED,
651
+ "check": "bus.event_map",
652
+ "target": str(bus_path),
653
+ "detail": "_bus.py not found — bus catalog check skipped",
654
+ }
655
+ )
656
+ return findings
657
+
658
+ if not proj_path.is_file():
659
+ findings.append(
660
+ {
661
+ "category": CAT_SKIPPED,
662
+ "check": "bus.handlers",
663
+ "target": str(proj_path),
664
+ "detail": "daemon/projector.py not found — handler check skipped",
665
+ }
666
+ )
667
+ return findings
668
+
669
+ try:
670
+ bus_text = bus_path.read_text(encoding="utf-8", errors="replace")
671
+ except OSError as exc:
672
+ findings.append(
673
+ {
674
+ "category": CAT_MALFORMED,
675
+ "check": "bus.event_map",
676
+ "target": str(bus_path),
677
+ "detail": f"read error: {exc}",
678
+ }
679
+ )
680
+ return findings
681
+ try:
682
+ proj_text = proj_path.read_text(encoding="utf-8", errors="replace")
683
+ except OSError as exc:
684
+ findings.append(
685
+ {
686
+ "category": CAT_MALFORMED,
687
+ "check": "bus.handlers",
688
+ "target": str(proj_path),
689
+ "detail": f"read error: {exc}",
690
+ }
691
+ )
692
+ return findings
693
+
694
+ event_keys = _extract_string_keys_from_dict_literal(bus_text, "BUS_EVENT_MAP")
695
+ handler_keys = _extract_string_keys_from_dict_literal(proj_text, "_HANDLERS")
696
+
697
+ if event_keys is None:
698
+ findings.append(
699
+ {
700
+ "category": CAT_MALFORMED,
701
+ "check": "bus.event_map",
702
+ "target": str(bus_path),
703
+ "detail": "could not locate BUS_EVENT_MAP literal",
704
+ }
705
+ )
706
+ return findings
707
+ if handler_keys is None:
708
+ findings.append(
709
+ {
710
+ "category": CAT_MALFORMED,
711
+ "check": "bus.handlers",
712
+ "target": str(proj_path),
713
+ "detail": "could not locate _HANDLERS literal",
714
+ }
715
+ )
716
+ return findings
717
+
718
+ audit_marker = set(_AUDIT_MARKER_EVENTS)
719
+ for evt in sorted(event_keys):
720
+ if evt in audit_marker:
721
+ continue
722
+ if evt in handler_keys:
723
+ continue
724
+ findings.append(
725
+ {
726
+ "category": CAT_MISSING,
727
+ "check": "bus.handlers",
728
+ "target": evt,
729
+ "detail": (
730
+ "event in BUS_EVENT_MAP has no projector handler in "
731
+ "daemon/projector.py::_HANDLERS and is not in the "
732
+ "audit-marker exclusion list"
733
+ ),
734
+ }
735
+ )
736
+
737
+ # Reverse direction: handler defined for an unknown event.
738
+ # This is informational — events may be emitted via callsites that bypass
739
+ # BUS_EVENT_MAP (e.g. legacy direct-emit paths kept for bus-as-truth
740
+ # cutover compat). Surface as `external` so it shows up in audit but does
741
+ # not fail the gate.
742
+ for h in sorted(handler_keys):
743
+ if h in event_keys:
744
+ continue
745
+ findings.append(
746
+ {
747
+ "category": CAT_EXTERNAL,
748
+ "check": "bus.handlers.orphan",
749
+ "target": h,
750
+ "detail": (
751
+ "_HANDLERS dispatches an event_type that is not "
752
+ "registered in BUS_EVENT_MAP — likely emitted via a "
753
+ "legacy or direct-emit callsite (advisory only)"
754
+ ),
755
+ }
756
+ )
757
+
758
+ return findings
759
+
760
+
761
+ def check_skill_refs(plugin_root: Path) -> List[Dict[str, Any]]:
762
+ """Validate that any agent frontmatter ``skills:`` reference resolves
763
+ to a SKILL.md on disk under ``skills/``.
764
+
765
+ Frontmatter shape varies:
766
+
767
+ skills: foo, bar
768
+ skills: [foo, bar]
769
+ skills:
770
+ - foo
771
+ - bar
772
+
773
+ We accept all three forms by stripping brackets / dashes and splitting
774
+ on commas / newlines.
775
+ """
776
+ findings: List[Dict[str, Any]] = []
777
+ agents_dir = plugin_root / "agents"
778
+ skills_dir = plugin_root / "skills"
779
+ if not agents_dir.is_dir() or not skills_dir.is_dir():
780
+ findings.append(
781
+ {
782
+ "category": CAT_SKIPPED,
783
+ "check": "skills.refs",
784
+ "target": str(agents_dir if not agents_dir.is_dir() else skills_dir),
785
+ "detail": "agents/ or skills/ directory missing — skill-ref check skipped",
786
+ }
787
+ )
788
+ return findings
789
+
790
+ # Build the set of skill identifiers present on disk.
791
+ # A skill is any directory containing SKILL.md, OR a domain dir with
792
+ # SKILL.md at its root (single-skill domains). We index by:
793
+ # - bare name (last path segment of the dir holding SKILL.md)
794
+ # - "wicked-garden:{domain}:{skill-name}" colon-form
795
+ available: Set[str] = set()
796
+ for skill_md in skills_dir.rglob("SKILL.md"):
797
+ try:
798
+ rel = skill_md.relative_to(skills_dir)
799
+ except ValueError:
800
+ continue
801
+ parts = rel.parts[:-1] # drop SKILL.md
802
+ if not parts:
803
+ continue
804
+ # Bare leaf-name form
805
+ available.add(parts[-1])
806
+ # Colon-namespaced form for multi-skill domains
807
+ if len(parts) == 1:
808
+ available.add(f"wicked-garden:{parts[0]}")
809
+ elif len(parts) >= 2:
810
+ available.add(f"wicked-garden:{parts[0]}:{parts[-1]}")
811
+
812
+ # Walk agents
813
+ for md_path in agents_dir.rglob("*.md"):
814
+ if md_path.name.upper() == "README.MD":
815
+ continue
816
+ try:
817
+ text = md_path.read_text(encoding="utf-8", errors="replace")
818
+ except OSError:
819
+ continue
820
+ m = _FRONTMATTER_RE.search(text)
821
+ if not m:
822
+ continue
823
+ # Find the ``skills:`` block
824
+ block = m.group(1)
825
+ sk_match = re.search(
826
+ r"^\s*skills\s*:\s*(.*?)(?=^\s*[A-Za-z_][\w-]*\s*:|\Z)",
827
+ block,
828
+ re.DOTALL | re.MULTILINE,
829
+ )
830
+ if not sk_match:
831
+ continue
832
+ raw = sk_match.group(1)
833
+ # Normalise inline / list / multiline forms
834
+ cleaned = raw.replace("[", " ").replace("]", " ")
835
+ cleaned = re.sub(r"^\s*-\s*", "", cleaned, flags=re.MULTILINE)
836
+ tokens = [
837
+ t.strip().strip("'\"")
838
+ for piece in cleaned.split(",")
839
+ for t in piece.splitlines()
840
+ ]
841
+ for tok in tokens:
842
+ if not tok:
843
+ continue
844
+ if tok in available:
845
+ continue
846
+ # Only flag if it LOOKS like a skill ref (no spaces, plausible chars)
847
+ if " " in tok or len(tok) > 80:
848
+ continue
849
+ findings.append(
850
+ {
851
+ "category": CAT_MISSING,
852
+ "check": "skills.refs",
853
+ "target": f"{md_path.relative_to(plugin_root)} → {tok}",
854
+ "detail": (
855
+ f"agent declares skills include '{tok}' but no "
856
+ "SKILL.md under skills/ matches that id"
857
+ ),
858
+ }
859
+ )
860
+
861
+ return findings
862
+
863
+
864
+ def run_all_checks(plugin_root: Optional[Path] = None) -> Dict[str, Any]:
865
+ """Run every check and return a structured report.
866
+
867
+ Returns:
868
+ {
869
+ "ok": bool, # False if any blocking finding
870
+ "checks_run": [name, ...],
871
+ "findings": [ {category, check, target, detail}, ... ],
872
+ "summary": {category: count, ...},
873
+ }
874
+ """
875
+ root = plugin_root or _plugin_root()
876
+ findings: List[Dict[str, Any]] = []
877
+ checks_run: List[str] = []
878
+
879
+ for name, fn in (
880
+ ("phases", check_phases),
881
+ ("gate_policy", check_gate_policy),
882
+ ("bus_handlers", check_bus_handlers),
883
+ ("skill_refs", check_skill_refs),
884
+ ):
885
+ try:
886
+ results = fn(root)
887
+ except Exception as exc: # fail-open per individual check
888
+ results = [
889
+ {
890
+ "category": CAT_MALFORMED,
891
+ "check": f"{name}.exception",
892
+ "target": name,
893
+ "detail": f"{type(exc).__name__}: {exc}",
894
+ }
895
+ ]
896
+ checks_run.append(name)
897
+ findings.extend(results)
898
+
899
+ summary: Dict[str, int] = {}
900
+ for f in findings:
901
+ summary[f["category"]] = summary.get(f["category"], 0) + 1
902
+
903
+ blocking = sum(summary.get(c, 0) for c in _BLOCKING_CATEGORIES)
904
+ return {
905
+ "ok": blocking == 0,
906
+ "checks_run": checks_run,
907
+ "findings": findings,
908
+ "summary": summary,
909
+ }
910
+
911
+
912
+ def format_briefing(
913
+ report: Dict[str, Any],
914
+ max_lines: int = 8,
915
+ include_advisory: bool = False,
916
+ ) -> Optional[str]:
917
+ """Format a session-briefing string for bootstrap.py.
918
+
919
+ Returns:
920
+ - None when there is nothing worth surfacing in the SessionStart
921
+ briefing (no blocking findings AND advisory-only findings are
922
+ either absent or suppressed by `include_advisory=False`).
923
+ - A short multi-line block otherwise.
924
+
925
+ The default ``include_advisory=False`` keeps the briefing quiet during
926
+ normal operation: drop-in plugin references (`wicked-testing:*`,
927
+ `wicked-brain:*`) and known orphan handlers populate the advisory
928
+ bucket and would otherwise add 10-15 lines to every SessionStart.
929
+ The CLI runner shows ALL findings unconditionally — this filter only
930
+ affects the bootstrap surface.
931
+ """
932
+ findings = report.get("findings", []) or []
933
+ if not findings:
934
+ return None
935
+ summary = report.get("summary") or {}
936
+ blocking = {
937
+ c: n for c, n in summary.items() if c in _BLOCKING_CATEGORIES and n > 0
938
+ }
939
+ advisory = {
940
+ c: n
941
+ for c, n in summary.items()
942
+ if c not in _BLOCKING_CATEGORIES and n > 0
943
+ }
944
+ # Bootstrap default: quiet unless something is broken. CI runs the
945
+ # CLI which always shows everything.
946
+ if not blocking and not include_advisory:
947
+ return None
948
+ if not blocking and not advisory:
949
+ return None
950
+ head_parts = []
951
+ if blocking:
952
+ head_parts.append(
953
+ ", ".join(f"{c}={n}" for c, n in sorted(blocking.items()))
954
+ )
955
+ if advisory and include_advisory:
956
+ head_parts.append(
957
+ "advisory: "
958
+ + ", ".join(f"{c}={n}" for c, n in sorted(advisory.items()))
959
+ )
960
+ head = "[Registry] startup-time validation surfaced findings — " + "; ".join(
961
+ head_parts
962
+ )
963
+ lines = [head]
964
+ # Show blocking findings first, capped, then advisory.
965
+ blocking_list = [f for f in findings if f["category"] in _BLOCKING_CATEGORIES]
966
+ if include_advisory:
967
+ advisory_list = [
968
+ f for f in findings if f["category"] not in _BLOCKING_CATEGORIES
969
+ ]
970
+ else:
971
+ advisory_list = []
972
+ shown = 0
973
+ for f in blocking_list + advisory_list:
974
+ if shown >= max_lines:
975
+ break
976
+ lines.append(
977
+ f" - [{f['category']}] {f['check']}: {f['target']} — {f['detail']}"
978
+ )
979
+ shown += 1
980
+ surfaced_total = len(blocking_list) + len(advisory_list)
981
+ leftover = surfaced_total - shown
982
+ if leftover > 0:
983
+ lines.append(
984
+ f" ... and {leftover} more (run scripts/_validate_registry.py for full list)"
985
+ )
986
+ return "\n".join(lines)
987
+
988
+
989
+ # ---------------------------------------------------------------------------
990
+ # CLI
991
+ # ---------------------------------------------------------------------------
992
+
993
+ def _emit(report: Dict[str, Any], as_json: bool) -> None:
994
+ if as_json:
995
+ # Use sys.stdout.write + json.dumps to honour the cross-platform
996
+ # rule (no `printf` ambiguity); print() in Python is line-buffered
997
+ # and adds a single newline which is fine on every platform.
998
+ sys.stdout.write(json.dumps(report, indent=2))
999
+ sys.stdout.write("\n")
1000
+ return
1001
+ summary = report.get("summary") or {}
1002
+ summary_str = ", ".join(f"{c}={n}" for c, n in sorted(summary.items())) or "clean"
1003
+ sys.stdout.write(
1004
+ f"wicked-garden registry validator — checks: "
1005
+ f"{', '.join(report.get('checks_run', []))}\n"
1006
+ )
1007
+ sys.stdout.write(f" summary: {summary_str}\n")
1008
+ for f in report.get("findings", []):
1009
+ sys.stdout.write(
1010
+ f" [{f['category']}] {f['check']}: {f['target']} — {f['detail']}\n"
1011
+ )
1012
+ if report.get("ok"):
1013
+ sys.stdout.write("OK\n")
1014
+ else:
1015
+ sys.stdout.write("FAIL\n")
1016
+
1017
+
1018
+ def main(argv: Optional[List[str]] = None) -> int:
1019
+ argv = list(sys.argv[1:] if argv is None else argv)
1020
+ as_json = "--json" in argv
1021
+ if "--help" in argv or "-h" in argv:
1022
+ sys.stdout.write(
1023
+ "Usage: _validate_registry.py [--json]\n\n"
1024
+ "Validate that wicked-garden in-code allowlists "
1025
+ "(phases, reviewers, bus handlers, skill refs) point to "
1026
+ "assets that actually exist on disk / in catalog.\n\n"
1027
+ "Exit code: 0 = clean, 1 = blocking findings, 2 = unexpected error.\n"
1028
+ )
1029
+ return 0
1030
+ try:
1031
+ report = run_all_checks()
1032
+ except Exception as exc: # pragma: no cover — defensive
1033
+ sys.stderr.write(
1034
+ f"_validate_registry: unexpected error: {type(exc).__name__}: {exc}\n"
1035
+ )
1036
+ return 2
1037
+ _emit(report, as_json=as_json)
1038
+ return 0 if report.get("ok") else 1
1039
+
1040
+
1041
+ if __name__ == "__main__":
1042
+ sys.exit(main())