claude-dev-env 1.59.0 → 1.60.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/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +30 -15
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.mjs +128 -6
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
const workflowDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
|
|
9
|
-
const gotchasSource = readFileSync(
|
|
10
|
-
join(workflowDirectory, '..', 'reference', 'gotchas.md'),
|
|
11
|
-
'utf8',
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
function lensPromptBody(builderName) {
|
|
15
|
-
const builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
16
|
-
assert.notEqual(builderStart, -1, `expected ${builderName} to exist`);
|
|
17
|
-
const nextBuilderStart = convergeSource.indexOf('\nfunction ', builderStart + 1);
|
|
18
|
-
const builderEnd = nextBuilderStart === -1 ? convergeSource.length : nextBuilderStart;
|
|
19
|
-
return convergeSource.slice(builderStart, builderEnd);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
23
|
-
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
27
|
-
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
31
|
-
assert.match(convergeSource, /function prefetchMainForRound\(/);
|
|
32
|
-
const prefetchCallIndex = convergeSource.indexOf('await prefetchMainForRound(');
|
|
33
|
-
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
34
|
-
assert.notEqual(prefetchCallIndex, -1, 'expected prefetchMainForRound to be invoked');
|
|
35
|
-
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
36
|
-
assert.ok(
|
|
37
|
-
prefetchCallIndex < parallelLensIndex,
|
|
38
|
-
'expected the round prefetch to run before the parallel lenses spawn',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
43
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
44
|
-
assert.doesNotMatch(
|
|
45
|
-
bugbotPrompt,
|
|
46
|
-
/use the existing scripts; pass --owner/,
|
|
47
|
-
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
52
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
53
|
-
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
54
|
-
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
55
|
-
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
56
|
-
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
57
|
-
assert.match(invocationLine, /--reviewer bugbot/);
|
|
58
|
-
assert.doesNotMatch(
|
|
59
|
-
invocationLine,
|
|
60
|
-
/--owner|--repo/,
|
|
61
|
-
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
66
|
-
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
67
|
-
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('repair-convergence filters unresolved threads to bot authors and skips human threads', () => {
|
|
71
|
-
const repairPrompt = lensPromptBody('repairConvergence');
|
|
72
|
-
assert.match(
|
|
73
|
-
repairPrompt,
|
|
74
|
-
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
75
|
-
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
76
|
-
);
|
|
77
|
-
assert.match(
|
|
78
|
-
repairPrompt,
|
|
79
|
-
/skip.*human|human.*skip/is,
|
|
80
|
-
'expected an explicit instruction to skip human reviewer threads',
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('repair-convergence no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
85
|
-
const repairPrompt = lensPromptBody('repairConvergence');
|
|
86
|
-
assert.doesNotMatch(
|
|
87
|
-
repairPrompt,
|
|
88
|
-
/fetch every thread where isResolved is false/,
|
|
89
|
-
'the unfiltered instruction could resolve human reviewer threads',
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('bugbot lens delay instructions are shell-agnostic with PowerShell as an alternative', () => {
|
|
94
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
95
|
-
assert.match(bugbotPrompt, /sleep 60/, 'expected a shell-agnostic 60-second poll delay');
|
|
96
|
-
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
97
|
-
assert.match(
|
|
98
|
-
bugbotPrompt,
|
|
99
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
100
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
101
|
-
);
|
|
102
|
-
assert.doesNotMatch(
|
|
103
|
-
bugbotPrompt,
|
|
104
|
-
/wait 8 seconds(?!,)/,
|
|
105
|
-
'the vague "wait 8 seconds" phrasing must carry a concrete command',
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('copilot gate delay instruction is shell-agnostic with PowerShell as an alternative', () => {
|
|
110
|
-
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
111
|
-
assert.match(copilotPrompt, /sleep 360/, 'expected a shell-agnostic 360-second poll delay');
|
|
112
|
-
assert.match(
|
|
113
|
-
copilotPrompt,
|
|
114
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
115
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('gotchas doc describes the reviewer wait as shell-agnostic', () => {
|
|
120
|
-
assert.match(
|
|
121
|
-
gotchasSource,
|
|
122
|
-
/\bsleep\b/i,
|
|
123
|
-
'expected the wait guidance to name a shell-agnostic sleep',
|
|
124
|
-
);
|
|
125
|
-
assert.doesNotMatch(
|
|
126
|
-
gotchasSource,
|
|
127
|
-
/a single PowerShell\s*`?Start-Sleep`?\s*loop/i,
|
|
128
|
-
'PowerShell Start-Sleep must be an alternative, not the sole mechanism',
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
function finalizeRepairBranch() {
|
|
133
|
-
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
134
|
-
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
135
|
-
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
136
|
-
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
137
|
-
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
138
|
-
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
139
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
140
|
-
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
144
|
-
assert.doesNotMatch(
|
|
145
|
-
finalizeRepairBranch(),
|
|
146
|
-
/head\s*=\s*repair/,
|
|
147
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
function fixBranchAfter(branchLabel) {
|
|
152
|
-
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
153
|
-
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
154
|
-
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
155
|
-
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
156
|
-
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
157
|
-
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
158
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
159
|
-
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
163
|
-
assert.doesNotMatch(
|
|
164
|
-
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
165
|
-
/head\s*=\s*fixProgress/,
|
|
166
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
171
|
-
assert.doesNotMatch(
|
|
172
|
-
fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
|
|
173
|
-
/head\s*=\s*fixProgress/,
|
|
174
|
-
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
179
|
-
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
180
|
-
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
181
|
-
const resolveHeadIndex = convergeSource.indexOf('head = await resolveHead()', convergeBranchStart);
|
|
182
|
-
assert.notEqual(resolveHeadIndex, -1, 'expected CONVERGE to re-resolve HEAD via resolveHead()');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('fix prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
186
|
-
const fixPrompt = lensPromptBody('applyFixes');
|
|
187
|
-
assert.match(fixPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
188
|
-
assert.match(
|
|
189
|
-
fixPrompt,
|
|
190
|
-
/databaseId/,
|
|
191
|
-
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
192
|
-
);
|
|
193
|
-
assert.match(
|
|
194
|
-
fixPrompt,
|
|
195
|
-
/not the numeric comment id/,
|
|
196
|
-
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('fix prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
201
|
-
assert.doesNotMatch(
|
|
202
|
-
lensPromptBody('applyFixes'),
|
|
203
|
-
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
204
|
-
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
205
|
-
);
|
|
206
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const workflowDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
|
|
9
|
+
const gotchasSource = readFileSync(
|
|
10
|
+
join(workflowDirectory, '..', 'reference', 'gotchas.md'),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function lensPromptBody(builderName) {
|
|
15
|
+
const builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
16
|
+
assert.notEqual(builderStart, -1, `expected ${builderName} to exist`);
|
|
17
|
+
const nextBuilderStart = convergeSource.indexOf('\nfunction ', builderStart + 1);
|
|
18
|
+
const builderEnd = nextBuilderStart === -1 ? convergeSource.length : nextBuilderStart;
|
|
19
|
+
return convergeSource.slice(builderStart, builderEnd);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
23
|
+
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
27
|
+
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
31
|
+
assert.match(convergeSource, /function prefetchMainForRound\(/);
|
|
32
|
+
const prefetchCallIndex = convergeSource.indexOf('await prefetchMainForRound(');
|
|
33
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
34
|
+
assert.notEqual(prefetchCallIndex, -1, 'expected prefetchMainForRound to be invoked');
|
|
35
|
+
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
36
|
+
assert.ok(
|
|
37
|
+
prefetchCallIndex < parallelLensIndex,
|
|
38
|
+
'expected the round prefetch to run before the parallel lenses spawn',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
43
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
44
|
+
assert.doesNotMatch(
|
|
45
|
+
bugbotPrompt,
|
|
46
|
+
/use the existing scripts; pass --owner/,
|
|
47
|
+
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
52
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
53
|
+
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
54
|
+
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
55
|
+
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
56
|
+
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
57
|
+
assert.match(invocationLine, /--reviewer bugbot/);
|
|
58
|
+
assert.doesNotMatch(
|
|
59
|
+
invocationLine,
|
|
60
|
+
/--owner|--repo/,
|
|
61
|
+
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
66
|
+
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
67
|
+
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('repair-convergence filters unresolved threads to bot authors and skips human threads', () => {
|
|
71
|
+
const repairPrompt = lensPromptBody('repairConvergence');
|
|
72
|
+
assert.match(
|
|
73
|
+
repairPrompt,
|
|
74
|
+
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
75
|
+
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
76
|
+
);
|
|
77
|
+
assert.match(
|
|
78
|
+
repairPrompt,
|
|
79
|
+
/skip.*human|human.*skip/is,
|
|
80
|
+
'expected an explicit instruction to skip human reviewer threads',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('repair-convergence no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
85
|
+
const repairPrompt = lensPromptBody('repairConvergence');
|
|
86
|
+
assert.doesNotMatch(
|
|
87
|
+
repairPrompt,
|
|
88
|
+
/fetch every thread where isResolved is false/,
|
|
89
|
+
'the unfiltered instruction could resolve human reviewer threads',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('bugbot lens delay instructions are shell-agnostic with PowerShell as an alternative', () => {
|
|
94
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
95
|
+
assert.match(bugbotPrompt, /sleep 60/, 'expected a shell-agnostic 60-second poll delay');
|
|
96
|
+
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
97
|
+
assert.match(
|
|
98
|
+
bugbotPrompt,
|
|
99
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
100
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
101
|
+
);
|
|
102
|
+
assert.doesNotMatch(
|
|
103
|
+
bugbotPrompt,
|
|
104
|
+
/wait 8 seconds(?!,)/,
|
|
105
|
+
'the vague "wait 8 seconds" phrasing must carry a concrete command',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('copilot gate delay instruction is shell-agnostic with PowerShell as an alternative', () => {
|
|
110
|
+
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
111
|
+
assert.match(copilotPrompt, /sleep 360/, 'expected a shell-agnostic 360-second poll delay');
|
|
112
|
+
assert.match(
|
|
113
|
+
copilotPrompt,
|
|
114
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
115
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('gotchas doc describes the reviewer wait as shell-agnostic', () => {
|
|
120
|
+
assert.match(
|
|
121
|
+
gotchasSource,
|
|
122
|
+
/\bsleep\b/i,
|
|
123
|
+
'expected the wait guidance to name a shell-agnostic sleep',
|
|
124
|
+
);
|
|
125
|
+
assert.doesNotMatch(
|
|
126
|
+
gotchasSource,
|
|
127
|
+
/a single PowerShell\s*`?Start-Sleep`?\s*loop/i,
|
|
128
|
+
'PowerShell Start-Sleep must be an alternative, not the sole mechanism',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function finalizeRepairBranch() {
|
|
133
|
+
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
134
|
+
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
135
|
+
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
136
|
+
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
137
|
+
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
138
|
+
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
139
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
140
|
+
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
144
|
+
assert.doesNotMatch(
|
|
145
|
+
finalizeRepairBranch(),
|
|
146
|
+
/head\s*=\s*repair/,
|
|
147
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function fixBranchAfter(branchLabel) {
|
|
152
|
+
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
153
|
+
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
154
|
+
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
155
|
+
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
156
|
+
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
157
|
+
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
158
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
159
|
+
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
163
|
+
assert.doesNotMatch(
|
|
164
|
+
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
165
|
+
/head\s*=\s*fixProgress/,
|
|
166
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
171
|
+
assert.doesNotMatch(
|
|
172
|
+
fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
|
|
173
|
+
/head\s*=\s*fixProgress/,
|
|
174
|
+
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
179
|
+
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
180
|
+
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
181
|
+
const resolveHeadIndex = convergeSource.indexOf('head = await resolveHead()', convergeBranchStart);
|
|
182
|
+
assert.notEqual(resolveHeadIndex, -1, 'expected CONVERGE to re-resolve HEAD via resolveHead()');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('fix prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
186
|
+
const fixPrompt = lensPromptBody('applyFixes');
|
|
187
|
+
assert.match(fixPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
188
|
+
assert.match(
|
|
189
|
+
fixPrompt,
|
|
190
|
+
/databaseId/,
|
|
191
|
+
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
192
|
+
);
|
|
193
|
+
assert.match(
|
|
194
|
+
fixPrompt,
|
|
195
|
+
/not the numeric comment id/,
|
|
196
|
+
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('fix prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
201
|
+
assert.doesNotMatch(
|
|
202
|
+
lensPromptBody('applyFixes'),
|
|
203
|
+
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
204
|
+
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
205
|
+
);
|
|
206
|
+
});
|
|
@@ -105,6 +105,31 @@ const FIX_SCHEMA = {
|
|
|
105
105
|
required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary'],
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const CONVERGENCE_SUMMARY_SCHEMA = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
properties: {
|
|
112
|
+
verdictLine: { type: 'string', description: 'one factual BLUF sentence: converged?, distinct issue-class count, all fixed or deferred. No hedging words.' },
|
|
113
|
+
issueClasses: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
properties: {
|
|
119
|
+
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' },
|
|
120
|
+
count: { type: 'integer', description: 'number of raw findings grouped into this class' },
|
|
121
|
+
severity: { type: 'string', enum: ['P0', 'P1', 'P2'], description: 'most severe among the class' },
|
|
122
|
+
category: { type: 'string', enum: ['bug', 'code-standard'] },
|
|
123
|
+
status: { type: 'string', enum: ['fixed', 'deferred'] },
|
|
124
|
+
whatItWas: { type: 'string', description: 'at most 2 sentences, plain language, what the problem was' },
|
|
125
|
+
},
|
|
126
|
+
required: ['plainName', 'count', 'severity', 'category', 'status', 'whatItWas'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ['verdictLine', 'issueClasses'],
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
const CONVERGENCE_SCHEMA = {
|
|
109
134
|
type: 'object',
|
|
110
135
|
additionalProperties: false,
|
|
@@ -124,6 +149,17 @@ const READY_SCHEMA = {
|
|
|
124
149
|
required: ['ready'],
|
|
125
150
|
}
|
|
126
151
|
|
|
152
|
+
const CLEAN_AUDIT_SCHEMA = {
|
|
153
|
+
type: 'object',
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
properties: {
|
|
156
|
+
posted: { type: 'boolean', description: 'true only when post_audit_thread.py printed the review URL confirming the CLEAN bugteam review landed on HEAD' },
|
|
157
|
+
reviewUrl: { type: 'string', description: 'the posted review URL when posted is true, otherwise an empty string' },
|
|
158
|
+
reason: { type: 'string', description: 'when posted is false, the one-line reason the post did not land (a permission denial, a classifier block, or a script error)' },
|
|
159
|
+
},
|
|
160
|
+
required: ['posted', 'reviewUrl', 'reason'],
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
const SEVERITY_RANK = { P0: 0, P1: 1, P2: 2 }
|
|
128
164
|
const SHA_COMPARISON_PREFIX_LENGTH = 7
|
|
129
165
|
|
|
@@ -579,17 +615,41 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
579
615
|
|
|
580
616
|
/**
|
|
581
617
|
* Post the terminal CLEAN bugteam audit artifact so check_convergence.py sees
|
|
582
|
-
* a clean bugteam review on the converged HEAD.
|
|
618
|
+
* a clean bugteam review on the converged HEAD. The post is load-bearing: the
|
|
619
|
+
* convergence gate's bugteam-review check can never pass until this review
|
|
620
|
+
* lands, so the result reports whether the post succeeded rather than
|
|
621
|
+
* discarding it. A blocked post (a permission or auto-mode-classifier denial)
|
|
622
|
+
* or a script error returns posted:false with the reason so the caller can
|
|
623
|
+
* surface a blocker instead of re-converging into the iteration cap.
|
|
583
624
|
* @param {string} head converged PR HEAD SHA
|
|
584
|
-
* @returns {Promise<
|
|
625
|
+
* @returns {Promise<object>} CLEAN_AUDIT_SCHEMA result
|
|
585
626
|
*/
|
|
586
627
|
function postCleanAudit(head) {
|
|
587
628
|
return convergeAgent(
|
|
588
629
|
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${head}. All review lenses are clean on this HEAD.\n\n` +
|
|
589
630
|
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
590
631
|
`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` +
|
|
591
|
-
`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
|
|
592
|
-
|
|
632
|
+
`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` +
|
|
633
|
+
`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.`,
|
|
634
|
+
{ label: 'post-clean-audit', phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Blocker message for a CLEAN bugteam audit that did not land. The convergence
|
|
640
|
+
* gate's bugteam-review check can never pass without this review, so a blocked
|
|
641
|
+
* post stops the run with an actionable message rather than re-converging until
|
|
642
|
+
* the iteration cap. Handles a dead post agent (a null result) as not posted.
|
|
643
|
+
* @param {string} head converged PR HEAD SHA
|
|
644
|
+
* @param {object} auditResult CLEAN_AUDIT_SCHEMA result from postCleanAudit, or null when the agent died
|
|
645
|
+
* @returns {string} the blocker message naming the post failure and the unblock path
|
|
646
|
+
*/
|
|
647
|
+
function cleanAuditBlocker(head, auditResult) {
|
|
648
|
+
const reason = auditResult?.reason || 'the post agent returned no result'
|
|
649
|
+
return (
|
|
650
|
+
`clean-audit post blocked: the CLEAN bugteam review could not be posted on HEAD ${head} (${reason}) — ` +
|
|
651
|
+
`the convergence gate's bugteam-review check can never pass without it, so the run stops rather than re-converge to the iteration cap. ` +
|
|
652
|
+
`Allow post_audit_thread.py for this run with a Bash permission rule, or post the CLEAN review by hand, then re-run.`
|
|
593
653
|
)
|
|
594
654
|
}
|
|
595
655
|
|
|
@@ -730,6 +790,51 @@ function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
|
730
790
|
)
|
|
731
791
|
}
|
|
732
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Spawn the convergence-summary agent at finalize so its StructuredOutput is
|
|
795
|
+
* recorded into the run journal for the closing report to read. The agent groups
|
|
796
|
+
* the deduped findings into plain-language issue classes, translates reviewer
|
|
797
|
+
* jargon to everyday English, and writes one BLUF verdict line. The side effect
|
|
798
|
+
* is the journal record; the return value is discarded by the caller.
|
|
799
|
+
* @param {Array<object>} distinctFindings deduped findings across every round
|
|
800
|
+
* @param {Array<string>} fixSummaries per-round fix-lens one-line summaries
|
|
801
|
+
* @param {number} roundCount the number of converge rounds the run took
|
|
802
|
+
* @param {string|null} standardsNote deferral note when a round was code-standard-only
|
|
803
|
+
* @param {string|null} copilotNote outage note when the Copilot gate was bypassed
|
|
804
|
+
* @returns {Promise<object>} CONVERGENCE_SUMMARY_SCHEMA result (journal side effect)
|
|
805
|
+
*/
|
|
806
|
+
function spawnConvergenceSummary(distinctFindings, fixSummaries, roundCount, standardsNote, copilotNote) {
|
|
807
|
+
const findingsBlock = distinctFindings.length
|
|
808
|
+
? distinctFindings
|
|
809
|
+
.map((each, position) => {
|
|
810
|
+
const truncatedDetail = (each.detail || '').slice(0, 400)
|
|
811
|
+
return `${position + 1}. [${each.severity}/${each.category}] ${each.file}:${each.line} — ${each.title} :: ${truncatedDetail}`
|
|
812
|
+
})
|
|
813
|
+
.join('\n')
|
|
814
|
+
: 'none — every lens was clean on a stable HEAD'
|
|
815
|
+
const fixSummariesBlock = fixSummaries.length
|
|
816
|
+
? fixSummaries.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
817
|
+
: 'none'
|
|
818
|
+
const standardsBlock = standardsNote ? `\nDeferred code-standard note: ${standardsNote}\n` : ''
|
|
819
|
+
const copilotBlock = copilotNote ? `\nCopilot gate note: ${copilotNote}\n` : ''
|
|
820
|
+
return convergeAgent(
|
|
821
|
+
`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` +
|
|
822
|
+
`Distinct findings caught across the run (already deduped):\n${findingsBlock}\n\n` +
|
|
823
|
+
`Per-round fix summaries:\n${fixSummariesBlock}\n${standardsBlock}${copilotBlock}\n` +
|
|
824
|
+
`Produce a summary an everyday reader understands:\n` +
|
|
825
|
+
`- 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` +
|
|
826
|
+
`- 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` +
|
|
827
|
+
`- plainName must carry NO tool token, rule id, file path, line number, severity code (P0/P1/P2), or bot name.\n` +
|
|
828
|
+
`- Lead with category 'bug' classes, then 'code-standard'. Cap to about 5 classes. whatItWas is at most 2 sentences. No paragraphs.\n` +
|
|
829
|
+
`- status is 'fixed' unless the fix summaries or the deferred code-standard note mark the class deferred, in which case status is 'deferred'.\n` +
|
|
830
|
+
`- Use NO hedging words anywhere (likely, probably, should, appears, seems, may, might, could, possibly). State facts ("caught and fixed").\n` +
|
|
831
|
+
`- When there are zero findings, return issueClasses: [] and a verdictLine stating the run converged with no issues caught.\n` +
|
|
832
|
+
`- verdictLine is one factual sentence naming the round count and that all classes are fixed or deferred.\n\n` +
|
|
833
|
+
`Return strictly the schema.`,
|
|
834
|
+
{ label: 'convergence-summary', phase: 'Finalize', schema: CONVERGENCE_SUMMARY_SCHEMA, agentType: 'general-purpose' },
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
|
|
733
838
|
let phase = 'CONVERGE'
|
|
734
839
|
let head = null
|
|
735
840
|
let rounds = 0
|
|
@@ -739,6 +844,8 @@ let bugbotDown = input.bugbotDisabled || false
|
|
|
739
844
|
let copilotDown = false
|
|
740
845
|
let copilotNote = null
|
|
741
846
|
let standardsNote = null
|
|
847
|
+
const allRoundFindings = []
|
|
848
|
+
const fixSummaries = []
|
|
742
849
|
|
|
743
850
|
while (iterations < CONFIG.maxIterations) {
|
|
744
851
|
iterations += 1
|
|
@@ -765,15 +872,22 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
765
872
|
const findings = roundOutcome.findings
|
|
766
873
|
if (isStandardsOnlyRound(findings)) {
|
|
767
874
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
875
|
+
allRoundFindings.push(...findings)
|
|
768
876
|
await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
769
877
|
standardsNote = `${findings.length} code-standard finding(s) deferred to a follow-up fix issue plus an environment-hardening PR — verify both land`
|
|
770
|
-
await postCleanAudit(head)
|
|
878
|
+
const auditResult = await postCleanAudit(head)
|
|
879
|
+
if (!auditResult?.posted) {
|
|
880
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
881
|
+
break
|
|
882
|
+
}
|
|
771
883
|
phase = 'COPILOT'
|
|
772
884
|
continue
|
|
773
885
|
}
|
|
774
886
|
if (findings.length > 0) {
|
|
775
887
|
log(`Round ${rounds}: ${findings.length} finding(s) — applying fixes`)
|
|
888
|
+
allRoundFindings.push(...findings)
|
|
776
889
|
const fixResult = await applyFixes(head, findings, 'converge-round')
|
|
890
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
777
891
|
const hadThreadBearingFinding = findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
778
892
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
779
893
|
if (!fixProgress.progressed) {
|
|
@@ -789,7 +903,11 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
789
903
|
continue
|
|
790
904
|
}
|
|
791
905
|
log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
|
|
792
|
-
await postCleanAudit(head)
|
|
906
|
+
const auditResult = await postCleanAudit(head)
|
|
907
|
+
if (!auditResult?.posted) {
|
|
908
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
909
|
+
break
|
|
910
|
+
}
|
|
793
911
|
phase = 'COPILOT'
|
|
794
912
|
continue
|
|
795
913
|
}
|
|
@@ -813,6 +931,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
813
931
|
if (copilotOutcome.kind === 'fix') {
|
|
814
932
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
815
933
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
934
|
+
allRoundFindings.push(...copilotOutcome.findings)
|
|
816
935
|
await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
817
936
|
standardsNote = `${copilotOutcome.findings.length} code-standard finding(s) deferred to a follow-up fix issue plus an environment-hardening PR — verify both land`
|
|
818
937
|
copilotDown = false
|
|
@@ -821,7 +940,9 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
821
940
|
continue
|
|
822
941
|
}
|
|
823
942
|
log(`Copilot raised ${copilotOutcome.findings.length} finding(s) — fixing and re-converging`)
|
|
943
|
+
allRoundFindings.push(...copilotOutcome.findings)
|
|
824
944
|
const fixResult = await applyFixes(head, copilotOutcome.findings, 'copilot')
|
|
945
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
825
946
|
const hadThreadBearingFinding = copilotOutcome.findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
826
947
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
827
948
|
if (!fixProgress.progressed) {
|
|
@@ -850,6 +971,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
850
971
|
const readyResult = await markReady(head, copilotDown)
|
|
851
972
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
852
973
|
if (readyOutcome.converged) {
|
|
974
|
+
await spawnConvergenceSummary(dedupeFindings(allRoundFindings), fixSummaries, rounds, standardsNote, copilotNote)
|
|
853
975
|
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote }
|
|
854
976
|
}
|
|
855
977
|
blocker = readyOutcome.blocker
|