attune-ai 2.0.0__py3-none-any.whl

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 (457) hide show
  1. attune/__init__.py +358 -0
  2. attune/adaptive/__init__.py +13 -0
  3. attune/adaptive/task_complexity.py +127 -0
  4. attune/agent_monitoring.py +414 -0
  5. attune/cache/__init__.py +117 -0
  6. attune/cache/base.py +166 -0
  7. attune/cache/dependency_manager.py +256 -0
  8. attune/cache/hash_only.py +251 -0
  9. attune/cache/hybrid.py +457 -0
  10. attune/cache/storage.py +285 -0
  11. attune/cache_monitor.py +356 -0
  12. attune/cache_stats.py +298 -0
  13. attune/cli/__init__.py +152 -0
  14. attune/cli/__main__.py +12 -0
  15. attune/cli/commands/__init__.py +1 -0
  16. attune/cli/commands/batch.py +264 -0
  17. attune/cli/commands/cache.py +248 -0
  18. attune/cli/commands/help.py +331 -0
  19. attune/cli/commands/info.py +140 -0
  20. attune/cli/commands/inspect.py +436 -0
  21. attune/cli/commands/inspection.py +57 -0
  22. attune/cli/commands/memory.py +48 -0
  23. attune/cli/commands/metrics.py +92 -0
  24. attune/cli/commands/orchestrate.py +184 -0
  25. attune/cli/commands/patterns.py +207 -0
  26. attune/cli/commands/profiling.py +202 -0
  27. attune/cli/commands/provider.py +98 -0
  28. attune/cli/commands/routing.py +285 -0
  29. attune/cli/commands/setup.py +96 -0
  30. attune/cli/commands/status.py +235 -0
  31. attune/cli/commands/sync.py +166 -0
  32. attune/cli/commands/tier.py +121 -0
  33. attune/cli/commands/utilities.py +114 -0
  34. attune/cli/commands/workflow.py +579 -0
  35. attune/cli/core.py +32 -0
  36. attune/cli/parsers/__init__.py +68 -0
  37. attune/cli/parsers/batch.py +118 -0
  38. attune/cli/parsers/cache.py +65 -0
  39. attune/cli/parsers/help.py +41 -0
  40. attune/cli/parsers/info.py +26 -0
  41. attune/cli/parsers/inspect.py +66 -0
  42. attune/cli/parsers/metrics.py +42 -0
  43. attune/cli/parsers/orchestrate.py +61 -0
  44. attune/cli/parsers/patterns.py +54 -0
  45. attune/cli/parsers/provider.py +40 -0
  46. attune/cli/parsers/routing.py +110 -0
  47. attune/cli/parsers/setup.py +42 -0
  48. attune/cli/parsers/status.py +47 -0
  49. attune/cli/parsers/sync.py +31 -0
  50. attune/cli/parsers/tier.py +33 -0
  51. attune/cli/parsers/workflow.py +77 -0
  52. attune/cli/utils/__init__.py +1 -0
  53. attune/cli/utils/data.py +242 -0
  54. attune/cli/utils/helpers.py +68 -0
  55. attune/cli_legacy.py +3957 -0
  56. attune/cli_minimal.py +1159 -0
  57. attune/cli_router.py +437 -0
  58. attune/cli_unified.py +814 -0
  59. attune/config/__init__.py +66 -0
  60. attune/config/xml_config.py +286 -0
  61. attune/config.py +545 -0
  62. attune/coordination.py +870 -0
  63. attune/core.py +1511 -0
  64. attune/core_modules/__init__.py +15 -0
  65. attune/cost_tracker.py +626 -0
  66. attune/dashboard/__init__.py +41 -0
  67. attune/dashboard/app.py +512 -0
  68. attune/dashboard/simple_server.py +435 -0
  69. attune/dashboard/standalone_server.py +547 -0
  70. attune/discovery.py +306 -0
  71. attune/emergence.py +306 -0
  72. attune/exceptions.py +123 -0
  73. attune/feedback_loops.py +373 -0
  74. attune/hot_reload/README.md +473 -0
  75. attune/hot_reload/__init__.py +62 -0
  76. attune/hot_reload/config.py +83 -0
  77. attune/hot_reload/integration.py +229 -0
  78. attune/hot_reload/reloader.py +298 -0
  79. attune/hot_reload/watcher.py +183 -0
  80. attune/hot_reload/websocket.py +177 -0
  81. attune/levels.py +577 -0
  82. attune/leverage_points.py +441 -0
  83. attune/logging_config.py +261 -0
  84. attune/mcp/__init__.py +10 -0
  85. attune/mcp/server.py +506 -0
  86. attune/memory/__init__.py +237 -0
  87. attune/memory/claude_memory.py +469 -0
  88. attune/memory/config.py +224 -0
  89. attune/memory/control_panel.py +1290 -0
  90. attune/memory/control_panel_support.py +145 -0
  91. attune/memory/cross_session.py +845 -0
  92. attune/memory/edges.py +179 -0
  93. attune/memory/encryption.py +159 -0
  94. attune/memory/file_session.py +770 -0
  95. attune/memory/graph.py +570 -0
  96. attune/memory/long_term.py +913 -0
  97. attune/memory/long_term_types.py +99 -0
  98. attune/memory/mixins/__init__.py +25 -0
  99. attune/memory/mixins/backend_init_mixin.py +249 -0
  100. attune/memory/mixins/capabilities_mixin.py +208 -0
  101. attune/memory/mixins/handoff_mixin.py +208 -0
  102. attune/memory/mixins/lifecycle_mixin.py +49 -0
  103. attune/memory/mixins/long_term_mixin.py +352 -0
  104. attune/memory/mixins/promotion_mixin.py +109 -0
  105. attune/memory/mixins/short_term_mixin.py +182 -0
  106. attune/memory/nodes.py +179 -0
  107. attune/memory/redis_bootstrap.py +540 -0
  108. attune/memory/security/__init__.py +31 -0
  109. attune/memory/security/audit_logger.py +932 -0
  110. attune/memory/security/pii_scrubber.py +640 -0
  111. attune/memory/security/secrets_detector.py +678 -0
  112. attune/memory/short_term.py +2192 -0
  113. attune/memory/simple_storage.py +302 -0
  114. attune/memory/storage/__init__.py +15 -0
  115. attune/memory/storage_backend.py +167 -0
  116. attune/memory/summary_index.py +583 -0
  117. attune/memory/types.py +446 -0
  118. attune/memory/unified.py +182 -0
  119. attune/meta_workflows/__init__.py +74 -0
  120. attune/meta_workflows/agent_creator.py +248 -0
  121. attune/meta_workflows/builtin_templates.py +567 -0
  122. attune/meta_workflows/cli_commands/__init__.py +56 -0
  123. attune/meta_workflows/cli_commands/agent_commands.py +321 -0
  124. attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
  125. attune/meta_workflows/cli_commands/config_commands.py +232 -0
  126. attune/meta_workflows/cli_commands/memory_commands.py +182 -0
  127. attune/meta_workflows/cli_commands/template_commands.py +354 -0
  128. attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
  129. attune/meta_workflows/cli_meta_workflows.py +59 -0
  130. attune/meta_workflows/form_engine.py +292 -0
  131. attune/meta_workflows/intent_detector.py +409 -0
  132. attune/meta_workflows/models.py +569 -0
  133. attune/meta_workflows/pattern_learner.py +738 -0
  134. attune/meta_workflows/plan_generator.py +384 -0
  135. attune/meta_workflows/session_context.py +397 -0
  136. attune/meta_workflows/template_registry.py +229 -0
  137. attune/meta_workflows/workflow.py +984 -0
  138. attune/metrics/__init__.py +12 -0
  139. attune/metrics/collector.py +31 -0
  140. attune/metrics/prompt_metrics.py +194 -0
  141. attune/models/__init__.py +172 -0
  142. attune/models/__main__.py +13 -0
  143. attune/models/adaptive_routing.py +437 -0
  144. attune/models/auth_cli.py +444 -0
  145. attune/models/auth_strategy.py +450 -0
  146. attune/models/cli.py +655 -0
  147. attune/models/empathy_executor.py +354 -0
  148. attune/models/executor.py +257 -0
  149. attune/models/fallback.py +762 -0
  150. attune/models/provider_config.py +282 -0
  151. attune/models/registry.py +472 -0
  152. attune/models/tasks.py +359 -0
  153. attune/models/telemetry/__init__.py +71 -0
  154. attune/models/telemetry/analytics.py +594 -0
  155. attune/models/telemetry/backend.py +196 -0
  156. attune/models/telemetry/data_models.py +431 -0
  157. attune/models/telemetry/storage.py +489 -0
  158. attune/models/token_estimator.py +420 -0
  159. attune/models/validation.py +280 -0
  160. attune/monitoring/__init__.py +52 -0
  161. attune/monitoring/alerts.py +946 -0
  162. attune/monitoring/alerts_cli.py +448 -0
  163. attune/monitoring/multi_backend.py +271 -0
  164. attune/monitoring/otel_backend.py +362 -0
  165. attune/optimization/__init__.py +19 -0
  166. attune/optimization/context_optimizer.py +272 -0
  167. attune/orchestration/__init__.py +67 -0
  168. attune/orchestration/agent_templates.py +707 -0
  169. attune/orchestration/config_store.py +499 -0
  170. attune/orchestration/execution_strategies.py +2111 -0
  171. attune/orchestration/meta_orchestrator.py +1168 -0
  172. attune/orchestration/pattern_learner.py +696 -0
  173. attune/orchestration/real_tools.py +931 -0
  174. attune/pattern_cache.py +187 -0
  175. attune/pattern_library.py +542 -0
  176. attune/patterns/debugging/all_patterns.json +81 -0
  177. attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
  178. attune/patterns/refactoring_memory.json +89 -0
  179. attune/persistence.py +564 -0
  180. attune/platform_utils.py +265 -0
  181. attune/plugins/__init__.py +28 -0
  182. attune/plugins/base.py +361 -0
  183. attune/plugins/registry.py +268 -0
  184. attune/project_index/__init__.py +32 -0
  185. attune/project_index/cli.py +335 -0
  186. attune/project_index/index.py +667 -0
  187. attune/project_index/models.py +504 -0
  188. attune/project_index/reports.py +474 -0
  189. attune/project_index/scanner.py +777 -0
  190. attune/project_index/scanner_parallel.py +291 -0
  191. attune/prompts/__init__.py +61 -0
  192. attune/prompts/config.py +77 -0
  193. attune/prompts/context.py +177 -0
  194. attune/prompts/parser.py +285 -0
  195. attune/prompts/registry.py +313 -0
  196. attune/prompts/templates.py +208 -0
  197. attune/redis_config.py +302 -0
  198. attune/redis_memory.py +799 -0
  199. attune/resilience/__init__.py +56 -0
  200. attune/resilience/circuit_breaker.py +256 -0
  201. attune/resilience/fallback.py +179 -0
  202. attune/resilience/health.py +300 -0
  203. attune/resilience/retry.py +209 -0
  204. attune/resilience/timeout.py +135 -0
  205. attune/routing/__init__.py +43 -0
  206. attune/routing/chain_executor.py +433 -0
  207. attune/routing/classifier.py +217 -0
  208. attune/routing/smart_router.py +234 -0
  209. attune/routing/workflow_registry.py +343 -0
  210. attune/scaffolding/README.md +589 -0
  211. attune/scaffolding/__init__.py +35 -0
  212. attune/scaffolding/__main__.py +14 -0
  213. attune/scaffolding/cli.py +240 -0
  214. attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
  215. attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
  216. attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
  217. attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
  218. attune/socratic/__init__.py +256 -0
  219. attune/socratic/ab_testing.py +958 -0
  220. attune/socratic/blueprint.py +533 -0
  221. attune/socratic/cli.py +703 -0
  222. attune/socratic/collaboration.py +1114 -0
  223. attune/socratic/domain_templates.py +924 -0
  224. attune/socratic/embeddings.py +738 -0
  225. attune/socratic/engine.py +794 -0
  226. attune/socratic/explainer.py +682 -0
  227. attune/socratic/feedback.py +772 -0
  228. attune/socratic/forms.py +629 -0
  229. attune/socratic/generator.py +732 -0
  230. attune/socratic/llm_analyzer.py +637 -0
  231. attune/socratic/mcp_server.py +702 -0
  232. attune/socratic/session.py +312 -0
  233. attune/socratic/storage.py +667 -0
  234. attune/socratic/success.py +730 -0
  235. attune/socratic/visual_editor.py +860 -0
  236. attune/socratic/web_ui.py +958 -0
  237. attune/telemetry/__init__.py +39 -0
  238. attune/telemetry/agent_coordination.py +475 -0
  239. attune/telemetry/agent_tracking.py +367 -0
  240. attune/telemetry/approval_gates.py +545 -0
  241. attune/telemetry/cli.py +1231 -0
  242. attune/telemetry/commands/__init__.py +14 -0
  243. attune/telemetry/commands/dashboard_commands.py +696 -0
  244. attune/telemetry/event_streaming.py +409 -0
  245. attune/telemetry/feedback_loop.py +567 -0
  246. attune/telemetry/usage_tracker.py +591 -0
  247. attune/templates.py +754 -0
  248. attune/test_generator/__init__.py +38 -0
  249. attune/test_generator/__main__.py +14 -0
  250. attune/test_generator/cli.py +234 -0
  251. attune/test_generator/generator.py +355 -0
  252. attune/test_generator/risk_analyzer.py +216 -0
  253. attune/test_generator/templates/unit_test.py.jinja2 +272 -0
  254. attune/tier_recommender.py +384 -0
  255. attune/tools.py +183 -0
  256. attune/trust/__init__.py +28 -0
  257. attune/trust/circuit_breaker.py +579 -0
  258. attune/trust_building.py +527 -0
  259. attune/validation/__init__.py +19 -0
  260. attune/validation/xml_validator.py +281 -0
  261. attune/vscode_bridge.py +173 -0
  262. attune/workflow_commands.py +780 -0
  263. attune/workflow_patterns/__init__.py +33 -0
  264. attune/workflow_patterns/behavior.py +249 -0
  265. attune/workflow_patterns/core.py +76 -0
  266. attune/workflow_patterns/output.py +99 -0
  267. attune/workflow_patterns/registry.py +255 -0
  268. attune/workflow_patterns/structural.py +288 -0
  269. attune/workflows/__init__.py +539 -0
  270. attune/workflows/autonomous_test_gen.py +1268 -0
  271. attune/workflows/base.py +2667 -0
  272. attune/workflows/batch_processing.py +342 -0
  273. attune/workflows/bug_predict.py +1084 -0
  274. attune/workflows/builder.py +273 -0
  275. attune/workflows/caching.py +253 -0
  276. attune/workflows/code_review.py +1048 -0
  277. attune/workflows/code_review_adapters.py +312 -0
  278. attune/workflows/code_review_pipeline.py +722 -0
  279. attune/workflows/config.py +645 -0
  280. attune/workflows/dependency_check.py +644 -0
  281. attune/workflows/document_gen/__init__.py +25 -0
  282. attune/workflows/document_gen/config.py +30 -0
  283. attune/workflows/document_gen/report_formatter.py +162 -0
  284. attune/workflows/document_gen/workflow.py +1426 -0
  285. attune/workflows/document_manager.py +216 -0
  286. attune/workflows/document_manager_README.md +134 -0
  287. attune/workflows/documentation_orchestrator.py +1205 -0
  288. attune/workflows/history.py +510 -0
  289. attune/workflows/keyboard_shortcuts/__init__.py +39 -0
  290. attune/workflows/keyboard_shortcuts/generators.py +391 -0
  291. attune/workflows/keyboard_shortcuts/parsers.py +416 -0
  292. attune/workflows/keyboard_shortcuts/prompts.py +295 -0
  293. attune/workflows/keyboard_shortcuts/schema.py +193 -0
  294. attune/workflows/keyboard_shortcuts/workflow.py +509 -0
  295. attune/workflows/llm_base.py +363 -0
  296. attune/workflows/manage_docs.py +87 -0
  297. attune/workflows/manage_docs_README.md +134 -0
  298. attune/workflows/manage_documentation.py +821 -0
  299. attune/workflows/new_sample_workflow1.py +149 -0
  300. attune/workflows/new_sample_workflow1_README.md +150 -0
  301. attune/workflows/orchestrated_health_check.py +849 -0
  302. attune/workflows/orchestrated_release_prep.py +600 -0
  303. attune/workflows/output.py +413 -0
  304. attune/workflows/perf_audit.py +863 -0
  305. attune/workflows/pr_review.py +762 -0
  306. attune/workflows/progress.py +785 -0
  307. attune/workflows/progress_server.py +322 -0
  308. attune/workflows/progressive/README 2.md +454 -0
  309. attune/workflows/progressive/README.md +454 -0
  310. attune/workflows/progressive/__init__.py +82 -0
  311. attune/workflows/progressive/cli.py +219 -0
  312. attune/workflows/progressive/core.py +488 -0
  313. attune/workflows/progressive/orchestrator.py +723 -0
  314. attune/workflows/progressive/reports.py +520 -0
  315. attune/workflows/progressive/telemetry.py +274 -0
  316. attune/workflows/progressive/test_gen.py +495 -0
  317. attune/workflows/progressive/workflow.py +589 -0
  318. attune/workflows/refactor_plan.py +694 -0
  319. attune/workflows/release_prep.py +895 -0
  320. attune/workflows/release_prep_crew.py +969 -0
  321. attune/workflows/research_synthesis.py +404 -0
  322. attune/workflows/routing.py +168 -0
  323. attune/workflows/secure_release.py +593 -0
  324. attune/workflows/security_adapters.py +297 -0
  325. attune/workflows/security_audit.py +1329 -0
  326. attune/workflows/security_audit_phase3.py +355 -0
  327. attune/workflows/seo_optimization.py +633 -0
  328. attune/workflows/step_config.py +234 -0
  329. attune/workflows/telemetry_mixin.py +269 -0
  330. attune/workflows/test5.py +125 -0
  331. attune/workflows/test5_README.md +158 -0
  332. attune/workflows/test_coverage_boost_crew.py +849 -0
  333. attune/workflows/test_gen/__init__.py +52 -0
  334. attune/workflows/test_gen/ast_analyzer.py +249 -0
  335. attune/workflows/test_gen/config.py +88 -0
  336. attune/workflows/test_gen/data_models.py +38 -0
  337. attune/workflows/test_gen/report_formatter.py +289 -0
  338. attune/workflows/test_gen/test_templates.py +381 -0
  339. attune/workflows/test_gen/workflow.py +655 -0
  340. attune/workflows/test_gen.py +54 -0
  341. attune/workflows/test_gen_behavioral.py +477 -0
  342. attune/workflows/test_gen_parallel.py +341 -0
  343. attune/workflows/test_lifecycle.py +526 -0
  344. attune/workflows/test_maintenance.py +627 -0
  345. attune/workflows/test_maintenance_cli.py +590 -0
  346. attune/workflows/test_maintenance_crew.py +840 -0
  347. attune/workflows/test_runner.py +622 -0
  348. attune/workflows/tier_tracking.py +531 -0
  349. attune/workflows/xml_enhanced_crew.py +285 -0
  350. attune_ai-2.0.0.dist-info/METADATA +1026 -0
  351. attune_ai-2.0.0.dist-info/RECORD +457 -0
  352. attune_ai-2.0.0.dist-info/WHEEL +5 -0
  353. attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
  354. attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
  355. attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  356. attune_ai-2.0.0.dist-info/top_level.txt +5 -0
  357. attune_healthcare/__init__.py +13 -0
  358. attune_healthcare/monitors/__init__.py +9 -0
  359. attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
  360. attune_healthcare/monitors/monitoring/__init__.py +44 -0
  361. attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
  362. attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
  363. attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
  364. attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
  365. attune_llm/README.md +553 -0
  366. attune_llm/__init__.py +28 -0
  367. attune_llm/agent_factory/__init__.py +53 -0
  368. attune_llm/agent_factory/adapters/__init__.py +85 -0
  369. attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
  370. attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
  371. attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
  372. attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
  373. attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
  374. attune_llm/agent_factory/adapters/native.py +228 -0
  375. attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
  376. attune_llm/agent_factory/base.py +305 -0
  377. attune_llm/agent_factory/crews/__init__.py +67 -0
  378. attune_llm/agent_factory/crews/code_review.py +1113 -0
  379. attune_llm/agent_factory/crews/health_check.py +1262 -0
  380. attune_llm/agent_factory/crews/refactoring.py +1128 -0
  381. attune_llm/agent_factory/crews/security_audit.py +1018 -0
  382. attune_llm/agent_factory/decorators.py +287 -0
  383. attune_llm/agent_factory/factory.py +558 -0
  384. attune_llm/agent_factory/framework.py +193 -0
  385. attune_llm/agent_factory/memory_integration.py +328 -0
  386. attune_llm/agent_factory/resilient.py +320 -0
  387. attune_llm/agents_md/__init__.py +22 -0
  388. attune_llm/agents_md/loader.py +218 -0
  389. attune_llm/agents_md/parser.py +271 -0
  390. attune_llm/agents_md/registry.py +307 -0
  391. attune_llm/claude_memory.py +466 -0
  392. attune_llm/cli/__init__.py +8 -0
  393. attune_llm/cli/sync_claude.py +487 -0
  394. attune_llm/code_health.py +1313 -0
  395. attune_llm/commands/__init__.py +51 -0
  396. attune_llm/commands/context.py +375 -0
  397. attune_llm/commands/loader.py +301 -0
  398. attune_llm/commands/models.py +231 -0
  399. attune_llm/commands/parser.py +371 -0
  400. attune_llm/commands/registry.py +429 -0
  401. attune_llm/config/__init__.py +29 -0
  402. attune_llm/config/unified.py +291 -0
  403. attune_llm/context/__init__.py +22 -0
  404. attune_llm/context/compaction.py +455 -0
  405. attune_llm/context/manager.py +434 -0
  406. attune_llm/contextual_patterns.py +361 -0
  407. attune_llm/core.py +907 -0
  408. attune_llm/git_pattern_extractor.py +435 -0
  409. attune_llm/hooks/__init__.py +24 -0
  410. attune_llm/hooks/config.py +306 -0
  411. attune_llm/hooks/executor.py +289 -0
  412. attune_llm/hooks/registry.py +302 -0
  413. attune_llm/hooks/scripts/__init__.py +39 -0
  414. attune_llm/hooks/scripts/evaluate_session.py +201 -0
  415. attune_llm/hooks/scripts/first_time_init.py +285 -0
  416. attune_llm/hooks/scripts/pre_compact.py +207 -0
  417. attune_llm/hooks/scripts/session_end.py +183 -0
  418. attune_llm/hooks/scripts/session_start.py +163 -0
  419. attune_llm/hooks/scripts/suggest_compact.py +225 -0
  420. attune_llm/learning/__init__.py +30 -0
  421. attune_llm/learning/evaluator.py +438 -0
  422. attune_llm/learning/extractor.py +514 -0
  423. attune_llm/learning/storage.py +560 -0
  424. attune_llm/levels.py +227 -0
  425. attune_llm/pattern_confidence.py +414 -0
  426. attune_llm/pattern_resolver.py +272 -0
  427. attune_llm/pattern_summary.py +350 -0
  428. attune_llm/providers.py +967 -0
  429. attune_llm/routing/__init__.py +32 -0
  430. attune_llm/routing/model_router.py +362 -0
  431. attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
  432. attune_llm/security/PHASE2_COMPLETE.md +384 -0
  433. attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  434. attune_llm/security/QUICK_REFERENCE.md +316 -0
  435. attune_llm/security/README.md +262 -0
  436. attune_llm/security/__init__.py +62 -0
  437. attune_llm/security/audit_logger.py +929 -0
  438. attune_llm/security/audit_logger_example.py +152 -0
  439. attune_llm/security/pii_scrubber.py +640 -0
  440. attune_llm/security/secrets_detector.py +678 -0
  441. attune_llm/security/secrets_detector_example.py +304 -0
  442. attune_llm/security/secure_memdocs.py +1192 -0
  443. attune_llm/security/secure_memdocs_example.py +278 -0
  444. attune_llm/session_status.py +745 -0
  445. attune_llm/state.py +246 -0
  446. attune_llm/utils/__init__.py +5 -0
  447. attune_llm/utils/tokens.py +349 -0
  448. attune_software/SOFTWARE_PLUGIN_README.md +57 -0
  449. attune_software/__init__.py +13 -0
  450. attune_software/cli/__init__.py +120 -0
  451. attune_software/cli/inspect.py +362 -0
  452. attune_software/cli.py +574 -0
  453. attune_software/plugin.py +188 -0
  454. workflow_scaffolding/__init__.py +11 -0
  455. workflow_scaffolding/__main__.py +12 -0
  456. workflow_scaffolding/cli.py +206 -0
  457. workflow_scaffolding/generator.py +265 -0
