claude-dev-env 1.28.1 → 1.29.1

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 (54) hide show
  1. package/agents/caveman.md +74 -0
  2. package/hooks/blocking/code_rules_enforcer.py +82 -7
  3. package/hooks/blocking/code_rules_path_utils.py +31 -0
  4. package/hooks/blocking/es_exe_path_rewriter.py +159 -0
  5. package/hooks/blocking/hedging_language_blocker.py +12 -2
  6. package/hooks/blocking/test_code_rules_enforcer.py +148 -0
  7. package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
  8. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  9. package/hooks/blocking/test_code_rules_path_utils.py +52 -0
  10. package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
  11. package/hooks/blocking/test_hedging_language_blocker.py +7 -6
  12. package/hooks/config/dynamic_stderr_handler.py +22 -0
  13. package/hooks/config/path_rewriter_constants.py +13 -0
  14. package/hooks/config/project_paths_reader.py +78 -0
  15. package/hooks/config/setup_project_paths_constants.py +41 -0
  16. package/hooks/config/test_dynamic_stderr_handler.py +48 -0
  17. package/hooks/config/test_messages.py +5 -1
  18. package/hooks/config/test_path_rewriter_constants.py +57 -0
  19. package/hooks/config/test_project_paths_reader.py +149 -0
  20. package/hooks/config/test_setup_project_paths_constants.py +74 -0
  21. package/hooks/git-hooks/test_config.py +1 -0
  22. package/hooks/git-hooks/test_gate_utils.py +1 -0
  23. package/hooks/git-hooks/test_pre_commit.py +1 -0
  24. package/hooks/git-hooks/test_pre_push.py +1 -0
  25. package/hooks/hooks.json +10 -0
  26. package/hooks/session/test_untracked_repo_detector.py +192 -0
  27. package/hooks/session/untracked_repo_detector.py +103 -0
  28. package/hooks/validators/exempt_paths.py +17 -14
  29. package/hooks/validators/test_exempt_paths.py +65 -0
  30. package/hooks/validators/test_git_checks.py +17 -17
  31. package/package.json +1 -1
  32. package/scripts/config/__init__.py +1 -0
  33. package/scripts/config/groq_bugteam_config.py +118 -0
  34. package/scripts/config/test_groq_bugteam_config.py +72 -0
  35. package/scripts/groq_bugteam.README.md +129 -0
  36. package/scripts/groq_bugteam.py +586 -0
  37. package/scripts/setup_project_paths.py +352 -0
  38. package/scripts/test_groq_bugteam.py +391 -0
  39. package/scripts/test_setup_project_paths.py +532 -0
  40. package/scripts/test_setup_project_paths_config.py +6 -0
  41. package/skills/bugteam/CONSTRAINTS.md +1 -1
  42. package/skills/bugteam/PROMPTS.md +1 -1
  43. package/skills/bugteam/SKILL.md +5 -5
  44. package/skills/bugteam/SKILL_EVALS.md +5 -5
  45. package/skills/bugteam/reference/audit-and-teammates.md +3 -3
  46. package/skills/bugteam/reference/audit-contract.md +159 -0
  47. package/skills/bugteam/reference/team-setup.md +2 -2
  48. package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
  49. package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
  50. package/skills/copilot-review/SKILL.md +145 -0
  51. package/skills/findbugs/SKILL.md +14 -22
  52. package/skills/qbug/SKILL.md +56 -13
  53. package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
  54. package/skills/qbug/test_qbug_skill_post_fix_audit.py +103 -0
