claude-dev-env 1.59.0 → 1.60.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 (62) hide show
  1. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  2. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  3. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  4. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  5. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  6. package/hooks/blocking/code_rules_duplicate_body.py +152 -0
  7. package/hooks/blocking/code_rules_enforcer.py +30 -15
  8. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  9. package/hooks/blocking/config/__init__.py +5 -0
  10. package/hooks/blocking/config/verified_commit_constants.py +106 -0
  11. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  12. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  13. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  14. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  15. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  16. package/hooks/blocking/test_verification_verdict_store.py +278 -0
  17. package/hooks/blocking/test_verified_commit_gate.py +368 -0
  18. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  19. package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
  20. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  21. package/hooks/blocking/verification_verdict_store.py +446 -0
  22. package/hooks/blocking/verified_commit_gate.py +523 -0
  23. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  24. package/hooks/blocking/verifier_verdict_minter.py +299 -0
  25. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  26. package/hooks/hooks.json +43 -1
  27. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  28. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  29. package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
  30. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  31. package/package.json +1 -1
  32. package/rules/file-global-constants.md +7 -1
  33. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  34. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  35. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  36. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  37. package/skills/autoconverge/SKILL.md +54 -17
  38. package/skills/autoconverge/reference/closing-report.md +59 -17
  39. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  40. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
  41. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  42. package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
  43. package/skills/autoconverge/workflow/converge.mjs +128 -6
  44. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  45. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  46. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  47. package/skills/autoconverge/workflow/render_report.py +488 -397
  48. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  49. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  50. package/skills/autoconverge/workflow/test_render_report.py +488 -259
  51. package/skills/pr-converge/reference/per-tick.md +28 -8
  52. package/skills/rebase/SKILL.md +2 -4
  53. package/system-prompts/software-engineer.xml +2 -6
  54. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  55. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  56. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  57. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  58. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  59. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  60. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  61. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  62. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -0,0 +1,371 @@
