reprompt-cli 1.9.0__tar.gz → 1.9.1__tar.gz

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 (298) hide show
  1. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/PKG-INFO +1 -1
  2. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/pyproject.toml +1 -1
  3. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/__init__.py +1 -1
  4. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/cli.py +12 -6
  5. reprompt_cli-1.9.1/src/reprompt/core/lint.py +242 -0
  6. reprompt_cli-1.9.1/tests/test_lint.py +237 -0
  7. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/uv.lock +1 -1
  8. reprompt_cli-1.9.0/src/reprompt/core/lint.py +0 -112
  9. reprompt_cli-1.9.0/tests/test_lint.py +0 -81
  10. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.editorconfig +0 -0
  11. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/dependabot.yml +0 -0
  17. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/workflows/ci.yml +0 -0
  18. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/workflows/publish.yml +0 -0
  19. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.gitignore +0 -0
  20. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.pre-commit-config.yaml +0 -0
  21. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.pre-commit-hooks.yaml +0 -0
  22. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.testmondata-shm +0 -0
  23. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.testmondata-wal +0 -0
  24. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CHANGELOG.md +0 -0
  25. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CODE_OF_CONDUCT.md +0 -0
  26. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CONTRIBUTING.md +0 -0
  27. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/LICENSE +0 -0
  28. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/README.md +0 -0
  29. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/SECURITY.md +0 -0
  30. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  31. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/action.yml +0 -0
  32. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/demo.gif +0 -0
  33. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-128.png +0 -0
  34. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-16.png +0 -0
  35. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-256.png +0 -0
  36. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-32.png +0 -0
  37. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-48.png +0 -0
  38. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-512.png +0 -0
  39. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-96.png +0 -0
  40. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon.svg +0 -0
  41. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-128.png +0 -0
  42. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-16.png +0 -0
  43. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-256.png +0 -0
  44. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-32.png +0 -0
  45. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-48.png +0 -0
  46. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-512.png +0 -0
  47. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-96.png +0 -0
  48. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon.svg +0 -0
  49. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-128.png +0 -0
  50. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-16.png +0 -0
  51. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-256.png +0 -0
  52. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-32.png +0 -0
  53. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-48.png +0 -0
  54. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-512.png +0 -0
  55. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-96.png +0 -0
  56. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon.svg +0 -0
  57. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-128.png +0 -0
  58. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-16.png +0 -0
  59. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-256.png +0 -0
  60. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-32.png +0 -0
  61. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-48.png +0 -0
  62. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-512.png +0 -0
  63. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-96.png +0 -0
  64. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon.svg +0 -0
  65. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/generate.sh +0 -0
  66. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  67. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  68. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/scripts/generate_demo_data.py +0 -0
  69. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/__init__.py +0 -0
  70. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/aider.py +0 -0
  71. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/base.py +0 -0
  72. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/chatgpt.py +0 -0
  73. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_chat.py +0 -0
  74. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_code.py +0 -0
  75. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/cline.py +0 -0
  76. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/codex.py +0 -0
  77. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/cursor.py +0 -0
  78. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/filters.py +0 -0
  79. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/gemini.py +0 -0
  80. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/openclaw.py +0 -0
  81. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/__init__.py +0 -0
  82. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/handler.py +0 -0
  83. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/host.py +0 -0
  84. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/manifest.py +0 -0
  85. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/protocol.py +0 -0
  86. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/__init__.py +0 -0
  87. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/telemetry.py +0 -0
  88. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/wrapped.py +0 -0
  89. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/config.py +0 -0
  90. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/__init__.py +0 -0
  91. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/agent.py +0 -0
  92. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/analyzer.py +0 -0
  93. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/compress.py +0 -0
  94. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/conversation.py +0 -0
  95. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/cost.py +0 -0
  96. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/dashboard.py +0 -0
  97. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/dedup.py +0 -0
  98. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/digest.py +0 -0
  99. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/distill.py +0 -0
  100. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/effectiveness.py +0 -0
  101. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/extractors.py +0 -0
  102. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/extractors_zh.py +0 -0
  103. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/insights.py +0 -0
  104. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/lang_detect.py +0 -0
  105. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/library.py +0 -0
  106. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/merge_view.py +0 -0
  107. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/models.py +0 -0
  108. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/persona.py +0 -0
  109. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/pipeline.py +0 -0
  110. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/privacy.py +0 -0
  111. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/privacy_scan.py +0 -0
  112. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/prompt_dna.py +0 -0
  113. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/recommend.py +0 -0
  114. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/repetition.py +0 -0
  115. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/scorer.py +0 -0
  116. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/segmenter.py +0 -0
  117. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_meta.py +0 -0
  118. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_quality.py +0 -0
  119. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_type.py +0 -0
  120. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/style.py +0 -0
  121. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/suggestions.py +0 -0
  122. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/templates.py +0 -0
  123. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/timeutil.py +0 -0
  124. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/trends.py +0 -0
  125. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/wrapped.py +0 -0
  126. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/demo.py +0 -0
  127. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/__init__.py +0 -0
  128. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/base.py +0 -0
  129. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/local_embed.py +0 -0
  130. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/ollama.py +0 -0
  131. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/openai_embed.py +0 -0
  132. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/tfidf.py +0 -0
  133. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/mcp.py +0 -0
  134. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/mcp_main.py +0 -0
  135. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/__init__.py +0 -0
  136. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/agent_terminal.py +0 -0
  137. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/chartjs.min.js +0 -0
  138. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/compress_terminal.py +0 -0
  139. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/dashboard_terminal.py +0 -0
  140. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/distill_terminal.py +0 -0
  141. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/export.py +0 -0
  142. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/html_report.py +0 -0
  143. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/json_out.py +0 -0
  144. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/markdown.py +0 -0
  145. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/repetition_terminal.py +0 -0
  146. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/sessions_terminal.py +0 -0
  147. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/terminal.py +0 -0
  148. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_html.py +0 -0
  149. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_terminal.py +0 -0
  150. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/py.typed +0 -0
  151. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/__init__.py +0 -0
  152. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/client.py +0 -0
  153. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/clipboard.py +0 -0
  154. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/storage/__init__.py +0 -0
  155. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/storage/db.py +0 -0
  156. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/__init__.py +0 -0
  157. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/collector.py +0 -0
  158. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/consent.py +0 -0
  159. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/events.py +0 -0
  160. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/prompt.py +0 -0
  161. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/queue.py +0 -0
  162. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/sender.py +0 -0
  163. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/__init__.py +0 -0
  164. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/conftest.py +0 -0
  165. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/aider_chat_history.md +0 -0
  166. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/chatgpt_conversations.json +0 -0
  167. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/claude_chat_export.json +0 -0
  168. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/claude_session.jsonl +0 -0
  169. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  170. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/export/default_export.md +0 -0
  171. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/export/full_export.md +0 -0
  172. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/gemini_session.json +0 -0
  173. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/openclaw_session.jsonl +0 -0
  174. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_aider.py +0 -0
  175. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_chatgpt.py +0 -0
  176. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_claude.py +0 -0
  177. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_claude_chat.py +0 -0
  178. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_cline.py +0 -0
  179. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_gemini.py +0 -0
  180. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_openclaw.py +0 -0
  181. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_agent.py +0 -0
  182. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_agent_cli.py +0 -0
  183. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_analyzer.py +0 -0
  184. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_cli.py +0 -0
  185. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_e2e.py +0 -0
  186. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_handler.py +0 -0
  187. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_integration.py +0 -0
  188. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_manifest.py +0 -0
  189. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_protocol.py +0 -0
  190. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli.py +0 -0
  191. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli_deprecations.py +0 -0
  192. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli_library_effectiveness.py +0 -0
  193. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_clipboard.py +0 -0
  194. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_codex_adapter.py +0 -0
  195. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compare_best_worst.py +0 -0
  196. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress.py +0 -0
  197. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_cli.py +0 -0
  198. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_dna.py +0 -0
  199. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_html.py +0 -0
  200. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_insights.py +0 -0
  201. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_config.py +0 -0
  202. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_conversation.py +0 -0
  203. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_copy_flag.py +0 -0
  204. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cost.py +0 -0
  205. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_coverage_boost.py +0 -0
  206. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cursor_adapter.py +0 -0
  207. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_dashboard.py +0 -0
  208. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db.py +0 -0
  209. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_digest.py +0 -0
  210. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_effectiveness.py +0 -0
  211. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_session_quality.py +0 -0
  212. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_trends.py +0 -0
  213. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_dedup.py +0 -0
  214. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_demo.py +0 -0
  215. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_deprecated_commands.py +0 -0
  216. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_digest.py +0 -0
  217. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_digest_cli.py +0 -0
  218. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill.py +0 -0
  219. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill_cli.py +0 -0
  220. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill_weights.py +0 -0
  221. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_e2e.py +0 -0
  222. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_effectiveness.py +0 -0
  223. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_local.py +0 -0
  224. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_ollama.py +0 -0
  225. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_openai.py +0 -0
  226. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_empty_state.py +0 -0
  227. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export.py +0 -0
  228. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export_cli.py +0 -0
  229. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export_snapshot.py +0 -0
  230. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors.py +0 -0
  231. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_routing.py +0 -0
  232. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_zh.py +0 -0
  233. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_zh_e2e.py +0 -0
  234. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_html_report.py +0 -0
  235. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_import_cli.py +0 -0
  236. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_import_e2e.py +0 -0
  237. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights.py +0 -0
  238. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights_cli.py +0 -0
  239. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights_expanded.py +0 -0
  240. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_install_hook.py +0 -0
  241. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_lang_detect.py +0 -0
  242. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_library.py +0 -0
  243. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_lint_cli.py +0 -0
  244. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_markdown.py +0 -0
  245. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_mcp.py +0 -0
  246. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_merge_view.py +0 -0
  247. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_models.py +0 -0
  248. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_output.py +0 -0
  249. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_base.py +0 -0
  250. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_chatgpt.py +0 -0
  251. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_claude.py +0 -0
  252. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_persona.py +0 -0
  253. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_pipeline.py +0 -0
  254. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy.py +0 -0
  255. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_cli.py +0 -0
  256. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_e2e.py +0 -0
  257. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_output.py +0 -0
  258. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_scan.py +0 -0
  259. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_prompt_dna.py +0 -0
  260. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_public_api.py +0 -0
  261. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_recommend.py +0 -0
  262. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition.py +0 -0
  263. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition_cli.py +0 -0
  264. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition_output.py +0 -0
  265. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_schema_version.py +0 -0
  266. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_score_cli.py +0 -0
  267. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_scorer.py +0 -0
  268. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_segmenter.py +0 -0
  269. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_session_quality.py +0 -0
  270. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_session_type.py +0 -0
  271. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sessions_cli.py +0 -0
  272. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sessions_output.py +0 -0
  273. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_share_e2e.py +0 -0
  274. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sharing_client.py +0 -0
  275. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_source_filter.py +0 -0
  276. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_style.py +0 -0
  277. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_style_trends.py +0 -0
  278. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_suggestions.py +0 -0
  279. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_cli.py +0 -0
  280. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_collector.py +0 -0
  281. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_consent.py +0 -0
  282. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_e2e.py +0 -0
  283. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_events.py +0 -0
  284. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_prompt.py +0 -0
  285. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_queue.py +0 -0
  286. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_sender.py +0 -0
  287. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_template_cli.py +0 -0
  288. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_templates.py +0 -0
  289. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_timeutil.py +0 -0
  290. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_trends.py +0 -0
  291. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_trends_cli.py +0 -0
  292. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_use_cli.py +0 -0
  293. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped.py +0 -0
  294. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_cli.py +0 -0
  295. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_e2e.py +0 -0
  296. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_html.py +0 -0
  297. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_output.py +0 -0
  298. {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_share.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reprompt-cli
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: Discover, analyze, and optimize your prompts from AI coding sessions
5
5
  Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
6
6
  Project-URL: Repository, https://github.com/reprompt-dev/reprompt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.9.0"
3
+ version = "1.9.1"
4
4
  description = "Discover, analyze, and optimize your prompts from AI coding sessions"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  """reprompt — Discover, analyze, and evolve your best prompts from AI coding sessions."""
2
2
 
3
- __version__ = "1.9.0"
3
+ __version__ = "1.9.1"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -820,13 +820,19 @@ def lint(
820
820
  import json as json_mod
821
821
 
822
822
  from reprompt.config import Settings
823
- from reprompt.core.lint import format_lint_results, lint_prompts
823
+ from reprompt.core.lint import format_lint_results, lint_prompts, load_lint_config
824
824
  from reprompt.core.pipeline import get_adapters
825
825
  from reprompt.storage.db import PromptDB
826
826
 
827
827
  settings = Settings()
828
828
  db = PromptDB(settings.db_path)
829
829
 
830
+ # Load lint config from .reprompt.toml / pyproject.toml
831
+ lint_config = load_lint_config()
832
+
833
+ # CLI flags override config file
834
+ effective_threshold = score_threshold if score_threshold > 0 else lint_config.score_threshold
835
+
830
836
  # Collect prompts from DB (already scanned)
831
837
  rows = db.get_all_prompts()
832
838
  texts = [r["text"] for r in rows]
@@ -858,11 +864,11 @@ def lint(
858
864
  console.print("No prompts found. Run [bold]reprompt scan[/bold] first.")
859
865
  raise typer.Exit(0)
860
866
 
861
- violations = lint_prompts(texts)
867
+ violations = lint_prompts(texts, config=lint_config)
862
868
 
863
869
  # Score threshold mode (CI integration)
864
870
  score_data = None
865
- if score_threshold > 0:
871
+ if effective_threshold > 0:
866
872
  from reprompt.core.extractors import extract_features
867
873
  from reprompt.core.scorer import score_prompt
868
874
 
@@ -875,8 +881,8 @@ def lint(
875
881
  "avg_score": round(avg_score, 1),
876
882
  "min_score": min(scores) if scores else 0,
877
883
  "max_score": max(scores) if scores else 0,
878
- "threshold": score_threshold,
879
- "pass": avg_score >= score_threshold,
884
+ "threshold": effective_threshold,
885
+ "pass": avg_score >= effective_threshold,
880
886
  }
881
887
 
882
888
  if json_output:
@@ -905,7 +911,7 @@ def lint(
905
911
  console.print(
906
912
  f"\n Score: avg {score_data['avg_score']}/100"
907
913
  f" (min {score_data['min_score']}, max {score_data['max_score']})"
908
- f" — threshold {score_threshold} → {status}"
914
+ f" — threshold {effective_threshold} → {status}"
909
915
  )
910
916
 
911
917
  if copy:
@@ -0,0 +1,242 @@
1
+ """Prompt quality linting rules.
2
+
3
+ Checks prompts against configurable quality rules and returns violations.
4
+ Designed for CI integration — each rule produces a severity + message.
5
+
6
+ Configuration loaded from (highest priority wins):
7
+ 1. CLI flags (--score-threshold, --strict)
8
+ 2. .reprompt.toml in CWD or parents
9
+ 3. [tool.reprompt.lint] in pyproject.toml
10
+ 4. Built-in defaults
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib
21
+ else:
22
+ try:
23
+ import tomllib
24
+ except ModuleNotFoundError: # Python 3.10
25
+ import tomli as tomllib # type: ignore[no-redefine]
26
+
27
+
28
+ @dataclass
29
+ class LintViolation:
30
+ """A single lint rule violation."""
31
+
32
+ rule: str
33
+ severity: str # "error" | "warning"
34
+ message: str
35
+ prompt_text: str
36
+
37
+
38
+ @dataclass
39
+ class LintConfig:
40
+ """Configuration for lint rules. Loaded from .reprompt.toml or pyproject.toml."""
41
+
42
+ # Rule thresholds (0 = disabled)
43
+ min_length: int = 20
44
+ short_prompt: int = 40
45
+
46
+ # Boolean rules (False = disabled)
47
+ vague_prompt: bool = True
48
+ debug_needs_reference: bool = True
49
+
50
+ # File extensions for debug-needs-reference
51
+ file_extensions: list[str] = field(
52
+ default_factory=lambda: [".py", ".ts", ".js", ".go", ".rs", ".java", ".rb", ".cpp", ".c"]
53
+ )
54
+
55
+ # CI score threshold (0 = disabled, set via --score-threshold or config)
56
+ score_threshold: int = 0
57
+
58
+
59
+ # --- Default config ---
60
+ DEFAULT_CONFIG = LintConfig()
61
+
62
+ VAGUE_STARTERS = frozenset(
63
+ {"fix it", "fix this", "do it", "help me", "change it", "update it", "make it work"}
64
+ )
65
+
66
+
67
+ def load_lint_config(start_dir: Path | None = None) -> LintConfig:
68
+ """Load lint config from .reprompt.toml or pyproject.toml, walking up from start_dir.
69
+
70
+ Returns DEFAULT_CONFIG if no config file found.
71
+ """
72
+ if start_dir is None:
73
+ start_dir = Path.cwd()
74
+
75
+ # Walk up to find config files
76
+ current = start_dir.resolve()
77
+ for _ in range(20): # safety limit
78
+ # Check .reprompt.toml first (project-specific)
79
+ reprompt_toml = current / ".reprompt.toml"
80
+ if reprompt_toml.is_file():
81
+ return _parse_reprompt_toml(reprompt_toml)
82
+
83
+ # Check pyproject.toml
84
+ pyproject = current / "pyproject.toml"
85
+ if pyproject.is_file():
86
+ config = _parse_pyproject_toml(pyproject)
87
+ if config is not None:
88
+ return config
89
+
90
+ parent = current.parent
91
+ if parent == current:
92
+ break
93
+ current = parent
94
+
95
+ return LintConfig()
96
+
97
+
98
+ def _parse_reprompt_toml(path: Path) -> LintConfig:
99
+ """Parse a .reprompt.toml file."""
100
+ try:
101
+ with open(path, "rb") as f:
102
+ data = tomllib.load(f)
103
+ return _build_config(data.get("lint", {}))
104
+ except Exception:
105
+ return LintConfig()
106
+
107
+
108
+ def _parse_pyproject_toml(path: Path) -> LintConfig | None:
109
+ """Parse [tool.reprompt.lint] from pyproject.toml. Returns None if section missing."""
110
+ try:
111
+ with open(path, "rb") as f:
112
+ data = tomllib.load(f)
113
+ lint_section = data.get("tool", {}).get("reprompt", {}).get("lint")
114
+ if lint_section is None:
115
+ return None
116
+ return _build_config(lint_section)
117
+ except Exception:
118
+ return None
119
+
120
+
121
+ def _build_config(lint_data: dict) -> LintConfig:
122
+ """Build LintConfig from a parsed TOML dict."""
123
+ config = LintConfig()
124
+
125
+ # Top-level lint settings
126
+ if "score-threshold" in lint_data:
127
+ config.score_threshold = int(lint_data["score-threshold"])
128
+
129
+ # Rule settings
130
+ rules = lint_data.get("rules", {})
131
+
132
+ # min-length: int threshold or false
133
+ if "min-length" in rules:
134
+ val = rules["min-length"]
135
+ config.min_length = int(val) if val else 0
136
+
137
+ # short-prompt: int threshold or false
138
+ if "short-prompt" in rules:
139
+ val = rules["short-prompt"]
140
+ config.short_prompt = int(val) if val else 0
141
+
142
+ # vague-prompt: bool
143
+ if "vague-prompt" in rules:
144
+ config.vague_prompt = bool(rules["vague-prompt"])
145
+
146
+ # debug-needs-reference: bool
147
+ if "debug-needs-reference" in rules:
148
+ config.debug_needs_reference = bool(rules["debug-needs-reference"])
149
+
150
+ # file-extensions: list
151
+ if "file-extensions" in rules:
152
+ config.file_extensions = list(rules["file-extensions"])
153
+
154
+ return config
155
+
156
+
157
+ def lint_prompt(text: str, config: LintConfig | None = None) -> list[LintViolation]:
158
+ """Lint a single prompt against quality rules."""
159
+ if config is None:
160
+ config = DEFAULT_CONFIG
161
+
162
+ violations: list[LintViolation] = []
163
+ stripped = text.strip().lower()
164
+
165
+ # Rule 1: Too short
166
+ if config.min_length > 0 and len(stripped) < config.min_length:
167
+ violations.append(
168
+ LintViolation(
169
+ rule="min-length",
170
+ severity="error",
171
+ message=f"Prompt is too short ({len(stripped)} chars, minimum {config.min_length})",
172
+ prompt_text=text,
173
+ )
174
+ )
175
+ elif config.short_prompt > 0 and len(stripped) < config.short_prompt:
176
+ violations.append(
177
+ LintViolation(
178
+ rule="short-prompt",
179
+ severity="warning",
180
+ message=(f"Prompt is short ({len(stripped)} chars) — consider adding context"),
181
+ prompt_text=text,
182
+ )
183
+ )
184
+
185
+ # Rule 2: Vague starter
186
+ if config.vague_prompt and stripped in VAGUE_STARTERS:
187
+ violations.append(
188
+ LintViolation(
189
+ rule="vague-prompt",
190
+ severity="error",
191
+ message="Prompt is too vague — specify what to fix/change and where",
192
+ prompt_text=text,
193
+ )
194
+ )
195
+
196
+ # Rule 3: No file/function reference in debug prompts
197
+ if config.debug_needs_reference:
198
+ debug_words = {"fix", "debug", "bug", "error", "broken", "failing", "crash"}
199
+ has_debug_intent = any(w in stripped for w in debug_words)
200
+ indicators = list(config.file_extensions) + ["()", "line ", "file "]
201
+ has_reference = any(indicator in text for indicator in indicators)
202
+ min_len = config.min_length if config.min_length > 0 else 20
203
+ if has_debug_intent and not has_reference and len(stripped) >= min_len:
204
+ violations.append(
205
+ LintViolation(
206
+ rule="debug-needs-reference",
207
+ severity="warning",
208
+ message="Debug prompt lacks file/function reference — add specifics",
209
+ prompt_text=text,
210
+ )
211
+ )
212
+
213
+ return violations
214
+
215
+
216
+ def lint_prompts(texts: list[str], config: LintConfig | None = None) -> list[LintViolation]:
217
+ """Lint multiple prompts and return all violations."""
218
+ violations: list[LintViolation] = []
219
+ for text in texts:
220
+ violations.extend(lint_prompt(text, config))
221
+ return violations
222
+
223
+
224
+ def format_lint_results(violations: list[LintViolation], total_prompts: int) -> str:
225
+ """Format lint results for terminal/CI output."""
226
+ if not violations:
227
+ return f"✓ {total_prompts} prompts checked, no issues found"
228
+
229
+ errors = [v for v in violations if v.severity == "error"]
230
+ warnings = [v for v in violations if v.severity == "warning"]
231
+
232
+ lines: list[str] = []
233
+ lines.append(f"Checked {total_prompts} prompts\n")
234
+
235
+ for v in violations:
236
+ prefix = "✗" if v.severity == "error" else "!"
237
+ display = v.prompt_text[:60] + "..." if len(v.prompt_text) > 60 else v.prompt_text
238
+ lines.append(f' {prefix} [{v.rule}] "{display}"')
239
+ lines.append(f" {v.message}")
240
+
241
+ lines.append(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
242
+ return "\n".join(lines)
@@ -0,0 +1,237 @@
1
+ """Tests for prompt linting rules and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from reprompt.core.lint import (
8
+ LintConfig,
9
+ LintViolation,
10
+ format_lint_results,
11
+ lint_prompt,
12
+ lint_prompts,
13
+ load_lint_config,
14
+ )
15
+
16
+
17
+ class TestLintPrompt:
18
+ def test_good_prompt_no_violations(self):
19
+ violations = lint_prompt("fix the authentication bug in auth.py — login returns 401")
20
+ assert violations == []
21
+
22
+ def test_too_short_error(self):
23
+ violations = lint_prompt("fix bug")
24
+ assert len(violations) == 1
25
+ assert violations[0].rule == "min-length"
26
+ assert violations[0].severity == "error"
27
+
28
+ def test_short_warning(self):
29
+ violations = lint_prompt("fix the auth bug please")
30
+ rules = [v.rule for v in violations]
31
+ assert "short-prompt" in rules
32
+
33
+ def test_vague_prompt_error(self):
34
+ violations = lint_prompt("fix it")
35
+ rules = [v.rule for v in violations]
36
+ assert "vague-prompt" in rules
37
+
38
+ def test_debug_without_reference(self):
39
+ violations = lint_prompt("fix the authentication error in the login flow")
40
+ rules = [v.rule for v in violations]
41
+ assert "debug-needs-reference" in rules
42
+
43
+ def test_debug_with_reference_ok(self):
44
+ violations = lint_prompt("fix the authentication error in auth.py login flow")
45
+ rules = [v.rule for v in violations]
46
+ assert "debug-needs-reference" not in rules
47
+
48
+ def test_non_debug_no_reference_warning(self):
49
+ """Non-debug prompts don't need file references."""
50
+ violations = lint_prompt("add pagination to the search results page")
51
+ rules = [v.rule for v in violations]
52
+ assert "debug-needs-reference" not in rules
53
+
54
+
55
+ class TestLintPrompts:
56
+ def test_multiple_prompts(self):
57
+ violations = lint_prompts(
58
+ [
59
+ "fix the authentication bug in auth.py — login returns 401",
60
+ "fix it",
61
+ "add pagination to search results with cursor-based navigation",
62
+ ]
63
+ )
64
+ assert len(violations) >= 1 # "fix it" triggers violations
65
+
66
+ def test_empty_list(self):
67
+ assert lint_prompts([]) == []
68
+
69
+
70
+ class TestFormatResults:
71
+ def test_no_violations(self):
72
+ result = format_lint_results([], 10)
73
+ assert "no issues found" in result
74
+ assert "10" in result
75
+
76
+ def test_with_violations(self):
77
+ violations = lint_prompt("fix it")
78
+ result = format_lint_results(violations, 5)
79
+ assert "error" in result
80
+ assert "fix it" in result
81
+
82
+ def test_truncates_long_prompts(self):
83
+ long_prompt = "x" * 100
84
+ v = LintViolation(rule="test", severity="error", message="test", prompt_text=long_prompt)
85
+ result = format_lint_results([v], 1)
86
+ assert "..." in result
87
+
88
+
89
+ # -- LintConfig tests --
90
+
91
+
92
+ class TestLintConfig:
93
+ def test_default_config(self):
94
+ config = LintConfig()
95
+ assert config.min_length == 20
96
+ assert config.short_prompt == 40
97
+ assert config.vague_prompt is True
98
+ assert config.debug_needs_reference is True
99
+ assert config.score_threshold == 0
100
+ assert ".py" in config.file_extensions
101
+
102
+ def test_custom_min_length(self):
103
+ config = LintConfig(min_length=10)
104
+ # "fix bug" is 7 chars — under 10
105
+ violations = lint_prompt("fix bug", config=config)
106
+ assert any(v.rule == "min-length" for v in violations)
107
+ # "very short" is 10 chars — at threshold, should pass
108
+ violations = lint_prompt("0123456789", config=config)
109
+ assert not any(v.rule == "min-length" for v in violations)
110
+
111
+ def test_disabled_min_length(self):
112
+ config = LintConfig(min_length=0)
113
+ violations = lint_prompt("hi", config=config)
114
+ assert not any(v.rule == "min-length" for v in violations)
115
+
116
+ def test_disabled_short_prompt(self):
117
+ config = LintConfig(min_length=0, short_prompt=0)
118
+ violations = lint_prompt("fix the auth bug please", config=config)
119
+ assert not any(v.rule == "short-prompt" for v in violations)
120
+
121
+ def test_disabled_vague_prompt(self):
122
+ config = LintConfig(min_length=0, vague_prompt=False)
123
+ violations = lint_prompt("fix it", config=config)
124
+ assert not any(v.rule == "vague-prompt" for v in violations)
125
+
126
+ def test_disabled_debug_needs_reference(self):
127
+ config = LintConfig(debug_needs_reference=False)
128
+ violations = lint_prompt("fix the authentication error in the login flow", config=config)
129
+ assert not any(v.rule == "debug-needs-reference" for v in violations)
130
+
131
+ def test_custom_file_extensions(self):
132
+ config = LintConfig(file_extensions=[".py", ".ts"])
133
+ # .go is not in extensions, so no reference detected
134
+ violations = lint_prompt("fix the authentication error in auth.go flow", config=config)
135
+ assert any(v.rule == "debug-needs-reference" for v in violations)
136
+ # .py IS in extensions
137
+ violations = lint_prompt("fix the authentication error in auth.py flow", config=config)
138
+ assert not any(v.rule == "debug-needs-reference" for v in violations)
139
+
140
+ def test_custom_short_threshold(self):
141
+ config = LintConfig(short_prompt=60)
142
+ violations = lint_prompt("add pagination to the search results page")
143
+ rules_default = [v.rule for v in violations]
144
+ assert "short-prompt" not in rules_default # 42 chars, under 40 default = no warning
145
+
146
+ violations = lint_prompt("add pagination to the search results page", config=config)
147
+ rules_custom = [v.rule for v in violations]
148
+ assert "short-prompt" in rules_custom # 42 chars, under 60 threshold
149
+
150
+ def test_lint_prompts_with_config(self):
151
+ config = LintConfig(min_length=0, short_prompt=0, vague_prompt=False)
152
+ violations = lint_prompts(["fix it", "hi", "ok"], config=config)
153
+ # With all rules disabled, everything passes
154
+ assert violations == []
155
+
156
+
157
+ # -- Config file loading tests --
158
+
159
+
160
+ class TestLoadLintConfig:
161
+ def test_no_config_returns_defaults(self, tmp_path: Path):
162
+ config = load_lint_config(start_dir=tmp_path)
163
+ assert config.min_length == 20
164
+ assert config.short_prompt == 40
165
+ assert config.score_threshold == 0
166
+
167
+ def test_reprompt_toml(self, tmp_path: Path):
168
+ toml_file = tmp_path / ".reprompt.toml"
169
+ toml_file.write_text(
170
+ "[lint]\nscore-threshold = 60\n\n[lint.rules]\nmin-length = 10\nvague-prompt = false\n"
171
+ )
172
+ config = load_lint_config(start_dir=tmp_path)
173
+ assert config.score_threshold == 60
174
+ assert config.min_length == 10
175
+ assert config.vague_prompt is False
176
+ # Unset rules keep defaults
177
+ assert config.short_prompt == 40
178
+ assert config.debug_needs_reference is True
179
+
180
+ def test_pyproject_toml(self, tmp_path: Path):
181
+ pyproject = tmp_path / "pyproject.toml"
182
+ pyproject.write_text(
183
+ "[tool.reprompt.lint]\n"
184
+ "score-threshold = 45\n\n"
185
+ "[tool.reprompt.lint.rules]\n"
186
+ "short-prompt = 0\n"
187
+ "debug-needs-reference = false\n"
188
+ )
189
+ config = load_lint_config(start_dir=tmp_path)
190
+ assert config.score_threshold == 45
191
+ assert config.short_prompt == 0
192
+ assert config.debug_needs_reference is False
193
+
194
+ def test_reprompt_toml_takes_precedence(self, tmp_path: Path):
195
+ """When both exist, .reprompt.toml wins."""
196
+ (tmp_path / ".reprompt.toml").write_text("[lint]\nscore-threshold = 70\n")
197
+ (tmp_path / "pyproject.toml").write_text("[tool.reprompt.lint]\nscore-threshold = 30\n")
198
+ config = load_lint_config(start_dir=tmp_path)
199
+ assert config.score_threshold == 70
200
+
201
+ def test_walks_up_directories(self, tmp_path: Path):
202
+ """Config in parent dir should be found."""
203
+ (tmp_path / ".reprompt.toml").write_text("[lint]\nscore-threshold = 55\n")
204
+ subdir = tmp_path / "src" / "module"
205
+ subdir.mkdir(parents=True)
206
+ config = load_lint_config(start_dir=subdir)
207
+ assert config.score_threshold == 55
208
+
209
+ def test_pyproject_without_reprompt_section(self, tmp_path: Path):
210
+ """pyproject.toml without [tool.reprompt] should return defaults."""
211
+ (tmp_path / "pyproject.toml").write_text("[project]\nname = 'foo'\n")
212
+ config = load_lint_config(start_dir=tmp_path)
213
+ assert config.min_length == 20 # default
214
+
215
+ def test_invalid_toml_returns_defaults(self, tmp_path: Path):
216
+ (tmp_path / ".reprompt.toml").write_text("this is not valid toml {{{{")
217
+ config = load_lint_config(start_dir=tmp_path)
218
+ assert config.min_length == 20 # graceful fallback
219
+
220
+ def test_file_extensions_config(self, tmp_path: Path):
221
+ (tmp_path / ".reprompt.toml").write_text(
222
+ '[lint.rules]\nfile-extensions = [".py", ".ts", ".vue"]\n'
223
+ )
224
+ config = load_lint_config(start_dir=tmp_path)
225
+ assert config.file_extensions == [".py", ".ts", ".vue"]
226
+
227
+ def test_disable_all_rules(self, tmp_path: Path):
228
+ (tmp_path / ".reprompt.toml").write_text(
229
+ "[lint.rules]\n"
230
+ "min-length = 0\n"
231
+ "short-prompt = 0\n"
232
+ "vague-prompt = false\n"
233
+ "debug-needs-reference = false\n"
234
+ )
235
+ config = load_lint_config(start_dir=tmp_path)
236
+ violations = lint_prompts(["fix it", "hi", "ok"], config=config)
237
+ assert violations == []
@@ -2210,7 +2210,7 @@ wheels = [
2210
2210
 
2211
2211
  [[package]]
2212
2212
  name = "reprompt-cli"
2213
- version = "1.9.0"
2213
+ version = "1.9.1"
2214
2214
  source = { editable = "." }
2215
2215
  dependencies = [
2216
2216
  { name = "pydantic-settings" },
@@ -1,112 +0,0 @@
1
- """Prompt quality linting rules.
2
-
3
- Checks prompts against configurable quality rules and returns violations.
4
- Designed for CI integration — each rule produces a severity + message.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from dataclasses import dataclass
10
-
11
-
12
- @dataclass
13
- class LintViolation:
14
- """A single lint rule violation."""
15
-
16
- rule: str
17
- severity: str # "error" | "warning"
18
- message: str
19
- prompt_text: str
20
-
21
-
22
- # --- Default thresholds ---
23
- MIN_LENGTH = 20
24
- SHORT_WARNING_LENGTH = 40
25
- VAGUE_STARTERS = frozenset(
26
- {"fix it", "fix this", "do it", "help me", "change it", "update it", "make it work"}
27
- )
28
-
29
-
30
- def lint_prompt(text: str) -> list[LintViolation]:
31
- """Lint a single prompt against quality rules."""
32
- violations: list[LintViolation] = []
33
- stripped = text.strip().lower()
34
-
35
- # Rule 1: Too short
36
- if len(stripped) < MIN_LENGTH:
37
- violations.append(
38
- LintViolation(
39
- rule="min-length",
40
- severity="error",
41
- message=f"Prompt is too short ({len(stripped)} chars, minimum {MIN_LENGTH})",
42
- prompt_text=text,
43
- )
44
- )
45
- elif len(stripped) < SHORT_WARNING_LENGTH:
46
- violations.append(
47
- LintViolation(
48
- rule="short-prompt",
49
- severity="warning",
50
- message=f"Prompt is short ({len(stripped)} chars) — consider adding context",
51
- prompt_text=text,
52
- )
53
- )
54
-
55
- # Rule 2: Vague starter
56
- if stripped in VAGUE_STARTERS:
57
- violations.append(
58
- LintViolation(
59
- rule="vague-prompt",
60
- severity="error",
61
- message="Prompt is too vague — specify what to fix/change and where",
62
- prompt_text=text,
63
- )
64
- )
65
-
66
- # Rule 3: No file/function reference in debug prompts
67
- debug_words = {"fix", "debug", "bug", "error", "broken", "failing", "crash"}
68
- has_debug_intent = any(w in stripped for w in debug_words)
69
- has_reference = any(
70
- indicator in text
71
- for indicator in (".py", ".ts", ".js", ".go", ".rs", "()", "line ", "file ")
72
- )
73
- if has_debug_intent and not has_reference and len(stripped) >= MIN_LENGTH:
74
- violations.append(
75
- LintViolation(
76
- rule="debug-needs-reference",
77
- severity="warning",
78
- message="Debug prompt lacks file/function reference — add specifics",
79
- prompt_text=text,
80
- )
81
- )
82
-
83
- return violations
84
-
85
-
86
- def lint_prompts(texts: list[str]) -> list[LintViolation]:
87
- """Lint multiple prompts and return all violations."""
88
- violations: list[LintViolation] = []
89
- for text in texts:
90
- violations.extend(lint_prompt(text))
91
- return violations
92
-
93
-
94
- def format_lint_results(violations: list[LintViolation], total_prompts: int) -> str:
95
- """Format lint results for terminal/CI output."""
96
- if not violations:
97
- return f"✓ {total_prompts} prompts checked, no issues found"
98
-
99
- errors = [v for v in violations if v.severity == "error"]
100
- warnings = [v for v in violations if v.severity == "warning"]
101
-
102
- lines: list[str] = []
103
- lines.append(f"Checked {total_prompts} prompts\n")
104
-
105
- for v in violations:
106
- prefix = "✗" if v.severity == "error" else "!"
107
- display = v.prompt_text[:60] + "..." if len(v.prompt_text) > 60 else v.prompt_text
108
- lines.append(f' {prefix} [{v.rule}] "{display}"')
109
- lines.append(f" {v.message}")
110
-
111
- lines.append(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
112
- return "\n".join(lines)