claude-dev-env 4.3.0 → 7.0.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 (293) hide show
  1. package/.agents/agents/pr-description-writer.md +2 -2
  2. package/.agents/agents/test_agent_frontmatter.py +2 -2
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -3
  4. package/.agents/skills/e-code-review/SKILL.md +12 -12
  5. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
  6. package/.agents/skills/e-code-review/reference/fix.md +15 -17
  7. package/.agents/skills/e-code-review/reference/loop.md +35 -39
  8. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
  9. package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
  10. package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
  11. package/.agents/skills/issue-tracker/SKILL.md +5 -4
  12. package/.agents/skills/pull-request/SKILL.md +11 -13
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
  15. package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
  16. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
  17. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
  18. package/.agents/skills/skill-builder/SKILL.md +3 -196
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  20. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  22. package/bin/install.agents-home.test.mjs +15 -13
  23. package/bin/install.mjs +28 -0
  24. package/bin/install.policy-lint.test.mjs +78 -0
  25. package/bin/install.prune.test.mjs +59 -20
  26. package/bin/install.pstack-plugin.test.mjs +114 -0
  27. package/bin/install.test.mjs +37 -0
  28. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  29. package/hooks/advisory/pr_done_reminder.py +342 -0
  30. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  31. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  32. package/hooks/blocking/code_rules_paired_test.py +22 -15
  33. package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
  34. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  35. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  36. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  37. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  38. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  39. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  40. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  41. package/hooks/git-hooks/pre_commit.py +52 -11
  42. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  44. package/hooks/git-hooks/test_pre_commit.py +246 -0
  45. package/hooks/hooks.json +1 -1
  46. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
  47. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  48. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  49. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
  50. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  51. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  52. package/package.json +2 -2
  53. package/rules/durable-post-artifacts.md +2 -1
  54. package/rules/git-workflow.md +6 -6
  55. package/scripts/check.ps1 +0 -1
  56. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  57. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  58. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  59. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  60. package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
  61. package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
  62. package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
  63. package/scripts/policy_lint/adapter_detectors.py +13 -6
  64. package/scripts/policy_lint/adapters.py +7 -4
  65. package/scripts/policy_lint/config/constants.py +5 -0
  66. package/scripts/policy_lint/engine.py +48 -4
  67. package/scripts/policy_lint/model.py +1 -0
  68. package/scripts/policy_lint/registry.py +1 -1
  69. package/scripts/policy_lint/selection.py +2 -1
  70. package/scripts/policy_lint/selection_git.py +2 -0
  71. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  72. package/scripts/shared_tree_paths.py +32 -3
  73. package/scripts/test_shared_tree_paths.py +33 -0
  74. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  75. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  76. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  77. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  78. package/scripts/tests/test_policy_lint_selection.py +25 -2
  79. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  80. package/scripts/tests/test_rules.py +3 -7
  81. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  82. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  83. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  84. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  85. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  86. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  87. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  88. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  89. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  90. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  91. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  92. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  93. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  94. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  95. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  96. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  97. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  98. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  99. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  100. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  101. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  102. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  103. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  104. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  105. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  106. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  107. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  108. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  109. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  110. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  111. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  112. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  113. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  114. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  115. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  116. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  117. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  118. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  119. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  120. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  121. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  122. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  123. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  124. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  125. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  126. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  127. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  128. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  129. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  130. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  131. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  132. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  133. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  134. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  135. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  136. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  137. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  138. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  139. package/.agents/skills/autoconverge/SKILL.md +0 -431
  140. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  141. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  142. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  143. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  144. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  145. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  146. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  147. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  148. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  149. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  150. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  151. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  152. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  153. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  154. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  155. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  156. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  157. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  158. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  159. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  160. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  161. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  162. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  163. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  164. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  165. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  166. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  167. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  168. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  169. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  170. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  171. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  172. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  173. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  174. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  175. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  176. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  177. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  178. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  179. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  180. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  181. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  182. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  183. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  184. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  185. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  186. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  187. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  188. package/.agents/skills/hitl/SKILL.md +0 -45
  189. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  190. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  191. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  192. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  193. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  194. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  195. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  196. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  197. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  198. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  199. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  200. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  201. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  202. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  203. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  204. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  205. package/.agents/skills/prototype/SKILL.md +0 -85
  206. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  207. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  208. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  209. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  210. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  211. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  212. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  213. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  214. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  215. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  216. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  217. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  218. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  219. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  220. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  221. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  222. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  223. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  224. package/.agents/skills/rebase/AGENTS.md +0 -1
  225. package/.agents/skills/rebase/SKILL.md +0 -178
  226. package/.agents/skills/review-router/SKILL.md +0 -51
  227. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  228. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  229. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  230. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  231. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  232. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  233. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  234. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  235. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  236. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  237. package/.agents/skills/review-tier/SKILL.md +0 -56
  238. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  239. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  240. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  241. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  242. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  243. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  244. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  245. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  246. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  247. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  248. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  249. package/.agents/skills/session-log/AGENTS.md +0 -1
  250. package/.agents/skills/session-log/SKILL.md +0 -170
  251. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  252. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  253. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  254. package/.agents/skills/session-tidy/SKILL.md +0 -191
  255. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  256. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  257. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  258. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  259. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  260. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  261. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  262. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  263. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  264. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  265. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  266. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  267. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  268. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  269. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  270. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  271. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  272. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  273. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  274. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  275. package/.agents/skills/update/AGENTS.md +0 -1
  276. package/.agents/skills/update/SKILL.md +0 -190
  277. package/_shared/pr-loop/AGENTS.md +0 -1
  278. package/_shared/pr-loop/CLAUDE.md +0 -1
  279. package/_shared/pr-loop/audit-contract.md +0 -183
  280. package/_shared/pr-loop/audit-reply-template.md +0 -151
  281. package/_shared/pr-loop/code-rules-gate.md +0 -65
  282. package/_shared/pr-loop/fix-protocol.md +0 -60
  283. package/_shared/pr-loop/gh-payloads.md +0 -75
  284. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  285. package/_shared/pr-loop/precatch-rubric.md +0 -66
  286. package/_shared/pr-loop/preflight-proposal.md +0 -56
  287. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  288. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  289. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  290. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  291. package/_shared/pr-loop/state-schema.md +0 -45
  292. package/_shared/pr-loop/worker-spawn.md +0 -189
  293. /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
