claude-dev-env 1.86.1 → 1.87.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/package.json +1 -1
- package/skills/autoconverge/SKILL.md +10 -3
- package/skills/autoconverge/reference/convergence.md +25 -5
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +176 -6
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +13 -13
- package/skills/autoconverge/workflow/converge.mjs +131 -83
package/package.json
CHANGED
|
@@ -324,7 +324,10 @@ out of a Bash command string entirely and independent of which auto-allow path
|
|
|
324
324
|
matches.
|
|
325
325
|
|
|
326
326
|
- **Converge:** `parallel([Bugbot lens, code-review lens, bug-audit lens])` on
|
|
327
|
-
the current HEAD, full `origin/main...HEAD` diff.
|
|
327
|
+
the current HEAD, full `origin/main...HEAD` diff. The preflight step fetches
|
|
328
|
+
`origin/main` once for the round and enumerates the diff (changed-file list plus
|
|
329
|
+
diffstat); each lens receives that list and reads only the files it needs rather
|
|
330
|
+
than re-deriving the diff, forming its own review judgment. Dedup findings; one
|
|
328
331
|
`clean-coder` applies the round's fixes per the `pr-fix-protocol` skill
|
|
329
332
|
(`../pr-fix-protocol/SKILL.md`) — fix, reply, resolve — landing every fix in
|
|
330
333
|
one commit per round, which the workflow journal records; re-verify next
|
|
@@ -344,8 +347,12 @@ matches.
|
|
|
344
347
|
out-of-usage notice (the requester hit their quota) on the HEAD, or surfaces no
|
|
345
348
|
review at all after the configured cap — the gate logs a notice and the run marks the PR
|
|
346
349
|
ready with the Copilot gate bypassed. `copilotNote` records the bypass.
|
|
347
|
-
- **Convergence check:** `check_convergence.py` is the authoritative gate;
|
|
348
|
-
full pass
|
|
350
|
+
- **Convergence check:** `check_convergence.py` is the authoritative gate; one
|
|
351
|
+
agent runs it and, on a full pass, marks `draft=false` in the same turn. Each
|
|
352
|
+
spawned agent runs on the model tier its role needs — opus/medium for the review
|
|
353
|
+
lenses and the code-editing fix steps, sonnet/medium for the verify, commit, and
|
|
354
|
+
recovery steps, haiku/low for the mechanical probes (preflight, Copilot gate,
|
|
355
|
+
CLEAN-audit post, convergence check).
|
|
349
356
|
|
|
350
357
|
## Multiple PRs
|
|
351
358
|
|
|
@@ -46,9 +46,14 @@ tracks CONVERGE passes only and is never the cap.
|
|
|
46
46
|
|
|
47
47
|
**CONVERGE** (one round = one parallel sweep):
|
|
48
48
|
|
|
49
|
-
1. Resolve the current PR HEAD SHA.
|
|
49
|
+
1. Resolve the current PR HEAD SHA. The same preflight step also fetches
|
|
50
|
+
`origin/main` once for the round and enumerates the diff — the
|
|
51
|
+
`git diff --name-status origin/main...HEAD` changed-file list and the
|
|
52
|
+
`git diff --stat` diffstat — and carries both into the round.
|
|
50
53
|
2. Run three lenses in parallel on that HEAD, each over the full
|
|
51
|
-
`origin/main...HEAD` diff
|
|
54
|
+
`origin/main...HEAD` diff. Each lens receives the preflight's changed-file
|
|
55
|
+
list and diffstat and reads only the files it needs from that list rather than
|
|
56
|
+
re-deriving the diff; each lens forms its own review judgment.
|
|
52
57
|
- **Bugbot lens** — drive Cursor Bugbot to a verdict on HEAD (trigger and
|
|
53
58
|
poll its CI check run when needed) and return its findings, or mark itself
|
|
54
59
|
down when Bugbot is opted out or unreachable.
|
|
@@ -89,9 +94,24 @@ tracks CONVERGE passes only and is never the cap.
|
|
|
89
94
|
|
|
90
95
|
**Convergence check**:
|
|
91
96
|
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
failing gate
|
|
97
|
+
- One agent runs `check_convergence.py` and, on a full pass, marks the PR ready
|
|
98
|
+
(`draft=false`) in the same turn, ending the run. A failure returns to CONVERGE
|
|
99
|
+
so the next round addresses the failing gate; the repair path re-runs this same
|
|
100
|
+
check, and only a passing check marks the PR ready.
|
|
101
|
+
|
|
102
|
+
## Model tiers
|
|
103
|
+
|
|
104
|
+
Each spawned agent runs on the model and effort its role needs, so the run spends
|
|
105
|
+
the strongest model only where judgment is dense:
|
|
106
|
+
|
|
107
|
+
- **opus / medium** — the review lenses (Bugbot, code-review, bug-audit, reuse)
|
|
108
|
+
and the code-editing steps that fix findings (fix-edit, conflict-edit,
|
|
109
|
+
repair-edit, standards-edit).
|
|
110
|
+
- **sonnet / medium** — the verify steps, the commit steps, and the recovery
|
|
111
|
+
edits that clear a commit-gate or verdict rejection.
|
|
112
|
+
- **haiku / low** — the mechanical steps: the preflight git-and-availability
|
|
113
|
+
probe, the Copilot gate, the CLEAN-audit post, and the convergence check that
|
|
114
|
+
marks the PR ready.
|
|
95
115
|
|
|
96
116
|
## Full-diff rule
|
|
97
117
|
|
|
@@ -58,7 +58,7 @@ function buildRunGeneralUtilityTask(convergeAgentStub) {
|
|
|
58
58
|
'CONFIG',
|
|
59
59
|
'prCoordinates',
|
|
60
60
|
'CLEAN_AUDIT_SCHEMA',
|
|
61
|
-
'
|
|
61
|
+
'TIERS',
|
|
62
62
|
'describeStandardsDeferral',
|
|
63
63
|
'describeNotRunLens',
|
|
64
64
|
'serializeOneLineJson',
|
|
@@ -70,7 +70,7 @@ function buildRunGeneralUtilityTask(convergeAgentStub) {
|
|
|
70
70
|
{ prLoopScripts: 'x' },
|
|
71
71
|
'coords',
|
|
72
72
|
{},
|
|
73
|
-
{},
|
|
73
|
+
{ haikuLow: { model: 'haiku', effort: 'low' } },
|
|
74
74
|
describeStandardsDeferral,
|
|
75
75
|
describeNotRunLens,
|
|
76
76
|
serializeOneLineJson,
|
|
@@ -36,6 +36,14 @@ function functionSource(functionName) {
|
|
|
36
36
|
return convergeSource.slice(functionStart, functionEnd);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function docstringBefore(functionName) {
|
|
40
|
+
const functionStart = convergeSource.indexOf(`function ${functionName}(`);
|
|
41
|
+
assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
|
|
42
|
+
const docStart = convergeSource.lastIndexOf('/**', functionStart);
|
|
43
|
+
assert.notEqual(docStart, -1, `expected a JSDoc block before ${functionName}`);
|
|
44
|
+
return convergeSource.slice(docStart, functionStart);
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
40
48
|
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
41
49
|
});
|
|
@@ -60,10 +68,36 @@ test('the merged preflight-git task fetches origin/main once before the parallel
|
|
|
60
68
|
assert.match(gitTaskBody, /PREFLIGHT_GIT_SCHEMA/, 'expected the merged task to return the {sha, conflicting, fetched, copilot, bugbot} schema');
|
|
61
69
|
});
|
|
62
70
|
|
|
63
|
-
test('the merged preflight-git agent
|
|
71
|
+
test('the merged preflight-git agent spreads the haikuLow tier', () => {
|
|
64
72
|
const gitTaskBody = functionSource('runGitTask');
|
|
65
|
-
assert.match(
|
|
66
|
-
|
|
73
|
+
assert.match(
|
|
74
|
+
gitTaskBody,
|
|
75
|
+
/\.\.\.TIERS\.haikuLow/,
|
|
76
|
+
'expected the git-utility agent to spread the single-sourced haikuLow tier rather than inline model/effort literals',
|
|
77
|
+
);
|
|
78
|
+
assert.doesNotMatch(
|
|
79
|
+
gitTaskBody,
|
|
80
|
+
/model: 'haiku', effort: 'low'/,
|
|
81
|
+
'expected no inline haikuLow literals duplicating the TIERS definition',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the preflight-git prompt states the correct number of read-only steps it enumerates', () => {
|
|
86
|
+
const body = functionSource('runGitTask');
|
|
87
|
+
assert.match(
|
|
88
|
+
body,
|
|
89
|
+
/Run five read-only preflight steps/,
|
|
90
|
+
'expected the stated step count to match the five enumerated STEP blocks',
|
|
91
|
+
);
|
|
92
|
+
assert.match(body, /STEP 5 —/, 'expected the fifth step to be enumerated');
|
|
93
|
+
assert.doesNotMatch(body, /Run four read-only preflight steps/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the runGitTask docstring names the diff enumeration and its changedFiles/diffstat return fields', () => {
|
|
97
|
+
const doc = docstringBefore('runGitTask');
|
|
98
|
+
assert.match(doc, /diff|changed-file|enumerat/i, 'expected the docstring to name the diff enumeration operation');
|
|
99
|
+
assert.match(doc, /changedFiles/, 'expected the docstring return shape to name changedFiles');
|
|
100
|
+
assert.match(doc, /diffstat/, 'expected the docstring return shape to name diffstat');
|
|
67
101
|
});
|
|
68
102
|
|
|
69
103
|
test('the reviewer-availability probe rides the merged preflight-git spawn, not a separate agent', () => {
|
|
@@ -98,7 +132,7 @@ test('the Bugbot lens is not spawned pre-spawn when the shared gate reports Bugb
|
|
|
98
132
|
const lensArray = convergeSource.slice(parallelLensIndex, lensArrayEnd);
|
|
99
133
|
assert.match(
|
|
100
134
|
lensArray,
|
|
101
|
-
/isBugbotDownPreSpawn \? Promise\.resolve\(\{ sha: head, clean: true, down: true, notSpawned: true, findings: \[\] \}\) : runBugbotLens\(head\)/,
|
|
135
|
+
/isBugbotDownPreSpawn \? Promise\.resolve\(\{ sha: head, clean: true, down: true, notSpawned: true, findings: \[\] \}\) : runBugbotLens\(head, reviewerAvailability\)/,
|
|
102
136
|
'expected the Bugbot lens slot to substitute a resolved down placeholder instead of spawning runBugbotLens when isBugbotDownPreSpawn is true',
|
|
103
137
|
);
|
|
104
138
|
});
|
|
@@ -263,7 +297,7 @@ test('the COPILOT fix branch does not re-assign head from the fix before re-conv
|
|
|
263
297
|
test('the CONVERGE branch refreshes HEAD via the merged preflight-git task only when the threaded head is invalidated', () => {
|
|
264
298
|
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
265
299
|
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
266
|
-
const invalidGuardIndex = convergeSource.indexOf('if (!isResolvedHeadUsable(head)
|
|
300
|
+
const invalidGuardIndex = convergeSource.indexOf('if (!isResolvedHeadUsable(head)', convergeBranchStart);
|
|
267
301
|
const headRefreshCallIndex = convergeSource.indexOf("runGitTask('preflight-git')", convergeBranchStart);
|
|
268
302
|
assert.notEqual(invalidGuardIndex, -1, 'expected CONVERGE to gate the refresh on an invalidated head');
|
|
269
303
|
assert.notEqual(headRefreshCallIndex, -1, 'expected CONVERGE to refresh HEAD via the merged preflight-git task');
|
|
@@ -273,6 +307,19 @@ test('the CONVERGE branch refreshes HEAD via the merged preflight-git task only
|
|
|
273
307
|
);
|
|
274
308
|
});
|
|
275
309
|
|
|
310
|
+
test('the CONVERGE branch refreshes preflight when the availability enumeration was computed against a different SHA than the head under review', () => {
|
|
311
|
+
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
312
|
+
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
313
|
+
const guardIndex = convergeSource.indexOf('if (!isResolvedHeadUsable(head)', convergeBranchStart);
|
|
314
|
+
assert.notEqual(guardIndex, -1, 'expected the refresh guard to exist');
|
|
315
|
+
const guardLine = convergeSource.slice(guardIndex, convergeSource.indexOf('\n', guardIndex));
|
|
316
|
+
assert.match(
|
|
317
|
+
guardLine,
|
|
318
|
+
/reviewerAvailability\?\.sha !== head/,
|
|
319
|
+
'expected the refresh guard to fire when the reviewerAvailability changed-file enumeration was computed against a SHA other than the current head, so a rebase or reuse push that advances HEAD before round 1 hands the lenses the diff for the head they actually review',
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
276
323
|
test('each fix push, each lens-retry, and the convergence repair invalidate the threaded head so the next CONVERGE entry refreshes it', () => {
|
|
277
324
|
const invalidationMatches = convergeSource.match(/^ +head = null$/gm) || [];
|
|
278
325
|
assert.equal(
|
|
@@ -830,8 +877,18 @@ for (const { name, isAsync } of taskDispatchers) {
|
|
|
830
877
|
});
|
|
831
878
|
}
|
|
832
879
|
|
|
833
|
-
test('runGeneralUtilityTask only handles the
|
|
880
|
+
test('runGeneralUtilityTask only handles the post-clean-audit task it is called with', () => {
|
|
834
881
|
const generalBody = functionSource('runGeneralUtilityTask');
|
|
882
|
+
assert.match(
|
|
883
|
+
generalBody,
|
|
884
|
+
/task === 'post-clean-audit'/,
|
|
885
|
+
'expected runGeneralUtilityTask to handle the post-clean-audit task',
|
|
886
|
+
);
|
|
887
|
+
assert.doesNotMatch(
|
|
888
|
+
generalBody,
|
|
889
|
+
/task === 'mark-ready'/,
|
|
890
|
+
'the mark-ready step is merged into the FINALIZE convergence check (runConvergenceCheck), so its branch must be removed here',
|
|
891
|
+
);
|
|
835
892
|
assert.doesNotMatch(
|
|
836
893
|
generalBody,
|
|
837
894
|
/task === 'bugbot-lens'/,
|
|
@@ -1041,3 +1098,116 @@ test('the Monitor ceiling the guidance names covers the longest interval-times-a
|
|
|
1041
1098
|
);
|
|
1042
1099
|
}
|
|
1043
1100
|
});
|
|
1101
|
+
|
|
1102
|
+
function optionsLineForLabel(label) {
|
|
1103
|
+
const labelIndex = convergeSource.indexOf(`label: '${label}'`);
|
|
1104
|
+
assert.notEqual(labelIndex, -1, `expected an agent options object labeled ${label}`);
|
|
1105
|
+
const lineStart = convergeSource.lastIndexOf('\n', labelIndex);
|
|
1106
|
+
const lineEnd = convergeSource.indexOf('\n', labelIndex);
|
|
1107
|
+
return convergeSource.slice(lineStart, lineEnd);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
test('the TIERS map defines the opus/sonnet/haiku model-effort tiers', () => {
|
|
1111
|
+
assert.match(convergeSource, /opusMedium:\s*\{ model: 'opus', effort: 'medium' \}/);
|
|
1112
|
+
assert.match(convergeSource, /sonnetMedium:\s*\{ model: 'sonnet', effort: 'medium' \}/);
|
|
1113
|
+
assert.match(convergeSource, /haikuLow:\s*\{ model: 'haiku', effort: 'low' \}/);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test('every convergeAgent spawn options object carries a model and effort tier', () => {
|
|
1117
|
+
const optionObjects = convergeSource.match(/\{ label[^\n]*\}/g) || [];
|
|
1118
|
+
assert.ok(
|
|
1119
|
+
optionObjects.length >= 20,
|
|
1120
|
+
`expected to find the per-spawn options objects, found ${optionObjects.length}`,
|
|
1121
|
+
);
|
|
1122
|
+
for (const optionObject of optionObjects) {
|
|
1123
|
+
const hasTier = optionObject.includes('...TIERS.');
|
|
1124
|
+
const hasInlineModelEffort = /model:\s*'/.test(optionObject) && /effort:\s*'/.test(optionObject);
|
|
1125
|
+
assert.ok(
|
|
1126
|
+
hasTier || hasInlineModelEffort,
|
|
1127
|
+
`expected the spawn options to carry a model and effort tier: ${optionObject}`,
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test('the four review lenses run on the opusMedium tier', () => {
|
|
1133
|
+
for (const label of ['lens:bugbot', 'lens:code-review', 'lens:bug-audit', 'lens:reuse']) {
|
|
1134
|
+
assert.match(
|
|
1135
|
+
optionsLineForLabel(label),
|
|
1136
|
+
/\.\.\.TIERS\.opusMedium/,
|
|
1137
|
+
`expected ${label} to run on the opusMedium tier`,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test('the copilot gate runs on the haikuLow tier', () => {
|
|
1143
|
+
assert.match(optionsLineForLabel('copilot-gate'), /\.\.\.TIERS\.haikuLow/);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test('the merged finalize convergence check runs on the haikuLow tier', () => {
|
|
1147
|
+
assert.match(functionSource('runConvergenceCheck'), /\.\.\.TIERS\.haikuLow/);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
test('the preflight-git task returns the changed-file list and diffstat for the lenses', () => {
|
|
1151
|
+
const body = functionSource('runGitTask');
|
|
1152
|
+
assert.match(body, /git diff --name-status origin\/main\.\.\.HEAD/);
|
|
1153
|
+
assert.match(body, /git diff --stat origin\/main\.\.\.HEAD/);
|
|
1154
|
+
assert.match(body, /changedFiles/);
|
|
1155
|
+
assert.match(body, /diffstat/);
|
|
1156
|
+
const schemaStart = convergeSource.indexOf('const PREFLIGHT_GIT_SCHEMA =');
|
|
1157
|
+
const schemaEnd = convergeSource.indexOf('\n}', schemaStart);
|
|
1158
|
+
const schema = convergeSource.slice(schemaStart, schemaEnd);
|
|
1159
|
+
assert.match(schema, /changedFiles:/);
|
|
1160
|
+
assert.match(schema, /diffstat:/);
|
|
1161
|
+
assert.match(schema, /required:[^\n]*'changedFiles'[^\n]*'diffstat'/);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
test('each per-round lens and the reuse lens inject the preflight changed-file context', () => {
|
|
1165
|
+
for (const builder of ['runBugbotLens', 'runCodeReviewLens', 'runAuditLens', 'runReuseAuditPass']) {
|
|
1166
|
+
assert.match(
|
|
1167
|
+
lensPromptBody(builder),
|
|
1168
|
+
/renderLensDiffContext\(preflightResult\)/,
|
|
1169
|
+
`expected ${builder} to inject the changed-file context from the preflight result`,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('renderLensDiffContext reuses the changed-file list when present and falls back to self-enumeration otherwise', () => {
|
|
1175
|
+
const renderLensDiffContext = new Function(
|
|
1176
|
+
`${functionSource('renderLensDiffContext')}\nreturn renderLensDiffContext;`,
|
|
1177
|
+
)();
|
|
1178
|
+
const withList = renderLensDiffContext({ changedFiles: 'M\ta.py', diffstat: ' a.py | 2 +-' });
|
|
1179
|
+
assert.match(withList, /Changed files/);
|
|
1180
|
+
assert.match(withList, /a\.py/);
|
|
1181
|
+
const fallback = renderLensDiffContext(null);
|
|
1182
|
+
assert.match(fallback, /git diff --name-only/, 'expected a missing file list to fall back to self-enumeration');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test('the merged FINALIZE check runs check_convergence then marks the PR ready on pass in one agent', () => {
|
|
1186
|
+
const body = functionSource('runConvergenceCheck');
|
|
1187
|
+
assert.match(body, /check_convergence\.py/, 'expected the merged check to run check_convergence.py');
|
|
1188
|
+
assert.match(body, /gh pr ready/, 'expected the merged check to mark the PR ready on the passing path');
|
|
1189
|
+
assert.match(body, /schema: FINALIZE_SCHEMA/, 'expected the merged check to return the {pass, failures, ready} schema');
|
|
1190
|
+
assert.match(body, /context\.copilotDown/, 'expected the merged check to carry the copilotDown opt-out context');
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test('FINALIZE_SCHEMA carries pass, failures, and ready', () => {
|
|
1194
|
+
const schemaStart = convergeSource.indexOf('const FINALIZE_SCHEMA =');
|
|
1195
|
+
assert.notEqual(schemaStart, -1, 'expected FINALIZE_SCHEMA to exist');
|
|
1196
|
+
const schema = convergeSource.slice(schemaStart, convergeSource.indexOf('\n}', schemaStart));
|
|
1197
|
+
for (const field of ['pass', 'failures', 'ready']) {
|
|
1198
|
+
assert.match(schema, new RegExp(`${field}:`), `expected FINALIZE_SCHEMA to carry ${field}`);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
test('the FINALIZE phase drives the merged check and reads ready from its result without a separate mark-ready spawn', () => {
|
|
1203
|
+
const finalizeStart = convergeSource.indexOf("if (phase === 'FINALIZE') {");
|
|
1204
|
+
assert.notEqual(finalizeStart, -1, 'expected a FINALIZE phase block');
|
|
1205
|
+
const finalizeBody = convergeSource.slice(finalizeStart, finalizeStart + 1000);
|
|
1206
|
+
assert.match(finalizeBody, /runConvergenceCheck\(\{ head, bugbotDown, copilotDown \}\)/);
|
|
1207
|
+
assert.match(finalizeBody, /classifyReadyOutcome\(finalizeResult\)/);
|
|
1208
|
+
assert.doesNotMatch(
|
|
1209
|
+
finalizeBody,
|
|
1210
|
+
/runGeneralUtilityTask\('mark-ready'/,
|
|
1211
|
+
'the separate mark-ready spawn is merged into the FINALIZE convergence check',
|
|
1212
|
+
);
|
|
1213
|
+
});
|
|
@@ -250,35 +250,35 @@ test('the COPILOT phase recomputes copilotDown from each gate outcome via resolv
|
|
|
250
250
|
);
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
-
test('
|
|
253
|
+
test('the merged FINALIZE check receives copilotDown so its mark-ready step can opt the unflagged hook out of the Copilot gate', () => {
|
|
254
254
|
const finalizeStart = convergeSource.indexOf("if (phase === 'FINALIZE') {");
|
|
255
255
|
assert.notEqual(finalizeStart, -1, 'expected a FINALIZE phase block');
|
|
256
|
-
const
|
|
257
|
-
assert.notEqual(
|
|
258
|
-
const callSlice = convergeSource.slice(
|
|
256
|
+
const checkCall = convergeSource.indexOf('runConvergenceCheck(', finalizeStart);
|
|
257
|
+
assert.notEqual(checkCall, -1, 'expected the FINALIZE phase to route the merged convergence check');
|
|
258
|
+
const callSlice = convergeSource.slice(checkCall, checkCall + 60);
|
|
259
259
|
assert.match(
|
|
260
260
|
callSlice,
|
|
261
261
|
/copilotDown/,
|
|
262
|
-
'expected
|
|
262
|
+
'expected the merged check context to include copilotDown so its mark-ready step can opt the unflagged hook out of the Copilot gate',
|
|
263
263
|
);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
test('the
|
|
267
|
-
const
|
|
266
|
+
test('the merged FINALIZE check opts the unflagged convergence hook out of Copilot when copilotDown before gh pr ready', () => {
|
|
267
|
+
const checkBody = functionBody('runConvergenceCheck');
|
|
268
268
|
assert.match(
|
|
269
|
-
|
|
269
|
+
checkBody,
|
|
270
270
|
/context\.copilotDown/,
|
|
271
|
-
'expected the
|
|
271
|
+
'expected the merged check to branch on copilotDown',
|
|
272
272
|
);
|
|
273
273
|
assert.match(
|
|
274
|
-
|
|
274
|
+
checkBody,
|
|
275
275
|
/CLAUDE_REVIEWS_DISABLED/,
|
|
276
|
-
'expected the
|
|
276
|
+
'expected the merged check prompt to set CLAUDE_REVIEWS_DISABLED so the unflagged hook re-derives the Copilot bypass',
|
|
277
277
|
);
|
|
278
278
|
assert.match(
|
|
279
|
-
|
|
279
|
+
checkBody,
|
|
280
280
|
/copilot/,
|
|
281
|
-
'expected the
|
|
281
|
+
'expected the merged check opt-out to name the copilot token',
|
|
282
282
|
);
|
|
283
283
|
});
|
|
284
284
|
|
|
@@ -32,6 +32,12 @@ const CONFIG = {
|
|
|
32
32
|
|
|
33
33
|
const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '
|
|
34
34
|
|
|
35
|
+
const TIERS = {
|
|
36
|
+
opusMedium: { model: 'opus', effort: 'medium' },
|
|
37
|
+
sonnetMedium: { model: 'sonnet', effort: 'medium' },
|
|
38
|
+
haikuLow: { model: 'haiku', effort: 'low' },
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
const HEADLESS_SAFETY_PREAMBLE =
|
|
36
42
|
'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' +
|
|
37
43
|
'- 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' +
|
|
@@ -84,8 +90,10 @@ const convergeAgent = (prompt, options) =>
|
|
|
84
90
|
* 'preflight-git', bundles the mechanical git reads and the reviewer-availability
|
|
85
91
|
* probe into a single agent startup: it prints the PR HEAD SHA, fetches origin
|
|
86
92
|
* main so the review lenses diff against an up-to-date base, polls GitHub
|
|
87
|
-
* mergeability,
|
|
88
|
-
* Bugbot,
|
|
93
|
+
* mergeability, runs the shared reviewer_availability.py CLI for Copilot and
|
|
94
|
+
* Bugbot, and enumerates the origin/main...HEAD diff so the review lenses reuse
|
|
95
|
+
* one changed-file list rather than each re-deriving it, returning
|
|
96
|
+
* {sha, conflicting, fetched, changedFiles, diffstat, copilot, bugbot} in one
|
|
89
97
|
* structured result. The reviewer availability rides this same preflight so the
|
|
90
98
|
* round's first git-utility spawn carries the pre-spawn reviewer decision without
|
|
91
99
|
* a separate agent. The agent never edits code, so it runs on the cheapest model
|
|
@@ -98,7 +106,7 @@ function runGitTask(task) {
|
|
|
98
106
|
throw new Error(`runGitTask has no handler for task ${task}`)
|
|
99
107
|
}
|
|
100
108
|
return convergeAgent(
|
|
101
|
-
`Run
|
|
109
|
+
`Run five read-only preflight steps for ${prCoordinates}. Do not edit, commit, push, rebase, or modify any files — read only.\n\n` +
|
|
102
110
|
`STEP 1 — resolve HEAD. Print the current PR HEAD SHA. Run exactly:\n` +
|
|
103
111
|
` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
|
|
104
112
|
`Return the full 40-character SHA in the sha field.\n\n` +
|
|
@@ -112,8 +120,12 @@ function runGitTask(task) {
|
|
|
112
120
|
`STEP 4 — check whether GitHub Copilot and Cursor Bugbot are available to review this PR, before either reviewer's own agent is spawned. Run exactly:\n` +
|
|
113
121
|
` python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer copilot\n` +
|
|
114
122
|
` python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer bugbot\n` +
|
|
115
|
-
`Each run exits 0 when that reviewer is available and non-zero when it is down, and prints one line naming the reason (stdout when available, stderr when down) — capture that line. In the copilot and bugbot fields, report down as whether that reviewer's run exited non-zero and reason as its printed line
|
|
116
|
-
|
|
123
|
+
`Each run exits 0 when that reviewer is available and non-zero when it is down, and prints one line naming the reason (stdout when available, stderr when down) — capture that line. In the copilot and bugbot fields, report down as whether that reviewer's run exited non-zero and reason as its printed line.\n\n` +
|
|
124
|
+
`STEP 5 — enumerate the diff against the refreshed base so the parallel review lenses reuse this file list rather than each re-deriving the diff. Run exactly:\n` +
|
|
125
|
+
` git diff --name-status origin/main...HEAD\n` +
|
|
126
|
+
` git diff --stat origin/main...HEAD\n` +
|
|
127
|
+
`Return the first command's output verbatim in changedFiles and the second's in diffstat (both strings; an empty string when a command produced no output).`,
|
|
128
|
+
{ label: 'git-utility', phase: 'Converge', schema: PREFLIGHT_GIT_SCHEMA, agentType: 'Explore', ...TIERS.haikuLow },
|
|
117
129
|
)
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -138,7 +150,7 @@ function runFixerTask(task, context) {
|
|
|
138
150
|
`- 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` +
|
|
139
151
|
`- 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` +
|
|
140
152
|
`- 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.`,
|
|
141
|
-
{ label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
153
|
+
{ label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
142
154
|
)
|
|
143
155
|
}
|
|
144
156
|
if (task === 'commit-recover') {
|
|
@@ -152,7 +164,7 @@ function runFixerTask(task, context) {
|
|
|
152
164
|
`- 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` +
|
|
153
165
|
`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.` +
|
|
154
166
|
PRE_COMMIT_GATE_STEP,
|
|
155
|
-
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
167
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
156
168
|
)
|
|
157
169
|
}
|
|
158
170
|
const objection = context.objection || VERIFY_OBJECTION_FALLBACK
|
|
@@ -166,7 +178,7 @@ function runFixerTask(task, context) {
|
|
|
166
178
|
`- 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` +
|
|
167
179
|
`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.` +
|
|
168
180
|
PRE_COMMIT_GATE_STEP,
|
|
169
|
-
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
181
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
170
182
|
)
|
|
171
183
|
}
|
|
172
184
|
|
|
@@ -237,7 +249,7 @@ function runCodeEditorTask(task, context) {
|
|
|
237
249
|
`- 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` +
|
|
238
250
|
`Always include a one-line summary.` +
|
|
239
251
|
PRE_COMMIT_GATE_STEP,
|
|
240
|
-
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
252
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.opusMedium },
|
|
241
253
|
)
|
|
242
254
|
}
|
|
243
255
|
if (task === 'conflict-edit') {
|
|
@@ -248,7 +260,7 @@ function runCodeEditorTask(task, context) {
|
|
|
248
260
|
`- 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` +
|
|
249
261
|
`- 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` +
|
|
250
262
|
`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.`,
|
|
251
|
-
{ label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
263
|
+
{ label, phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.opusMedium },
|
|
252
264
|
)
|
|
253
265
|
}
|
|
254
266
|
if (task === 'repair-edit') {
|
|
@@ -268,7 +280,7 @@ function runCodeEditorTask(task, context) {
|
|
|
268
280
|
`- 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` +
|
|
269
281
|
`Always include a one-line summary.` +
|
|
270
282
|
PRE_COMMIT_GATE_STEP,
|
|
271
|
-
{ label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
283
|
+
{ label, phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.opusMedium },
|
|
272
284
|
)
|
|
273
285
|
}
|
|
274
286
|
if (task === 'repair-commit') {
|
|
@@ -284,7 +296,7 @@ function runCodeEditorTask(task, context) {
|
|
|
284
296
|
`- 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` +
|
|
285
297
|
`- 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` +
|
|
286
298
|
`- 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.`,
|
|
287
|
-
{ label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
299
|
+
{ label, phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
288
300
|
)
|
|
289
301
|
}
|
|
290
302
|
if (task === 'standards-edit') {
|
|
@@ -300,7 +312,7 @@ function runCodeEditorTask(task, context) {
|
|
|
300
312
|
`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` +
|
|
301
313
|
`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.` +
|
|
302
314
|
PRE_COMMIT_GATE_STEP,
|
|
303
|
-
{ label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
315
|
+
{ label, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.opusMedium },
|
|
304
316
|
)
|
|
305
317
|
}
|
|
306
318
|
if (task === 'standards-resolve-threads') {
|
|
@@ -314,7 +326,7 @@ function runCodeEditorTask(task, context) {
|
|
|
314
326
|
`Findings:\n${findingsBlock}\n\n` +
|
|
315
327
|
`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 ${issueReference}." 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` +
|
|
316
328
|
`Return a one-line summary naming the threads you resolved.`,
|
|
317
|
-
{ label, phase: 'Converge', agentType: 'clean-coder' },
|
|
329
|
+
{ label, phase: 'Converge', agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
318
330
|
)
|
|
319
331
|
}
|
|
320
332
|
if (task === 'hardening-commit') {
|
|
@@ -325,7 +337,7 @@ function runCodeEditorTask(task, context) {
|
|
|
325
337
|
`- 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` +
|
|
326
338
|
`- Title the PR as a Conventional Commit — a type prefix (feat, fix, chore, docs, refactor, perf, ci, style, test, build, revert), an optional scope in parentheses, then a colon and a short summary, e.g. "feat(hooks): block the deferred violation class". The target repo's CI validates the PR title as a semantic commit and rejects a non-conforming title.\n\n` +
|
|
327
339
|
`Return the full https URL of the DRAFT hardening PR in hardeningPrUrl (empty string when no PR was opened) and a one-line summary.`,
|
|
328
|
-
{ label, phase: 'Converge', schema: HARDENING_COMMIT_SCHEMA, agentType: 'clean-coder' },
|
|
340
|
+
{ label, phase: 'Converge', schema: HARDENING_COMMIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
329
341
|
)
|
|
330
342
|
}
|
|
331
343
|
if (task === 'commit-recover') {
|
|
@@ -339,7 +351,7 @@ function runCodeEditorTask(task, context) {
|
|
|
339
351
|
`- 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` +
|
|
340
352
|
`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.` +
|
|
341
353
|
PRE_COMMIT_GATE_STEP,
|
|
342
|
-
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
354
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
343
355
|
)
|
|
344
356
|
}
|
|
345
357
|
// verify-recover
|
|
@@ -354,7 +366,7 @@ function runCodeEditorTask(task, context) {
|
|
|
354
366
|
`- 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` +
|
|
355
367
|
`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.` +
|
|
356
368
|
PRE_COMMIT_GATE_STEP,
|
|
357
|
-
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
369
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
|
|
358
370
|
)
|
|
359
371
|
}
|
|
360
372
|
|
|
@@ -377,7 +389,7 @@ function runVerifierTask(task, context) {
|
|
|
377
389
|
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
378
390
|
`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` +
|
|
379
391
|
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
380
|
-
{ label, phase: 'Converge', agentType: 'code-verifier' },
|
|
392
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
|
|
381
393
|
)
|
|
382
394
|
}
|
|
383
395
|
if (task === 'repair-verify') {
|
|
@@ -391,7 +403,7 @@ function runVerifierTask(task, context) {
|
|
|
391
403
|
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
392
404
|
`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` +
|
|
393
405
|
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
394
|
-
{ label, phase: 'Finalize', agentType: 'code-verifier' },
|
|
406
|
+
{ label, phase: 'Finalize', agentType: 'code-verifier', ...TIERS.sonnetMedium },
|
|
395
407
|
)
|
|
396
408
|
}
|
|
397
409
|
return convergeAgent(
|
|
@@ -410,7 +422,7 @@ function runVerifierTask(task, context) {
|
|
|
410
422
|
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
411
423
|
" ```\n" +
|
|
412
424
|
` 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.`,
|
|
413
|
-
{ label, phase: 'Converge', agentType: 'code-verifier' },
|
|
425
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
|
|
414
426
|
)
|
|
415
427
|
}
|
|
416
428
|
|
|
@@ -435,11 +447,10 @@ function serializeOneLineJson(valueToSerialize) {
|
|
|
435
447
|
}
|
|
436
448
|
|
|
437
449
|
/**
|
|
438
|
-
* Spawn a fresh general-utility general-purpose agent for
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* @param {'post-clean-audit'|'mark-ready'} task the short task name
|
|
450
|
+
* Spawn a fresh general-utility general-purpose agent for its administrative task:
|
|
451
|
+
* 'post-clean-audit' posts the terminal CLEAN bugteam review. The agent edits no
|
|
452
|
+
* code.
|
|
453
|
+
* @param {'post-clean-audit'} task the short task name
|
|
443
454
|
* @param {object} context task-specific context
|
|
444
455
|
* @returns {Promise<object>} the task result
|
|
445
456
|
*/
|
|
@@ -480,41 +491,44 @@ function runGeneralUtilityTask(task, context) {
|
|
|
480
491
|
`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` +
|
|
481
492
|
`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` +
|
|
482
493
|
`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.`,
|
|
483
|
-
{ label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
|
-
if (task === 'mark-ready') {
|
|
487
|
-
const copilotOptOut = context.copilotDown
|
|
488
|
-
? `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`
|
|
489
|
-
: ''
|
|
490
|
-
return convergeAgent(
|
|
491
|
-
`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` +
|
|
492
|
-
copilotOptOut +
|
|
493
|
-
`1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
494
|
-
`2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
495
|
-
`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}.`,
|
|
496
|
-
{ label, phase: 'Finalize', schema: READY_SCHEMA, agentType: 'general-purpose' },
|
|
494
|
+
{ label, phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose', ...TIERS.haikuLow },
|
|
497
495
|
)
|
|
498
496
|
}
|
|
499
497
|
throw new Error(`runGeneralUtilityTask: unknown task ${task}`)
|
|
500
498
|
}
|
|
501
499
|
|
|
502
500
|
/**
|
|
503
|
-
* Spawn a fresh convergence-check
|
|
504
|
-
*
|
|
505
|
-
*
|
|
501
|
+
* Spawn a fresh convergence-check general-utility agent that runs the convergence
|
|
502
|
+
* gate and, when it passes, marks the PR ready in the same turn — one agent doing
|
|
503
|
+
* check_convergence.py and (on exit 0) gh pr ready rather than a separate
|
|
504
|
+
* check-then-mark pair. A failing check returns its FAIL lines with ready:false so
|
|
505
|
+
* the FINALIZE loop routes to the repair path, which loops back and re-runs this
|
|
506
|
+
* same combined check; only the passing path ever attempts gh pr ready. The agent
|
|
507
|
+
* edits no code, so it runs on the cheapest model at low effort.
|
|
508
|
+
* @param {object} context carries head, bugbotDown, and copilotDown
|
|
509
|
+
* @returns {Promise<object>} FINALIZE_SCHEMA result
|
|
506
510
|
*/
|
|
507
511
|
function runConvergenceCheck(context) {
|
|
508
|
-
const label = 'check
|
|
512
|
+
const label = 'finalize-check'
|
|
509
513
|
const bugbotDownFlag = context.bugbotDown ? ' --bugbot-down' : ''
|
|
510
514
|
const copilotDownFlag = context.copilotDown ? ' --copilot-down' : ''
|
|
515
|
+
const copilotOptOut = context.copilotDown
|
|
516
|
+
? ` Copilot is down this run, so before gh pr ready export the token in this same shell session so the independent mark-ready blocker hook's convergence re-check inherits it:\n bash: export CLAUDE_REVIEWS_DISABLED="copilot" (PowerShell: $env:CLAUDE_REVIEWS_DISABLED = "copilot")\n`
|
|
517
|
+
: ''
|
|
511
518
|
return convergeAgent(
|
|
512
|
-
`Run the convergence gate for ${prCoordinates} and
|
|
513
|
-
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n
|
|
514
|
-
`Exit 0 -> every gate passed:
|
|
515
|
-
`Exit 1 ->
|
|
516
|
-
`Exit 2 -> retry once; if it still errors,
|
|
517
|
-
|
|
519
|
+
`Run the convergence gate for ${prCoordinates} on HEAD ${context.head} and, only when every gate passes, mark the PR ready. Do not edit code.\n\n` +
|
|
520
|
+
`1. Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n` +
|
|
521
|
+
` Exit 0 -> every gate passed: set pass:true, failures:[].\n` +
|
|
522
|
+
` Exit 1 -> set pass:false and failures to each printed FAIL line verbatim.\n` +
|
|
523
|
+
` Exit 2 -> retry once; if it still errors, set pass:false, failures:["check_convergence gh error"].\n\n` +
|
|
524
|
+
`2. Only when step 1 set pass:true, mark the PR ready and confirm it left draft state:\n` +
|
|
525
|
+
copilotOptOut +
|
|
526
|
+
` Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
527
|
+
` Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
528
|
+
` Set ready:true only when the re-query prints false (the PR is no longer a draft); set ready:false when gh pr ready errors or the re-query still prints true.\n` +
|
|
529
|
+
` When step 1 set pass:false, do NOT run gh pr ready — set ready:false.\n\n` +
|
|
530
|
+
`Return strictly the schema {pass, failures, ready}.`,
|
|
531
|
+
{ label, phase: 'Finalize', schema: FINALIZE_SCHEMA, agentType: 'general-purpose', ...TIERS.haikuLow },
|
|
518
532
|
)
|
|
519
533
|
}
|
|
520
534
|
|
|
@@ -599,10 +613,12 @@ const PREFLIGHT_GIT_SCHEMA = {
|
|
|
599
613
|
description: 'true only when GitHub reports the PR branch conflicts with its base (mergeable:false or mergeable_state:dirty); false when it merges cleanly or mergeability could not be computed',
|
|
600
614
|
},
|
|
601
615
|
fetched: { type: 'boolean', description: 'true when git fetch origin main completed successfully' },
|
|
616
|
+
changedFiles: { type: 'string', description: 'git diff --name-status origin/main...HEAD output — the changed-file list the review lenses reuse instead of re-deriving the diff' },
|
|
617
|
+
diffstat: { type: 'string', description: 'git diff --stat origin/main...HEAD output' },
|
|
602
618
|
copilot: REVIEWER_AVAILABILITY_SCHEMA.properties.copilot,
|
|
603
619
|
bugbot: REVIEWER_AVAILABILITY_SCHEMA.properties.bugbot,
|
|
604
620
|
},
|
|
605
|
-
required: ['sha', 'conflicting', 'fetched', 'copilot', 'bugbot'],
|
|
621
|
+
required: ['sha', 'conflicting', 'fetched', 'changedFiles', 'diffstat', 'copilot', 'bugbot'],
|
|
606
622
|
}
|
|
607
623
|
|
|
608
624
|
const FIX_SCHEMA = {
|
|
@@ -703,23 +719,15 @@ function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
|
|
|
703
719
|
)
|
|
704
720
|
}
|
|
705
721
|
|
|
706
|
-
const
|
|
722
|
+
const FINALIZE_SCHEMA = {
|
|
707
723
|
type: 'object',
|
|
708
724
|
additionalProperties: false,
|
|
709
725
|
properties: {
|
|
710
726
|
pass: { type: 'boolean', description: 'true only when check_convergence.py exits 0' },
|
|
711
727
|
failures: { type: 'array', items: { type: 'string' }, description: 'FAIL lines from check_convergence.py when pass is false' },
|
|
728
|
+
ready: { type: 'boolean', description: 'true only when the convergence check passed and gh pr ready confirmed the PR left draft state (isDraft false); false on any failing check or a mark-ready that did not clear draft' },
|
|
712
729
|
},
|
|
713
|
-
required: ['pass', 'failures'],
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const READY_SCHEMA = {
|
|
717
|
-
type: 'object',
|
|
718
|
-
additionalProperties: false,
|
|
719
|
-
properties: {
|
|
720
|
-
ready: { type: 'boolean', description: 'true only when isDraft is confirmed false after gh pr ready' },
|
|
721
|
-
},
|
|
722
|
-
required: ['ready'],
|
|
730
|
+
required: ['pass', 'failures', 'ready'],
|
|
723
731
|
}
|
|
724
732
|
|
|
725
733
|
const CLEAN_AUDIT_SCHEMA = {
|
|
@@ -1281,7 +1289,7 @@ function isMergeConflicting(mergeState) {
|
|
|
1281
1289
|
* agent (null result) or a ready:false report means `gh pr ready` did not land
|
|
1282
1290
|
* (auth or token drift, a transient gh failure), so the PR is still a draft and
|
|
1283
1291
|
* the run must surface a blocker rather than claim success.
|
|
1284
|
-
* @param {object|null|undefined} readyResult the
|
|
1292
|
+
* @param {object|null|undefined} readyResult the FINALIZE_SCHEMA result carrying the ready field, or null on agent failure
|
|
1285
1293
|
* @returns {{converged: boolean, blocker: string|null}} convergence decision
|
|
1286
1294
|
*/
|
|
1287
1295
|
function classifyReadyOutcome(readyResult) {
|
|
@@ -1405,16 +1413,50 @@ const input = runInput.input
|
|
|
1405
1413
|
activeRepoPath = typeof input.repoPath === 'string' && input.repoPath ? input.repoPath : null
|
|
1406
1414
|
const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNumber} (https://github.com/${input.owner}/${input.repo}/pull/${input.prNumber})`
|
|
1407
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Render the changed-file context a review lens reuses instead of re-deriving the
|
|
1418
|
+
* origin/main...HEAD diff itself. The preflight-git task already enumerated the
|
|
1419
|
+
* diff once for the round, so a lens reads the file list and diffstat below and
|
|
1420
|
+
* opens only the files it needs; its review judgment stays its own. A preflight
|
|
1421
|
+
* result missing the file list — a dead preflight agent, or a round that resolved
|
|
1422
|
+
* HEAD before the enumeration ran — falls back to instructing the lens to
|
|
1423
|
+
* enumerate the diff itself.
|
|
1424
|
+
*
|
|
1425
|
+
* ::
|
|
1426
|
+
*
|
|
1427
|
+
* renderLensDiffContext({changedFiles: 'M\ta.py', diffstat: ' a.py | 2 +-'})
|
|
1428
|
+
* -> a block quoting the file list and diffstat
|
|
1429
|
+
* renderLensDiffContext(null) -> the enumerate-it-yourself fallback
|
|
1430
|
+
*
|
|
1431
|
+
* @param {{changedFiles?: string, diffstat?: string}|null|undefined} preflightResult the preflight-git result carrying changedFiles and diffstat
|
|
1432
|
+
* @returns {string} the diff-context block to inject into a lens prompt
|
|
1433
|
+
*/
|
|
1434
|
+
function renderLensDiffContext(preflightResult) {
|
|
1435
|
+
const changedFiles = typeof preflightResult?.changedFiles === 'string' ? preflightResult.changedFiles.trim() : ''
|
|
1436
|
+
const diffstat = typeof preflightResult?.diffstat === 'string' ? preflightResult.diffstat.trim() : ''
|
|
1437
|
+
if (changedFiles.length === 0) {
|
|
1438
|
+
return `The workflow already fetched origin/main this round, so do NOT run git fetch; run git diff --name-only origin/main...HEAD to enumerate the changed files, then read the complete diff of each.\n\n`
|
|
1439
|
+
}
|
|
1440
|
+
const diffstatBlock = diffstat.length > 0 ? `Diffstat (git diff --stat origin/main...HEAD):\n${diffstat}\n\n` : ''
|
|
1441
|
+
return (
|
|
1442
|
+
`The workflow already enumerated the origin/main...HEAD diff this round; use the changed-file list and diffstat below rather than re-deriving the diff, and read only the files you need. Your review judgment stays independent.\n\n` +
|
|
1443
|
+
`Changed files (git diff --name-status origin/main...HEAD):\n${changedFiles}\n\n` +
|
|
1444
|
+
diffstatBlock
|
|
1445
|
+
)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1408
1448
|
/**
|
|
1409
1449
|
* Bugbot lens: ensure Cursor Bugbot has rendered a verdict on the given HEAD,
|
|
1410
1450
|
* triggering and polling its CI check run when needed, and return its findings.
|
|
1411
1451
|
* @param {string} head PR HEAD SHA to evaluate
|
|
1452
|
+
* @param {object|null|undefined} preflightResult the preflight-git result carrying the changed-file list and diffstat for this round
|
|
1412
1453
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
1413
1454
|
*/
|
|
1414
|
-
function runBugbotLens(head) {
|
|
1455
|
+
function runBugbotLens(head, preflightResult) {
|
|
1415
1456
|
return convergeAgent(
|
|
1416
1457
|
`You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
|
|
1417
1458
|
`Goal: return Bugbot's verdict on HEAD ${head}. Do not edit code, commit, or push. You may post the literal trigger comment described below.\n\n` +
|
|
1459
|
+
renderLensDiffContext(preflightResult) +
|
|
1418
1460
|
`Procedure (use the existing scripts; each step below shows the exact flags that script accepts):\n` +
|
|
1419
1461
|
`1. Opt-out: python "${CONFIG.prLoopScripts}/reviews_disabled.py" --reviewer bugbot. Exit 0 means disabled -> return {sha, clean:true, down:true, findings:[]}.\n` +
|
|
1420
1462
|
`2. Silent pass: python "${CONFIG.sharedScripts}/check_bugbot_ci.py" --owner ${input.owner} --repo ${input.repo} --sha ${head} --check-clean. Exit 0 means the CI check completed clean with no review -> return clean with no findings.\n` +
|
|
@@ -1427,7 +1469,7 @@ function runBugbotLens(head) {
|
|
|
1427
1469
|
`4. No review yet on HEAD: check_bugbot_ci.py --check-active. If active (exit 0), poll: repeat check_bugbot_ci.py --check-clean / --check-active every 60 seconds (wait each 60-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above) for up to 25 iterations, then re-fetch the review. If not active (exit 1), post the literal comment "bugbot run" (no @mention, no other text) via ${REVIEWER_GATE_SENTINEL}python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --body "bugbot run", wait 8 seconds inside this turn with the Monitor tool (per the WAITS AND POLLS rule above), then poll as above.\n` +
|
|
1428
1470
|
`5. If after the full poll budget Bugbot has neither a check run nor a review on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} (treat as down).\n\n` +
|
|
1429
1471
|
`Scope is the whole PR; you are only reading Bugbot's own output here. For each finding set category: 'code-standard' when it is a pure CODE_RULES/style violation (naming, comments, type hints, magic values, structure) with no behavioral impact; 'bug' otherwise. Return strictly the schema.`,
|
|
1430
|
-
{ label: 'lens:bugbot', phase: 'Converge', schema: LENS_SCHEMA },
|
|
1472
|
+
{ label: 'lens:bugbot', phase: 'Converge', schema: LENS_SCHEMA, ...TIERS.opusMedium },
|
|
1431
1473
|
)
|
|
1432
1474
|
}
|
|
1433
1475
|
|
|
@@ -1435,15 +1477,17 @@ function runBugbotLens(head) {
|
|
|
1435
1477
|
* Code-review lens: a full-diff /code-review-style pass that reports findings
|
|
1436
1478
|
* without applying any fix.
|
|
1437
1479
|
* @param {string} head PR HEAD SHA to evaluate
|
|
1480
|
+
* @param {object|null|undefined} preflightResult the preflight-git result carrying the changed-file list and diffstat for this round
|
|
1438
1481
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
1439
1482
|
*/
|
|
1440
|
-
function runCodeReviewLens(head) {
|
|
1483
|
+
function runCodeReviewLens(head, preflightResult) {
|
|
1441
1484
|
return convergeAgent(
|
|
1442
1485
|
`You are the code-review lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
1443
|
-
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or to a single file
|
|
1486
|
+
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or to a single file.\n\n` +
|
|
1487
|
+
renderLensDiffContext(preflightResult) +
|
|
1444
1488
|
`Apply correctness-focused review: real bugs, broken logic, incorrect error handling, data-loss or security risks, contract mismatches, and reuse/simplification problems. Report only defensible findings with concrete file:line evidence.\n\n` +
|
|
1445
1489
|
`Do NOT edit, commit, or push — reporting only. Return strictly the schema: clean=true with empty findings when the diff is sound, otherwise one entry per finding (severity P0/P1/P2; category 'code-standard' for pure CODE_RULES/style violations with no behavioral impact, 'bug' otherwise; replyToCommentId=null since these are not yet GitHub threads). Set sha=${'`'}${head}${'`'}, down=false.`,
|
|
1446
|
-
{ label: 'lens:code-review', phase: 'Converge', schema: LENS_SCHEMA, agentType: 'code-quality-agent' },
|
|
1490
|
+
{ label: 'lens:code-review', phase: 'Converge', schema: LENS_SCHEMA, agentType: 'code-quality-agent', ...TIERS.opusMedium },
|
|
1447
1491
|
)
|
|
1448
1492
|
}
|
|
1449
1493
|
|
|
@@ -1451,15 +1495,17 @@ function runCodeReviewLens(head) {
|
|
|
1451
1495
|
* Bug-audit lens: the bugteam-class second-opinion audit over the full diff,
|
|
1452
1496
|
* applying the shared A–P audit rubric. Reports findings without fixing.
|
|
1453
1497
|
* @param {string} head PR HEAD SHA to evaluate
|
|
1498
|
+
* @param {object|null|undefined} preflightResult the preflight-git result carrying the changed-file list and diffstat for this round
|
|
1454
1499
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
1455
1500
|
*/
|
|
1456
|
-
function runAuditLens(head) {
|
|
1501
|
+
function runAuditLens(head, preflightResult) {
|
|
1457
1502
|
return convergeAgent(
|
|
1458
1503
|
`You are the second-opinion bug-audit lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
1459
|
-
`Read the audit rubric at ${CONFIG.bugteamRubric} and apply its categories (A through P) against the FULL origin/main...HEAD diff — every file the PR touches, never a delta cut
|
|
1504
|
+
`Read the audit rubric at ${CONFIG.bugteamRubric} and apply its categories (A through P) against the FULL origin/main...HEAD diff — every file the PR touches, never a delta cut.\n\n` +
|
|
1505
|
+
renderLensDiffContext(preflightResult) +
|
|
1460
1506
|
`This is a clean-room audit: assume nothing from other lenses. Report only findings backed by concrete file:line evidence. Do NOT edit, commit, or push.\n\n` +
|
|
1461
1507
|
`Return strictly the schema: clean=true with empty findings when the diff passes every category, otherwise one entry per finding (severity P0/P1/P2; category 'code-standard' for pure CODE_RULES/style violations with no behavioral impact, 'bug' otherwise; replyToCommentId=null). Set sha=${'`'}${head}${'`'}, down=false.`,
|
|
1462
|
-
{ label: 'lens:bug-audit', phase: 'Converge', schema: LENS_SCHEMA, agentType: 'code-quality-agent' },
|
|
1508
|
+
{ label: 'lens:bug-audit', phase: 'Converge', schema: LENS_SCHEMA, agentType: 'code-quality-agent', ...TIERS.opusMedium },
|
|
1463
1509
|
)
|
|
1464
1510
|
}
|
|
1465
1511
|
|
|
@@ -1471,18 +1517,21 @@ function runAuditLens(head) {
|
|
|
1471
1517
|
* reuse pass routes the qualifying findings through applyFixes so they are
|
|
1472
1518
|
* implemented in one commit before the convergence rounds begin.
|
|
1473
1519
|
* @param {string} head PR HEAD SHA to evaluate
|
|
1520
|
+
* @param {object|null|undefined} preflightResult the preflight-git result carrying the changed-file list and diffstat for this round
|
|
1474
1521
|
* @returns {Promise<object>} LENS_SCHEMA result carrying the qualifying reuse findings
|
|
1475
1522
|
*/
|
|
1476
|
-
function runReuseAuditPass(head) {
|
|
1523
|
+
function runReuseAuditPass(head, preflightResult) {
|
|
1477
1524
|
return convergeAgent(
|
|
1478
1525
|
`You are the REUSE lens for ${prCoordinates}, HEAD ${head}. This pass runs once before convergence to find where the PR re-implements behavior the codebase already provides.\n\n` +
|
|
1479
|
-
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or a single file
|
|
1526
|
+
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or a single file.\n\n` +
|
|
1527
|
+
renderLensDiffContext(preflightResult) +
|
|
1528
|
+
`For every new function, helper, constant, type, or block of logic the PR introduces, search the repository (Serena symbol search, grep, and the project's config/ and shared/ modules) for an existing equivalent that already provides the same behavior.\n\n` +
|
|
1480
1529
|
`Report a reuse finding ONLY when ALL THREE criteria hold — when any one is in doubt, omit the finding:\n` +
|
|
1481
1530
|
` A. CERTAIN: an existing symbol or module unquestionably covers the new code's behavior, and you can cite it at file:line.\n` +
|
|
1482
1531
|
` B. BEHAVIORALLY IDENTICAL: replacing the new code with the existing one changes no observable behavior — same inputs, outputs, side effects, and error handling.\n` +
|
|
1483
1532
|
` C. AUTONOMOUSLY IMPLEMENTABLE: the replacement is a mechanical edit (import and call the existing symbol, delete the duplicate) that needs no product decision, no API the existing code lacks, and no human judgment.\n\n` +
|
|
1484
1533
|
`Do NOT edit, commit, or push — report only; a separate fix step applies what you return. Return strictly the schema: clean=true with empty findings when no reuse case clears all three criteria, otherwise one entry per qualifying reuse improvement. For each: file and line of the duplicate in the PR; severity P2; category 'code-standard'; title naming the existing symbol to reuse; detail giving the existing symbol's file:line and the exact mechanical replacement; replyToCommentId=null. Set sha=${'`'}${head}${'`'}, down=false.`,
|
|
1485
|
-
{ label: 'lens:reuse', phase: 'Reuse', schema: LENS_SCHEMA, agentType: 'code-quality-agent' },
|
|
1534
|
+
{ label: 'lens:reuse', phase: 'Reuse', schema: LENS_SCHEMA, agentType: 'code-quality-agent', ...TIERS.opusMedium },
|
|
1486
1535
|
)
|
|
1487
1536
|
}
|
|
1488
1537
|
|
|
@@ -1629,7 +1678,7 @@ function runCopilotGate(head) {
|
|
|
1629
1678
|
` - Copilot findings on HEAD -> return them (each with its inline comment id in replyToCommentId; category 'code-standard' for pure CODE_RULES/style violations with no behavioral impact, 'bug' otherwise), clean:false, down:false.\n` +
|
|
1630
1679
|
` - No review after ${CONFIG.copilotMaxPolls} attempts -> Copilot is down for this run (unreachable, or silently out of quota with no notice): return {sha:${'`'}${head}${'`'}, clean:false, down:true, findings:[]}.\n\n` +
|
|
1631
1680
|
`Return strictly the schema.`,
|
|
1632
|
-
{ label: 'copilot-gate', phase: 'Copilot gate', schema: COPILOT_SCHEMA },
|
|
1681
|
+
{ label: 'copilot-gate', phase: 'Copilot gate', schema: COPILOT_SCHEMA, ...TIERS.haikuLow },
|
|
1633
1682
|
)
|
|
1634
1683
|
}
|
|
1635
1684
|
|
|
@@ -2017,7 +2066,7 @@ if (isResolvedHeadUsable(preflight?.sha)) {
|
|
|
2017
2066
|
|
|
2018
2067
|
log('Reuse pass: scanning the full diff for certain, behaviorally identical, autonomously implementable reuse improvements before convergence')
|
|
2019
2068
|
if (isResolvedHeadUsable(head)) {
|
|
2020
|
-
const reuse = await runReuseAuditPass(head)
|
|
2069
|
+
const reuse = await runReuseAuditPass(head, preflight)
|
|
2021
2070
|
const reuseFindings = reuse?.findings || []
|
|
2022
2071
|
if (reuseFindings.length > 0) {
|
|
2023
2072
|
log(`Reuse pass: ${reuseFindings.length} qualifying reuse improvement(s) — applying before convergence`)
|
|
@@ -2039,7 +2088,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
2039
2088
|
iterations += 1
|
|
2040
2089
|
if (phase === 'CONVERGE') {
|
|
2041
2090
|
rounds += 1
|
|
2042
|
-
if (!isResolvedHeadUsable(head)) {
|
|
2091
|
+
if (!isResolvedHeadUsable(head) || reviewerAvailability?.sha !== head) {
|
|
2043
2092
|
const refreshedPreflight = await runGitTask('preflight-git')
|
|
2044
2093
|
reviewerAvailability = refreshedPreflight
|
|
2045
2094
|
head = isResolvedHeadUsable(refreshedPreflight?.sha) ? refreshedPreflight.sha : null
|
|
@@ -2055,9 +2104,9 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
2055
2104
|
const isBugbotDownPreSpawn = resolveReviewerDown(reviewerAvailability?.bugbot, input.bugbotDisabled || false)
|
|
2056
2105
|
log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
|
|
2057
2106
|
const lenses = await parallel([
|
|
2058
|
-
() => (isBugbotDownPreSpawn ? Promise.resolve({ sha: head, clean: true, down: true, notSpawned: true, findings: [] }) : runBugbotLens(head)),
|
|
2059
|
-
() => runCodeReviewLens(head),
|
|
2060
|
-
() => runAuditLens(head),
|
|
2107
|
+
() => (isBugbotDownPreSpawn ? Promise.resolve({ sha: head, clean: true, down: true, notSpawned: true, findings: [] }) : runBugbotLens(head, reviewerAvailability)),
|
|
2108
|
+
() => runCodeReviewLens(head, reviewerAvailability),
|
|
2109
|
+
() => runAuditLens(head, reviewerAvailability),
|
|
2061
2110
|
])
|
|
2062
2111
|
bugbotDown = lenses[0] == null ? true : resolveReviewerDown(lenses[0], input.bugbotDisabled || false)
|
|
2063
2112
|
const roundOutcome = resolveRoundOutcome(lenses)
|
|
@@ -2193,15 +2242,14 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
2193
2242
|
}
|
|
2194
2243
|
|
|
2195
2244
|
if (phase === 'FINALIZE') {
|
|
2196
|
-
const
|
|
2197
|
-
const convergenceOutcome = classifyConvergenceOutcome(
|
|
2245
|
+
const finalizeResult = await runConvergenceCheck({ head, bugbotDown, copilotDown })
|
|
2246
|
+
const convergenceOutcome = classifyConvergenceOutcome(finalizeResult)
|
|
2198
2247
|
if (convergenceOutcome.kind === 'retry') {
|
|
2199
2248
|
log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
|
|
2200
2249
|
continue
|
|
2201
2250
|
}
|
|
2202
2251
|
if (convergenceOutcome.kind === 'ready') {
|
|
2203
|
-
const
|
|
2204
|
-
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
2252
|
+
const readyOutcome = classifyReadyOutcome(finalizeResult)
|
|
2205
2253
|
if (readyOutcome.converged) {
|
|
2206
2254
|
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote, deferredPrs }
|
|
2207
2255
|
}
|