claude-dev-env 1.75.0 → 1.77.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/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -0
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +8 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_docstrings.py +378 -0
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +48 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +174 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +176 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +267 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/blocking_check_limits.py +56 -0
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +76 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +5 -1
- package/rules/package-inventory-stale-entry.md +16 -0
- package/rules/plain-illustrative-docstrings.md +56 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
- package/skills/autoconverge/workflow/converge.mjs +86 -110
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -128,27 +128,32 @@ function resumeGitAgent(agentId, task, head) {
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Spawn the fixer clean-coder agent for one fix batch, establishing its role and
|
|
131
|
-
* the PR coordinates so each later resume (
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
131
|
+
* the PR coordinates so each later resume (commit and recovery edits) continues
|
|
132
|
+
* the same session. The fixer never verifies — a separate verifier agent grades
|
|
133
|
+
* the working tree, so the verdict comes from a different session than the one
|
|
134
|
+
* that commits and recovers, mirroring the repair and conflict paths. The spawn
|
|
135
|
+
* makes no edits — the first recovery resume is the earliest step that touches
|
|
136
|
+
* the working tree. Returns the runtime agent id so the resume calls target the
|
|
137
|
+
* live session; a runtime without resume support returns no agent id, and each
|
|
138
|
+
* resume falls back to a fresh spawn.
|
|
136
139
|
* @param {string} head PR HEAD SHA
|
|
137
140
|
* @param {Array<object>} findings the findings to fix
|
|
138
141
|
* @param {string} sourceLabel short description of where the findings came from
|
|
139
|
-
* @param {string} task initial task name
|
|
140
142
|
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
141
143
|
*/
|
|
142
|
-
async function spawnFixerAgent(head, findings, sourceLabel
|
|
144
|
+
async function spawnFixerAgent(head, findings, sourceLabel) {
|
|
143
145
|
const result = await convergeAgent(
|
|
144
|
-
`You are the fixer agent for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Across this session you run a sequence of steps
|
|
146
|
+
`You are the fixer agent for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Across this session you run a sequence of steps: commit and push the working-tree fixes once a separate verifier passes them, and recover when a verify objection or a commit-gate block needs another edit. A separate verifier agent grades the working tree, so you never verify your own edits. Make NO edits in this first turn — confirm only that the working tree is on the PR branch at HEAD ${head} with uncommitted fixes present, then wait for the next step's instructions. Reply READY.`,
|
|
145
147
|
{ label: `fixer:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
|
|
146
148
|
)
|
|
147
149
|
return result?.agentId
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
151
|
-
* Resume the fixer agent for
|
|
153
|
+
* Resume the fixer agent for commit or recovery edits. The fixer never verifies;
|
|
154
|
+
* a separate verifier agent emits the verdict, so commit, verify-recover, and
|
|
155
|
+
* commit-recover all run on the editing session while the verdict that gates the
|
|
156
|
+
* commit comes from a different session.
|
|
152
157
|
* @param {string} agentId the agent id from spawnFixerAgent
|
|
153
158
|
* @param {string} task the short task name
|
|
154
159
|
* @param {object} context task-specific context
|
|
@@ -156,18 +161,6 @@ async function spawnFixerAgent(head, findings, sourceLabel, task) {
|
|
|
156
161
|
*/
|
|
157
162
|
function resumeFixerAgent(agentId, task, context) {
|
|
158
163
|
const label = `fixer:${context.sourceLabel}`
|
|
159
|
-
if (task === 'verify-commit') {
|
|
160
|
-
const findingsBlock = renderFindingsBlock(context.findings)
|
|
161
|
-
return convergeAgent(
|
|
162
|
-
`You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
163
|
-
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
164
|
-
`Steps:\n` +
|
|
165
|
-
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
166
|
-
`2. Verify the uncommitted working-tree changes resolve every finding above: run the relevant tests and the named gates against the working tree. Read the diff (git diff) and confirm each finding is fixed test-first per CODE_RULES.\n` +
|
|
167
|
-
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
168
|
-
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
164
|
if (task === 'commit') {
|
|
172
165
|
return convergeAgent(
|
|
173
166
|
`You are the COMMIT step for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
@@ -181,6 +174,20 @@ function resumeFixerAgent(agentId, task, context) {
|
|
|
181
174
|
{ label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
182
175
|
)
|
|
183
176
|
}
|
|
177
|
+
if (task === 'commit-recover') {
|
|
178
|
+
const attempt = context.attempt || 1
|
|
179
|
+
return convergeAgent(
|
|
180
|
+
`You are the COMMIT-RECOVERY fixer (attempt ${attempt}) for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. A prior commit step was blocked by a commit-time hook or gate that requires a code change. A separate verify step then a separate commit step run after you.\n\n` +
|
|
181
|
+
`The blocking hook or gate said:\n${context.blockerDetail}\n\n` +
|
|
182
|
+
`Rules:\n` +
|
|
183
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
|
|
184
|
+
`- Fix ONLY the violation named above, test-first (failing test, then minimum code to pass) per CODE_RULES. Do not re-open the original findings, and do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
185
|
+
`- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push — the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
|
|
186
|
+
`Return values: edited=true with a one-line summary when you changed code to clear the block; edited=false, resolvedWithoutCommit=false when the block cannot be cleared with a code change.` +
|
|
187
|
+
PRE_COMMIT_GATE_STEP,
|
|
188
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
189
|
+
)
|
|
190
|
+
}
|
|
184
191
|
const objection = context.objection || VERIFY_OBJECTION_FALLBACK
|
|
185
192
|
const attempt = context.attempt || 1
|
|
186
193
|
return convergeAgent(
|
|
@@ -197,77 +204,41 @@ function resumeFixerAgent(agentId, task, context) {
|
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
/**
|
|
200
|
-
* Joined fixer recovery loop:
|
|
201
|
-
*
|
|
207
|
+
* Joined fixer recovery loop: a separate verifier agent grades the working-tree
|
|
208
|
+
* fixes while the fixer session only recovers and commits, so the verdict that
|
|
209
|
+
* gates the commit comes from a different session than the one that edits and
|
|
210
|
+
* pushes — the same editor/verifier separation the repair and conflict paths
|
|
211
|
+
* use. The verify step routes through verifyWithRecovery (verify on the verifier,
|
|
212
|
+
* recover on the fixer); the commit step routes through commitWithRecovery
|
|
213
|
+
* (commit and commit-recover on the fixer, re-verify on the verifier). A failed
|
|
214
|
+
* verdict returns the unchanged HEAD so the round reads as not-progressed.
|
|
202
215
|
* @param {string} fixerAgentId the fixer agent id from spawnFixerAgent
|
|
216
|
+
* @param {string} verifierId the verifier agent id from spawnVerifierAgent
|
|
203
217
|
* @param {string} head PR HEAD SHA
|
|
204
218
|
* @param {Array<object>} findings the findings to fix
|
|
205
219
|
* @param {string} sourceLabel short description of where the findings came from
|
|
206
220
|
* @returns {Promise<object>} FIX_SCHEMA result
|
|
207
221
|
*/
|
|
208
|
-
async function fixerWithRecovery(fixerAgentId, head, findings, sourceLabel) {
|
|
209
|
-
const verifyTranscript = await
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (recoverEdit?.edited !== true) break
|
|
222
|
-
const reVerify = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
223
|
-
const reVerdict = extractVerdict(reVerify)
|
|
224
|
-
if (!reVerdict || reVerdict.all_pass !== true) break
|
|
225
|
-
recoveryResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
226
|
-
}
|
|
227
|
-
return recoveryResult
|
|
228
|
-
}
|
|
229
|
-
return commitResult
|
|
230
|
-
}
|
|
231
|
-
let attempt = 0
|
|
232
|
-
let lastTranscript = verifyTranscript
|
|
233
|
-
while ((!verdict || verdict.all_pass !== true) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
234
|
-
attempt += 1
|
|
235
|
-
const objection = extractVerifyObjection(lastTranscript)
|
|
236
|
-
const recoverEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
237
|
-
head, findings, sourceLabel, objection, attempt,
|
|
238
|
-
})
|
|
239
|
-
if (recoverEdit?.edited !== true) break
|
|
240
|
-
lastTranscript = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
241
|
-
const freshVerdict = extractVerdict(lastTranscript)
|
|
242
|
-
if (freshVerdict && freshVerdict.all_pass === true) {
|
|
243
|
-
const commitResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
244
|
-
if (commitNeedsCodeRecovery(commitResult)) {
|
|
245
|
-
let commitAttempt = 0
|
|
246
|
-
let commitRecovery = commitResult
|
|
247
|
-
while (commitNeedsCodeRecovery(commitRecovery) && commitAttempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
248
|
-
commitAttempt += 1
|
|
249
|
-
const commitEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
250
|
-
head, findings, sourceLabel, objection: commitRecovery.blockerDetail, attempt: commitAttempt,
|
|
251
|
-
})
|
|
252
|
-
if (commitEdit?.edited !== true) break
|
|
253
|
-
const reVerify2 = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
254
|
-
const reVerdict2 = extractVerdict(reVerify2)
|
|
255
|
-
if (!reVerdict2 || reVerdict2.all_pass !== true) break
|
|
256
|
-
commitRecovery = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
257
|
-
}
|
|
258
|
-
return commitRecovery
|
|
259
|
-
}
|
|
260
|
-
return commitResult
|
|
222
|
+
async function fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel) {
|
|
223
|
+
const verifyTranscript = await verifyWithRecovery({
|
|
224
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
|
|
225
|
+
runRecoverEdit: (objection, attempt) => resumeFixerAgent(fixerAgentId, 'verify-recover', { head, findings, sourceLabel, objection, attempt }),
|
|
226
|
+
})
|
|
227
|
+
if (!verdictPassed(verifyTranscript)) {
|
|
228
|
+
return {
|
|
229
|
+
newSha: head,
|
|
230
|
+
pushed: false,
|
|
231
|
+
resolvedWithoutCommit: false,
|
|
232
|
+
summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
|
|
233
|
+
blockedNeedingEdit: false,
|
|
234
|
+
blockerDetail: '',
|
|
261
235
|
}
|
|
262
236
|
}
|
|
263
|
-
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
blockedNeedingEdit: false,
|
|
269
|
-
blockerDetail: '',
|
|
270
|
-
}
|
|
237
|
+
return commitWithRecovery({
|
|
238
|
+
runCommit: () => resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel }),
|
|
239
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
|
|
240
|
+
runRecoverEdit: (detail, attempt) => resumeFixerAgent(fixerAgentId, 'commit-recover', { head, findings, sourceLabel, blockerDetail: detail, attempt }),
|
|
241
|
+
})
|
|
271
242
|
}
|
|
272
243
|
|
|
273
244
|
/**
|
|
@@ -424,7 +395,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
|
|
|
424
395
|
|
|
425
396
|
/**
|
|
426
397
|
* Spawn the verifier code-verifier agent once per converge round, establishing
|
|
427
|
-
* its role so each later resume (repair-verify, hardening-verify) continues the
|
|
398
|
+
* its role so each later resume (fix-verify, repair-verify, hardening-verify) continues the
|
|
428
399
|
* same session. The spawn makes no edits — verification only. Returns the runtime
|
|
429
400
|
* agent id so the resume calls target the live session; a runtime without resume
|
|
430
401
|
* support returns no agent id, and each resume falls back to a fresh spawn.
|
|
@@ -447,6 +418,18 @@ async function spawnVerifierAgent() {
|
|
|
447
418
|
*/
|
|
448
419
|
function resumeVerifierAgent(agentId, task, context) {
|
|
449
420
|
const label = `verifier:${task}`
|
|
421
|
+
if (task === 'fix-verify') {
|
|
422
|
+
const findingsBlock = renderFindingsBlock(context.findings)
|
|
423
|
+
return convergeAgent(
|
|
424
|
+
`You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
425
|
+
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
426
|
+
`Steps:\n` +
|
|
427
|
+
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
428
|
+
`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` +
|
|
429
|
+
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
430
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
431
|
+
)
|
|
432
|
+
}
|
|
450
433
|
if (task === 'repair-verify') {
|
|
451
434
|
const failureBlock = context.failures.length
|
|
452
435
|
? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
@@ -929,15 +912,6 @@ function parseLastVerdictFence(transcript) {
|
|
|
929
912
|
}
|
|
930
913
|
}
|
|
931
914
|
|
|
932
|
-
/**
|
|
933
|
-
* Extract the full verdict object from a transcript carrying a verdict fence.
|
|
934
|
-
* @param {string|null|undefined} transcript the agent transcript text
|
|
935
|
-
* @returns {object|null} the parsed verdict with all_pass, findings, and manifest_sha256, or null
|
|
936
|
-
*/
|
|
937
|
-
function extractVerdict(transcript) {
|
|
938
|
-
return parseLastVerdictFence(transcript)
|
|
939
|
-
}
|
|
940
|
-
|
|
941
915
|
/**
|
|
942
916
|
* Decide whether a workflow code-verifier transcript ended in a passing
|
|
943
917
|
* verdict. Reads the LAST ```verdict ...``` fenced JSON block via the shared
|
|
@@ -1055,7 +1029,7 @@ function commitNeedsCodeRecovery(commitResult) {
|
|
|
1055
1029
|
* resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
|
|
1056
1030
|
* against it interpolates the literal string 'HEAD undefined' into their prompts
|
|
1057
1031
|
* and produces a spurious clean verdict on a non-existent commit.
|
|
1058
|
-
* @param {string|null|undefined} resolvedHead the SHA from
|
|
1032
|
+
* @param {string|null|undefined} resolvedHead the SHA from the git-utility agent resume for 'resolve-head'
|
|
1059
1033
|
* @returns {boolean} true only when the SHA is a non-empty string
|
|
1060
1034
|
*/
|
|
1061
1035
|
function isResolvedHeadUsable(resolvedHead) {
|
|
@@ -1068,7 +1042,7 @@ function isResolvedHeadUsable(resolvedHead) {
|
|
|
1068
1042
|
* not-conflicting so the run proceeds straight to the bug checks rather than
|
|
1069
1043
|
* force-pushing a rebase on a verdict that does not exist — a transient check
|
|
1070
1044
|
* failure must never trigger a destructive rebase.
|
|
1071
|
-
* @param {object|null|undefined} mergeState the
|
|
1045
|
+
* @param {object|null|undefined} mergeState the git-utility agent resume result for 'check-merge-conflicts'
|
|
1072
1046
|
* @returns {boolean} true only when the check reported conflicting:true
|
|
1073
1047
|
*/
|
|
1074
1048
|
function isMergeConflicting(mergeState) {
|
|
@@ -1357,14 +1331,16 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
|
|
|
1357
1331
|
}
|
|
1358
1332
|
|
|
1359
1333
|
/**
|
|
1360
|
-
* Fix lens: edit (clean-coder, no commit) -> verify (code-verifier
|
|
1361
|
-
* verdict fence binding the working tree) -> commit (clean-coder, one
|
|
1362
|
-
* push, no edits).
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1334
|
+
* Fix lens: edit (clean-coder, no commit) -> verify (a separate code-verifier
|
|
1335
|
+
* emits a verdict fence binding the working tree) -> commit (clean-coder, one
|
|
1336
|
+
* commit + push, no edits). The verifier is a distinct persistent group from the
|
|
1337
|
+
* fixer, so the verdict that gates the commit comes from a different session than
|
|
1338
|
+
* the one that edits and pushes — the same editor/verifier separation the repair
|
|
1339
|
+
* and conflict paths use, and the separation a workflow code-verifier needs to
|
|
1340
|
+
* produce the verdict the verified-commit gate requires, which the SubagentStop
|
|
1341
|
+
* minter cannot mint for workflow-spawned agents. When verification fails (or the
|
|
1342
|
+
* edit step stalled with no thread to resolve), the commit step is skipped and the
|
|
1343
|
+
* unchanged HEAD is returned so the round reads as not-progressed.
|
|
1368
1344
|
* @param {string} head PR HEAD SHA the findings were raised against
|
|
1369
1345
|
* @param {Array<object>} findings deduped findings across all lenses
|
|
1370
1346
|
* @param {string} sourceLabel short description of where the findings came from
|
|
@@ -1383,8 +1359,9 @@ async function applyFixes(head, findings, sourceLabel) {
|
|
|
1383
1359
|
blockerDetail: '',
|
|
1384
1360
|
}
|
|
1385
1361
|
}
|
|
1386
|
-
const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel
|
|
1387
|
-
|
|
1362
|
+
const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel)
|
|
1363
|
+
const verifierId = await spawnVerifierAgent()
|
|
1364
|
+
return fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel)
|
|
1388
1365
|
}
|
|
1389
1366
|
|
|
1390
1367
|
/**
|
|
@@ -1561,7 +1538,7 @@ function standardsDeferralNote(findingsCount, hardeningPrOpened) {
|
|
|
1561
1538
|
* @param {string} sourceLabel short description of where the findings came from
|
|
1562
1539
|
* @returns {Promise<object>} `{ hardeningPrOpened }` — true only when the hardening PR was opened this round
|
|
1563
1540
|
*/
|
|
1564
|
-
async function spawnStandardsFollowUp(head, findings, sourceLabel
|
|
1541
|
+
async function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
1565
1542
|
const codeEditorId = await spawnCodeEditorAgent()
|
|
1566
1543
|
const editResult = await resumeCodeEditorAgent(codeEditorId, 'standards-edit', { head, findings, sourceLabel })
|
|
1567
1544
|
if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
|
|
@@ -1647,7 +1624,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1647
1624
|
if (isStandardsOnlyRound(findings)) {
|
|
1648
1625
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
1649
1626
|
const generalId = await spawnGeneralUtilityAgent()
|
|
1650
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round'
|
|
1627
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
1651
1628
|
standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1652
1629
|
const auditResult = await resumeGeneralUtilityAgent(generalId, 'post-clean-audit', { head })
|
|
1653
1630
|
if (!auditResult?.posted) {
|
|
@@ -1704,8 +1681,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1704
1681
|
if (copilotOutcome.kind === 'fix') {
|
|
1705
1682
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
1706
1683
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
1707
|
-
const
|
|
1708
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot', copilotGeneralId)
|
|
1684
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
1709
1685
|
standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1710
1686
|
copilotDown = false
|
|
1711
1687
|
copilotNote = null
|
|
@@ -23,6 +23,11 @@ from bugteam_scripts_constants.bugteam_code_rules_gate_constants import (
|
|
|
23
23
|
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
|
|
24
24
|
BANNED_NOUN_SPAN_GROUP_INDEX,
|
|
25
25
|
BANNED_NOUN_VIOLATION_PATTERN,
|
|
26
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX,
|
|
27
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX,
|
|
28
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX,
|
|
29
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX,
|
|
30
|
+
INLINE_DUPLICATE_BODY_VIOLATION_PATTERN,
|
|
26
31
|
HUNK_HEADER_RAW_PATTERN,
|
|
27
32
|
ISOLATION_DEFINITION_LINE_GROUP_INDEX,
|
|
28
33
|
ISOLATION_SPAN_GROUP_INDEX,
|
|
@@ -958,6 +963,43 @@ def banned_noun_span_range(violation_text: str) -> range | None:
|
|
|
958
963
|
return range(definition_line, definition_line + line_span)
|
|
959
964
|
|
|
960
965
|
|
|
966
|
+
def inline_duplicate_body_span_lines(violation_text: str) -> frozenset[int] | None:
|
|
967
|
+
"""Return the union of both spans of a same-file inline-duplicate issue, or None.
|
|
968
|
+
|
|
969
|
+
The same-file inline-duplicate message names two functions that share a body —
|
|
970
|
+
the helper and the enclosing function carrying the inline copy — and the live
|
|
971
|
+
Write/Edit hook scopes the violation by the UNION of both spans, blocking when
|
|
972
|
+
an edit touches either function. So the message carries both spans: ``(inline
|
|
973
|
+
duplicate body spans: helper at line H spanning P lines, enclosing at line E
|
|
974
|
+
spanning Q lines)``. The two spans can be disjoint (an unrelated function may
|
|
975
|
+
sit between the helper and its inline copy), so this returns the union as a
|
|
976
|
+
line-number set rather than a single contiguous range — a range covering the
|
|
977
|
+
gap would wrongly block an edit confined to that intervening function, which
|
|
978
|
+
the PreToolUse path leaves unflagged.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
The frozenset of every line in the helper span and the enclosing span, or
|
|
985
|
+
None when the text is not a same-file inline-duplicate violation.
|
|
986
|
+
"""
|
|
987
|
+
span_match = INLINE_DUPLICATE_BODY_VIOLATION_PATTERN.search(violation_text)
|
|
988
|
+
if span_match is None:
|
|
989
|
+
return None
|
|
990
|
+
helper_line = int(span_match.group(INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX))
|
|
991
|
+
helper_span = int(span_match.group(INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX))
|
|
992
|
+
enclosing_line = int(
|
|
993
|
+
span_match.group(INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX)
|
|
994
|
+
)
|
|
995
|
+
enclosing_span = int(
|
|
996
|
+
span_match.group(INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX)
|
|
997
|
+
)
|
|
998
|
+
helper_lines = range(helper_line, helper_line + helper_span)
|
|
999
|
+
enclosing_lines = range(enclosing_line, enclosing_line + enclosing_span)
|
|
1000
|
+
return frozenset(helper_lines) | frozenset(enclosing_lines)
|
|
1001
|
+
|
|
1002
|
+
|
|
961
1003
|
def _all_span_range_extractors() -> tuple[Callable[[str], range | None], ...]:
|
|
962
1004
|
return (
|
|
963
1005
|
function_length_span_range,
|
|
@@ -1003,10 +1045,15 @@ def split_violations_by_scope(
|
|
|
1003
1045
|
|
|
1004
1046
|
Returns:
|
|
1005
1047
|
Tuple ``(blocking, advisory)``. When *all_added_line_numbers* is
|
|
1006
|
-
None, every issue is blocking.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1048
|
+
None, every issue is blocking. A same-file inline-duplicate violation
|
|
1049
|
+
carries both the helper span and the enclosing span;
|
|
1050
|
+
``inline_duplicate_body_span_lines`` reconstructs their union as a
|
|
1051
|
+
line-number set, and the violation is blocking when an added line falls
|
|
1052
|
+
in either span — matching the live Write/Edit hook's union scoping. Every
|
|
1053
|
+
other diff-scoped violation (function-length, HOME/TMP isolation,
|
|
1054
|
+
banned-noun) carries one enclosing-unit span fragment that
|
|
1055
|
+
``enclosing_span_range`` reconstructs through one shared extractor
|
|
1056
|
+
registry; such a violation is blocking
|
|
1010
1057
|
when its declared span intersects the added lines (the unit grew or its
|
|
1011
1058
|
signature changed in this diff) and advisory otherwise (a pre-existing
|
|
1012
1059
|
untouched unit). Every other issue is blocking when its ``Line N:``
|
|
@@ -1017,6 +1064,13 @@ def split_violations_by_scope(
|
|
|
1017
1064
|
blocking: list[str] = []
|
|
1018
1065
|
advisory: list[str] = []
|
|
1019
1066
|
for each_issue in all_issues:
|
|
1067
|
+
inline_duplicate_lines = inline_duplicate_body_span_lines(each_issue)
|
|
1068
|
+
if inline_duplicate_lines is not None:
|
|
1069
|
+
if inline_duplicate_lines & all_added_line_numbers:
|
|
1070
|
+
blocking.append(each_issue)
|
|
1071
|
+
else:
|
|
1072
|
+
advisory.append(each_issue)
|
|
1073
|
+
continue
|
|
1020
1074
|
span_range = enclosing_span_range(each_issue)
|
|
1021
1075
|
if span_range is not None:
|
|
1022
1076
|
if any(each_line in all_added_line_numbers for each_line in span_range):
|
package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py
CHANGED
|
@@ -31,6 +31,15 @@ BANNED_NOUN_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
|
31
31
|
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX: int = 1
|
|
32
32
|
BANNED_NOUN_SPAN_GROUP_INDEX: int = 2
|
|
33
33
|
|
|
34
|
+
INLINE_DUPLICATE_BODY_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
35
|
+
r"\(inline duplicate body spans: helper at line (\d+) spanning (\d+) lines, "
|
|
36
|
+
r"enclosing at line (\d+) spanning (\d+) lines\)"
|
|
37
|
+
)
|
|
38
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX: int = 1
|
|
39
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX: int = 2
|
|
40
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX: int = 3
|
|
41
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX: int = 4
|
|
42
|
+
|
|
34
43
|
ALL_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
35
44
|
{".py", ".js", ".ts", ".tsx", ".jsx"}
|
|
36
45
|
)
|
|
@@ -1022,3 +1022,45 @@ def test_main_staged_mode_passes_on_staged_deletion_of_clean_file(
|
|
|
1022
1022
|
"a staged deletion has no staged blob; the gate must skip it cleanly "
|
|
1023
1023
|
"rather than fail closed as if the file were unreadable"
|
|
1024
1024
|
)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
_INLINE_DUPLICATE_MESSAGE = (
|
|
1028
|
+
"Function '_wait_for_render' duplicates an inline block in '_navigate_then_wait'"
|
|
1029
|
+
" — this function body is also present inline (Reuse before create / DRY) "
|
|
1030
|
+
"(inline duplicate body spans: helper at line 4 spanning 10 lines, "
|
|
1031
|
+
"enclosing at line 16 spanning 11 lines)"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def test_inline_duplicate_body_span_lines_unions_helper_and_enclosing_spans() -> None:
|
|
1036
|
+
"""The same-file inline-duplicate message carries both spans, and the gate
|
|
1037
|
+
recovers their union as a line-number set so a touch of either function blocks —
|
|
1038
|
+
mirroring the live Write/Edit hook's union scoping."""
|
|
1039
|
+
span_lines = gate_module.inline_duplicate_body_span_lines(_INLINE_DUPLICATE_MESSAGE)
|
|
1040
|
+
assert span_lines == frozenset(range(4, 14)) | frozenset(range(16, 27))
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def test_inline_duplicate_blocks_when_only_enclosing_copy_added() -> None:
|
|
1044
|
+
"""An added line in the enclosing span alone blocks, because the live hook scopes
|
|
1045
|
+
by the union of both spans and blocks the same edit — the common shape where a
|
|
1046
|
+
block is copied INTO a growing enclosing function, leaving the helper untouched."""
|
|
1047
|
+
added_line_in_enclosing_only = 18
|
|
1048
|
+
blocking, advisory = gate_module.split_violations_by_scope(
|
|
1049
|
+
[_INLINE_DUPLICATE_MESSAGE],
|
|
1050
|
+
all_added_line_numbers={added_line_in_enclosing_only},
|
|
1051
|
+
)
|
|
1052
|
+
assert blocking == [_INLINE_DUPLICATE_MESSAGE]
|
|
1053
|
+
assert advisory == []
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def test_inline_duplicate_advises_when_gap_line_added() -> None:
|
|
1057
|
+
"""An edit confined to the gap between the helper span (4-13) and the enclosing
|
|
1058
|
+
span (16-26) must not block, matching the live hook. The union set keeps the gap
|
|
1059
|
+
out of scope where a single contiguous range would wrongly block it."""
|
|
1060
|
+
gap_line_between_spans = 14
|
|
1061
|
+
blocking, advisory = gate_module.split_violations_by_scope(
|
|
1062
|
+
[_INLINE_DUPLICATE_MESSAGE],
|
|
1063
|
+
all_added_line_numbers={gap_line_between_spans},
|
|
1064
|
+
)
|
|
1065
|
+
assert advisory == [_INLINE_DUPLICATE_MESSAGE]
|
|
1066
|
+
assert blocking == []
|