claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -26,9 +26,11 @@ const CONFIG = {
26
26
  copilotMaxPolls: 8,
27
27
  sharedScripts: '$HOME/.claude/skills/pr-converge/scripts',
28
28
  prLoopScripts: '$HOME/.claude/_shared/pr-loop/scripts',
29
- bugteamRubric: '$HOME/.claude/skills/bugteam/reference/audit-contract.md',
29
+ bugteamRubric: '$HOME/.claude/_shared/pr-loop/audit-contract.md',
30
30
  }
31
31
 
32
+ const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '
33
+
32
34
  const HEADLESS_SAFETY_PREAMBLE =
33
35
  'HEADLESS RUN — you run unattended: no human can answer a permission or confirmation prompt, and any such prompt stalls the entire convergence run. The destructive_command_blocker hook matches dangerous patterns (rm -rf, git reset --hard, dd, mkfs, chmod -R, fork bombs) as raw text anywhere in a Bash command, with no quote-awareness — so a destructive string stalls you even when it is only data you never execute. Therefore:\n' +
34
36
  '- Never place a destructive-command literal inside a Bash command — not in echo, not in a heredoc, and not as an argument to python -c, node -e, or awk. To exercise or verify destructive_command_blocker (or any hook) behavior, run the committed test suite, e.g. python -m pytest <test_file>, which passes the command strings as in-language data rather than as a shell command.\n' +
@@ -77,38 +79,32 @@ const convergeAgent = (prompt, options) =>
77
79
  agent(`${HEADLESS_SAFETY_PREAMBLE}${worktreeDirective(activeRepoPath)}${prompt}`, options)
78
80
 
79
81
  /**
80
- * Spawn a fresh git-utility Explore agent for a specific task. 'resolve-head'
81
- * prints the PR HEAD SHA, 'prefetch-main' fetches origin main so the review
82
- * lenses diff against an up-to-date base, and any other task reports whether the
83
- * branch has merge conflicts. The agent never edits code.
84
- * @param {string} task the short task name
85
- * @param {string} head optional HEAD SHA for conflict checks
86
- * @returns {Promise<object|string>} the structured output, or the transcript string when the 'prefetch-main' resume runs schema-less
82
+ * Spawn a fresh git-utility Explore agent for a specific task. The one task,
83
+ * 'preflight-git', bundles the three mechanical git reads into a single agent
84
+ * startup: it prints the PR HEAD SHA, fetches origin main so the review lenses
85
+ * diff against an up-to-date base, and polls GitHub mergeability, returning
86
+ * {sha, conflicting, fetched} in one structured result. The agent never edits
87
+ * code, so it runs on the cheapest model at low effort.
88
+ * @param {string} task the short task name ('preflight-git')
89
+ * @returns {Promise<object>} the structured PREFLIGHT_GIT_SCHEMA output
87
90
  */