@@ -0,0 +1,586 @@
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. Output is JSON on stdout.
11
+
12
+ Pipeline (per invocation):
13
+ 1. Read PR metadata, unified diff, file contents from stdin.
14
+ 2. Call Groq with the audit prompt. Parse findings as JSON.
15
+ 3. For each finding, call Groq with the fix prompt. Parse a file patch.
16
+ 4. Write patched files to the worktree, stage, commit, push.
17
+ 5. Emit JSON: findings, fix outcomes, commit sha, review body.
18
+
19
+ The caller is responsible for PR review posting -- this script emits a
20
+ ``review_body`` string but does not talk to the GitHub API.
21
+
22
+ Stdin schema::
23
+
24
+ {
25
+ "pr_number": int,
26
+ "owner": str,
27
+ "repo": str,
28
+ "base_ref": str,
29
+ "head_ref": str,
30
+ "diff": str, # unified diff text
31
+ "files_content": {path: str}, # current content of each file in diff
32
+ "worktree_path": str, # absolute path to a worktree on head_ref
33
+ "apply_fixes": bool # default true
34
+ }
35
+
36
+ Stdout schema::
37
+
38
+ {
39
+ "findings": [ {severity, category, file, line, title, description}, ... ],
40
+ "fix_outcomes": [ {finding_index, status, reason?}, ... ],
41
+ "commit_sha": str,
42
+ "review_body": str,
43
+ "audit_model": str,
44
+ "fix_model": str,
45
+ "error": str # only on hard failure
46
+ }
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import json
52
+ import os
53
+ import re
54
+ import subprocess
55
+ import sys
56
+ import time
57
+ import urllib.error
58
+ import urllib.request
59
+ from dataclasses import dataclass
60
+
61
+ from config.groq_bugteam_config import (
62
+ AUDIT_SYSTEM_PROMPT,
63
+ FIX_SYSTEM_PROMPT,
64
+ GROQ_API_ENDPOINT,
65
+ GROQ_AUDIT_MAX_COMPLETION_TOKENS,
66
+ GROQ_AUDIT_TEMPERATURE,
67
+ GROQ_FALLBACK_MODEL,
68
+ GROQ_FIX_MAX_COMPLETION_TOKENS,
69
+ GROQ_FIX_TEMPERATURE,
70
+ GROQ_PRIMARY_MODEL,
71
+ GROQ_REQUEST_TIMEOUT_SECONDS,
72
+ GROQ_RETRY_BACKOFF_SECONDS,
73
+ JSON_INDENT_SPACES,
74
+ MAXIMUM_DIFF_CHARACTERS,
75
+ MAXIMUM_FILE_CONTENT_CHARACTERS,
76
+ MAXIMUM_FINDINGS_PER_PR,
77
+ NO_FINDINGS_REVIEW_BODY,
78
+ PIPELINE_FAILURE_EXIT_CODE,
79
+ REVIEW_BODY_HEADER_TEMPLATE,
80
+ TEXT_CLAMP_HEAD_PARTS,
81
+ TEXT_CLAMP_TOTAL_PARTS,
82
+ )
83
+
84
+ @dataclass(frozen=True)
85
+ class GroqCallResult:
86
+ content: str
87
+ model: str
88
+
89
+
90
+ def is_recoverable_http_error(error: urllib.error.HTTPError) -> bool:
91
+ return error.code in (408, 429, 500, 502, 503, 504)
92
+
93
+
94
+ def should_skip_to_next_model(error: urllib.error.HTTPError) -> bool:
95
+ return error.code == 413
96
+
97
+
98
+ def clamp_text(text: str, max_characters: int) -> str:
99
+ if len(text) <= max_characters:
100
+ return text
101
+ head_length = max_characters * TEXT_CLAMP_HEAD_PARTS // TEXT_CLAMP_TOTAL_PARTS
102
+ tail_length = max_characters - head_length
103
+ head = text[:head_length]
104
+ tail = text[-tail_length:]
105
+ truncated_count = len(text) - max_characters
106
+ return f"{head}\n\n... [truncated {truncated_count} chars] ...\n\n{tail}"
107
+
108
+
109
+ def post_to_groq(
110
+ api_key: str,
111
+ model: str,
112
+ messages: list,
113
+ temperature: float,
114
+ max_completion_tokens: int,
115
+ ) -> str:
116
+ payload = json.dumps(
117
+ {
118
+ "model": model,
119
+ "messages": messages,
120
+ "response_format": {"type": "json_object"},
121
+ "temperature": temperature,
122
+ "max_completion_tokens": max_completion_tokens,
123
+ }
124
+ ).encode("utf-8")
125
+ request = urllib.request.Request(
126
+ GROQ_API_ENDPOINT,
127
+ data=payload,
128
+ headers={
129
+ "Authorization": f"Bearer {api_key}",
130
+ "Content-Type": "application/json",
131
+ "User-Agent": "groq-bugteam/1.0",
132
+ },
133
+ method="POST",
134
+ )
135
+ with urllib.request.urlopen(
136
+ request, timeout=GROQ_REQUEST_TIMEOUT_SECONDS
137
+ ) as open_connection:
138
+ raw_response_bytes = open_connection.read()
139
+ parsed = json.loads(raw_response_bytes.decode("utf-8"))
140
+ return parsed["choices"][0]["message"]["content"]
141
+
142
+
143
+ def call_groq_with_fallback(
144
+ api_key: str, messages: list, temperature: float, max_completion_tokens: int
145
+ ) -> GroqCallResult:
146
+ last_error: Exception | None = None
147
+ for model in (GROQ_PRIMARY_MODEL, GROQ_FALLBACK_MODEL):
148
+ for attempt_index, backoff_seconds in enumerate(
149
+ (0, *GROQ_RETRY_BACKOFF_SECONDS)
150
+ ):
151
+ if backoff_seconds:
152
+ time.sleep(backoff_seconds)
153
+ try:
154
+ content = post_to_groq(
155
+ api_key, model, messages, temperature, max_completion_tokens
156
+ )
157
+ return GroqCallResult(content=content, model=model)
158
+ except urllib.error.HTTPError as http_error:
159
+ last_error = http_error
160
+ if should_skip_to_next_model(http_error):
161
+ break
162
+ if not is_recoverable_http_error(http_error):
163
+ break
164
+ except (
165
+ urllib.error.URLError,
166
+ TimeoutError,
167
+ json.JSONDecodeError,
168
+ ) as transport_error:
169
+ last_error = transport_error
170
+ raise RuntimeError(f"Groq request failed after fallbacks: {last_error}")
171
+
172
+
173
+ def parse_json_object(raw_text: str) -> dict:
174
+ try:
175
+ return json.loads(raw_text)
176
+ except json.JSONDecodeError:
177
+ pass
178
+ match = re.search(r"\{[\s\S]*\}", raw_text)
179
+ if not match:
180
+ raise ValueError("Groq response did not contain a JSON object")
181
+ return json.loads(match.group(0))
182
+
183
+
184
+ def normalize_findings(raw_findings: list, files_content: dict) -> list:
185
+ normalized = []
186
+ for each_raw in raw_findings:
187
+ file_path = str(each_raw.get("file", "")).strip()
188
+ if not file_path or file_path not in files_content:
189
+ continue
190
+ try:
191
+ line_number = int(each_raw.get("line", 0))
192
+ except (TypeError, ValueError):
193
+ line_number = 0
194
+ severity = str(each_raw.get("severity", "P2")).upper()
195
+ if severity not in ("P0", "P1", "P2"):
196
+ severity = "P2"
197
+ category = str(each_raw.get("category", "J")).upper()[:1]
198
+ normalized.append(
199
+ {
200
+ "severity": severity,
201
+ "category": category,
202
+ "file": file_path,
203
+ "line": line_number,
204
+ "title": str(each_raw.get("title", "")).strip()[:200],
205
+ "description": str(each_raw.get("description", "")).strip(),
206
+ }
207
+ )
208
+ return normalized
209
+
210
+
211
+ def run_audit(api_key: str, diff_text: str, files_content: dict) -> tuple:
212
+ clamped_diff = clamp_text(diff_text, MAXIMUM_DIFF_CHARACTERS)
213
+ files_block_parts = []
214
+ for each_path, each_content in files_content.items():
215
+ clamped_content = clamp_text(each_content, MAXIMUM_FILE_CONTENT_CHARACTERS)
216
+ files_block_parts.append(f"--- FILE: {each_path} ---\n{clamped_content}")
217
+ user_message = (
218
+ "Audit the following pull request diff.\n\n"
219
+ "<diff>\n"
220
+ f"{clamped_diff}\n"
221
+ "</diff>\n\n"
222
+ "<files_post_change>\n"
223
+ + "\n\n".join(files_block_parts)
224
+ + "\n</files_post_change>\n"
225
+ )
226
+ groq_result = call_groq_with_fallback(
227
+ api_key,
228
+ messages=[
229
+ {"role": "system", "content": AUDIT_SYSTEM_PROMPT},
230
+ {"role": "user", "content": user_message},
231
+ ],
232
+ temperature=GROQ_AUDIT_TEMPERATURE,
233
+ max_completion_tokens=GROQ_AUDIT_MAX_COMPLETION_TOKENS,
234
+ )
235
+ parsed_content = parse_json_object(groq_result.content)
236
+ raw_findings = parsed_content.get("findings", [])[:MAXIMUM_FINDINGS_PER_PR]
237
+ return normalize_findings(raw_findings, files_content), groq_result.model
238
+
239
+
240
+ def should_write_fixed_file(
241
+ applied_indexes: set, updated_content: str, current_content: str
242
+ ) -> bool:
243
+ if not applied_indexes:
244
+ return False
245
+ return updated_content != current_content
246
+
247
+
248
+ def is_safe_relative_path(each_path: str) -> bool:
249
+ if os.path.isabs(each_path):
250
+ return False
251
+ normalized = os.path.normpath(each_path)
252
+ if normalized.startswith(".." + os.sep) or normalized == "..":
253
+ return False
254
+ parts = normalized.replace("\\", "/").split("/")
255
+ if ".." in parts:
256
+ return False
257
+ return True
258
+
259
+
260
+ def decode_subprocess_stderr(stderr_value) -> str:
261
+ if stderr_value is None:
262
+ return ""
263
+ if isinstance(stderr_value, bytes):
264
+ return stderr_value.decode("utf-8", "replace")
265
+ return str(stderr_value)
266
+
267
+
268
+ def build_fix_user_message(file_path: str, current_content: str, findings_block: str) -> str:
269
+ trailing_separator = "" if current_content.endswith("\n") else "\n"
270
+ return (
271
+ f"Fix the findings listed below in file `{file_path}`.\n\n"
272
+ "<findings>\n"
273
+ f"{findings_block}\n"
274
+ "</findings>\n\n"
275
+ "<current_file_contents>\n"
276
+ f"{current_content}"
277
+ f"{trailing_separator}</current_file_contents>\n"
278
+ )
279
+
280
+
281
+ def preserve_trailing_newline(original: str, updated: str) -> str:
282
+ original_ends_with_newline = original.endswith("\n")
283
+ updated_ends_with_newline = updated.endswith("\n")
284
+ if original_ends_with_newline and not updated_ends_with_newline:
285
+ return updated + "\n"
286
+ if not original_ends_with_newline and updated_ends_with_newline:
287
+ return updated.rstrip("\n")
288
+ return updated
289
+
290
+
291
+ def group_findings_by_file(findings: list) -> dict:
292
+ grouped: dict = {}
293
+ for each_index, each_finding in enumerate(findings):
294
+ grouped.setdefault(each_finding["file"], []).append((each_index, each_finding))
295
+ return grouped
296
+
297
+
298
+ def generate_fix_for_file(
299
+ api_key: str,
300
+ file_path: str,
301
+ current_content: str,
302
+ findings_for_file: list,
303
+ ) -> tuple:
304
+ findings_block = json.dumps(
305
+ [
306
+ {
307
+ "finding_index": each_global_index,
308
+ "severity": each_finding["severity"],
309
+ "category": each_finding["category"],
310
+ "line": each_finding["line"],
311
+ "title": each_finding["title"],
312
+ "description": each_finding["description"],
313
+ }
314
+ for each_global_index, each_finding in findings_for_file
315
+ ],
316
+ indent=JSON_INDENT_SPACES,
317
+ )
318
+ user_message = build_fix_user_message(file_path, current_content, findings_block)
319
+ groq_result = call_groq_with_fallback(
320
+ api_key,
321
+ messages=[
322
+ {"role": "system", "content": FIX_SYSTEM_PROMPT},
323
+ {"role": "user", "content": user_message},
324
+ ],
325
+ temperature=GROQ_FIX_TEMPERATURE,
326
+ max_completion_tokens=GROQ_FIX_MAX_COMPLETION_TOKENS,
327
+ )
328
+ return parse_json_object(groq_result.content), groq_result.model
329
+
330
+
331
+ def apply_fixes_and_commit(
332
+ worktree_path: str,
333
+ fixes: dict,
334
+ commit_message: str,
335
+ ) -> str:
336
+ if not fixes:
337
+ return ""
338
+ worktree_root = os.path.realpath(worktree_path)
339
+ for each_path, each_new_content in fixes.items():
340
+ if not is_safe_relative_path(each_path):
341
+ raise ValueError(
342
+ f"Refusing to write unsafe path from Groq response: {each_path!r}"
343
+ )
344
+ absolute_path = os.path.join(worktree_root, each_path)
345
+ resolved_path = os.path.realpath(absolute_path)
346
+ if (
347
+ resolved_path != worktree_root
348
+ and not resolved_path.startswith(worktree_root + os.sep)
349
+ ):
350
+ raise ValueError(
351
+ f"Refusing to write path that escapes worktree: {each_path!r}"
352
+ )
353
+ parent_directory = os.path.dirname(absolute_path)
354
+ if parent_directory:
355
+ os.makedirs(parent_directory, exist_ok=True)
356
+ with open(absolute_path, "w", encoding="utf-8", newline="\n") as fix_handle:
357
+ fix_handle.write(each_new_content)
358
+ changed_paths = list(fixes.keys())
359
+ subprocess.run(
360
+ ["git", "-C", worktree_path, "add", "--", *changed_paths],
361
+ check=True,
362
+ capture_output=True,
363
+ )
364
+ status_result = subprocess.run(
365
+ ["git", "-C", worktree_path, "status", "--porcelain"],
366
+ check=True,
367
+ capture_output=True,
368
+ text=True,
369
+ )
370
+ if not status_result.stdout.strip():
371
+ return ""
372
+ subprocess.run(
373
+ ["git", "-C", worktree_path, "commit", "-m", commit_message],
374
+ check=True,
375
+ capture_output=True,
376
+ )
377
+ rev_parse_result = subprocess.run(
378
+ ["git", "-C", worktree_path, "rev-parse", "HEAD"],
379
+ check=True,
380
+ capture_output=True,
381
+ text=True,
382
+ )
383
+ return rev_parse_result.stdout.strip()
384
+
385
+
386
+ def push_current_branch(worktree_path: str, head_ref: str) -> None:
387
+ subprocess.run(
388
+ ["git", "-C", worktree_path, "push", "origin", f"HEAD:{head_ref}"],
389
+ check=True,
390
+ capture_output=True,
391
+ )
392
+
393
+
394
+ def build_review_body(
395
+ findings: list, audit_model: str, commit_sha: str, fix_outcomes: list
396
+ ) -> str:
397
+ if not findings:
398
+ return NO_FINDINGS_REVIEW_BODY.format(model=audit_model)
399
+ severity_counts = {"P0": 0, "P1": 0, "P2": 0}
400
+ for each_finding in findings:
401
+ severity_counts[each_finding["severity"]] += 1
402
+ header = REVIEW_BODY_HEADER_TEMPLATE.format(
403
+ p0=severity_counts["P0"], p1=severity_counts["P1"], p2=severity_counts["P2"]
404
+ )
405
+ lines = [header, ""]
406
+ if commit_sha:
407
+ lines.append(f"Auto-fix commit: `{commit_sha[:7]}`")
408
+ lines.append("")
409
+ lines.append(f"Audit model: `{audit_model}`")
410
+ lines.append("")
411
+ lines.append("### Findings")
412
+ lines.append("")
413
+ for each_index, each_finding in enumerate(findings):
414
+ status_for_finding = next(
415
+ (
416
+ each_outcome
417
+ for each_outcome in fix_outcomes
418
+ if each_outcome["finding_index"] == each_index
419
+ ),
420
+ None,
421
+ )
422
+ status_label = "not attempted"
423
+ if status_for_finding:
424
+ status_label = status_for_finding["status"]
425
+ if status_for_finding.get("reason"):
426
+ status_label = f"{status_label}: {status_for_finding['reason']}"
427
+ lines.append(
428
+ f"- **[{each_finding['severity']} / {each_finding['category']}] "
429
+ f"{each_finding['title']}** — `{each_finding['file']}:{each_finding['line']}` "
430
+ f"— _{status_label}_"
431
+ )
432
+ lines.append(f" {each_finding['description']}")
433
+ return "\n".join(lines)
434
+
435
+
436
+ def run_pipeline(input_data: dict) -> dict:
437
+ api_key = os.environ.get("GROQ_API_KEY", "").strip()
438
+ if not api_key:
439
+ return {"error": "GROQ_API_KEY not set in environment"}
440
+
441
+ diff_text = input_data.get("diff", "")
442
+ files_content = input_data.get("files_content", {})
443
+ worktree_path = input_data.get("worktree_path", "")
444
+ head_ref = input_data.get("head_ref", "")
445
+ pr_number = input_data.get("pr_number", 0)
446
+ apply_fixes_requested = bool(input_data.get("apply_fixes", True))
447
+
448
+ if not diff_text.strip():
449
+ return {"error": "diff is empty; nothing to audit"}
450
+ if apply_fixes_requested and (not worktree_path or not head_ref):
451
+ return {"error": "apply_fixes requires worktree_path and head_ref"}
452
+
453
+ findings, audit_model = run_audit(api_key, diff_text, files_content)
454
+
455
+ fix_outcomes: list = []
456
+ files_to_write: dict = {}
457
+ fix_model = ""
458
+
459
+ if findings and apply_fixes_requested:
460
+ grouped = group_findings_by_file(findings)
461
+ for each_file_path, each_findings_for_file in grouped.items():
462
+ current_content = files_content.get(each_file_path, "")
463
+ try:
464
+ fix_result, fix_model = generate_fix_for_file(
465
+ api_key, each_file_path, current_content, each_findings_for_file
466
+ )
467
+ except Exception as fix_error:
468
+ for each_global_index, _each_finding in each_findings_for_file:
469
+ fix_outcomes.append(
470
+ {
471
+ "finding_index": each_global_index,
472
+ "status": "fix_call_failed",
473
+ "reason": str(fix_error)[:200],
474
+ }
475
+ )
476
+ continue
477
+ raw_updated_content = fix_result.get("updated_content", current_content)
478
+ applied_indexes = set(fix_result.get("applied_finding_indexes", []))
479
+ skipped_entries = {
480
+ each_skipped["finding_index"]: each_skipped.get("reason", "")
481
+ for each_skipped in fix_result.get("skipped", [])
482
+ }
483
+ updated_content = preserve_trailing_newline(current_content, raw_updated_content)
484
+ content_changed = updated_content != current_content
485
+ if should_write_fixed_file(applied_indexes, updated_content, current_content):
486
+ files_to_write[each_file_path] = updated_content
487
+ for each_global_index, _each_finding in each_findings_for_file:
488
+ if each_global_index in applied_indexes and content_changed:
489
+ fix_outcomes.append(
490
+ {"finding_index": each_global_index, "status": "fixed"}
491
+ )
492
+ elif each_global_index in applied_indexes:
493
+ fix_outcomes.append(
494
+ {
495
+ "finding_index": each_global_index,
496
+ "status": "skipped",
497
+ "reason": "model claimed fix applied but file content is unchanged",
498
+ }
499
+ )
500
+ elif each_global_index in skipped_entries:
501
+ fix_outcomes.append(
502
+ {
503
+ "finding_index": each_global_index,
504
+ "status": "skipped",
505
+ "reason": skipped_entries[each_global_index][:200],
506
+ }
507
+ )
508
+ else:
509
+ fix_outcomes.append(
510
+ {"finding_index": each_global_index, "status": "not_addressed"}
511
+ )
512
+
513
+ commit_sha = ""
514
+ if files_to_write and apply_fixes_requested:
515
+ applied_count = sum(
516
+ 1 for each_outcome in fix_outcomes if each_outcome["status"] == "fixed"
517
+ )
518
+ commit_message = (
519
+ f"fix(groq-bugteam): auto-fix audit findings for PR #{pr_number}\n\n"
520
+ f"Addressed {applied_count} of {len(findings)} findings from groq-bugteam audit."
521
+ )
522
+ try:
523
+ commit_sha = apply_fixes_and_commit(
524
+ worktree_path, files_to_write, commit_message
525
+ )
526
+ if commit_sha:
527
+ push_current_branch(worktree_path, head_ref)
528
+ except subprocess.CalledProcessError as git_error:
529
+ stderr_preview = decode_subprocess_stderr(git_error.stderr)[:500]
530
+ return {
531
+ "findings": findings,
532
+ "fix_outcomes": fix_outcomes,
533
+ "commit_sha": "",
534
+ "review_body": build_review_body(
535
+ findings, audit_model, "", fix_outcomes
536
+ ),
537
+ "audit_model": audit_model,
538
+ "fix_model": fix_model,
539
+ "error": f"git operation failed: {stderr_preview}",
540
+ }
541
+ except ValueError as unsafe_path_error:
542
+ return {
543
+ "findings": findings,
544
+ "fix_outcomes": fix_outcomes,
545
+ "commit_sha": "",
546
+ "review_body": build_review_body(
547
+ findings, audit_model, "", fix_outcomes
548
+ ),
549
+ "audit_model": audit_model,
550
+ "fix_model": fix_model,
551
+ "error": f"unsafe fix rejected: {unsafe_path_error}",
552
+ }
553
+
554
+ review_body = build_review_body(findings, audit_model, commit_sha, fix_outcomes)
555
+
556
+ return {
557
+ "findings": findings,
558
+ "fix_outcomes": fix_outcomes,
559
+ "commit_sha": commit_sha,
560
+ "review_body": review_body,
561
+ "audit_model": audit_model,
562
+ "fix_model": fix_model,
563
+ }
564
+
565
+
566
+ def main() -> None:
567
+ try:
568
+ stdin_text = sys.stdin.read()
569
+ input_data = json.loads(stdin_text)
570
+ except (json.JSONDecodeError, ValueError) as parse_error:
571
+ json.dump({"error": f"stdin is not valid JSON: {parse_error}"}, sys.stdout)
572
+ sys.exit(1)
573
+
574
+ try:
575
+ pipeline_outcome = run_pipeline(input_data)
576
+ except Exception as pipeline_error:
577
+ pipeline_outcome = {"error": f"pipeline failed: {pipeline_error}"}
578
+
579
+ json.dump(pipeline_outcome, sys.stdout, indent=JSON_INDENT_SPACES)
580
+ sys.stdout.write("\n")
581
+ if "error" in pipeline_outcome:
582
+ sys.exit(PIPELINE_FAILURE_EXIT_CODE)
583
+
584
+
585
+ if __name__ == "__main__":
586
+ main()