claude-dev-env 1.79.0 → 1.81.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 (76) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +3 -1
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
  3. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
  10. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
  12. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  13. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  14. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
  15. package/audit-rubrics/CLAUDE.md +3 -2
  16. package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
  17. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
  18. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
  19. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
  20. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
  21. package/audit-rubrics/prompts/CLAUDE.md +2 -1
  22. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
  23. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
  24. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
  25. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
  26. package/docs/CODE_RULES.md +2 -2
  27. package/hooks/blocking/CLAUDE.md +2 -0
  28. package/hooks/blocking/code_rules_annotations_length.py +59 -11
  29. package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
  30. package/hooks/blocking/code_rules_command_dispatch.py +140 -0
  31. package/hooks/blocking/code_rules_docstrings.py +93 -0
  32. package/hooks/blocking/code_rules_enforcer.py +58 -4
  33. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  34. package/hooks/blocking/code_rules_js_conventions.py +246 -0
  35. package/hooks/blocking/code_rules_naming_collection.py +5 -0
  36. package/hooks/blocking/code_rules_test_assertions.py +3 -0
  37. package/hooks/blocking/code_rules_unused_imports.py +7 -66
  38. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
  39. package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
  40. package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
  41. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
  42. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
  43. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
  45. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  46. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
  47. package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
  48. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  49. package/hooks/hooks_constants/CLAUDE.md +2 -0
  50. package/hooks/hooks_constants/blocking_check_limits.py +10 -0
  51. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  52. package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
  53. package/hooks/hooks_constants/js_conventions_constants.py +54 -0
  54. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  55. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  56. package/package.json +1 -1
  57. package/rules/docstring-prose-matches-implementation.md +3 -1
  58. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
  59. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
  60. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
  61. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
  62. package/skills/autoconverge/SKILL.md +28 -7
  63. package/skills/autoconverge/reference/convergence.md +3 -3
  64. package/skills/autoconverge/reference/stop-conditions.md +1 -1
  65. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  66. package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
  67. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
  68. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  69. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  70. package/skills/autoconverge/workflow/converge.mjs +235 -247
  71. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  72. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  73. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  74. package/skills/pr-converge/SKILL.md +28 -2
  75. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  76. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -16,14 +16,14 @@ export const meta = {
16
16
  phases: [
17
17
  { title: 'Reuse', detail: 'Before convergence, one reuse lens scans the full diff for reuse improvements that are certain, behaviorally identical, and autonomously implementable, and applies the qualifying ones in one commit' },
18
18
  { title: 'Converge', detail: 'Bugbot + code-review + bug-audit in parallel each round; one clean-coder applies all fixes; loop until all three are clean on a stable HEAD' },
19
- { title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge; when Copilot is down or out of quota, log a notice and mark the PR ready with the gate bypassed' },
19
+ { title: 'Copilot gate', detail: 'When the quota pre-check reports Copilot out of premium-request quota or unavailable, skip the gate with no agent spawned; otherwise request a Copilot review and poll up to the configured cap, route findings back into Converge, and when Copilot is down or out of quota log a notice and mark the PR ready with the gate bypassed' },
20
20
  { title: 'Finalize', detail: 'Run check_convergence.py; mark draft=false on a full pass' },
21
21
  ],
22
22
  }
23
23
 
24
24
  const CONFIG = {
25
25
  maxIterations: 20,
26
- copilotMaxPolls: 3,
26
+ copilotMaxPolls: 8,
27
27
  sharedScripts: '$HOME/.claude/skills/pr-converge/scripts',
28
28
  prLoopScripts: '$HOME/.claude/_shared/pr-loop/scripts',
29
29
  bugteamRubric: '$HOME/.claude/skills/bugteam/reference/audit-contract.md',
@@ -69,43 +69,25 @@ const worktreeDirective = (repoPath) =>
69
69
  * @param {object} options the agent() options (label, phase, schema, agentType, model)
70
70
  * @returns {Promise<*>} the agent() result
71
71
  */
72
- const convergeAgent = (prompt, options) => {
73
- const isResume = typeof options?.resume === 'string' && options.resume.length > 0
74
- const fullPrompt = isResume
75
- ? prompt
76
- : `${HEADLESS_SAFETY_PREAMBLE}${worktreeDirective(activeRepoPath)}${prompt}`
77
- return agent(fullPrompt, options)
78
- }
79
-
80
- /**
81
- * Spawn the git/utility Explore agent once before the converge loop.
82
- * @returns {Promise<string>} the agent id
83
- */
84
- async function spawnGitAgent() {
85
- const result = await convergeAgent(
86
- `You are the git-utility agent for ${prCoordinates}. Your role is to resolve the PR HEAD SHA, fetch origin main, and check merge conflicts when asked. Do not edit code.\n\n` +
87
- `Initial task: print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
88
- `gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
89
- `Return the full 40-character SHA in the sha field.`,
90
- { label: 'git-utility', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore' },
91
- )
92
- return result?.agentId
93
- }
72
+ const convergeAgent = (prompt, options) =>
73
+ agent(`${HEADLESS_SAFETY_PREAMBLE}${worktreeDirective(activeRepoPath)}${prompt}`, options)
94
74
 
95
75
  /**
96
- * Resume the git/utility agent for a specific task.
97
- * @param {string} agentId the agent id from spawnGitAgent
76
+ * Spawn a fresh git-utility Explore agent for a specific task. 'resolve-head'
77
+ * prints the PR HEAD SHA, 'prefetch-main' fetches origin main so the review
78
+ * lenses diff against an up-to-date base, and any other task reports whether the
79
+ * branch has merge conflicts. The agent never edits code.
98
80
  * @param {string} task the short task name
99
81
  * @param {string} head optional HEAD SHA for conflict checks
100
- * @returns {Promise<object>} the structured output
82
+ * @returns {Promise<object|string>} the structured output, or the transcript string when the 'prefetch-main' resume runs schema-less
101
83
  */
102
- function resumeGitAgent(agentId, task, head) {
84
+ function runGitTask(task, head) {
103
85
  if (task === 'resolve-head') {
104
86
  return convergeAgent(
105
87
  `Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
106
88
  `gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
107
89
  `Return the full 40-character SHA in the sha field. Do not modify any files.`,
108
- { label: 'git-utility', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore', resume: agentId },
90
+ { label: 'git-utility', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore' },
109
91
  )
110
92
  }
111
93
  if (task === 'prefetch-main') {
@@ -113,7 +95,7 @@ function resumeGitAgent(agentId, task, head) {
113
95
  `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` +
114
96
  `git fetch origin main\n` +
115
97
  `Do not edit, commit, push, rebase, or modify any files — fetch only.`,
116
- { label: 'git-utility', phase: 'Converge', agentType: 'Explore', resume: agentId },
98
+ { label: 'git-utility', phase: 'Converge', agentType: 'Explore' },
117
99
  )
118
100
  }
119
101
  return convergeAgent(
@@ -122,44 +104,20 @@ function resumeGitAgent(agentId, task, head) {
122
104
  ` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
123
105
  `up to 5 times, 5 seconds apart (delay each retry with "sleep 5", or the PowerShell alternative "Start-Sleep -Seconds 5"), stopping as soon as mergeable is true or false.\n\n` +
124
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.`,
125
- { label: 'git-utility', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore', resume: agentId },
107
+ { label: 'git-utility', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },
126
108
  )
127
109
  }
128
110
 
129
111
  /**
130
- * Spawn the fixer clean-coder agent for one fix batch, establishing its role and
131
- * the PR coordinates so each later resume (commit and recovery edits) continues
132
- * the same session. The fixer never verifies a separate verifier agent grades
133
- * the working tree, so the verdict comes from a different session than the one
134
- * that commits and recovers, mirroring the repair and conflict paths. The spawn
135
- * makes no edits — the first recovery resume is the earliest step that touches
136
- * the working tree. Returns the runtime agent id so the resume calls target the
137
- * live session; a runtime without resume support returns no agent id, and each
138
- * resume falls back to a fresh spawn.
139
- * @param {string} head PR HEAD SHA
140
- * @param {Array<object>} findings the findings to fix
141
- * @param {string} sourceLabel short description of where the findings came from
142
- * @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
143
- */
144
- async function spawnFixerAgent(head, findings, sourceLabel) {
145
- const result = await convergeAgent(
146
- `You are the fixer agent for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Across this session you run a sequence of steps: commit and push the working-tree fixes once a separate verifier passes them, and recover when a verify objection or a commit-gate block needs another edit. A separate verifier agent grades the working tree, so you never verify your own edits. Make NO edits in this first turn — confirm only that the working tree is on the PR branch at HEAD ${head} with uncommitted fixes present, then wait for the next step's instructions. Reply READY.`,
147
- { label: `fixer:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
148
- )
149
- return result?.agentId
150
- }
151
-
152
- /**
153
- * Resume the fixer agent for commit or recovery edits. The fixer never verifies;
154
- * a separate verifier agent emits the verdict, so commit, verify-recover, and
155
- * commit-recover all run on the editing session while the verdict that gates the
156
- * commit comes from a different session.
157
- * @param {string} agentId the agent id from spawnFixerAgent
112
+ * Spawn a fresh fixer clean-coder agent for a commit or recovery edit. The fixer
113
+ * never verifies; a separate verifier agent emits the verdict, so the verdict
114
+ * that gates the commit comes from a different agent than the one that commits
115
+ * and recovers.
158
116
  * @param {string} task the short task name
159
117
  * @param {object} context task-specific context
160
118
  * @returns {Promise<object>} the structured output
161
119
  */
162
- function resumeFixerAgent(agentId, task, context) {
120
+ function runFixerTask(task, context) {
163
121
  const label = `fixer:${context.sourceLabel}`
164
122
  if (task === 'commit') {
165
123
  return convergeAgent(
@@ -171,7 +129,7 @@ function resumeFixerAgent(agentId, task, context) {
171
129
  `- On a successful push: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a one-line summary.\n` +
172
130
  `- When a commit-time hook or gate (for example code_rules_gate, the CODE_RULES commit gate) rejects the commit because the fix needs a code change: keep the no-edit rule, return newSha=${context.head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=true, blockerDetail=<the verbatim hook message naming the file and rule>, and a summary. A recovery fixer runs after you to clear it.\n` +
173
131
  `- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${context.head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
174
- { label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
132
+ { label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder' },
175
133
  )
176
134
  }
177
135
  if (task === 'commit-recover') {
@@ -185,7 +143,7 @@ function resumeFixerAgent(agentId, task, context) {
185
143
  `- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push — the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
186
144
  `Return values: edited=true with a one-line summary when you changed code to clear the block; edited=false, resolvedWithoutCommit=false when the block cannot be cleared with a code change.` +
187
145
  PRE_COMMIT_GATE_STEP,
188
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
146
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
189
147
  )
190
148
  }
191
149
  const objection = context.objection || VERIFY_OBJECTION_FALLBACK
@@ -199,30 +157,29 @@ function resumeFixerAgent(agentId, task, context) {
199
157
  `- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push — the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
200
158
  `Return values: edited=true with a one-line summary when you changed code to address the objections; edited=false, resolvedWithoutCommit=false when the objections cannot be cleared with a code change.` +
201
159
  PRE_COMMIT_GATE_STEP,
202
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
160
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
203
161
  )
204
162
  }
205
163
 
206
164
  /**
207
- * Joined fixer recovery loop: a separate verifier agent grades the working-tree
208
- * fixes while the fixer session only recovers and commits, so the verdict that
209
- * gates the commit comes from a different session than the one that edits and
210
- * pushes — the same editor/verifier separation the repair and conflict paths
211
- * use. The verify step routes through verifyWithRecovery (verify on the verifier,
212
- * recover on the fixer); the commit step routes through commitWithRecovery
213
- * (commit and commit-recover on the fixer, re-verify on the verifier). A failed
214
- * verdict returns the unchanged HEAD so the round reads as not-progressed.
215
- * @param {string} fixerAgentId the fixer agent id from spawnFixerAgent
216
- * @param {string} verifierId the verifier agent id from spawnVerifierAgent
165
+ * Joined fixer recovery loop: the verify step spawns a code-verifier agent to
166
+ * grade the working-tree fixes while the commit and recovery steps spawn
167
+ * clean-coder fixer agents, so the verdict that gates the commit comes from a
168
+ * different agent than the one that edits and pushes — the same editor/verifier
169
+ * separation the repair and conflict paths use. The verify step routes through
170
+ * verifyWithRecovery (verify on the verifier, recover on the fixer); the commit
171
+ * step routes through commitWithRecovery (commit and commit-recover on the fixer,
172
+ * re-verify on the verifier). A failed verdict returns the unchanged HEAD so the
173
+ * round reads as not-progressed.
217
174
  * @param {string} head PR HEAD SHA
218
175
  * @param {Array<object>} findings the findings to fix
219
176
  * @param {string} sourceLabel short description of where the findings came from
220
177
  * @returns {Promise<object>} FIX_SCHEMA result
221
178
  */
222
- async function fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel) {
179
+ async function fixerWithRecovery(head, findings, sourceLabel) {
223
180
  const verifyTranscript = await verifyWithRecovery({
224
- runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
225
- runRecoverEdit: (objection, attempt) => resumeFixerAgent(fixerAgentId, 'verify-recover', { head, findings, sourceLabel, objection, attempt }),
181
+ runVerify: () => runVerifierTask('fix-verify', { head, findings, sourceLabel }),
182
+ runRecoverEdit: (objection, attempt) => runFixerTask('verify-recover', { head, findings, sourceLabel, objection, attempt }),
226
183
  })
227
184
  if (!verdictPassed(verifyTranscript)) {
228
185
  return {
@@ -235,38 +192,22 @@ async function fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourc
235
192
  }
236
193
  }
237
194
  return commitWithRecovery({
238
- runCommit: () => resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel }),
239
- runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
240
- runRecoverEdit: (detail, attempt) => resumeFixerAgent(fixerAgentId, 'commit-recover', { head, findings, sourceLabel, blockerDetail: detail, attempt }),
195
+ runCommit: () => runFixerTask('commit', { head, findings, sourceLabel }),
196
+ runVerify: () => runVerifierTask('fix-verify', { head, findings, sourceLabel }),
197
+ runRecoverEdit: (detail, attempt) => runFixerTask('commit-recover', { head, findings, sourceLabel, blockerDetail: detail, attempt }),
241
198
  })
242
199
  }
243
200
 
244
201
  /**
245
- * Spawn the code-editor clean-coder agent once per converge round, establishing
246
- * its role so each later resume (fix-edit, conflict-edit, repair-edit,
247
- * repair-commit, standards-edit, hardening-commit, commit-recover, verify-recover)
248
- * continues the same session. The spawn makes no edits — each resume carries the
249
- * task-specific edit instructions. Returns the runtime agent id so the resume
250
- * calls target the live session; a runtime without resume support returns no
251
- * agent id, and each resume falls back to a fresh spawn.
252
- * @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
253
- */
254
- async function spawnCodeEditorAgent() {
255
- const result = await convergeAgent(
256
- `You are the code-editor agent for ${prCoordinates}. Across this converge round you run a sequence of edit and commit steps, each delivered as its own instruction. Make NO edits in this first turn — wait for the first task's instructions. Reply READY.`,
257
- { label: 'code-editor', phase: 'Converge', agentType: 'clean-coder' },
258
- )
259
- return result?.agentId
260
- }
261
-
262
- /**
263
- * Resume the code-editor agent for a specific edit task.
264
- * @param {string} agentId the agent id from spawnCodeEditorAgent
202
+ * Spawn a fresh code-editor clean-coder agent for a specific edit task (fix-edit,
203
+ * conflict-edit, repair-edit, repair-commit, standards-edit,
204
+ * standards-resolve-threads, hardening-commit, commit-recover, verify-recover).
205
+ * Each task carries its own edit instructions.
265
206
  * @param {string} task the short task name
266
207
  * @param {object} context task-specific context
267
- * @returns {Promise<object>} the structured output
208
+ * @returns {Promise<object|string>} the structured output, or the transcript string when a schema-less task ('hardening-commit', 'standards-resolve-threads') runs
268
209
  */
269
- function resumeCodeEditorAgent(agentId, task, context) {
210
+ function runCodeEditorTask(task, context) {
270
211
  const label = `code-editor:${task}`
271
212
  if (task === 'fix-edit') {
272
213
  const findingsBlock = renderFindingsBlock(context.findings)
@@ -287,7 +228,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
287
228
  `- When every finding was already addressed so no code change was needed — yet you still resolved each GitHub review thread above: edited=false, resolvedWithoutCommit=true. Only set this when every thread that carries a comment id is resolved; otherwise the round is treated as stalled.\n` +
288
229
  `Always include a one-line summary.` +
289
230
  PRE_COMMIT_GATE_STEP,
290
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
231
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
291
232
  )
292
233
  }
293
234
  if (task === 'conflict-edit') {
@@ -298,7 +239,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
298
239
  `- Rebase the branch onto origin/main and resolve every conflict so the tree is clean and conflict-free: git fetch origin main; git rebase origin/main; resolve each conflict, preserving the intent of both the PR's change and the incoming base change. A rebase creates local commits, which is fine.\n` +
299
240
  `- Do NOT push and do NOT force-push — the commit step force-pushes after the verify step binds a verdict. Pushing here would change the surface the verifier binds to.\n\n` +
300
241
  `Return rebased=true with a one-line summary when you rebased onto origin/main and resolved the conflicts; rebased=false with a summary when the branch did not actually need a rebase or you could not complete it.`,
301
- { label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
242
+ { label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder' },
302
243
  )
303
244
  }
304
245
  if (task === 'repair-edit') {
@@ -318,7 +259,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
318
259
  `- resolvedWithoutCommit=true only when you addressed the gates with neither a code change nor a rebase (bot threads resolved only), so there is nothing for the commit step to push.\n` +
319
260
  `Always include a one-line summary.` +
320
261
  PRE_COMMIT_GATE_STEP,
321
- { label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
262
+ { label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
322
263
  )
323
264
  }
324
265
  if (task === 'repair-commit') {
@@ -334,7 +275,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
334
275
  `- On a successful push: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a one-line summary.\n` +
335
276
  `- When a commit-time hook or gate (for example code_rules_gate, the CODE_RULES commit gate) rejects the commit because the fix needs a code change: keep the no-edit rule, return newSha=${context.head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=true, blockerDetail=<the verbatim hook message naming the file and rule>, and a summary. A recovery fixer runs after you to clear it.\n` +
336
277
  `- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${context.head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
337
- { label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
278
+ { label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
338
279
  )
339
280
  }
340
281
  if (task === 'standards-edit') {
@@ -350,7 +291,20 @@ function resumeCodeEditorAgent(agentId, task, context) {
350
291
  `3. For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply via python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "Code-standard-only finding — deferred to follow-up issue <url>." Then resolve the thread by its PRRT_ node id (GraphQL lookup on comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n\n` +
351
292
  `Return the issue URL in issueUrl (empty string when it could not be filed), the hardening checkout path and branch, hardeningEdited, and a one-line summary.` +
352
293
  PRE_COMMIT_GATE_STEP,
353
- { label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
294
+ { label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
295
+ )
296
+ }
297
+ if (task === 'standards-resolve-threads') {
298
+ const findingsBlock = renderFindingsBlock(context.findings)
299
+ const threadIds = context.findings
300
+ .flatMap((each) => collectFindingThreadIds(each))
301
+ .filter((each) => typeof each === 'number')
302
+ return convergeAgent(
303
+ `You are the THREAD-RESOLUTION step for a code-standard-only round on ${prCoordinates}, HEAD ${context.head} (${context.sourceLabel}). This run already filed the deferred-fix follow-up issue ${context.issueUrl}, so this batch's code-standard findings defer to that same issue. Make NO code edits, NO commit, and NO push — only reply to and resolve the review threads this batch carries.\n\n` +
304
+ `Findings:\n${findingsBlock}\n\n` +
305
+ `For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply via python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "Code-standard-only finding — deferred to follow-up issue ${context.issueUrl}." Then resolve the thread by its PRRT_ node id (GraphQL lookup on comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n\n` +
306
+ `Return a one-line summary naming the threads you resolved.`,
307
+ { label, phase: 'Converge', agentType: 'clean-coder' },
354
308
  )
355
309
  }
356
310
  if (task === 'hardening-commit') {
@@ -360,7 +314,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
360
314
  `- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Only commit and push what is already there.\n` +
361
315
  `- In ${context.hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${context.hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${context.issueUrl || '(none)'} and states the PR hardens the environment so the deferred violation classes are blocked at Write/Edit time. Honor the gh-body-file rule: write a BOM-free temp file and pass --body-file.\n\n` +
362
316
  `Return a one-line summary naming the hardening PR URL.`,
363
- { label, phase: 'Converge', agentType: 'clean-coder', resume: agentId },
317
+ { label, phase: 'Converge', agentType: 'clean-coder' },
364
318
  )
365
319
  }
366
320
  if (task === 'commit-recover') {
@@ -374,7 +328,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
374
328
  `- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push — the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
375
329
  `Return values: edited=true with a one-line summary when you changed code to clear the block; edited=false, resolvedWithoutCommit=false when the block cannot be cleared with a code change.` +
376
330
  PRE_COMMIT_GATE_STEP,
377
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
331
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
378
332
  )
379
333
  }
380
334
  // verify-recover
@@ -389,34 +343,19 @@ function resumeCodeEditorAgent(agentId, task, context) {
389
343
  `- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push — the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
390
344
  `Return values: edited=true with a one-line summary when you changed code to address the objections; edited=false, resolvedWithoutCommit=false when the objections cannot be cleared with a code change.` +
391
345
  PRE_COMMIT_GATE_STEP,
392
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
346
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
393
347
  )
394
348
  }
395
349
 
396
350
  /**
397
- * Spawn the verifier code-verifier agent once per converge round, establishing
398
- * its role so each later resume (fix-verify, repair-verify, hardening-verify) continues the
399
- * same session. The spawn makes no edits verification only. Returns the runtime
400
- * agent id so the resume calls target the live session; a runtime without resume
401
- * support returns no agent id, and each resume falls back to a fresh spawn.
402
- * @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
403
- */
404
- async function spawnVerifierAgent() {
405
- const result = await convergeAgent(
406
- `You are the verifier agent for ${prCoordinates}. Across this converge round you run a sequence of verify steps, each delivered as its own instruction; each ends with a fenced verdict block. Do NO edits of any kind — verification only. Make no move in this first turn — wait for the first verify task's instructions. Reply READY.`,
407
- { label: 'verifier', phase: 'Converge', agentType: 'code-verifier' },
408
- )
409
- return result?.agentId
410
- }
411
-
412
- /**
413
- * Resume the verifier agent for a specific verify task.
414
- * @param {string} agentId the agent id from spawnVerifierAgent
351
+ * Spawn a fresh verifier code-verifier agent for a specific verify task
352
+ * (fix-verify, repair-verify, hardening-verify). The agent makes no edits —
353
+ * verification only and ends its message with a fenced verdict block.
415
354
  * @param {string} task the short task name
416
355
  * @param {object} context task-specific context
417
356
  * @returns {Promise<string>} the verifier transcript carrying the verdict fence
418
357
  */
419
- function resumeVerifierAgent(agentId, task, context) {
358
+ function runVerifierTask(task, context) {
420
359
  const label = `verifier:${task}`
421
360
  if (task === 'fix-verify') {
422
361
  const findingsBlock = renderFindingsBlock(context.findings)
@@ -427,7 +366,7 @@ function resumeVerifierAgent(agentId, task, context) {
427
366
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
428
367
  `2. Verify the uncommitted working-tree changes resolve every finding above: run the relevant tests and the named gates against the working tree. Read the diff (git diff) and confirm each finding is fixed test-first per CODE_RULES.\n` +
429
368
  `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
430
- { label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
369
+ { label, phase: 'Converge', agentType: 'code-verifier' },
431
370
  )
432
371
  }
433
372
  if (task === 'repair-verify') {
@@ -441,7 +380,7 @@ function resumeVerifierAgent(agentId, task, context) {
441
380
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
442
381
  `2. Verify the working tree against origin/main: any bot-thread code fix is correct test-first per CODE_RULES, and a rebase (if any) left a clean, conflict-free tree. Read the diff (git diff origin/main) and run the relevant tests and named gates.\n` +
443
382
  `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
444
- { label, phase: 'Finalize', agentType: 'code-verifier', resume: agentId },
383
+ { label, phase: 'Finalize', agentType: 'code-verifier' },
445
384
  )
446
385
  }
447
386
  return convergeAgent(
@@ -460,37 +399,20 @@ function resumeVerifierAgent(agentId, task, context) {
460
399
  ` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
461
400
  " ```\n" +
462
401
  ` When verification fails, set all_pass to false and list the unresolved concerns in findings; still include the manifest_sha256. The verdict fence must be the last thing in your message.`,
463
- { label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
464
- )
465
- }
466
-
467
- /**
468
- * Spawn the general-utility general-purpose agent once per converge round,
469
- * establishing its role so each later resume (post-clean-audit, mark-ready)
470
- * continues the same session. The spawn edits no code.
471
- * Returns the runtime agent id so the resume calls target the live session; a
472
- * runtime without resume support returns no agent id, and each resume falls back
473
- * to a fresh spawn.
474
- * @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
475
- */
476
- async function spawnGeneralUtilityAgent() {
477
- const result = await convergeAgent(
478
- `You are the general-utility agent for ${prCoordinates}. Across this converge round you run a sequence of administrative steps (posting the clean audit, marking the PR ready), each delivered as its own instruction. Do not edit code, commit, or push. Make no move in this first turn — wait for the first task's instructions. Reply READY.`,
479
- { label: 'general-utility', phase: 'Converge', agentType: 'general-purpose' },
402
+ { label, phase: 'Converge', agentType: 'code-verifier' },
480
403
  )
481
- return result?.agentId
482
404
  }
483
405
 
484
406
  /**
485
- * Resume the general-utility agent for one of its two administrative tasks:
486
- * 'post-clean-audit' posts the terminal CLEAN bugteam review, and 'mark-ready'
487
- * marks the PR ready and confirms it left draft state.
488
- * @param {string} agentId the agent id from spawnGeneralUtilityAgent
407
+ * Spawn a fresh general-utility general-purpose agent for one of its two
408
+ * administrative tasks: 'post-clean-audit' posts the terminal CLEAN bugteam
409
+ * review, and 'mark-ready' marks the PR ready and confirms it left draft state.
410
+ * The agent edits no code.
489
411
  * @param {'post-clean-audit'|'mark-ready'} task the short task name
490
412
  * @param {object} context task-specific context
491
413
  * @returns {Promise<object>} the task result
492
414
  */
493
- function resumeGeneralUtilityAgent(agentId, task, context) {
415
+ function runGeneralUtilityTask(task, context) {
494
416
  const label = `general-utility:${task}`
495
417
  if (task === 'post-clean-audit') {
496
418
  return convergeAgent(
@@ -499,7 +421,7 @@ function resumeGeneralUtilityAgent(agentId, task, context) {
499
421
  `python "${CONFIG.prLoopScripts}/post_audit_thread.py" --skill bugteam --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --commit ${context.head} --state CLEAN --findings-json <temp-file>\n` +
500
422
  `Run the script with --help first if any flag name differs. This posts the APPROVE review body that check_convergence.py reads for the bugteam gate. Do not edit code, commit, or push.\n\n` +
501
423
  `Report whether the review landed. When the script prints a review URL, return {posted:true, reviewUrl:<that URL>, reason:""}. When the script is denied (a permission prompt or auto-mode-classifier block), errors, or prints anything other than a review URL, return {posted:false, reviewUrl:"", reason:<the denial message or error as one line>}. Do not retry a denied post.`,
502
- { label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose', resume: agentId },
424
+ { label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
503
425
  )
504
426
  }
505
427
  if (task === 'mark-ready') {
@@ -512,35 +434,18 @@ function resumeGeneralUtilityAgent(agentId, task, context) {
512
434
  `1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
513
435
  `2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
514
436
  `Return {ready:true} only when step 2 prints false (the PR is no longer a draft). If step 1 errors or step 2 still prints true, return {ready:false}.`,
515
- { label, phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose', resume: agentId },
437
+ { label, phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose' },
516
438
  )
517
439
  }
518
- throw new Error(`resumeGeneralUtilityAgent: unknown task ${task}`)
519
- }
520
-
521
- /**
522
- * Spawn the convergence-check Explore agent once before the converge loop,
523
- * establishing its role so each per-round resume runs the convergence gate in the
524
- * same session. The spawn edits no code. Returns the runtime agent id so the
525
- * resume calls target the live session; a runtime without resume support returns
526
- * no agent id, and each resume falls back to a fresh spawn.
527
- * @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
528
- */
529
- async function spawnConvergenceCheckAgent() {
530
- const result = await convergeAgent(
531
- `You are the convergence-check agent for ${prCoordinates}. Each round you run the authoritative convergence gate and report its result, delivered as its own instruction. Do not edit code. Make no move in this first turn — wait for the first check instruction. Reply READY.`,
532
- { label: 'convergence-check', phase: 'Converge', agentType: 'Explore' },
533
- )
534
- return result?.agentId
440
+ throw new Error(`runGeneralUtilityTask: unknown task ${task}`)
535
441
  }
536
442
 
537
443
  /**
538
- * Resume the convergence-check agent for the convergence check.
539
- * @param {string} agentId the agent id from spawnConvergenceCheckAgent
444
+ * Spawn a fresh convergence-check Explore agent for the convergence check.
540
445
  * @param {object} context carries bugbotDown and copilotDown
541
446
  * @returns {Promise<object>} CONVERGENCE_SCHEMA result
542
447
  */
543
- function resumeConvergenceCheckAgent(agentId, context) {
448
+ function runConvergenceCheck(context) {
544
449
  const label = 'check-convergence'
545
450
  const bugbotDownFlag = context.bugbotDown ? ' --bugbot-down' : ''
546
451
  const copilotDownFlag = context.copilotDown ? ' --copilot-down' : ''
@@ -550,7 +455,7 @@ function resumeConvergenceCheckAgent(agentId, context) {
550
455
  `Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
551
456
  `Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
552
457
  `Exit 2 -> retry once; if it still errors, return {pass:false, failures:["check_convergence gh error"]}.`,
553
- { label, phase: 'Finalize', schema: CONVERGENCE_SCHEMA, agentType: 'Explore', resume: agentId },
458
+ { label, phase: 'Finalize', schema: CONVERGENCE_SCHEMA, agentType: 'Explore' },
554
459
  )
555
460
  }
556
461
 
@@ -1029,7 +934,7 @@ function commitNeedsCodeRecovery(commitResult) {
1029
934
  * resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
1030
935
  * against it interpolates the literal string 'HEAD undefined' into their prompts
1031
936
  * and produces a spurious clean verdict on a non-existent commit.
1032
- * @param {string|null|undefined} resolvedHead the SHA from the git-utility agent resume for 'resolve-head'
937
+ * @param {string|null|undefined} resolvedHead the SHA from the git-utility 'resolve-head' task
1033
938
  * @returns {boolean} true only when the SHA is a non-empty string
1034
939
  */
1035
940
  function isResolvedHeadUsable(resolvedHead) {
@@ -1042,7 +947,7 @@ function isResolvedHeadUsable(resolvedHead) {
1042
947
  * not-conflicting so the run proceeds straight to the bug checks rather than
1043
948
  * force-pushing a rebase on a verdict that does not exist — a transient check
1044
949
  * failure must never trigger a destructive rebase.
1045
- * @param {object|null|undefined} mergeState the git-utility agent resume result for 'check-merge-conflicts'
950
+ * @param {object|null|undefined} mergeState the git-utility 'check-merge-conflicts' task result
1046
951
  * @returns {boolean} true only when the check reported conflicting:true
1047
952
  */
1048
953
  function isMergeConflicting(mergeState) {
@@ -1333,10 +1238,10 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
1333
1238
  /**
1334
1239
  * Fix lens: edit (clean-coder, no commit) -> verify (a separate code-verifier
1335
1240
  * emits a verdict fence binding the working tree) -> commit (clean-coder, one
1336
- * commit + push, no edits). The verifier is a distinct persistent group from the
1337
- * fixer, so the verdict that gates the commit comes from a different session than
1338
- * the one that edits and pushes — the same editor/verifier separation the repair
1339
- * and conflict paths use, and the separation a workflow code-verifier needs to
1241
+ * commit + push, no edits). The verifier is a distinct agent from the fixer, so
1242
+ * the verdict that gates the commit comes from a different agent than the one
1243
+ * that edits and pushes — the same editor/verifier separation the repair and
1244
+ * conflict paths use, and the separation a workflow code-verifier needs to
1340
1245
  * produce the verdict the verified-commit gate requires, which the SubagentStop
1341
1246
  * minter cannot mint for workflow-spawned agents. When verification fails (or the
1342
1247
  * edit step stalled with no thread to resolve), the commit step is skipped and the
@@ -1347,8 +1252,7 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
1347
1252
  * @returns {Promise<object>} FIX_SCHEMA result
1348
1253
  */
1349
1254
  async function applyFixes(head, findings, sourceLabel) {
1350
- const codeEditorId = await spawnCodeEditorAgent()
1351
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'fix-edit', { head, findings, sourceLabel })
1255
+ const editResult = await runCodeEditorTask('fix-edit', { head, findings, sourceLabel })
1352
1256
  if (editResult?.resolvedWithoutCommit === true && editResult?.edited !== true) {
1353
1257
  return {
1354
1258
  newSha: head,
@@ -1359,9 +1263,7 @@ async function applyFixes(head, findings, sourceLabel) {
1359
1263
  blockerDetail: '',
1360
1264
  }
1361
1265
  }
1362
- const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel)
1363
- const verifierId = await spawnVerifierAgent()
1364
- return fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel)
1266
+ return fixerWithRecovery(head, findings, sourceLabel)
1365
1267
  }
1366
1268
 
1367
1269
  /**
@@ -1370,7 +1272,7 @@ async function applyFixes(head, findings, sourceLabel) {
1370
1272
  * post stops the run with an actionable message rather than re-converging until
1371
1273
  * the iteration cap. Handles a dead post agent (a null result) as not posted.
1372
1274
  * @param {string} head converged PR HEAD SHA
1373
- * @param {object} auditResult CLEAN_AUDIT_SCHEMA result from the post-clean-audit resume, or null when the agent died
1275
+ * @param {object} auditResult CLEAN_AUDIT_SCHEMA result from the post-clean-audit task, or null when the agent died
1374
1276
  * @returns {string} the blocker message naming the post failure and the unblock path
1375
1277
  */
1376
1278
  function cleanAuditBlocker(head, auditResult) {
@@ -1400,7 +1302,7 @@ function runCopilotGate(head) {
1400
1302
  ` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
1401
1303
  `2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (delay each attempt with "sleep 360", or the PowerShell alternative "Start-Sleep -Seconds 360"). 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` +
1402
1304
  ` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
1403
- ` - Copilot review present and clean/approved on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:false, findings:[]}.\n` +
1305
+ ` - 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` +
1404
1306
  ` - Copilot findings on HEAD -> return them (each with its inline comment id in replyToCommentId; category 'code-standard' for pure CODE_RULES/style violations with no behavioral impact, 'bug' otherwise), clean:false, down:false.\n` +
1405
1307
  ` - No review after ${CONFIG.copilotMaxPolls} attempts -> Copilot is down for this run (unreachable, or silently out of quota with no notice): return {sha:${'`'}${head}${'`'}, clean:false, down:true, findings:[]}.\n\n` +
1406
1308
  `Return strictly the schema.`,
@@ -1423,8 +1325,7 @@ function runCopilotGate(head) {
1423
1325
  * @returns {Promise<object>} FIX_SCHEMA result
1424
1326
  */
1425
1327
  async function repairConvergence(head, failures) {
1426
- const codeEditorId = await spawnCodeEditorAgent()
1427
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'repair-edit', { head, failures })
1328
+ const editResult = await runCodeEditorTask('repair-edit', { head, failures })
1428
1329
  const hasPushWork = editResult?.edited === true || editResult?.rebased === true
1429
1330
  if (!hasPushWork) {
1430
1331
  return {
@@ -1436,10 +1337,9 @@ async function repairConvergence(head, failures) {
1436
1337
  blockerDetail: '',
1437
1338
  }
1438
1339
  }
1439
- const verifierId = await spawnVerifierAgent()
1440
1340
  const verifyTranscript = await verifyWithRecovery({
1441
- runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
1442
- runRecoverEdit: (objection, attempt) => resumeCodeEditorAgent(codeEditorId, 'verify-recover', { head, sourceLabel: 'repair', objection, attempt }),
1341
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1342
+ runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'repair', objection, attempt }),
1443
1343
  })
1444
1344
  if (!verdictPassed(verifyTranscript)) {
1445
1345
  return {
@@ -1453,9 +1353,9 @@ async function repairConvergence(head, failures) {
1453
1353
  }
1454
1354
  const wasRebased = editResult?.rebased === true
1455
1355
  return commitWithRecovery({
1456
- runCommit: () => resumeCodeEditorAgent(codeEditorId, 'repair-commit', { head, wasRebased }),
1457
- runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
1458
- runRecoverEdit: (detail, attempt) => resumeCodeEditorAgent(codeEditorId, 'commit-recover', { head, sourceLabel: 'repair', blockerDetail: detail, attempt }),
1356
+ runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased }),
1357
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1358
+ runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'repair', blockerDetail: detail, attempt }),
1459
1359
  })
1460
1360
  }
1461
1361
 
@@ -1473,24 +1373,22 @@ async function repairConvergence(head, failures) {
1473
1373
  * @param {string} head PR HEAD SHA before any rebase
1474
1374
  * @returns {Promise<string>} the HEAD SHA after a successful rebase push, or the unchanged head
1475
1375
  */
1476
- async function resolveMergeConflicts(head, gitAgentId) {
1477
- const mergeState = await resumeGitAgent(gitAgentId, 'check-merge-conflicts', head)
1376
+ async function resolveMergeConflicts(head) {
1377
+ const mergeState = await runGitTask('check-merge-conflicts', head)
1478
1378
  if (!isMergeConflicting(mergeState)) return head
1479
1379
  log(`Pre-flight: ${prCoordinates} conflicts with origin/main — rebasing clean before the bug checks`)
1480
- const codeEditorId = await spawnCodeEditorAgent()
1481
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'conflict-edit', { head })
1380
+ const editResult = await runCodeEditorTask('conflict-edit', { head })
1482
1381
  if (editResult?.rebased !== true) return head
1483
1382
  const failures = ['PR branch had merge conflicts with origin/main; the rebase must leave a clean, conflict-free tree']
1484
- const verifierId = await spawnVerifierAgent()
1485
1383
  const verifyTranscript = await verifyWithRecovery({
1486
- runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
1487
- runRecoverEdit: (objection, attempt) => resumeCodeEditorAgent(codeEditorId, 'verify-recover', { head, sourceLabel: 'conflict-rebase', objection, attempt }),
1384
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1385
+ runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'conflict-rebase', objection, attempt }),
1488
1386
  })
1489
1387
  if (!verdictPassed(verifyTranscript)) return head
1490
1388
  const commitResult = await commitWithRecovery({
1491
- runCommit: () => resumeCodeEditorAgent(codeEditorId, 'repair-commit', { head, wasRebased: true }),
1492
- runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
1493
- runRecoverEdit: (detail, attempt) => resumeCodeEditorAgent(codeEditorId, 'commit-recover', { head, sourceLabel: 'conflict-rebase', blockerDetail: detail, attempt }),
1389
+ runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased: true }),
1390
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1391
+ runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'conflict-rebase', blockerDetail: detail, attempt }),
1494
1392
  })
1495
1393
  return commitResult?.newSha || head
1496
1394
  }
@@ -1507,19 +1405,35 @@ function isStandardsOnlyRound(findings) {
1507
1405
  return findings.length > 0 && findings.every((each) => each.category === 'code-standard')
1508
1406
  }
1509
1407
 
1408
+ /**
1409
+ * Decide whether a standards-only round should file the follow-up fix issue and
1410
+ * open the environment-hardening PR. Standards findings are deferred rather than
1411
+ * fixed on this PR, so the same code-standard findings re-surface on every
1412
+ * converge round and on the Copilot gate; without this guard each re-entry files
1413
+ * a fresh duplicate follow-up issue and hardening PR for the one deferred finding
1414
+ * class. The follow-up issue is filed once per convergence run — a round whose
1415
+ * filing succeeds latches the guard, while a round whose filing failed leaves it
1416
+ * clear so a later round retries the filing.
1417
+ * @param {boolean} hasAlreadyFiled true when an earlier standards-only round in this run already filed the follow-up issue
1418
+ * @returns {boolean} true when this round should attempt the follow-up filing
1419
+ */
1420
+ function shouldOpenStandardsFollowUp(hasAlreadyFiled) {
1421
+ return hasAlreadyFiled !== true
1422
+ }
1423
+
1510
1424
  /**
1511
1425
  * Build the standards-deferral note for the closing report, naming the
1512
- * environment-hardening PR only when one was opened this round so the note
1426
+ * environment-hardening PR only when one was opened for this run so the note
1513
1427
  * never claims a PR the skip paths did not produce.
1514
1428
  * @param {number} findingsCount count of deferred code-standard findings
1515
- * @param {boolean} hardeningPrOpened true when the hardening PR was opened this round
1429
+ * @param {boolean} wasHardeningPrOpened true when the hardening PR was opened for this run
1516
1430
  * @returns {string} the human-facing deferral note
1517
1431
  */
1518
- function standardsDeferralNote(findingsCount, hardeningPrOpened) {
1432
+ function standardsDeferralNote(findingsCount, wasHardeningPrOpened) {
1519
1433
  const base = `${findingsCount} code-standard finding(s) deferred to a follow-up fix issue`
1520
- return hardeningPrOpened
1434
+ return wasHardeningPrOpened
1521
1435
  ? `${base} plus an environment-hardening PR — verify both land`
1522
- : `${base} — verify it lands (no environment-hardening PR was opened this round)`
1436
+ : `${base} — verify it lands (no environment-hardening PR was opened for this run)`
1523
1437
  }
1524
1438
 
1525
1439
  /**
@@ -1530,31 +1444,102 @@ function standardsDeferralNote(findingsCount, hardeningPrOpened) {
1530
1444
  * commit (clean-coder makes one commit, pushes, and opens the DRAFT hardening
1531
1445
  * PR). Splitting the edit from the push lets a workflow code-verifier produce the
1532
1446
  * verdict the verified-commit gate requires for the cross-repo hardening commit.
1533
- * This PR's own branch is never touched. When the edit staged no hardening, or
1534
- * the verify step fails, the follow-up issue still stands and the commit step is
1535
- * skipped.
1447
+ * This PR's own branch is never touched. When a hardening PR already opened for
1448
+ * this run, the edit staged no hardening, or the verify step fails, the follow-up
1449
+ * issue still stands and the commit step is skipped.
1536
1450
  * @param {string} head PR HEAD SHA the findings were raised against
1537
1451
  * @param {Array<object>} findings deduped code-standard-only findings
1538
1452
  * @param {string} sourceLabel short description of where the findings came from
1539
- * @returns {Promise<object>} `{ hardeningPrOpened }` true only when the hardening PR was opened this round
1453
+ * @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
1454
+ * @returns {Promise<object>} `{ followUpIssueFiled, issueUrl, hardeningPrOpened }` — 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 only when the hardening PR was opened on this call
1540
1455
  */
1541
- async function spawnStandardsFollowUp(head, findings, sourceLabel) {
1542
- const codeEditorId = await spawnCodeEditorAgent()
1543
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'standards-edit', { head, findings, sourceLabel })
1456
+ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened) {
1457
+ const editResult = await runCodeEditorTask('standards-edit', { head, findings, sourceLabel })
1458
+ const followUpIssueFiled = typeof editResult?.issueUrl === 'string' && editResult.issueUrl.length > 0
1459
+ const followUpIssueUrl = followUpIssueFiled ? editResult.issueUrl : ''
1460
+ if (hasHardeningPrAlreadyOpened === true) {
1461
+ return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
1462
+ }
1544
1463
  if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
1545
- return { hardeningPrOpened: false }
1464
+ return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
1546
1465
  }
1547
- const verifierId = await spawnVerifierAgent()
1548
- const verifyTranscript = await resumeVerifierAgent(verifierId, 'hardening-verify', {
1466
+ const verifyTranscript = await runVerifierTask('hardening-verify', {
1549
1467
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch,
1550
1468
  })
1551
1469
  if (!verdictPassed(verifyTranscript)) {
1552
- return { hardeningPrOpened: false }
1470
+ return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
1553
1471
  }
1554
- await resumeCodeEditorAgent(codeEditorId, 'hardening-commit', {
1472
+ await runCodeEditorTask('hardening-commit', {
1555
1473
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
1556
1474
  })
1557
- return { hardeningPrOpened: true }
1475
+ return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: true }
1476
+ }
1477
+
1478
+ /**
1479
+ * Report whether any finding in a batch carries a GitHub review thread that needs
1480
+ * an inline reply and resolution.
1481
+ * @param {Array<object>} findings the batch of findings
1482
+ * @returns {boolean} true when at least one finding carries a review comment id
1483
+ */
1484
+ function findingsCarryThreads(findings) {
1485
+ return findings.some((each) => collectFindingThreadIds(each).length > 0)
1486
+ }
1487
+
1488
+ /**
1489
+ * On the reuse path — after this run already filed the deferred-fix follow-up
1490
+ * issue — reply to and resolve this batch's code-standard review threads against
1491
+ * that same issue. The issue filing and hardening PR stay gated behind the
1492
+ * run-once flags; only the per-batch thread resolution runs here, so a later
1493
+ * standards-only round's bot threads are still marked resolved and the FINALIZE
1494
+ * zero-unresolved-bot-threads gate passes. A batch of in-memory audit findings
1495
+ * that carries no review thread needs no agent, so the resolve step is skipped.
1496
+ * @param {string} head PR HEAD SHA the findings were raised against
1497
+ * @param {Array<object>} findings this batch's deduped code-standard-only findings
1498
+ * @param {string} sourceLabel short description of where the findings came from
1499
+ * @returns {Promise<void>}
1500
+ */
1501
+ async function resolveStandardsThreadsForBatch(head, findings, sourceLabel) {
1502
+ if (!findingsCarryThreads(findings)) {
1503
+ return
1504
+ }
1505
+ await runCodeEditorTask('standards-resolve-threads', {
1506
+ head, findings, sourceLabel, issueUrl: standardsFollowUpIssueUrl,
1507
+ })
1508
+ }
1509
+
1510
+ /**
1511
+ * File the deferred follow-up issue and open the environment-hardening PR at most
1512
+ * once per convergence run, then reuse them. The two guards latch independently:
1513
+ * the follow-up issue latches only when its filing succeeds, and the hardening PR
1514
+ * latches the moment one opens and never re-opens. A standards-only round whose
1515
+ * issue filing already succeeded — in the converge phase or at the Copilot gate —
1516
+ * skips spawnStandardsFollowUp, resolves this batch's own code-standard review
1517
+ * threads against the already-filed issue via resolveStandardsThreadsForBatch, and
1518
+ * returns the remembered hardening outcome. A round whose issue filing failed
1519
+ * re-runs spawnStandardsFollowUp to retry the filing, passing the remembered
1520
+ * hardening state so an already-opened hardening PR is never re-opened. The run
1521
+ * therefore files one follow-up issue and opens one hardening PR rather than a
1522
+ * fresh duplicate per re-entry — while every round still resolves the review
1523
+ * threads its own findings carry, even when the filing must retry after the
1524
+ * hardening PR has opened.
1525
+ * @param {string} head PR HEAD SHA the findings were raised against
1526
+ * @param {Array<object>} findings deduped code-standard-only findings
1527
+ * @param {string} sourceLabel short description of where the findings came from
1528
+ * @returns {Promise<boolean>} true when a hardening PR was opened for this run
1529
+ */
1530
+ async function openStandardsFollowUpOnce(head, findings, sourceLabel) {
1531
+ if (!shouldOpenStandardsFollowUp(hasStandardsFollowUpFiled)) {
1532
+ 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'}`)
1533
+ await resolveStandardsThreadsForBatch(head, findings, sourceLabel)
1534
+ return wasStandardsHardeningPrOpened
1535
+ }
1536
+ const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened)
1537
+ hasStandardsFollowUpFiled = standardsOutcome?.followUpIssueFiled === true
1538
+ if (hasStandardsFollowUpFiled) {
1539
+ standardsFollowUpIssueUrl = standardsOutcome.issueUrl
1540
+ }
1541
+ wasStandardsHardeningPrOpened = wasStandardsHardeningPrOpened || standardsOutcome?.hardeningPrOpened === true
1542
+ return wasStandardsHardeningPrOpened
1558
1543
  }
1559
1544
 
1560
1545
  let phase = 'CONVERGE'
@@ -1563,23 +1548,24 @@ let rounds = 0
1563
1548
  let iterations = 0
1564
1549
  let blocker = null
1565
1550
  let bugbotDown = input.bugbotDisabled || false
1566
- let copilotDown = false
1551
+ let copilotDown = input.copilotDisabled || false
1567
1552
  let copilotNote = null
1568
1553
  let standardsNote = null
1554
+ let hasStandardsFollowUpFiled = false
1555
+ let wasStandardsHardeningPrOpened = false
1556
+ let standardsFollowUpIssueUrl = ''
1569
1557
  let reuseNote = null
1570
1558
 
1571
- const gitAgentId = await spawnGitAgent()
1572
-
1573
- const preflightHead = await resumeGitAgent(gitAgentId, 'resolve-head')
1559
+ const preflightHead = await runGitTask('resolve-head')
1574
1560
  if (isResolvedHeadUsable(preflightHead?.sha)) {
1575
- await resolveMergeConflicts(preflightHead.sha, gitAgentId)
1561
+ await resolveMergeConflicts(preflightHead.sha)
1576
1562
  }
1577
1563
 
1578
1564
  log('Reuse pass: scanning the full diff for certain, behaviorally identical, autonomously implementable reuse improvements before convergence')
1579
- const reuseHeadResult = await resumeGitAgent(gitAgentId, 'resolve-head')
1565
+ const reuseHeadResult = await runGitTask('resolve-head')
1580
1566
  const reuseHead = reuseHeadResult?.sha
1581
1567
  if (isResolvedHeadUsable(reuseHead)) {
1582
- await resumeGitAgent(gitAgentId, 'prefetch-main')
1568
+ await runGitTask('prefetch-main')
1583
1569
  const reuse = await runReuseAuditPass(reuseHead)
1584
1570
  const reuseFindings = reuse?.findings || []
1585
1571
  if (reuseFindings.length > 0) {
@@ -1595,19 +1581,17 @@ if (isResolvedHeadUsable(reuseHead)) {
1595
1581
  log('Reuse pass: could not resolve HEAD — proceeding to convergence')
1596
1582
  }
1597
1583
 
1598
- const convergenceId = await spawnConvergenceCheckAgent()
1599
-
1600
1584
  while (iterations < CONFIG.maxIterations) {
1601
1585
  iterations += 1
1602
1586
  if (phase === 'CONVERGE') {
1603
1587
  rounds += 1
1604
- const headResult = await resumeGitAgent(gitAgentId, 'resolve-head')
1588
+ const headResult = await runGitTask('resolve-head')
1605
1589
  head = headResult?.sha
1606
1590
  if (!isResolvedHeadUsable(head)) {
1607
1591
  log(`Round ${rounds}: resolve-head agent returned no SHA — retrying without spawning lenses`)
1608
1592
  continue
1609
1593
  }
1610
- await resumeGitAgent(gitAgentId, 'prefetch-main')
1594
+ await runGitTask('prefetch-main')
1611
1595
  log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
1612
1596
  const lenses = await parallel([
1613
1597
  () => runBugbotLens(head),
@@ -1623,10 +1607,9 @@ while (iterations < CONFIG.maxIterations) {
1623
1607
  const findings = roundOutcome.findings
1624
1608
  if (isStandardsOnlyRound(findings)) {
1625
1609
  log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
1626
- const generalId = await spawnGeneralUtilityAgent()
1627
- const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
1628
- standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
1629
- const auditResult = await resumeGeneralUtilityAgent(generalId, 'post-clean-audit', { head })
1610
+ const wasHardeningPrOpened = await openStandardsFollowUpOnce(head, findings, 'converge-round')
1611
+ standardsNote = standardsDeferralNote(findings.length, wasHardeningPrOpened)
1612
+ const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
1630
1613
  if (!auditResult?.posted) {
1631
1614
  blocker = cleanAuditBlocker(head, auditResult)
1632
1615
  break
@@ -1652,8 +1635,7 @@ while (iterations < CONFIG.maxIterations) {
1652
1635
  continue
1653
1636
  }
1654
1637
  log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
1655
- const cleanGeneralId = await spawnGeneralUtilityAgent()
1656
- const auditResult = await resumeGeneralUtilityAgent(cleanGeneralId, 'post-clean-audit', { head })
1638
+ const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
1657
1639
  if (!auditResult?.posted) {
1658
1640
  blocker = cleanAuditBlocker(head, auditResult)
1659
1641
  break
@@ -1663,6 +1645,13 @@ while (iterations < CONFIG.maxIterations) {
1663
1645
  }
1664
1646
 
1665
1647
  if (phase === 'COPILOT') {
1648
+ if (input.copilotDisabled) {
1649
+ copilotDown = true
1650
+ 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'
1651
+ 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.')
1652
+ phase = 'FINALIZE'
1653
+ continue
1654
+ }
1666
1655
  const copilot = await runCopilotGate(head)
1667
1656
  const copilotOutcome = classifyCopilotOutcome(copilot)
1668
1657
  copilotDown = resolveCopilotDown(copilotOutcome)
@@ -1681,8 +1670,8 @@ while (iterations < CONFIG.maxIterations) {
1681
1670
  if (copilotOutcome.kind === 'fix') {
1682
1671
  if (isStandardsOnlyRound(copilotOutcome.findings)) {
1683
1672
  log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
1684
- const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
1685
- standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome?.hardeningPrOpened === true)
1673
+ const wasHardeningPrOpened = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot')
1674
+ standardsNote = standardsDeferralNote(copilotOutcome.findings.length, wasHardeningPrOpened)
1686
1675
  copilotDown = false
1687
1676
  copilotNote = null
1688
1677
  phase = 'FINALIZE'
@@ -1708,15 +1697,14 @@ while (iterations < CONFIG.maxIterations) {
1708
1697
  }
1709
1698
 
1710
1699
  if (phase === 'FINALIZE') {
1711
- const convergence = await resumeConvergenceCheckAgent(convergenceId, { bugbotDown, copilotDown })
1700
+ const convergence = await runConvergenceCheck({ bugbotDown, copilotDown })
1712
1701
  const convergenceOutcome = classifyConvergenceOutcome(convergence)
1713
1702
  if (convergenceOutcome.kind === 'retry') {
1714
1703
  log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
1715
1704
  continue
1716
1705
  }
1717
1706
  if (convergenceOutcome.kind === 'ready') {
1718
- const finalGeneralId = await spawnGeneralUtilityAgent()
1719
- const readyResult = await resumeGeneralUtilityAgent(finalGeneralId, 'mark-ready', { head, copilotDown })
1707
+ const readyResult = await runGeneralUtilityTask('mark-ready', { head, copilotDown })
1720
1708
  const readyOutcome = classifyReadyOutcome(readyResult)
1721
1709
  if (readyOutcome.converged) {
1722
1710
  return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote }