claude-dev-env 1.59.0 → 1.61.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 +4 -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-f-silent-failures.md +1 -1
- 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/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -15
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -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 +118 -0
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -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_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -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 +213 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +490 -0
- package/hooks/blocking/test_verified_commit_gate.py +495 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +686 -0
- package/hooks/blocking/verified_commit_gate.py +535 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +221 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/orphan-css-class.md +23 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
- package/skills/autoconverge/workflow/converge.mjs +520 -57
- 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 +518 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -105,6 +105,77 @@ const FIX_SCHEMA = {
|
|
|
105
105
|
required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary'],
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const EDIT_SCHEMA = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
properties: {
|
|
112
|
+
edited: { type: 'boolean', description: 'true when the step edited code to fix at least one finding' },
|
|
113
|
+
resolvedWithoutCommit: { type: 'boolean', description: 'true when every finding was already addressed so no code change was made, yet each finding thread was still resolved' },
|
|
114
|
+
summary: { type: 'string' },
|
|
115
|
+
},
|
|
116
|
+
required: ['edited', 'resolvedWithoutCommit', 'summary'],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const REPAIR_EDIT_SCHEMA = {
|
|
120
|
+
type: 'object',
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
properties: {
|
|
123
|
+
edited: { type: 'boolean', description: 'true when a still-applicable bot-thread concern was fixed test-first in the working tree' },
|
|
124
|
+
rebased: { type: 'boolean', description: 'true when the branch was rebased onto origin/main to restore mergeability' },
|
|
125
|
+
resolvedWithoutCommit: { type: 'boolean', description: 'true when the failing gates were addressed with neither a code change nor a rebase — bot threads resolved only, so there is nothing for the commit step to push' },
|
|
126
|
+
summary: { type: 'string' },
|
|
127
|
+
},
|
|
128
|
+
required: ['edited', 'rebased', 'resolvedWithoutCommit', 'summary'],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const STANDARDS_EDIT_SCHEMA = {
|
|
132
|
+
type: 'object',
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
properties: {
|
|
135
|
+
issueUrl: { type: 'string', description: 'the follow-up fix issue URL, or an empty string when the issue could not be filed' },
|
|
136
|
+
hardeningRepoPath: { type: 'string', description: 'absolute path of the environment-config repo checkout the hardening branch was edited in, or an empty string when no hardening edit was made' },
|
|
137
|
+
hardeningBranch: { type: 'string', description: 'the hardening branch name created in that repo, or an empty string when no hardening edit was made' },
|
|
138
|
+
hardeningEdited: { type: 'boolean', description: 'true when hooks or rules were edited in the working tree of the hardening repo, uncommitted, so the verify and commit steps have a surface to bind and push' },
|
|
139
|
+
summary: { type: 'string' },
|
|
140
|
+
},
|
|
141
|
+
required: ['issueUrl', 'hardeningRepoPath', 'hardeningBranch', 'hardeningEdited', 'summary'],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const VERDICT_FENCE_STEPS =
|
|
145
|
+
`Compute the binding hash for the live surface by running exactly:\n` +
|
|
146
|
+
` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash "<REPO>"\n` +
|
|
147
|
+
` (substitute the REPO path you resolved). That prints a single 64-char hex hash on stdout — capture it.\n` +
|
|
148
|
+
`Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
|
|
149
|
+
" ```verdict\n" +
|
|
150
|
+
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
151
|
+
" ```\n" +
|
|
152
|
+
` 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.`
|
|
153
|
+
|
|
154
|
+
const CONVERGENCE_SUMMARY_SCHEMA = {
|
|
155
|
+
type: 'object',
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
properties: {
|
|
158
|
+
verdictLine: { type: 'string', description: 'one factual BLUF sentence: converged?, distinct issue-class count, all fixed or deferred. No hedging words.' },
|
|
159
|
+
issueClasses: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
items: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
properties: {
|
|
165
|
+
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' },
|
|
166
|
+
count: { type: 'integer', description: 'number of raw findings grouped into this class' },
|
|
167
|
+
severity: { type: 'string', enum: ['P0', 'P1', 'P2'], description: 'most severe among the class' },
|
|
168
|
+
category: { type: 'string', enum: ['bug', 'code-standard'] },
|
|
169
|
+
status: { type: 'string', enum: ['fixed', 'deferred'] },
|
|
170
|
+
whatItWas: { type: 'string', description: 'at most 2 sentences, plain language, what the problem was' },
|
|
171
|
+
},
|
|
172
|
+
required: ['plainName', 'count', 'severity', 'category', 'status', 'whatItWas'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
required: ['verdictLine', 'issueClasses'],
|
|
177
|
+
}
|
|
178
|
+
|
|
108
179
|
const CONVERGENCE_SCHEMA = {
|
|
109
180
|
type: 'object',
|
|
110
181
|
additionalProperties: false,
|
|
@@ -124,6 +195,17 @@ const READY_SCHEMA = {
|
|
|
124
195
|
required: ['ready'],
|
|
125
196
|
}
|
|
126
197
|
|
|
198
|
+
const CLEAN_AUDIT_SCHEMA = {
|
|
199
|
+
type: 'object',
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
properties: {
|
|
202
|
+
posted: { type: 'boolean', description: 'true only when post_audit_thread.py printed the review URL confirming the CLEAN bugteam review landed on HEAD' },
|
|
203
|
+
reviewUrl: { type: 'string', description: 'the posted review URL when posted is true, otherwise an empty string' },
|
|
204
|
+
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)' },
|
|
205
|
+
},
|
|
206
|
+
required: ['posted', 'reviewUrl', 'reason'],
|
|
207
|
+
}
|
|
208
|
+
|
|
127
209
|
const SEVERITY_RANK = { P0: 0, P1: 1, P2: 2 }
|
|
128
210
|
const SHA_COMPARISON_PREFIX_LENGTH = 7
|
|
129
211
|
|
|
@@ -277,6 +359,33 @@ function normalizeShaForComparison(sha) {
|
|
|
277
359
|
return sha.slice(0, SHA_COMPARISON_PREFIX_LENGTH).toLowerCase()
|
|
278
360
|
}
|
|
279
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Decide whether a workflow code-verifier transcript ended in a passing
|
|
364
|
+
* verdict. The verify step runs with no schema so its verdict lands as plain
|
|
365
|
+
* assistant text; this reads the LAST ```verdict ...``` fenced JSON block and
|
|
366
|
+
* returns true only when it parses to an object with all_pass true. A missing
|
|
367
|
+
* fence, a parse failure, or all_pass false reads as not-passed so the commit
|
|
368
|
+
* step is skipped and the round reads as not-progressed.
|
|
369
|
+
* @param {string|null|undefined} verifyTranscript the verifier's transcript text
|
|
370
|
+
* @returns {boolean} true only when the last verdict fence reports all_pass true
|
|
371
|
+
*/
|
|
372
|
+
function verdictPassed(verifyTranscript) {
|
|
373
|
+
if (typeof verifyTranscript !== 'string') return false
|
|
374
|
+
const fencePattern = /```verdict\s*\n([\s\S]*?)```/g
|
|
375
|
+
let lastFenceBody = null
|
|
376
|
+
let eachMatch
|
|
377
|
+
while ((eachMatch = fencePattern.exec(verifyTranscript)) !== null) {
|
|
378
|
+
lastFenceBody = eachMatch[1]
|
|
379
|
+
}
|
|
380
|
+
if (lastFenceBody === null) return false
|
|
381
|
+
try {
|
|
382
|
+
const verdictRecord = JSON.parse(lastFenceBody)
|
|
383
|
+
return verdictRecord != null && verdictRecord.all_pass === true
|
|
384
|
+
} catch {
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
280
389
|
/**
|
|
281
390
|
* Decide whether a fix lens actually advanced the round: a pushed fix that moved
|
|
282
391
|
* HEAD progressed, and so did an all-stale round whose findings were every one
|
|
@@ -540,15 +649,12 @@ function runAuditLens(head) {
|
|
|
540
649
|
}
|
|
541
650
|
|
|
542
651
|
/**
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* @
|
|
546
|
-
* @param {Array<object>} findings deduped findings across all lenses
|
|
547
|
-
* @param {string} sourceLabel short description of where the findings came from
|
|
548
|
-
* @returns {Promise<object>} FIX_SCHEMA result
|
|
652
|
+
* Render the numbered findings block shared by the fix steps.
|
|
653
|
+
* @param {Array<object>} findings deduped findings to render
|
|
654
|
+
* @returns {string} one numbered line per finding, with any thread-id note
|
|
549
655
|
*/
|
|
550
|
-
function
|
|
551
|
-
|
|
656
|
+
function renderFindingsBlock(findings) {
|
|
657
|
+
return findings
|
|
552
658
|
.map((each, position) => {
|
|
553
659
|
const eachThreadIds = collectFindingThreadIds(each)
|
|
554
660
|
const threadNote = eachThreadIds.length
|
|
@@ -557,39 +663,156 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
557
663
|
return `${position + 1}. [${each.severity}] ${each.file}:${each.line} — ${each.title}\n ${each.detail}${threadNote}`
|
|
558
664
|
})
|
|
559
665
|
.join('\n')
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Edit step: one clean-coder fixes every finding test-first in the working
|
|
670
|
+
* tree and resolves the GitHub review threads, making NO commit or push so the
|
|
671
|
+
* verify step can bind a verdict to the unstaged fixes.
|
|
672
|
+
* @param {string} head PR HEAD SHA the findings were raised against
|
|
673
|
+
* @param {Array<object>} findings deduped findings across all lenses
|
|
674
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
675
|
+
* @returns {Promise<object>} EDIT_SCHEMA result
|
|
676
|
+
*/
|
|
677
|
+
function applyFixesEdit(head, findings, sourceLabel) {
|
|
678
|
+
const findingsBlock = renderFindingsBlock(findings)
|
|
560
679
|
const threadIds = findings
|
|
561
680
|
.flatMap((each) => collectFindingThreadIds(each))
|
|
562
681
|
.filter((each) => typeof each === 'number')
|
|
563
682
|
return convergeAgent(
|
|
564
|
-
`You are fixing ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
683
|
+
`You are the EDIT step fixing ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
565
684
|
`Findings:\n${findingsBlock}\n\n` +
|
|
566
685
|
`Rules:\n` +
|
|
567
686
|
`- Confirm the working tree is on the PR branch at HEAD ${head} with no unrelated edits before you start.\n` +
|
|
568
687
|
`- Fix every finding test-first (failing test, then minimum code to pass) per CODE_RULES. Verify each concern against current code; a finding whose concern no longer applies needs no code change but still needs its thread resolved.\n` +
|
|
569
|
-
`-
|
|
688
|
+
`- Leave all fixes in the working tree. Do NOT commit and do NOT push — the commit step does that after verification. Committing or pushing here would change the surface the verifier binds to.\n` +
|
|
570
689
|
`- For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply with python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "<what changed>". Then resolve the PR review thread by thread node id (PRRT_...): look up the thread id for that comment via GraphQL (match on comment databaseId == <id> in the pull request's reviewThreads), then call the github MCP pull_request_review_write method=resolve_thread with threadId=<PRRT_...> (not the numeric comment id), or run the resolveReviewThread GraphQL mutation with the same threadId.\n` +
|
|
571
690
|
`- Findings with replyToCommentId null are in-memory audit findings: fix them, no reply needed.\n\n` +
|
|
572
691
|
`Return values:\n` +
|
|
573
|
-
`- When you
|
|
574
|
-
`- When every finding was already addressed so no code change
|
|
692
|
+
`- When you edited code to fix at least one finding: edited=true, resolvedWithoutCommit=false.\n` +
|
|
693
|
+
`- 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` +
|
|
575
694
|
`Always include a one-line summary.`,
|
|
576
|
-
{ label: `fix:${sourceLabel}`, phase: 'Converge', schema:
|
|
695
|
+
{ label: `fix-edit:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Verify step: a code-verifier checks the working-tree fixes against the
|
|
701
|
+
* findings, computes the binding surface hash, and ends with a verdict fence
|
|
702
|
+
* as plain assistant text (NO schema, so the fence is not consumed as
|
|
703
|
+
* structured output). The fence's manifest_sha256 is what unlocks the
|
|
704
|
+
* verified-commit gate for the commit step. The verifier makes no edits.
|
|
705
|
+
* @param {string} head PR HEAD SHA the findings were raised against
|
|
706
|
+
* @param {Array<object>} findings deduped findings the fixes must address
|
|
707
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
708
|
+
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
709
|
+
*/
|
|
710
|
+
function verifyFixesInWorkingTree(head, findings, sourceLabel) {
|
|
711
|
+
const findingsBlock = renderFindingsBlock(findings)
|
|
712
|
+
return convergeAgent(
|
|
713
|
+
`You are the VERIFY step for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
714
|
+
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
715
|
+
`Steps:\n` +
|
|
716
|
+
`1. Resolve the worktree repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
717
|
+
`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` +
|
|
718
|
+
`3. ${VERDICT_FENCE_STEPS}`,
|
|
719
|
+
{ label: `fix-verify:${sourceLabel}`, phase: 'Converge', agentType: 'code-verifier' },
|
|
577
720
|
)
|
|
578
721
|
}
|
|
579
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Commit step: one clean-coder commits the already-verified working-tree fixes
|
|
725
|
+
* in a single commit and pushes to the PR branch, making NO further file edits
|
|
726
|
+
* — any edit changes the surface and invalidates the verifier verdict that
|
|
727
|
+
* unlocks the commit gate.
|
|
728
|
+
* @param {string} head PR HEAD SHA before the fix commit
|
|
729
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
730
|
+
* @returns {Promise<object>} FIX_SCHEMA result
|
|
731
|
+
*/
|
|
732
|
+
function commitVerifiedFixes(head, sourceLabel) {
|
|
733
|
+
return convergeAgent(
|
|
734
|
+
`You are the COMMIT step for fixes (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
735
|
+
`Rules:\n` +
|
|
736
|
+
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the commit would be blocked. Do not run a formatter, do not touch a test, do not re-fix anything — only commit and push what is already there.\n` +
|
|
737
|
+
`- Make ONE commit for all the working-tree fixes, then push to the PR branch.\n\n` +
|
|
738
|
+
`Return values: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, and a one-line summary. If the commit or push is blocked or fails, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, and a summary naming the failure.`,
|
|
739
|
+
{ label: `fix-commit:${sourceLabel}`, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
740
|
+
)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Fix lens: edit (clean-coder, no commit) -> verify (code-verifier emits a
|
|
745
|
+
* verdict fence binding the working tree) -> commit (clean-coder, one commit +
|
|
746
|
+
* push, no edits). Splitting the single editing-and-committing agent lets a
|
|
747
|
+
* workflow code-verifier produce the verdict the verified-commit gate requires,
|
|
748
|
+
* which the SubagentStop minter cannot mint for workflow-spawned agents. When
|
|
749
|
+
* verification fails (or the edit step stalled with no thread to resolve), the
|
|
750
|
+
* commit step is skipped and the unchanged HEAD is returned so the round reads
|
|
751
|
+
* as not-progressed.
|
|
752
|
+
* @param {string} head PR HEAD SHA the findings were raised against
|
|
753
|
+
* @param {Array<object>} findings deduped findings across all lenses
|
|
754
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
755
|
+
* @returns {Promise<object>} FIX_SCHEMA result
|
|
756
|
+
*/
|
|
757
|
+
async function applyFixes(head, findings, sourceLabel) {
|
|
758
|
+
const editResult = await applyFixesEdit(head, findings, sourceLabel)
|
|
759
|
+
if (editResult?.resolvedWithoutCommit === true && editResult?.edited !== true) {
|
|
760
|
+
return {
|
|
761
|
+
newSha: head,
|
|
762
|
+
pushed: false,
|
|
763
|
+
resolvedWithoutCommit: true,
|
|
764
|
+
summary: editResult?.summary || 'fixes resolved without a code change',
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const verifyTranscript = await verifyFixesInWorkingTree(head, findings, sourceLabel)
|
|
768
|
+
if (!verdictPassed(verifyTranscript)) {
|
|
769
|
+
return {
|
|
770
|
+
newSha: head,
|
|
771
|
+
pushed: false,
|
|
772
|
+
resolvedWithoutCommit: false,
|
|
773
|
+
summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return commitVerifiedFixes(head, sourceLabel)
|
|
777
|
+
}
|
|
778
|
+
|
|
580
779
|
/**
|
|
581
780
|
* Post the terminal CLEAN bugteam audit artifact so check_convergence.py sees
|
|
582
|
-
* a clean bugteam review on the converged HEAD.
|
|
781
|
+
* a clean bugteam review on the converged HEAD. The post is load-bearing: the
|
|
782
|
+
* convergence gate's bugteam-review check can never pass until this review
|
|
783
|
+
* lands, so the result reports whether the post succeeded rather than
|
|
784
|
+
* discarding it. A blocked post (a permission or auto-mode-classifier denial)
|
|
785
|
+
* or a script error returns posted:false with the reason so the caller can
|
|
786
|
+
* surface a blocker instead of re-converging into the iteration cap.
|
|
583
787
|
* @param {string} head converged PR HEAD SHA
|
|
584
|
-
* @returns {Promise<
|
|
788
|
+
* @returns {Promise<object>} CLEAN_AUDIT_SCHEMA result
|
|
585
789
|
*/
|
|
586
790
|
function postCleanAudit(head) {
|
|
587
791
|
return convergeAgent(
|
|
588
792
|
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${head}. All review lenses are clean on this HEAD.\n\n` +
|
|
589
793
|
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
590
794
|
`python "${CONFIG.prLoopScripts}/post_audit_thread.py" --skill bugteam --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --commit ${head} --state CLEAN --findings-json <temp-file>\n` +
|
|
591
|
-
`Run the script with --help first if any flag name differs. This posts the APPROVE review body that check_convergence.py reads for the bugteam gate. Do not edit code, commit, or push
|
|
592
|
-
|
|
795
|
+
`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` +
|
|
796
|
+
`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.`,
|
|
797
|
+
{ label: 'post-clean-audit', phase: 'Converge', schema: CLEAN_AUDIT_SCHEMA, agentType: 'general-purpose' },
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Blocker message for a CLEAN bugteam audit that did not land. The convergence
|
|
803
|
+
* gate's bugteam-review check can never pass without this review, so a blocked
|
|
804
|
+
* post stops the run with an actionable message rather than re-converging until
|
|
805
|
+
* the iteration cap. Handles a dead post agent (a null result) as not posted.
|
|
806
|
+
* @param {string} head converged PR HEAD SHA
|
|
807
|
+
* @param {object} auditResult CLEAN_AUDIT_SCHEMA result from postCleanAudit, or null when the agent died
|
|
808
|
+
* @returns {string} the blocker message naming the post failure and the unblock path
|
|
809
|
+
*/
|
|
810
|
+
function cleanAuditBlocker(head, auditResult) {
|
|
811
|
+
const reason = auditResult?.reason || 'the post agent returned no result'
|
|
812
|
+
return (
|
|
813
|
+
`clean-audit post blocked: the CLEAN bugteam review could not be posted on HEAD ${head} (${reason}) — ` +
|
|
814
|
+
`the convergence gate's bugteam-review check can never pass without it, so the run stops rather than re-converge to the iteration cap. ` +
|
|
815
|
+
`Allow post_audit_thread.py for this run with a Bash permission rule, or post the CLEAN review by hand, then re-run.`
|
|
593
816
|
)
|
|
594
817
|
}
|
|
595
818
|
|
|
@@ -664,28 +887,121 @@ function markReady(head, copilotDown) {
|
|
|
664
887
|
}
|
|
665
888
|
|
|
666
889
|
/**
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* the PR is
|
|
890
|
+
* Repair edit step: one clean-coder resolves the lingering bot review threads
|
|
891
|
+
* the convergence check flagged, fixes any still-applicable bot-thread concern
|
|
892
|
+
* test-first in the working tree, and rebases onto origin/main when the PR is
|
|
893
|
+
* not mergeable — making NO commit and NO push, so the verify step can bind a
|
|
894
|
+
* verdict to the resulting surface before the commit step pushes it. Human
|
|
895
|
+
* reviewer threads are never touched.
|
|
670
896
|
* @param {string} head current PR HEAD SHA
|
|
671
897
|
* @param {Array<string>} failures FAIL lines from the convergence check
|
|
672
|
-
* @returns {Promise<object>}
|
|
898
|
+
* @returns {Promise<object>} REPAIR_EDIT_SCHEMA result
|
|
673
899
|
*/
|
|
674
|
-
function
|
|
900
|
+
function repairConvergenceEdit(head, failures) {
|
|
675
901
|
const failureBlock = failures.length
|
|
676
902
|
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
677
903
|
: 'none reported'
|
|
678
904
|
return convergeAgent(
|
|
679
|
-
`
|
|
680
|
-
`
|
|
681
|
-
|
|
682
|
-
`-
|
|
683
|
-
`-
|
|
684
|
-
|
|
685
|
-
|
|
905
|
+
`You are the EDIT step repairing the convergence gates that failed for ${prCoordinates} on HEAD ${head}. A separate verify step then a separate commit step run after you.\n\n` +
|
|
906
|
+
`Failing gates:\n${failureBlock}\n\n` +
|
|
907
|
+
`Address only the failing gates, and make NO commit and NO push — leave every code change in the working tree (a rebase necessarily creates local commits, which is fine; just do not push them):\n` +
|
|
908
|
+
`- Unresolved bot review threads: fetch the threads where isResolved is false (gh api graphql, or the github MCP pull_request_read get_review_comments), then keep only the bot-authored ones — a thread whose root comment author login contains "cursor", "claude", or "copilot" (case-insensitive substring). Explicitly skip every human reviewer thread; the convergence gate counts only unresolved bot threads, so touching a human thread is out of scope. For each bot thread, verify the concern against current code; if it still applies, fix it test-first in the working tree and leave the fix uncommitted; either way post an inline reply and resolve the thread by its PRRT_ node id (GraphQL lookup matching the comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n` +
|
|
909
|
+
`- PR not mergeable: rebase onto origin/main FIRST, before applying any uncommitted bot-thread fix, so the rebase runs on a clean tree (git fetch origin main; git rebase origin/main; resolve conflicts). Do NOT force-push — the commit step does that after verification.\n` +
|
|
910
|
+
`- A dirty bot review or a still-pending requested reviewer: leave it; the next round re-runs that reviewer.\n\n` +
|
|
911
|
+
`Return values:\n` +
|
|
912
|
+
`- edited=true when you changed code in the working tree to fix a bot-thread concern.\n` +
|
|
913
|
+
`- rebased=true when you rebased the branch onto origin/main.\n` +
|
|
914
|
+
`- 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` +
|
|
915
|
+
`Always include a one-line summary.`,
|
|
916
|
+
{ label: 'repair-edit', phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Repair verify step: a code-verifier confirms the working-tree repair (any
|
|
922
|
+
* bot-thread fix plus any rebase result) is sound, computes the binding surface
|
|
923
|
+
* hash, and ends with a verdict fence as plain assistant text (NO schema). The
|
|
924
|
+
* fence's manifest_sha256 unlocks the verified-commit gate for the commit step's
|
|
925
|
+
* push. The verifier makes no edits.
|
|
926
|
+
* @param {string} head PR HEAD SHA the repair started from
|
|
927
|
+
* @param {Array<string>} failures FAIL lines the repair addressed
|
|
928
|
+
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
929
|
+
*/
|
|
930
|
+
function verifyRepairChanges(head, failures) {
|
|
931
|
+
const failureBlock = failures.length
|
|
932
|
+
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
933
|
+
: 'none reported'
|
|
934
|
+
return convergeAgent(
|
|
935
|
+
`You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
936
|
+
`Concerns the working-tree repair must resolve (the gates the convergence check flagged):\n${failureBlock}\n\n` +
|
|
937
|
+
`Steps:\n` +
|
|
938
|
+
`1. Resolve the worktree repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
939
|
+
`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` +
|
|
940
|
+
`3. ${VERDICT_FENCE_STEPS}`,
|
|
941
|
+
{ label: 'repair-verify', phase: 'Finalize', agentType: 'code-verifier' },
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Repair commit step: one clean-coder commits any uncommitted bot-thread fix in
|
|
947
|
+
* a single commit and pushes to the PR branch (force-with-lease when the edit
|
|
948
|
+
* step rebased), making NO further file edits — any edit changes the surface and
|
|
949
|
+
* invalidates the verifier verdict that unlocks the commit gate.
|
|
950
|
+
* @param {string} head PR HEAD SHA before the repair push
|
|
951
|
+
* @param {boolean} wasRebased true when the edit step rebased the branch, so the push must be force-with-lease
|
|
952
|
+
* @returns {Promise<object>} FIX_SCHEMA result
|
|
953
|
+
*/
|
|
954
|
+
function commitRepairFixes(head, wasRebased) {
|
|
955
|
+
const pushInstruction = wasRebased
|
|
956
|
+
? 'The edit step rebased the branch, so push with git push --force-with-lease.'
|
|
957
|
+
: 'Push to the PR branch with a plain git push.'
|
|
958
|
+
return convergeAgent(
|
|
959
|
+
`You are the COMMIT step for the convergence repair on ${prCoordinates}, HEAD ${head}. The edit step left its repair in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
960
|
+
`Rules:\n` +
|
|
961
|
+
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Do not run a formatter, do not re-fix anything — only commit and push what is already there.\n` +
|
|
962
|
+
`- Commit any uncommitted bot-thread fix in ONE commit (skip the commit when the working tree carries only already-committed rebase results). ${pushInstruction}\n\n` +
|
|
963
|
+
`Return values: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, and a one-line summary. If the commit or push is blocked or fails, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, and a summary naming the failure.`,
|
|
964
|
+
{ label: 'repair-commit', phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
|
|
686
965
|
)
|
|
687
966
|
}
|
|
688
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Address the gates a convergence check reported as failing, then hand control
|
|
970
|
+
* back to the converge phase: edit (clean-coder resolves bot threads, applies
|
|
971
|
+
* any fix and rebase in the working tree, no push) -> verify (code-verifier
|
|
972
|
+
* emits a verdict fence binding the working tree) -> commit (clean-coder, one
|
|
973
|
+
* commit + push, no edits). Splitting the edit from the push lets a workflow
|
|
974
|
+
* code-verifier produce the verdict the verified-commit gate requires for the
|
|
975
|
+
* bot-thread fix commit and the post-rebase force-push. When the edit resolved
|
|
976
|
+
* the gates with neither a code change nor a rebase, or the verify step fails,
|
|
977
|
+
* the commit step is skipped and the unchanged HEAD is returned.
|
|
978
|
+
* @param {string} head current PR HEAD SHA
|
|
979
|
+
* @param {Array<string>} failures FAIL lines from the convergence check
|
|
980
|
+
* @returns {Promise<object>} FIX_SCHEMA result
|
|
981
|
+
*/
|
|
982
|
+
async function repairConvergence(head, failures) {
|
|
983
|
+
const editResult = await repairConvergenceEdit(head, failures)
|
|
984
|
+
const hasPushWork = editResult?.edited === true || editResult?.rebased === true
|
|
985
|
+
if (!hasPushWork) {
|
|
986
|
+
return {
|
|
987
|
+
newSha: head,
|
|
988
|
+
pushed: false,
|
|
989
|
+
resolvedWithoutCommit: true,
|
|
990
|
+
summary: editResult?.summary || 'convergence gates resolved without a code change or rebase',
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const verifyTranscript = await verifyRepairChanges(head, failures)
|
|
994
|
+
if (!verdictPassed(verifyTranscript)) {
|
|
995
|
+
return {
|
|
996
|
+
newSha: head,
|
|
997
|
+
pushed: false,
|
|
998
|
+
resolvedWithoutCommit: false,
|
|
999
|
+
summary: `repair verify step did not pass the working-tree repair on HEAD ${head} — not pushing`,
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return commitRepairFixes(head, editResult?.rebased === true)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
689
1005
|
/**
|
|
690
1006
|
* Decide whether a review round surfaced ONLY code-standard violations — pure
|
|
691
1007
|
* CODE_RULES/style findings with no behavioral impact. Such a round passes for
|
|
@@ -699,34 +1015,164 @@ function isStandardsOnlyRound(findings) {
|
|
|
699
1015
|
}
|
|
700
1016
|
|
|
701
1017
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
1018
|
+
* Standards-deferral edit step: one clean-coder files the follow-up fix issue,
|
|
1019
|
+
* stages an environment-hardening hooks/rules change in the config repo's
|
|
1020
|
+
* working tree WITHOUT committing, and resolves the PR's code-standard threads.
|
|
1021
|
+
* Leaving the hardening edit uncommitted lets the verify step bind a verdict to
|
|
1022
|
+
* it before the commit step opens the PR. The PR's own branch is never touched.
|
|
707
1023
|
* @param {string} head PR HEAD SHA the findings were raised against
|
|
708
1024
|
* @param {Array<object>} findings deduped code-standard-only findings
|
|
709
1025
|
* @param {string} sourceLabel short description of where the findings came from
|
|
710
|
-
* @returns {Promise<
|
|
1026
|
+
* @returns {Promise<object>} STANDARDS_EDIT_SCHEMA result
|
|
711
1027
|
*/
|
|
712
|
-
function
|
|
713
|
-
const findingsBlock = findings
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
? `\n (GitHub review comment ids: ${eachThreadIds.join(', ')})`
|
|
718
|
-
: ''
|
|
719
|
-
return `${position + 1}. [${each.severity}] ${each.file}:${each.line} — ${each.title}\n ${each.detail}${threadNote}`
|
|
720
|
-
})
|
|
721
|
-
.join('\n')
|
|
1028
|
+
function standardsFollowUpEdit(head, findings, sourceLabel) {
|
|
1029
|
+
const findingsBlock = renderFindingsBlock(findings)
|
|
1030
|
+
const threadIds = findings
|
|
1031
|
+
.flatMap((each) => collectFindingThreadIds(each))
|
|
1032
|
+
.filter((each) => typeof each === 'number')
|
|
722
1033
|
return convergeAgent(
|
|
723
|
-
`
|
|
1034
|
+
`You are the EDIT step deferring a code-standard-only round on ${prCoordinates}, HEAD ${head} (${sourceLabel}). The round surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact); the run treats it as passed and defers the fixes to follow-up work, which you now stage. A separate verify step then a separate commit step open the hardening PR after you. Do NOT commit or push to the PR's own branch.\n\n` +
|
|
724
1035
|
`Findings:\n${findingsBlock}\n\n` +
|
|
725
|
-
`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` +
|
|
726
|
-
`2.
|
|
727
|
-
`3. For each finding that carries a GitHub review comment id: 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).\n\n` +
|
|
728
|
-
`Return
|
|
729
|
-
{ label: `standards-
|
|
1036
|
+
`1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR. Capture the issue URL.\n` +
|
|
1037
|
+
`2. Stage the environment-hardening change: in the Claude environment config repo (the repo owning ~/.claude hooks and rules — JonEcho/llm-settings for hooks, jl-cmd/claude-code-config for rules/skills; pick whichever owns the surface that would block these violation classes), find or clone a local checkout, fetch origin, and create a branch off origin/main. Edit the hooks/rules in that checkout's WORKING TREE so each violation class found here is blocked at Write/Edit time, before code is written. Do NOT commit and do NOT push — the commit step does that after the verify step binds a verdict to the working tree. Return the checkout's absolute path in hardeningRepoPath, the branch name in hardeningBranch, and set hardeningEdited=true. When no hardening is feasible for these classes, leave hardeningRepoPath and hardeningBranch empty and hardeningEdited=false; the follow-up issue still stands.\n` +
|
|
1038
|
+
`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` +
|
|
1039
|
+
`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.`,
|
|
1040
|
+
{ label: `standards-edit:${sourceLabel}`, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1041
|
+
)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Standards-hardening verify step: a code-verifier confirms the uncommitted
|
|
1046
|
+
* hooks/rules change staged in the hardening repo blocks the deferred violation
|
|
1047
|
+
* classes, computes the binding surface hash for that repo, and ends with a
|
|
1048
|
+
* verdict fence as plain assistant text (NO schema) — unlocking the
|
|
1049
|
+
* verified-commit gate for the cross-repo hardening commit. The verifier makes
|
|
1050
|
+
* no edits.
|
|
1051
|
+
* @param {string} hardeningRepoPath absolute path of the hardening repo checkout the edit staged
|
|
1052
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
1053
|
+
* @returns {Promise<string>} the verifier transcript carrying the verdict fence
|
|
1054
|
+
*/
|
|
1055
|
+
function verifyHardeningChanges(hardeningRepoPath, sourceLabel) {
|
|
1056
|
+
return convergeAgent(
|
|
1057
|
+
`You are the VERIFY step for an environment-hardening change (${sourceLabel}) staged in the working tree of ${hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
1058
|
+
`Concern the working-tree change must resolve: the edited hooks/rules block the code-standard violation classes from the deferred round at Write/Edit time, and a hook change carries a passing test per CODE_RULES.\n\n` +
|
|
1059
|
+
`Steps:\n` +
|
|
1060
|
+
`1. cd into ${hardeningRepoPath}, then resolve its repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
1061
|
+
`2. Verify the uncommitted working-tree change in REPO: read the diff (git diff) and run the hook/rule tests in that repo, confirming each violation class is now blocked.\n` +
|
|
1062
|
+
`3. ${VERDICT_FENCE_STEPS}`,
|
|
1063
|
+
{ label: `standards-verify:${sourceLabel}`, phase: 'Converge', agentType: 'code-verifier' },
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Standards-hardening commit step: one clean-coder commits the verified
|
|
1069
|
+
* working-tree hooks/rules change in a single commit, pushes the hardening
|
|
1070
|
+
* branch, and opens the DRAFT hardening PR — making NO further file edits, since
|
|
1071
|
+
* any edit changes the surface and invalidates the verdict that unlocks the
|
|
1072
|
+
* commit gate. The PR's own branch is never touched.
|
|
1073
|
+
* @param {string} hardeningRepoPath absolute path of the hardening repo checkout
|
|
1074
|
+
* @param {string} hardeningBranch the hardening branch the edit step created
|
|
1075
|
+
* @param {string} issueUrl the follow-up fix issue URL the PR body references
|
|
1076
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
1077
|
+
* @returns {Promise<string>} agent transcript (unused)
|
|
1078
|
+
*/
|
|
1079
|
+
function commitHardeningPr(hardeningRepoPath, hardeningBranch, issueUrl, sourceLabel) {
|
|
1080
|
+
return convergeAgent(
|
|
1081
|
+
`You are the COMMIT step opening the environment-hardening PR (${sourceLabel}) for the change staged in ${hardeningRepoPath} on branch ${hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree. Do NOT touch the PR's own branch.\n\n` +
|
|
1082
|
+
`Rules:\n` +
|
|
1083
|
+
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Only commit and push what is already there.\n` +
|
|
1084
|
+
`- In ${hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${issueUrl || '(none)'} and states the PR hardens the environment so the deferred violation classes are blocked at Write/Edit time. Honor the gh-body-file rule: write a BOM-free temp file and pass --body-file.\n\n` +
|
|
1085
|
+
`Return a one-line summary naming the hardening PR URL.`,
|
|
1086
|
+
{ label: `standards-commit:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
|
|
1087
|
+
)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Build the standards-deferral note for the closing report, naming the
|
|
1092
|
+
* environment-hardening PR only when one was opened this round so the note
|
|
1093
|
+
* never claims a PR the skip paths did not produce.
|
|
1094
|
+
* @param {number} findingsCount count of deferred code-standard findings
|
|
1095
|
+
* @param {boolean} hardeningPrOpened true when the hardening PR was opened this round
|
|
1096
|
+
* @returns {string} the human-facing deferral note
|
|
1097
|
+
*/
|
|
1098
|
+
function standardsDeferralNote(findingsCount, hardeningPrOpened) {
|
|
1099
|
+
const base = `${findingsCount} code-standard finding(s) deferred to a follow-up fix issue`
|
|
1100
|
+
return hardeningPrOpened
|
|
1101
|
+
? `${base} plus an environment-hardening PR — verify both land`
|
|
1102
|
+
: `${base} — verify it lands (no environment-hardening PR was opened this round)`
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Defer a standards-only round: edit (clean-coder files the follow-up fix issue,
|
|
1107
|
+
* stages an environment-hardening hooks/rules change in the config repo's
|
|
1108
|
+
* working tree without committing, and resolves the PR's code-standard threads)
|
|
1109
|
+
* -> verify (code-verifier binds a verdict to the hardening working tree) ->
|
|
1110
|
+
* commit (clean-coder makes one commit, pushes, and opens the DRAFT hardening
|
|
1111
|
+
* PR). Splitting the edit from the push lets a workflow code-verifier produce the
|
|
1112
|
+
* verdict the verified-commit gate requires for the cross-repo hardening commit.
|
|
1113
|
+
* This PR's own branch is never touched. When the edit staged no hardening, or
|
|
1114
|
+
* the verify step fails, the follow-up issue still stands and the commit step is
|
|
1115
|
+
* skipped.
|
|
1116
|
+
* @param {string} head PR HEAD SHA the findings were raised against
|
|
1117
|
+
* @param {Array<object>} findings deduped code-standard-only findings
|
|
1118
|
+
* @param {string} sourceLabel short description of where the findings came from
|
|
1119
|
+
* @returns {Promise<object>} `{ hardeningPrOpened }` — true only when the hardening PR was opened this round
|
|
1120
|
+
*/
|
|
1121
|
+
async function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
1122
|
+
const editResult = await standardsFollowUpEdit(head, findings, sourceLabel)
|
|
1123
|
+
if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
|
|
1124
|
+
return { hardeningPrOpened: false }
|
|
1125
|
+
}
|
|
1126
|
+
const verifyTranscript = await verifyHardeningChanges(editResult.hardeningRepoPath, sourceLabel)
|
|
1127
|
+
if (!verdictPassed(verifyTranscript)) {
|
|
1128
|
+
return { hardeningPrOpened: false }
|
|
1129
|
+
}
|
|
1130
|
+
await commitHardeningPr(editResult.hardeningRepoPath, editResult.hardeningBranch, editResult.issueUrl, sourceLabel)
|
|
1131
|
+
return { hardeningPrOpened: true }
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Spawn the convergence-summary agent at finalize so its StructuredOutput is
|
|
1136
|
+
* recorded into the run journal for the closing report to read. The agent groups
|
|
1137
|
+
* the deduped findings into plain-language issue classes, translates reviewer
|
|
1138
|
+
* jargon to everyday English, and writes one BLUF verdict line. The side effect
|
|
1139
|
+
* is the journal record; the return value is discarded by the caller.
|
|
1140
|
+
* @param {Array<object>} distinctFindings deduped findings across every round
|
|
1141
|
+
* @param {Array<string>} fixSummaries per-round fix-lens one-line summaries
|
|
1142
|
+
* @param {number} roundCount the number of converge rounds the run took
|
|
1143
|
+
* @param {string|null} standardsNote deferral note when a round was code-standard-only
|
|
1144
|
+
* @param {string|null} copilotNote outage note when the Copilot gate was bypassed
|
|
1145
|
+
* @returns {Promise<object>} CONVERGENCE_SUMMARY_SCHEMA result (journal side effect)
|
|
1146
|
+
*/
|
|
1147
|
+
function spawnConvergenceSummary(distinctFindings, fixSummaries, roundCount, standardsNote, copilotNote) {
|
|
1148
|
+
const findingsBlock = distinctFindings.length
|
|
1149
|
+
? distinctFindings
|
|
1150
|
+
.map((each, position) => {
|
|
1151
|
+
const truncatedDetail = (each.detail || '').slice(0, 400)
|
|
1152
|
+
return `${position + 1}. [${each.severity}/${each.category}] ${each.file}:${each.line} — ${each.title} :: ${truncatedDetail}`
|
|
1153
|
+
})
|
|
1154
|
+
.join('\n')
|
|
1155
|
+
: 'none — every lens was clean on a stable HEAD'
|
|
1156
|
+
const fixSummariesBlock = fixSummaries.length
|
|
1157
|
+
? fixSummaries.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
1158
|
+
: 'none'
|
|
1159
|
+
const standardsBlock = standardsNote ? `\nDeferred code-standard note: ${standardsNote}\n` : ''
|
|
1160
|
+
const copilotBlock = copilotNote ? `\nCopilot gate note: ${copilotNote}\n` : ''
|
|
1161
|
+
return convergeAgent(
|
|
1162
|
+
`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` +
|
|
1163
|
+
`Distinct findings caught across the run (already deduped):\n${findingsBlock}\n\n` +
|
|
1164
|
+
`Per-round fix summaries:\n${fixSummariesBlock}\n${standardsBlock}${copilotBlock}\n` +
|
|
1165
|
+
`Produce a summary an everyday reader understands:\n` +
|
|
1166
|
+
`- 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` +
|
|
1167
|
+
`- 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` +
|
|
1168
|
+
`- plainName must carry NO tool token, rule id, file path, line number, severity code (P0/P1/P2), or bot name.\n` +
|
|
1169
|
+
`- Lead with category 'bug' classes, then 'code-standard'. Cap to about 5 classes. whatItWas is at most 2 sentences. No paragraphs.\n` +
|
|
1170
|
+
`- status is 'fixed' unless the fix summaries or the deferred code-standard note mark the class deferred, in which case status is 'deferred'.\n` +
|
|
1171
|
+
`- Use NO hedging words anywhere (likely, probably, should, appears, seems, may, might, could, possibly). State facts ("caught and fixed").\n` +
|
|
1172
|
+
`- When there are zero findings, return issueClasses: [] and a verdictLine stating the run converged with no issues caught.\n` +
|
|
1173
|
+
`- verdictLine is one factual sentence naming the round count and that all classes are fixed or deferred.\n\n` +
|
|
1174
|
+
`Return strictly the schema.`,
|
|
1175
|
+
{ label: 'convergence-summary', phase: 'Finalize', schema: CONVERGENCE_SUMMARY_SCHEMA, agentType: 'general-purpose' },
|
|
730
1176
|
)
|
|
731
1177
|
}
|
|
732
1178
|
|
|
@@ -739,6 +1185,8 @@ let bugbotDown = input.bugbotDisabled || false
|
|
|
739
1185
|
let copilotDown = false
|
|
740
1186
|
let copilotNote = null
|
|
741
1187
|
let standardsNote = null
|
|
1188
|
+
const allRoundFindings = []
|
|
1189
|
+
const fixSummaries = []
|
|
742
1190
|
|
|
743
1191
|
while (iterations < CONFIG.maxIterations) {
|
|
744
1192
|
iterations += 1
|
|
@@ -765,15 +1213,22 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
765
1213
|
const findings = roundOutcome.findings
|
|
766
1214
|
if (isStandardsOnlyRound(findings)) {
|
|
767
1215
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1216
|
+
allRoundFindings.push(...findings)
|
|
1217
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
1218
|
+
standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1219
|
+
const auditResult = await postCleanAudit(head)
|
|
1220
|
+
if (!auditResult?.posted) {
|
|
1221
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
1222
|
+
break
|
|
1223
|
+
}
|
|
771
1224
|
phase = 'COPILOT'
|
|
772
1225
|
continue
|
|
773
1226
|
}
|
|
774
1227
|
if (findings.length > 0) {
|
|
775
1228
|
log(`Round ${rounds}: ${findings.length} finding(s) — applying fixes`)
|
|
1229
|
+
allRoundFindings.push(...findings)
|
|
776
1230
|
const fixResult = await applyFixes(head, findings, 'converge-round')
|
|
1231
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
777
1232
|
const hadThreadBearingFinding = findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
778
1233
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
779
1234
|
if (!fixProgress.progressed) {
|
|
@@ -789,7 +1244,11 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
789
1244
|
continue
|
|
790
1245
|
}
|
|
791
1246
|
log(`Round ${rounds}: all lenses clean on ${head?.slice(0, 7)} — posting clean audit artifact`)
|
|
792
|
-
await postCleanAudit(head)
|
|
1247
|
+
const auditResult = await postCleanAudit(head)
|
|
1248
|
+
if (!auditResult?.posted) {
|
|
1249
|
+
blocker = cleanAuditBlocker(head, auditResult)
|
|
1250
|
+
break
|
|
1251
|
+
}
|
|
793
1252
|
phase = 'COPILOT'
|
|
794
1253
|
continue
|
|
795
1254
|
}
|
|
@@ -813,15 +1272,18 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
813
1272
|
if (copilotOutcome.kind === 'fix') {
|
|
814
1273
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
815
1274
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
816
|
-
|
|
817
|
-
|
|
1275
|
+
allRoundFindings.push(...copilotOutcome.findings)
|
|
1276
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
1277
|
+
standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
818
1278
|
copilotDown = false
|
|
819
1279
|
copilotNote = null
|
|
820
1280
|
phase = 'FINALIZE'
|
|
821
1281
|
continue
|
|
822
1282
|
}
|
|
823
1283
|
log(`Copilot raised ${copilotOutcome.findings.length} finding(s) — fixing and re-converging`)
|
|
1284
|
+
allRoundFindings.push(...copilotOutcome.findings)
|
|
824
1285
|
const fixResult = await applyFixes(head, copilotOutcome.findings, 'copilot')
|
|
1286
|
+
if (fixResult?.summary) fixSummaries.push(fixResult.summary)
|
|
825
1287
|
const hadThreadBearingFinding = copilotOutcome.findings.some((each) => collectFindingThreadIds(each).length > 0)
|
|
826
1288
|
const fixProgress = detectFixProgress(fixResult, head, hadThreadBearingFinding)
|
|
827
1289
|
if (!fixProgress.progressed) {
|
|
@@ -850,6 +1312,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
850
1312
|
const readyResult = await markReady(head, copilotDown)
|
|
851
1313
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
852
1314
|
if (readyOutcome.converged) {
|
|
1315
|
+
await spawnConvergenceSummary(dedupeFindings(allRoundFindings), fixSummaries, rounds, standardsNote, copilotNote)
|
|
853
1316
|
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote }
|
|
854
1317
|
}
|
|
855
1318
|
blocker = readyOutcome.blocker
|