@@ -1,1024 +0,0 @@
1
- """Render a visual convergence summary HTML report from an autoconverge journal."""
2
-
3
- import argparse
4
- import html
5
- import json
6
- import re
7
- import sys
8
- from dataclasses import dataclass
9
- from pathlib import Path
10
- from typing import TextIO
11
-
12
- from autoconverge_report_constants.render_report_constants import (
13
- CATEGORY_BUG,
14
- CATEGORY_LABEL_BY_VALUE,
15
- CATEGORY_SORT_ORDER,
16
- CAUSE_MUTED_STYLE,
17
- DEFAULT_FINDING_CATEGORY,
18
- DEFAULT_FINDING_SEVERITY,
19
- DEFAULT_ISSUE_ICON,
20
- HTML_TITLE_AND_STYLE_TEMPLATE,
21
- HTML_STYLE_BLOCK,
22
- ISO_DATE_LENGTH,
23
- ISSUE_CLASS_FIELD_AFTER_LINES,
24
- ISSUE_CLASS_FIELD_BEFORE_LINES,
25
- ISSUE_CLASS_FIELD_CATEGORY,
26
- ISSUE_CLASS_FIELD_CAUSE,
27
- ISSUE_CLASS_FIELD_COUNT,
28
- ISSUE_CLASS_FIELD_MEDIUM,
29
- ISSUE_CLASS_FIELD_PLAINNAME,
30
- ISSUE_CLASS_FIELD_SEVERITY,
31
- ISSUE_CLASS_FIELD_STATUS,
32
- ISSUE_ICON_BY_CATEGORY,
33
- JOURNAL_SIBLING_SUBAGENTS,
34
- JOURNAL_SIBLING_WORKFLOWS,
35
- LABEL_CONVERGENCE_SUMMARY,
36
- LABEL_COPILOT_GATE,
37
- LABEL_PREFIX_FIX,
38
- LABEL_PREFIX_LENS,
39
- LABEL_RESOLVE_HEAD,
40
- MEDIUM_CODE,
41
- MEDIUM_TERMINAL,
42
- SCENE_FIELD_CAPTION,
43
- SCENE_FIELD_CONDITION,
44
- SCENE_FIELD_RESULT,
45
- SCENE_FIELD_TRIGGER,
46
- SCORECARD_LABEL_CAUGHT,
47
- SCORECARD_LABEL_REMAINING,
48
- SCORECARD_LABEL_ROUNDS,
49
- SEVERITY_SORT_RANK,
50
- SHORT_SHA_LENGTH,
51
- STATUS_LABEL_BY_VALUE,
52
- STRUCTURED_OUTPUT_TOOL_NAME,
53
- SUMMARY_FIELD_FIX_SCENES,
54
- SUMMARY_FIELD_ISSUE_CLASSES,
55
- SUMMARY_FIELD_PR_FIX,
56
- SUMMARY_FIELD_PR_PROBLEM,
57
- SUMMARY_FIELD_PROBLEM_SCENES,
58
- SUMMARY_FIELD_VERDICT_LINE,
59
- TIMELINE_AFTER_LABEL,
60
- TIMELINE_AFTER_PILL,
61
- TIMELINE_BEFORE_LABEL,
62
- TIMELINE_BEFORE_PILL,
63
- TIMELINE_TERMINAL_BAR_LABEL,
64
- )
65
-
66
-
67
- @dataclass(frozen=True)
68
- class RawFinding:
69
- """A single finding from a lens or copilot-gate agent result."""
70
-
71
- file: str
72
- line: int
73
- severity: str
74
- title: str
75
- detail: str
76
- category: str
77
-
78
-
79
- @dataclass(frozen=True)
80
- class FixRecord:
81
- """The structured result of a fix agent, with base-sha context attached."""
82
-
83
- new_sha: str
84
- pushed: bool
85
- resolved_without_commit: bool
86
- summary: str
87
- base_sha: str
88
-
89
-
90
- @dataclass(frozen=True)
91
- class PrMetadata:
92
- """Owner, repo, number, final sha, and round count for the PR being reported on."""
93
-
94
- owner: str
95
- repo: str
96
- number: int
97
- final_sha: str
98
- round_count: int
99
-
100
-
101
- @dataclass
102
- class RunData:
103
- """Aggregated metrics parsed from a workflow journal and agent transcripts."""
104
-
105
- generated_date: str
106
- total_finding_count: int
107
- fix_commit_count: int
108
- all_distinct_findings: list[RawFinding]
109
- fix_by_round: dict[int, FixRecord]
110
- convergence_summary: dict | None
111
-
112
-
113
- def _resolve_agents_dir(journal_path: Path) -> Path:
114
- """Return the directory containing per-agent transcript files for this run.
115
-
116
- Args:
117
- journal_path: Absolute path to the wf_<runId>.json journal file.
118
-
119
- Returns:
120
- Path to the subagents/workflows/<runId>/ directory.
121
- """
122
- run_id = journal_path.stem
123
- return (
124
- journal_path.parent.parent
125
- / JOURNAL_SIBLING_SUBAGENTS
126
- / JOURNAL_SIBLING_WORKFLOWS
127
- / run_id
128
- )
129
-
130
-
131
- def _extract_structured_output(transcript_path: Path) -> dict | None:
132
- """Return the last StructuredOutput tool input from an agent transcript.
133
-
134
- Args:
135
- transcript_path: Path to an agent-<id>.jsonl file.
136
-
137
- Returns:
138
- The input dict of the last StructuredOutput tool_use, or None when absent.
139
- """
140
- last_input: dict | None = None
141
- try:
142
- with transcript_path.open(encoding="utf-8") as transcript_file:
143
- for each_line in transcript_file:
144
- last_input = _last_structured_input_in_line(each_line, last_input)
145
- except OSError:
146
- return None
147
-
148
- return last_input
149
-
150
-
151
- def _last_structured_input_in_line(
152
- transcript_line: str, prior_input: dict | None
153
- ) -> dict | None:
154
- """Return the StructuredOutput input on a transcript line, else the prior input.
155
-
156
- Args:
157
- transcript_line: One raw JSON line from an agent transcript.
158
- prior_input: The last StructuredOutput input seen before this line.
159
-
160
- Returns:
161
- The input dict of the last StructuredOutput tool_use on this line, or
162
- prior_input when the line carries none.
163
- """
164
- try:
165
- parsed = json.loads(transcript_line)
166
- except (ValueError, json.JSONDecodeError):
167
- return prior_input
168
-
169
- message = parsed.get("message") if isinstance(parsed, dict) else None
170
- content_list = message.get("content") if isinstance(message, dict) else None
171
- if not isinstance(content_list, list):
172
- return prior_input
173
-
174
- latest_input = prior_input
175
- for each_block in content_list:
176
- if not isinstance(each_block, dict):
177
- continue
178
- if (
179
- each_block.get("type") == "tool_use"
180
- and each_block.get("name") == STRUCTURED_OUTPUT_TOOL_NAME
181
- and isinstance(each_block.get("input"), dict)
182
- ):
183
- latest_input = each_block["input"]
184
-
185
- return latest_input
186
-
187
-
188
- def _build_dedup_key(file_path: str, line: int, title: str) -> tuple[str, int, str]:
189
- """Return a deduplication key for a finding.
190
-
191
- Args:
192
- file_path: The file field from the finding.
193
- line: The line number from the finding.
194
- title: The title field from the finding.
195
-
196
- Returns:
197
- A tuple of (file, line, lowercased_title).
198
- """
199
- return (file_path, line, title.lower())
200
-
201
-
202
- def _parse_finding_from_dict(raw: dict) -> RawFinding:
203
- """Construct a RawFinding from a raw agent result dict.
204
-
205
- Args:
206
- raw: The raw finding dict from the agent result.
207
-
208
- Returns:
209
- A RawFinding dataclass instance.
210
- """
211
- return RawFinding(
212
- file=raw.get("file", ""),
213
- line=raw.get("line", 0),
214
- severity=raw.get("severity", DEFAULT_FINDING_SEVERITY),
215
- title=raw.get("title", ""),
216
- detail=raw.get("detail", ""),
217
- category=raw.get("category", DEFAULT_FINDING_CATEGORY),
218
- )
219
-
220
-
221
- def _parse_fix_record(agent_result: dict, base_sha: str) -> FixRecord:
222
- """Construct a FixRecord from a fix agent's structured output.
223
-
224
- Args:
225
- agent_result: The structured output dict from the fix agent.
226
- base_sha: The HEAD sha the round reviewed before fixing.
227
-
228
- Returns:
229
- A FixRecord dataclass instance.
230
- """
231
- return FixRecord(
232
- new_sha=agent_result.get("newSha", ""),
233
- pushed=bool(agent_result.get("pushed", False)),
234
- resolved_without_commit=bool(agent_result.get("resolvedWithoutCommit", False)),
235
- summary=agent_result.get("summary", ""),
236
- base_sha=base_sha,
237
- )
238
-
239
-
240
- def _parse_progress_entries(
241
- progress_entries: list[dict],
242
- agents_dir: Path,
243
- ) -> tuple[list[RawFinding], dict[int, FixRecord]]:
244
- """Walk workflowProgress in order and collect findings and fix results by round.
245
-
246
- Args:
247
- progress_entries: The workflowProgress list from the journal.
248
- agents_dir: Directory containing per-agent .jsonl transcript files.
249
-
250
- Returns:
251
- A tuple of (all_raw_findings, fix_by_round).
252
- """
253
- all_findings: list[RawFinding] = []
254
- fix_by_round: dict[int, FixRecord] = {}
255
- current_round = 0
256
- current_round_base_sha = ""
257
-
258
- for each_entry in progress_entries:
259
- label: str = each_entry.get("label", "")
260
- agent_id: str | None = each_entry.get("agentId")
261
-
262
- if label == LABEL_RESOLVE_HEAD:
263
- current_round += 1
264
- current_round_base_sha = ""
265
- continue
266
-
267
- if agent_id is None:
268
- continue
269
-
270
- transcript_path = agents_dir / f"agent-{agent_id}.jsonl"
271
- agent_result = _extract_structured_output(transcript_path)
272
- if agent_result is None:
273
- continue
274
-
275
- is_lens = label.startswith(LABEL_PREFIX_LENS)
276
- is_copilot = label == LABEL_COPILOT_GATE
277
- is_fix = label.startswith(LABEL_PREFIX_FIX)
278
-
279
- if is_lens or is_copilot:
280
- sha = agent_result.get("sha", "")
281
- if not current_round_base_sha and sha:
282
- current_round_base_sha = sha
283
- raw_findings: list[dict] = agent_result.get("findings", [])
284
- for each_raw in raw_findings:
285
- all_findings.append(_parse_finding_from_dict(each_raw))
286
-
287
- if is_fix:
288
- fix_by_round[current_round] = _parse_fix_record(
289
- agent_result, current_round_base_sha
290
- )
291
-
292
- return all_findings, fix_by_round
293
-
294
-
295
- def _dedup_findings(all_findings: list[RawFinding]) -> list[RawFinding]:
296
- """Deduplicate findings globally by (file, line, lower title), keeping earliest round.
297
-
298
- Args:
299
- all_findings: All raw findings in discovery order.
300
-
301
- Returns:
302
- A list of distinct findings with the earliest occurrence retained.
303
- """
304
- seen_keys: set[tuple[str, int, str]] = set()
305
- distinct: list[RawFinding] = []
306
- for each_finding in all_findings:
307
- dedup_key = _build_dedup_key(
308
- each_finding.file, each_finding.line, each_finding.title
309
- )
310
- if dedup_key not in seen_keys:
311
- seen_keys.add(dedup_key)
312
- distinct.append(each_finding)
313
- return distinct
314
-
315
-
316
- def _extract_convergence_summary(
317
- progress_entries: list[dict], agents_dir: Path
318
- ) -> dict | None:
319
- """Return the convergence-summary StructuredOutput, or None when absent.
320
-
321
- Args:
322
- progress_entries: The workflowProgress list from the journal.
323
- agents_dir: Directory containing per-agent .jsonl transcript files.
324
-
325
- Returns:
326
- The summarizer agent's last StructuredOutput input dict, or None when
327
- no convergence-summary entry exists or its transcript is unreadable.
328
- """
329
- for each_entry in progress_entries:
330
- if each_entry.get("label") != LABEL_CONVERGENCE_SUMMARY:
331
- continue
332
- agent_id = each_entry.get("agentId")
333
- if not agent_id:
334
- return None
335
- transcript_path = agents_dir / f"agent-{agent_id}.jsonl"
336
- return _extract_structured_output(transcript_path)
337
- return None
338
-
339
-
340
- def load_run_data(journal_path: Path) -> RunData:
341
- """Parse a workflow journal and its agent transcripts into aggregated metrics.
342
-
343
- Args:
344
- journal_path: Path to the wf_<runId>.json journal file.
345
-
346
- Returns:
347
- A RunData instance with finding lists, fix records, and the summary populated.
348
- """
349
- iso_date_length = ISO_DATE_LENGTH
350
- journal = json.loads(journal_path.read_text(encoding="utf-8"))
351
- timestamp: str = journal.get("timestamp", "")
352
- generated_date = (
353
- timestamp[:iso_date_length] if len(timestamp) >= iso_date_length else ""
354
- )
355
-
356
- progress_entries: list[dict] = journal.get("workflowProgress", [])
357
- agents_dir = _resolve_agents_dir(journal_path)
358
-
359
- all_raw_findings, fix_by_round = _parse_progress_entries(
360
- progress_entries, agents_dir
361
- )
362
- distinct_findings = _dedup_findings(all_raw_findings)
363
- convergence_summary = _extract_convergence_summary(progress_entries, agents_dir)
364
-
365
- fix_commit_count = sum(1 for each_fix in fix_by_round.values() if each_fix.pushed)
366
-
367
- return RunData(
368
- generated_date=generated_date,
369
- total_finding_count=len(distinct_findings),
370
- fix_commit_count=fix_commit_count,
371
- all_distinct_findings=distinct_findings,
372
- fix_by_round=fix_by_round,
373
- convergence_summary=convergence_summary,
374
- )
375
-
376
-
377
- def _is_summary_structurally_valid(convergence_summary: object) -> bool:
378
- """Return whether the summary carries a verdict string and an issue-class list.
379
-
380
- Args:
381
- convergence_summary: The parsed convergence summary, which may be None or
382
- any non-dict JSON value an agent emits in place of the expected object.
383
-
384
- Returns:
385
- True when the summary is a dict whose verdictLine is a str and whose
386
- issueClasses is a list, else False.
387
- """
388
- if not isinstance(convergence_summary, dict):
389
- return False
390
- verdict_line = convergence_summary.get(SUMMARY_FIELD_VERDICT_LINE)
391
- issue_classes = convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES)
392
- return isinstance(verdict_line, str) and isinstance(issue_classes, list)
393
-
394
-
395
- def _coerce_count(raw_count: object) -> int:
396
- """Return a non-negative integer count from an LLM-authored field, else zero.
397
-
398
- Args:
399
- raw_count: The count value as read from the convergence summary, which an
400
- agent may emit as null, a non-numeric string, or a valid number.
401
-
402
- Returns:
403
- The value parsed to an int, or 0 when it is null, non-numeric, or absent.
404
- """
405
- if isinstance(raw_count, bool):
406
- return int(raw_count)
407
- if isinstance(raw_count, int):
408
- return raw_count
409
- if isinstance(raw_count, (float, str)):
410
- try:
411
- return int(float(raw_count))
412
- except (TypeError, ValueError):
413
- return 0
414
- return 0
415
-
416
-
417
- def _pluralize(count: int, singular: str, plural: str) -> str:
418
- """Return a 'count word' phrase choosing the singular or plural noun.
419
-
420
- Args:
421
- count: The quantity that decides the noun form.
422
- singular: The noun to use when count is exactly one.
423
- plural: The noun to use for every other count.
424
-
425
- Returns:
426
- A phrase like '1 fix commit' or '2 fix commits'.
427
- """
428
- noun = singular if count == 1 else plural
429
- return f"{count} {noun}"
430
-
431
-
432
- def _render_verdict_banner(
433
- convergence_summary: dict, run_data: RunData, final_sha_short: str
434
- ) -> str:
435
- """Return the verdict banner with the verdict line and a Python-computed sub-line.
436
-
437
- Args:
438
- convergence_summary: A structurally valid convergence summary.
439
- run_data: Aggregated metrics from the journal.
440
- final_sha_short: First eight characters of the final commit sha.
441
-
442
- Returns:
443
- An HTML .verdict banner string with a check circle, vtext, and vsub.
444
- """
445
- verdict_line = str(convergence_summary.get(SUMMARY_FIELD_VERDICT_LINE, ""))
446
- fix_commit_phrase = _pluralize(
447
- run_data.fix_commit_count, "fix commit", "fix commits"
448
- )
449
- vsub = f"{fix_commit_phrase} &middot; final commit {html.escape(final_sha_short)}"
450
- return (
451
- '<div class="verdict">'
452
- '<div class="check">&#10003;</div>'
453
- "<div>"
454
- f'<div class="vtext">{html.escape(verdict_line)}</div>'
455
- f'<div class="vsub">{vsub}</div>'
456
- "</div>"
457
- "</div>"
458
- )
459
-
460
-
461
- def _render_scorecard(run_data: RunData, round_count: int) -> str:
462
- """Return the at-a-glance scorecard: findings caught, rounds, and zero remaining.
463
-
464
- Args:
465
- run_data: Aggregated metrics from the journal.
466
- round_count: Total number of convergence rounds.
467
-
468
- Returns:
469
- An HTML .scorecard grid of three .stat tiles; the remaining tile is marked
470
- .good and reads zero, since the report renders only on a converged run.
471
- """
472
- remaining_count = 0
473
- return (
474
- '<div class="scorecard">'
475
- f'<div class="stat"><div class="stat-num">{run_data.total_finding_count}</div>'
476
- f'<div class="stat-label">{SCORECARD_LABEL_CAUGHT}</div></div>'
477
- f'<div class="stat"><div class="stat-num">{round_count}</div>'
478
- f'<div class="stat-label">{SCORECARD_LABEL_ROUNDS}</div></div>'
479
- f'<div class="stat good"><div class="stat-num">{remaining_count}</div>'
480
- f'<div class="stat-label">{SCORECARD_LABEL_REMAINING}</div></div>'
481
- "</div>"
482
- )
483
-
484
-
485
- def _render_scene_row(scene: dict, is_problem: bool) -> str:
486
- """Return one .scene row plus its caption for a problem-or-fix scene.
487
-
488
- Args:
489
- scene: A scene dict with trigger, condition, result, and caption.
490
- is_problem: True for a problem scene (bad result), False for a fix scene.
491
-
492
- Returns:
493
- An HTML .scene row followed by a .scene-cap caption line.
494
- """
495
- trigger = html.escape(str(scene.get(SCENE_FIELD_TRIGGER, "")))
496
- condition = str(scene.get(SCENE_FIELD_CONDITION, "")).strip()
497
- result_text = html.escape(str(scene.get(SCENE_FIELD_RESULT, "")))
498
- caption = html.escape(str(scene.get(SCENE_FIELD_CAPTION, "")))
499
-
500
- result_class = "res-bad" if is_problem else "res-good"
501
- result_mark = "&#10007;" if is_problem else "&#10003;"
502
-
503
- parts = [f'<span class="chip">{trigger}</span>']
504
- if condition:
505
- parts.append('<span class="arrow">&rarr;</span>')
506
- parts.append(f'<span class="note">{html.escape(condition)}</span>')
507
- parts.append('<span class="arrow">&rarr;</span>')
508
- parts.append(f'<span class="{result_class}">{result_mark} {result_text}</span>')
509
-
510
- scene_row = f'<div class="scene">{"".join(parts)}</div>'
511
- caption_row = f'<div class="scene-cap">{caption}</div>'
512
- return scene_row + caption_row
513
-
514
-
515
- def _render_pf_card(convergence_summary: dict, is_problem: bool) -> str:
516
- """Return a single problem-or-fix card drawing its scenes or a fallback caption.
517
-
518
- Args:
519
- convergence_summary: A structurally valid convergence summary.
520
- is_problem: True for the problem card, False for the fix card.
521
-
522
- Returns:
523
- An HTML .pf card string with scene rows or a single fallback caption.
524
- """
525
- if is_problem:
526
- card_class = "problem"
527
- tag_label = "Problem"
528
- scenes_field = SUMMARY_FIELD_PROBLEM_SCENES
529
- fallback_field = SUMMARY_FIELD_PR_PROBLEM
530
- else:
531
- card_class = "fix"
532
- tag_label = "Fix"
533
- scenes_field = SUMMARY_FIELD_FIX_SCENES
534
- fallback_field = SUMMARY_FIELD_PR_FIX
535
-
536
- scenes = [
537
- each_scene
538
- for each_scene in convergence_summary.get(scenes_field, [])
539
- if isinstance(each_scene, dict)
540
- ]
541
- if scenes:
542
- body = "".join(
543
- _render_scene_row(each_scene, is_problem) for each_scene in scenes
544
- )
545
- else:
546
- fallback_text = html.escape(str(convergence_summary.get(fallback_field, "")))
547
- body = f'<div class="scene-cap">{fallback_text}</div>'
548
-
549
- return (
550
- f'<div class="pf {card_class}">'
551
- f'<span class="pf-tag">{tag_label}</span>'
552
- f"{body}"
553
- "</div>"
554
- )
555
-
556
-
557
- def _render_pf_grid(convergence_summary: dict) -> str:
558
- """Return the 'What this PR does' grid with a problem card and a fix card.
559
-
560
- Args:
561
- convergence_summary: A structurally valid convergence summary.
562
-
563
- Returns:
564
- An HTML .pf-grid string with both cards.
565
- """
566
- problem_card = _render_pf_card(convergence_summary, is_problem=True)
567
- fix_card = _render_pf_card(convergence_summary, is_problem=False)
568
- return f'<div class="pf-grid">{problem_card}{fix_card}</div>'
569
-
570
-
571
- def _render_panel_body(lines: list[str], medium: str) -> str:
572
- """Return the inner HTML for a before-or-after panel body.
573
-
574
- Args:
575
- lines: The literal short lines to show, joined with line breaks.
576
- medium: One of 'terminal', 'code', or 'text'.
577
-
578
- Returns:
579
- An HTML panel-body string escaped and joined with <br>.
580
- """
581
- escaped_lines = [html.escape(str(each_line)) for each_line in lines]
582
- joined = "<br>".join(escaped_lines)
583
- if medium == MEDIUM_TERMINAL:
584
- return (
585
- '<div class="terminal">'
586
- '<div class="term-bar"><i class="r"></i><i class="y"></i><i class="g"></i>'
587
- f"<span>{TIMELINE_TERMINAL_BAR_LABEL}</span></div>"
588
- f'<div class="term-body">{joined}</div>'
589
- "</div>"
590
- )
591
- panel_class = "code-panel" if medium == MEDIUM_CODE else "text-panel"
592
- return f'<div class="{panel_class}">{joined}</div>'
593
-
594
-
595
- def _render_term_grid(issue_class: dict, medium: str) -> str:
596
- """Return the before/after panel grid for one issue class.
597
-
598
- Args:
599
- issue_class: One issue-class dict from the summary.
600
- medium: The medium that styles both panels.
601
-
602
- Returns:
603
- An HTML .term-grid string with a before panel and an after panel.
604
- """
605
- before_lines = list(issue_class.get(ISSUE_CLASS_FIELD_BEFORE_LINES, []))
606
- after_lines = list(issue_class.get(ISSUE_CLASS_FIELD_AFTER_LINES, []))
607
- before_body = _render_panel_body(before_lines, medium)
608
- after_body = _render_panel_body(after_lines, medium)
609
- return (
610
- '<div class="term-grid">'
611
- '<div class="term-wrap before">'
612
- f'<div class="tlabel">{TIMELINE_BEFORE_LABEL} '
613
- f'<span class="pill-x">{TIMELINE_BEFORE_PILL}</span></div>'
614
- f"{before_body}"
615
- "</div>"
616
- '<div class="term-wrap after">'
617
- f'<div class="tlabel">{TIMELINE_AFTER_LABEL} '
618
- f'<span class="pill-c">{TIMELINE_AFTER_PILL}</span></div>'
619
- f"{after_body}"
620
- "</div>"
621
- "</div>"
622
- )
623
-
624
-
625
- def _render_cause_line(issue_class: dict) -> str:
626
- """Return the cause line plus a muted severity/category/count/status parenthetical.
627
-
628
- Args:
629
- issue_class: One issue-class dict from the summary.
630
-
631
- Returns:
632
- An HTML .cause string.
633
- """
634
- cause = html.escape(str(issue_class.get(ISSUE_CLASS_FIELD_CAUSE, "")))
635
- severity = str(issue_class.get(ISSUE_CLASS_FIELD_SEVERITY, DEFAULT_FINDING_SEVERITY))
636
- category = str(issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG))
637
- count = _coerce_count(issue_class.get(ISSUE_CLASS_FIELD_COUNT, 0))
638
- status = str(issue_class.get(ISSUE_CLASS_FIELD_STATUS, ""))
639
-
640
- category_label = CATEGORY_LABEL_BY_VALUE.get(category, category)
641
- status_label = STATUS_LABEL_BY_VALUE.get(status, status)
642
- parenthetical = (
643
- f"({html.escape(severity)} {html.escape(category_label)} "
644
- f"&middot; &times;{count} &middot; {html.escape(status_label)})"
645
- )
646
- return (
647
- '<div class="cause">'
648
- f"<b>Why it happened:</b> {cause} "
649
- f'<span style="{CAUSE_MUTED_STYLE}">{parenthetical}</span>'
650
- "</div>"
651
- )
652
-
653
-
654
- def _issue_class_sort_key(issue_class: dict) -> tuple[int, int]:
655
- """Return a sort key ordering bug classes first, then by severity.
656
-
657
- Args:
658
- issue_class: One issue-class dict from the summary.
659
-
660
- Returns:
661
- A tuple of (category rank, severity rank); lower sorts first.
662
- """
663
- category = issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG)
664
- severity = issue_class.get(ISSUE_CLASS_FIELD_SEVERITY, DEFAULT_FINDING_SEVERITY)
665
- category_rank = CATEGORY_SORT_ORDER.get(category, len(CATEGORY_SORT_ORDER))
666
- severity_rank = SEVERITY_SORT_RANK.get(severity, len(SEVERITY_SORT_RANK))
667
- return (category_rank, severity_rank)
668
-
669
-
670
- def _render_issue_class_heading(issue_class: dict) -> str:
671
- """Return the per-class heading with a category icon, plain name, and count.
672
-
673
- Args:
674
- issue_class: One issue-class dict from the summary.
675
-
676
- Returns:
677
- An HTML .bug-head block: a category icon and the plain bug name grouped in
678
- a .bug-title span, plus a finding-count chip.
679
- """
680
- plain_name = html.escape(str(issue_class.get(ISSUE_CLASS_FIELD_PLAINNAME, "")))
681
- count = _coerce_count(issue_class.get(ISSUE_CLASS_FIELD_COUNT, 0))
682
- count_phrase = html.escape(_pluralize(count, "finding", "findings"))
683
- category = str(issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG))
684
- icon = ISSUE_ICON_BY_CATEGORY.get(category, DEFAULT_ISSUE_ICON)
685
- return (
686
- '<div class="bug-head">'
687
- '<span class="bug-title">'
688
- f'<span class="bug-icon">{icon}</span>'
689
- f'<span class="bug-name">{plain_name}</span>'
690
- "</span>"
691
- f'<span class="bug-count">{count_phrase}</span>'
692
- "</div>"
693
- )
694
-
695
-
696
- def _render_issue_class_block(issue_class: dict) -> str:
697
- """Return one issue-class block: a name heading, before/after panels, a cause line.
698
-
699
- Args:
700
- issue_class: One issue-class dict from the summary.
701
-
702
- Returns:
703
- An HTML fragment that opens with the .bug-head name heading, then a
704
- .term-grid when panel lines exist, then the .cause line; the heading and
705
- cause line alone when both before and after lines are empty.
706
- """
707
- heading = _render_issue_class_heading(issue_class)
708
- medium = str(issue_class.get(ISSUE_CLASS_FIELD_MEDIUM, MEDIUM_TERMINAL))
709
- before_lines = list(issue_class.get(ISSUE_CLASS_FIELD_BEFORE_LINES, []))
710
- after_lines = list(issue_class.get(ISSUE_CLASS_FIELD_AFTER_LINES, []))
711
-
712
- cause_line = _render_cause_line(issue_class)
713
- if not before_lines and not after_lines:
714
- return heading + cause_line
715
-
716
- term_grid = _render_term_grid(issue_class, medium)
717
- return heading + term_grid + cause_line
718
-
719
-
720
- def _render_issue_class_panels(convergence_summary: dict) -> str:
721
- """Return the per-issue-class before/after panels and cause lines.
722
-
723
- Args:
724
- convergence_summary: A structurally valid convergence summary.
725
-
726
- Returns:
727
- An HTML fragment with one block per issue class, ordered bug classes
728
- first then by severity.
729
- """
730
- issue_classes = [
731
- each_class
732
- for each_class in convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES, [])
733
- if isinstance(each_class, dict)
734
- ]
735
- if not issue_classes:
736
- return (
737
- '<p class="subtitle">No issues were caught &mdash; '
738
- "every review lens was clean.</p>"
739
- )
740
- sorted_classes = sorted(issue_classes, key=_issue_class_sort_key)
741
- return "".join(
742
- _render_issue_class_block(each_class) for each_class in sorted_classes
743
- )
744
-
745
-
746
- def _render_caught_lead(
747
- convergence_summary: dict, run_data: RunData, round_count: int
748
- ) -> str:
749
- """Return the run-stats lead line that opens the caught section.
750
-
751
- Args:
752
- convergence_summary: A structurally valid convergence summary.
753
- run_data: Aggregated metrics from the journal.
754
- round_count: Total number of convergence rounds.
755
-
756
- Returns:
757
- An HTML .subtitle line stating bug-class, finding, round, and fix counts.
758
- """
759
- issue_classes = [
760
- each_class
761
- for each_class in convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES, [])
762
- if isinstance(each_class, dict)
763
- ]
764
- class_phrase = _pluralize(len(issue_classes), "bug class", "bug classes")
765
- finding_phrase = _pluralize(run_data.total_finding_count, "finding", "findings")
766
- round_phrase = _pluralize(round_count, "round", "rounds")
767
- fix_phrase = _pluralize(run_data.fix_commit_count, "fix commit", "fix commits")
768
- return (
769
- f'<p class="subtitle">{class_phrase} ({finding_phrase} in all), '
770
- f"caught and fixed across {round_phrase} in {fix_phrase}.</p>"
771
- )
772
-
773
-
774
- def _appendix_finding_sort_key(finding: RawFinding) -> tuple[int, int, str, int]:
775
- """Return a sort key grouping appendix findings by category then severity.
776
-
777
- Args:
778
- finding: One distinct finding.
779
-
780
- Returns:
781
- A tuple of (category rank, severity rank, file, line).
782
- """
783
- category_rank = CATEGORY_SORT_ORDER.get(finding.category, len(CATEGORY_SORT_ORDER))
784
- severity_rank = SEVERITY_SORT_RANK.get(finding.severity, len(SEVERITY_SORT_RANK))
785
- return (category_rank, severity_rank, finding.file, finding.line)
786
-
787
-
788
- def _render_appendix(findings: list[RawFinding]) -> str:
789
- """Return a collapsed <details> appendix of raw distinct findings.
790
-
791
- Args:
792
- findings: The distinct findings to list, grouped by category then severity.
793
-
794
- Returns:
795
- An HTML <details> string listing each finding as 'file:line — P# — title'.
796
- """
797
- if not findings:
798
- return ""
799
- sorted_findings = sorted(findings, key=_appendix_finding_sort_key)
800
- items = "".join(
801
- f'<div class="appendix-item">'
802
- f"{html.escape(each_finding.file)}:{each_finding.line} &mdash; "
803
- f"{html.escape(each_finding.severity)} &mdash; "
804
- f"{html.escape(each_finding.title)}"
805
- f"</div>"
806
- for each_finding in sorted_findings
807
- )
808
- return (
809
- '<details class="appendix">'
810
- f"<summary>Raw findings ({len(findings)})</summary>"
811
- f'<div class="appendix-body">{items}</div>'
812
- "</details>"
813
- )
814
-
815
-
816
- def _render_summary_body(
817
- run_data: RunData, round_count: int, final_sha_short: str
818
- ) -> str:
819
- """Return the body for a run that carries a valid convergence summary.
820
-
821
- Args:
822
- run_data: Aggregated metrics from the journal.
823
- round_count: Total number of convergence rounds.
824
- final_sha_short: First eight characters of the final commit sha.
825
-
826
- Returns:
827
- An HTML body fragment: verdict banner, an at-a-glance scorecard,
828
- problem/fix cards, then a single caught section that opens with a
829
- run-stats lead line and holds the issue-class before/after panels,
830
- followed by the collapsed appendix.
831
- """
832
- convergence_summary = run_data.convergence_summary
833
- if convergence_summary is None:
834
- return _render_degraded_body(run_data, round_count, final_sha_short)
835
-
836
- verdict_banner = _render_verdict_banner(
837
- convergence_summary, run_data, final_sha_short
838
- )
839
- scorecard = _render_scorecard(run_data, round_count)
840
- pf_grid = _render_pf_grid(convergence_summary)
841
- caught_lead = _render_caught_lead(convergence_summary, run_data, round_count)
842
- issue_panels = _render_issue_class_panels(convergence_summary)
843
- appendix = _render_appendix(run_data.all_distinct_findings)
844
- return (
845
- f"{verdict_banner}"
846
- f"{scorecard}"
847
- f"<h2>What this PR does</h2>{pf_grid}"
848
- f"<h2>What was caught &mdash; and how it looked</h2>{caught_lead}{issue_panels}"
849
- f"{appendix}"
850
- )
851
-
852
-
853
- def _render_degraded_body(
854
- run_data: RunData, round_count: int, final_sha_short: str
855
- ) -> str:
856
- """Return the minimal degraded body for a run with no valid summary.
857
-
858
- Args:
859
- run_data: Aggregated metrics from the journal.
860
- round_count: Total number of convergence rounds.
861
- final_sha_short: First eight characters of the final commit sha.
862
-
863
- Returns:
864
- An HTML body fragment: a plain run-stats note and the collapsed
865
- raw-findings appendix, with no scene, table, rollup, or timeline markup.
866
- """
867
- fix_commit_phrase = _pluralize(
868
- run_data.fix_commit_count, "fix commit", "fix commits"
869
- )
870
- note = (
871
- '<p class="subtitle">'
872
- f"{run_data.total_finding_count} distinct findings across {round_count} "
873
- f"rounds, resolved in {fix_commit_phrase}. "
874
- f"Final commit <code>{html.escape(final_sha_short)}</code>."
875
- "</p>"
876
- )
877
- appendix = _render_appendix(run_data.all_distinct_findings)
878
- return f"{note}{appendix}"
879
-
880
-
881
- def render_report_html(
882
- run_data: RunData, pr_metadata: PrMetadata, generated_date: str
883
- ) -> str:
884
- """Render the convergence summary report as an HTML string.
885
-
886
- Args:
887
- run_data: Aggregated metrics from the workflow journal and transcripts.
888
- pr_metadata: Owner, repo, number, final sha, and round count for the PR.
889
- generated_date: ISO date string derived from the journal timestamp.
890
-
891
- Returns:
892
- A page-content HTML string (a title, a style block, and the body
893
- markup) with no surrounding <!DOCTYPE>, <html>, <head>, or <body>
894
- tags, ready for the Artifact tool to wrap and publish.
895
- """
896
- short_sha_length = SHORT_SHA_LENGTH
897
- pr_number = pr_metadata.number
898
- owner = html.escape(pr_metadata.owner)
899
- repo = html.escape(pr_metadata.repo)
900
- final_sha_short = pr_metadata.final_sha[:short_sha_length]
901
- round_count = pr_metadata.round_count
902
-
903
- title_and_style_html = HTML_TITLE_AND_STYLE_TEMPLATE.format(
904
- pr_number=pr_number,
905
- style_block=HTML_STYLE_BLOCK,
906
- )
907
-
908
- subtitle = (
909
- f'<p class="subtitle">{owner}/{repo} &middot; '
910
- f"{run_data.total_finding_count} findings over {round_count} rounds "
911
- f"&middot; {html.escape(generated_date)}</p>"
912
- )
913
-
914
- if _is_summary_structurally_valid(run_data.convergence_summary):
915
- body_main = _render_summary_body(run_data, round_count, final_sha_short)
916
- else:
917
- body_main = _render_degraded_body(run_data, round_count, final_sha_short)
918
-
919
- fix_commit_phrase = _pluralize(
920
- run_data.fix_commit_count, "fix commit", "fix commits"
921
- )
922
- footer = (
923
- f"<footer>{owner}/{repo} &middot; PR #{pr_number} &middot; "
924
- f"{run_data.total_finding_count} findings &middot; {round_count} rounds "
925
- f"&middot; {fix_commit_phrase} &middot; "
926
- f"generated {html.escape(generated_date)} from the autoconverge run journal.</footer>"
927
- )
928
-
929
- body_content = (
930
- f"<h1>PR #{pr_number} Convergence Summary</h1>"
931
- f"{subtitle}"
932
- f"{body_main}"
933
- f"{footer}"
934
- )
935
-
936
- return (
937
- f"{title_and_style_html}\n"
938
- f'<div class="container">\n'
939
- f"{body_content}\n"
940
- f"</div>"
941
- )
942
-
943
-
944
- def _parse_pr_arg(pr_arg: str, err_stream: TextIO) -> tuple[str, str, int] | None:
945
- """Parse an 'owner/repo#number' string into its three components.
946
-
947
- Args:
948
- pr_arg: A string in the form 'owner/repo#number'.
949
- err_stream: Stream to write error messages to.
950
-
951
- Returns:
952
- A tuple of (owner, repo, pr_number_int), or None on parse failure.
953
- """
954
- match = re.fullmatch(r"([^/]+)/([^#]+)#(\d+)", pr_arg)
955
- if not match:
956
- err_stream.write(
957
- f"Invalid --pr format: {pr_arg!r}. Expected owner/repo#number.\n"
958
- )
959
- return None
960
- return match.group(1), match.group(2), int(match.group(3))
961
-
962
-
963
- def main(out_stream: TextIO = sys.stdout, err_stream: TextIO = sys.stderr) -> int:
964
- """Parse CLI arguments, load run data, render HTML, write the file, emit the path.
965
-
966
- Args:
967
- out_stream: Stream to write the output file path to on success.
968
- err_stream: Stream to write error messages to.
969
-
970
- Returns:
971
- Exit code (0 on success, 1 on argument error).
972
- """
973
- argument_parser = argparse.ArgumentParser(
974
- description="Render autoconverge convergence summary HTML."
975
- )
976
- argument_parser.add_argument(
977
- "--journal", required=True, help="Path to wf_<runId>.json"
978
- )
979
- argument_parser.add_argument("--out", required=True, help="Output HTML file path")
980
- argument_parser.add_argument("--pr", required=True, help="owner/repo#number")
981
- argument_parser.add_argument("--final-sha", required=True, help="Final commit SHA")
982
- argument_parser.add_argument(
983
- "--rounds", required=True, type=int, help="Total round count"
984
- )
985
- argument_parser.add_argument(
986
- "--summary-file",
987
- default=None,
988
- help="Path to a JSON file with the convergence summary to inject",
989
- )
990
-
991
- parsed_args = argument_parser.parse_args()
992
-
993
- journal_path = Path(parsed_args.journal).resolve()
994
- out_path = Path(parsed_args.out)
995
-
996
- parsed_pr = _parse_pr_arg(parsed_args.pr, err_stream)
997
- if parsed_pr is None:
998
- return 1
999
-
1000
- owner, repo, pr_number = parsed_pr
1001
-
1002
- pr_metadata = PrMetadata(
1003
- owner=owner,
1004
- repo=repo,
1005
- number=pr_number,
1006
- final_sha=parsed_args.final_sha,
1007
- round_count=parsed_args.rounds,
1008
- )
1009
-
1010
- run_data = load_run_data(journal_path)
1011
- if parsed_args.summary_file:
1012
- run_data.convergence_summary = json.loads(
1013
- Path(parsed_args.summary_file).read_text(encoding="utf-8")
1014
- )
1015
- html_content = render_report_html(run_data, pr_metadata, run_data.generated_date)
1016
-
1017
- out_path.parent.mkdir(parents=True, exist_ok=True)
1018
- out_path.write_text(html_content, encoding="utf-8")
1019
- out_stream.write(str(out_path) + "\n")
1020
- return 0
1021
-
1022
-
1023
- if __name__ == "__main__":
1024
- sys.exit(main())