claude-dev-env 1.79.0 → 1.80.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 (33) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/hooks/blocking/code_rules_enforcer.py +4 -0
  10. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  11. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  12. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  13. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  14. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  15. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  16. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  17. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  18. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  19. package/package.json +1 -1
  20. package/rules/docstring-prose-matches-implementation.md +1 -0
  21. package/skills/autoconverge/SKILL.md +26 -5
  22. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  23. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  24. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  25. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  26. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  27. package/skills/autoconverge/workflow/converge.mjs +110 -228
  28. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  29. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  30. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  31. package/skills/pr-converge/SKILL.md +28 -2
  32. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  33. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -16,7 +16,7 @@ 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 three times, 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
  }
@@ -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,21 @@ 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, hardening-commit,
204
+ * commit-recover, verify-recover). Each task carries its own edit instructions.
265
205
  * @param {string} task the short task name
266
206
  * @param {object} context task-specific context
267
- * @returns {Promise<object>} the structured output
207
+ * @returns {Promise<object|string>} the structured output, or the transcript string when the 'hardening-commit' resume runs schema-less
268
208
  */
269
- function resumeCodeEditorAgent(agentId, task, context) {
209
+ function runCodeEditorTask(task, context) {
270
210
  const label = `code-editor:${task}`
271
211
  if (task === 'fix-edit') {
272
212
  const findingsBlock = renderFindingsBlock(context.findings)
@@ -287,7 +227,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
287
227
  `- 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
228
  `Always include a one-line summary.` +
289
229
  PRE_COMMIT_GATE_STEP,
290
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
230
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
291
231
  )
292
232
  }
293
233
  if (task === 'conflict-edit') {
@@ -298,7 +238,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
298
238
  `- 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
239
  `- 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
240
  `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 },
241
+ { label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder' },
302
242
  )
303
243
  }
304
244
  if (task === 'repair-edit') {
@@ -318,7 +258,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
318
258
  `- 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
259
  `Always include a one-line summary.` +
320
260
  PRE_COMMIT_GATE_STEP,
321
- { label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
261
+ { label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
322
262
  )
323
263
  }
324
264
  if (task === 'repair-commit') {
@@ -334,7 +274,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
334
274
  `- 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
275
  `- 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
276
  `- 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 },
277
+ { label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
338
278
  )
339
279
  }
340
280
  if (task === 'standards-edit') {
@@ -350,7 +290,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
350
290
  `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
291
  `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
292
  PRE_COMMIT_GATE_STEP,
353
- { label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
293
+ { label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
354
294
  )
355
295
  }
356
296
  if (task === 'hardening-commit') {
@@ -360,7 +300,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
360
300
  `- 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
301
  `- 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
302
  `Return a one-line summary naming the hardening PR URL.`,
363
- { label, phase: 'Converge', agentType: 'clean-coder', resume: agentId },
303
+ { label, phase: 'Converge', agentType: 'clean-coder' },
364
304
  )
365
305
  }
366
306
  if (task === 'commit-recover') {
@@ -374,7 +314,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
374
314
  `- 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
315
  `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
316
  PRE_COMMIT_GATE_STEP,
377
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
317
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
378
318
  )
379
319
  }
380
320
  // verify-recover
