claude-dev-env 1.73.0 → 1.75.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.
- package/CLAUDE.md +2 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/block_main_commit.py +14 -0
- package/hooks/blocking/bot_mention_comment_blocker.py +7 -0
- package/hooks/blocking/claude_md_orphan_file_blocker.py +14 -42
- package/hooks/blocking/code_rules_docstrings.py +223 -0
- package/hooks/blocking/code_rules_enforcer.py +16 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +12 -5
- package/hooks/blocking/convergence_gate_blocker.py +17 -3
- package/hooks/blocking/destructive_command_blocker.py +7 -0
- package/hooks/blocking/docstring_rule_gate_count_blocker.py +321 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/gh_body_arg_blocker.py +8 -0
- package/hooks/blocking/gh_pr_author_enforcer.py +7 -0
- package/hooks/blocking/hedging_language_blocker.py +17 -23
- package/hooks/blocking/hook_prose_detector_consistency.py +7 -0
- package/hooks/blocking/intent_only_ending_blocker.py +18 -26
- package/hooks/blocking/md_to_html_blocker.py +10 -2
- package/hooks/blocking/open_questions_in_plans_blocker.py +10 -2
- package/hooks/blocking/package_inventory_stale_blocker.py +398 -0
- package/hooks/blocking/plain_language_blocker.py +6 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +6 -0
- package/hooks/blocking/pr_description_enforcer.py +6 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +5 -6
- package/hooks/blocking/precommit_code_rules_gate.py +10 -1
- package/hooks/blocking/pytest_testpaths_orphan_blocker.py +8 -0
- package/hooks/blocking/question_to_user_enforcer.py +19 -23
- package/hooks/blocking/send_user_file_open_locally_blocker.py +70 -0
- package/hooks/blocking/sensitive_file_protector.py +15 -1
- package/hooks/blocking/session_handoff_blocker.py +15 -23
- package/hooks/blocking/state_description_blocker.py +6 -0
- package/hooks/blocking/subprocess_budget_completeness.py +9 -3
- package/hooks/blocking/tdd_enforcer.py +6 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +207 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +188 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +61 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +203 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_hook_block_logger_coverage.py +53 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +329 -0
- package/hooks/blocking/test_plain_language_blocker.py +36 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +55 -8
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +114 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +42 -0
- package/hooks/blocking/test_state_description_blocker.py +41 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +49 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +4 -19
- package/hooks/blocking/verdict_directory_write_blocker.py +10 -1
- package/hooks/blocking/verified_commit_gate.py +11 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +16 -1
- package/hooks/blocking/windows_rmtree_blocker.py +7 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +10 -5
- package/hooks/blocking/write_existing_file_blocker.py +16 -1
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +8 -1
- package/hooks/hooks_constants/blocking_check_limits.py +13 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/docstring_rule_gate_count_blocker_constants.py +90 -0
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/hook_block_logger.py +59 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +56 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +111 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +3 -2
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +17 -3
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +18 -0
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +44 -0
- package/hooks/hooks_constants/test_hook_block_logger.py +159 -0
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/lifecycle/config_change_guard.py +12 -0
- package/hooks/lifecycle/test_config_change_guard.py +23 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/hook_format_validator.py +13 -0
- package/hooks/validation/mypy_validator.py +30 -1
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_hook_format_validator.py +64 -0
- package/hooks/validation/test_mypy_validator.py +23 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/rules/package-inventory-stale-entry.md +24 -0
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/autoconverge/SKILL.md +21 -1
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +398 -116
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +36 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +599 -606
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
|
@@ -33,7 +33,8 @@ const HEADLESS_SAFETY_PREAMBLE =
|
|
|
33
33
|
'HEADLESS RUN — you run unattended: no human can answer a permission or confirmation prompt, and any such prompt stalls the entire convergence run. The destructive_command_blocker hook matches dangerous patterns (rm -rf, git reset --hard, dd, mkfs, chmod -R, fork bombs) as raw text anywhere in a Bash command, with no quote-awareness — so a destructive string stalls you even when it is only data you never execute. Therefore:\n' +
|
|
34
34
|
'- Never place a destructive-command literal inside a Bash command — not in echo, not in a heredoc, and not as an argument to python -c, node -e, or awk. To exercise or verify destructive_command_blocker (or any hook) behavior, run the committed test suite, e.g. python -m pytest <test_file>, which passes the command strings as in-language data rather than as a shell command.\n' +
|
|
35
35
|
'- When a commit message, or a PR / issue / review-comment body, must describe destructive-command behavior, write that text to a file and pass it by path (git commit -F <file>, gh ... --body-file <file>); never inline it with git commit -m or gh ... -b, where the literal lands in the Bash command and stalls you.\n' +
|
|
36
|
-
'- Keep scratch files and cleanup inside the OS temp dir
|
|
36
|
+
'- Keep scratch files and cleanup inside the OS temp dir; never target a repository or worktree path.\n' +
|
|
37
|
+
'- rm shape rules — the hook grants several rm auto-allow paths. The simplest one accepts a standalone Bash call whose target resolves inside the ephemeral namespace (/tmp, /temp, the OS temp root, or the run worktree); a compound path accepts an rm joined with benign reporting segments when every rm target is an absolute ephemeral path. Both of those paths fail closed on $(...) command substitution and on backtick subshells. The compound path additionally fails closed on any $ in the target — including $CLAUDE_JOB_DIR. The standalone path declines a $-bearing target only when the literal path is not already under an ephemeral root, so it does not by itself stop a $VAR that expands inside an ephemeral root. A third, broad path matches only when the command itself declares an ephemeral working directory (it cds into one, or runs under one): that cwd-scoped path resolves the target against the declared cwd, fails closed on $(...) , backticks, and unknown variables, and resolves the known temporary variables TEMP, TMP, TMPDIR, and CLAUDE_JOB_DIR to the OS temp root, so under that declared ephemeral cwd a bare $CLAUDE_JOB_DIR/tmp/<name> target and a relative target after a cd are auto-allowed. Even so, prefer a Python helper for any cleanup whose path is variable-built or whose setup/teardown spans multiple steps: author the helper file and run it as python <file>.py, which keeps every destructive literal out of a Bash command string entirely and never depends on which auto-allow path matches.\n' +
|
|
37
38
|
'- If a step appears to require a real destructive command, use a non-destructive equivalent or report it as a blocker instead of running it.\n\n'
|
|
38
39
|
|
|
39
40
|
let activeRepoPath = null
|
|
@@ -68,8 +69,507 @@ const worktreeDirective = (repoPath) =>
|
|
|
68
69
|
* @param {object} options the agent() options (label, phase, schema, agentType, model)
|
|
69
70
|
* @returns {Promise<*>} the agent() result
|
|
70
71
|
*/
|
|
71
|
-
const convergeAgent = (prompt, options) =>
|
|
72
|
-
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resume the git/utility agent for a specific task.
|
|
97
|
+
* @param {string} agentId the agent id from spawnGitAgent
|
|
98
|
+
* @param {string} task the short task name
|
|
99
|
+
* @param {string} head optional HEAD SHA for conflict checks
|
|
100
|
+
* @returns {Promise<object>} the structured output
|
|
101
|
+
*/
|
|
102
|
+
function resumeGitAgent(agentId, task, head) {
|
|
103
|
+
if (task === 'resolve-head') {
|
|
104
|
+
return convergeAgent(
|
|
105
|
+
`Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
|
|
106
|
+
`gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
|
|
107
|
+
`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 },
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
if (task === 'prefetch-main') {
|
|
112
|
+
return convergeAgent(
|
|
113
|
+
`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
|
+
`git fetch origin main\n` +
|
|
115
|
+
`Do not edit, commit, push, rebase, or modify any files — fetch only.`,
|
|
116
|
+
{ label: 'git-utility', phase: 'Converge', agentType: 'Explore', resume: agentId },
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return convergeAgent(
|
|
120
|
+
`Report whether ${prCoordinates} (HEAD ${head}) has merge conflicts with its base branch. Do not edit, commit, push, or rebase — read only.\n\n` +
|
|
121
|
+
`GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
|
|
122
|
+
` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
|
|
123
|
+
`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
|
+
`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 },
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Spawn the fixer clean-coder agent for one fix batch, establishing its role and
|
|
131
|
+
* the PR coordinates so each later resume (verify-commit, commit, and recovery)
|
|
132
|
+
* continues the same session. The spawn makes no edits — the verify-commit resume
|
|
133
|
+
* is the first step that touches the working tree. Returns the runtime agent id
|
|
134
|
+
* so the resume calls target the live session; a runtime without resume support
|
|
135
|
+
* returns no agent id, and each resume falls back to a fresh spawn.
|
|
136
|
+
* @param {string} head PR HEAD SHA
|
|
137
|
+
* @param {Array<object>} findings the findings to fix
|
|
138
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
139
|
+
* @param {string} task initial task name
|
|
140
|
+
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
141
|
+
*/
|
|
142
|
+
async function spawnFixerAgent(head, findings, sourceLabel, task) {
|
|
143
|
+
const result = await convergeAgent(
|
|
144
|
+
`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 (the first is ${task}): verify the working-tree fixes, commit and push them, and recover when a verify objection or a commit-gate block needs another edit. 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 verify-commit instructions. Reply READY.`,
|
|
145
|
+
{ label: `fixer:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
|
|
146
|
+
)
|
|
147
|
+
return result?.agentId
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resume the fixer agent for verify-commit or recovery.
|
|
152
|
+
* @param {string} agentId the agent id from spawnFixerAgent
|
|
153
|
+
* @param {string} task the short task name
|
|
154
|
+
* @param {object} context task-specific context
|
|
155
|
+
* @returns {Promise<object>} the structured output
|
|
156
|
+
*/
|
|
157
|
+
function resumeFixerAgent(agentId, task, context) {
|
|
158
|
+
const label = `fixer:${context.sourceLabel}`
|
|
159
|
+
if (task === 'verify-commit') {
|
|
160
|
+
const findingsBlock = renderFindingsBlock(context.findings)
|
|
161
|
+
return convergeAgent(
|
|
162
|
+
`You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
163
|
+
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
164
|
+
`Steps:\n` +
|
|
165
|
+
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
166
|
+
`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` +
|
|
167
|
+
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
168
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (task === 'commit') {
|
|
172
|
+
return convergeAgent(
|
|
173
|
+
`You are the COMMIT step for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
174
|
+
`Rules:\n` +
|
|
175
|
+
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the commit would be blocked. Do not run a formatter, do not touch a test, do not re-fix anything — only commit and push what is already there.\n` +
|
|
176
|
+
`- Make ONE commit for all the working-tree fixes, then push to the PR branch.\n\n` +
|
|
177
|
+
`Return values:\n` +
|
|
178
|
+
`- 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` +
|
|
179
|
+
`- 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` +
|
|
180
|
+
`- 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.`,
|
|
181
|
+
{ label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
const objection = context.objection || VERIFY_OBJECTION_FALLBACK
|
|
185
|
+
const attempt = context.attempt || 1
|
|
186
|
+
return convergeAgent(
|
|
187
|
+
`You are the VERIFY-RECOVERY fixer (attempt ${attempt}) for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The verify step rejected the working-tree fixes; its verdict named what is still unresolved. A separate verify step then a separate commit step run after you.\n\n` +
|
|
188
|
+
`The verify step's objections:\n${objection}\n\n` +
|
|
189
|
+
`Rules:\n` +
|
|
190
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
|
|
191
|
+
`- Address every objection above test-first (failing test, then minimum code to pass) per CODE_RULES, so each named concern is genuinely resolved the way the verdict requires. Do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
192
|
+
`- 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` +
|
|
193
|
+
`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.` +
|
|
194
|
+
PRE_COMMIT_GATE_STEP,
|
|
195
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Joined fixer recovery loop: resume the fixer agent for verify+commit, extract the verdict,
|
|
201
|
+
* and recover when the verdict fails or the commit is blocked.
|
|
202
|
+
* @param {string} fixerAgentId the fixer agent id from spawnFixerAgent
|
|
203
|
+
* @param {string} head PR HEAD SHA
|
|
204
|
+
* @param {Array<object>} findings the findings to fix
|
|
205
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
206
|
+
* @returns {Promise<object>} FIX_SCHEMA result
|
|
207
|
+
*/
|
|
208
|
+
async function fixerWithRecovery(fixerAgentId, head, findings, sourceLabel) {
|
|
209
|
+
const verifyTranscript = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
210
|
+
const verdict = extractVerdict(verifyTranscript)
|
|
211
|
+
if (verdict && verdict.all_pass === true) {
|
|
212
|
+
const commitResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
213
|
+
if (commitNeedsCodeRecovery(commitResult)) {
|
|
214
|
+
let attempt = 0
|
|
215
|
+
let recoveryResult = commitResult
|
|
216
|
+
while (commitNeedsCodeRecovery(recoveryResult) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
217
|
+
attempt += 1
|
|
218
|
+
const recoverEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
219
|
+
head, findings, sourceLabel, objection: recoveryResult.blockerDetail, attempt,
|
|
220
|
+
})
|
|
221
|
+
if (recoverEdit?.edited !== true) break
|
|
222
|
+
const reVerify = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
223
|
+
const reVerdict = extractVerdict(reVerify)
|
|
224
|
+
if (!reVerdict || reVerdict.all_pass !== true) break
|
|
225
|
+
recoveryResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
226
|
+
}
|
|
227
|
+
return recoveryResult
|
|
228
|
+
}
|
|
229
|
+
return commitResult
|
|
230
|
+
}
|
|
231
|
+
let attempt = 0
|
|
232
|
+
let lastTranscript = verifyTranscript
|
|
233
|
+
while ((!verdict || verdict.all_pass !== true) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
234
|
+
attempt += 1
|
|
235
|
+
const objection = extractVerifyObjection(lastTranscript)
|
|
236
|
+
const recoverEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
237
|
+
head, findings, sourceLabel, objection, attempt,
|
|
238
|
+
})
|
|
239
|
+
if (recoverEdit?.edited !== true) break
|
|
240
|
+
lastTranscript = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
241
|
+
const freshVerdict = extractVerdict(lastTranscript)
|
|
242
|
+
if (freshVerdict && freshVerdict.all_pass === true) {
|
|
243
|
+
const commitResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
244
|
+
if (commitNeedsCodeRecovery(commitResult)) {
|
|
245
|
+
let commitAttempt = 0
|
|
246
|
+
let commitRecovery = commitResult
|
|
247
|
+
while (commitNeedsCodeRecovery(commitRecovery) && commitAttempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
248
|
+
commitAttempt += 1
|
|
249
|
+
const commitEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
250
|
+
head, findings, sourceLabel, objection: commitRecovery.blockerDetail, attempt: commitAttempt,
|
|
251
|
+
})
|
|
252
|
+
if (commitEdit?.edited !== true) break
|
|
253
|
+
const reVerify2 = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
254
|
+
const reVerdict2 = extractVerdict(reVerify2)
|
|
255
|
+
if (!reVerdict2 || reVerdict2.all_pass !== true) break
|
|
256
|
+
commitRecovery = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
257
|
+
}
|
|
258
|
+
return commitRecovery
|
|
259
|
+
}
|
|
260
|
+
return commitResult
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
newSha: head,
|
|
265
|
+
pushed: false,
|
|
266
|
+
resolvedWithoutCommit: false,
|
|
267
|
+
summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
|
|
268
|
+
blockedNeedingEdit: false,
|
|
269
|
+
blockerDetail: '',
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Spawn the code-editor clean-coder agent once per converge round, establishing
|
|
275
|
+
* its role so each later resume (fix-edit, conflict-edit, repair-edit,
|
|
276
|
+
* repair-commit, standards-edit, hardening-commit, commit-recover, verify-recover)
|
|
277
|
+
* continues the same session. The spawn makes no edits — each resume carries the
|
|
278
|
+
* task-specific edit instructions. Returns the runtime agent id so the resume
|
|
279
|
+
* calls target the live session; a runtime without resume support returns no
|
|
280
|
+
* agent id, and each resume falls back to a fresh spawn.
|
|
281
|
+
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
282
|
+
*/
|
|
283
|
+
async function spawnCodeEditorAgent() {
|
|
284
|
+
const result = await convergeAgent(
|
|
285
|
+
`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.`,
|
|
286
|
+
{ label: 'code-editor', phase: 'Converge', agentType: 'clean-coder' },
|
|
287
|
+
)
|
|
288
|
+
return result?.agentId
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resume the code-editor agent for a specific edit task.
|
|
293
|
+
* @param {string} agentId the agent id from spawnCodeEditorAgent
|
|
294
|
+
* @param {string} task the short task name
|
|
295
|
+
* @param {object} context task-specific context
|
|
296
|
+
* @returns {Promise<object>} the structured output
|
|
297
|
+
*/
|
|
298
|
+
function resumeCodeEditorAgent(agentId, task, context) {
|
|
299
|
+
const label = `code-editor:${task}`
|
|
300
|
+
if (task === 'fix-edit') {
|
|
301
|
+
const findingsBlock = renderFindingsBlock(context.findings)
|
|
302
|
+
const threadIds = context.findings
|
|
303
|
+
.flatMap((each) => collectFindingThreadIds(each))
|
|
304
|
+
.filter((each) => typeof each === 'number')
|
|
305
|
+
return convergeAgent(
|
|
306
|
+
`You are the EDIT step fixing ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
307
|
+
`Findings:\n${findingsBlock}\n\n` +
|
|
308
|
+
`Rules:\n` +
|
|
309
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with no unrelated edits before you start.\n` +
|
|
310
|
+
`- Fix every finding test-first (failing test, then minimum code to pass) per CODE_RULES. Verify each concern against current code; a finding whose concern no longer applies needs no code change but still needs its thread resolved.\n` +
|
|
311
|
+
`- Leave all fixes in the working tree. Do NOT commit and do NOT push — the commit step does that after verification. Committing or pushing here would change the surface the verifier binds to.\n` +
|
|
312
|
+
`- For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply with python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "<what changed>". Then resolve the PR review thread by thread node id (PRRT_...): look up the thread id for that comment via GraphQL (match on comment databaseId == <id> in the pull request's reviewThreads), then call the github MCP pull_request_review_write method=resolve_thread with threadId=<PRRT_...> (not the numeric comment id), or run the resolveReviewThread GraphQL mutation with the same threadId.\n` +
|
|
313
|
+
`- Findings with replyToCommentId null are in-memory audit findings: fix them, no reply needed.\n\n` +
|
|
314
|
+
`Return values:\n` +
|
|
315
|
+
`- When you edited code to fix at least one finding: edited=true, resolvedWithoutCommit=false.\n` +
|
|
316
|
+
`- 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` +
|
|
317
|
+
`Always include a one-line summary.` +
|
|
318
|
+
PRE_COMMIT_GATE_STEP,
|
|
319
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
if (task === 'conflict-edit') {
|
|
323
|
+
return convergeAgent(
|
|
324
|
+
`You are the EDIT step resolving merge conflicts for ${prCoordinates}, HEAD ${context.head}, before the bug checks run. The PR branch conflicts with origin/main. A separate verify step then a separate commit step run after you.\n\n` +
|
|
325
|
+
`Rules:\n` +
|
|
326
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with no unrelated edits before you start.\n` +
|
|
327
|
+
`- 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` +
|
|
328
|
+
`- 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` +
|
|
329
|
+
`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.`,
|
|
330
|
+
{ label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
if (task === 'repair-edit') {
|
|
334
|
+
const failureBlock = context.failures.length
|
|
335
|
+
? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
336
|
+
: 'none reported'
|
|
337
|
+
return convergeAgent(
|
|
338
|
+
`You are the EDIT step repairing the convergence gates that failed for ${prCoordinates} on HEAD ${context.head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
339
|
+
`Failing gates:\n${failureBlock}\n\n` +
|
|
340
|
+
`Address only the failing gates, and make NO commit and NO push — leave every code change in the working tree (a rebase necessarily creates local commits, which is fine; just do not push them):\n` +
|
|
341
|
+
`- Unresolved bot review threads: fetch the threads where isResolved is false (gh api graphql, or the github MCP pull_request_read get_review_comments), then keep only the bot-authored ones — a thread whose root comment author login contains "cursor", "claude", or "copilot" (case-insensitive substring). Explicitly skip every human reviewer thread; the convergence gate counts only unresolved bot threads, so touching a human thread is out of scope. For each bot thread, verify the concern against current code; if it still applies, fix it test-first in the working tree and leave the fix uncommitted; either way post an inline reply and resolve the thread by its PRRT_ node id (GraphQL lookup matching the comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n` +
|
|
342
|
+
`- PR not mergeable: rebase onto origin/main FIRST, before applying any uncommitted bot-thread fix, so the rebase runs on a clean tree (git fetch origin main; git rebase origin/main; resolve conflicts). Do NOT force-push — the commit step does that after verification.\n` +
|
|
343
|
+
`- A dirty bot review or a still-pending requested reviewer: leave it; the next round re-runs that reviewer.\n\n` +
|
|
344
|
+
`Return values:\n` +
|
|
345
|
+
`- edited=true when you changed code in the working tree to fix a bot-thread concern.\n` +
|
|
346
|
+
`- rebased=true when you rebased the branch onto origin/main.\n` +
|
|
347
|
+
`- 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` +
|
|
348
|
+
`Always include a one-line summary.` +
|
|
349
|
+
PRE_COMMIT_GATE_STEP,
|
|
350
|
+
{ label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
if (task === 'repair-commit') {
|
|
354
|
+
const pushInstruction = context.wasRebased
|
|
355
|
+
? 'The edit step rebased the branch, so push with git push --force-with-lease.'
|
|
356
|
+
: 'Push to the PR branch with a plain git push.'
|
|
357
|
+
return convergeAgent(
|
|
358
|
+
`You are the COMMIT step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
359
|
+
`Rules:\n` +
|
|
360
|
+
`- 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. Do not run a formatter, do not re-fix anything — only commit and push what is already there.\n` +
|
|
361
|
+
`- Commit any uncommitted bot-thread fix in ONE commit (skip the commit when the working tree carries only already-committed rebase results). ${pushInstruction}\n\n` +
|
|
362
|
+
`Return values:\n` +
|
|
363
|
+
`- 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` +
|
|
364
|
+
`- 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` +
|
|
365
|
+
`- 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.`,
|
|
366
|
+
{ label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
if (task === 'standards-edit') {
|
|
370
|
+
const findingsBlock = renderFindingsBlock(context.findings)
|
|
371
|
+
const threadIds = context.findings
|
|
372
|
+
.flatMap((each) => collectFindingThreadIds(each))
|
|
373
|
+
.filter((each) => typeof each === 'number')
|
|
374
|
+
return convergeAgent(
|
|
375
|
+
`You are the EDIT step deferring a code-standard-only round on ${prCoordinates}, HEAD ${context.head} (${context.sourceLabel}). The round surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact); the run treats it as passed and defers the fixes to follow-up work, which you now stage. A separate verify step then a separate commit step open the hardening PR after you. Do NOT commit or push to the PR's own branch.\n\n` +
|
|
376
|
+
`Findings:\n${findingsBlock}\n\n` +
|
|
377
|
+
`1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR. Capture the issue URL.\n` +
|
|
378
|
+
`2. Stage the environment-hardening change: in the Claude environment config repo (the repo owning ~/.claude hooks and rules — JonEcho/llm-settings for hooks, jl-cmd/claude-code-config for rules/skills; pick whichever owns the surface that would block these violation classes), find or clone a local checkout, fetch origin, and create a branch off origin/main. Edit the hooks/rules in that checkout's WORKING TREE so each violation class found here is blocked at Write/Edit time, before code is written. Do NOT commit and do NOT push — the commit step does that after the verify step binds a verdict to the working tree. Return the checkout's absolute path in hardeningRepoPath, the branch name in hardeningBranch, and set hardeningEdited=true. When no hardening is feasible for these classes, leave hardeningRepoPath and hardeningBranch empty and hardeningEdited=false; the follow-up issue still stands.\n` +
|
|
379
|
+
`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` +
|
|
380
|
+
`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.` +
|
|
381
|
+
PRE_COMMIT_GATE_STEP,
|
|
382
|
+
{ label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
if (task === 'hardening-commit') {
|
|
386
|
+
return convergeAgent(
|
|
387
|
+
`You are the COMMIT step opening the environment-hardening PR (${context.sourceLabel}) for the change staged in ${context.hardeningRepoPath} on branch ${context.hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree. Do NOT touch the PR's own branch.\n\n` +
|
|
388
|
+
`Rules:\n` +
|
|
389
|
+
`- 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` +
|
|
390
|
+
`- 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` +
|
|
391
|
+
`Return a one-line summary naming the hardening PR URL.`,
|
|
392
|
+
{ label, phase: 'Converge', agentType: 'clean-coder', resume: agentId },
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
if (task === 'commit-recover') {
|
|
396
|
+
const attempt = context.attempt || 1
|
|
397
|
+
return convergeAgent(
|
|
398
|
+
`You are the COMMIT-RECOVERY fixer (attempt ${attempt}) for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. A prior commit step was blocked by a commit-time hook or gate that requires a code change. A separate verify step then a separate commit step run after you.\n\n` +
|
|
399
|
+
`The blocking hook or gate said:\n${context.blockerDetail}\n\n` +
|
|
400
|
+
`Rules:\n` +
|
|
401
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
|
|
402
|
+
`- Fix ONLY the violation named above, test-first (failing test, then minimum code to pass) per CODE_RULES. Do not re-open the original findings, and do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
403
|
+
`- 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` +
|
|
404
|
+
`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.` +
|
|
405
|
+
PRE_COMMIT_GATE_STEP,
|
|
406
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
// verify-recover
|
|
410
|
+
const attempt = context.attempt || 1
|
|
411
|
+
const objection = context.objection || VERIFY_OBJECTION_FALLBACK
|
|
412
|
+
return convergeAgent(
|
|
413
|
+
`You are the VERIFY-RECOVERY fixer (attempt ${attempt}) for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The verify step rejected the working-tree fixes; its verdict named what is still unresolved. A separate verify step then a separate commit step run after you.\n\n` +
|
|
414
|
+
`The verify step's objections:\n${objection}\n\n` +
|
|
415
|
+
`Rules:\n` +
|
|
416
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
|
|
417
|
+
`- Address every objection above test-first (failing test, then minimum code to pass) per CODE_RULES, so each named concern is genuinely resolved the way the verdict requires. Do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
418
|
+
`- 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` +
|
|
419
|
+
`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.` +
|
|
420
|
+
PRE_COMMIT_GATE_STEP,
|
|
421
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Spawn the verifier code-verifier agent once per converge round, establishing
|
|
427
|
+
* its role so each later resume (repair-verify, hardening-verify) continues the
|
|
428
|
+
* same session. The spawn makes no edits — verification only. Returns the runtime
|
|
429
|
+
* agent id so the resume calls target the live session; a runtime without resume
|
|
430
|
+
* support returns no agent id, and each resume falls back to a fresh spawn.
|
|
431
|
+
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
432
|
+
*/
|
|
433
|
+
async function spawnVerifierAgent() {
|
|
434
|
+
const result = await convergeAgent(
|
|
435
|
+
`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.`,
|
|
436
|
+
{ label: 'verifier', phase: 'Converge', agentType: 'code-verifier' },
|
|
437
|
+
)
|
|
438
|
+
return result?.agentId
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Resume the verifier agent for a specific verify task.
|
|
443
|
+
* @param {string} agentId the agent id from spawnVerifierAgent
|
|
444
|
+
* @param {string} task the short task name
|
|
445
|
+
* @param {object} context task-specific context
|
|
446
|
+
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
447
|
+
*/
|
|
448
|
+
function resumeVerifierAgent(agentId, task, context) {
|
|
449
|
+
const label = `verifier:${task}`
|
|
450
|
+
if (task === 'repair-verify') {
|
|
451
|
+
const failureBlock = context.failures.length
|
|
452
|
+
? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
453
|
+
: 'none reported'
|
|
454
|
+
return convergeAgent(
|
|
455
|
+
`You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
456
|
+
`Concerns the working-tree repair must resolve (the gates the convergence check flagged):\n${failureBlock}\n\n` +
|
|
457
|
+
`Steps:\n` +
|
|
458
|
+
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
459
|
+
`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` +
|
|
460
|
+
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
461
|
+
{ label, phase: 'Finalize', agentType: 'code-verifier', resume: agentId },
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
return convergeAgent(
|
|
465
|
+
`You are the VERIFY step for an environment-hardening change (${context.sourceLabel}) staged in the working tree of ${context.hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
466
|
+
`Concern the working-tree change must resolve: the edited hooks/rules block the code-standard violation classes from the deferred round at Write/Edit time, and a hook change carries a passing test per CODE_RULES.\n\n` +
|
|
467
|
+
`Steps:\n` +
|
|
468
|
+
`1. cd into ${context.hardeningRepoPath}, then resolve its repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
469
|
+
`2. Verify the uncommitted working-tree change in REPO: read the diff (git diff) and run the hook/rule tests in that repo, confirming each violation class is now blocked.\n` +
|
|
470
|
+
`3. Compute the binding hash for the live surface:\n` +
|
|
471
|
+
` The hardening branch is: ${context.hardeningBranch}\n` +
|
|
472
|
+
` Run exactly:\n` +
|
|
473
|
+
` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "${context.hardeningBranch}"\n` +
|
|
474
|
+
` (substitute the REPO path you resolved for the script path). That prints a single 64-char hex hash on stdout — capture it.\n` +
|
|
475
|
+
` Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
|
|
476
|
+
" ```verdict\n" +
|
|
477
|
+
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
478
|
+
" ```\n" +
|
|
479
|
+
` 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.`,
|
|
480
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Spawn the general-utility general-purpose agent once per converge round,
|
|
486
|
+
* establishing its role so each later resume (post-clean-audit, mark-ready)
|
|
487
|
+
* continues the same session. The spawn edits no code.
|
|
488
|
+
* Returns the runtime agent id so the resume calls target the live session; a
|
|
489
|
+
* runtime without resume support returns no agent id, and each resume falls back
|
|
490
|
+
* to a fresh spawn.
|
|
491
|
+
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
492
|
+
*/
|
|
493
|
+
async function spawnGeneralUtilityAgent() {
|
|
494
|
+
const result = await convergeAgent(
|
|
495
|
+
`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.`,
|
|
496
|
+
{ label: 'general-utility', phase: 'Converge', agentType: 'general-purpose' },
|
|
497
|
+
)
|
|
498
|
+
return result?.agentId
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Resume the general-utility agent for one of its two administrative tasks:
|
|
503
|
+
* 'post-clean-audit' posts the terminal CLEAN bugteam review, and 'mark-ready'
|
|
504
|
+
* marks the PR ready and confirms it left draft state.
|
|
505
|
+
* @param {string} agentId the agent id from spawnGeneralUtilityAgent
|
|
506
|
+
* @param {'post-clean-audit'|'mark-ready'} task the short task name
|
|
507
|
+
* @param {object} context task-specific context
|
|
508
|
+
* @returns {Promise<object>} the task result
|
|
509
|
+
*/
|
|
510
|
+
function resumeGeneralUtilityAgent(agentId, task, context) {
|
|
511
|
+
const label = `general-utility:${task}`
|
|
512
|
+
if (task === 'post-clean-audit') {
|
|
513
|
+
return convergeAgent(
|
|
514
|
+
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${context.head}. All review lenses are clean on this HEAD.\n\n` +
|
|
515
|
+
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
516
|
+
`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` +
|
|
517
|
+
`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` +
|
|
518
|
+
`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.`,
|
|
519
|
+
{ label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose', resume: agentId },
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
if (task === 'mark-ready') {
|
|
523
|
+
const copilotOptOut = context.copilotDown
|
|
524
|
+
? `0. Copilot is down this run, so opt the independent mark-ready blocker hook out of the Copilot gate before step 1. Export the token in the same shell session as step 1 so the hook's convergence re-check inherits it:\n bash: export CLAUDE_REVIEWS_DISABLED="copilot" (PowerShell: $env:CLAUDE_REVIEWS_DISABLED = "copilot")\n`
|
|
525
|
+
: ''
|
|
526
|
+
return convergeAgent(
|
|
527
|
+
`All convergence gates pass for ${prCoordinates} on HEAD ${context.head}. Mark the PR ready, then confirm it left draft state. Do not edit code.\n\n` +
|
|
528
|
+
copilotOptOut +
|
|
529
|
+
`1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
530
|
+
`2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
531
|
+
`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}.`,
|
|
532
|
+
{ label, phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose', resume: agentId },
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
throw new Error(`resumeGeneralUtilityAgent: unknown task ${task}`)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Spawn the convergence-check Explore agent once before the converge loop,
|
|
540
|
+
* establishing its role so each per-round resume runs the convergence gate in the
|
|
541
|
+
* same session. The spawn edits no code. Returns the runtime agent id so the
|
|
542
|
+
* resume calls target the live session; a runtime without resume support returns
|
|
543
|
+
* no agent id, and each resume falls back to a fresh spawn.
|
|
544
|
+
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
545
|
+
*/
|
|
546
|
+
async function spawnConvergenceCheckAgent() {
|
|
547
|
+
const result = await convergeAgent(
|
|
548
|
+
`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.`,
|
|
549
|
+
{ label: 'convergence-check', phase: 'Converge', agentType: 'Explore' },
|
|
550
|
+
)
|
|
551
|
+
return result?.agentId
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Resume the convergence-check agent for the convergence check.
|
|
556
|
+
* @param {string} agentId the agent id from spawnConvergenceCheckAgent
|
|
557
|
+
* @param {object} context carries bugbotDown and copilotDown
|
|
558
|
+
* @returns {Promise<object>} CONVERGENCE_SCHEMA result
|
|
559
|
+
*/
|
|
560
|
+
function resumeConvergenceCheckAgent(agentId, context) {
|
|
561
|
+
const label = 'check-convergence'
|
|
562
|
+
const bugbotDownFlag = context.bugbotDown ? ' --bugbot-down' : ''
|
|
563
|
+
const copilotDownFlag = context.copilotDown ? ' --copilot-down' : ''
|
|
564
|
+
return convergeAgent(
|
|
565
|
+
`Run the convergence gate for ${prCoordinates} and report the result. Do not edit code.\n\n` +
|
|
566
|
+
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n\n` +
|
|
567
|
+
`Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
|
|
568
|
+
`Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
|
|
569
|
+
`Exit 2 -> retry once; if it still errors, return {pass:false, failures:["check_convergence gh error"]}.`,
|
|
570
|
+
{ label, phase: 'Finalize', schema: CONVERGENCE_SCHEMA, agentType: 'Explore', resume: agentId },
|
|
571
|
+
)
|
|
572
|
+
}
|
|
73
573
|
|
|
74
574
|
const PRE_COMMIT_GATE_STEP =
|
|
75
575
|
`\n\nFINAL STEP — pre-commit gate check (do NOT commit): before your turn ends, prove your working-tree changes CAN be committed by dry-running the CODE_RULES commit gate that gates git commit (precommit_code_rules_gate). From inside the checkout that holds your changes, resolve its root with git rev-parse --show-toplevel, stage your changes with git add -A, then run exactly:\n` +
|
|
@@ -223,31 +723,6 @@ function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
|
|
|
223
723
|
)
|
|
224
724
|
}
|
|
225
725
|
|
|
226
|
-
const CONVERGENCE_SUMMARY_SCHEMA = {
|
|
227
|
-
type: 'object',
|
|
228
|
-
additionalProperties: false,
|
|
229
|
-
properties: {
|
|
230
|
-
verdictLine: { type: 'string', description: 'one factual BLUF sentence: converged?, distinct issue-class count, all fixed or deferred. No hedging words.' },
|
|
231
|
-
issueClasses: {
|
|
232
|
-
type: 'array',
|
|
233
|
-
items: {
|
|
234
|
-
type: 'object',
|
|
235
|
-
additionalProperties: false,
|
|
236
|
-
properties: {
|
|
237
|
-
plainName: { type: 'string', description: 'everyday-language name of the issue class — no tool tokens, rule ids, file paths, line numbers, severity codes (P0/P1/P2), or bot names' },
|
|
238
|
-
count: { type: 'integer', description: 'number of raw findings grouped into this class' },
|
|
239
|
-
severity: { type: 'string', enum: ['P0', 'P1', 'P2'], description: 'most severe among the class' },
|
|
240
|
-
category: { type: 'string', enum: ['bug', 'code-standard'] },
|
|
241
|
-
status: { type: 'string', enum: ['fixed', 'deferred'] },
|
|
242
|
-
whatItWas: { type: 'string', description: 'at most 2 sentences, plain language, what the problem was' },
|
|
243
|
-
},
|
|
244
|
-
required: ['plainName', 'count', 'severity', 'category', 'status', 'whatItWas'],
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
required: ['verdictLine', 'issueClasses'],
|
|
249
|
-
}
|
|
250
|
-
|
|
251
726
|
const CONVERGENCE_SCHEMA = {
|
|
252
727
|
type: 'object',
|
|
253
728
|
additionalProperties: false,
|
|
@@ -432,32 +907,49 @@ function normalizeShaForComparison(sha) {
|
|
|
432
907
|
}
|
|
433
908
|
|
|
434
909
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* step is skipped and the round reads as not-progressed.
|
|
441
|
-
* @param {string|null|undefined} verifyTranscript the verifier's transcript text
|
|
442
|
-
* @returns {boolean} true only when the last verdict fence reports all_pass true
|
|
910
|
+
* Parse the LAST ```verdict ...``` fenced JSON block from a transcript.
|
|
911
|
+
* Guards against non-string input, iterates all fence matches for the last one,
|
|
912
|
+
* parses the JSON, and returns the object or null on any failure.
|
|
913
|
+
* @param {string|null|undefined} transcript the agent transcript text
|
|
914
|
+
* @returns {object|null} the parsed verdict object, or null when absent or malformed
|
|
443
915
|
*/
|
|
444
|
-
function
|
|
445
|
-
if (typeof
|
|
916
|
+
function parseLastVerdictFence(transcript) {
|
|
917
|
+
if (typeof transcript !== 'string') return null
|
|
446
918
|
const fencePattern = /```verdict\s*\n([\s\S]*?)```/g
|
|
447
919
|
let lastFenceBody = null
|
|
448
920
|
let eachMatch
|
|
449
|
-
while ((eachMatch = fencePattern.exec(
|
|
921
|
+
while ((eachMatch = fencePattern.exec(transcript)) !== null) {
|
|
450
922
|
lastFenceBody = eachMatch[1]
|
|
451
923
|
}
|
|
452
|
-
if (lastFenceBody === null) return
|
|
924
|
+
if (lastFenceBody === null) return null
|
|
453
925
|
try {
|
|
454
|
-
|
|
455
|
-
return verdictRecord != null && verdictRecord.all_pass === true
|
|
926
|
+
return JSON.parse(lastFenceBody)
|
|
456
927
|
} catch {
|
|
457
|
-
return
|
|
928
|
+
return null
|
|
458
929
|
}
|
|
459
930
|
}
|
|
460
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Extract the full verdict object from a transcript carrying a verdict fence.
|
|
934
|
+
* @param {string|null|undefined} transcript the agent transcript text
|
|
935
|
+
* @returns {object|null} the parsed verdict with all_pass, findings, and manifest_sha256, or null
|
|
936
|
+
*/
|
|
937
|
+
function extractVerdict(transcript) {
|
|
938
|
+
return parseLastVerdictFence(transcript)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Decide whether a workflow code-verifier transcript ended in a passing
|
|
943
|
+
* verdict. Reads the LAST ```verdict ...``` fenced JSON block via the shared
|
|
944
|
+
* parser and returns true only when it parses to an object with all_pass true.
|
|
945
|
+
* @param {string|null|undefined} verifyTranscript the verifier's transcript text
|
|
946
|
+
* @returns {boolean} true only when the last verdict fence reports all_pass true
|
|
947
|
+
*/
|
|
948
|
+
function verdictPassed(verifyTranscript) {
|
|
949
|
+
const verdictRecord = parseLastVerdictFence(verifyTranscript)
|
|
950
|
+
return verdictRecord != null && verdictRecord.all_pass === true
|
|
951
|
+
}
|
|
952
|
+
|
|
461
953
|
const VERIFY_OBJECTION_FALLBACK = 'The verify step rejected the working-tree fixes without a parseable verdict; re-read the fix-verify transcript above and address every concern it raised.'
|
|
462
954
|
|
|
463
955
|
/**
|
|
@@ -499,25 +991,14 @@ function renderVerifyObjectionLine(eachFinding) {
|
|
|
499
991
|
* @returns {string} a human-readable block of the verifier's objections
|
|
500
992
|
*/
|
|
501
993
|
function extractVerifyObjection(verifyTranscript) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const verdictRecord = JSON.parse(lastFenceBody)
|
|
512
|
-
const allObjections = Array.isArray(verdictRecord?.findings) ? verdictRecord.findings : []
|
|
513
|
-
const renderedObjections = allObjections
|
|
514
|
-
.map((eachFinding) => renderVerifyObjectionLine(eachFinding))
|
|
515
|
-
.filter((eachLine) => eachLine !== null)
|
|
516
|
-
if (renderedObjections.length === 0) return VERIFY_OBJECTION_FALLBACK
|
|
517
|
-
return renderedObjections.map((eachLine, position) => `${position + 1}. ${eachLine}`).join('\n')
|
|
518
|
-
} catch {
|
|
519
|
-
return VERIFY_OBJECTION_FALLBACK
|
|
520
|
-
}
|
|
994
|
+
const verdictRecord = parseLastVerdictFence(verifyTranscript)
|
|
995
|
+
if (verdictRecord == null) return VERIFY_OBJECTION_FALLBACK
|
|
996
|
+
const allObjections = Array.isArray(verdictRecord?.findings) ? verdictRecord.findings : []
|
|
997
|
+
const renderedObjections = allObjections
|
|
998
|
+
.map((eachFinding) => renderVerifyObjectionLine(eachFinding))
|
|
999
|
+
.filter((eachLine) => eachLine !== null)
|
|
1000
|
+
if (renderedObjections.length === 0) return VERIFY_OBJECTION_FALLBACK
|
|
1001
|
+
return renderedObjections.map((eachLine, position) => `${position + 1}. ${eachLine}`).join('\n')
|
|
521
1002
|
}
|
|
522
1003
|
|
|
523
1004
|
/**
|
|
@@ -723,37 +1204,6 @@ const input = runInput.input
|
|
|
723
1204
|
activeRepoPath = typeof input.repoPath === 'string' && input.repoPath ? input.repoPath : null
|
|
724
1205
|
const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNumber} (https://github.com/${input.owner}/${input.repo}/pull/${input.prNumber})`
|
|
725
1206
|
|
|
726
|
-
/**
|
|
727
|
-
* Resolve the current PR HEAD SHA from GitHub.
|
|
728
|
-
* @returns {Promise<string>} the 40-char HEAD SHA
|
|
729
|
-
*/
|
|
730
|
-
async function resolveHead() {
|
|
731
|
-
const head = await convergeAgent(
|
|
732
|
-
`Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
|
|
733
|
-
`gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
|
|
734
|
-
`Return the full 40-character SHA in the sha field. Do not modify any files.`,
|
|
735
|
-
{ label: 'resolve-head', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore' },
|
|
736
|
-
)
|
|
737
|
-
return head?.sha
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Fetch origin/main once per round before the parallel lenses spawn. The
|
|
742
|
-
* code-review and bug-audit lenses both diff against origin/main; running their
|
|
743
|
-
* own git fetch in parallel contends on the worktree .git lock and fails
|
|
744
|
-
* intermittently, so a single serial fetch here keeps the ref current and the
|
|
745
|
-
* parallel lenses do no git fetch of their own.
|
|
746
|
-
* @returns {Promise<string>} agent transcript (unused)
|
|
747
|
-
*/
|
|
748
|
-
function prefetchMainForRound() {
|
|
749
|
-
return convergeAgent(
|
|
750
|
-
`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` +
|
|
751
|
-
`git fetch origin main\n` +
|
|
752
|
-
`Do not edit, commit, push, rebase, or modify any files — fetch only.`,
|
|
753
|
-
{ label: 'prefetch-main', phase: 'Converge', agentType: 'Explore' },
|
|
754
|
-
)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
1207
|
/**
|
|
758
1208
|
* Bugbot lens: ensure Cursor Bugbot has rendered a verdict on the given HEAD,
|
|
759
1209
|
* triggering and polling its CI check run when needed, and return its findings.
|
|
@@ -855,139 +1305,6 @@ function renderFindingsBlock(findings) {
|
|
|
855
1305
|
.join('\n')
|
|
856
1306
|
}
|
|
857
1307
|
|
|
858
|
-
/**
|
|
859
|
-
* Edit step: one clean-coder fixes every finding test-first in the working
|
|
860
|
-
* tree and resolves the GitHub review threads, making NO commit or push so the
|
|
861
|
-
* verify step can bind a verdict to the unstaged fixes.
|
|
862
|
-
* @param {string} head PR HEAD SHA the findings were raised against
|
|
863
|
-
* @param {Array<object>} findings deduped findings across all lenses
|
|
864
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
865
|
-
* @returns {Promise<object>} EDIT_SCHEMA result
|
|
866
|
-
*/
|
|
867
|
-
function applyFixesEdit(head, findings, sourceLabel) {
|
|
868
|
-
const findingsBlock = renderFindingsBlock(findings)
|
|
869
|
-
const threadIds = findings
|
|
870
|
-
.flatMap((each) => collectFindingThreadIds(each))
|
|
871
|
-
.filter((each) => typeof each === 'number')
|
|
872
|
-
return convergeAgent(
|
|
873
|
-
`You are the EDIT step fixing ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
874
|
-
`Findings:\n${findingsBlock}\n\n` +
|
|
875
|
-
`Rules:\n` +
|
|
876
|
-
`- Confirm the working tree is on the PR branch at HEAD ${head} with no unrelated edits before you start.\n` +
|
|
877
|
-
`- Fix every finding test-first (failing test, then minimum code to pass) per CODE_RULES. Verify each concern against current code; a finding whose concern no longer applies needs no code change but still needs its thread resolved.\n` +
|
|
878
|
-
`- Leave all fixes in the working tree. Do NOT commit and do NOT push — the commit step does that after verification. Committing or pushing here would change the surface the verifier binds to.\n` +
|
|
879
|
-
`- For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply with python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "<what changed>". Then resolve the PR review thread by thread node id (PRRT_...): look up the thread id for that comment via GraphQL (match on comment databaseId == <id> in the pull request's reviewThreads), then call the github MCP pull_request_review_write method=resolve_thread with threadId=<PRRT_...> (not the numeric comment id), or run the resolveReviewThread GraphQL mutation with the same threadId.\n` +
|
|
880
|
-
`- Findings with replyToCommentId null are in-memory audit findings: fix them, no reply needed.\n\n` +
|
|
881
|
-
`Return values:\n` +
|
|
882
|
-
`- When you edited code to fix at least one finding: edited=true, resolvedWithoutCommit=false.\n` +
|
|
883
|
-
`- 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` +
|
|
884
|
-
`Always include a one-line summary.` +
|
|
885
|
-
PRE_COMMIT_GATE_STEP,
|
|
886
|
-
{ label: `fix-edit:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
887
|
-
)
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Verify step: a code-verifier checks the working-tree fixes against the
|
|
892
|
-
* findings, computes the binding surface hash, and ends with a verdict fence
|
|
893
|
-
* as plain assistant text (NO schema, so the fence is not consumed as
|
|
894
|
-
* structured output). The fence's manifest_sha256 is what unlocks the
|
|
895
|
-
* verified-commit gate for the commit step. The verifier makes no edits.
|
|
896
|
-
* @param {string} head PR HEAD SHA the findings were raised against
|
|
897
|
-
* @param {Array<object>} findings deduped findings the fixes must address
|
|
898
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
899
|
-
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
900
|
-
*/
|
|
901
|
-
function verifyFixesInWorkingTree(head, findings, sourceLabel) {
|
|
902
|
-
const findingsBlock = renderFindingsBlock(findings)
|
|
903
|
-
return convergeAgent(
|
|
904
|
-
`You are the VERIFY step for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
905
|
-
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
906
|
-
`Steps:\n` +
|
|
907
|
-
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
908
|
-
`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` +
|
|
909
|
-
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
910
|
-
{ label: `fix-verify:${sourceLabel}`, phase: 'Converge', agentType: 'code-verifier' },
|
|
911
|
-
)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Commit step: one clean-coder commits the already-verified working-tree fixes
|
|
916
|
-
* in a single commit and pushes to the PR branch, making NO further file edits
|
|
917
|
-
* — any edit changes the surface and invalidates the verifier verdict that
|
|
918
|
-
* unlocks the commit gate.
|
|
919
|
-
* @param {string} head PR HEAD SHA before the fix commit
|
|
920
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
921
|
-
* @returns {Promise<object>} FIX_SCHEMA result
|
|
922
|
-
*/
|
|
923
|
-
function commitVerifiedFixes(head, sourceLabel) {
|
|
924
|
-
return convergeAgent(
|
|
925
|
-
`You are the COMMIT step for fixes (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
926
|
-
`Rules:\n` +
|
|
927
|
-
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the commit would be blocked. Do not run a formatter, do not touch a test, do not re-fix anything — only commit and push what is already there.\n` +
|
|
928
|
-
`- Make ONE commit for all the working-tree fixes, then push to the PR branch.\n\n` +
|
|
929
|
-
`Return values:\n` +
|
|
930
|
-
`- 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` +
|
|
931
|
-
`- 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=${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` +
|
|
932
|
-
`- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
|
|
933
|
-
{ label: `fix-commit:${sourceLabel}`, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
934
|
-
)
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* Commit-recovery fixer: when a commit step is blocked by a commit-time hook or
|
|
939
|
-
* gate that requires a code change, one clean-coder fixes only that blocking
|
|
940
|
-
* violation test-first in the working tree and leaves it uncommitted, so the
|
|
941
|
-
* re-verify step can bind a fresh verdict and the retry commit can push. It does
|
|
942
|
-
* not re-open the original findings or touch GitHub threads — the edit step
|
|
943
|
-
* already handled those.
|
|
944
|
-
* @param {string} head PR HEAD SHA the fixes were raised against
|
|
945
|
-
* @param {string} blockerDetail verbatim hook/gate message naming the file and rule to change
|
|
946
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
947
|
-
* @param {number} attempt the 1-based recovery attempt number
|
|
948
|
-
* @returns {Promise<object>} EDIT_SCHEMA result
|
|
949
|
-
*/
|
|
950
|
-
function recoverCommitBlockEdit(head, blockerDetail, sourceLabel, attempt) {
|
|
951
|
-
return convergeAgent(
|
|
952
|
-
`You are the COMMIT-RECOVERY fixer (attempt ${attempt}) for fixes (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. A prior commit step was blocked by a commit-time hook or gate that requires a code change. A separate verify step then a separate commit step run after you.\n\n` +
|
|
953
|
-
`The blocking hook or gate said:\n${blockerDetail}\n\n` +
|
|
954
|
-
`Rules:\n` +
|
|
955
|
-
`- Confirm the working tree is on the PR branch at HEAD ${head} with the prior fixes still present.\n` +
|
|
956
|
-
`- Fix ONLY the violation named above, test-first (failing test, then minimum code to pass) per CODE_RULES. Do not re-open the original findings, and do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
957
|
-
`- 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` +
|
|
958
|
-
`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.` +
|
|
959
|
-
PRE_COMMIT_GATE_STEP,
|
|
960
|
-
{ label: `fix-recover:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
961
|
-
)
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Verify-recovery fixer: when the verify step rejects the working-tree fixes, one
|
|
966
|
-
* clean-coder re-fixes against the verdict's stated objections, test-first, and
|
|
967
|
-
* leaves the work uncommitted so the re-verify step can bind a fresh verdict. The
|
|
968
|
-
* objection text names which findings the verifier judged unresolved and why, so
|
|
969
|
-
* the fixer addresses those concerns; it does not touch GitHub review threads —
|
|
970
|
-
* the edit step already replied to and resolved those.
|
|
971
|
-
* @param {string} head PR HEAD SHA the fixes were raised against
|
|
972
|
-
* @param {string} objection the verifier's rendered objections from the failed verdict
|
|
973
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
974
|
-
* @param {number} attempt the 1-based recovery attempt number
|
|
975
|
-
* @returns {Promise<object>} EDIT_SCHEMA result
|
|
976
|
-
*/
|
|
977
|
-
function recoverVerifyFailEdit(head, objection, sourceLabel, attempt) {
|
|
978
|
-
return convergeAgent(
|
|
979
|
-
`You are the VERIFY-RECOVERY fixer (attempt ${attempt}) for fixes (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The verify step rejected the working-tree fixes; its verdict named what is still unresolved. A separate verify step then a separate commit step run after you.\n\n` +
|
|
980
|
-
`The verify step's objections:\n${objection}\n\n` +
|
|
981
|
-
`Rules:\n` +
|
|
982
|
-
`- Confirm the working tree is on the PR branch at HEAD ${head} with the prior fixes still present.\n` +
|
|
983
|
-
`- Address every objection above test-first (failing test, then minimum code to pass) per CODE_RULES, so each named concern is genuinely resolved the way the verdict requires. Do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
984
|
-
`- 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` +
|
|
985
|
-
`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.` +
|
|
986
|
-
PRE_COMMIT_GATE_STEP,
|
|
987
|
-
{ label: `fix-verify-recover:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
988
|
-
)
|
|
989
|
-
}
|
|
990
|
-
|
|
991
1308
|
const FIX_RECOVERY_MAX_ATTEMPTS = 2
|
|
992
1309
|
|
|
993
1310
|
/**
|
|
@@ -1054,7 +1371,8 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
|
|
|
1054
1371
|
* @returns {Promise<object>} FIX_SCHEMA result
|
|
1055
1372
|
*/
|
|
1056
1373
|
async function applyFixes(head, findings, sourceLabel) {
|
|
1057
|
-
const
|
|
1374
|
+
const codeEditorId = await spawnCodeEditorAgent()
|
|
1375
|
+
const editResult = await resumeCodeEditorAgent(codeEditorId, 'fix-edit', { head, findings, sourceLabel })
|
|
1058
1376
|
if (editResult?.resolvedWithoutCommit === true && editResult?.edited !== true) {
|
|
1059
1377
|
return {
|
|
1060
1378
|
newSha: head,
|
|
@@ -1065,47 +1383,8 @@ async function applyFixes(head, findings, sourceLabel) {
|
|
|
1065
1383
|
blockerDetail: '',
|
|
1066
1384
|
}
|
|
1067
1385
|
}
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
runRecoverEdit: (objection, attempt) => recoverVerifyFailEdit(head, objection, sourceLabel, attempt),
|
|
1071
|
-
})
|
|
1072
|
-
if (!verdictPassed(verifyTranscript)) {
|
|
1073
|
-
return {
|
|
1074
|
-
newSha: head,
|
|
1075
|
-
pushed: false,
|
|
1076
|
-
resolvedWithoutCommit: false,
|
|
1077
|
-
summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
|
|
1078
|
-
blockedNeedingEdit: false,
|
|
1079
|
-
blockerDetail: '',
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return commitWithRecovery({
|
|
1083
|
-
runCommit: () => commitVerifiedFixes(head, sourceLabel),
|
|
1084
|
-
runVerify: () => verifyFixesInWorkingTree(head, findings, sourceLabel),
|
|
1085
|
-
runRecoverEdit: (detail, attempt) => recoverCommitBlockEdit(head, detail, sourceLabel, attempt),
|
|
1086
|
-
})
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
* Post the terminal CLEAN bugteam audit artifact so check_convergence.py sees
|
|
1091
|
-
* a clean bugteam review on the converged HEAD. The post is load-bearing: the
|
|
1092
|
-
* convergence gate's bugteam-review check can never pass until this review
|
|
1093
|
-
* lands, so the result reports whether the post succeeded rather than
|
|
1094
|
-
* discarding it. A blocked post (a permission or auto-mode-classifier denial)
|
|
1095
|
-
* or a script error returns posted:false with the reason so the caller can
|
|
1096
|
-
* surface a blocker instead of re-converging into the iteration cap.
|
|
1097
|
-
* @param {string} head converged PR HEAD SHA
|
|
1098
|
-
* @returns {Promise<object>} CLEAN_AUDIT_SCHEMA result
|
|
1099
|
-
*/
|
|
1100
|
-
function postCleanAudit(head) {
|
|
1101
|
-
return convergeAgent(
|
|
1102
|
-
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${head}. All review lenses are clean on this HEAD.\n\n` +
|
|
1103
|
-
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
1104
|
-
`python "${CONFIG.prLoopScripts}/post_audit_thread.py" --skill bugteam --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --commit ${head} --state CLEAN --findings-json <temp-file>\n` +
|
|
1105
|
-
`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` +
|
|
1106
|
-
`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.`,
|
|
1107
|
-
{ label: 'post-clean-audit', phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
|
|
1108
|
-
)
|
|
1386
|
+
const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel, 'verify-commit')
|
|
1387
|
+
return fixerWithRecovery(fixerAgentId, head, findings, sourceLabel)
|
|
1109
1388
|
}
|
|
1110
1389
|
|
|
1111
1390
|
/**
|
|
@@ -1114,7 +1393,7 @@ function postCleanAudit(head) {
|
|
|
1114
1393
|
* post stops the run with an actionable message rather than re-converging until
|
|
1115
1394
|
* the iteration cap. Handles a dead post agent (a null result) as not posted.
|
|
1116
1395
|
* @param {string} head converged PR HEAD SHA
|
|
1117
|
-
* @param {object} auditResult CLEAN_AUDIT_SCHEMA result from
|
|
1396
|
+
* @param {object} auditResult CLEAN_AUDIT_SCHEMA result from the post-clean-audit resume, or null when the agent died
|
|
1118
1397
|
* @returns {string} the blocker message naming the post failure and the unblock path
|
|
1119
1398
|
*/
|
|
1120
1399
|
function cleanAuditBlocker(head, auditResult) {
|
|
@@ -1152,133 +1431,6 @@ function runCopilotGate(head) {
|
|
|
1152
1431
|
)
|
|
1153
1432
|
}
|
|
1154
1433
|
|
|
1155
|
-
/**
|
|
1156
|
-
* Run the authoritative convergence gate.
|
|
1157
|
-
* @param {boolean} bugbotDown pass --bugbot-down when Bugbot is opted out or proved unreachable this run
|
|
1158
|
-
* @param {boolean} copilotDown pass --copilot-down when Copilot is down or out of quota this run
|
|
1159
|
-
* @returns {Promise<object>} CONVERGENCE_SCHEMA result
|
|
1160
|
-
*/
|
|
1161
|
-
function checkConvergence(bugbotDown, copilotDown) {
|
|
1162
|
-
const bugbotDownFlag = bugbotDown ? ' --bugbot-down' : ''
|
|
1163
|
-
const copilotDownFlag = copilotDown ? ' --copilot-down' : ''
|
|
1164
|
-
return convergeAgent(
|
|
1165
|
-
`Run the convergence gate for ${prCoordinates} and report the result. Do not edit code.\n\n` +
|
|
1166
|
-
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n\n` +
|
|
1167
|
-
`Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
|
|
1168
|
-
`Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
|
|
1169
|
-
`Exit 2 -> retry once; if it still errors, return {pass:false, failures:["check_convergence gh error"]}.`,
|
|
1170
|
-
{ label: 'check-convergence', phase: 'Finalize', schema: CONVERGENCE_SCHEMA, agentType: 'Explore' },
|
|
1171
|
-
)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/**
|
|
1175
|
-
* Mark the PR ready for review (draft=false) and confirm the transition landed.
|
|
1176
|
-
* When Copilot is down this run, the mark-ready agent first opts the
|
|
1177
|
-
* independent mark-ready blocker hook out of the Copilot gate by exporting
|
|
1178
|
-
* the Copilot token into CLAUDE_REVIEWS_DISABLED: that hook re-runs
|
|
1179
|
-
* check_convergence.py without --copilot-down, so the env token is the only
|
|
1180
|
-
* channel a genuine Copilot outage has to pass its Copilot review gate.
|
|
1181
|
-
* @param {string} head converged PR HEAD SHA
|
|
1182
|
-
* @param {boolean} copilotDown true when the Copilot gate was bypassed for an outage this run
|
|
1183
|
-
* @returns {Promise<object>} READY_SCHEMA result
|
|
1184
|
-
*/
|
|
1185
|
-
function markReady(head, copilotDown) {
|
|
1186
|
-
const copilotOptOut = copilotDown
|
|
1187
|
-
? `0. Copilot is down this run, so opt the independent mark-ready blocker hook out of the Copilot gate before step 1. Export the token in the same shell session as step 1 so the hook's convergence re-check inherits it:\n bash: export CLAUDE_REVIEWS_DISABLED="copilot" (PowerShell: $env:CLAUDE_REVIEWS_DISABLED = "copilot")\n`
|
|
1188
|
-
: ''
|
|
1189
|
-
return convergeAgent(
|
|
1190
|
-
`All convergence gates pass for ${prCoordinates} on HEAD ${head}. Mark the PR ready, then confirm it left draft state. Do not edit code.\n\n` +
|
|
1191
|
-
copilotOptOut +
|
|
1192
|
-
`1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
1193
|
-
`2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
1194
|
-
`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}.`,
|
|
1195
|
-
{ label: 'mark-ready', phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose' },
|
|
1196
|
-
)
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
/**
|
|
1200
|
-
* Repair edit step: one clean-coder resolves the lingering bot review threads
|
|
1201
|
-
* the convergence check flagged, fixes any still-applicable bot-thread concern
|
|
1202
|
-
* test-first in the working tree, and rebases onto origin/main when the PR is
|
|
1203
|
-
* not mergeable — making NO commit and NO push, so the verify step can bind a
|
|
1204
|
-
* verdict to the resulting surface before the commit step pushes it. Human
|
|
1205
|
-
* reviewer threads are never touched.
|
|
1206
|
-
* @param {string} head current PR HEAD SHA
|
|
1207
|
-
* @param {Array<string>} failures FAIL lines from the convergence check
|
|
1208
|
-
* @returns {Promise<object>} REPAIR_EDIT_SCHEMA result
|
|
1209
|
-
*/
|
|
1210
|
-
function repairConvergenceEdit(head, failures) {
|
|
1211
|
-
const failureBlock = failures.length
|
|
1212
|
-
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
1213
|
-
: 'none reported'
|
|
1214
|
-
return convergeAgent(
|
|
1215
|
-
`You are the EDIT step repairing the convergence gates that failed for ${prCoordinates} on HEAD ${head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
1216
|
-
`Failing gates:\n${failureBlock}\n\n` +
|
|
1217
|
-
`Address only the failing gates, and make NO commit and NO push — leave every code change in the working tree (a rebase necessarily creates local commits, which is fine; just do not push them):\n` +
|
|
1218
|
-
`- Unresolved bot review threads: fetch the threads where isResolved is false (gh api graphql, or the github MCP pull_request_read get_review_comments), then keep only the bot-authored ones — a thread whose root comment author login contains "cursor", "claude", or "copilot" (case-insensitive substring). Explicitly skip every human reviewer thread; the convergence gate counts only unresolved bot threads, so touching a human thread is out of scope. For each bot thread, verify the concern against current code; if it still applies, fix it test-first in the working tree and leave the fix uncommitted; either way post an inline reply and resolve the thread by its PRRT_ node id (GraphQL lookup matching the comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n` +
|
|
1219
|
-
`- PR not mergeable: rebase onto origin/main FIRST, before applying any uncommitted bot-thread fix, so the rebase runs on a clean tree (git fetch origin main; git rebase origin/main; resolve conflicts). Do NOT force-push — the commit step does that after verification.\n` +
|
|
1220
|
-
`- A dirty bot review or a still-pending requested reviewer: leave it; the next round re-runs that reviewer.\n\n` +
|
|
1221
|
-
`Return values:\n` +
|
|
1222
|
-
`- edited=true when you changed code in the working tree to fix a bot-thread concern.\n` +
|
|
1223
|
-
`- rebased=true when you rebased the branch onto origin/main.\n` +
|
|
1224
|
-
`- 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` +
|
|
1225
|
-
`Always include a one-line summary.` +
|
|
1226
|
-
PRE_COMMIT_GATE_STEP,
|
|
1227
|
-
{ label: 'repair-edit', phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1228
|
-
)
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* Repair verify step: a code-verifier confirms the working-tree repair (any
|
|
1233
|
-
* bot-thread fix plus any rebase result) is sound, computes the binding surface
|
|
1234
|
-
* hash, and ends with a verdict fence as plain assistant text (NO schema). The
|
|
1235
|
-
* fence's manifest_sha256 unlocks the verified-commit gate for the commit step's
|
|
1236
|
-
* push. The verifier makes no edits.
|
|
1237
|
-
* @param {string} head PR HEAD SHA the repair started from
|
|
1238
|
-
* @param {Array<string>} failures FAIL lines the repair addressed
|
|
1239
|
-
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
1240
|
-
*/
|
|
1241
|
-
function verifyRepairChanges(head, failures) {
|
|
1242
|
-
const failureBlock = failures.length
|
|
1243
|
-
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
1244
|
-
: 'none reported'
|
|
1245
|
-
return convergeAgent(
|
|
1246
|
-
`You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
1247
|
-
`Concerns the working-tree repair must resolve (the gates the convergence check flagged):\n${failureBlock}\n\n` +
|
|
1248
|
-
`Steps:\n` +
|
|
1249
|
-
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
1250
|
-
`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` +
|
|
1251
|
-
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
1252
|
-
{ label: 'repair-verify', phase: 'Finalize', agentType: 'code-verifier' },
|
|
1253
|
-
)
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
* Repair commit step: one clean-coder commits any uncommitted bot-thread fix in
|
|
1258
|
-
* a single commit and pushes to the PR branch (force-with-lease when the edit
|
|
1259
|
-
* step rebased), making NO further file edits — any edit changes the surface and
|
|
1260
|
-
* invalidates the verifier verdict that unlocks the commit gate.
|
|
1261
|
-
* @param {string} head PR HEAD SHA before the repair push
|
|
1262
|
-
* @param {boolean} wasRebased true when the edit step rebased the branch, so the push must be force-with-lease
|
|
1263
|
-
* @returns {Promise<object>} FIX_SCHEMA result
|
|
1264
|
-
*/
|
|
1265
|
-
function commitRepairFixes(head, wasRebased) {
|
|
1266
|
-
const pushInstruction = wasRebased
|
|
1267
|
-
? 'The edit step rebased the branch, so push with git push --force-with-lease.'
|
|
1268
|
-
: 'Push to the PR branch with a plain git push.'
|
|
1269
|
-
return convergeAgent(
|
|
1270
|
-
`You are the COMMIT step for the convergence repair on ${prCoordinates}, HEAD ${head}. The edit step left its repair in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
1271
|
-
`Rules:\n` +
|
|
1272
|
-
`- 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. Do not run a formatter, do not re-fix anything — only commit and push what is already there.\n` +
|
|
1273
|
-
`- Commit any uncommitted bot-thread fix in ONE commit (skip the commit when the working tree carries only already-committed rebase results). ${pushInstruction}\n\n` +
|
|
1274
|
-
`Return values:\n` +
|
|
1275
|
-
`- 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` +
|
|
1276
|
-
`- 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=${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` +
|
|
1277
|
-
`- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
|
|
1278
|
-
{ label: 'repair-commit', phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
1279
|
-
)
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
1434
|
/**
|
|
1283
1435
|
* Address the gates a convergence check reported as failing, then hand control
|
|
1284
1436
|
* back to the converge phase: edit (clean-coder resolves bot threads, applies
|
|
@@ -1294,7 +1446,8 @@ function commitRepairFixes(head, wasRebased) {
|
|
|
1294
1446
|
* @returns {Promise<object>} FIX_SCHEMA result
|
|
1295
1447
|
*/
|
|
1296
1448
|
async function repairConvergence(head, failures) {
|
|
1297
|
-
const
|
|
1449
|
+
const codeEditorId = await spawnCodeEditorAgent()
|
|
1450
|
+
const editResult = await resumeCodeEditorAgent(codeEditorId, 'repair-edit', { head, failures })
|
|
1298
1451
|
const hasPushWork = editResult?.edited === true || editResult?.rebased === true
|
|
1299
1452
|
if (!hasPushWork) {
|
|
1300
1453
|
return {
|
|
@@ -1306,9 +1459,10 @@ async function repairConvergence(head, failures) {
|
|
|
1306
1459
|
blockerDetail: '',
|
|
1307
1460
|
}
|
|
1308
1461
|
}
|
|
1462
|
+
const verifierId = await spawnVerifierAgent()
|
|
1309
1463
|
const verifyTranscript = await verifyWithRecovery({
|
|
1310
|
-
runVerify: () =>
|
|
1311
|
-
runRecoverEdit: (objection, attempt) =>
|
|
1464
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
|
|
1465
|
+
runRecoverEdit: (objection, attempt) => resumeCodeEditorAgent(codeEditorId, 'verify-recover', { head, sourceLabel: 'repair', objection, attempt }),
|
|
1312
1466
|
})
|
|
1313
1467
|
if (!verdictPassed(verifyTranscript)) {
|
|
1314
1468
|
return {
|
|
@@ -1322,51 +1476,12 @@ async function repairConvergence(head, failures) {
|
|
|
1322
1476
|
}
|
|
1323
1477
|
const wasRebased = editResult?.rebased === true
|
|
1324
1478
|
return commitWithRecovery({
|
|
1325
|
-
runCommit: () =>
|
|
1326
|
-
runVerify: () =>
|
|
1327
|
-
runRecoverEdit: (detail, attempt) =>
|
|
1479
|
+
runCommit: () => resumeCodeEditorAgent(codeEditorId, 'repair-commit', { head, wasRebased }),
|
|
1480
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
|
|
1481
|
+
runRecoverEdit: (detail, attempt) => resumeCodeEditorAgent(codeEditorId, 'commit-recover', { head, sourceLabel: 'repair', blockerDetail: detail, attempt }),
|
|
1328
1482
|
})
|
|
1329
1483
|
}
|
|
1330
1484
|
|
|
1331
|
-
/**
|
|
1332
|
-
* Pre-flight merge-conflict check: ask GitHub whether the PR branch still merges
|
|
1333
|
-
* cleanly into its base. GitHub computes mergeability asynchronously, so the
|
|
1334
|
-
* agent polls until .mergeable resolves to a boolean before judging. Read-only —
|
|
1335
|
-
* it makes no edit, commit, push, or rebase.
|
|
1336
|
-
* @param {string} head PR HEAD SHA the check runs against
|
|
1337
|
-
* @returns {Promise<object>} MERGE_CONFLICT_SCHEMA result
|
|
1338
|
-
*/
|
|
1339
|
-
function checkMergeConflicts(head) {
|
|
1340
|
-
return convergeAgent(
|
|
1341
|
-
`Report whether ${prCoordinates} (HEAD ${head}) has merge conflicts with its base branch. Do not edit, commit, push, or rebase — read only.\n\n` +
|
|
1342
|
-
`GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
|
|
1343
|
-
` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
|
|
1344
|
-
`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` +
|
|
1345
|
-
`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.`,
|
|
1346
|
-
{ label: 'check-merge-conflicts', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },
|
|
1347
|
-
)
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/**
|
|
1351
|
-
* Conflict-resolution edit step: one clean-coder rebases the PR branch onto
|
|
1352
|
-
* origin/main and resolves every conflict in the working tree, making NO push —
|
|
1353
|
-
* the verify step binds a verdict to the rebased tree before the commit step
|
|
1354
|
-
* force-pushes it. A rebase necessarily creates local commits, which is expected;
|
|
1355
|
-
* only the force-push is withheld so the verifier binds the surface first.
|
|
1356
|
-
* @param {string} head PR HEAD SHA before the rebase
|
|
1357
|
-
* @returns {Promise<object>} CONFLICT_EDIT_SCHEMA result
|
|
1358
|
-
*/
|
|
1359
|
-
function resolveConflictsEdit(head) {
|
|
1360
|
-
return convergeAgent(
|
|
1361
|
-
`You are the EDIT step resolving merge conflicts for ${prCoordinates}, HEAD ${head}, before the bug checks run. The PR branch conflicts with origin/main. A separate verify step then a separate commit step run after you.\n\n` +
|
|
1362
|
-
`Rules:\n` +
|
|
1363
|
-
`- Confirm the working tree is on the PR branch at HEAD ${head} with no unrelated edits before you start.\n` +
|
|
1364
|
-
`- 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` +
|
|
1365
|
-
`- 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` +
|
|
1366
|
-
`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.`,
|
|
1367
|
-
{ label: 'resolve-conflicts-edit', phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1368
|
-
)
|
|
1369
|
-
}
|
|
1370
1485
|
|
|
1371
1486
|
/**
|
|
1372
1487
|
* Pre-flight conflict resolution: when the PR branch conflicts with its base,
|
|
@@ -1381,22 +1496,24 @@ function resolveConflictsEdit(head) {
|
|
|
1381
1496
|
* @param {string} head PR HEAD SHA before any rebase
|
|
1382
1497
|
* @returns {Promise<string>} the HEAD SHA after a successful rebase push, or the unchanged head
|
|
1383
1498
|
*/
|
|
1384
|
-
async function resolveMergeConflicts(head) {
|
|
1385
|
-
const mergeState = await
|
|
1499
|
+
async function resolveMergeConflicts(head, gitAgentId) {
|
|
1500
|
+
const mergeState = await resumeGitAgent(gitAgentId, 'check-merge-conflicts', head)
|
|
1386
1501
|
if (!isMergeConflicting(mergeState)) return head
|
|
1387
1502
|
log(`Pre-flight: ${prCoordinates} conflicts with origin/main — rebasing clean before the bug checks`)
|
|
1388
|
-
const
|
|
1503
|
+
const codeEditorId = await spawnCodeEditorAgent()
|
|
1504
|
+
const editResult = await resumeCodeEditorAgent(codeEditorId, 'conflict-edit', { head })
|
|
1389
1505
|
if (editResult?.rebased !== true) return head
|
|
1390
1506
|
const failures = ['PR branch had merge conflicts with origin/main; the rebase must leave a clean, conflict-free tree']
|
|
1507
|
+
const verifierId = await spawnVerifierAgent()
|
|
1391
1508
|
const verifyTranscript = await verifyWithRecovery({
|
|
1392
|
-
runVerify: () =>
|
|
1393
|
-
runRecoverEdit: (objection, attempt) =>
|
|
1509
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
|
|
1510
|
+
runRecoverEdit: (objection, attempt) => resumeCodeEditorAgent(codeEditorId, 'verify-recover', { head, sourceLabel: 'conflict-rebase', objection, attempt }),
|
|
1394
1511
|
})
|
|
1395
1512
|
if (!verdictPassed(verifyTranscript)) return head
|
|
1396
1513
|
const commitResult = await commitWithRecovery({
|
|
1397
|
-
runCommit: () =>
|
|
1398
|
-
runVerify: () =>
|
|
1399
|
-
runRecoverEdit: (detail, attempt) =>
|
|
1514
|
+
runCommit: () => resumeCodeEditorAgent(codeEditorId, 'repair-commit', { head, wasRebased: true }),
|
|
1515
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'repair-verify', { head, failures }),
|
|
1516
|
+
runRecoverEdit: (detail, attempt) => resumeCodeEditorAgent(codeEditorId, 'commit-recover', { head, sourceLabel: 'conflict-rebase', blockerDetail: detail, attempt }),
|
|
1400
1517
|
})
|
|
1401
1518
|
return commitResult?.newSha || head
|
|
1402
1519
|
}
|
|
@@ -1413,90 +1530,6 @@ function isStandardsOnlyRound(findings) {
|
|
|
1413
1530
|
return findings.length > 0 && findings.every((each) => each.category === 'code-standard')
|
|
1414
1531
|
}
|
|
1415
1532
|
|
|
1416
|
-
/**
|
|
1417
|
-
* Standards-deferral edit step: one clean-coder files the follow-up fix issue,
|
|
1418
|
-
* stages an environment-hardening hooks/rules change in the config repo's
|
|
1419
|
-
* working tree WITHOUT committing, and resolves the PR's code-standard threads.
|
|
1420
|
-
* Leaving the hardening edit uncommitted lets the verify step bind a verdict to
|
|
1421
|
-
* it before the commit step opens the PR. The PR's own branch is never touched.
|
|
1422
|
-
* @param {string} head PR HEAD SHA the findings were raised against
|
|
1423
|
-
* @param {Array<object>} findings deduped code-standard-only findings
|
|
1424
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
1425
|
-
* @returns {Promise<object>} STANDARDS_EDIT_SCHEMA result
|
|
1426
|
-
*/
|
|
1427
|
-
function standardsFollowUpEdit(head, findings, sourceLabel) {
|
|
1428
|
-
const findingsBlock = renderFindingsBlock(findings)
|
|
1429
|
-
const threadIds = findings
|
|
1430
|
-
.flatMap((each) => collectFindingThreadIds(each))
|
|
1431
|
-
.filter((each) => typeof each === 'number')
|
|
1432
|
-
return convergeAgent(
|
|
1433
|
-
`You are the EDIT step deferring a code-standard-only round on ${prCoordinates}, HEAD ${head} (${sourceLabel}). The round surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact); the run treats it as passed and defers the fixes to follow-up work, which you now stage. A separate verify step then a separate commit step open the hardening PR after you. Do NOT commit or push to the PR's own branch.\n\n` +
|
|
1434
|
-
`Findings:\n${findingsBlock}\n\n` +
|
|
1435
|
-
`1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR. Capture the issue URL.\n` +
|
|
1436
|
-
`2. Stage the environment-hardening change: in the Claude environment config repo (the repo owning ~/.claude hooks and rules — JonEcho/llm-settings for hooks, jl-cmd/claude-code-config for rules/skills; pick whichever owns the surface that would block these violation classes), find or clone a local checkout, fetch origin, and create a branch off origin/main. Edit the hooks/rules in that checkout's WORKING TREE so each violation class found here is blocked at Write/Edit time, before code is written. Do NOT commit and do NOT push — the commit step does that after the verify step binds a verdict to the working tree. Return the checkout's absolute path in hardeningRepoPath, the branch name in hardeningBranch, and set hardeningEdited=true. When no hardening is feasible for these classes, leave hardeningRepoPath and hardeningBranch empty and hardeningEdited=false; the follow-up issue still stands.\n` +
|
|
1437
|
-
`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` +
|
|
1438
|
-
`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.` +
|
|
1439
|
-
PRE_COMMIT_GATE_STEP,
|
|
1440
|
-
{ label: `standards-edit:${sourceLabel}`, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1441
|
-
)
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
/**
|
|
1445
|
-
* Standards-hardening verify step: a code-verifier confirms the uncommitted
|
|
1446
|
-
* hooks/rules change staged in the hardening repo blocks the deferred violation
|
|
1447
|
-
* classes, computes the binding surface hash for that repo by branch (cwd-immune),
|
|
1448
|
-
* and ends with a verdict fence as plain assistant text (NO schema) — unlocking the
|
|
1449
|
-
* verified-commit gate for the cross-repo hardening commit. The verifier makes
|
|
1450
|
-
* no edits.
|
|
1451
|
-
* @param {string} hardeningRepoPath absolute path of the hardening repo checkout the edit staged
|
|
1452
|
-
* @param {string} hardeningBranch the branch in the hardening repo that the edit staged the change on
|
|
1453
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
1454
|
-
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
1455
|
-
*/
|
|
1456
|
-
function verifyHardeningChanges(hardeningRepoPath, hardeningBranch, sourceLabel) {
|
|
1457
|
-
return convergeAgent(
|
|
1458
|
-
`You are the VERIFY step for an environment-hardening change (${sourceLabel}) staged in the working tree of ${hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
1459
|
-
`Concern the working-tree change must resolve: the edited hooks/rules block the code-standard violation classes from the deferred round at Write/Edit time, and a hook change carries a passing test per CODE_RULES.\n\n` +
|
|
1460
|
-
`Steps:\n` +
|
|
1461
|
-
`1. cd into ${hardeningRepoPath}, then resolve its repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
1462
|
-
`2. Verify the uncommitted working-tree change in REPO: read the diff (git diff) and run the hook/rule tests in that repo, confirming each violation class is now blocked.\n` +
|
|
1463
|
-
`3. Compute the binding hash for the live surface:\n` +
|
|
1464
|
-
` The hardening branch is: ${hardeningBranch}\n` +
|
|
1465
|
-
` Run exactly:\n` +
|
|
1466
|
-
` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "${hardeningBranch}"\n` +
|
|
1467
|
-
` (substitute the REPO path you resolved for the script path). That prints a single 64-char hex hash on stdout — capture it.\n` +
|
|
1468
|
-
` Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
|
|
1469
|
-
" ```verdict\n" +
|
|
1470
|
-
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
1471
|
-
" ```\n" +
|
|
1472
|
-
` 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.`,
|
|
1473
|
-
{ label: `standards-verify:${sourceLabel}`, phase: 'Converge', agentType: 'code-verifier' },
|
|
1474
|
-
)
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
/**
|
|
1478
|
-
* Standards-hardening commit step: one clean-coder commits the verified
|
|
1479
|
-
* working-tree hooks/rules change in a single commit, pushes the hardening
|
|
1480
|
-
* branch, and opens the DRAFT hardening PR — making NO further file edits, since
|
|
1481
|
-
* any edit changes the surface and invalidates the verdict that unlocks the
|
|
1482
|
-
* commit gate. The PR's own branch is never touched.
|
|
1483
|
-
* @param {string} hardeningRepoPath absolute path of the hardening repo checkout
|
|
1484
|
-
* @param {string} hardeningBranch the hardening branch the edit step created
|
|
1485
|
-
* @param {string} issueUrl the follow-up fix issue URL the PR body references
|
|
1486
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
1487
|
-
* @returns {Promise<string>} agent transcript (unused)
|
|
1488
|
-
*/
|
|
1489
|
-
function commitHardeningPr(hardeningRepoPath, hardeningBranch, issueUrl, sourceLabel) {
|
|
1490
|
-
return convergeAgent(
|
|
1491
|
-
`You are the COMMIT step opening the environment-hardening PR (${sourceLabel}) for the change staged in ${hardeningRepoPath} on branch ${hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree. Do NOT touch the PR's own branch.\n\n` +
|
|
1492
|
-
`Rules:\n` +
|
|
1493
|
-
`- 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` +
|
|
1494
|
-
`- In ${hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${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` +
|
|
1495
|
-
`Return a one-line summary naming the hardening PR URL.`,
|
|
1496
|
-
{ label: `standards-commit:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
|
|
1497
|
-
)
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
1533
|
/**
|
|
1501
1534
|
* Build the standards-deferral note for the closing report, naming the
|
|
1502
1535
|
* environment-hardening PR only when one was opened this round so the note
|
|
@@ -1528,64 +1561,25 @@ function standardsDeferralNote(findingsCount, hardeningPrOpened) {
|
|
|
1528
1561
|
* @param {string} sourceLabel short description of where the findings came from
|
|
1529
1562
|
* @returns {Promise<object>} `{ hardeningPrOpened }` — true only when the hardening PR was opened this round
|
|
1530
1563
|
*/
|
|
1531
|
-
async function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
1532
|
-
const
|
|
1564
|
+
async function spawnStandardsFollowUp(head, findings, sourceLabel, generalId) {
|
|
1565
|
+
const codeEditorId = await spawnCodeEditorAgent()
|
|
1566
|
+
const editResult = await resumeCodeEditorAgent(codeEditorId, 'standards-edit', { head, findings, sourceLabel })
|
|
1533
1567
|
if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
|
|
1534
1568
|
return { hardeningPrOpened: false }
|
|
1535
1569
|
}
|
|
1536
|
-
const
|
|
1570
|
+
const verifierId = await spawnVerifierAgent()
|
|
1571
|
+
const verifyTranscript = await resumeVerifierAgent(verifierId, 'hardening-verify', {
|
|
1572
|
+
head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch,
|
|
1573
|
+
})
|
|
1537
1574
|
if (!verdictPassed(verifyTranscript)) {
|
|
1538
1575
|
return { hardeningPrOpened: false }
|
|
1539
1576
|
}
|
|
1540
|
-
await
|
|
1577
|
+
await resumeCodeEditorAgent(codeEditorId, 'hardening-commit', {
|
|
1578
|
+
head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
|
|
1579
|
+
})
|
|
1541
1580
|
return { hardeningPrOpened: true }
|
|
1542
1581
|
}
|
|
1543
1582
|
|
|
1544
|
-
/**
|
|
1545
|
-
* Spawn the convergence-summary agent at finalize so its StructuredOutput is
|
|
1546
|
-
* recorded into the run journal for the closing report to read. The agent groups
|
|
1547
|
-
* the deduped findings into plain-language issue classes, translates reviewer
|
|
1548
|
-
* jargon to everyday English, and writes one BLUF verdict line. The side effect
|
|
1549
|
-
* is the journal record; the return value is discarded by the caller.
|
|
1550
|
-
* @param {Array<object>} distinctFindings deduped findings across every round
|
|
1551
|
-
* @param {Array<string>} fixSummaries per-round fix-lens one-line summaries
|
|
1552
|
-
* @param {number} roundCount the number of converge rounds the run took
|
|
1553
|
-
* @param {string|null} standardsNote deferral note when a round was code-standard-only
|
|
1554
|
-
* @param {string|null} copilotNote outage note when the Copilot gate was bypassed
|
|
1555
|
-
* @returns {Promise<object>} CONVERGENCE_SUMMARY_SCHEMA result (journal side effect)
|
|
1556
|
-
*/
|
|
1557
|
-
function spawnConvergenceSummary(distinctFindings, fixSummaries, roundCount, standardsNote, copilotNote) {
|
|
1558
|
-
const findingsBlock = distinctFindings.length
|
|
1559
|
-
? distinctFindings
|
|
1560
|
-
.map((each, position) => {
|
|
1561
|
-
const truncatedDetail = (each.detail || '').slice(0, 400)
|
|
1562
|
-
return `${position + 1}. [${each.severity}/${each.category}] ${each.file}:${each.line} — ${each.title} :: ${truncatedDetail}`
|
|
1563
|
-
})
|
|
1564
|
-
.join('\n')
|
|
1565
|
-
: 'none — every lens was clean on a stable HEAD'
|
|
1566
|
-
const fixSummariesBlock = fixSummaries.length
|
|
1567
|
-
? fixSummaries.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
1568
|
-
: 'none'
|
|
1569
|
-
const standardsBlock = standardsNote ? `\nDeferred code-standard note: ${standardsNote}\n` : ''
|
|
1570
|
-
const copilotBlock = copilotNote ? `\nCopilot gate note: ${copilotNote}\n` : ''
|
|
1571
|
-
return convergeAgent(
|
|
1572
|
-
`You write the plain-language convergence summary for ${prCoordinates}. The autoconverge run reached convergence in ${roundCount} round(s). Use ONLY the findings and fix summaries below; invent nothing not present.\n\n` +
|
|
1573
|
-
`Distinct findings caught across the run (already deduped):\n${findingsBlock}\n\n` +
|
|
1574
|
-
`Per-round fix summaries:\n${fixSummariesBlock}\n${standardsBlock}${copilotBlock}\n` +
|
|
1575
|
-
`Produce a summary an everyday reader understands:\n` +
|
|
1576
|
-
`- GROUP near-duplicate findings into issue CLASSES: the same KIND of problem across different files or lines becomes ONE class with a count. Example: seven "Missing return type annotation on test function" findings become one class with count 7.\n` +
|
|
1577
|
-
`- TRANSLATE reviewer jargon into plain everyday English. Examples: "CodingGuidelineID 1000000 / Repository guideline (Types)" -> "a typing rule the project enforces"; "missing return type annotation / Add -> None" -> "a test did not declare what it returns"; "Banned identifier result" -> "a vague variable name the project bans"; a magic-value finding -> "a raw number or string that should be a named value".\n` +
|
|
1578
|
-
`- plainName must carry NO tool token, rule id, file path, line number, severity code (P0/P1/P2), or bot name.\n` +
|
|
1579
|
-
`- Lead with category 'bug' classes, then 'code-standard'. Cap to about 5 classes. whatItWas is at most 2 sentences. No paragraphs.\n` +
|
|
1580
|
-
`- status is 'fixed' unless the fix summaries or the deferred code-standard note mark the class deferred, in which case status is 'deferred'.\n` +
|
|
1581
|
-
`- Use NO hedging words anywhere (likely, probably, should, appears, seems, may, might, could, possibly). State facts ("caught and fixed").\n` +
|
|
1582
|
-
`- When there are zero findings, return issueClasses: [] and a verdictLine stating the run converged with no issues caught.\n` +
|
|
1583
|
-
`- verdictLine is one factual sentence naming the round count and that all classes are fixed or deferred.\n\n` +
|
|
1584
|
-
`Return strictly the schema.`,
|
|
1585
|
-
{ label: 'convergence-summary', phase: 'Finalize', schema: CONVERGENCE_SUMMARY_SCHEMA, agentType: 'general-purpose' },
|
|
1586
|
-
)
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
1583
|
let phase = 'CONVERGE'
|
|
1590
1584
|
let head = null
|
|
1591
1585
|
let rounds = 0
|
|
@@ -1596,25 +1590,24 @@ let copilotDown = false
|
|
|
1596
1590
|
let copilotNote = null
|
|
1597
1591
|
let standardsNote = null
|
|
1598
1592
|
let reuseNote = null
|
|
1599
|
-
const allRoundFindings = []
|
|
1600
|
-
const fixSummaries = []
|
|
1601
1593
|
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1594
|
+
const gitAgentId = await spawnGitAgent()
|
|
1595
|
+
|
|
1596
|
+
const preflightHead = await resumeGitAgent(gitAgentId, 'resolve-head')
|
|
1597
|
+
if (isResolvedHeadUsable(preflightHead?.sha)) {
|
|
1598
|
+
await resolveMergeConflicts(preflightHead.sha, gitAgentId)
|
|
1605
1599
|
}
|
|
1606
1600
|
|
|
1607
1601
|
log('Reuse pass: scanning the full diff for certain, behaviorally identical, autonomously implementable reuse improvements before convergence')
|
|
1608
|
-
const
|
|
1602
|
+
const reuseHeadResult = await resumeGitAgent(gitAgentId, 'resolve-head')
|
|
1603
|
+
const reuseHead = reuseHeadResult?.sha
|
|
1609
1604
|
if (isResolvedHeadUsable(reuseHead)) {
|
|
1610
|
-
await
|
|
1605
|
+
await resumeGitAgent(gitAgentId, 'prefetch-main')
|
|
1611
1606
|
const reuse = await runReuseAuditPass(reuseHead)
|
|
1612
1607
|
const reuseFindings = reuse?.findings || []
|
|
1613
1608
|
if (reuseFindings.length > 0) {
|
|
1614
1609
|
log(`Reuse pass: ${reuseFindings.length} qualifying reuse improvement(s) — applying before convergence`)
|
|
1615
|
-
allRoundFindings.push(...reuseFindings)
|
|
1616
1610
|
const reuseFix = await applyFixes(reuseHead, reuseFindings, 'reuse-pass')
|
|
1617
|
-
if (reuseFix?.summary) fixSummaries.push(reuseFix.summary)
|
|
1618
1611
|
reuseNote = reuseFix?.pushed === true
|
|
1619
1612
|
? `${reuseFindings.length} reuse improvement(s) applied before convergence (${reuseFix.newSha?.slice(0, SHA_COMPARISON_PREFIX_LENGTH)})`
|
|
1620
1613
|
: `${reuseFindings.length} reuse improvement(s) identified before convergence but not landed — the code-review lens re-surfaces any that remain`
|
|
@@ -1625,16 +1618,19 @@ if (isResolvedHeadUsable(reuseHead)) {
|
|
|
1625
1618
|
log('Reuse pass: could not resolve HEAD — proceeding to convergence')
|
|
1626
1619
|
}
|
|
1627
1620
|
|
|
1621
|
+
const convergenceId = await spawnConvergenceCheckAgent()
|
|
1622
|
+
|
|
1628
1623
|
while (iterations < CONFIG.maxIterations) {
|
|
1629
1624
|
iterations += 1
|
|
1630
1625
|
if (phase === 'CONVERGE') {
|
|
1631
1626
|
rounds += 1
|
|
1632
|
-
|
|
1627
|
+
const headResult = await resumeGitAgent(gitAgentId, 'resolve-head')
|
|
1628
|
+
head = headResult?.sha
|
|
1633
1629
|
if (!isResolvedHeadUsable(head)) {
|
|
1634
1630
|
log(`Round ${rounds}: resolve-head agent returned no SHA — retrying without spawning lenses`)
|
|
1635
1631
|
continue
|
|
1636
1632
|
}
|
|
1637
|
-
await
|
|
1633
|
+
await resumeGitAgent(gitAgentId, 'prefetch-main')
|
|
1638
1634
|
log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
|
|
1639
1635
|
const lenses = await parallel([
|
|
1640
1636
|
() => runBugbotLens(head),
|
|
@@ -1650,10 +1646,10 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1650
1646
|
const findings = roundOutcome.findings
|
|
1651
1647
|
if (isStandardsOnlyRound(findings)) {
|
|
1652
1648
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
1653
|
-
|
|
1654
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
1649
|
+
const generalId = await spawnGeneralUtilityAgent()
|
|
1650
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round', generalId)
|
|
1655
1651
|
standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1656
|
-
const auditResult = await
|
|
1652
|
+
const auditResult = await resumeGeneralUtilityAgent(generalId, 'post-clean-audit', { head })
|
|
1657
1653
|
if (!auditResult?.posted) {
|
|
1658
1654
|
blocker = cleanAuditBlocker(head, auditResult)
|
|
1659
1655
|
break
|
|
@@ -1663,9 +1659,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1663
1659
|
}
|
|
1664
1660
|
if (findings.length > 0) {
|
|
1665
1661
|
log(`Round ${rounds}: ${findings.length} finding(s) — applying fixes`)
|
|
1666
|
-
allRoundFindings.push(...findings)
|
|
1667
1662
|
const fixResult = await applyFixes(head, findings, 'converge-round')
|
|
1668
|
-
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
1669
1663
|
const hadThreadBearingFinding = findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
1670
1664
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
1671
1665
|
if (!fixProgress.progressed) {
|
|
@@ -1681,7 +1675,8 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1681
1675
|
continue
|
|
1682
1676
|
}
|
|
1683
1677
|
log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
|
|
1684
|
-
const
|
|
1678
|
+
const cleanGeneralId = await spawnGeneralUtilityAgent()
|
|
1679
|
+
const auditResult = await resumeGeneralUtilityAgent(cleanGeneralId, 'post-clean-audit', { head })
|
|
1685
1680
|
if (!auditResult?.posted) {
|
|
1686
1681
|
blocker = cleanAuditBlocker(head, auditResult)
|
|
1687
1682
|
break
|
|
@@ -1709,8 +1704,8 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1709
1704
|
if (copilotOutcome.kind === 'fix') {
|
|
1710
1705
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
1711
1706
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
1712
|
-
|
|
1713
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
1707
|
+
const copilotGeneralId = await spawnGeneralUtilityAgent()
|
|
1708
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot', copilotGeneralId)
|
|
1714
1709
|
standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1715
1710
|
copilotDown = false
|
|
1716
1711
|
copilotNote = null
|
|
@@ -1718,9 +1713,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1718
1713
|
continue
|
|
1719
1714
|
}
|
|
1720
1715
|
log(`Copilot raised ${copilotOutcome.findings.length} finding(s) — fixing and re-converging`)
|
|
1721
|
-
allRoundFindings.push(...copilotOutcome.findings)
|
|
1722
1716
|
const fixResult = await applyFixes(head, copilotOutcome.findings, 'copilot')
|
|
1723
|
-
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
1724
1717
|
const hadThreadBearingFinding = copilotOutcome.findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
1725
1718
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
1726
1719
|
if (!fixProgress.progressed) {
|
|
@@ -1739,17 +1732,17 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1739
1732
|
}
|
|
1740
1733
|
|
|
1741
1734
|
if (phase === 'FINALIZE') {
|
|
1742
|
-
const convergence = await
|
|
1735
|
+
const convergence = await resumeConvergenceCheckAgent(convergenceId, { bugbotDown, copilotDown })
|
|
1743
1736
|
const convergenceOutcome = classifyConvergenceOutcome(convergence)
|
|
1744
1737
|
if (convergenceOutcome.kind === 'retry') {
|
|
1745
1738
|
log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
|
|
1746
1739
|
continue
|
|
1747
1740
|
}
|
|
1748
1741
|
if (convergenceOutcome.kind === 'ready') {
|
|
1749
|
-
const
|
|
1742
|
+
const finalGeneralId = await spawnGeneralUtilityAgent()
|
|
1743
|
+
const readyResult = await resumeGeneralUtilityAgent(finalGeneralId, 'mark-ready', { head, copilotDown })
|
|
1750
1744
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
1751
1745
|
if (readyOutcome.converged) {
|
|
1752
|
-
await spawnConvergenceSummary(dedupeFindings(allRoundFindings), fixSummaries, rounds, standardsNote, copilotNote)
|
|
1753
1746
|
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote }
|
|
1754
1747
|
}
|
|
1755
1748
|
blocker = readyOutcome.blocker
|