88
- function runGitTask(task, head) {
89
- if (task === 'resolve-head') {
90
- return convergeAgent(
91
- `Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
92
- `gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
93
- `Return the full 40-character SHA in the sha field. Do not modify any files.`,
94
- { label: 'git-utility', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore' },
95
- )
96
- }
97
- if (task === 'prefetch-main') {
98
- return convergeAgent(
99
- `Refresh the base ref for ${prCoordinates} so the parallel review lenses can diff against an up-to-date origin/main without each running its own fetch. Run exactly:\n` +
100
- `git fetch origin main\n` +
101
- `Do not edit, commit, push, rebase, or modify any files — fetch only.`,
102
- { label: 'git-utility', phase: 'Converge', agentType: 'Explore' },
103
- )
91
+ function runGitTask(task) {
92
+ if (task !== 'preflight-git') {
93
+ throw new Error(`runGitTask has no handler for task ${task}`)
104
94
  }
105
95
  return convergeAgent(
106
- `Report whether ${prCoordinates} (HEAD ${head}) has merge conflicts with its base branch. Do not edit, commit, push, or rebase — read only.\n\n` +
107
- `GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
96
+ `Run three read-only git preflight steps for ${prCoordinates}. Do not edit, commit, push, rebase, or modify any files — read only.\n\n` +
97
+ `STEP 1 resolve HEAD. Print the current PR HEAD SHA. Run exactly:\n` +
98
+ ` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
99
+ `Return the full 40-character SHA in the sha field.\n\n` +
100
+ `STEP 2 — refresh the base ref so the parallel review lenses can diff against an up-to-date origin/main without each running its own fetch. Run exactly:\n` +
101
+ ` git fetch origin main\n` +
102
+ `Return fetched:true when the fetch completed, fetched:false when it failed.\n\n` +
103
+ `STEP 3 — report whether the PR has merge conflicts with its base branch. GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
108
104
  ` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
109
- `up to 5 times, 5 seconds apart (wait each 5-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above), stopping as soon as mergeable is true or false.\n\n` +
105
+ `up to 5 times, 5 seconds apart (wait each 5-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above), stopping as soon as mergeable is true or false.\n` +
110
106
  `Return conflicting:true when mergeable is false or state is "dirty" (the branch conflicts with the base). Return conflicting:false when mergeable is true, or when mergeable stays null after the full poll budget — mergeability is unknown, so let the bug checks proceed rather than rebase on a guess.`,
111
- { label: 'git-utility', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },
107
+ { label: 'git-utility', phase: 'Converge', schema: PREFLIGHT_GIT_SCHEMA, agentType: 'Explore', model: 'haiku', effort: 'low' },
112
108
  )
113
109
  }
114
110
 
@@ -464,6 +460,27 @@ function runConvergenceCheck(context) {
464
460
  )
465
461
  }
466
462
 
463
+ /**
464
+ * Spawn a single reviewer-availability Explore agent once per CONVERGE round,
465
+ * before either reviewer's own agent is spawned. It runs the shared
466
+ * reviewer_availability.py CLI for both Copilot and Bugbot and reports each
467
+ * one's down state and reason, so resolveReviewerDown can decide whether to
468
+ * spawn the Bugbot lens and the Copilot gate without depending on an
469
+ * externally pre-set input flag.
470
+ * @returns {Promise<object>} REVIEWER_AVAILABILITY_SCHEMA result
471
+ */
472
+ function runReviewerAvailabilityCheck() {
473
+ return convergeAgent(
474
+ `Check whether GitHub Copilot and Cursor Bugbot are available to review ${prCoordinates} this round, before either reviewer's agent is spawned. Do not edit code, commit, or push.\n\n` +
475
+ `Run exactly:\n` +
476
+ `python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer copilot\n` +
477
+ `python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer bugbot\n` +
478
+ `Each run exits 0 when that reviewer is available and non-zero when it is down, and prints one line naming the reason (stdout when available, stderr when down) — capture that line.\n\n` +
479
+ `Return strictly the schema: copilot.down and bugbot.down report whether that reviewer's run exited non-zero, and copilot.reason / bugbot.reason carry its printed line.`,
480
+ { label: 'reviewer-availability', phase: 'Converge', schema: REVIEWER_AVAILABILITY_SCHEMA, agentType: 'Explore' },
481
+ )
482
+ }
483
+
467
484
  const PRE_COMMIT_GATE_STEP =
468
485
  `\n\nFINAL STEP — pre-commit gate check (do NOT commit): before your turn ends, prove your working-tree changes CAN be committed by dry-running the CODE_RULES commit gate that gates git commit (precommit_code_rules_gate). From inside the checkout that holds your changes, resolve its root with git rev-parse --show-toplevel, stage your changes with git add -A, then run exactly:\n` +
469
486
  ` python "${CONFIG.prLoopScripts}/code_rules_gate.py" --repo-root "<that root>" --staged\n` +
@@ -509,11 +526,44 @@ const COPILOT_SCHEMA = {
509
526
  required: ['sha', 'clean', 'down', 'findings'],
510
527
  }
511
528
 
512
- const HEAD_SCHEMA = {
529
+ const REVIEWER_AVAILABILITY_SCHEMA = {
513
530
  type: 'object',
514
531
  additionalProperties: false,
515
- properties: { sha: { type: 'string' } },
516
- required: ['sha'],
532
+ properties: {
533
+ copilot: {
534
+ type: 'object',
535
+ additionalProperties: false,
536
+ properties: {
537
+ down: { type: 'boolean', description: 'true when reviewer_availability.py --reviewer copilot exited non-zero (opted out or out of premium-request quota)' },
538
+ reason: { type: 'string', description: 'the one-line reason reviewer_availability.py printed for Copilot' },
539
+ },
540
+ required: ['down', 'reason'],
541
+ },
542
+ bugbot: {
543
+ type: 'object',
544
+ additionalProperties: false,
545
+ properties: {
546
+ down: { type: 'boolean', description: 'true when reviewer_availability.py --reviewer bugbot exited non-zero (opted out via CLAUDE_REVIEWS_DISABLED)' },
547
+ reason: { type: 'string', description: 'the one-line reason reviewer_availability.py printed for Bugbot' },
548
+ },
549
+ required: ['down', 'reason'],
550
+ },
551
+ },
552
+ required: ['copilot', 'bugbot'],
553
+ }
554
+
555
+ const PREFLIGHT_GIT_SCHEMA = {
556
+ type: 'object',
557
+ additionalProperties: false,
558
+ properties: {
559
+ sha: { type: 'string', description: 'the full 40-character PR HEAD SHA' },
560
+ conflicting: {
561
+ type: 'boolean',
562
+ description: 'true only when GitHub reports the PR branch conflicts with its base (mergeable:false or mergeable_state:dirty); false when it merges cleanly or mergeability could not be computed',
563
+ },
564
+ fetched: { type: 'boolean', description: 'true when git fetch origin main completed successfully' },
565
+ },
566
+ required: ['sha', 'conflicting', 'fetched'],
517
567
  }
518
568
 
519
569
  const FIX_SCHEMA = {
@@ -553,18 +603,6 @@ const REPAIR_EDIT_SCHEMA = {
553
603
  required: ['edited', 'rebased', 'resolvedWithoutCommit', 'summary'],
554
604
  }
555
605
 
556
- const MERGE_CONFLICT_SCHEMA = {
557
- type: 'object',
558
- additionalProperties: false,
559
- properties: {
560
- conflicting: {
561
- type: 'boolean',
562
- description: 'true only when GitHub reports the PR branch conflicts with its base (mergeable:false or mergeable_state:dirty); false when it merges cleanly or mergeability could not be computed',
563
- },
564
- },
565
- required: ['conflicting'],
566
- }
567
-
568
606
  const CONFLICT_EDIT_SCHEMA = {
569
607
  type: 'object',
570
608
  additionalProperties: false,
@@ -749,19 +787,27 @@ function isMoreSevere(candidateSeverity, currentSeverity) {
749
787
  }
750
788
 
751
789
  /**
752
- * Decide whether the convergence check should bypass the Bugbot gate this round,
753
- * recomputed from the current Bugbot lens result rather than latched across the
754
- * run, so a recovered Bugbot re-arms the gate. A dead lens agent (null/undefined
755
- * result) produces no Bugbot verdict on this HEAD, so it is treated as down to
756
- * keep the convergence gate from demanding a verdict that cannot exist.
757
- * @param {object|null|undefined} bugbotLens the current round's Bugbot lens result
758
- * @param {boolean} bugbotDisabled true when Bugbot is opted out for the whole run
759
- * @returns {boolean} true when the Bugbot gate is bypassed for the current HEAD
790
+ * Decide whether a reviewer is skipped this round — the one shared gate both
791
+ * Bugbot and Copilot consult before either reviewer's agent is spawned. The
792
+ * run's own disable flag always wins, so a deferred PR seeded with
793
+ * copilotDisabled or bugbotDisabled skips the reviewer without a probe.
794
+ * Otherwise the decision comes from the carried entry's down field, read from
795
+ * the round-start availability probe for a pre-spawn decision. A missing entry
796
+ * (a dead probe agent, or no result yet) reads as available rather than down,
797
+ * so an outage in the probe itself never wedges convergence the reviewer's
798
+ * own runtime detection still runs and can report down on its own. This
799
+ * fail-open null handling suits a pre-spawn decision; a caller computing a
800
+ * post-round verdict from a reviewer's own lens/gate result — where a dead
801
+ * agent has no verdict to report, not an available one — guards that null
802
+ * case itself before calling this function.
803
+ * @param {{down: boolean}|null|undefined} reviewerAvailabilityEntry the probe's per-reviewer entry, or a reviewer's own result, carrying a down field
804
+ * @param {boolean} isReviewerDisabledByInput true when the run input opts this reviewer out for the whole run
805
+ * @returns {boolean} true when the reviewer is treated as down this round
760
806
  */
761
- function resolveBugbotDown(bugbotLens, bugbotDisabled) {
762
- if (bugbotDisabled) return true
763
- if (bugbotLens == null) return true
764
- return bugbotLens.down === true
807
+ function resolveReviewerDown(reviewerAvailabilityEntry, isReviewerDisabledByInput) {
808
+ if (isReviewerDisabledByInput) return true
809
+ if (reviewerAvailabilityEntry == null) return false
810
+ return reviewerAvailabilityEntry.down === true
765
811
  }
766
812
 
767
813
  /**
@@ -946,10 +992,10 @@ function commitNeedsCodeRecovery(commitResult) {
946
992
 
947
993
  /**
948
994
  * Decide whether a resolved HEAD SHA is safe to spawn lenses against. A dead
949
- * resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
995
+ * preflight-git agent or a malformed result yields a falsy SHA; spawning lenses
950
996
  * against it interpolates the literal string 'HEAD undefined' into their prompts
951
997
  * and produces a spurious clean verdict on a non-existent commit.
952
- * @param {string|null|undefined} resolvedHead the SHA from the git-utility 'resolve-head' task
998
+ * @param {string|null|undefined} resolvedHead the SHA from the git-utility 'preflight-git' task
953
999
  * @returns {boolean} true only when the SHA is a non-empty string
954
1000
  */
955
1001
  function isResolvedHeadUsable(resolvedHead) {
@@ -957,12 +1003,12 @@ function isResolvedHeadUsable(resolvedHead) {
957
1003
  }
958
1004
 
959
1005
  /**
960
- * Decide whether the pre-flight merge-conflict check found the PR branch in
961
- * conflict with its base. A dead check agent (null/undefined result) reports
1006
+ * Decide whether the pre-flight mergeability probe found the PR branch in
1007
+ * conflict with its base. A dead preflight agent (null/undefined result) reports
962
1008
  * not-conflicting so the run proceeds straight to the bug checks rather than
963
- * force-pushing a rebase on a verdict that does not exist — a transient check
1009
+ * force-pushing a rebase on a verdict that does not exist — a transient probe
964
1010
  * failure must never trigger a destructive rebase.
965
- * @param {object|null|undefined} mergeState the git-utility 'check-merge-conflicts' task result
1011
+ * @param {object|null|undefined} mergeState the git-utility 'preflight-git' task result carrying the conflicting field
966
1012
  * @returns {boolean} true only when the check reported conflicting:true
967
1013
  */
968
1014
  function isMergeConflicting(mergeState) {
@@ -1015,9 +1061,10 @@ function classifyCopilotOutcome(copilot) {
1015
1061
 
1016
1062
  /**
1017
1063
  * Decide whether the Copilot review gate is bypassed for this COPILOT pass from
1018
- * the gate outcome, mirroring resolveBugbotDown so the flag is recomputed every
1019
- * pass rather than left sticky. Only a 'down' outcome (Copilot out of quota or
1020
- * unreachable after the poll cap) bypasses the convergence Copilot gate; an
1064
+ * the gate outcome, mirroring resolveReviewerDown's post-round bugbotDown
1065
+ * recompute so the flag is recomputed every pass rather than left sticky. Only
1066
+ * a 'down' outcome (Copilot out of quota or unreachable after the poll cap)
1067
+ * bypasses the convergence Copilot gate; an
1021
1068
  * 'approved', 'fix', or 'retry' outcome means Copilot answered this pass, so the
1022
1069
  * gate must be evaluated against its review and is never bypassed. Recomputing
1023
1070
  * from the current outcome is what lets a recovered Copilot — one that returns
@@ -1105,9 +1152,6 @@ const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNum
1105
1152
  * @returns {Promise<object>} LENS_SCHEMA result
1106
1153
  */
1107
1154
  function runBugbotLens(head) {
1108
- if (input.bugbotDisabled) {
1109
- return Promise.resolve({ sha: head, clean: true, down: true, findings: [] })
1110
- }
1111
1155
  return convergeAgent(
1112
1156
  `You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
1113
1157
  `Goal: return Bugbot's verdict on HEAD ${head}. Do not edit code, commit, or push. You may post the literal trigger comment described below.\n\n` +
@@ -1120,7 +1164,7 @@ function runBugbotLens(head) {
1120
1164
  ` Only count entries whose commit_id starts with ${head}.\n` +
1121
1165
  ` - If findings exist on HEAD -> return them (each with its inline comment id in replyToCommentId when present, else null).\n` +
1122
1166
  ` - If a clean review exists on HEAD -> return clean.\n` +
1123
- `4. No review yet on HEAD: check_bugbot_ci.py --check-active. If active (exit 0), poll: repeat check_bugbot_ci.py --check-clean / --check-active every 60 seconds (wait each 60-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above) for up to 25 iterations, then re-fetch the review. If not active (exit 1), post the literal comment "bugbot run" (no @mention, no other text) via python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --body "bugbot run", wait 8 seconds inside this turn with the Monitor tool (per the WAITS AND POLLS rule above), then poll as above.\n` +
1167
+ `4. No review yet on HEAD: check_bugbot_ci.py --check-active. If active (exit 0), poll: repeat check_bugbot_ci.py --check-clean / --check-active every 60 seconds (wait each 60-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above) for up to 25 iterations, then re-fetch the review. If not active (exit 1), post the literal comment "bugbot run" (no @mention, no other text) via ${REVIEWER_GATE_SENTINEL}python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --body "bugbot run", wait 8 seconds inside this turn with the Monitor tool (per the WAITS AND POLLS rule above), then poll as above.\n` +
1124
1168
  `5. If after the full poll budget Bugbot has neither a check run nor a review on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} (treat as down).\n\n` +
1125
1169
  `Scope is the whole PR; you are only reading Bugbot's own output here. For each finding set category: 'code-standard' when it is a pure CODE_RULES/style violation (naming, comments, type hints, magic values, structure) with no behavioral impact; 'bug' otherwise. Return strictly the schema.`,
1126
1170
  { label: 'lens:bugbot', phase: 'Converge', schema: LENS_SCHEMA },
@@ -1314,7 +1358,7 @@ function runCopilotGate(head) {
1314
1358
  `You are the Copilot gate for ${prCoordinates}, HEAD ${head}. Do not edit code, commit, or push.\n\n` +
1315
1359
  `Copilot can run out of usage. When the newest Copilot review on HEAD carries an out-of-usage notice — a body stating Copilot was unable to review because the user who requested the review has reached their quota limit, or any equivalent quota / premium-request / usage-limit exhaustion message rather than an actual code review — Copilot is down for this run: return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} and stop. Do NOT re-request a review, do NOT keep polling, and do NOT treat the notice as a finding.\n\n` +
1316
1360
  `1. Read any existing Copilot review on HEAD first: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}. This lists every Copilot review across all commits newest-first; only count entries whose commit_id starts with ${head}. If the newest such HEAD-scoped Copilot review is the out-of-usage notice above -> return the down result and stop. A notice on any earlier commit is NOT down: ignore it and continue. With no Copilot review on HEAD, skip a duplicate request: python "${CONFIG.sharedScripts}/check_pending_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --user copilot. Exit 0 means a request is already pending; otherwise request one:\n` +
1317
- ` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
1361
+ ` ${REVIEWER_GATE_SENTINEL}gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
1318
1362
  `2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (wait each 360-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above; if the attempt budget is spent with no review on HEAD, return down: true). Each attempt: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} for the top-level review state, plus gh api "repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/comments" --paginate --slurp for inline comment ids (Copilot's login contains "copilot", case-insensitive). Only count entries whose commit_id starts with ${head}.\n` +
1319
1363
  ` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
1320
1364
  ` - Copilot review present on HEAD whose state is APPROVED, or COMMENTED with no inline findings -> a clean pass: return {sha:${'`'}${head}${'`'}, clean:true, down:false, findings:[]}.\n` +
@@ -1377,20 +1421,22 @@ async function repairConvergence(head, failures) {
1377
1421
 
1378
1422
  /**
1379
1423
  * Pre-flight conflict resolution: when the PR branch conflicts with its base,
1380
- * rebase it clean before the bug checks run — check (Explore probes mergeability)
1381
- * -> edit (clean-coder rebases and resolves, no push) -> verify (code-verifier
1382
- * binds a verdict to the rebased tree) -> commit (clean-coder force-with-lease
1383
- * pushes). Returns the post-rebase HEAD so the first converge round runs its
1384
- * lenses on the conflict-free diff. A non-conflicting PR, a rebase the edit step
1385
- * declined, or a failed verdict returns the unchanged HEAD so the run proceeds to
1386
- * the bug checks unchanged. A mid-run conflict (origin/main advancing later) is
1387
- * still caught by the FINALIZE convergence repair, which also rebases.
1424
+ * rebase it clean before the bug checks run — edit (clean-coder rebases and
1425
+ * resolves, no push) -> verify (code-verifier binds a verdict to the rebased
1426
+ * tree) -> commit (clean-coder force-with-lease pushes). The conflict decision
1427
+ * comes from the merged 'preflight-git' probe the caller already ran, so this
1428
+ * function spawns no mergeability agent of its own. Returns the post-rebase
1429
+ * HEAD so the first converge round runs its lenses on the conflict-free diff.
1430
+ * A non-conflicting PR, a rebase the edit step declined, or a failed verdict
1431
+ * returns the unchanged HEAD so the run proceeds to the bug checks unchanged.
1432
+ * A mid-run conflict (origin/main advancing later) is still caught by the
1433
+ * FINALIZE convergence repair, which also rebases.
1388
1434
  * @param {string} head PR HEAD SHA before any rebase
1435
+ * @param {boolean} isConflicting the isMergeConflicting decision over the preflight-git result
1389
1436
  * @returns {Promise<string>} the HEAD SHA after a successful rebase push, or the unchanged head
1390
1437
  */
1391
- async function resolveMergeConflicts(head) {
1392
- const mergeState = await runGitTask('check-merge-conflicts', head)
1393
- if (!isMergeConflicting(mergeState)) return head
1438
+ async function resolveMergeConflicts(head, isConflicting) {
1439
+ if (!isConflicting) return head
1394
1440
  log(`Pre-flight: ${prCoordinates} conflicts with origin/main — rebasing clean before the bug checks`)
1395
1441
  const editResult = await runCodeEditorTask('conflict-edit', { head })
1396
1442
  if (editResult?.rebased !== true) return head
@@ -1485,9 +1531,10 @@ function parseDeferredPr(prUrl) {
1485
1531
  * @param {Array<object>} findings deduped code-standard-only findings
1486
1532
  * @param {string} sourceLabel short description of where the findings came from
1487
1533
  * @param {boolean} hasHardeningPrAlreadyOpened true when an earlier round already opened the environment-hardening PR for this run, so the verify and commit steps are skipped and no second PR opens while the edit retries the issue filing
1488
- * @returns {Promise<object>} `{ followUpIssueFiled, issueUrl, hardeningPrOpened, deferredPr }` followUpIssueFiled true when the standards-edit step returned a non-empty issue URL, issueUrl that filed URL (empty string when the filing failed) so a later reuse-path round can reference it when resolving its own threads, hardeningPrOpened true when the hardening-commit step returned a non-empty hardeningPrUrl (a PR opened) so the run-once latch holds even when that URL does not parse into coordinates, and false when the commit step returned an empty URL (no PR opened) so a later round retries the open, and deferredPr the opened PR's `{owner, repo, prNumber}` (null when no PR was opened or the committed URL does not parse) so the self-closing orchestrator can converge it in turn
1534
+ * @param {{copilotDisabled: boolean, bugbotDisabled: boolean}} deferredReviewerFlags this run's latest resolved reviewer-down state, carried onto the deferred PR so a later generation converging it seeds the known-unavailable state instead of re-learning it
1535
+ * @returns {Promise<object>} `{ followUpIssueFiled, issueUrl, hardeningPrOpened, deferredPr }` — followUpIssueFiled true when the standards-edit step returned a non-empty issue URL, issueUrl that filed URL (empty string when the filing failed) so a later reuse-path round can reference it when resolving its own threads, hardeningPrOpened true when the hardening-commit step returned a non-empty hardeningPrUrl (a PR opened) so the run-once latch holds even when that URL does not parse into coordinates, and false when the commit step returned an empty URL (no PR opened) so a later round retries the open, and deferredPr the opened PR's `{owner, repo, prNumber, copilotDisabled, bugbotDisabled}` (null when no PR was opened or the committed URL does not parse) so the self-closing orchestrator can converge it in turn
1489
1536
  */
1490
- async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened) {
1537
+ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened, deferredReviewerFlags) {
1491
1538
  const editResult = await runCodeEditorTask('standards-edit', { head, findings, sourceLabel })
1492
1539
  const followUpIssueFiled = typeof editResult?.issueUrl === 'string' && editResult.issueUrl.length > 0
1493
1540
  const followUpIssueUrl = followUpIssueFiled ? editResult.issueUrl : ''
@@ -1506,7 +1553,11 @@ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningP
1506
1553
  const commitResult = await runCodeEditorTask('hardening-commit', {
1507
1554
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
1508
1555
  })
1509
- const deferredPr = parseDeferredPr(commitResult?.hardeningPrUrl)
1556
+ const parsedDeferredPr = parseDeferredPr(commitResult?.hardeningPrUrl)
1557
+ const deferredPr =
1558
+ parsedDeferredPr === null
1559
+ ? null
1560
+ : { ...parsedDeferredPr, copilotDisabled: deferredReviewerFlags.copilotDisabled, bugbotDisabled: deferredReviewerFlags.bugbotDisabled }
1510
1561
  const hardeningPrOpened = typeof commitResult?.hardeningPrUrl === 'string' && commitResult.hardeningPrUrl.length > 0
1511
1562
  return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened, deferredPr }
1512
1563
  }
@@ -1561,15 +1612,16 @@ async function resolveStandardsThreadsForBatch(head, findings, sourceLabel) {
1561
1612
  * @param {string} head PR HEAD SHA the findings were raised against
1562
1613
  * @param {Array<object>} findings deduped code-standard-only findings
1563
1614
  * @param {string} sourceLabel short description of where the findings came from
1564
- * @returns {Promise<object>} `{ hardeningPrOpened, deferredPr }` hardeningPrOpened true when a hardening PR was opened for this run, and deferredPr the opened PR's `{owner, repo, prNumber}` when this call opened it (null otherwise) so the self-closing orchestrator can converge it in turn
1615
+ * @param {{copilotDisabled: boolean, bugbotDisabled: boolean}} deferredReviewerFlags this run's latest resolved reviewer-down state, carried onto the deferred PR so a later generation converging it seeds the known-unavailable state instead of re-learning it
1616
+ * @returns {Promise<object>} `{ hardeningPrOpened, deferredPr }` — hardeningPrOpened true when a hardening PR was opened for this run, and deferredPr the opened PR's `{owner, repo, prNumber, copilotDisabled, bugbotDisabled}` when this call opened it (null otherwise) so the self-closing orchestrator can converge it in turn
1565
1617
  */
1566
- async function openStandardsFollowUpOnce(head, findings, sourceLabel) {
1618
+ async function openStandardsFollowUpOnce(head, findings, sourceLabel, deferredReviewerFlags) {
1567
1619
  if (!shouldOpenStandardsFollowUp(hasStandardsFollowUpFiled)) {
1568
1620
  log(`Standards deferral (${sourceLabel}): reusing the follow-up fix issue already filed for this run rather than filing a duplicate; environment-hardening PR ${wasStandardsHardeningPrOpened ? 'was opened for this run' : 'was not opened for this run'}`)
1569
1621
  await resolveStandardsThreadsForBatch(head, findings, sourceLabel)
1570
1622
  return { hardeningPrOpened: wasStandardsHardeningPrOpened, deferredPr: null }
1571
1623
  }
1572
- const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened)
1624
+ const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened, deferredReviewerFlags)
1573
1625
  hasStandardsFollowUpFiled = standardsOutcome?.followUpIssueFiled === true
1574
1626
  if (hasStandardsFollowUpFiled) {
1575
1627
  standardsFollowUpIssueUrl = standardsOutcome.issueUrl
@@ -1591,26 +1643,27 @@ let hasStandardsFollowUpFiled = false
1591
1643
  let wasStandardsHardeningPrOpened = false
1592
1644
  let standardsFollowUpIssueUrl = ''
1593
1645
  let reuseNote = null
1646
+ let reviewerAvailability = null
1594
1647
  const deferredPrs = []
1595
1648
 
1596
- const preflightHead = await runGitTask('resolve-head')
1597
- if (isResolvedHeadUsable(preflightHead?.sha)) {
1598
- await resolveMergeConflicts(preflightHead.sha)
1649
+ const preflight = await runGitTask('preflight-git')
1650
+ if (isResolvedHeadUsable(preflight?.sha)) {
1651
+ head = await resolveMergeConflicts(preflight.sha, isMergeConflicting(preflight))
1599
1652
  }
1600
1653
 
1601
1654
  log('Reuse pass: scanning the full diff for certain, behaviorally identical, autonomously implementable reuse improvements before convergence')
1602
- const reuseHeadResult = await runGitTask('resolve-head')
1603
- const reuseHead = reuseHeadResult?.sha
1604
- if (isResolvedHeadUsable(reuseHead)) {
1605
- await runGitTask('prefetch-main')
1606
- const reuse = await runReuseAuditPass(reuseHead)
1655
+ if (isResolvedHeadUsable(head)) {
1656
+ const reuse = await runReuseAuditPass(head)
1607
1657
  const reuseFindings = reuse?.findings || []
1608
1658
  if (reuseFindings.length > 0) {
1609
1659
  log(`Reuse pass: ${reuseFindings.length} qualifying reuse improvement(s) — applying before convergence`)
1610
- const reuseFix = await applyFixes(reuseHead, reuseFindings, 'reuse-pass')
1660
+ const reuseFix = await applyFixes(head, reuseFindings, 'reuse-pass')
1611
1661
  reuseNote = reuseFix?.pushed === true
1612
1662
  ? `${reuseFindings.length} reuse improvement(s) applied before convergence (${reuseFix.newSha?.slice(0, SHA_COMPARISON_PREFIX_LENGTH)})`
1613
1663
  : `${reuseFindings.length} reuse improvement(s) identified before convergence but not landed — the code-review lens re-surfaces any that remain`
1664
+ if (reuseFix?.pushed === true) {
1665
+ head = isResolvedHeadUsable(reuseFix.newSha) ? reuseFix.newSha : null
1666
+ }
1614
1667
  } else {
1615
1668
  log('Reuse pass: no reuse case cleared all three criteria — proceeding to convergence')
1616
1669
  }
@@ -1622,29 +1675,33 @@ while (iterations < CONFIG.maxIterations) {
1622
1675
  iterations += 1
1623
1676
  if (phase === 'CONVERGE') {
1624
1677
  rounds += 1
1625
- const headResult = await runGitTask('resolve-head')
1626
- head = headResult?.sha
1627
1678
  if (!isResolvedHeadUsable(head)) {
1628
- log(`Round ${rounds}: resolve-head agent returned no SHA — retrying without spawning lenses`)
1679
+ const refreshedPreflight = await runGitTask('preflight-git')
1680
+ head = isResolvedHeadUsable(refreshedPreflight?.sha) ? refreshedPreflight.sha : null
1681
+ }
1682
+ if (!isResolvedHeadUsable(head)) {
1683
+ log(`Round ${rounds}: preflight-git agent returned no SHA — retrying without spawning lenses`)
1629
1684
  continue
1630
1685
  }
1631
- await runGitTask('prefetch-main')
1686
+ reviewerAvailability = await runReviewerAvailabilityCheck()
1687
+ const isBugbotDownPreSpawn = resolveReviewerDown(reviewerAvailability?.bugbot, input.bugbotDisabled || false)
1632
1688
  log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
1633
1689
  const lenses = await parallel([
1634
- () => runBugbotLens(head),
1690
+ () => (isBugbotDownPreSpawn ? Promise.resolve({ sha: head, clean: true, down: true, findings: [] }) : runBugbotLens(head)),
1635
1691
  () => runCodeReviewLens(head),
1636
1692
  () => runAuditLens(head),
1637
1693
  ])
1638
- bugbotDown = resolveBugbotDown(lenses[0], input.bugbotDisabled || false)
1694
+ bugbotDown = lenses[0] == null ? true : resolveReviewerDown(lenses[0], input.bugbotDisabled || false)
1639
1695
  const roundOutcome = resolveRoundOutcome(lenses)
1640
1696
  if (roundOutcome.allLensesDead) {
1641
1697
  log(`Round ${rounds}: every lens agent died — retrying without posting a clean artifact`)
1698
+ head = null
1642
1699
  continue
1643
1700
  }
1644
1701
  const findings = roundOutcome.findings
1645
1702
  if (isStandardsOnlyRound(findings)) {
1646
1703
  log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
1647
- const standardsOutcome = await openStandardsFollowUpOnce(head, findings, 'converge-round')
1704
+ const standardsOutcome = await openStandardsFollowUpOnce(head, findings, 'converge-round', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
1648
1705
  standardsNote = standardsDeferralNote(findings.length, standardsOutcome.hardeningPrOpened)
1649
1706
  if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
1650
1707
  const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
@@ -1666,10 +1723,12 @@ while (iterations < CONFIG.maxIterations) {
1666
1723
  : `fix lens landed no push for ${findings.length} finding(s) on HEAD ${head}`
1667
1724
  break
1668
1725
  }
1726
+ head = null
1669
1727
  continue
1670
1728
  }
1671
1729
  if (!roundOutcome.roundClean) {
1672
1730
  log(`Round ${rounds}: a lens reported not-clean with no findings on ${head?.slice(0, 7)} — re-converging without a clean artifact`)
1731
+ head = null
1673
1732
  continue
1674
1733
  }
1675
1734
  log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
@@ -1683,10 +1742,10 @@ while (iterations < CONFIG.maxIterations) {
1683
1742
  }
1684
1743
 
1685
1744
  if (phase === 'COPILOT') {
1686
- if (input.copilotDisabled) {
1745
+ if (resolveReviewerDown(reviewerAvailability?.copilot, input.copilotDisabled || false)) {
1687
1746
  copilotDown = true
1688
- copilotNote = 'Copilot was skipped by the quota pre-check (out of premium-request quota, unreachable, or unconfigured) — the Copilot gate was bypassed and the PR was marked ready without a Copilot review'
1689
- log('Copilot gate: the quota pre-check reported Copilot unavailable — skipping the Copilot gate with no agent spawned and proceeding to mark-ready with the gate bypassed.')
1747
+ copilotNote = 'Copilot was unavailable or out of premium-request quota this round — the Copilot gate was bypassed with no agent spawned and the PR was marked ready without a Copilot review'
1748
+ log('Copilot gate: the shared reviewer-availability probe (or the run input) reported Copilot unavailable — skipping the Copilot gate with no agent spawned and proceeding to mark-ready with the gate bypassed.')
1690
1749
  phase = 'FINALIZE'
1691
1750
  continue
1692
1751
  }
@@ -1708,7 +1767,7 @@ while (iterations < CONFIG.maxIterations) {
1708
1767
  if (copilotOutcome.kind === 'fix') {
1709
1768
  if (isStandardsOnlyRound(copilotOutcome.findings)) {
1710
1769
  log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
1711
- const standardsOutcome = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot')
1770
+ const standardsOutcome = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
1712
1771
  standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome.hardeningPrOpened)
1713
1772
  if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
1714
1773
  copilotDown = false
@@ -1726,6 +1785,7 @@ while (iterations < CONFIG.maxIterations) {
1726
1785
  : `copilot fix lens landed no push for ${copilotOutcome.findings.length} finding(s) on HEAD ${head}`
1727
1786
  break
1728
1787
  }
1788
+ head = null
1729
1789
  phase = 'CONVERGE'
1730
1790
  continue
1731
1791
  }
@@ -1753,6 +1813,7 @@ while (iterations < CONFIG.maxIterations) {
1753
1813
  }
1754
1814
  log(`Convergence check failed: ${convergenceOutcome.failures.join('; ')} — repairing then re-converging`)
1755
1815
  await repairConvergence(head, convergenceOutcome.failures)
1816
+ head = null
1756
1817
  phase = 'CONVERGE'
1757
1818
  continue
1758
1819
  }
@@ -17,8 +17,7 @@ from autoconverge_report_constants.render_report_constants import (
17
17
  DEFAULT_FINDING_CATEGORY,
18
18
  DEFAULT_FINDING_SEVERITY,
19
19
  DEFAULT_ISSUE_ICON,
20
- HTML_DOCTYPE,
21
- HTML_HEAD_TEMPLATE,
20
+ HTML_TITLE_AND_STYLE_TEMPLATE,
22
21
  HTML_STYLE_BLOCK,
23
22
  ISO_DATE_LENGTH,
24
23
  ISSUE_CLASS_FIELD_AFTER_LINES,
@@ -890,7 +889,9 @@ def render_report_html(
890
889
  generated_date: ISO date string derived from the journal timestamp.
891
890
 
892
891
  Returns:
893
- A complete HTML document string.
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.
894
895
  """
895
896
  short_sha_length = SHORT_SHA_LENGTH
896
897
  pr_number = pr_metadata.number
@@ -899,7 +900,7 @@ def render_report_html(
899
900
  final_sha_short = pr_metadata.final_sha[:short_sha_length]
900
901
  round_count = pr_metadata.round_count
901
902
 
902
- head_html = HTML_HEAD_TEMPLATE.format(
903
+ title_and_style_html = HTML_TITLE_AND_STYLE_TEMPLATE.format(
903
904
  pr_number=pr_number,
904
905
  style_block=HTML_STYLE_BLOCK,
905
906
  )
@@ -933,15 +934,10 @@ def render_report_html(
933
934
  )
934
935
 
935
936
  return (
936
- f"{HTML_DOCTYPE}\n"
937
- f"<html lang='en'>\n"
938
- f"{head_html}\n"
939
- f"<body>\n"
937
+ f"{title_and_style_html}\n"
940
938
  f'<div class="container">\n'
941
939
  f"{body_content}\n"
942
- f"</div>\n"
943
- f"</body>\n"
944
- f"</html>"
940
+ f"</div>"
945
941
  )
946
942
 
947
943
 
@@ -10,7 +10,7 @@ Each loop: a `code-quality-agent` (fresh context, all A–P audit categories) pr
10
10
 
11
11
  | File | Purpose |
12
12
  |---|---|
13
- | `SKILL.md` | Hub — pre-flight call, refusals, audit-posting protocol, progress checklist, and situation-to-reference table. Read this first. |
13
+ | `SKILL.md` | Hub — pre-flight call, refusals, the audit-posting step (applies the `post-audit-findings` skill), progress checklist, and situation-to-reference table. Read this first. |
14
14
  | `PROMPTS.md` | Spawn XML, A–P category bindings, outcome XML schemas. |
15
15
  | `CONSTRAINTS.md` | Invariants — what the loop must never violate. |
16
16
  | `EXAMPLES.md` | Exit scenarios: converged, cap-reached, stuck, refusal, mixed-outcome. |
@@ -278,7 +278,7 @@ cd into `<worktree_path>` before any git or file operation.
278
278
  [ ] git add + commit
279
279
  [ ] git push
280
280
  [ ] Per finding: atomically post the unified-template reply, then call resolve_thread (no yield between them)
281
- [ ] Publish fix summary via /doc-gist, capture URL
281
+ [ ] Publish fix summary via the Artifact tool, capture URL
282
282
  [ ] Append fix summary URL to parent review via add_reply_to_pull_request_comment
283
283
  [ ] Write fix outcomes XML
284
284
  </self_audit_checklist>
@@ -343,14 +343,15 @@ cd into `<worktree_path>` before any git or file operation.
343
343
  `loop_comment_index` entry alongside `finding_comment_id`, see
344
344
  [reference/obstacles/fix-resolve-thread.md](reference/obstacles/fix-resolve-thread.md)).
345
345
 
346
- 9. Publish the fix summary gist via `/doc-gist`. Pass the fix report
347
- (what was fixed, what was skipped, what was left unaddressed) as the
348
- gist body. Capture the returned gist URL.
346
+ 9. Write the fix summary (what was fixed, what was skipped, what was left
347
+ unaddressed) to a self-contained HTML file, load the `artifact-design`
348
+ skill, then publish it with the `Artifact` tool. Capture the returned
349
+ URL.
349
350
 
350
- 10. Append the fix summary gist URL (from step 9) to the parent review
351
+ 10. Append the fix summary URL (from step 9) to the parent review
351
352
  via `add_reply_to_pull_request_comment(commentId=<id>, body=...,
352
353
  owner=<O>, repo=<R>, pullNumber=<N>)`. The body carries the
353
- gist URL plus a one-line summary of fixes applied this loop.
354
+ artifact URL plus a one-line summary of fixes applied this loop.
354
355
 
355
356
  11. Write `.bugteam-pr<N>-loop<L>.fix-outcomes.xml` inside
356
357
  `<worktree_path>` (schema below) and return its path.