@@ -389,34 +329,19 @@ function resumeCodeEditorAgent(agentId, task, context) {
389
329
  `- 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
330
  `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
331
  PRE_COMMIT_GATE_STEP,
392
- { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
332
+ { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
393
333
  )
394
334
  }
395
335
 
396
336
  /**
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
337
+ * Spawn a fresh verifier code-verifier agent for a specific verify task
338
+ * (fix-verify, repair-verify, hardening-verify). The agent makes no edits —
339
+ * verification only and ends its message with a fenced verdict block.
415
340
  * @param {string} task the short task name
416
341
  * @param {object} context task-specific context
417
342
  * @returns {Promise<string>} the verifier transcript carrying the verdict fence
418
343
  */
419
- function resumeVerifierAgent(agentId, task, context) {
344
+ function runVerifierTask(task, context) {
420
345
  const label = `verifier:${task}`
421
346
  if (task === 'fix-verify') {
422
347
  const findingsBlock = renderFindingsBlock(context.findings)
@@ -427,7 +352,7 @@ function resumeVerifierAgent(agentId, task, context) {
427
352
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
428
353
  `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
354
  `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
430
- { label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
355
+ { label, phase: 'Converge', agentType: 'code-verifier' },
431
356
  )
432
357
  }
433
358
  if (task === 'repair-verify') {
@@ -441,7 +366,7 @@ function resumeVerifierAgent(agentId, task, context) {
441
366
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
442
367
  `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
368
  `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
444
- { label, phase: 'Finalize', agentType: 'code-verifier', resume: agentId },
369
+ { label, phase: 'Finalize', agentType: 'code-verifier' },
445
370
  )
446
371
  }
447
372
  return convergeAgent(
@@ -460,37 +385,20 @@ function resumeVerifierAgent(agentId, task, context) {
460
385
  ` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
461
386
  " ```\n" +
462
387
  ` 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 },
388
+ { label, phase: 'Converge', agentType: 'code-verifier' },
464
389
  )
465
390
  }
466
391
 
467
392
  /**
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' },
480
- )
481
- return result?.agentId
482
- }
483
-
484
- /**
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
393
+ * Spawn a fresh general-utility general-purpose agent for one of its two
394
+ * administrative tasks: 'post-clean-audit' posts the terminal CLEAN bugteam
395
+ * review, and 'mark-ready' marks the PR ready and confirms it left draft state.
396
+ * The agent edits no code.
489
397
  * @param {'post-clean-audit'|'mark-ready'} task the short task name
490
398
  * @param {object} context task-specific context
491
399
  * @returns {Promise<object>} the task result
492
400
  */
493
- function resumeGeneralUtilityAgent(agentId, task, context) {
401
+ function runGeneralUtilityTask(task, context) {
494
402
  const label = `general-utility:${task}`
495
403
  if (task === 'post-clean-audit') {
496
404
  return convergeAgent(
@@ -499,7 +407,7 @@ function resumeGeneralUtilityAgent(agentId, task, context) {
499
407
  `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
408
  `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
409
  `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 },
410
+ { label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
503
411
  )
504
412
  }
505
413
  if (task === 'mark-ready') {
@@ -512,35 +420,18 @@ function resumeGeneralUtilityAgent(agentId, task, context) {
512
420
  `1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
513
421
  `2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
514
422
  `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 },
423
+ { label, phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose' },
516
424
  )
517
425
  }
518
- throw new Error(`resumeGeneralUtilityAgent: unknown task ${task}`)
426
+ throw new Error(`runGeneralUtilityTask: unknown task ${task}`)
519
427
  }
520
428
 
521
429
  /**
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
535
- }
536
-
537
- /**
538
- * Resume the convergence-check agent for the convergence check.
539
- * @param {string} agentId the agent id from spawnConvergenceCheckAgent
430
+ * Spawn a fresh convergence-check Explore agent for the convergence check.
540
431
  * @param {object} context carries bugbotDown and copilotDown
541
432
  * @returns {Promise<object>} CONVERGENCE_SCHEMA result
542
433
  */
543
- function resumeConvergenceCheckAgent(agentId, context) {
434
+ function runConvergenceCheck(context) {
544
435
  const label = 'check-convergence'
545
436
  const bugbotDownFlag = context.bugbotDown ? ' --bugbot-down' : ''
546
437
  const copilotDownFlag = context.copilotDown ? ' --copilot-down' : ''
@@ -550,7 +441,7 @@ function resumeConvergenceCheckAgent(agentId, context) {
550
441
  `Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
551
442
  `Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
552
443
  `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 },
444
+ { label, phase: 'Finalize', schema: CONVERGENCE_SCHEMA, agentType: 'Explore' },
554
445
  )
555
446
  }
556
447
 
@@ -1029,7 +920,7 @@ function commitNeedsCodeRecovery(commitResult) {
1029
920
  * resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
1030
921
  * against it interpolates the literal string 'HEAD undefined' into their prompts
1031
922
  * 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'
923
+ * @param {string|null|undefined} resolvedHead the SHA from the git-utility 'resolve-head' task
1033
924
  * @returns {boolean} true only when the SHA is a non-empty string
1034
925
  */
1035
926
  function isResolvedHeadUsable(resolvedHead) {
@@ -1042,7 +933,7 @@ function isResolvedHeadUsable(resolvedHead) {
1042
933
  * not-conflicting so the run proceeds straight to the bug checks rather than
1043
934
  * force-pushing a rebase on a verdict that does not exist — a transient check
1044
935
  * failure must never trigger a destructive rebase.
1045
- * @param {object|null|undefined} mergeState the git-utility agent resume result for 'check-merge-conflicts'
936
+ * @param {object|null|undefined} mergeState the git-utility 'check-merge-conflicts' task result
1046
937
  * @returns {boolean} true only when the check reported conflicting:true
1047
938
  */
1048
939
  function isMergeConflicting(mergeState) {
@@ -1333,10 +1224,10 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
1333
1224
  /**
1334
1225
  * Fix lens: edit (clean-coder, no commit) -> verify (a separate code-verifier
1335
1226
  * 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
1227
+ * commit + push, no edits). The verifier is a distinct agent from the fixer, so
1228
+ * the verdict that gates the commit comes from a different agent than the one
1229
+ * that edits and pushes — the same editor/verifier separation the repair and
1230
+ * conflict paths use, and the separation a workflow code-verifier needs to
1340
1231
  * produce the verdict the verified-commit gate requires, which the SubagentStop
1341
1232
  * minter cannot mint for workflow-spawned agents. When verification fails (or the
1342
1233
  * edit step stalled with no thread to resolve), the commit step is skipped and the
@@ -1347,8 +1238,7 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
1347
1238
  * @returns {Promise<object>} FIX_SCHEMA result
1348
1239
  */
1349
1240
  async function applyFixes(head, findings, sourceLabel) {
1350
- const codeEditorId = await spawnCodeEditorAgent()
1351
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'fix-edit', { head, findings, sourceLabel })
1241
+ const editResult = await runCodeEditorTask('fix-edit', { head, findings, sourceLabel })
1352
1242
  if (editResult?.resolvedWithoutCommit === true && editResult?.edited !== true) {
1353
1243
  return {
1354
1244
  newSha: head,
@@ -1359,9 +1249,7 @@ async function applyFixes(head, findings, sourceLabel) {
1359
1249
  blockerDetail: '',
1360
1250
  }
1361
1251
  }
1362
- const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel)
1363
- const verifierId = await spawnVerifierAgent()
1364
- return fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel)
1252
+ return fixerWithRecovery(head, findings, sourceLabel)
1365
1253
  }
1366
1254
 
1367
1255
  /**
@@ -1370,7 +1258,7 @@ async function applyFixes(head, findings, sourceLabel) {
1370
1258
  * post stops the run with an actionable message rather than re-converging until
1371
1259
  * the iteration cap. Handles a dead post agent (a null result) as not posted.
1372
1260
  * @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
1261
+ * @param {object} auditResult CLEAN_AUDIT_SCHEMA result from the post-clean-audit task, or null when the agent died
1374
1262
  * @returns {string} the blocker message naming the post failure and the unblock path
1375
1263
  */
1376
1264
  function cleanAuditBlocker(head, auditResult) {
@@ -1423,8 +1311,7 @@ function runCopilotGate(head) {
1423
1311
  * @returns {Promise<object>} FIX_SCHEMA result
1424
1312
  */
1425
1313
  async function repairConvergence(head, failures) {
1426
- const codeEditorId = await spawnCodeEditorAgent()
1427
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'repair-edit', { head, failures })
1314
+ const editResult = await runCodeEditorTask('repair-edit', { head, failures })
1428
1315
  const hasPushWork = editResult?.edited === true || editResult?.rebased === true
1429
1316
  if (!hasPushWork) {
1430
1317
  return {
@@ -1436,10 +1323,9 @@ async function repairConvergence(head, failures) {
1436
1323
  blockerDetail: '',
1437
1324
  }
1438
1325
  }
1439
- const verifierId = await spawnVerifierAgent()
1440
1326
  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 }),
1327
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1328
+ runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'repair', objection, attempt }),
1443
1329
  })
1444
1330
  if (!verdictPassed(verifyTranscript)) {
1445
1331
  return {
@@ -1453,9 +1339,9 @@ async function repairConvergence(head, failures) {
1453
1339
  }
1454
1340
  const wasRebased = editResult?.rebased === true
1455
1341
  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 }),
1342
+ runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased }),
1343
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1344
+ runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'repair', blockerDetail: detail, attempt }),
1459
1345
  })