attune_llm/levels.py ADDED
@@ -0,0 +1,227 @@
1
+ """Empathy Level Definitions
2
+
3
+ Defines behavior for each of the 5 empathy levels.
4
+
5
+ Copyright 2025 Smart AI Memory, LLC
6
+ Licensed under Fair Source 0.9
7
+ """
8
+
9
+ from enum import IntEnum
10
+
11
+
12
+ class EmpathyLevel(IntEnum):
13
+ """The 5 levels of AI-human collaboration empathy.
14
+
15
+ Each level builds on previous levels.
16
+ """
17
+
18
+ REACTIVE = 1 # Help after being asked
19
+ GUIDED = 2 # Ask clarifying questions
20
+ PROACTIVE = 3 # Act before being asked (patterns)
21
+ ANTICIPATORY = 4 # Predict future needs (trajectory)
22
+ SYSTEMS = 5 # Build structures that scale (cross-domain)
23
+
24
+ @classmethod
25
+ def get_description(cls, level: int) -> str:
26
+ """Get description of empathy level"""
27
+ descriptions = {
28
+ 1: "Reactive: Help after being asked. Traditional Q&A.",
29
+ 2: "Guided: Ask clarifying questions. Collaborative exploration.",
30
+ 3: "Proactive: Act before being asked. Pattern detection.",
31
+ 4: "Anticipatory: Predict future needs. Trajectory analysis.",
32
+ 5: "Systems: Build structures that scale. Cross-domain learning.",
33
+ }
34
+ return descriptions.get(level, "Unknown level")
35
+
36
+ @classmethod
37
+ def get_system_prompt(cls, level: int) -> str:
38
+ """Get system prompt for operating at specific level"""
39
+ base = """You are an AI assistant using the Empathy Framework for collaboration.
40
+
41
+ Your responses should be:
42
+ - Honest about experience (not predictive claims)
43
+ - Clear about reasoning
44
+ - Transparent about confidence
45
+ - Focused on user's actual needs
46
+ """
47
+
48
+ level_specific = {
49
+ 1: """
50
+ LEVEL 1 (REACTIVE):
51
+ - Respond directly to user's question
52
+ - Be accurate and complete
53
+ - Don't anticipate or assume
54
+ """,
55
+ 2: """
56
+ LEVEL 2 (GUIDED):
57
+ - Ask 1-2 calibrated questions before responding
58
+ - Understand user's actual goal
59
+ - Tailor response to their specific situation
60
+
61
+ Calibrated questions:
62
+ - "What are you hoping to accomplish?"
63
+ - "How does this fit into your workflow?"
64
+ - "What would make this most helpful?"
65
+ """,
66
+ 3: """
67
+ LEVEL 3 (PROACTIVE):
68
+ - Detect patterns in user behavior
69
+ - Act before being asked when confident
70
+ - Explain your reasoning
71
+ - Provide escape hatch if wrong
72
+
73
+ When acting proactively:
74
+ 1. Reference the detected pattern
75
+ 2. Explain why you're acting
76
+ 3. Show what you did
77
+ 4. Ask if this was helpful
78
+ """,
79
+ 4: """
80
+ LEVEL 4 (ANTICIPATORY):
81
+ - Analyze system trajectory
82
+ - Predict future bottlenecks BEFORE they occur
83
+ - Alert user with time to prevent issues
84
+ - Design structural relief in advance
85
+
86
+ Anticipatory format:
87
+ 1. Current state analysis
88
+ 2. Trajectory prediction (where headed)
89
+ 3. Alert about future bottleneck
90
+ 4. Prevention steps (actionable)
91
+ 5. Reasoning (based on experience)
92
+
93
+ Use phrases like:
94
+ - "In our experience..."
95
+ - "This trajectory suggests..."
96
+ - "Alert: Before this becomes critical..."
97
+ - NOT "Will happen in X days" (be honest, not predictive)
98
+ """,
99
+ 5: """
100
+ LEVEL 5 (SYSTEMS):
101
+ - Identify cross-domain patterns
102
+ - Build reusable structures
103
+ - Contribute to shared knowledge
104
+ - Enable scaling
105
+
106
+ Pattern contribution:
107
+ 1. Identify core principle (domain-agnostic)
108
+ 2. Show applicability to other domains
109
+ 3. Provide adaptation guidelines
110
+ """,
111
+ }
112
+
113
+ return base + level_specific.get(level, "")
114
+
115
+ @classmethod
116
+ def get_temperature_recommendation(cls, level: int) -> float:
117
+ """Get recommended temperature for each level.
118
+
119
+ Higher levels benefit from lower temperature (more focused).
120
+ """
121
+ temps = {
122
+ 1: 0.7, # Reactive: Some creativity okay
123
+ 2: 0.6, # Guided: Focused questions
124
+ 3: 0.5, # Proactive: Pattern recognition
125
+ 4: 0.3, # Anticipatory: Precise analysis
126
+ 5: 0.4, # Systems: Structured abstraction
127
+ }
128
+ return temps.get(level, 0.7)
129
+
130
+ @classmethod
131
+ def get_required_context(cls, level: int) -> dict[str, bool]:
132
+ """Get context requirements for each level.
133
+
134
+ Returns dict of {context_type: required}
135
+ """
136
+ requirements = {
137
+ 1: {
138
+ "conversation_history": False,
139
+ "user_patterns": False,
140
+ "trajectory_data": False,
141
+ "pattern_library": False,
142
+ },
143
+ 2: {
144
+ "conversation_history": True, # Need recent context
145
+ "user_patterns": False,
146
+ "trajectory_data": False,
147
+ "pattern_library": False,
148
+ },
149
+ 3: {
150
+ "conversation_history": True,
151
+ "user_patterns": True, # Need detected patterns
152
+ "trajectory_data": False,
153
+ "pattern_library": False,
154
+ },
155
+ 4: {
156
+ "conversation_history": True,
157
+ "user_patterns": True,
158
+ "trajectory_data": True, # Need historical data
159
+ "pattern_library": False,
160
+ },
161
+ 5: {
162
+ "conversation_history": True,
163
+ "user_patterns": True,
164
+ "trajectory_data": True,
165
+ "pattern_library": True, # Need cross-domain patterns
166
+ },
167
+ }
168
+
169
+ return requirements.get(level, requirements[1])
170
+
171
+ @classmethod
172
+ def get_max_tokens_recommendation(cls, level: int) -> int:
173
+ """Get recommended max_tokens for each level.
174
+
175
+ Higher levels often need longer responses.
176
+ """
177
+ return {
178
+ 1: 1024, # Reactive: Concise answers
179
+ 2: 1536, # Guided: Questions + answer
180
+ 3: 2048, # Proactive: Explanation + action
181
+ 4: 4096, # Anticipatory: Full analysis
182
+ 5: 4096, # Systems: Pattern abstraction
183
+ }.get(level, 1024)
184
+
185
+ @classmethod
186
+ def should_use_json_mode(cls, level: int) -> bool:
187
+ """Determine if JSON mode is beneficial for level.
188
+
189
+ Levels 4-5 benefit from structured output.
190
+ """
191
+ return level >= 4
192
+
193
+ @classmethod
194
+ def get_typical_use_cases(cls, level: int) -> list[str]:
195
+ """Get typical use cases for each level"""
196
+ return {
197
+ 1: [
198
+ "One-off questions",
199
+ "Simple information lookup",
200
+ "No context needed",
201
+ "Compliance scenarios",
202
+ ],
203
+ 2: [
204
+ "Ambiguous requests",
205
+ "Multiple valid approaches",
206
+ "Learning user preferences",
207
+ "High-stakes decisions",
208
+ ],
209
+ 3: [
210
+ "Established workflows",
211
+ "Repetitive tasks",
212
+ "Time-sensitive operations",
213
+ "Pattern-rich interactions",
214
+ ],
215
+ 4: [
216
+ "Predictable future events",
217
+ "Growth trajectory visible",
218
+ "Structural issues emerging",
219
+ "Prevention better than cure",
220
+ ],
221
+ 5: [
222
+ "Multiple similar domains",
223
+ "Reusable patterns identified",
224
+ "Scaling requirements",
225
+ "Cross-pollination opportunities",
226
+ ],
227
+ }.get(level, [])
@@ -0,0 +1,414 @@
1
+ """Pattern Confidence Scoring
2
+
3
+ Tracks how often stored fixes resolve similar issues,
4
+ building confidence scores over time.
5
+
6
+ Usage:
7
+ from attune_llm.pattern_confidence import PatternConfidenceTracker
8
+
9
+ tracker = PatternConfidenceTracker("./patterns")
10
+
11
+ # Record when a pattern is suggested
12
+ tracker.record_suggestion("bug_20250915_abc123")
13
+
14
+ # Record when a pattern fix is applied
15
+ tracker.record_application("bug_20250915_abc123", successful=True)
16
+
17
+ # Get confidence stats
18
+ stats = tracker.get_pattern_stats("bug_20250915_abc123")
19
+ # Returns: {"times_suggested": 15, "times_applied": 12, "success_rate": 0.92}
20
+
21
+ Author: Empathy Framework Team
22
+ Version: 2.1.3
23
+ License: Fair Source 0.9
24
+ """
25
+
26
+ import json
27
+ import logging
28
+ from dataclasses import asdict, dataclass, field
29
+ from datetime import datetime
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @dataclass
37
+ class PatternUsageStats:
38
+ """Statistics for a single pattern's usage."""
39
+
40
+ pattern_id: str
41
+ times_suggested: int = 0
42
+ times_applied: int = 0
43
+ times_successful: int = 0
44
+ times_unsuccessful: int = 0
45
+ last_suggested: str | None = None
46
+ last_applied: str | None = None
47
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
48
+ feedback: list[dict] = field(default_factory=list)
49
+
50
+ @property
51
+ def success_rate(self) -> float:
52
+ """Calculate success rate of applied fixes."""
53
+ total = self.times_successful + self.times_unsuccessful
54
+ if total == 0:
55
+ return 0.0
56
+ return self.times_successful / total
57
+
58
+ @property
59
+ def application_rate(self) -> float:
60
+ """Calculate how often suggestions lead to applications."""
61
+ if self.times_suggested == 0:
62
+ return 0.0
63
+ return self.times_applied / self.times_suggested
64
+
65
+ @property
66
+ def confidence_score(self) -> float:
67
+ """Calculate overall confidence score (0.0 - 1.0)."""
68
+ # Base confidence from success rate
69
+ base = self.success_rate
70
+
71
+ # Boost for high application rate
72
+ if self.application_rate > 0.5:
73
+ base += 0.1
74
+
75
+ # Boost for volume
76
+ if self.times_applied >= 5:
77
+ base += 0.1
78
+
79
+ # Decay for low usage
80
+ if self.times_suggested < 2:
81
+ base *= 0.8
82
+
83
+ return min(max(base, 0.0), 1.0)
84
+
85
+
86
+ class PatternConfidenceTracker:
87
+ """Tracks pattern usage and calculates confidence scores.
88
+
89
+ Stores usage data in patterns/confidence/usage_stats.json
90
+ """
91
+
92
+ def __init__(self, patterns_dir: str = "./patterns"):
93
+ self.patterns_dir = Path(patterns_dir)
94
+ self.confidence_dir = self.patterns_dir / "confidence"
95
+ self.stats_file = self.confidence_dir / "usage_stats.json"
96
+ self._stats: dict[str, PatternUsageStats] = {}
97
+ self._loaded = False
98
+
99
+ def _ensure_loaded(self) -> None:
100
+ """Ensure stats are loaded from disk."""
101
+ if self._loaded:
102
+ return
103
+
104
+ self.confidence_dir.mkdir(parents=True, exist_ok=True)
105
+
106
+ if self.stats_file.exists():
107
+ try:
108
+ with open(self.stats_file, encoding="utf-8") as f:
109
+ data = json.load(f)
110
+ for pattern_id, stats_dict in data.get("patterns", {}).items():
111
+ self._stats[pattern_id] = PatternUsageStats(
112
+ pattern_id=pattern_id,
113
+ **{k: v for k, v in stats_dict.items() if k != "pattern_id"},
114
+ )
115
+ except (json.JSONDecodeError, OSError) as e:
116
+ logger.warning("Failed to load usage stats: %s", e)
117
+
118
+ self._loaded = True
119
+
120
+ def _save(self) -> None:
121
+ """Save stats to disk."""
122
+ self.confidence_dir.mkdir(parents=True, exist_ok=True)
123
+
124
+ data = {
125
+ "version": "1.0",
126
+ "updated_at": datetime.now().isoformat(),
127
+ "patterns": {
128
+ pattern_id: {
129
+ **asdict(stats),
130
+ "success_rate": stats.success_rate,
131
+ "application_rate": stats.application_rate,
132
+ "confidence_score": stats.confidence_score,
133
+ }
134
+ for pattern_id, stats in self._stats.items()
135
+ },
136
+ }
137
+
138
+ try:
139
+ with open(self.stats_file, "w", encoding="utf-8") as f:
140
+ json.dump(data, f, indent=2, default=str)
141
+ except OSError as e:
142
+ logger.error("Failed to save usage stats: %s", e)
143
+
144
+ def _get_or_create_stats(self, pattern_id: str) -> PatternUsageStats:
145
+ """Get or create stats for a pattern."""
146
+ self._ensure_loaded()
147
+ if pattern_id not in self._stats:
148
+ self._stats[pattern_id] = PatternUsageStats(pattern_id=pattern_id)
149
+ return self._stats[pattern_id]
150
+
151
+ def record_suggestion(self, pattern_id: str) -> None:
152
+ """Record that a pattern was suggested to the user.
153
+
154
+ Call this when a pattern is shown as a potential fix.
155
+ """
156
+ stats = self._get_or_create_stats(pattern_id)
157
+ stats.times_suggested += 1
158
+ stats.last_suggested = datetime.now().isoformat()
159
+ self._save()
160
+ logger.debug("Pattern %s suggested (total: %d)", pattern_id, stats.times_suggested)
161
+
162
+ def record_application(
163
+ self,
164
+ pattern_id: str,
165
+ successful: bool = True,
166
+ notes: str | None = None,
167
+ ) -> None:
168
+ """Record that a pattern fix was applied.
169
+
170
+ Args:
171
+ pattern_id: The pattern that was applied
172
+ successful: Whether the fix resolved the issue
173
+ notes: Optional feedback notes
174
+
175
+ """
176
+ stats = self._get_or_create_stats(pattern_id)
177
+ stats.times_applied += 1
178
+ stats.last_applied = datetime.now().isoformat()
179
+
180
+ if successful:
181
+ stats.times_successful += 1
182
+ else:
183
+ stats.times_unsuccessful += 1
184
+
185
+ if notes:
186
+ stats.feedback.append(
187
+ {
188
+ "date": datetime.now().isoformat(),
189
+ "successful": successful,
190
+ "notes": notes,
191
+ },
192
+ )
193
+
194
+ self._save()
195
+ logger.debug(
196
+ "Pattern %s applied (success=%s, rate=%.0f%%)",
197
+ pattern_id,
198
+ successful,
199
+ stats.success_rate * 100,
200
+ )
201
+
202
+ def get_pattern_stats(self, pattern_id: str) -> dict[str, Any]:
203
+ """Get usage statistics for a pattern.
204
+
205
+ Returns:
206
+ Dict with usage stats and calculated scores
207
+
208
+ """
209
+ stats = self._get_or_create_stats(pattern_id)
210
+ return {
211
+ "pattern_id": pattern_id,
212
+ "times_suggested": stats.times_suggested,
213
+ "times_applied": stats.times_applied,
214
+ "times_successful": stats.times_successful,
215
+ "times_unsuccessful": stats.times_unsuccessful,
216
+ "success_rate": round(stats.success_rate, 2),
217
+ "application_rate": round(stats.application_rate, 2),
218
+ "confidence_score": round(stats.confidence_score, 2),
219
+ "last_suggested": stats.last_suggested,
220
+ "last_applied": stats.last_applied,
221
+ "feedback_count": len(stats.feedback),
222
+ }
223
+
224
+ def get_all_stats(self) -> list[dict[str, Any]]:
225
+ """Get stats for all tracked patterns."""
226
+ self._ensure_loaded()
227
+ return [self.get_pattern_stats(pid) for pid in self._stats]
228
+
229
+ def get_top_patterns(self, limit: int = 10) -> list[dict[str, Any]]:
230
+ """Get top patterns by confidence score.
231
+
232
+ Args:
233
+ limit: Maximum patterns to return
234
+
235
+ Returns:
236
+ List of pattern stats, sorted by confidence
237
+
238
+ """
239
+ self._ensure_loaded()
240
+ all_stats = self.get_all_stats()
241
+ sorted_stats = sorted(
242
+ all_stats,
243
+ key=lambda s: (s["confidence_score"], s["times_applied"]),
244
+ reverse=True,
245
+ )
246
+ return sorted_stats[:limit]
247
+
248
+ def get_stale_patterns(self, days: int = 90) -> list[dict[str, Any]]:
249
+ """Get patterns that haven't been used recently.
250
+
251
+ Args:
252
+ days: Number of days to consider stale
253
+
254
+ Returns:
255
+ List of stale pattern stats
256
+
257
+ """
258
+ self._ensure_loaded()
259
+ stale = []
260
+ cutoff = datetime.now().timestamp() - (days * 86400)
261
+
262
+ for pattern_id, stats in self._stats.items():
263
+ last_used = stats.last_applied or stats.last_suggested
264
+ if last_used:
265
+ try:
266
+ last_ts = datetime.fromisoformat(last_used).timestamp()
267
+ if last_ts < cutoff:
268
+ stale.append(self.get_pattern_stats(pattern_id))
269
+ except ValueError:
270
+ continue
271
+
272
+ return stale
273
+
274
+ def update_pattern_summary(self) -> bool:
275
+ """Update the patterns_summary.md with confidence scores.
276
+
277
+ This adds a confidence section to the generated summary.
278
+ """
279
+ try:
280
+ # Get top patterns
281
+ top_patterns = self.get_top_patterns(5)
282
+ if not top_patterns:
283
+ return False
284
+
285
+ # Generate additional markdown
286
+ confidence_section = [
287
+ "## Pattern Confidence (Top 5)",
288
+ "",
289
+ ]
290
+
291
+ for p in top_patterns:
292
+ score = p["confidence_score"]
293
+ icon = "🟢" if score >= 0.8 else "🟡" if score >= 0.5 else "🔴"
294
+ confidence_section.append(
295
+ f"- {icon} **{p['pattern_id']}**: {score:.0%} confidence "
296
+ f"({p['times_applied']} applied, {p['times_successful']} successful)",
297
+ )
298
+
299
+ confidence_section.append("")
300
+
301
+ # Read existing summary
302
+ summary_path = Path("./.claude/patterns_summary.md")
303
+ if not summary_path.exists():
304
+ return False
305
+
306
+ content = summary_path.read_text(encoding="utf-8")
307
+
308
+ # Insert before "How to Use" section
309
+ insert_point = content.find("## How to Use These Patterns")
310
+ if insert_point == -1:
311
+ content += "\n" + "\n".join(confidence_section)
312
+ else:
313
+ content = (
314
+ content[:insert_point]
315
+ + "\n".join(confidence_section)
316
+ + "\n"
317
+ + content[insert_point:]
318
+ )
319
+
320
+ summary_path.write_text(content, encoding="utf-8")
321
+ logger.info("Updated patterns_summary.md with confidence scores")
322
+ return True
323
+
324
+ except Exception as e:
325
+ logger.error("Failed to update summary: %s", e)
326
+ return False
327
+
328
+
329
+ def main():
330
+ """CLI entry point for pattern confidence."""
331
+ import argparse
332
+
333
+ parser = argparse.ArgumentParser(description="Pattern confidence tracking")
334
+ parser.add_argument("--patterns-dir", default="./patterns", help="Patterns directory")
335
+
336
+ subparsers = parser.add_subparsers(dest="command")
337
+
338
+ # List command
339
+ list_parser = subparsers.add_parser("list", help="List all pattern stats")
340
+ list_parser.add_argument("--top", type=int, default=10, help="Show top N patterns")
341
+
342
+ # Stats command
343
+ stats_parser = subparsers.add_parser("stats", help="Show stats for a pattern")
344
+ stats_parser.add_argument("pattern_id", help="Pattern ID")
345
+
346
+ # Record command
347
+ record_parser = subparsers.add_parser("record", help="Record pattern usage")
348
+ record_parser.add_argument("pattern_id", help="Pattern ID")
349
+ record_parser.add_argument("--suggested", action="store_true", help="Record suggestion")
350
+ record_parser.add_argument("--applied", action="store_true", help="Record application")
351
+ record_parser.add_argument("--success", action="store_true", help="Mark as successful")
352
+ record_parser.add_argument("--notes", help="Feedback notes")
353
+
354
+ # Stale command
355
+ stale_parser = subparsers.add_parser("stale", help="List stale patterns")
356
+ stale_parser.add_argument("--days", type=int, default=90, help="Days to consider stale")
357
+
358
+ args = parser.parse_args()
359
+
360
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
361
+
362
+ tracker = PatternConfidenceTracker(args.patterns_dir)
363
+
364
+ if args.command == "list":
365
+ top = tracker.get_top_patterns(args.top)
366
+ if not top:
367
+ print("No pattern usage data recorded yet.")
368
+ return
369
+
370
+ print(f"\nTop {len(top)} Patterns by Confidence:\n")
371
+ for i, p in enumerate(top, 1):
372
+ score = p["confidence_score"]
373
+ icon = "🟢" if score >= 0.8 else "🟡" if score >= 0.5 else "🔴"
374
+ print(f"{i}. {icon} {p['pattern_id']}")
375
+ print(f" Confidence: {score:.0%}")
376
+ print(f" Applied: {p['times_applied']} times ({p['times_successful']} successful)")
377
+ print(f" Suggested: {p['times_suggested']} times")
378
+ print()
379
+
380
+ elif args.command == "stats":
381
+ stats = tracker.get_pattern_stats(args.pattern_id)
382
+ print(f"\nStats for {args.pattern_id}:\n")
383
+ for key, value in stats.items():
384
+ print(f" {key}: {value}")
385
+
386
+ elif args.command == "record":
387
+ if args.suggested:
388
+ tracker.record_suggestion(args.pattern_id)
389
+ print(f"✓ Recorded suggestion for {args.pattern_id}")
390
+
391
+ if args.applied:
392
+ tracker.record_application(
393
+ args.pattern_id,
394
+ successful=args.success,
395
+ notes=args.notes,
396
+ )
397
+ print(f"✓ Recorded application for {args.pattern_id} (success={args.success})")
398
+
399
+ elif args.command == "stale":
400
+ stale = tracker.get_stale_patterns(args.days)
401
+ if not stale:
402
+ print(f"No patterns unused in the last {args.days} days.")
403
+ return
404
+
405
+ print(f"\nPatterns unused in {args.days}+ days:\n")
406
+ for p in stale:
407
+ print(f" - {p['pattern_id']} (last used: {p['last_applied'] or p['last_suggested']})")
408
+
409
+ else:
410
+ parser.print_help()
411
+
412
+
413
+ if __name__ == "__main__":
414
+ main()