claude-dev-env 1.38.0 → 1.39.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 (271) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1236 -161
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
  43. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  44. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  45. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  46. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  47. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  48. package/hooks/config/any_type_config.py +7 -0
  49. package/hooks/config/banned_identifiers_constants.py +11 -0
  50. package/hooks/config/blocking_check_limits.py +38 -0
  51. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  52. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  53. package/hooks/config/convergence_branch_constants.py +9 -0
  54. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  55. package/hooks/config/html_companion_constants.py +20 -0
  56. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  57. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  58. package/hooks/hooks.json +28 -20
  59. package/hooks/pyproject.toml +69 -0
  60. package/hooks/validators/mypy_integration.py +47 -1
  61. package/hooks/validators/run_all_validators.py +3 -3
  62. package/hooks/validators/test_mypy_integration.py +50 -1
  63. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  64. package/hooks/workflow/md_to_html_companion.py +365 -0
  65. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  66. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  67. package/package.json +1 -1
  68. package/rules/gh-body-file.md +2 -0
  69. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  70. package/scripts/check.ps1 +106 -0
  71. package/scripts/config/timing.py +11 -0
  72. package/scripts/sweep_empty_dirs.py +138 -0
  73. package/scripts/sync_to_cursor/rules.py +1 -1
  74. package/scripts/test_sweep_empty_dirs.py +183 -0
  75. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  76. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  77. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  78. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  79. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  80. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  81. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  82. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  83. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  84. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  85. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  86. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  87. package/skills/bugteam/CONSTRAINTS.md +21 -22
  88. package/skills/bugteam/EXAMPLES.md +3 -3
  89. package/skills/bugteam/PROMPTS.md +227 -67
  90. package/skills/bugteam/SKILL.md +114 -455
  91. package/skills/bugteam/reference/README.md +1 -1
  92. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  93. package/skills/bugteam/reference/audit-contract.md +4 -22
  94. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  95. package/skills/bugteam/reference/design-rationale.md +2 -2
  96. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  97. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  100. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  113. package/skills/bugteam/reference/team-setup.md +106 -9
  114. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  115. package/skills/bugteam/scripts/README.md +60 -0
  116. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  117. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  118. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  119. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  120. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  121. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  122. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  123. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  124. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  125. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  126. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  127. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  128. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  129. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  130. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  131. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  133. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  134. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  135. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  136. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  137. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  138. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  139. package/skills/bugteam/test_skill_additions.py +1 -11
  140. package/skills/code/SKILL.md +176 -0
  141. package/skills/doc-gist/SKILL.md +99 -0
  142. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  143. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  144. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  145. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  146. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  147. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  148. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  149. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  150. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  151. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  152. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  153. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  154. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  155. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  156. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  157. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  158. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  159. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  160. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  161. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  162. package/skills/doc-gist/references/examples/README.md +5 -0
  163. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  164. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  165. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  166. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  167. package/skills/findbugs/SKILL.md +68 -2
  168. package/skills/monitor-open-prs/SKILL.md +13 -32
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  170. package/skills/pr-consistency-audit/SKILL.md +112 -0
  171. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  172. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  173. package/skills/pr-converge/SKILL.md +227 -23
  174. package/skills/pr-converge/config/__init__.py +0 -0
  175. package/skills/pr-converge/config/constants.py +62 -0
  176. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  177. package/skills/pr-converge/reference/examples.md +43 -11
  178. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  179. package/skills/pr-converge/reference/ground-rules.md +5 -3
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  181. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  190. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  191. package/skills/pr-converge/reference/per-tick.md +90 -31
  192. package/skills/pr-converge/reference/state-schema.md +22 -1
  193. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  194. package/skills/pr-converge/scripts/README.md +34 -46
  195. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  196. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  197. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  198. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  199. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  200. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  201. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  202. package/skills/qbug/SKILL.md +132 -27
  203. package/skills/session-log/SKILL.md +216 -114
  204. package/skills/session-tidy/SKILL.md +1 -1
  205. package/skills/skill-builder/SKILL.md +138 -56
  206. package/skills/skill-builder/references/delegation-map.md +72 -113
  207. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  208. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  209. package/skills/skill-builder/references/skill-types.md +228 -0
  210. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  211. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  212. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  213. package/skills/skill-builder/workflows/new-skill.md +80 -168
  214. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  215. package/skills/structure-prompt/SKILL.md +50 -0
  216. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  217. package/skills/structure-prompt/reference/block-classification.md +27 -0
  218. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  219. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  220. package/skills/structure-prompt/reference/cleanup.md +33 -0
  221. package/skills/structure-prompt/reference/constraints.md +33 -0
  222. package/skills/structure-prompt/reference/directives.md +37 -0
  223. package/skills/structure-prompt/reference/examples.md +72 -0
  224. package/skills/structure-prompt/reference/instantiation.md +51 -0
  225. package/skills/structure-prompt/reference/output-contract.md +72 -0
  226. package/skills/structure-prompt/reference/per-category.md +23 -0
  227. package/skills/structure-prompt/reference/persona.md +38 -0
  228. package/skills/structure-prompt/reference/research.md +33 -0
  229. package/skills/structure-prompt/reference/structure.md +28 -0
  230. package/agents/code-standards-agent.md +0 -93
  231. package/agents/groq-coder.md +0 -113
  232. package/agents/plan-executor.md +0 -226
  233. package/agents/project-docs-analyzer.md +0 -53
  234. package/agents/project-structure-organizer-agent.md +0 -72
  235. package/agents/skill-to-agent-converter.md +0 -370
  236. package/agents/skill-writer-agent.md +0 -470
  237. package/agents/user-docs-writer.md +0 -67
  238. package/agents/workflow-visual-documenter.md +0 -82
  239. package/commands/readability-review.md +0 -20
  240. package/hooks/mypy.ini +0 -2
  241. package/hooks/notification/attention_needed_notify.py +0 -71
  242. package/hooks/notification/claude_notification_handler.py +0 -67
  243. package/hooks/notification/notification_utils.py +0 -267
  244. package/hooks/notification/subagent_complete_notify.py +0 -381
  245. package/hooks/notification/test_attention_needed_notify.py +0 -47
  246. package/hooks/notification/test_claude_notification_handler.py +0 -54
  247. package/hooks/notification/test_notification_utils.py +0 -91
  248. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  249. package/scripts/config/groq_bugteam_config.py +0 -230
  250. package/scripts/config/test_groq_bugteam_config.py +0 -83
  251. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  252. package/scripts/groq_bugteam.README.md +0 -131
  253. package/scripts/groq_bugteam.py +0 -647
  254. package/scripts/groq_bugteam_dotenv.py +0 -40
  255. package/scripts/groq_bugteam_spec.py +0 -226
  256. package/scripts/test_groq_bugteam.py +0 -529
  257. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  258. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  259. package/scripts/test_groq_bugteam_spec.py +0 -338
  260. package/skills/bugteam/SKILL_EVALS.md +0 -309
  261. package/skills/dream/SKILL.md +0 -118
  262. package/skills/ingest/SKILL.md +0 -40
  263. package/skills/npm-creator/SKILL.md +0 -187
  264. package/skills/readability-review/SKILL.md +0 -127
  265. package/skills/resume-review/SKILL.md +0 -261
  266. package/skills/rule-audit/SKILL.md +0 -307
  267. package/skills/rule-creator/SKILL.md +0 -150
  268. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  269. package/skills/skill-writer/REFERENCE.md +0 -284
  270. package/skills/skill-writer/SKILL.md +0 -222
  271. package/skills/tdd-team/SKILL.md +0 -128