1460
1346
  }
1461
1347
 
@@ -1473,24 +1359,22 @@ async function repairConvergence(head, failures) {
1473
1359
  * @param {string} head PR HEAD SHA before any rebase
1474
1360
  * @returns {Promise<string>} the HEAD SHA after a successful rebase push, or the unchanged head
1475
1361
  */
1476
- async function resolveMergeConflicts(head, gitAgentId) {
1477
- const mergeState = await resumeGitAgent(gitAgentId, 'check-merge-conflicts', head)
1362
+ async function resolveMergeConflicts(head) {
1363
+ const mergeState = await runGitTask('check-merge-conflicts', head)
1478
1364
  if (!isMergeConflicting(mergeState)) return head
1479
1365
  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 })
1366
+ const editResult = await runCodeEditorTask('conflict-edit', { head })
1482
1367
  if (editResult?.rebased !== true) return head
1483
1368
  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
1369
  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 }),
1370
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1371
+ runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'conflict-rebase', objection, attempt }),
1488
1372
  })
1489
1373
  if (!verdictPassed(verifyTranscript)) return head
1490
1374
  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 }),
1375
+ runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased: true }),
1376
+ runVerify: () => runVerifierTask('repair-verify', { head, failures }),
1377
+ runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'conflict-rebase', blockerDetail: detail, attempt }),
1494
1378
  })
