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,1214 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ guard_pipeline.py — Autonomous session-close guard evaluation (Issue #448).
4
+
5
+ Runs a tiered profile of surface-level verification checks at session close
6
+ (and optionally at build-phase approval). Does NOT hard-block — surfaces
7
+ findings to the session summary + wicked-bus. Fail-open everywhere.
8
+
9
+ Scope limits (important):
10
+ * All checks are intentionally cheap. This is NOT a substitute for
11
+ engineering:review, crew:gate, or a proper static-analysis run.
12
+ * R1-R6 scan uses regex/line-pattern heuristics only — high recall for
13
+ obvious tells, zero semantic understanding. Document this limitation
14
+ in any downstream report.
15
+ * Semantic-reviewer integration (#444) is optional and imported lazily.
16
+ If unavailable we emit a "semantic-review-unavailable" finding with
17
+ severity=info and continue.
18
+
19
+ The 5 checks:
20
+ 1. bulletproof_scan — R1-R6 surface heuristics on changed files
21
+ 2. debug_artifacts — print/console.log/pdb/breakpoint leftovers
22
+ 3. adr_constraints — ADR MUST/MUST NOT phrases vs the diff
23
+ 4. semantic_review — delegates to scripts.qe.semantic_review (#444)
24
+ 5. skip_log — unresolved skip-reeval entries (audit_skip_log.py)
25
+
26
+ Usage (as a module):
27
+ from platform.guard_pipeline import run_pipeline
28
+ report = run_pipeline(profile_name="scalpel", cwd=Path.cwd())
29
+
30
+ Usage (as a CLI — for debugging):
31
+ python3 guard_pipeline.py run --profile scalpel
32
+ python3 guard_pipeline.py run --profile standard --project-dir ./some/proj
33
+
34
+ Frozen R1-R6 reference (copied from CLAUDE.md, single source of truth):
35
+ R1 — no dead code (commented-out blocks, unreferenced exports)
36
+ R2 — no bare panics (`raise Exception`, bare `panic!`, process.exit, System.exit)
37
+ R3 — no magic values (literal numbers in conditionals/returns without names)
38
+ R4 — no swallowed errors (`except: pass`, empty catch, `// swallow`)
39
+ R5 — no unbounded ops (while True w/o break, for without range, unbounded recursion hints)
40
+ R6 — no god functions (> 80 lines, > 8 parameters in a single def)
41
+
42
+ These are heuristics. False-positives are expected — the guard pipeline
43
+ surfaces them as findings, not blockers. Engineering review owns final calls.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import json
49
+ import os
50
+ import re
51
+ import subprocess
52
+ import sys
53
+ import time
54
+ from dataclasses import asdict, dataclass, field
55
+ from pathlib import Path
56
+ from typing import Any, Callable, Dict, List, Optional, Tuple
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Path setup — allow sibling imports (guard_profiles + scripts/_bus etc.)
60
+ # ---------------------------------------------------------------------------
61
+
62
+ _THIS_DIR = Path(__file__).resolve().parent
63
+ _SCRIPTS_ROOT = _THIS_DIR.parent
64
+ sys.path.insert(0, str(_SCRIPTS_ROOT))
65
+ sys.path.insert(0, str(_THIS_DIR))
66
+
67
+ from guard_profiles import ( # noqa: E402 (sys.path manipulation above)
68
+ GuardProfile,
69
+ SCALPEL,
70
+ auto_select,
71
+ get_profile,
72
+ )
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Constants
77
+ # ---------------------------------------------------------------------------
78
+
79
+ PIPELINE_VERSION = "1.0"
80
+
81
+ # Severity levels — keep in sync with briefing renderer.
82
+ SEVERITY_BLOCK = "block" # would be a hard-block in a full gate (we still fail-open)
83
+ SEVERITY_WARN = "warn" # advisory
84
+ SEVERITY_INFO = "info" # informational (e.g. "scan skipped — no files")
85
+
86
+ # Per-check time budget fraction of the profile budget. Leaves headroom for
87
+ # emission and summary assembly.
88
+ _PER_CHECK_BUDGET_FRACTION = 0.8
89
+
90
+ # Files/dirs we always skip when scanning the diff.
91
+ _SKIP_PARTS = frozenset({
92
+ "node_modules", ".git", "__pycache__", ".venv", "venv", "dist", "build",
93
+ ".tox", ".mypy_cache", ".ruff_cache", ".pytest_cache", ".claude",
94
+ "htmlcov", "coverage", ".next", ".nuxt", "target",
95
+ })
96
+
97
+ # Extensions in scope for bulletproof/debug scans.
98
+ _CODE_EXTENSIONS = frozenset({
99
+ ".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
100
+ ".rs", ".go", ".java", ".kt", ".rb", ".cs",
101
+ })
102
+
103
+ # Max lines we read per file — an unsanitized megafile shouldn't burn our budget.
104
+ _MAX_LINES_PER_FILE = 5000
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Data model
109
+ # ---------------------------------------------------------------------------
110
+
111
+ @dataclass
112
+ class Finding:
113
+ """A single guard-pipeline finding.
114
+
115
+ `rule_id` is the sub-rule (e.g. "R2", "adr-MUST-NOT"). `severity` is one
116
+ of the SEVERITY_* constants. `file` and `line` are optional — checks may
117
+ aggregate findings without a pinned location.
118
+ """
119
+
120
+ check: str
121
+ rule_id: str
122
+ severity: str
123
+ message: str
124
+ file: Optional[str] = None
125
+ line: Optional[int] = None
126
+
127
+ def to_dict(self) -> Dict[str, Any]:
128
+ return {k: v for k, v in asdict(self).items() if v is not None}
129
+
130
+
131
+ @dataclass
132
+ class CheckResult:
133
+ """Result of a single check: findings + timing + status."""
134
+
135
+ name: str
136
+ status: str # "ok" | "skip" | "error"
137
+ findings: List[Finding] = field(default_factory=list)
138
+ duration_ms: int = 0
139
+ note: Optional[str] = None # short human reason (e.g. "no changed files")
140
+
141
+ def to_dict(self) -> Dict[str, Any]:
142
+ return {
143
+ "name": self.name,
144
+ "status": self.status,
145
+ "duration_ms": self.duration_ms,
146
+ "note": self.note,
147
+ "findings": [f.to_dict() for f in self.findings],
148
+ }
149
+
150
+
151
+ @dataclass
152
+ class PipelineReport:
153
+ """Aggregated guard-pipeline output."""
154
+
155
+ pipeline_version: str
156
+ profile: str
157
+ budget_seconds: float
158
+ duration_ms: int
159
+ status: str # "ok" | "budget_exceeded" | "error"
160
+ checks: List[CheckResult] = field(default_factory=list)
161
+ findings_by_severity: Dict[str, int] = field(default_factory=dict)
162
+ total_findings: int = 0
163
+
164
+ def to_dict(self) -> Dict[str, Any]:
165
+ return {
166
+ "pipeline_version": self.pipeline_version,
167
+ "profile": self.profile,
168
+ "budget_seconds": self.budget_seconds,
169
+ "duration_ms": self.duration_ms,
170
+ "status": self.status,
171
+ "total_findings": self.total_findings,
172
+ "findings_by_severity": self.findings_by_severity,
173
+ "checks": [c.to_dict() for c in self.checks],
174
+ }
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Diff helpers
179
+ # ---------------------------------------------------------------------------
180
+
181
+ def _path_in_scope(path: str) -> bool:
182
+ """Skip vendored/generated/irrelevant paths."""
183
+ parts = Path(path).parts
184
+ return not (set(parts) & _SKIP_PARTS)
185
+
186
+
187
+ def _changed_files(cwd: Optional[Path] = None) -> List[str]:
188
+ """Return changed files — staged + unstaged + diff vs HEAD~1. Deduped."""
189
+ cwdp = str(cwd) if cwd else None
190
+ files: List[str] = []
191
+ seen: set = set()
192
+
193
+ def _collect(cmd: List[str]) -> None:
194
+ try:
195
+ result = subprocess.run(cmd, cwd=cwdp, capture_output=True, text=True, timeout=3)
196
+ if result.returncode == 0:
197
+ for line in result.stdout.splitlines():
198
+ line = line.strip()
199
+ if not line or line in seen:
200
+ continue
201
+ if not _path_in_scope(line):
202
+ continue
203
+ # Resolve against cwd so downstream reads succeed
204
+ full = str((cwd or Path.cwd()) / line) if cwd else line
205
+ if os.path.isfile(full):
206
+ seen.add(line)
207
+ files.append(full)
208
+ except Exception:
209
+ pass # fail open — we just return whatever we have so far
210
+
211
+ _collect(["git", "diff", "--name-only", "HEAD"])
212
+ _collect(["git", "diff", "--name-only", "--cached"])
213
+ return files
214
+
215
+
216
+ def _read_lines(path: str) -> List[str]:
217
+ """Read up to _MAX_LINES_PER_FILE lines. Empty on error."""
218
+ try:
219
+ text = Path(path).read_text(encoding="utf-8", errors="replace")
220
+ lines = text.splitlines()
221
+ return lines[:_MAX_LINES_PER_FILE]
222
+ except Exception:
223
+ return []
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Check 1: bulletproof_scan (R1-R6 surface heuristics)
228
+ # ---------------------------------------------------------------------------
229
+
230
+ # Each entry: (rule_id, regex, extensions, message, severity)
231
+ # Extensions=None means "any code file".
232
+ _BULLETPROOF_PATTERNS: List[Tuple[str, re.Pattern, Optional[frozenset], str, str]] = [
233
+ # R2 — bare panics
234
+ ("R2", re.compile(r"\braise\s+Exception\s*\("), frozenset({".py"}),
235
+ "R2: bare `raise Exception(...)` — use a typed exception", SEVERITY_WARN),
236
+ ("R2", re.compile(r"\bpanic!\s*\("), frozenset({".rs"}),
237
+ "R2: bare `panic!` — prefer Result/Error propagation", SEVERITY_WARN),
238
+ ("R2", re.compile(r"\bprocess\.exit\s*\("),
239
+ frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}),
240
+ "R2: `process.exit(...)` in library code — throw instead", SEVERITY_WARN),
241
+ ("R2", re.compile(r"\bSystem\.exit\s*\("), frozenset({".java", ".kt"}),
242
+ "R2: `System.exit(...)` — throw or return", SEVERITY_WARN),
243
+
244
+ # R4 — swallowed errors (single-line form; multiline form handled separately)
245
+ ("R4", re.compile(r"except[^:]*:\s*pass\s*(#.*)?$"), frozenset({".py"}),
246
+ "R4: bare `except: pass` — swallowed exception", SEVERITY_WARN),
247
+ ("R4", re.compile(r"catch\s*\([^)]*\)\s*\{\s*\}"),
248
+ frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".java", ".kt", ".cs"}),
249
+ "R4: empty catch block — swallowed exception", SEVERITY_WARN),
250
+ ("R4", re.compile(r"//\s*swallow|//\s*ignore\s+err"), None,
251
+ "R4: explicit error-swallowing comment", SEVERITY_WARN),
252
+
253
+ # R5 — unbounded ops (structural hint, not semantic)
254
+ ("R5", re.compile(r"\bwhile\s+True\s*:"), frozenset({".py"}),
255
+ "R5: `while True:` — confirm the loop has a bounded exit", SEVERITY_INFO),
256
+ ("R5", re.compile(r"\bwhile\s*\(\s*true\s*\)\s*\{"),
257
+ frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".java", ".kt", ".cs"}),
258
+ "R5: `while (true)` — confirm the loop has a bounded exit", SEVERITY_INFO),
259
+
260
+ # R1 — dead code hint (commented-out blocks of 3+ consecutive lines)
261
+ # We do NOT try to detect via regex — handled in a separate pass below.
262
+ ]
263
+
264
+
265
+ _EXCEPT_RE = re.compile(r"^(\s*)except\b[^:]*:\s*(#.*)?$")
266
+ _BODY_PASS_RE = re.compile(r"^\s*pass\s*(#.*)?$")
267
+
268
+
269
+ def _detect_multiline_swallow(path: str, lines: List[str]) -> List[Finding]:
270
+ """R4: flag multi-line `except ...:\n pass` patterns (Python)."""
271
+ findings: List[Finding] = []
272
+ if not path.endswith(".py"):
273
+ return findings
274
+ for i, line in enumerate(lines):
275
+ m = _EXCEPT_RE.match(line)
276
+ if not m:
277
+ continue
278
+ # Find the next non-blank, non-comment line
279
+ for j in range(i + 1, min(i + 5, len(lines))):
280
+ nxt = lines[j]
281
+ stripped = nxt.strip()
282
+ if not stripped or stripped.startswith("#"):
283
+ continue
284
+ if _BODY_PASS_RE.match(nxt):
285
+ findings.append(Finding(
286
+ check="bulletproof_scan", rule_id="R4", severity=SEVERITY_WARN,
287
+ message="R4: `except ...: pass` (multi-line) — swallowed exception",
288
+ file=path, line=i + 1,
289
+ ))
290
+ break
291
+ return findings
292
+
293
+
294
+ def _count_python_god_functions(path: str, lines: List[str]) -> List[Finding]:
295
+ """R6: flag Python defs with > 80 lines or > 8 params."""
296
+ findings: List[Finding] = []
297
+ def_re = re.compile(r"^(\s*)def\s+(\w+)\s*\(([^)]*)\)")
298
+ i = 0
299
+ while i < len(lines):
300
+ m = def_re.match(lines[i])
301
+ if not m:
302
+ i += 1
303
+ continue
304
+ indent = len(m.group(1))
305
+ name = m.group(2)
306
+ # Count params — crude but stdlib-only.
307
+ params_blob = m.group(3).strip()
308
+ param_count = 0
309
+ if params_blob:
310
+ # Ignore `self` / `cls` and default-value commas inside brackets.
311
+ depth = 0
312
+ current = ""
313
+ parts: List[str] = []
314
+ for ch in params_blob:
315
+ if ch in "([{":
316
+ depth += 1
317
+ elif ch in ")]}":
318
+ depth -= 1
319
+ if ch == "," and depth == 0:
320
+ parts.append(current.strip())
321
+ current = ""
322
+ else:
323
+ current += ch
324
+ if current.strip():
325
+ parts.append(current.strip())
326
+ parts = [p for p in parts if p and p not in ("self", "cls")]
327
+ param_count = len(parts)
328
+ if param_count > 8:
329
+ findings.append(Finding(
330
+ check="bulletproof_scan", rule_id="R6", severity=SEVERITY_WARN,
331
+ message=f"R6: function `{name}` has {param_count} parameters (> 8)",
332
+ file=path, line=i + 1,
333
+ ))
334
+ # Find function end — next line at <= indent that isn't blank
335
+ body_start = i + 1
336
+ end = len(lines)
337
+ for j in range(body_start, len(lines)):
338
+ stripped = lines[j].rstrip()
339
+ if not stripped:
340
+ continue
341
+ leading = len(lines[j]) - len(lines[j].lstrip())
342
+ if leading <= indent:
343
+ end = j
344
+ break
345
+ body_len = end - i
346
+ if body_len > 80:
347
+ findings.append(Finding(
348
+ check="bulletproof_scan", rule_id="R6", severity=SEVERITY_WARN,
349
+ message=f"R6: function `{name}` is {body_len} lines long (> 80)",
350
+ file=path, line=i + 1,
351
+ ))
352
+ i = max(end, i + 1)
353
+ return findings
354
+
355
+
356
+ def _count_commented_blocks(path: str, lines: List[str]) -> List[Finding]:
357
+ """R1: flag runs of >= 3 consecutive commented-out code lines."""
358
+ findings: List[Finding] = []
359
+ ext = Path(path).suffix.lower()
360
+ comment_re: Optional[re.Pattern] = None
361
+ if ext == ".py":
362
+ # Python: commented line that looks like code (contains `=`, `(`, `:`)
363
+ comment_re = re.compile(r"^\s*#\s*[A-Za-z_].*[=(:]")
364
+ elif ext in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
365
+ ".rs", ".go", ".java", ".kt", ".cs"}:
366
+ comment_re = re.compile(r"^\s*//\s*[A-Za-z_].*[=(;{]")
367
+ if comment_re is None:
368
+ return findings
369
+
370
+ run_start: Optional[int] = None
371
+ run_len = 0
372
+ for idx, line in enumerate(lines):
373
+ if comment_re.match(line):
374
+ if run_start is None:
375
+ run_start = idx
376
+ run_len += 1
377
+ else:
378
+ if run_len >= 3 and run_start is not None:
379
+ findings.append(Finding(
380
+ check="bulletproof_scan", rule_id="R1", severity=SEVERITY_INFO,
381
+ message=f"R1: {run_len} consecutive commented-out code lines",
382
+ file=path, line=run_start + 1,
383
+ ))
384
+ run_start = None
385
+ run_len = 0
386
+ if run_len >= 3 and run_start is not None:
387
+ findings.append(Finding(
388
+ check="bulletproof_scan", rule_id="R1", severity=SEVERITY_INFO,
389
+ message=f"R1: {run_len} consecutive commented-out code lines",
390
+ file=path, line=run_start + 1,
391
+ ))
392
+ return findings
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # R3 + R5 AST heuristics (Python only) — #462, Item 3
397
+ # ---------------------------------------------------------------------------
398
+
399
+ # R3 allow-list: numeric literals that are "boring idioms" we don't flag.
400
+ # 0, 1, -1 cover the vast majority of loop guards, sentinels, empty-checks,
401
+ # and index math where a named constant would be overkill.
402
+ _R3_ALLOWED_NUMBERS = frozenset({0, 1, -1})
403
+
404
+
405
+ def _detect_magic_values_r3(path: str, tree: "ast.AST") -> List[Finding]:
406
+ """R3 — flag magic numeric literals in Compare nodes (Python AST).
407
+
408
+ Heuristic: walk every ast.Compare and flag right-hand-side comparators
409
+ whose value is a numeric literal NOT in {0, 1, -1}. Examples:
410
+
411
+ if x > 42: # flagged: 42 is magic
412
+ if n == 0: # NOT flagged: 0 is allow-listed
413
+ if x == -1: # NOT flagged: -1 is allow-listed
414
+ if count > THRESHOLD: # NOT flagged: Name, not Constant
415
+
416
+ Conservative on purpose — we expect false positives (e.g. HTTP status
417
+ codes 200/400/404 in test fixtures) and treat them as info/warn
418
+ surface, not a blocker. Engineering review owns final calls.
419
+
420
+ Performance: AST walk is O(nodes); on typical Python files this runs
421
+ in sub-millisecond time. Pre-parsed tree is passed in so the caller
422
+ can reuse it across R3/R5 without re-parsing.
423
+ """
424
+ import ast # stdlib — local import keeps module-level cost unchanged
425
+ findings: List[Finding] = []
426
+
427
+ for node in ast.walk(tree):
428
+ if not isinstance(node, ast.Compare):
429
+ continue
430
+ # Each Compare can have multiple comparators (e.g. `a < b < c`);
431
+ # we scan each comparator, not just the first.
432
+ for comparator in node.comparators:
433
+ # ast.Constant covers int, float, bool. We want numerics
434
+ # only (exclude bools — they are technically ints but are
435
+ # never "magic").
436
+ if isinstance(comparator, ast.Constant):
437
+ value = comparator.value
438
+ if isinstance(value, bool):
439
+ continue
440
+ if isinstance(value, (int, float)) and value not in _R3_ALLOWED_NUMBERS:
441
+ line_no = getattr(comparator, "lineno", getattr(node, "lineno", 0))
442
+ findings.append(Finding(
443
+ check="bulletproof_scan",
444
+ rule_id="R3",
445
+ severity=SEVERITY_WARN,
446
+ message=f"R3: magic value {value} in conditional at {path}:{line_no}",
447
+ file=path,
448
+ line=line_no,
449
+ ))
450
+ # Also handle ast.UnaryOp wrapping a Constant (e.g. `-42`).
451
+ # -1 is allow-listed above as a plain value; here we catch
452
+ # `-42`, `-100`, etc. that the parser emits as UnaryOp(USub).
453
+ elif isinstance(comparator, ast.UnaryOp) and isinstance(comparator.op, ast.USub):
454
+ operand = comparator.operand
455
+ if isinstance(operand, ast.Constant) and isinstance(operand.value, (int, float)) \
456
+ and not isinstance(operand.value, bool):
457
+ effective = -operand.value
458
+ if effective in _R3_ALLOWED_NUMBERS:
459
+ continue
460
+ line_no = getattr(comparator, "lineno", getattr(node, "lineno", 0))
461
+ findings.append(Finding(
462
+ check="bulletproof_scan",
463
+ rule_id="R3",
464
+ severity=SEVERITY_WARN,
465
+ message=f"R3: magic value {effective} in conditional at {path}:{line_no}",
466
+ file=path,
467
+ line=line_no,
468
+ ))
469
+ return findings
470
+
471
+
472
+ def _func_has_guard_before_call(func: "ast.AST", func_name: str) -> bool:
473
+ """Return True if the function body has an early-return / conditional-return
474
+ BEFORE any recursive self-call. Heuristic used by R5.
475
+
476
+ Walks top-level statements in the function body in order. A statement
477
+ counts as a "guard" if it is:
478
+ * a top-level `return` (bounded-exit early-out)
479
+ * an `If` whose body contains a `return` at any nested level
480
+ * a `raise` (exit path that prevents recursion from running)
481
+
482
+ Statements after the first recursive call do not count.
483
+ """
484
+ import ast
485
+
486
+ def _contains_recursive_call(node: "ast.AST") -> bool:
487
+ for sub in ast.walk(node):
488
+ if isinstance(sub, ast.Call):
489
+ callee = sub.func
490
+ if isinstance(callee, ast.Name) and callee.id == func_name:
491
+ return True
492
+ # Also flag `self.func_name(...)` / `Class.func_name(...)`
493
+ if isinstance(callee, ast.Attribute) and callee.attr == func_name:
494
+ return True
495
+ return False
496
+
497
+ def _contains_return(node: "ast.AST") -> bool:
498
+ for sub in ast.walk(node):
499
+ if isinstance(sub, (ast.Return, ast.Raise)):
500
+ return True
501
+ return False
502
+
503
+ for stmt in func.body: # type: ignore[attr-defined]
504
+ # If the statement itself contains the recursive call, we've run
505
+ # out of guards — stop looking.
506
+ if _contains_recursive_call(stmt):
507
+ return False
508
+ # A bare `return` or `raise` at the top level is a guard.
509
+ if isinstance(stmt, (ast.Return, ast.Raise)):
510
+ return True
511
+ # An `if` whose body contains a return/raise is a guard.
512
+ if isinstance(stmt, ast.If) and _contains_return(stmt):
513
+ return True
514
+ return False
515
+
516
+
517
+ def _detect_unbounded_recursion_r5(path: str, tree: "ast.AST") -> List[Finding]:
518
+ """R5 — flag functions that call themselves with no guard clause.
519
+
520
+ Heuristic: for each FunctionDef / AsyncFunctionDef, check whether the
521
+ function calls `<own_name>(...)` anywhere in its body AND the body
522
+ lacks any top-level return/raise/if-return guard BEFORE the first
523
+ recursive call. Examples:
524
+
525
+ def f(n): def f(n):
526
+ return f(n - 1) if n <= 0:
527
+ return 0
528
+ return f(n - 1)
529
+ # flagged # NOT flagged (guard clause)
530
+
531
+ Performance: O(nodes) AST walk; matches R3 cost. Conservative — we
532
+ only look for same-name calls (not mutual recursion). Documented
533
+ limitation: this is a structural hint, not a proof of unboundedness.
534
+ False positives possible when the guard is deep inside a try/except
535
+ or a for-else clause. Engineering review owns final calls.
536
+ """
537
+ import ast
538
+ findings: List[Finding] = []
539
+
540
+ for node in ast.walk(tree):
541
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
542
+ continue
543
+ name = node.name
544
+ # Does this function call itself anywhere?
545
+ calls_self = False
546
+ for sub in ast.walk(node):
547
+ if isinstance(sub, ast.Call):
548
+ callee = sub.func
549
+ if isinstance(callee, ast.Name) and callee.id == name:
550
+ calls_self = True
551
+ break
552
+ if isinstance(callee, ast.Attribute) and callee.attr == name:
553
+ calls_self = True
554
+ break
555
+ if not calls_self:
556
+ continue
557
+ # Guard present? — early return / raise / conditional return BEFORE
558
+ # the recursive call.
559
+ if _func_has_guard_before_call(node, name):
560
+ continue
561
+ line_no = getattr(node, "lineno", 0)
562
+ findings.append(Finding(
563
+ check="bulletproof_scan",
564
+ rule_id="R5",
565
+ severity=SEVERITY_WARN,
566
+ message=f"R5: possibly unbounded recursion in {name} at {path}:{line_no}",
567
+ file=path,
568
+ line=line_no,
569
+ ))
570
+ return findings
571
+
572
+
573
+ def _run_python_ast_heuristics(path: str, lines: List[str]) -> List[Finding]:
574
+ """Parse Python source once, then run R3 + R5 over the shared tree.
575
+
576
+ Returns an empty list on parse error — we do not attempt to salvage
577
+ partial findings from a syntactically-broken file. This respects the
578
+ per-check budget: a single parse failure costs ms, not seconds.
579
+ """
580
+ import ast
581
+ try:
582
+ source = "\n".join(lines)
583
+ tree = ast.parse(source)
584
+ except SyntaxError:
585
+ return []
586
+ except Exception:
587
+ return []
588
+ findings: List[Finding] = []
589
+ findings.extend(_detect_magic_values_r3(path, tree))
590
+ findings.extend(_detect_unbounded_recursion_r5(path, tree))
591
+ return findings
592
+
593
+
594
+ def check_bulletproof_scan(
595
+ files: List[str],
596
+ *,
597
+ budget_seconds: float,
598
+ **_kwargs: Any,
599
+ ) -> CheckResult:
600
+ """Run R1-R6 surface heuristics on code files."""
601
+ t0 = time.monotonic()
602
+ result = CheckResult(name="bulletproof_scan", status="ok")
603
+
604
+ if not files:
605
+ result.status = "skip"
606
+ result.note = "no changed files"
607
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
608
+ return result
609
+
610
+ deadline = t0 + budget_seconds
611
+ for filepath in files:
612
+ if time.monotonic() > deadline:
613
+ result.note = "budget exceeded — partial scan"
614
+ break
615
+ ext = Path(filepath).suffix.lower()
616
+ if ext not in _CODE_EXTENSIONS:
617
+ continue
618
+ lines = _read_lines(filepath)
619
+ if not lines:
620
+ continue
621
+
622
+ for line_num, line in enumerate(lines, start=1):
623
+ for rule_id, pattern, exts, msg, severity in _BULLETPROOF_PATTERNS:
624
+ if exts is not None and ext not in exts:
625
+ continue
626
+ if pattern.search(line):
627
+ result.findings.append(Finding(
628
+ check="bulletproof_scan", rule_id=rule_id, severity=severity,
629
+ message=msg, file=filepath, line=line_num,
630
+ ))
631
+
632
+ if ext == ".py":
633
+ result.findings.extend(_count_python_god_functions(filepath, lines))
634
+ result.findings.extend(_detect_multiline_swallow(filepath, lines))
635
+ # R3 + R5 AST heuristics (#462, Item 3). Single parse shared
636
+ # across both rules. Fails open on SyntaxError — a broken file
637
+ # contributes zero R3/R5 findings rather than a scan failure.
638
+ result.findings.extend(_run_python_ast_heuristics(filepath, lines))
639
+ result.findings.extend(_count_commented_blocks(filepath, lines))
640
+
641
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
642
+ return result
643
+
644
+
645
+ # ---------------------------------------------------------------------------
646
+ # Check 2: debug_artifacts
647
+ # ---------------------------------------------------------------------------
648
+
649
+ _DEBUG_PATTERNS: List[Tuple[str, re.Pattern, Optional[frozenset], str]] = [
650
+ ("debug.console", re.compile(r"\bconsole\.log\s*\("),
651
+ frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}), "console.log"),
652
+ ("debug.debugger", re.compile(r"\bdebugger\s*;?"),
653
+ frozenset({".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}), "debugger"),
654
+ ("debug.pdb", re.compile(r"\bpdb\.set_trace\s*\("),
655
+ frozenset({".py"}), "pdb.set_trace"),
656
+ ("debug.breakpoint", re.compile(r"\bbreakpoint\s*\("),
657
+ frozenset({".py"}), "breakpoint()"),
658
+ ("debug.print", re.compile(r"^\s*print\s*\("),
659
+ frozenset({".py"}), "print()"),
660
+ ]
661
+
662
+ # Files we tolerate prints in (scripts, tests, examples)
663
+ _PRINT_ALLOWED_PATTERNS = (
664
+ "/tests/", "/test_", "/examples/", "/scripts/",
665
+ "/hooks/", "cli.py", "__main__.py",
666
+ )
667
+
668
+
669
+ def _print_allowed(path: str) -> bool:
670
+ norm = path.replace("\\", "/")
671
+ return any(pat in norm for pat in _PRINT_ALLOWED_PATTERNS)
672
+
673
+
674
+ def check_debug_artifacts(
675
+ files: List[str],
676
+ *,
677
+ budget_seconds: float,
678
+ **_kwargs: Any,
679
+ ) -> CheckResult:
680
+ """Scan changed files for debug leftovers."""
681
+ t0 = time.monotonic()
682
+ result = CheckResult(name="debug_artifacts", status="ok")
683
+
684
+ if not files:
685
+ result.status = "skip"
686
+ result.note = "no changed files"
687
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
688
+ return result
689
+
690
+ deadline = t0 + budget_seconds
691
+ for filepath in files:
692
+ if time.monotonic() > deadline:
693
+ result.note = "budget exceeded — partial scan"
694
+ break
695
+ ext = Path(filepath).suffix.lower()
696
+ if ext not in _CODE_EXTENSIONS:
697
+ continue
698
+ lines = _read_lines(filepath)
699
+ for line_num, line in enumerate(lines, start=1):
700
+ for rule_id, pattern, exts, label in _DEBUG_PATTERNS:
701
+ if exts is not None and ext not in exts:
702
+ continue
703
+ # Suppress `print()` in files where it's expected (tests, scripts, hooks).
704
+ if label == "print()" and _print_allowed(filepath):
705
+ continue
706
+ if pattern.search(line):
707
+ result.findings.append(Finding(
708
+ check="debug_artifacts", rule_id=rule_id, severity=SEVERITY_WARN,
709
+ message=f"{label} left in changed file",
710
+ file=filepath, line=line_num,
711
+ ))
712
+
713
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
714
+ return result
715
+
716
+
717
+ # ---------------------------------------------------------------------------
718
+ # Check 3: adr_constraints
719
+ # ---------------------------------------------------------------------------
720
+
721
+ # Matches "MUST", "MUST NOT", "SHALL NOT" etc. in uppercase context — RFC-2119 tone
722
+ _ADR_MUST_RE = re.compile(r"\b(MUST NOT|SHALL NOT|MUST|SHALL)\b\s+(.{5,140}?)(?:[.!?]|$)")
723
+
724
+ _ADR_SEARCH_ROOTS = ("docs/adr", "adr", "architecture/decisions", "docs/decisions")
725
+
726
+
727
+ def _find_adr_files(cwd: Path) -> List[Path]:
728
+ """Return ADR markdown files under well-known roots (capped for budget)."""
729
+ files: List[Path] = []
730
+ for root in _ADR_SEARCH_ROOTS:
731
+ rootp = cwd / root
732
+ if not rootp.is_dir():
733
+ continue
734
+ try:
735
+ for p in rootp.rglob("*.md"):
736
+ if not _path_in_scope(str(p)):
737
+ continue
738
+ files.append(p)
739
+ if len(files) >= 50:
740
+ return files
741
+ except Exception:
742
+ continue
743
+ return files
744
+
745
+
746
+ def _extract_constraints(adr_files: List[Path]) -> List[Tuple[Path, str, str]]:
747
+ """Return [(adr_path, verb, phrase), ...] for MUST/SHALL phrases."""
748
+ constraints: List[Tuple[Path, str, str]] = []
749
+ for adr in adr_files:
750
+ try:
751
+ text = adr.read_text(encoding="utf-8", errors="replace")
752
+ except Exception:
753
+ continue
754
+ for match in _ADR_MUST_RE.finditer(text):
755
+ verb = match.group(1)
756
+ phrase = match.group(2).strip()
757
+ if 5 <= len(phrase) <= 140:
758
+ constraints.append((adr, verb, phrase))
759
+ if len(constraints) >= 200:
760
+ return constraints
761
+ return constraints
762
+
763
+
764
+ def check_adr_constraints(
765
+ files: List[str],
766
+ *,
767
+ budget_seconds: float,
768
+ cwd: Optional[Path] = None,
769
+ **_kwargs: Any,
770
+ ) -> CheckResult:
771
+ """Scan changed files for MUST-NOT phrase matches from ADRs.
772
+
773
+ Best-effort: finds literal substring matches of MUST-NOT phrases inside
774
+ the diff. False-positives are expected — surfaced as info, not warn.
775
+ """
776
+ t0 = time.monotonic()
777
+ result = CheckResult(name="adr_constraints", status="ok")
778
+
779
+ base = cwd or Path.cwd()
780
+ adr_files = _find_adr_files(base)
781
+ if not adr_files:
782
+ result.status = "skip"
783
+ result.note = "no ADRs found"
784
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
785
+ return result
786
+
787
+ constraints = _extract_constraints(adr_files)
788
+ must_not = [(adr, phrase) for (adr, verb, phrase) in constraints if "NOT" in verb]
789
+ if not must_not:
790
+ result.status = "skip"
791
+ result.note = f"{len(adr_files)} ADRs scanned, no MUST-NOT constraints"
792
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
793
+ return result
794
+
795
+ if not files:
796
+ result.status = "skip"
797
+ result.note = "no changed files"
798
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
799
+ return result
800
+
801
+ deadline = t0 + budget_seconds
802
+ for filepath in files:
803
+ if time.monotonic() > deadline:
804
+ result.note = "budget exceeded — partial scan"
805
+ break
806
+ lines = _read_lines(filepath)
807
+ if not lines:
808
+ continue
809
+ # Combine into one blob for substring search — small files only
810
+ text = "\n".join(lines)
811
+ for adr, phrase in must_not:
812
+ # Take first word of the phrase as a cheap prefilter; fall back to full match
813
+ head = phrase.split()[0] if phrase.split() else phrase
814
+ if head.lower() in text.lower() and phrase.lower()[:40] in text.lower():
815
+ result.findings.append(Finding(
816
+ check="adr_constraints",
817
+ rule_id="adr-MUST-NOT",
818
+ severity=SEVERITY_WARN,
819
+ message=f"Changed file contains text matching ADR constraint: '{phrase[:80]}' (from {adr.name})",
820
+ file=filepath,
821
+ ))
822
+
823
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
824
+ return result
825
+
826
+
827
+ # ---------------------------------------------------------------------------
828
+ # Check 4: semantic_review (lazy import — depends on PR #444 / #455)
829
+ # ---------------------------------------------------------------------------
830
+
831
+ def _call_semantic_reviewer(project_dir: Path, complexity: int) -> Optional[Dict[str, Any]]:
832
+ """Import + call the semantic reviewer lazily. None on unavailable."""
833
+ try:
834
+ # Imports from scripts/qe/semantic_review.py — shipping in PR #455
835
+ sys.path.insert(0, str(_SCRIPTS_ROOT / "qe"))
836
+ from semantic_review import run_semantic_review # type: ignore
837
+ except Exception:
838
+ return None
839
+
840
+ try:
841
+ result = run_semantic_review(project_dir, complexity)
842
+ if isinstance(result, dict):
843
+ return result
844
+ except Exception:
845
+ return None
846
+ return None
847
+
848
+
849
+ def check_semantic_review(
850
+ files: List[str],
851
+ *,
852
+ budget_seconds: float,
853
+ project_dir: Optional[Path] = None,
854
+ complexity: int = 3,
855
+ **_kwargs: Any,
856
+ ) -> CheckResult:
857
+ """Delegate to scripts.qe.semantic_review (PR #444). Fail-open."""
858
+ t0 = time.monotonic()
859
+ result = CheckResult(name="semantic_review", status="ok")
860
+
861
+ target = project_dir or Path.cwd()
862
+ report = _call_semantic_reviewer(target, complexity)
863
+ if report is None:
864
+ result.status = "skip"
865
+ result.note = "semantic-review-unavailable"
866
+ result.findings.append(Finding(
867
+ check="semantic_review",
868
+ rule_id="semantic-review-unavailable",
869
+ severity=SEVERITY_INFO,
870
+ message="semantic reviewer (scripts/qe/semantic_review.py) unavailable — skipped",
871
+ ))
872
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
873
+ return result
874
+
875
+ verdict = str(report.get("verdict", "APPROVE")).upper()
876
+ for raw in report.get("findings", []) or []:
877
+ if not isinstance(raw, dict):
878
+ continue
879
+ sev = SEVERITY_WARN
880
+ if verdict == "REJECT":
881
+ sev = SEVERITY_BLOCK
882
+ elif verdict == "APPROVE":
883
+ sev = SEVERITY_INFO
884
+ msg = str(raw.get("message") or raw.get("summary") or raw.get("title") or "")[:200]
885
+ if not msg:
886
+ continue
887
+ result.findings.append(Finding(
888
+ check="semantic_review",
889
+ rule_id=str(raw.get("rule_id") or raw.get("kind") or "spec-drift"),
890
+ severity=sev,
891
+ message=msg,
892
+ file=raw.get("file"),
893
+ line=raw.get("line") if isinstance(raw.get("line"), int) else None,
894
+ ))
895
+
896
+ if not result.findings and verdict != "APPROVE":
897
+ # Reviewer issued a verdict but no itemized findings — surface one rollup.
898
+ result.findings.append(Finding(
899
+ check="semantic_review",
900
+ rule_id="verdict",
901
+ severity=SEVERITY_WARN if verdict == "CONDITIONAL" else SEVERITY_BLOCK,
902
+ message=f"Semantic review verdict: {verdict} ({report.get('summary', '')[:120]})",
903
+ ))
904
+
905
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
906
+ return result
907
+
908
+
909
+ # ---------------------------------------------------------------------------
910
+ # Check 5: skip_log (unresolved skip-reeval entries)
911
+ # ---------------------------------------------------------------------------
912
+
913
+ def _call_audit_skip_log(project_dir: Path) -> List[Dict[str, Any]]:
914
+ """Import + call audit_skip_log.scan. Empty list on error."""
915
+ try:
916
+ sys.path.insert(0, str(_SCRIPTS_ROOT / "crew"))
917
+ from audit_skip_log import scan # type: ignore
918
+ entries = scan(project_dir)
919
+ return entries if isinstance(entries, list) else []
920
+ except Exception:
921
+ return []
922
+
923
+
924
+ def check_skip_log(
925
+ files: List[str],
926
+ *,
927
+ budget_seconds: float,
928
+ project_dir: Optional[Path] = None,
929
+ **_kwargs: Any,
930
+ ) -> CheckResult:
931
+ """Surface unresolved skip-reeval-log.json entries."""
932
+ t0 = time.monotonic()
933
+ result = CheckResult(name="skip_log", status="ok")
934
+
935
+ target = project_dir
936
+ if target is None or not target.is_dir():
937
+ result.status = "skip"
938
+ result.note = "no crew project directory"
939
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
940
+ return result
941
+
942
+ entries = _call_audit_skip_log(target)
943
+ if not entries:
944
+ result.note = "no unresolved skip entries"
945
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
946
+ return result
947
+
948
+ for entry in entries[:20]:
949
+ phase = entry.get("_source_phase", "?")
950
+ reason = entry.get("reason") or entry.get("note") or "unresolved skip"
951
+ result.findings.append(Finding(
952
+ check="skip_log",
953
+ rule_id="skip-unresolved",
954
+ severity=SEVERITY_WARN,
955
+ message=f"Unresolved skip in phase `{phase}`: {str(reason)[:120]}",
956
+ ))
957
+
958
+ if len(entries) > 20:
959
+ result.findings.append(Finding(
960
+ check="skip_log",
961
+ rule_id="skip-unresolved",
962
+ severity=SEVERITY_INFO,
963
+ message=f"... and {len(entries) - 20} more unresolved skip entries",
964
+ ))
965
+
966
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
967
+ return result
968
+
969
+
970
+ # ---------------------------------------------------------------------------
971
+ # Registry + runner
972
+ # ---------------------------------------------------------------------------
973
+
974
+ CHECK_REGISTRY: Dict[str, Callable[..., CheckResult]] = {
975
+ "bulletproof_scan": check_bulletproof_scan,
976
+ "debug_artifacts": check_debug_artifacts,
977
+ "adr_constraints": check_adr_constraints,
978
+ "semantic_review": check_semantic_review,
979
+ "skip_log": check_skip_log,
980
+ }
981
+
982
+
983
+ def run_pipeline(
984
+ profile_name: Optional[str] = None,
985
+ *,
986
+ cwd: Optional[Path] = None,
987
+ project_dir: Optional[Path] = None,
988
+ complexity: int = 3,
989
+ build_phase_just_closed: bool = False,
990
+ files: Optional[List[str]] = None,
991
+ ) -> PipelineReport:
992
+ """Run the guard pipeline end-to-end. Never raises.
993
+
994
+ Args:
995
+ profile_name: "scalpel" | "standard" | "deep". Auto-selects when None.
996
+ cwd: Working directory for git probes and relative paths. Defaults to
997
+ the process CWD.
998
+ project_dir: Crew project directory (for skip_log check). Optional.
999
+ complexity: Complexity score passed to the semantic reviewer.
1000
+ build_phase_just_closed: Promotes scalpel → standard via auto_select.
1001
+ files: Optional explicit file list (useful for tests). When None,
1002
+ uses `git diff --name-only`.
1003
+
1004
+ Returns:
1005
+ A fully populated PipelineReport. `status` is "ok" unless the
1006
+ total-budget deadline was exceeded ("budget_exceeded") or an
1007
+ unrecoverable error happened ("error").
1008
+ """
1009
+ base = cwd or Path.cwd()
1010
+ t0 = time.monotonic()
1011
+
1012
+ if profile_name:
1013
+ profile: GuardProfile = get_profile(profile_name)
1014
+ else:
1015
+ profile = auto_select(build_phase_just_closed=build_phase_just_closed, cwd=base)
1016
+
1017
+ # Hard deadline for the whole pipeline
1018
+ deadline = t0 + profile.budget_seconds
1019
+
1020
+ # Share one file-list across all diff-driven checks
1021
+ try:
1022
+ change_set = files if files is not None else _changed_files(base)
1023
+ except Exception:
1024
+ change_set = []
1025
+
1026
+ report = PipelineReport(
1027
+ pipeline_version=PIPELINE_VERSION,
1028
+ profile=profile.name,
1029
+ budget_seconds=profile.budget_seconds,
1030
+ duration_ms=0,
1031
+ status="ok",
1032
+ )
1033
+
1034
+ per_check_budget = max(
1035
+ 0.1,
1036
+ profile.budget_seconds * _PER_CHECK_BUDGET_FRACTION / max(1, len(profile.checks)),
1037
+ )
1038
+
1039
+ for name in profile.checks:
1040
+ if time.monotonic() > deadline:
1041
+ report.status = "budget_exceeded"
1042
+ report.checks.append(CheckResult(
1043
+ name=name, status="skip", note="pipeline budget exceeded",
1044
+ ))
1045
+ continue
1046
+
1047
+ fn = CHECK_REGISTRY.get(name)
1048
+ if fn is None:
1049
+ report.checks.append(CheckResult(
1050
+ name=name, status="error", note=f"unknown check: {name}",
1051
+ ))
1052
+ continue
1053
+
1054
+ try:
1055
+ check_result = fn(
1056
+ change_set,
1057
+ budget_seconds=per_check_budget,
1058
+ cwd=base,
1059
+ project_dir=project_dir,
1060
+ complexity=complexity,
1061
+ )
1062
+ except Exception as exc: # fail-open — one check error doesn't kill the pipeline
1063
+ check_result = CheckResult(
1064
+ name=name, status="error", note=f"{type(exc).__name__}: {str(exc)[:120]}",
1065
+ )
1066
+ report.checks.append(check_result)
1067
+
1068
+ # Aggregate findings
1069
+ by_sev: Dict[str, int] = {SEVERITY_BLOCK: 0, SEVERITY_WARN: 0, SEVERITY_INFO: 0}
1070
+ total = 0
1071
+ for c in report.checks:
1072
+ for f in c.findings:
1073
+ total += 1
1074
+ by_sev[f.severity] = by_sev.get(f.severity, 0) + 1
1075
+ report.findings_by_severity = by_sev
1076
+ report.total_findings = total
1077
+ report.duration_ms = int((time.monotonic() - t0) * 1000)
1078
+ return report
1079
+
1080
+
1081
+ # ---------------------------------------------------------------------------
1082
+ # Briefing integration — write findings to a session-scoped file
1083
+ # ---------------------------------------------------------------------------
1084
+
1085
+ def _session_guard_dir() -> Path:
1086
+ import tempfile
1087
+ session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
1088
+ safe = session_id.replace("/", "_").replace("\\", "_").replace("..", "_")
1089
+ return Path(tempfile.gettempdir()) / "wicked-guard" / safe
1090
+
1091
+
1092
+ def write_briefing_file(report: PipelineReport) -> Optional[Path]:
1093
+ """Write the report where bootstrap.py can pick it up next session.
1094
+
1095
+ Uses ${TMPDIR}/wicked-guard/{session_id}/findings.json. Fail-open — returns
1096
+ None on error.
1097
+ """
1098
+ try:
1099
+ out_dir = _session_guard_dir()
1100
+ out_dir.mkdir(parents=True, exist_ok=True)
1101
+ path = out_dir / "findings.json"
1102
+ with open(path, "w", encoding="utf-8") as f:
1103
+ json.dump(report.to_dict(), f, indent=2)
1104
+ return path
1105
+ except Exception:
1106
+ return None
1107
+
1108
+
1109
+ def render_summary(report: PipelineReport, *, max_lines: int = 8) -> str:
1110
+ """Render a compact, user-facing summary suitable for the session message."""
1111
+ if report.total_findings == 0:
1112
+ return f"[Guard] {report.profile} pipeline ran ({report.duration_ms}ms) — no findings"
1113
+
1114
+ header = (
1115
+ f"[Guard] {report.profile} pipeline surfaced {report.total_findings} finding(s) "
1116
+ f"({report.duration_ms}ms) — block={report.findings_by_severity.get(SEVERITY_BLOCK, 0)} "
1117
+ f"warn={report.findings_by_severity.get(SEVERITY_WARN, 0)} "
1118
+ f"info={report.findings_by_severity.get(SEVERITY_INFO, 0)}. "
1119
+ "Review next-session briefing for details (not a hard block)."
1120
+ )
1121
+
1122
+ # Show top findings by severity (block first, then warn)
1123
+ priority = {SEVERITY_BLOCK: 0, SEVERITY_WARN: 1, SEVERITY_INFO: 2}
1124
+ all_findings: List[Finding] = []
1125
+ for c in report.checks:
1126
+ all_findings.extend(c.findings)
1127
+ all_findings.sort(key=lambda f: priority.get(f.severity, 99))
1128
+
1129
+ lines = [header]
1130
+ for finding in all_findings[:max_lines]:
1131
+ loc = f" @ {finding.file}:{finding.line}" if finding.file and finding.line else (
1132
+ f" @ {finding.file}" if finding.file else ""
1133
+ )
1134
+ lines.append(f" - [{finding.severity}] {finding.rule_id}: {finding.message}{loc}")
1135
+ if len(all_findings) > max_lines:
1136
+ lines.append(f" ... and {len(all_findings) - max_lines} more")
1137
+ return "\n".join(lines)
1138
+
1139
+
1140
+ # ---------------------------------------------------------------------------
1141
+ # Bus emission
1142
+ # ---------------------------------------------------------------------------
1143
+
1144
+ def emit_findings_event(report: PipelineReport) -> None:
1145
+ """Emit a wicked.guard.findings event. Fail-open.
1146
+
1147
+ Uses scripts/_bus.py::emit_event when available and the event is in the
1148
+ BUS_EVENT_MAP. When not, writes a JSONL line to a session-scoped
1149
+ fallback file so we still have a trail (matches the #443 pattern).
1150
+ """
1151
+ payload = {
1152
+ "profile": report.profile,
1153
+ "duration_ms": report.duration_ms,
1154
+ "status": report.status,
1155
+ "total_findings": report.total_findings,
1156
+ "findings_by_severity": report.findings_by_severity,
1157
+ # Do NOT ship raw findings (paths/snippets) to the bus — deny-list rules.
1158
+ # Downstream subscribers can load the briefing file by session_id.
1159
+ "session_id": os.environ.get("CLAUDE_SESSION_ID", "default"),
1160
+ }
1161
+ try:
1162
+ sys.path.insert(0, str(_SCRIPTS_ROOT))
1163
+ from _bus import emit_event, BUS_EVENT_MAP # type: ignore
1164
+ if "wicked.guard.findings" in BUS_EVENT_MAP:
1165
+ emit_event("wicked.guard.findings", payload)
1166
+ return
1167
+ except Exception:
1168
+ pass # fail open — fall through to JSONL fallback below
1169
+
1170
+ # Fallback: append a JSONL line so we still have a local trail.
1171
+ try:
1172
+ out_dir = _session_guard_dir()
1173
+ out_dir.mkdir(parents=True, exist_ok=True)
1174
+ with open(out_dir / "bus-fallback.jsonl", "a", encoding="utf-8") as f:
1175
+ f.write(json.dumps({"event": "wicked.guard.findings", "payload": payload}) + "\n")
1176
+ except Exception:
1177
+ pass # fail open — telemetry should never block the caller
1178
+
1179
+
1180
+ # ---------------------------------------------------------------------------
1181
+ # CLI — for debugging only
1182
+ # ---------------------------------------------------------------------------
1183
+
1184
+ def _main_cli(argv: List[str]) -> int:
1185
+ import argparse
1186
+
1187
+ ap = argparse.ArgumentParser(description="Run the guard pipeline")
1188
+ ap.add_argument("command", choices=["run"])
1189
+ ap.add_argument("--profile", default=None, help="scalpel|standard|deep")
1190
+ ap.add_argument("--project-dir", default=None)
1191
+ ap.add_argument("--complexity", type=int, default=3)
1192
+ ap.add_argument("--emit-bus", action="store_true")
1193
+ ap.add_argument("--write-briefing", action="store_true")
1194
+ args = ap.parse_args(argv)
1195
+
1196
+ project_dir = Path(args.project_dir) if args.project_dir else None
1197
+ report = run_pipeline(
1198
+ profile_name=args.profile,
1199
+ project_dir=project_dir,
1200
+ complexity=args.complexity,
1201
+ )
1202
+
1203
+ sys.stdout.write(json.dumps(report.to_dict(), indent=2))
1204
+ sys.stdout.write("\n")
1205
+
1206
+ if args.emit_bus:
1207
+ emit_findings_event(report)
1208
+ if args.write_briefing:
1209
+ write_briefing_file(report)
1210
+ return 0
1211
+
1212
+
1213
+ if __name__ == "__main__":
1214
+ sys.exit(_main_cli(sys.argv[1:]))