1
+ """Merge every autoconverge run journal for one PR into a single combined journal."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import shutil
8
+ import stat
9
+ import sys
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import TextIO
14
+
15
+ import convergence_summary
16
+ import render_report
17
+ from autoconverge_report_constants.render_report_constants import (
18
+ ARGS_FIELD_OWNER,
19
+ ARGS_FIELD_PR_NUMBER,
20
+ ARGS_FIELD_REPO,
21
+ COMBINED_RUN_ID_PREFIX,
22
+ JOURNAL_FIELD_ARGS,
23
+ JOURNAL_FIELD_RESULT,
24
+ JOURNAL_FIELD_RUN_ID,
25
+ JOURNAL_FIELD_TIMESTAMP,
26
+ JOURNAL_FIELD_WORKFLOW_NAME,
27
+ JOURNAL_FIELD_WORKFLOW_PROGRESS,
28
+ JOURNAL_SIBLING_SUBAGENTS,
29
+ JOURNAL_SIBLING_WORKFLOWS,
30
+ LABEL_RESOLVE_HEAD,
31
+ ONEXC_PYTHON_MAJOR_VERSION,
32
+ ONEXC_PYTHON_MINOR_VERSION,
33
+ PROGRESS_FIELD_AGENT_ID,
34
+ PROGRESS_FIELD_LABEL,
35
+ PROJECTS_DIR_NAME,
36
+ RESULT_FIELD_FINAL_SHA,
37
+ SUMMARY_DETAIL_MAX_CHARS,
38
+ WORKFLOW_NAME_AUTOCONVERGE,
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class AggregateResult:
44
+ """The combined journal plus the aggregated inputs a closing report needs."""
45
+
46
+ combined_journal_path: Path
47
+ findings: list[dict]
48
+ fix_summaries: list[str]
49
+ round_count: int
50
+ final_sha: str
51
+
52
+
53
+ def _parse_args_object(journal: dict) -> dict:
54
+ """Return the journal's args as a dict, parsing a JSON string when needed.
55
+
56
+ Args:
57
+ journal: A parsed run-journal object.
58
+
59
+ Returns:
60
+ The args object as a dict, or an empty dict when absent or unparseable.
61
+ """
62
+ raw_args = journal.get(JOURNAL_FIELD_ARGS, "")
63
+ if isinstance(raw_args, dict):
64
+ return raw_args
65
+ try:
66
+ parsed = json.loads(raw_args)
67
+ except (ValueError, TypeError):
68
+ return {}
69
+ return parsed if isinstance(parsed, dict) else {}
70
+
71
+
72
+ def _resolve_projects_root(seed_journal_path: Path) -> Path:
73
+ """Return the projects directory that contains the seed run journal.
74
+
75
+ Args:
76
+ seed_journal_path: Path to one of the PR's wf_<runId>.json journals.
77
+
78
+ Returns:
79
+ The nearest ancestor directory named 'projects', or the seed's
80
+ four-levels-up ancestor when no such ancestor exists.
81
+ """
82
+ for each_ancestor in seed_journal_path.parents:
83
+ if each_ancestor.name == PROJECTS_DIR_NAME:
84
+ return each_ancestor
85
+ return seed_journal_path.parents[3]
86
+
87
+
88
+ def _discover_pr_runs(
89
+ projects_root: Path, owner: str, repo: str, pr_number: int
90
+ ) -> list[tuple[Path, dict]]:
91
+ """Return every autoconverge (journal path, journal) pair for the PR, oldest first.
92
+
93
+ Args:
94
+ projects_root: The projects directory holding all run trees.
95
+ owner: The PR's repository owner.
96
+ repo: The PR's repository name.
97
+ pr_number: The PR number.
98
+
99
+ Returns:
100
+ A timestamp-sorted list of (journal path, parsed journal) pairs.
101
+ """
102
+ runs: list[tuple[Path, dict]] = []
103
+ for each_journal_path in projects_root.glob("*/*/workflows/wf_*.json"):
104
+ try:
105
+ journal = json.loads(each_journal_path.read_text(encoding="utf-8"))
106
+ except (ValueError, OSError):
107
+ continue
108
+ if journal.get(JOURNAL_FIELD_WORKFLOW_NAME) != WORKFLOW_NAME_AUTOCONVERGE:
109
+ continue
110
+ args_object = _parse_args_object(journal)
111
+ matches_pr = (
112
+ args_object.get(ARGS_FIELD_OWNER) == owner
113
+ and args_object.get(ARGS_FIELD_REPO) == repo
114
+ and args_object.get(ARGS_FIELD_PR_NUMBER) == pr_number
115
+ )
116
+ if matches_pr:
117
+ runs.append((each_journal_path, journal))
118
+ runs.sort(key=lambda pair: pair[1].get(JOURNAL_FIELD_TIMESTAMP, ""))
119
+ return runs
120
+
121
+
122
+ def discover_pr_journals(
123
+ projects_root: Path, owner: str, repo: str, pr_number: int
124
+ ) -> list[Path]:
125
+ """Return every autoconverge journal path for the PR, oldest first.
126
+
127
+ Args:
128
+ projects_root: The projects directory holding all run trees.
129
+ owner: The PR's repository owner.
130
+ repo: The PR's repository name.
131
+ pr_number: The PR number.
132
+
133
+ Returns:
134
+ A timestamp-sorted list of journal paths matching the PR.
135
+ """
136
+ return [
137
+ path
138
+ for path, _journal in _discover_pr_runs(projects_root, owner, repo, pr_number)
139
+ ]
140
+
141
+
142
+ def _retry_after_chmod(
143
+ remove_func: Callable[[str], None], failing_path: str, *_exc_info: object
144
+ ) -> None:
145
+ """Clear the Windows read-only bit on a path, then retry the removal that failed.
146
+
147
+ Args:
148
+ remove_func: The os removal call rmtree was attempting when it failed.
149
+ failing_path: The path the failed removal call could not delete.
150
+ _exc_info: The exception or exc_info tuple the rmtree handler passes.
151
+ """
152
+ os.chmod(failing_path, stat.S_IWRITE)
153
+ remove_func(failing_path)
154
+
155
+
156
+ def _select_rmtree_handler_keyword() -> dict[str, Callable[..., None]]:
157
+ """Return the rmtree retry-handler keyword argument for the running Python.
158
+
159
+ Returns:
160
+ A single-entry dict using the 'onexc' keyword on the Python versions that
161
+ accept it and 'onerror' on the earlier versions that do not.
162
+ """
163
+ onexc_required_version = (ONEXC_PYTHON_MAJOR_VERSION, ONEXC_PYTHON_MINOR_VERSION)
164
+ if sys.version_info >= onexc_required_version:
165
+ return {"onexc": _retry_after_chmod}
166
+ return {"onerror": _retry_after_chmod}
167
+
168
+
169
+ def _force_remove(target_path: Path) -> None:
170
+ """Remove a directory tree, clearing the Windows read-only bit on failure.
171
+
172
+ Args:
173
+ target_path: The directory tree to remove when it exists.
174
+ """
175
+ if not target_path.exists():
176
+ return
177
+ handler_keyword = _select_rmtree_handler_keyword()
178
+ if "onexc" in handler_keyword:
179
+ shutil.rmtree(target_path, onexc=_retry_after_chmod)
180
+ return
181
+ shutil.rmtree(target_path, onerror=_retry_after_chmod)
182
+
183
+
184
+ def aggregate_pr_journals(
185
+ seed_journal_path: Path, owner: str, repo: str, pr_number: int, work_dir: Path
186
+ ) -> AggregateResult:
187
+ """Merge every autoconverge journal for the PR into one combined journal.
188
+
189
+ Args:
190
+ seed_journal_path: One of the PR's run journals, used to locate the rest.
191
+ owner: The PR's repository owner.
192
+ repo: The PR's repository name.
193
+ pr_number: The PR number.
194
+ work_dir: A directory the combined run tree is written under.
195
+
196
+ Returns:
197
+ An AggregateResult carrying the combined journal path, the deduped
198
+ findings, the fix summaries, the resolve-head round count, and the
199
+ final commit sha drawn from the latest run's result.
200
+ """
201
+ projects_root = _resolve_projects_root(seed_journal_path)
202
+ runs = _discover_pr_runs(projects_root, owner, repo, pr_number)
203
+
204
+ combined_run_id = f"{COMBINED_RUN_ID_PREFIX}{pr_number}"
205
+ dest_base = work_dir / combined_run_id
206
+ _force_remove(dest_base)
207
+ combined_workflows_dir = dest_base / JOURNAL_SIBLING_WORKFLOWS
208
+ combined_workflows_dir.mkdir(parents=True)
209
+ combined_agents_dir = (
210
+ dest_base
211
+ / JOURNAL_SIBLING_SUBAGENTS
212
+ / JOURNAL_SIBLING_WORKFLOWS
213
+ / combined_run_id
214
+ )
215
+ combined_agents_dir.mkdir(parents=True)
216
+
217
+ combined_progress: list[dict] = []
218
+ final_sha = ""
219
+ latest_timestamp = ""
220
+ for each_journal_path, each_journal in runs:
221
+ source_agents_dir = (
222
+ each_journal_path.parent.parent
223
+ / JOURNAL_SIBLING_SUBAGENTS
224
+ / JOURNAL_SIBLING_WORKFLOWS
225
+ / each_journal_path.stem
226
+ )
227
+ for each_entry in each_journal.get(JOURNAL_FIELD_WORKFLOW_PROGRESS, []):
228
+ agent_id = each_entry.get(PROGRESS_FIELD_AGENT_ID)
229
+ if agent_id:
230
+ source_transcript = source_agents_dir / f"agent-{agent_id}.jsonl"
231
+ if source_transcript.exists():
232
+ shutil.copy(
233
+ source_transcript,
234
+ combined_agents_dir / f"agent-{agent_id}.jsonl",
235
+ )
236
+ combined_progress.append(each_entry)
237
+ result_block = each_journal.get(JOURNAL_FIELD_RESULT) or {}
238
+ if result_block.get(RESULT_FIELD_FINAL_SHA):
239
+ final_sha = result_block[RESULT_FIELD_FINAL_SHA]
240
+ timestamp = each_journal.get(JOURNAL_FIELD_TIMESTAMP, "")
241
+ if timestamp > latest_timestamp:
242
+ latest_timestamp = timestamp
243
+
244
+ round_count = sum(
245
+ 1
246
+ for each_entry in combined_progress
247
+ if each_entry.get(PROGRESS_FIELD_LABEL) == LABEL_RESOLVE_HEAD
248
+ )
249
+ combined_journal = {
250
+ JOURNAL_FIELD_RUN_ID: combined_run_id,
251
+ JOURNAL_FIELD_TIMESTAMP: latest_timestamp,
252
+ JOURNAL_FIELD_WORKFLOW_NAME: WORKFLOW_NAME_AUTOCONVERGE,
253
+ JOURNAL_FIELD_ARGS: json.dumps(
254
+ {
255
+ ARGS_FIELD_OWNER: owner,
256
+ ARGS_FIELD_REPO: repo,
257
+ ARGS_FIELD_PR_NUMBER: pr_number,
258
+ }
259
+ ),
260
+ JOURNAL_FIELD_RESULT: {RESULT_FIELD_FINAL_SHA: final_sha},
261
+ JOURNAL_FIELD_WORKFLOW_PROGRESS: combined_progress,
262
+ }
263
+ combined_journal_path = combined_workflows_dir / f"{combined_run_id}.json"
264
+ combined_journal_path.write_text(json.dumps(combined_journal), encoding="utf-8")
265
+
266
+ run_data = render_report.load_run_data(combined_journal_path)
267
+ findings = [
268
+ {
269
+ "severity": each_finding.severity,
270
+ "category": each_finding.category,
271
+ "file": each_finding.file,
272
+ "line": each_finding.line,
273
+ "title": each_finding.title,
274
+ "detail": (each_finding.detail or "")[:SUMMARY_DETAIL_MAX_CHARS],
275
+ }
276
+ for each_finding in run_data.all_distinct_findings
277
+ ]
278
+ fix_summaries = [
279
+ each_fix.summary
280
+ for each_fix in run_data.fix_by_round.values()
281
+ if each_fix.summary
282
+ ]
283
+ return AggregateResult(
284
+ combined_journal_path=combined_journal_path,
285
+ findings=findings,
286
+ fix_summaries=fix_summaries,
287
+ round_count=round_count,
288
+ final_sha=final_sha,
289
+ )
290
+
291
+
292
+ def main(out_stream: TextIO = sys.stdout, err_stream: TextIO = sys.stderr) -> int:
293
+ """Aggregate a PR's journals, write the summary prompt, print the combined facts.
294
+
295
+ Args:
296
+ out_stream: Stream the result JSON is written to on success.
297
+ err_stream: Stream error messages are written to.
298
+
299
+ Returns:
300
+ Exit code (0 on success, 1 on argument error).
301
+ """
302
+ argument_parser = argparse.ArgumentParser(
303
+ description="Aggregate every autoconverge journal for a PR into one journal."
304
+ )
305
+ argument_parser.add_argument(
306
+ "--journal", required=True, help="Path to one of the PR's wf_<runId>.json files"
307
+ )
308
+ argument_parser.add_argument("--pr", required=True, help="owner/repo#number")
309
+ argument_parser.add_argument(
310
+ "--work-dir",
311
+ required=True,
312
+ help="Directory the combined run tree is written under",
313
+ )
314
+ argument_parser.add_argument(
315
+ "--out-prompt",
316
+ required=True,
317
+ help="Path the summary agent prompt is written to",
318
+ )
319
+ argument_parser.add_argument(
320
+ "--standards-note", default=None, help="Deferred code-standard note, when any"
321
+ )
322
+ argument_parser.add_argument(
323
+ "--copilot-note", default=None, help="Copilot gate outage note, when any"
324
+ )
325
+ parsed_args = argument_parser.parse_args()
326
+
327
+ pr_arg_pattern = r"(?P<owner>[^/]+)/(?P<repo>[^#]+)#(?P<number>\d+)"
328
+ pr_match = re.fullmatch(pr_arg_pattern, parsed_args.pr)
329
+ if pr_match is None:
330
+ err_stream.write(
331
+ f"Invalid --pr format: {parsed_args.pr!r}. Expected owner/repo#number.\n"
332
+ )
333
+ return 1
334
+ owner = pr_match.group("owner")
335
+ repo = pr_match.group("repo")
336
+ pr_number = int(pr_match.group("number"))
337
+
338
+ work_dir = Path(parsed_args.work_dir)
339
+ work_dir.mkdir(parents=True, exist_ok=True)
340
+ aggregation = aggregate_pr_journals(
341
+ Path(parsed_args.journal).resolve(), owner, repo, pr_number, work_dir
342
+ )
343
+
344
+ prompt = convergence_summary.build_summary_prompt(
345
+ owner,
346
+ repo,
347
+ pr_number,
348
+ aggregation.round_count,
349
+ aggregation.findings,
350
+ aggregation.fix_summaries,
351
+ parsed_args.standards_note,
352
+ parsed_args.copilot_note,
353
+ )
354
+ Path(parsed_args.out_prompt).write_text(prompt, encoding="utf-8")
355
+
356
+ out_stream.write(
357
+ json.dumps(
358
+ {
359
+ "combinedJournal": str(aggregation.combined_journal_path),
360
+ "roundCount": aggregation.round_count,
361
+ "finalSha": aggregation.final_sha,
362
+ "findingCount": len(aggregation.findings),
363
+ }
364
+ )
365
+ + "\n"
366
+ )
367
+ return 0
368
+
369
+
370
+ if __name__ == "__main__":
371
+ sys.exit(main())