1495
1379
  return commitResult?.newSha || head
1496
1380
  }
@@ -1539,19 +1423,17 @@ function standardsDeferralNote(findingsCount, hardeningPrOpened) {
1539
1423
  * @returns {Promise<object>} `{ hardeningPrOpened }` — true only when the hardening PR was opened this round
1540
1424
  */
1541
1425
  async function spawnStandardsFollowUp(head, findings, sourceLabel) {
1542
- const codeEditorId = await spawnCodeEditorAgent()
1543
- const editResult = await resumeCodeEditorAgent(codeEditorId, 'standards-edit', { head, findings, sourceLabel })
1426
+ const editResult = await runCodeEditorTask('standards-edit', { head, findings, sourceLabel })
1544
1427
  if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
1545
1428
  return { hardeningPrOpened: false }
1546
1429
  }
1547
- const verifierId = await spawnVerifierAgent()
1548
- const verifyTranscript = await resumeVerifierAgent(verifierId, 'hardening-verify', {
1430
+ const verifyTranscript = await runVerifierTask('hardening-verify', {
1549
1431
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch,
1550
1432
  })
1551
1433
  if (!verdictPassed(verifyTranscript)) {
1552
1434
  return { hardeningPrOpened: false }
1553
1435
  }
1554
- await resumeCodeEditorAgent(codeEditorId, 'hardening-commit', {
1436
+ await runCodeEditorTask('hardening-commit', {
1555
1437
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
1556
1438
  })
1557
1439
  return { hardeningPrOpened: true }
@@ -1563,23 +1445,21 @@ let rounds = 0
1563
1445
  let iterations = 0
1564
1446
  let blocker = null
1565
1447
  let bugbotDown = input.bugbotDisabled || false
1566
- let copilotDown = false
1448
+ let copilotDown = input.copilotDisabled || false
1567
1449
  let copilotNote = null
1568
1450
  let standardsNote = null
1569
1451
  let reuseNote = null
1570
1452
 
1571
- const gitAgentId = await spawnGitAgent()
1572
-
1573
- const preflightHead = await resumeGitAgent(gitAgentId, 'resolve-head')
1453
+ const preflightHead = await runGitTask('resolve-head')
1574
1454
  if (isResolvedHeadUsable(preflightHead?.sha)) {
1575
- await resolveMergeConflicts(preflightHead.sha, gitAgentId)
1455
+ await resolveMergeConflicts(preflightHead.sha)
1576
1456
  }
1577
1457
 
1578
1458
  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')
1459
+ const reuseHeadResult = await runGitTask('resolve-head')
1580
1460
  const reuseHead = reuseHeadResult?.sha
1581
1461
  if (isResolvedHeadUsable(reuseHead)) {
1582
- await resumeGitAgent(gitAgentId, 'prefetch-main')
1462
+ await runGitTask('prefetch-main')
1583
1463
  const reuse = await runReuseAuditPass(reuseHead)
1584
1464
  const reuseFindings = reuse?.findings || []
1585
1465
  if (reuseFindings.length > 0) {
@@ -1595,19 +1475,17 @@ if (isResolvedHeadUsable(reuseHead)) {
1595
1475
  log('Reuse pass: could not resolve HEAD — proceeding to convergence')
1596
1476
  }
1597
1477
 
1598
- const convergenceId = await spawnConvergenceCheckAgent()
1599
-
1600
1478
  while (iterations < CONFIG.maxIterations) {
1601
1479
  iterations += 1
1602
1480
  if (phase === 'CONVERGE') {
1603
1481
  rounds += 1
1604
- const headResult = await resumeGitAgent(gitAgentId, 'resolve-head')
1482
+ const headResult = await runGitTask('resolve-head')
1605
1483
  head = headResult?.sha
1606
1484
  if (!isResolvedHeadUsable(head)) {
1607
1485
  log(`Round ${rounds}: resolve-head agent returned no SHA — retrying without spawning lenses`)
1608
1486
  continue
1609
1487
  }
1610
- await resumeGitAgent(gitAgentId, 'prefetch-main')
1488
+ await runGitTask('prefetch-main')
1611
1489
  log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
1612
1490
  const lenses = await parallel([
1613
1491
  () => runBugbotLens(head),
@@ -1623,10 +1501,9 @@ while (iterations < CONFIG.maxIterations) {
1623
1501
  const findings = roundOutcome.findings
1624
1502
  if (isStandardsOnlyRound(findings)) {
1625
1503
  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
1504
  const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
1628
1505
  standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
1629
- const auditResult = await resumeGeneralUtilityAgent(generalId, 'post-clean-audit', { head })
1506
+ const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
1630
1507
  if (!auditResult?.posted) {
1631
1508
  blocker = cleanAuditBlocker(head, auditResult)
1632
1509
  break
@@ -1652,8 +1529,7 @@ while (iterations < CONFIG.maxIterations) {
1652
1529
  continue
1653
1530
  }
1654
1531
  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 })
1532
+ const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
1657
1533
  if (!auditResult?.posted) {
1658
1534
  blocker = cleanAuditBlocker(head, auditResult)
1659
1535
  break
@@ -1663,6 +1539,13 @@ while (iterations < CONFIG.maxIterations) {
1663
1539
  }
1664
1540
 
1665
1541
  if (phase === 'COPILOT') {
1542
+ if (input.copilotDisabled) {
1543
+ copilotDown = true
1544
+ 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'
1545
+ 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.')
1546
+ phase = 'FINALIZE'
1547
+ continue
1548
+ }
1666
1549
  const copilot = await runCopilotGate(head)
1667
1550
  const copilotOutcome = classifyCopilotOutcome(copilot)
1668
1551
  copilotDown = resolveCopilotDown(copilotOutcome)
@@ -1708,15 +1591,14 @@ while (iterations < CONFIG.maxIterations) {
1708
1591
  }
1709
1592
 
1710
1593
  if (phase === 'FINALIZE') {
1711
- const convergence = await resumeConvergenceCheckAgent(convergenceId, { bugbotDown, copilotDown })
1594
+ const convergence = await runConvergenceCheck({ bugbotDown, copilotDown })
1712
1595
  const convergenceOutcome = classifyConvergenceOutcome(convergence)
1713
1596
  if (convergenceOutcome.kind === 'retry') {
1714
1597
  log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
1715
1598
  continue
1716
1599
  }
1717
1600
  if (convergenceOutcome.kind === 'ready') {
1718
- const finalGeneralId = await spawnGeneralUtilityAgent()
1719
- const readyResult = await resumeGeneralUtilityAgent(finalGeneralId, 'mark-ready', { head, copilotDown })
1601
+ const readyResult = await runGeneralUtilityTask('mark-ready', { head, copilotDown })
1720
1602
  const readyOutcome = classifyReadyOutcome(readyResult)
1721
1603
  if (readyOutcome.converged) {
1722
1604
  return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote }