claude-dev-env 1.58.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/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- 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/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- 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/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -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_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -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/test_workflow_substitution_slot_blocker.py +242 -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/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -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 +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- 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.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- 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/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- 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
|
@@ -15,7 +15,7 @@ export const meta = {
|
|
|
15
15
|
whenToUse: 'Launched by the /autoconverge skill after it resolves PR scope, enters a worktree, and grants project .claude permissions.',
|
|
16
16
|
phases: [
|
|
17
17
|
{ title: 'Converge', detail: 'Bugbot + code-review + bug-audit in parallel each round; one clean-coder applies all fixes; loop until all three are clean on a stable HEAD' },
|
|
18
|
-
{ title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge' },
|
|
18
|
+
{ title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge; when Copilot is down or out of quota, log a notice and mark the PR ready with the gate bypassed' },
|
|
19
19
|
{ title: 'Finalize', detail: 'Run check_convergence.py; mark draft=false on a full pass' },
|
|
20
20
|
],
|
|
21
21
|
}
|
|
@@ -28,6 +28,24 @@ const CONFIG = {
|
|
|
28
28
|
bugteamRubric: '$HOME/.claude/skills/bugteam/reference/audit-contract.md',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const HEADLESS_SAFETY_PREAMBLE =
|
|
32
|
+
'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' +
|
|
33
|
+
'- 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' +
|
|
34
|
+
'- When a commit message, or a PR / issue / review-comment body, must describe destructive-command behavior, write that text to a file and pass it by path (git commit -F <file>, gh ... --body-file <file>); never inline it with git commit -m or gh ... -b, where the literal lands in the Bash command and stalls you.\n' +
|
|
35
|
+
'- Keep scratch files and cleanup inside the OS temp dir or $CLAUDE_JOB_DIR/tmp (auto-allowed as ephemeral); never target a repository or worktree path with rm -rf.\n' +
|
|
36
|
+
'- If a step appears to require a real destructive command, use a non-destructive equivalent or report it as a blocker instead of running it.\n\n'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Spawn a workflow agent with the headless-safety preamble prepended to its
|
|
40
|
+
* prompt. Every agent in this convergence loop runs unattended, so each one is
|
|
41
|
+
* routed through here to inherit the same no-confirmation-prompt guidance.
|
|
42
|
+
* @param {string} prompt the agent's role-specific instruction body
|
|
43
|
+
* @param {object} options the agent() options (label, phase, schema, agentType, model)
|
|
44
|
+
* @returns {Promise<*>} the agent() result
|
|
45
|
+
*/
|
|
46
|
+
const convergeAgent = (prompt, options) =>
|
|
47
|
+
agent(`${HEADLESS_SAFETY_PREAMBLE}${prompt}`, options)
|
|
48
|
+
|
|
31
49
|
const LENS_SCHEMA = {
|
|
32
50
|
type: 'object',
|
|
33
51
|
additionalProperties: false,
|
|
@@ -62,10 +80,10 @@ const COPILOT_SCHEMA = {
|
|
|
62
80
|
properties: {
|
|
63
81
|
sha: { type: 'string' },
|
|
64
82
|
clean: { type: 'boolean' },
|
|
83
|
+
down: { type: 'boolean', description: 'true when Copilot is down or out of quota — it posts an out-of-usage notice or never surfaces a review on HEAD after the poll cap; the gate is bypassed and the run proceeds to mark-ready' },
|
|
65
84
|
findings: LENS_SCHEMA.properties.findings,
|
|
66
|
-
blocker: { type: ['string', 'null'], description: 'non-null when Copilot never surfaced a review after the poll cap' },
|
|
67
85
|
},
|
|
68
|
-
required: ['sha', 'clean', '
|
|
86
|
+
required: ['sha', 'clean', 'down', 'findings'],
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
const HEAD_SCHEMA = {
|
|
@@ -87,6 +105,31 @@ const FIX_SCHEMA = {
|
|
|
87
105
|
required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary'],
|
|
88
106
|
}
|
|
89
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
|
+
|
|
90
133
|
const CONVERGENCE_SCHEMA = {
|
|
91
134
|
type: 'object',
|
|
92
135
|
additionalProperties: false,
|
|
@@ -106,6 +149,17 @@ const READY_SCHEMA = {
|
|
|
106
149
|
required: ['ready'],
|
|
107
150
|
}
|
|
108
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
|
+
|
|
109
163
|
const SEVERITY_RANK = { P0: 0, P1: 1, P2: 2 }
|
|
110
164
|
const SHA_COMPARISON_PREFIX_LENGTH = 7
|
|
111
165
|
|
|
@@ -324,22 +378,44 @@ function classifyReadyOutcome(readyResult) {
|
|
|
324
378
|
* Classify a Copilot gate result into the loop's next action. A dead gate agent
|
|
325
379
|
* (null result) is a retry rather than an approval, mirroring the converge
|
|
326
380
|
* lenses' dead-agent convention so a failed gate is never mistaken for a clean
|
|
327
|
-
* Copilot review. A
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
381
|
+
* Copilot review. A down result — Copilot out of quota or unreachable, so it
|
|
382
|
+
* posts an out-of-usage notice or never surfaces a review after the poll cap —
|
|
383
|
+
* routes to the 'down' kind, which logs a notice and proceeds to mark-ready with
|
|
384
|
+
* the Copilot gate bypassed, the same way a down Bugbot lens is bypassed; this is
|
|
385
|
+
* checked first so an outage proceeds rather than waiting on a review that will
|
|
386
|
+
* not arrive. Findings route to a fix step. The gate otherwise approves only when
|
|
387
|
+
* it explicitly reports clean:true with no findings — a clean:false result with
|
|
388
|
+
* zero findings is an unreliable or malformed gate response and retries rather
|
|
389
|
+
* than advancing to Finalize, so a PR never goes ready on a HEAD Copilot did not
|
|
390
|
+
* call clean.
|
|
332
391
|
* @param {object|null|undefined} copilot the Copilot gate result, or null on agent failure
|
|
333
|
-
* @returns {{kind: string,
|
|
392
|
+
* @returns {{kind: string, findings?: Array<object>}} the next action
|
|
334
393
|
*/
|
|
335
394
|
function classifyCopilotOutcome(copilot) {
|
|
336
395
|
if (copilot == null) return { kind: 'retry' }
|
|
337
|
-
if (copilot.
|
|
396
|
+
if (copilot.down === true) return { kind: 'down' }
|
|
338
397
|
if (copilot.findings.length > 0) return { kind: 'fix', findings: copilot.findings }
|
|
339
398
|
if (copilot.clean === true) return { kind: 'approved' }
|
|
340
399
|
return { kind: 'retry' }
|
|
341
400
|
}
|
|
342
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Decide whether the Copilot review gate is bypassed for this COPILOT pass from
|
|
404
|
+
* the gate outcome, mirroring resolveBugbotDown so the flag is recomputed every
|
|
405
|
+
* pass rather than left sticky. Only a 'down' outcome (Copilot out of quota or
|
|
406
|
+
* unreachable after the poll cap) bypasses the convergence Copilot gate; an
|
|
407
|
+
* 'approved', 'fix', or 'retry' outcome means Copilot answered this pass, so the
|
|
408
|
+
* gate must be evaluated against its review and is never bypassed. Recomputing
|
|
409
|
+
* from the current outcome is what lets a recovered Copilot — one that returns
|
|
410
|
+
* standards-only findings after an earlier down pass — reach FINALIZE without
|
|
411
|
+
* the stale bypass that would skip its non-clean review.
|
|
412
|
+
* @param {{kind: string}} copilotOutcome a classifyCopilotOutcome result
|
|
413
|
+
* @returns {boolean} true only when this pass's Copilot gate is bypassed
|
|
414
|
+
*/
|
|
415
|
+
function resolveCopilotDown(copilotOutcome) {
|
|
416
|
+
return copilotOutcome.kind === 'down'
|
|
417
|
+
}
|
|
418
|
+
|
|
343
419
|
/**
|
|
344
420
|
* Classify a convergence-check result into the loop's next action. A dead check
|
|
345
421
|
* agent (null/undefined result) is a retry rather than a failure: with no FAIL
|
|
@@ -412,7 +488,7 @@ const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNum
|
|
|
412
488
|
* @returns {Promise<string>} the 40-char HEAD SHA
|
|
413
489
|
*/
|
|
414
490
|
async function resolveHead() {
|
|
415
|
-
const head = await
|
|
491
|
+
const head = await convergeAgent(
|
|
416
492
|
`Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
|
|
417
493
|
`gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
|
|
418
494
|
`Return the full 40-character SHA in the sha field. Do not modify any files.`,
|
|
@@ -430,7 +506,7 @@ async function resolveHead() {
|
|
|
430
506
|
* @returns {Promise<string>} agent transcript (unused)
|
|
431
507
|
*/
|
|
432
508
|
function prefetchMainForRound() {
|
|
433
|
-
return
|
|
509
|
+
return convergeAgent(
|
|
434
510
|
`Refresh the base ref for ${prCoordinates} so the parallel review lenses can diff against an up-to-date origin/main without each running its own fetch. Run exactly:\n` +
|
|
435
511
|
`git fetch origin main\n` +
|
|
436
512
|
`Do not edit, commit, push, rebase, or modify any files — fetch only.`,
|
|
@@ -448,7 +524,7 @@ function runBugbotLens(head) {
|
|
|
448
524
|
if (input.bugbotDisabled) {
|
|
449
525
|
return Promise.resolve({ sha: head, clean: true, down: true, findings: [] })
|
|
450
526
|
}
|
|
451
|
-
return
|
|
527
|
+
return convergeAgent(
|
|
452
528
|
`You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
|
|
453
529
|
`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` +
|
|
454
530
|
`Procedure (use the existing scripts; each step below shows the exact flags that script accepts):\n` +
|
|
@@ -474,7 +550,7 @@ function runBugbotLens(head) {
|
|
|
474
550
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
475
551
|
*/
|
|
476
552
|
function runCodeReviewLens(head) {
|
|
477
|
-
return
|
|
553
|
+
return convergeAgent(
|
|
478
554
|
`You are the code-review lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
479
555
|
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or to a single file. 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 review the complete diff of each.\n\n` +
|
|
480
556
|
`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` +
|
|
@@ -490,7 +566,7 @@ function runCodeReviewLens(head) {
|
|
|
490
566
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
491
567
|
*/
|
|
492
568
|
function runAuditLens(head) {
|
|
493
|
-
return
|
|
569
|
+
return convergeAgent(
|
|
494
570
|
`You are the second-opinion bug-audit lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
495
571
|
`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. The workflow already fetched origin/main this round, so do NOT run git fetch; run git diff --name-only origin/main...HEAD first to enumerate scope.\n\n` +
|
|
496
572
|
`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` +
|
|
@@ -520,7 +596,7 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
520
596
|
const threadIds = findings
|
|
521
597
|
.flatMap((each) => collectFindingThreadIds(each))
|
|
522
598
|
.filter((each) => typeof each === 'number')
|
|
523
|
-
return
|
|
599
|
+
return convergeAgent(
|
|
524
600
|
`You are fixing ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
525
601
|
`Findings:\n${findingsBlock}\n\n` +
|
|
526
602
|
`Rules:\n` +
|
|
@@ -539,35 +615,65 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
539
615
|
|
|
540
616
|
/**
|
|
541
617
|
* Post the terminal CLEAN bugteam audit artifact so check_convergence.py sees
|
|
542
|
-
* 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.
|
|
543
624
|
* @param {string} head converged PR HEAD SHA
|
|
544
|
-
* @returns {Promise<
|
|
625
|
+
* @returns {Promise<object>} CLEAN_AUDIT_SCHEMA result
|
|
545
626
|
*/
|
|
546
627
|
function postCleanAudit(head) {
|
|
547
|
-
return
|
|
628
|
+
return convergeAgent(
|
|
548
629
|
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${head}. All review lenses are clean on this HEAD.\n\n` +
|
|
549
630
|
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
550
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` +
|
|
551
|
-
`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
|
|
552
|
-
|
|
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.`
|
|
553
653
|
)
|
|
554
654
|
}
|
|
555
655
|
|
|
556
656
|
/**
|
|
557
657
|
* Copilot gate: request a Copilot review on HEAD and poll until it lands or the
|
|
558
|
-
* poll cap is hit; return Copilot's findings or a
|
|
658
|
+
* poll cap is hit; return Copilot's findings or a down signal. Copilot is down
|
|
659
|
+
* when it posts an out-of-usage notice (the requester hit their quota) rather
|
|
660
|
+
* than a review, or surfaces no review at all after the poll cap; the gate
|
|
661
|
+
* reports either as down so the run logs a notice and proceeds to mark-ready with
|
|
662
|
+
* the gate bypassed rather than waiting on a review that will not arrive.
|
|
559
663
|
* @param {string} head converged PR HEAD SHA
|
|
560
664
|
* @returns {Promise<object>} COPILOT_SCHEMA result
|
|
561
665
|
*/
|
|
562
666
|
function runCopilotGate(head) {
|
|
563
|
-
return
|
|
667
|
+
return convergeAgent(
|
|
564
668
|
`You are the Copilot gate for ${prCoordinates}, HEAD ${head}. Do not edit code, commit, or push.\n\n` +
|
|
565
|
-
`
|
|
669
|
+
`Copilot can run out of usage. When the newest Copilot review on HEAD carries an out-of-usage notice — a body stating Copilot was unable to review because the user who requested the review has reached their quota limit, or any equivalent quota / premium-request / usage-limit exhaustion message rather than an actual code review — Copilot is down for this run: return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} and stop. Do NOT re-request a review, do NOT keep polling, and do NOT treat the notice as a finding.\n\n` +
|
|
670
|
+
`1. Read any existing Copilot review on HEAD first: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}. This lists every Copilot review across all commits newest-first; only count entries whose commit_id starts with ${head}. If the newest such HEAD-scoped Copilot review is the out-of-usage notice above -> return the down result and stop. A notice on any earlier commit is NOT down: ignore it and continue. With no Copilot review on HEAD, skip a duplicate request: python "${CONFIG.sharedScripts}/check_pending_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --user copilot. Exit 0 means a request is already pending; otherwise request one:\n` +
|
|
566
671
|
` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
|
|
567
672
|
`2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (delay each attempt with "sleep 360", or the PowerShell alternative "Start-Sleep -Seconds 360"). Each attempt: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} for the top-level review state, plus gh api "repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/comments" --paginate --slurp for inline comment ids (Copilot's login contains "copilot", case-insensitive). Only count entries whose commit_id starts with ${head}.\n` +
|
|
568
|
-
` -
|
|
569
|
-
` - Copilot
|
|
570
|
-
` -
|
|
673
|
+
` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
|
|
674
|
+
` - Copilot review present and clean/approved on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:false, findings:[]}.\n` +
|
|
675
|
+
` - 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` +
|
|
676
|
+
` - 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` +
|
|
571
677
|
`Return strictly the schema.`,
|
|
572
678
|
{ label: 'copilot-gate', phase: 'Copilot gate', schema: COPILOT_SCHEMA },
|
|
573
679
|
)
|
|
@@ -576,13 +682,15 @@ function runCopilotGate(head) {
|
|
|
576
682
|
/**
|
|
577
683
|
* Run the authoritative convergence gate.
|
|
578
684
|
* @param {boolean} bugbotDown pass --bugbot-down when Bugbot is opted out or proved unreachable this run
|
|
685
|
+
* @param {boolean} copilotDown pass --copilot-down when Copilot is down or out of quota this run
|
|
579
686
|
* @returns {Promise<object>} CONVERGENCE_SCHEMA result
|
|
580
687
|
*/
|
|
581
|
-
function checkConvergence(bugbotDown) {
|
|
688
|
+
function checkConvergence(bugbotDown, copilotDown) {
|
|
582
689
|
const bugbotDownFlag = bugbotDown ? ' --bugbot-down' : ''
|
|
583
|
-
|
|
690
|
+
const copilotDownFlag = copilotDown ? ' --copilot-down' : ''
|
|
691
|
+
return convergeAgent(
|
|
584
692
|
`Run the convergence gate for ${prCoordinates} and report the result. Do not edit code.\n\n` +
|
|
585
|
-
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}\n\n` +
|
|
693
|
+
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n\n` +
|
|
586
694
|
`Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
|
|
587
695
|
`Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
|
|
588
696
|
`Exit 2 -> retry once; if it still errors, return {pass:false, failures:["check_convergence gh error"]}.`,
|
|
@@ -592,12 +700,22 @@ function checkConvergence(bugbotDown) {
|
|
|
592
700
|
|
|
593
701
|
/**
|
|
594
702
|
* Mark the PR ready for review (draft=false) and confirm the transition landed.
|
|
703
|
+
* When Copilot is down this run, the mark-ready agent first opts the
|
|
704
|
+
* independent mark-ready blocker hook out of the Copilot gate by exporting
|
|
705
|
+
* the Copilot token into CLAUDE_REVIEWS_DISABLED: that hook re-runs
|
|
706
|
+
* check_convergence.py without --copilot-down, so the env token is the only
|
|
707
|
+
* channel a genuine Copilot outage has to pass its Copilot review gate.
|
|
595
708
|
* @param {string} head converged PR HEAD SHA
|
|
709
|
+
* @param {boolean} copilotDown true when the Copilot gate was bypassed for an outage this run
|
|
596
710
|
* @returns {Promise<object>} READY_SCHEMA result
|
|
597
711
|
*/
|
|
598
|
-
function markReady(head) {
|
|
599
|
-
|
|
712
|
+
function markReady(head, copilotDown) {
|
|
713
|
+
const copilotOptOut = copilotDown
|
|
714
|
+
? `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`
|
|
715
|
+
: ''
|
|
716
|
+
return convergeAgent(
|
|
600
717
|
`All convergence gates pass for ${prCoordinates} on HEAD ${head}. Mark the PR ready, then confirm it left draft state. Do not edit code.\n\n` +
|
|
718
|
+
copilotOptOut +
|
|
601
719
|
`1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
602
720
|
`2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
603
721
|
`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}.`,
|
|
@@ -617,7 +735,7 @@ function repairConvergence(head, failures) {
|
|
|
617
735
|
const failureBlock = failures.length
|
|
618
736
|
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
619
737
|
: 'none reported'
|
|
620
|
-
return
|
|
738
|
+
return convergeAgent(
|
|
621
739
|
`The convergence check for ${prCoordinates} failed these gates on HEAD ${head}:\n${failureBlock}\n\n` +
|
|
622
740
|
`Address only the failing gates:\n` +
|
|
623
741
|
`- Unresolved bot review threads: fetch the threads where isResolved is false (gh api graphql, or the github MCP pull_request_read get_review_comments), then keep only the bot-authored ones — a thread whose root comment author login contains "cursor", "claude", or "copilot" (case-insensitive substring). Explicitly skip every human reviewer thread; the convergence gate counts only unresolved bot threads, so touching a human thread is out of scope. For each bot thread, verify the concern against current code; if it still applies, fix it test-first; either way post an inline reply and resolve the thread.\n` +
|
|
@@ -661,7 +779,7 @@ function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
|
661
779
|
return `${position + 1}. [${each.severity}] ${each.file}:${each.line} — ${each.title}\n ${each.detail}${threadNote}`
|
|
662
780
|
})
|
|
663
781
|
.join('\n')
|
|
664
|
-
return
|
|
782
|
+
return convergeAgent(
|
|
665
783
|
`A review round on ${prCoordinates}, HEAD ${head}, surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact). The convergence run treats the round as passed and defers these to follow-up work, which you now create. Do NOT commit or push to the PR's own branch.\n\n` +
|
|
666
784
|
`Findings:\n${findingsBlock}\n\n` +
|
|
667
785
|
`1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR.\n` +
|
|
@@ -672,13 +790,62 @@ function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
|
672
790
|
)
|
|
673
791
|
}
|
|
674
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
|
+
|
|
675
838
|
let phase = 'CONVERGE'
|
|
676
839
|
let head = null
|
|
677
840
|
let rounds = 0
|
|
678
841
|
let iterations = 0
|
|
679
842
|
let blocker = null
|
|
680
843
|
let bugbotDown = input.bugbotDisabled || false
|
|
844
|
+
let copilotDown = false
|
|
845
|
+
let copilotNote = null
|
|
681
846
|
let standardsNote = null
|
|
847
|
+
const allRoundFindings = []
|
|
848
|
+
const fixSummaries = []
|
|
682
849
|
|
|
683
850
|
while (iterations < CONFIG.maxIterations) {
|
|
684
851
|
iterations += 1
|
|
@@ -705,15 +872,22 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
705
872
|
const findings = roundOutcome.findings
|
|
706
873
|
if (isStandardsOnlyRound(findings)) {
|
|
707
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)
|
|
708
876
|
await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
709
877
|
standardsNote = `${findings.length} code-standard finding(s) deferred to a follow-up fix issue plus an environment-hardening PR — verify both land`
|
|
710
|
-
await postCleanAudit(head)
|
|
878
|
+
const auditResult = await postCleanAudit(head)
|
|
879
|
+
if (!auditResult?.posted) {
|
|
880
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
881
|
+
break
|
|
882
|
+
}
|
|
711
883
|
phase = 'COPILOT'
|
|
712
884
|
continue
|
|
713
885
|
}
|
|
714
886
|
if (findings.length > 0) {
|
|
715
887
|
log(`Round ${rounds}: ${findings.length} finding(s) — applying fixes`)
|
|
888
|
+
allRoundFindings.push(...findings)
|
|
716
889
|
const fixResult = await applyFixes(head, findings, 'converge-round')
|
|
890
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
717
891
|
const hadThreadBearingFinding = findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
718
892
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
719
893
|
if (!fixProgress.progressed) {
|
|
@@ -729,7 +903,11 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
729
903
|
continue
|
|
730
904
|
}
|
|
731
905
|
log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
|
|
732
|
-
await postCleanAudit(head)
|
|
906
|
+
const auditResult = await postCleanAudit(head)
|
|
907
|
+
if (!auditResult?.posted) {
|
|
908
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
909
|
+
break
|
|
910
|
+
}
|
|
733
911
|
phase = 'COPILOT'
|
|
734
912
|
continue
|
|
735
913
|
}
|
|
@@ -737,24 +915,34 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
737
915
|
if (phase === 'COPILOT') {
|
|
738
916
|
const copilot = await runCopilotGate(head)
|
|
739
917
|
const copilotOutcome = classifyCopilotOutcome(copilot)
|
|
918
|
+
copilotDown = resolveCopilotDown(copilotOutcome)
|
|
919
|
+
copilotNote = null
|
|
740
920
|
if (copilotOutcome.kind === 'retry') {
|
|
741
921
|
log('Copilot gate agent died or returned an unreliable not-clean result with no findings — re-running the gate on the same HEAD')
|
|
742
922
|
continue
|
|
743
923
|
}
|
|
744
|
-
if (copilotOutcome.kind === '
|
|
745
|
-
|
|
746
|
-
|
|
924
|
+
if (copilotOutcome.kind === 'down') {
|
|
925
|
+
log('Copilot gate: Copilot is down or out of quota — no review on HEAD after the poll cap. Logging a notice and proceeding to mark-ready with the Copilot gate bypassed.')
|
|
926
|
+
copilotDown = true
|
|
927
|
+
copilotNote = 'Copilot was down or out of quota — the Copilot gate was bypassed and the PR was marked ready without a Copilot review'
|
|
928
|
+
phase = 'FINALIZE'
|
|
929
|
+
continue
|
|
747
930
|
}
|
|
748
931
|
if (copilotOutcome.kind === 'fix') {
|
|
749
932
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
750
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)
|
|
751
935
|
await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
752
936
|
standardsNote = `${copilotOutcome.findings.length} code-standard finding(s) deferred to a follow-up fix issue plus an environment-hardening PR — verify both land`
|
|
937
|
+
copilotDown = false
|
|
938
|
+
copilotNote = null
|
|
753
939
|
phase = 'FINALIZE'
|
|
754
940
|
continue
|
|
755
941
|
}
|
|
756
942
|
log(`Copilot raised ${copilotOutcome.findings.length} finding(s) — fixing and re-converging`)
|
|
943
|
+
allRoundFindings.push(...copilotOutcome.findings)
|
|
757
944
|
const fixResult = await applyFixes(head, copilotOutcome.findings, 'copilot')
|
|
945
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
758
946
|
const hadThreadBearingFinding = copilotOutcome.findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
759
947
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
760
948
|
if (!fixProgress.progressed) {
|
|
@@ -766,22 +954,25 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
766
954
|
phase = 'CONVERGE'
|
|
767
955
|
continue
|
|
768
956
|
}
|
|
957
|
+
copilotDown = false
|
|
958
|
+
copilotNote = null
|
|
769
959
|
phase = 'FINALIZE'
|
|
770
960
|
continue
|
|
771
961
|
}
|
|
772
962
|
|
|
773
963
|
if (phase === 'FINALIZE') {
|
|
774
|
-
const convergence = await checkConvergence(bugbotDown)
|
|
964
|
+
const convergence = await checkConvergence(bugbotDown, copilotDown)
|
|
775
965
|
const convergenceOutcome = classifyConvergenceOutcome(convergence)
|
|
776
966
|
if (convergenceOutcome.kind === 'retry') {
|
|
777
967
|
log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
|
|
778
968
|
continue
|
|
779
969
|
}
|
|
780
970
|
if (convergenceOutcome.kind === 'ready') {
|
|
781
|
-
const readyResult = await markReady(head)
|
|
971
|
+
const readyResult = await markReady(head, copilotDown)
|
|
782
972
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
783
973
|
if (readyOutcome.converged) {
|
|
784
|
-
|
|
974
|
+
await spawnConvergenceSummary(dedupeFindings(allRoundFindings), fixSummaries, rounds, standardsNote, copilotNote)
|
|
975
|
+
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote }
|
|
785
976
|
}
|
|
786
977
|
blocker = readyOutcome.blocker
|
|
787
978
|
break
|
|
@@ -799,4 +990,5 @@ return {
|
|
|
799
990
|
finalSha: head,
|
|
800
991
|
blocker: blocker || `iteration cap reached (${CONFIG.maxIterations})`,
|
|
801
992
|
standardsNote,
|
|
993
|
+
copilotNote,
|
|
802
994
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Build the convergence-summary agent prompt over a PR's aggregated findings."""
|
|
2
|
+
|
|
3
|
+
from autoconverge_report_constants.render_report_constants import (
|
|
4
|
+
GITHUB_PR_URL_TEMPLATE,
|
|
5
|
+
SUMMARY_COPILOT_NOTE_TEMPLATE,
|
|
6
|
+
SUMMARY_DETAIL_MAX_CHARS,
|
|
7
|
+
SUMMARY_FINDING_LINE_TEMPLATE,
|
|
8
|
+
SUMMARY_FINDINGS_EMPTY_TEXT,
|
|
9
|
+
SUMMARY_FIX_EMPTY_TEXT,
|
|
10
|
+
SUMMARY_FIX_LINE_TEMPLATE,
|
|
11
|
+
SUMMARY_PR_COORDINATES_TEMPLATE,
|
|
12
|
+
SUMMARY_PROMPT_TEMPLATE,
|
|
13
|
+
SUMMARY_STANDARDS_NOTE_TEMPLATE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _format_findings_block(findings: list[dict]) -> str:
|
|
18
|
+
"""Return the numbered findings block, or a clean-run sentence when empty.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
findings: Aggregated distinct findings, each carrying severity, category,
|
|
22
|
+
file, line, title, and detail keys.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A newline-joined numbered list, or a sentence stating every lens was clean.
|
|
26
|
+
"""
|
|
27
|
+
if not findings:
|
|
28
|
+
return SUMMARY_FINDINGS_EMPTY_TEXT
|
|
29
|
+
numbered_lines: list[str] = []
|
|
30
|
+
for position, each_finding in enumerate(findings):
|
|
31
|
+
detail = str(each_finding.get("detail", ""))[:SUMMARY_DETAIL_MAX_CHARS]
|
|
32
|
+
numbered_lines.append(
|
|
33
|
+
SUMMARY_FINDING_LINE_TEMPLATE.format(
|
|
34
|
+
number=position + 1,
|
|
35
|
+
severity=each_finding.get("severity", ""),
|
|
36
|
+
category=each_finding.get("category", ""),
|
|
37
|
+
file=each_finding.get("file", ""),
|
|
38
|
+
line=each_finding.get("line", 0),
|
|
39
|
+
title=each_finding.get("title", ""),
|
|
40
|
+
detail=detail,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return "\n".join(numbered_lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_fix_block(fix_summaries: list[str]) -> str:
|
|
47
|
+
"""Return the numbered per-round fix-summary block, or 'none' when empty.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
fix_summaries: One-line fix summaries collected across every round.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A newline-joined numbered list, or the empty-state literal.
|
|
54
|
+
"""
|
|
55
|
+
if not fix_summaries:
|
|
56
|
+
return SUMMARY_FIX_EMPTY_TEXT
|
|
57
|
+
return "\n".join(
|
|
58
|
+
SUMMARY_FIX_LINE_TEMPLATE.format(number=position + 1, summary=each_summary)
|
|
59
|
+
for position, each_summary in enumerate(fix_summaries)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_summary_prompt(
|
|
64
|
+
owner: str,
|
|
65
|
+
repo: str,
|
|
66
|
+
pr_number: int,
|
|
67
|
+
round_count: int,
|
|
68
|
+
findings: list[dict],
|
|
69
|
+
fix_summaries: list[str],
|
|
70
|
+
standards_note: str | None,
|
|
71
|
+
copilot_note: str | None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Return the convergence-summary agent prompt for a PR's aggregated findings.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
owner: The PR's repository owner.
|
|
77
|
+
repo: The PR's repository name.
|
|
78
|
+
pr_number: The PR number.
|
|
79
|
+
round_count: Total converge rounds across every run aggregated.
|
|
80
|
+
findings: Aggregated distinct findings across every run.
|
|
81
|
+
fix_summaries: One-line fix summaries collected across every run.
|
|
82
|
+
standards_note: Deferral note when a round was code-standard-only, else None.
|
|
83
|
+
copilot_note: Outage note when the Copilot gate was bypassed, else None.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The full agent prompt instructing a StructuredOutput convergence summary.
|
|
87
|
+
"""
|
|
88
|
+
pr_url = GITHUB_PR_URL_TEMPLATE.format(owner=owner, repo=repo, number=pr_number)
|
|
89
|
+
pr_coordinates = SUMMARY_PR_COORDINATES_TEMPLATE.format(
|
|
90
|
+
owner=owner, repo=repo, pr_number=pr_number, url=pr_url
|
|
91
|
+
)
|
|
92
|
+
standards_block = (
|
|
93
|
+
SUMMARY_STANDARDS_NOTE_TEMPLATE.format(note=standards_note)
|
|
94
|
+
if standards_note
|
|
95
|
+
else ""
|
|
96
|
+
)
|
|
97
|
+
copilot_block = (
|
|
98
|
+
SUMMARY_COPILOT_NOTE_TEMPLATE.format(note=copilot_note) if copilot_note else ""
|
|
99
|
+
)
|
|
100
|
+
return SUMMARY_PROMPT_TEMPLATE.format(
|
|
101
|
+
pr_coordinates=pr_coordinates,
|
|
102
|
+
owner=owner,
|
|
103
|
+
repo=repo,
|
|
104
|
+
pr_number=pr_number,
|
|
105
|
+
round_count=round_count,
|
|
106
|
+
findings_block=_format_findings_block(findings),
|
|
107
|
+
fix_block=_format_fix_block(fix_summaries),
|
|
108
|
+
standards_block=standards_block,
|
|
109
|
+
copilot_block=copilot_block,
|
|
110
|
+
)
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "ab1c2d3e4f5a6b7c8-u", "message": {"role": "user", "content": "Return the convergence summary."}}
|
|
2
|
+
{"type": "assistant", "uuid": "ab1c2d3e4f5a6b7c8-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_ab1c2d3e4f5a6b7c8", "name": "StructuredOutput", "input": {"prProblem": "DataBridge, the service that exports your records in batches, restarted an interrupted export from the beginning instead of continuing.", "prFix": "An interrupted export now resumes from the last finished batch.", "problemScenes": [{"trigger": "export stops at batch 90 of 100", "condition": "you restart it", "result": "starts again at batch 1", "caption": "A halted export threw away the 90 batches it had already finished and began again."}], "fixScenes": [{"trigger": "export stops at batch 90 of 100", "condition": "you restart it", "result": "continues at batch 91", "caption": "A restarted export now picks up at the next unfinished batch."}], "verdictLine": "Converged in 4 rounds; 3 distinct issue classes were caught and fixed.", "issueClasses": [{"plainName": "Tests did not declare their return type", "count": 7, "severity": "P2", "category": "code-standard", "status": "fixed", "cause": "Several new test functions did not state that they return nothing, which the project's type checker wants.", "medium": "code", "beforeLines": ["def test_resume_skip():", " ..."], "afterLines": ["def test_resume_skip() -> None:", " ..."]}, {"plainName": "A vague banned variable name", "count": 2, "severity": "P2", "category": "code-standard", "status": "fixed", "cause": "Two variables used a generic placeholder name the project bans.", "medium": "code", "beforeLines": ["result = fetch()"], "afterLines": ["exported_rows = fetch()"]}, {"plainName": "Hardcoded message strings", "count": 6, "severity": "P2", "category": "code-standard", "status": "fixed", "cause": "Warning text was written inline in code instead of shared configuration.", "medium": "text", "beforeLines": ["inline warning text"], "afterLines": ["shared config message"]}]}}]}}
|