@@ -1,647 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Groq-powered PR bug auditor and auto-fixer.
3
-
4
- Single-pass adaptation of the ``bugteam`` skill that replaces the multi-agent
5
- orchestration with direct calls to Groq's chat completions API. No orchestrated
6
- team, no 10-loop convergence: one audit call, one fix call, one commit and
7
- push per PR.
8
-
9
- Stateless and PII-free. All GitHub identifiers arrive on stdin as JSON;
10
- ``GROQ_API_KEY`` is read from the environment after loading
11
- ``packages/claude-dev-env/.env`` when that file exists (gitignored; see
12
- ``.env.example``). Output is JSON on stdout.
13
-
14
- Pipeline (per invocation):
15
- 1. Read PR metadata, unified diff, file contents from stdin.
16
- 2. Call Groq with the audit prompt. Parse findings as JSON.
17
- 3. For each finding, call Groq with the fix prompt. Parse a file patch.
18
- 4. Write patched files to the worktree, stage, commit, push.
19
- 5. Emit JSON: findings, fix outcomes, commit sha, review body.
20
-
21
- The caller is responsible for PR review posting -- this script emits a
22
- ``review_body`` string but does not talk to the GitHub API.
23
-
24
- Stdin schema::
25
-
26
- {
27
- "pr_number": int,
28
- "owner": str,
29
- "repo": str,
30
- "base_ref": str,
31
- "head_ref": str,
32
- "diff": str, # unified diff text
33
- "files_content": {path: str}, # current content of each file in diff
34
- "worktree_path": str, # absolute path to a worktree on head_ref
35
- "apply_fixes": bool # default true
36
- }
37
-
38
- Stdout schema::
39
-
40
- {
41
- "findings": [ {severity, category, file, line, title, description}, ... ],
42
- "fix_outcomes": [ {finding_index, status, reason?}, ... ],
43
- "commit_sha": str,
44
- "review_body": str,
45
- "audit_model": str,
46
- "fix_model": str,
47
- "error": str # only on hard failure
48
- }
49
- """
50
-
51
- from __future__ import annotations
52
-
53
- import json
54
- import os
55
- import re
56
- import subprocess
57
- import sys
58
- import time
59
- import urllib.error
60
- import urllib.request
61
- from dataclasses import dataclass
62
-
63
- from config.groq_bugteam_config import (
64
- AUDIT_SYSTEM_PROMPT,
65
- FIX_SYSTEM_PROMPT,
66
- GROQ_API_ENDPOINT,
67
- GROQ_AUDIT_MAX_COMPLETION_TOKENS,
68
- GROQ_AUDIT_TEMPERATURE,
69
- GROQ_FALLBACK_MODEL,
70
- GROQ_FIX_MAX_COMPLETION_TOKENS,
71
- GROQ_FIX_TEMPERATURE,
72
- GROQ_PRIMARY_MODEL,
73
- GROQ_REQUEST_TIMEOUT_SECONDS,
74
- GROQ_RETRY_BACKOFF_SECONDS,
75
- JSON_INDENT_SPACES,
76
- MAXIMUM_DIFF_CHARACTERS,
77
- MAXIMUM_FILE_CONTENT_CHARACTERS,
78
- MAXIMUM_FINDINGS_PER_PR,
79
- MISSING_API_KEY_ERROR,
80
- NO_FINDINGS_REVIEW_BODY,
81
- PIPELINE_FAILURE_EXIT_CODE,
82
- REVIEW_BODY_HEADER_TEMPLATE,
83
- TEXT_CLAMP_HEAD_PARTS,
84
- TEXT_CLAMP_TOTAL_PARTS,
85
- )
86
-
87
- from groq_bugteam_dotenv import load_claude_dev_env_dotenv_file
88
-
89
-
90
- @dataclass(frozen=True)
91
- class GroqCallResult:
92
- content: str
93
- model: str
94
-
95
-
96
- def is_recoverable_http_error(error: urllib.error.HTTPError) -> bool:
97
- return error.code in (408, 429, 500, 502, 503, 504)
98
-
99
-
100
- def should_skip_to_next_model(error: urllib.error.HTTPError) -> bool:
101
- return error.code == 413
102
-
103
-
104
- def clamp_text(text: str, max_characters: int) -> str:
105
- if len(text) <= max_characters:
106
- return text
107
- truncated_count = len(text)
108
- while True:
109
- truncation_marker = f"\n\n... [truncated {truncated_count} chars] ...\n\n"
110
- if len(truncation_marker) >= max_characters:
111
- return text[:max_characters]
112
- content_budget = max_characters - len(truncation_marker)
113
- refined_truncated_count = len(text) - content_budget
114
- if refined_truncated_count == truncated_count:
115
- break
116
- truncated_count = refined_truncated_count
117
- head_length = content_budget * TEXT_CLAMP_HEAD_PARTS // TEXT_CLAMP_TOTAL_PARTS
118
- tail_length = content_budget - head_length
119
- head = text[:head_length]
120
- tail = text[-tail_length:] if tail_length else ""
121
- return f"{head}{truncation_marker}{tail}"
122
-
123
-
124
- def post_to_groq(
125
- api_key: str,
126
- model: str,
127
- messages: list,
128
- temperature: float,
129
- max_completion_tokens: int,
130
- ) -> str:
131
- payload = json.dumps(
132
- {
133
- "model": model,
134
- "messages": messages,
135
- "response_format": {"type": "json_object"},
136
- "temperature": temperature,
137
- "max_completion_tokens": max_completion_tokens,
138
- }
139
- ).encode("utf-8")
140
- request = urllib.request.Request(
141
- GROQ_API_ENDPOINT,
142
- data=payload,
143
- headers={
144
- "Authorization": f"Bearer {api_key}",
145
- "Content-Type": "application/json",
146
- "User-Agent": "groq-bugteam/1.0",
147
- },
148
- method="POST",
149
- )
150
- with urllib.request.urlopen(
151
- request, timeout=GROQ_REQUEST_TIMEOUT_SECONDS
152
- ) as open_connection:
153
- raw_response_bytes = open_connection.read()
154
- parsed = json.loads(raw_response_bytes.decode("utf-8"))
155
- return parsed["choices"][0]["message"]["content"]
156
-
157
-
158
- def call_groq_with_fallback(
159
- api_key: str, messages: list, temperature: float, max_completion_tokens: int
160
- ) -> GroqCallResult:
161
- last_error: Exception | None = None
162
- for model in (GROQ_PRIMARY_MODEL, GROQ_FALLBACK_MODEL):
163
- for attempt_index, backoff_seconds in enumerate(
164
- (0, *GROQ_RETRY_BACKOFF_SECONDS)
165
- ):
166
- if backoff_seconds:
167
- time.sleep(backoff_seconds)
168
- try:
169
- content = post_to_groq(
170
- api_key, model, messages, temperature, max_completion_tokens
171
- )
172
- return GroqCallResult(content=content, model=model)
173
- except urllib.error.HTTPError as http_error:
174
- last_error = http_error
175
- if should_skip_to_next_model(http_error):
176
- break
177
- if not is_recoverable_http_error(http_error):
178
- raise RuntimeError(
179
- f"Groq request failed with non-recoverable HTTP error: {http_error}"
180
- ) from http_error
181
- except (
182
- urllib.error.URLError,
183
- TimeoutError,
184
- json.JSONDecodeError,
185
- ) as transport_error:
186
- last_error = transport_error
187
- raise RuntimeError(f"Groq request failed after fallbacks: {last_error}")
188
-
189
-
190
- def parse_json_object(raw_text: str) -> dict:
191
- try:
192
- return json.loads(raw_text)
193
- except json.JSONDecodeError:
194
- pass
195
- match = re.search(r"\{[\s\S]*\}", raw_text)
196
- if not match:
197
- raise ValueError("Groq response did not contain a JSON object")
198
- return json.loads(match.group(0))
199
-
200
-
201
- def coerce_indexes_to_int_set(raw_indexes: list | None) -> set[int]:
202
- coerced: set[int] = set()
203
- for each_raw_index in raw_indexes or []:
204
- try:
205
- coerced.add(int(each_raw_index))
206
- except (TypeError, ValueError):
207
- continue
208
- return coerced
209
-
210
-
211
- def coerce_skipped_entries(raw_skipped: list | None) -> dict[int, str]:
212
- coerced: dict[int, str] = {}
213
- for each_entry in raw_skipped or []:
214
- if not isinstance(each_entry, dict):
215
- continue
216
- try:
217
- finding_index = int(each_entry.get("finding_index"))
218
- except (TypeError, ValueError):
219
- continue
220
- raw_reason = each_entry.get("reason", "")
221
- coerced[finding_index] = "" if raw_reason is None else str(raw_reason)
222
- return coerced
223
-
224
-
225
- def normalize_findings(raw_findings: list, files_content: dict) -> list:
226
- normalized = []
227
- for each_raw in raw_findings:
228
- file_path = str(each_raw.get("file", "")).strip()
229
- if not file_path or file_path not in files_content:
230
- continue
231
- try:
232
- line_number = int(each_raw.get("line", 0))
233
- except (TypeError, ValueError):
234
- line_number = 0
235
- severity = str(each_raw.get("severity", "P2")).upper()
236
- if severity not in ("P0", "P1", "P2"):
237
- severity = "P2"
238
- category = str(each_raw.get("category", "J")).upper()[:1]
239
- normalized.append(
240
- {
241
- "severity": severity,
242
- "category": category,
243
- "file": file_path,
244
- "line": line_number,
245
- "title": str(each_raw.get("title", "")).strip()[:200],
246
- "description": str(each_raw.get("description", "")).strip(),
247
- }
248
- )
249
- return normalized
250
-
251
-
252
- def run_audit(api_key: str, diff_text: str, files_content: dict) -> tuple:
253
- clamped_diff = clamp_text(diff_text, MAXIMUM_DIFF_CHARACTERS)
254
- files_block_parts = []
255
- for each_path, each_content in files_content.items():
256
- clamped_content = clamp_text(each_content, MAXIMUM_FILE_CONTENT_CHARACTERS)
257
- files_block_parts.append(f"--- FILE: {each_path} ---\n{clamped_content}")
258
- user_message = (
259
- "Audit the following pull request diff.\n\n"
260
- "<diff>\n"
261
- f"{clamped_diff}\n"
262
- "</diff>\n\n"
263
- "<files_post_change>\n"
264
- + "\n\n".join(files_block_parts)
265
- + "\n</files_post_change>\n"
266
- )
267
- groq_result = call_groq_with_fallback(
268
- api_key,
269
- messages=[
270
- {"role": "system", "content": AUDIT_SYSTEM_PROMPT},
271
- {"role": "user", "content": user_message},
272
- ],
273
- temperature=GROQ_AUDIT_TEMPERATURE,
274
- max_completion_tokens=GROQ_AUDIT_MAX_COMPLETION_TOKENS,
275
- )
276
- parsed_content = parse_json_object(groq_result.content)
277
- raw_findings = parsed_content.get("findings", [])[:MAXIMUM_FINDINGS_PER_PR]
278
- return normalize_findings(raw_findings, files_content), groq_result.model
279
-
280
-
281
- def should_write_fixed_file(
282
- applied_indexes: set, updated_content: str, current_content: str
283
- ) -> bool:
284
- if not applied_indexes:
285
- return False
286
- return updated_content != current_content
287
-
288
-
289
- def is_safe_relative_path(each_path: str) -> bool:
290
- if os.path.isabs(each_path):
291
- return False
292
- posix_style_each_path = each_path.replace("\\", "/")
293
- if posix_style_each_path.startswith("/"):
294
- return False
295
- if each_path.startswith("\\"):
296
- return False
297
- normalized = os.path.normpath(each_path)
298
- if normalized.startswith(".." + os.sep) or normalized == "..":
299
- return False
300
- parts = normalized.replace("\\", "/").split("/")
301
- if ".." in parts:
302
- return False
303
- return True
304
-
305
-
306
- def decode_subprocess_stderr(stderr_value) -> str:
307
- if stderr_value is None:
308
- return ""
309
- if isinstance(stderr_value, bytes):
310
- return stderr_value.decode("utf-8", "replace")
311
- return str(stderr_value)
312
-
313
-
314
- def build_fix_user_message(file_path: str, current_content: str, findings_block: str) -> str:
315
- trailing_separator = "" if current_content.endswith("\n") else "\n"
316
- return (
317
- f"Fix the findings listed below in file `{file_path}`.\n\n"
318
- "<findings>\n"
319
- f"{findings_block}\n"
320
- "</findings>\n\n"
321
- "<current_file_contents>\n"
322
- f"{current_content}"
323
- f"{trailing_separator}</current_file_contents>\n"
324
- )
325
-
326
-
327
- def preserve_trailing_newline(original: str, updated: str) -> str:
328
- original_ends_with_newline = original.endswith("\n")
329
- updated_ends_with_newline = updated.endswith("\n")
330
- if original_ends_with_newline and not updated_ends_with_newline:
331
- return updated + "\n"
332
- if not original_ends_with_newline and updated_ends_with_newline:
333
- return updated.rstrip("\n")
334
- return updated
335
-
336
-
337
- def group_findings_by_file(findings: list) -> dict:
338
- grouped: dict = {}
339
- for each_index, each_finding in enumerate(findings):
340
- grouped.setdefault(each_finding["file"], []).append((each_index, each_finding))
341
- return grouped
342
-
343
-
344
- def generate_fix_for_file(
345
- api_key: str,
346
- file_path: str,
347
- current_content: str,
348
- findings_for_file: list,
349
- ) -> tuple:
350
- findings_block = json.dumps(
351
- [
352
- {
353
- "finding_index": each_global_index,
354
- "severity": each_finding["severity"],
355
- "category": each_finding["category"],
356
- "line": each_finding["line"],
357
- "title": each_finding["title"],
358
- "description": each_finding["description"],
359
- }
360
- for each_global_index, each_finding in findings_for_file
361
- ],
362
- indent=JSON_INDENT_SPACES,
363
- )
364
- user_message = build_fix_user_message(file_path, current_content, findings_block)
365
- groq_result = call_groq_with_fallback(
366
- api_key,
367
- messages=[
368
- {"role": "system", "content": FIX_SYSTEM_PROMPT},
369
- {"role": "user", "content": user_message},
370
- ],
371
- temperature=GROQ_FIX_TEMPERATURE,
372
- max_completion_tokens=GROQ_FIX_MAX_COMPLETION_TOKENS,
373
- )
374
- return parse_json_object(groq_result.content), groq_result.model
375
-
376
-
377
- def apply_fixes_and_commit(
378
- worktree_path: str,
379
- fixes: dict,
380
- commit_message: str,
381
- ) -> str:
382
- if not fixes:
383
- return ""
384
- worktree_root = os.path.realpath(worktree_path)
385
- for each_path, each_new_content in fixes.items():
386
- if not is_safe_relative_path(each_path):
387
- raise ValueError(
388
- f"Refusing to write unsafe path from Groq response: {each_path!r}"
389
- )
390
- absolute_path = os.path.join(worktree_root, each_path)
391
- resolved_path = os.path.realpath(absolute_path)
392
- if (
393
- resolved_path != worktree_root
394
- and not resolved_path.startswith(worktree_root + os.sep)
395
- ):
396
- raise ValueError(
397
- f"Refusing to write path that escapes worktree: {each_path!r}"
398
- )
399
- parent_directory = os.path.dirname(absolute_path)
400
- if parent_directory:
401
- os.makedirs(parent_directory, exist_ok=True)
402
- with open(absolute_path, "w", encoding="utf-8", newline="\n") as fix_handle:
403
- fix_handle.write(each_new_content)
404
- changed_paths = list(fixes.keys())
405
- subprocess.run(
406
- ["git", "-C", worktree_path, "add", "--", *changed_paths],
407
- check=True,
408
- capture_output=True,
409
- )
410
- status_result = subprocess.run(
411
- ["git", "-C", worktree_path, "status", "--porcelain"],
412
- check=True,
413
- capture_output=True,
414
- text=True,
415
- )
416
- if not status_result.stdout.strip():
417
- return ""
418
- subprocess.run(
419
- ["git", "-C", worktree_path, "commit", "-m", commit_message],
420
- check=True,
421
- capture_output=True,
422
- )
423
- rev_parse_result = subprocess.run(
424
- ["git", "-C", worktree_path, "rev-parse", "HEAD"],
425
- check=True,
426
- capture_output=True,
427
- text=True,
428
- )
429
- return rev_parse_result.stdout.strip()
430
-
431
-
432
- def push_current_branch(worktree_path: str, head_ref: str) -> None:
433
- subprocess.run(
434
- ["git", "-C", worktree_path, "push", "origin", f"HEAD:{head_ref}"],
435
- check=True,
436
- capture_output=True,
437
- )
438
-
439
-
440
- def build_review_body(
441
- findings: list, audit_model: str, commit_sha: str, fix_outcomes: list
442
- ) -> str:
443
- if not findings:
444
- return NO_FINDINGS_REVIEW_BODY.format(model=audit_model)
445
- severity_counts = {"P0": 0, "P1": 0, "P2": 0}
446
- for each_finding in findings:
447
- severity_counts[each_finding["severity"]] += 1
448
- header = REVIEW_BODY_HEADER_TEMPLATE.format(
449
- p0=severity_counts["P0"], p1=severity_counts["P1"], p2=severity_counts["P2"]
450
- )
451
- lines = [header, ""]
452
- if commit_sha:
453
- lines.append(f"Auto-fix commit: `{commit_sha[:7]}`")
454
- lines.append("")
455
- lines.append(f"Audit model: `{audit_model}`")
456
- lines.append("")
457
- lines.append("### Findings")
458
- lines.append("")
459
- for each_index, each_finding in enumerate(findings):
460
- status_for_finding = next(
461
- (
462
- each_outcome
463
- for each_outcome in fix_outcomes
464
- if each_outcome["finding_index"] == each_index
465
- ),
466
- None,
467
- )
468
- status_label = "not attempted"
469
- if status_for_finding:
470
- status_label = status_for_finding["status"]
471
- if status_for_finding.get("reason"):
472
- status_label = f"{status_label}: {status_for_finding['reason']}"
473
- lines.append(
474
- f"- **[{each_finding['severity']} / {each_finding['category']}] "
475
- f"{each_finding['title']}** — `{each_finding['file']}:{each_finding['line']}` "
476
- f"— _{status_label}_"
477
- )
478
- lines.append(f" {each_finding['description']}")
479
- return "\n".join(lines)
480
-
481
-
482
- def run_pipeline(input_data: dict) -> dict:
483
- load_claude_dev_env_dotenv_file()
484
- api_key = os.environ.get("GROQ_API_KEY", "").strip()
485
- if not api_key:
486
- return {"error": MISSING_API_KEY_ERROR}
487
-
488
- diff_text = input_data.get("diff", "")
489
- files_content = input_data.get("files_content", {})
490
- worktree_path = input_data.get("worktree_path", "")
491
- head_ref = input_data.get("head_ref", "")
492
- pr_number = input_data.get("pr_number", 0)
493
- apply_fixes_requested = bool(input_data.get("apply_fixes", True))
494
-
495
- if not diff_text.strip():
496
- return {"error": "diff is empty; nothing to audit"}
497
- if apply_fixes_requested and (not worktree_path or not head_ref):
498
- return {"error": "apply_fixes requires worktree_path and head_ref"}
499
-
500
- findings, audit_model = run_audit(api_key, diff_text, files_content)
501
-
502
- fix_outcomes: list = []
503
- files_to_write: dict = {}
504
- fix_model = ""
505
-
506
- if findings and apply_fixes_requested:
507
- grouped = group_findings_by_file(findings)
508
- for each_file_path, each_findings_for_file in grouped.items():
509
- current_content = files_content.get(each_file_path, "")
510
- try:
511
- fix_result, fix_model = generate_fix_for_file(
512
- api_key, each_file_path, current_content, each_findings_for_file
513
- )
514
- except Exception as fix_error:
515
- for each_global_index, _each_finding in each_findings_for_file:
516
- fix_outcomes.append(
517
- {
518
- "finding_index": each_global_index,
519
- "status": "fix_call_failed",
520
- "reason": str(fix_error)[:200],
521
- }
522
- )
523
- continue
524
- raw_updated_content = fix_result.get("updated_content", current_content)
525
- applied_indexes = coerce_indexes_to_int_set(
526
- fix_result.get("applied_finding_indexes", [])
527
- )
528
- skipped_entries = coerce_skipped_entries(fix_result.get("skipped", []))
529
- updated_content = preserve_trailing_newline(current_content, raw_updated_content)
530
- content_changed = updated_content != current_content
531
- if should_write_fixed_file(applied_indexes, updated_content, current_content):
532
- files_to_write[each_file_path] = updated_content
533
- for each_global_index, _each_finding in each_findings_for_file:
534
- if each_global_index in applied_indexes and content_changed:
535
- fix_outcomes.append(
536
- {"finding_index": each_global_index, "status": "fixed"}
537
- )
538
- elif each_global_index in applied_indexes:
539
- fix_outcomes.append(
540
- {
541
- "finding_index": each_global_index,
542
- "status": "skipped",
543
- "reason": "model claimed fix applied but file content is unchanged",
544
- }
545
- )
546
- elif each_global_index in skipped_entries:
547
- fix_outcomes.append(
548
- {
549
- "finding_index": each_global_index,
550
- "status": "skipped",
551
- "reason": skipped_entries[each_global_index][:200],
552
- }
553
- )
554
- else:
555
- fix_outcomes.append(
556
- {"finding_index": each_global_index, "status": "not_addressed"}
557
- )
558
-
559
- commit_sha = ""
560
- if files_to_write and apply_fixes_requested:
561
- applied_count = sum(
562
- 1 for each_outcome in fix_outcomes if each_outcome["status"] == "fixed"
563
- )
564
- commit_message = (
565
- f"fix(groq-bugteam): auto-fix audit findings for PR #{pr_number}\n\n"
566
- f"Addressed {applied_count} of {len(findings)} findings from groq-bugteam audit."
567
- )
568
- try:
569
- commit_sha = apply_fixes_and_commit(
570
- worktree_path, files_to_write, commit_message
571
- )
572
- if commit_sha:
573
- push_current_branch(worktree_path, head_ref)
574
- except subprocess.CalledProcessError as git_error:
575
- stderr_preview = decode_subprocess_stderr(git_error.stderr)[:500]
576
- return {
577
- "findings": findings,
578
- "fix_outcomes": fix_outcomes,
579
- "commit_sha": "",
580
- "review_body": build_review_body(
581
- findings, audit_model, "", fix_outcomes
582
- ),
583
- "audit_model": audit_model,
584
- "fix_model": fix_model,
585
- "error": f"git operation failed: {stderr_preview}",
586
- }
587
- except ValueError as unsafe_path_error:
588
- return {
589
- "findings": findings,
590
- "fix_outcomes": fix_outcomes,
591
- "commit_sha": "",
592
- "review_body": build_review_body(
593
- findings, audit_model, "", fix_outcomes
594
- ),
595
- "audit_model": audit_model,
596
- "fix_model": fix_model,
597
- "error": f"unsafe fix rejected: {unsafe_path_error}",
598
- }
599
-
600
- review_body = build_review_body(findings, audit_model, commit_sha, fix_outcomes)
601
-
602
- return {
603
- "findings": findings,
604
- "fix_outcomes": fix_outcomes,
605
- "commit_sha": commit_sha,
606
- "review_body": review_body,
607
- "audit_model": audit_model,
608
- "fix_model": fix_model,
609
- }
610
-
611
-
612
- def run_default_pipeline_main() -> None:
613
- try:
614
- stdin_text = sys.stdin.read()
615
- input_data = json.loads(stdin_text)
616
- except (json.JSONDecodeError, ValueError) as parse_error:
617
- json.dump({"error": f"stdin is not valid JSON: {parse_error}"}, sys.stdout)
618
- sys.exit(1)
619
-
620
- try:
621
- pipeline_outcome = run_pipeline(input_data)
622
- except Exception as pipeline_error:
623
- pipeline_outcome = {"error": f"pipeline failed: {pipeline_error}"}
624
-
625
- json.dump(pipeline_outcome, sys.stdout, indent=JSON_INDENT_SPACES)
626
- sys.stdout.write("\n")
627
- if "error" in pipeline_outcome:
628
- sys.exit(PIPELINE_FAILURE_EXIT_CODE)
629
-
630
-
631
- from groq_bugteam_spec import (
632
- apply_fix_from_spec,
633
- is_spec_mode_invocation,
634
- run_spec_mode_main,
635
- )
636
-
637
-
638
- def main() -> None:
639
- load_claude_dev_env_dotenv_file()
640
- if is_spec_mode_invocation(sys.argv[1:]):
641
- run_spec_mode_main()
642
- return
643
- run_default_pipeline_main()
644
-
645
-
646
- if __name__ == "__main__":
647
- main()
@@ -1,40 +0,0 @@
1
- """Load ``packages/claude-dev-env/.env`` into ``os.environ`` for local Groq use.
2
-
3
- Does not override variables already set in the process environment. Uses a
4
- minimal KEY=value parser (stdlib only) so ``groq_bugteam.py`` stays dependency
5
- free.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import os
11
- from pathlib import Path
12
-
13
-
14
- def claude_dev_env_dotenv_path() -> Path:
15
- """Absolute path to the gitignored ``.env`` beside ``groq_bugteam.py``'s package."""
16
- return Path(__file__).resolve().parent.parent / ".env"
17
-
18
-
19
- def load_claude_dev_env_dotenv_file(dotenv_path: Path | None = None) -> None:
20
- """Apply KEY=value lines from the dotenv file when the file exists."""
21
- resolved_path = dotenv_path if dotenv_path is not None else claude_dev_env_dotenv_path()
22
- if not resolved_path.is_file():
23
- return
24
- raw_text = resolved_path.read_text(encoding="utf-8")
25
- for each_line in raw_text.splitlines():
26
- stripped_line = each_line.strip()
27
- if not stripped_line or stripped_line.startswith("#"):
28
- continue
29
- if stripped_line.startswith("export "):
30
- stripped_line = stripped_line.removeprefix("export ").strip()
31
- if "=" not in stripped_line:
32
- continue
33
- key_part, _, value_part = stripped_line.partition("=")
34
- key_name = key_part.strip()
35
- value_text = value_part.strip()
36
- if len(value_text) >= 2 and value_text[0] == value_text[-1] and value_text[0] in "\"'":
37
- value_text = value_text[1:-1]
38
- if not key_name or key_name in os.environ:
39
- continue
40
- os.environ[key_name] = value_text