claude-dev-env 1.71.0 → 1.72.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 +8 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +5 -3
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +39 -0
- package/agents/clean-coder.md +1 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_docstrings.py +60 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_test_assertions.py +152 -1
- package/hooks/blocking/code_rules_type_escape.py +447 -2
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +93 -0
- package/hooks/blocking/test_code_rules_enforcer_object_parameter.py +499 -0
- package/hooks/blocking/test_code_rules_enforcer_stale_test_name.py +103 -0
- package/hooks/hooks_constants/blocking_check_limits.py +14 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Show-Asset.ps1 +106 -0
- package/skills/autoconverge/SKILL.md +30 -3
- package/skills/autoconverge/reference/convergence.md +41 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +90 -0
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +98 -0
- package/skills/autoconverge/workflow/converge.mjs +176 -6
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +47 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -0
|
@@ -14,6 +14,7 @@ export const meta = {
|
|
|
14
14
|
description: 'Drive one draft PR to convergence in a single autonomous run: parallel Bugbot + code-review + bug-audit lenses on the same HEAD each round, dedup findings, fix once, re-verify, then a Copilot wait-gate and a final convergence check that marks the PR ready.',
|
|
15
15
|
whenToUse: 'Launched by the /autoconverge skill after it resolves PR scope, enters a worktree, and grants project .claude permissions.',
|
|
16
16
|
phases: [
|
|
17
|
+
{ title: 'Reuse', detail: 'Before convergence, one reuse lens scans the full diff for reuse improvements that are certain, behaviorally identical, and autonomously implementable, and applies the qualifying ones in one commit' },
|
|
17
18
|
{ title: 'Converge', detail: 'Bugbot + code-review + bug-audit in parallel each round; one clean-coder applies all fixes; loop until all three are clean on a stable HEAD' },
|
|
18
19
|
{ title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge; when Copilot is down or out of quota, log a notice and mark the PR ready with the gate bypassed' },
|
|
19
20
|
{ title: 'Finalize', detail: 'Run check_convergence.py; mark draft=false on a full pass' },
|
|
@@ -46,6 +47,11 @@ const HEADLESS_SAFETY_PREAMBLE =
|
|
|
46
47
|
const convergeAgent = (prompt, options) =>
|
|
47
48
|
agent(`${HEADLESS_SAFETY_PREAMBLE}${prompt}`, options)
|
|
48
49
|
|
|
50
|
+
const PRE_COMMIT_GATE_STEP =
|
|
51
|
+
`\n\nFINAL STEP — pre-commit gate check (do NOT commit): before your turn ends, prove your working-tree changes CAN be committed by dry-running the CODE_RULES commit gate that gates git commit (precommit_code_rules_gate). From inside the checkout that holds your changes, resolve its root with git rev-parse --show-toplevel, stage your changes with git add -A, then run exactly:\n` +
|
|
52
|
+
` python "${CONFIG.prLoopScripts}/code_rules_gate.py" --repo-root "<that root>" --staged\n` +
|
|
53
|
+
`Exit 0 means the commit gate would accept the commit. On any non-zero exit, read every violation it prints, fix each one test-first per CODE_RULES, and re-run the gate until it exits 0. Then unstage with git restore --staged . so the verify step reads the working-tree diff. Make NO git commit and NO git push here — this is a dry committability check; the separate verify and commit steps run after you, and the verified-commit gate is their job, not yours. Your turn does not end while the commit gate would reject the commit.`
|
|
54
|
+
|
|
49
55
|
const LENS_SCHEMA = {
|
|
50
56
|
type: 'object',
|
|
51
57
|
additionalProperties: false,
|
|
@@ -130,6 +136,28 @@ const REPAIR_EDIT_SCHEMA = {
|
|
|
130
136
|
required: ['edited', 'rebased', 'resolvedWithoutCommit', 'summary'],
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
const MERGE_CONFLICT_SCHEMA = {
|
|
140
|
+
type: 'object',
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
properties: {
|
|
143
|
+
conflicting: {
|
|
144
|
+
type: 'boolean',
|
|
145
|
+
description: 'true only when GitHub reports the PR branch conflicts with its base (mergeable:false or mergeable_state:dirty); false when it merges cleanly or mergeability could not be computed',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ['conflicting'],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const CONFLICT_EDIT_SCHEMA = {
|
|
152
|
+
type: 'object',
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
rebased: { type: 'boolean', description: 'true when the branch was rebased onto origin/main and every conflict resolved in the working tree' },
|
|
156
|
+
summary: { type: 'string' },
|
|
157
|
+
},
|
|
158
|
+
required: ['rebased', 'summary'],
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
const STANDARDS_EDIT_SCHEMA = {
|
|
134
162
|
type: 'object',
|
|
135
163
|
additionalProperties: false,
|
|
@@ -529,6 +557,19 @@ function isResolvedHeadUsable(resolvedHead) {
|
|
|
529
557
|
return typeof resolvedHead === 'string' && resolvedHead.length > 0
|
|
530
558
|
}
|
|
531
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Decide whether the pre-flight merge-conflict check found the PR branch in
|
|
562
|
+
* conflict with its base. A dead check agent (null/undefined result) reports
|
|
563
|
+
* not-conflicting so the run proceeds straight to the bug checks rather than
|
|
564
|
+
* force-pushing a rebase on a verdict that does not exist — a transient check
|
|
565
|
+
* failure must never trigger a destructive rebase.
|
|
566
|
+
* @param {object|null|undefined} mergeState the checkMergeConflicts result
|
|
567
|
+
* @returns {boolean} true only when the check reported conflicting:true
|
|
568
|
+
*/
|
|
569
|
+
function isMergeConflicting(mergeState) {
|
|
570
|
+
return mergeState != null && mergeState.conflicting === true
|
|
571
|
+
}
|
|
572
|
+
|
|
532
573
|
/**
|
|
533
574
|
* Decide whether the mark-ready step actually cleared the draft state. The run
|
|
534
575
|
* reports converged only when the mark-ready agent confirms ready:true; a dead
|
|
@@ -749,6 +790,29 @@ function runAuditLens(head) {
|
|
|
749
790
|
)
|
|
750
791
|
}
|
|
751
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Reuse lens: a one-time pre-convergence pass that scans the full diff for
|
|
795
|
+
* places the PR re-implements behavior the codebase already provides, and
|
|
796
|
+
* returns only the reuse improvements that are certain, behaviorally identical,
|
|
797
|
+
* and autonomously implementable. It reports findings without editing; the
|
|
798
|
+
* reuse pass routes the qualifying findings through applyFixes so they are
|
|
799
|
+
* implemented in one commit before the convergence rounds begin.
|
|
800
|
+
* @param {string} head PR HEAD SHA to evaluate
|
|
801
|
+
* @returns {Promise<object>} LENS_SCHEMA result carrying the qualifying reuse findings
|
|
802
|
+
*/
|
|
803
|
+
function runReuseAuditPass(head) {
|
|
804
|
+
return convergeAgent(
|
|
805
|
+
`You are the REUSE lens for ${prCoordinates}, HEAD ${head}. This pass runs once before convergence to find where the PR re-implements behavior the codebase already provides.\n\n` +
|
|
806
|
+
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or a single file. The workflow already fetched origin/main, so do NOT run git fetch; run git diff --name-only origin/main...HEAD to enumerate the changed files, then read the complete diff of each. For every new function, helper, constant, type, or block of logic the PR introduces, search the repository (Serena symbol search, grep, and the project's config/ and shared/ modules) for an existing equivalent that already provides the same behavior.\n\n` +
|
|
807
|
+
`Report a reuse finding ONLY when ALL THREE criteria hold — when any one is in doubt, omit the finding:\n` +
|
|
808
|
+
` A. CERTAIN: an existing symbol or module unquestionably covers the new code's behavior, and you can cite it at file:line.\n` +
|
|
809
|
+
` B. BEHAVIORALLY IDENTICAL: replacing the new code with the existing one changes no observable behavior — same inputs, outputs, side effects, and error handling.\n` +
|
|
810
|
+
` C. AUTONOMOUSLY IMPLEMENTABLE: the replacement is a mechanical edit (import and call the existing symbol, delete the duplicate) that needs no product decision, no API the existing code lacks, and no human judgment.\n\n` +
|
|
811
|
+
`Do NOT edit, commit, or push — report only; a separate fix step applies what you return. Return strictly the schema: clean=true with empty findings when no reuse case clears all three criteria, otherwise one entry per qualifying reuse improvement. For each: file and line of the duplicate in the PR; severity P2; category 'code-standard'; title naming the existing symbol to reuse; detail giving the existing symbol's file:line and the exact mechanical replacement; replyToCommentId=null. Set sha=${'`'}${head}${'`'}, down=false.`,
|
|
812
|
+
{ label: 'lens:reuse', phase: 'Reuse', schema: LENS_SCHEMA, agentType: 'code-quality-agent' },
|
|
813
|
+
)
|
|
814
|
+
}
|
|
815
|
+
|
|
752
816
|
/**
|
|
753
817
|
* Render the numbered findings block shared by the fix steps.
|
|
754
818
|
* @param {Array<object>} findings deduped findings to render
|
|
@@ -792,7 +856,8 @@ function applyFixesEdit(head, findings, sourceLabel) {
|
|
|
792
856
|
`Return values:\n` +
|
|
793
857
|
`- When you edited code to fix at least one finding: edited=true, resolvedWithoutCommit=false.\n` +
|
|
794
858
|
`- 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` +
|
|
795
|
-
`Always include a one-line summary
|
|
859
|
+
`Always include a one-line summary.` +
|
|
860
|
+
PRE_COMMIT_GATE_STEP,
|
|
796
861
|
{ label: `fix-edit:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
797
862
|
)
|
|
798
863
|
}
|
|
@@ -865,7 +930,8 @@ function recoverCommitBlockEdit(head, blockerDetail, sourceLabel, attempt) {
|
|
|
865
930
|
`- Confirm the working tree is on the PR branch at HEAD ${head} with the prior fixes still present.\n` +
|
|
866
931
|
`- 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` +
|
|
867
932
|
`- 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` +
|
|
868
|
-
`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
|
|
933
|
+
`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.` +
|
|
934
|
+
PRE_COMMIT_GATE_STEP,
|
|
869
935
|
{ label: `fix-recover:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
870
936
|
)
|
|
871
937
|
}
|
|
@@ -891,7 +957,8 @@ function recoverVerifyFailEdit(head, objection, sourceLabel, attempt) {
|
|
|
891
957
|
`- Confirm the working tree is on the PR branch at HEAD ${head} with the prior fixes still present.\n` +
|
|
892
958
|
`- Address every objection above test-first (failing test, then minimum code to pass) per CODE_RULES, so each named concern is genuinely resolved the way the verdict requires. Do not touch GitHub review threads — the edit step already handled those.\n` +
|
|
893
959
|
`- 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` +
|
|
894
|
-
`Return values: edited=true with a one-line summary when you changed code to address the objections; edited=false, resolvedWithoutCommit=false when the objections cannot be cleared with a code change
|
|
960
|
+
`Return values: edited=true with a one-line summary when you changed code to address the objections; edited=false, resolvedWithoutCommit=false when the objections cannot be cleared with a code change.` +
|
|
961
|
+
PRE_COMMIT_GATE_STEP,
|
|
895
962
|
{ label: `fix-verify-recover:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
896
963
|
)
|
|
897
964
|
}
|
|
@@ -1130,7 +1197,8 @@ function repairConvergenceEdit(head, failures) {
|
|
|
1130
1197
|
`- edited=true when you changed code in the working tree to fix a bot-thread concern.\n` +
|
|
1131
1198
|
`- rebased=true when you rebased the branch onto origin/main.\n` +
|
|
1132
1199
|
`- 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` +
|
|
1133
|
-
`Always include a one-line summary
|
|
1200
|
+
`Always include a one-line summary.` +
|
|
1201
|
+
PRE_COMMIT_GATE_STEP,
|
|
1134
1202
|
{ label: 'repair-edit', phase: 'Finalize', schema: REPAIR_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1135
1203
|
)
|
|
1136
1204
|
}
|
|
@@ -1235,6 +1303,79 @@ async function repairConvergence(head, failures) {
|
|
|
1235
1303
|
})
|
|
1236
1304
|
}
|
|
1237
1305
|
|
|
1306
|
+
/**
|
|
1307
|
+
* Pre-flight merge-conflict check: ask GitHub whether the PR branch still merges
|
|
1308
|
+
* cleanly into its base. GitHub computes mergeability asynchronously, so the
|
|
1309
|
+
* agent polls until .mergeable resolves to a boolean before judging. Read-only —
|
|
1310
|
+
* it makes no edit, commit, push, or rebase.
|
|
1311
|
+
* @param {string} head PR HEAD SHA the check runs against
|
|
1312
|
+
* @returns {Promise<object>} MERGE_CONFLICT_SCHEMA result
|
|
1313
|
+
*/
|
|
1314
|
+
function checkMergeConflicts(head) {
|
|
1315
|
+
return convergeAgent(
|
|
1316
|
+
`Report whether ${prCoordinates} (HEAD ${head}) has merge conflicts with its base branch. Do not edit, commit, push, or rebase — read only.\n\n` +
|
|
1317
|
+
`GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
|
|
1318
|
+
` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
|
|
1319
|
+
`up to 5 times, 5 seconds apart (delay each retry with "sleep 5", or the PowerShell alternative "Start-Sleep -Seconds 5"), stopping as soon as mergeable is true or false.\n\n` +
|
|
1320
|
+
`Return conflicting:true when mergeable is false or state is "dirty" (the branch conflicts with the base). Return conflicting:false when mergeable is true, or when mergeable stays null after the full poll budget — mergeability is unknown, so let the bug checks proceed rather than rebase on a guess.`,
|
|
1321
|
+
{ label: 'check-merge-conflicts', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },
|
|
1322
|
+
)
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Conflict-resolution edit step: one clean-coder rebases the PR branch onto
|
|
1327
|
+
* origin/main and resolves every conflict in the working tree, making NO push —
|
|
1328
|
+
* the verify step binds a verdict to the rebased tree before the commit step
|
|
1329
|
+
* force-pushes it. A rebase necessarily creates local commits, which is expected;
|
|
1330
|
+
* only the force-push is withheld so the verifier binds the surface first.
|
|
1331
|
+
* @param {string} head PR HEAD SHA before the rebase
|
|
1332
|
+
* @returns {Promise<object>} CONFLICT_EDIT_SCHEMA result
|
|
1333
|
+
*/
|
|
1334
|
+
function resolveConflictsEdit(head) {
|
|
1335
|
+
return convergeAgent(
|
|
1336
|
+
`You are the EDIT step resolving merge conflicts for ${prCoordinates}, HEAD ${head}, before the bug checks run. The PR branch conflicts with origin/main. A separate verify step then a separate commit step run after you.\n\n` +
|
|
1337
|
+
`Rules:\n` +
|
|
1338
|
+
`- Confirm the working tree is on the PR branch at HEAD ${head} with no unrelated edits before you start.\n` +
|
|
1339
|
+
`- Rebase the branch onto origin/main and resolve every conflict so the tree is clean and conflict-free: git fetch origin main; git rebase origin/main; resolve each conflict, preserving the intent of both the PR's change and the incoming base change. A rebase creates local commits, which is fine.\n` +
|
|
1340
|
+
`- Do NOT push and do NOT force-push — the commit step force-pushes after the verify step binds a verdict. Pushing here would change the surface the verifier binds to.\n\n` +
|
|
1341
|
+
`Return rebased=true with a one-line summary when you rebased onto origin/main and resolved the conflicts; rebased=false with a summary when the branch did not actually need a rebase or you could not complete it.`,
|
|
1342
|
+
{ label: 'resolve-conflicts-edit', phase: 'Converge', schema: CONFLICT_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1343
|
+
)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Pre-flight conflict resolution: when the PR branch conflicts with its base,
|
|
1348
|
+
* rebase it clean before the bug checks run — check (Explore probes mergeability)
|
|
1349
|
+
* -> edit (clean-coder rebases and resolves, no push) -> verify (code-verifier
|
|
1350
|
+
* binds a verdict to the rebased tree) -> commit (clean-coder force-with-lease
|
|
1351
|
+
* pushes). Returns the post-rebase HEAD so the first converge round runs its
|
|
1352
|
+
* lenses on the conflict-free diff. A non-conflicting PR, a rebase the edit step
|
|
1353
|
+
* declined, or a failed verdict returns the unchanged HEAD so the run proceeds to
|
|
1354
|
+
* the bug checks unchanged. A mid-run conflict (origin/main advancing later) is
|
|
1355
|
+
* still caught by the FINALIZE convergence repair, which also rebases.
|
|
1356
|
+
* @param {string} head PR HEAD SHA before any rebase
|
|
1357
|
+
* @returns {Promise<string>} the HEAD SHA after a successful rebase push, or the unchanged head
|
|
1358
|
+
*/
|
|
1359
|
+
async function resolveMergeConflicts(head) {
|
|
1360
|
+
const mergeState = await checkMergeConflicts(head)
|
|
1361
|
+
if (!isMergeConflicting(mergeState)) return head
|
|
1362
|
+
log(`Pre-flight: ${prCoordinates} conflicts with origin/main — rebasing clean before the bug checks`)
|
|
1363
|
+
const editResult = await resolveConflictsEdit(head)
|
|
1364
|
+
if (editResult?.rebased !== true) return head
|
|
1365
|
+
const failures = ['PR branch had merge conflicts with origin/main; the rebase must leave a clean, conflict-free tree']
|
|
1366
|
+
const verifyTranscript = await verifyWithRecovery({
|
|
1367
|
+
runVerify: () => verifyRepairChanges(head, failures),
|
|
1368
|
+
runRecoverEdit: (objection, attempt) => recoverVerifyFailEdit(head, objection, 'conflict-rebase', attempt),
|
|
1369
|
+
})
|
|
1370
|
+
if (!verdictPassed(verifyTranscript)) return head
|
|
1371
|
+
const commitResult = await commitWithRecovery({
|
|
1372
|
+
runCommit: () => commitRepairFixes(head, true),
|
|
1373
|
+
runVerify: () => verifyRepairChanges(head, failures),
|
|
1374
|
+
runRecoverEdit: (detail, attempt) => recoverCommitBlockEdit(head, detail, 'conflict-rebase', attempt),
|
|
1375
|
+
})
|
|
1376
|
+
return commitResult?.newSha || head
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1238
1379
|
/**
|
|
1239
1380
|
* Decide whether a review round surfaced ONLY code-standard violations — pure
|
|
1240
1381
|
* CODE_RULES/style findings with no behavioral impact. Such a round passes for
|
|
@@ -1269,7 +1410,8 @@ function standardsFollowUpEdit(head, findings, sourceLabel) {
|
|
|
1269
1410
|
`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` +
|
|
1270
1411
|
`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` +
|
|
1271
1412
|
`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` +
|
|
1272
|
-
`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
|
|
1413
|
+
`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.` +
|
|
1414
|
+
PRE_COMMIT_GATE_STEP,
|
|
1273
1415
|
{ label: `standards-edit:${sourceLabel}`, phase: 'Converge', schema: STANDARDS_EDIT_SCHEMA, agentType: 'clean-coder' },
|
|
1274
1416
|
)
|
|
1275
1417
|
}
|
|
@@ -1428,9 +1570,36 @@ let bugbotDown = input.bugbotDisabled || false
|
|
|
1428
1570
|
let copilotDown = false
|
|
1429
1571
|
let copilotNote = null
|
|
1430
1572
|
let standardsNote = null
|
|
1573
|
+
let reuseNote = null
|
|
1431
1574
|
const allRoundFindings = []
|
|
1432
1575
|
const fixSummaries = []
|
|
1433
1576
|
|
|
1577
|
+
const preflightHead = await resolveHead()
|
|
1578
|
+
if (isResolvedHeadUsable(preflightHead)) {
|
|
1579
|
+
await resolveMergeConflicts(preflightHead)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
log('Reuse pass: scanning the full diff for certain, behaviorally identical, autonomously implementable reuse improvements before convergence')
|
|
1583
|
+
const reuseHead = await resolveHead()
|
|
1584
|
+
if (isResolvedHeadUsable(reuseHead)) {
|
|
1585
|
+
await prefetchMainForRound()
|
|
1586
|
+
const reuse = await runReuseAuditPass(reuseHead)
|
|
1587
|
+
const reuseFindings = reuse?.findings || []
|
|
1588
|
+
if (reuseFindings.length > 0) {
|
|
1589
|
+
log(`Reuse pass: ${reuseFindings.length} qualifying reuse improvement(s) — applying before convergence`)
|
|
1590
|
+
allRoundFindings.push(...reuseFindings)
|
|
1591
|
+
const reuseFix = await applyFixes(reuseHead, reuseFindings, 'reuse-pass')
|
|
1592
|
+
if (reuseFix?.summary) fixSummaries.push(reuseFix.summary)
|
|
1593
|
+
reuseNote = reuseFix?.pushed === true
|
|
1594
|
+
? `${reuseFindings.length} reuse improvement(s) applied before convergence (${reuseFix.newSha?.slice(0, SHA_COMPARISON_PREFIX_LENGTH)})`
|
|
1595
|
+
: `${reuseFindings.length} reuse improvement(s) identified before convergence but not landed — the code-review lens re-surfaces any that remain`
|
|
1596
|
+
} else {
|
|
1597
|
+
log('Reuse pass: no reuse case cleared all three criteria — proceeding to convergence')
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
log('Reuse pass: could not resolve HEAD — proceeding to convergence')
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1434
1603
|
while (iterations < CONFIG.maxIterations) {
|
|
1435
1604
|
iterations += 1
|
|
1436
1605
|
if (phase === 'CONVERGE') {
|
|
@@ -1556,7 +1725,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1556
1725
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
1557
1726
|
if (readyOutcome.converged) {
|
|
1558
1727
|
await spawnConvergenceSummary(dedupeFindings(allRoundFindings), fixSummaries, rounds, standardsNote, copilotNote)
|
|
1559
|
-
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote }
|
|
1728
|
+
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote }
|
|
1560
1729
|
}
|
|
1561
1730
|
blocker = readyOutcome.blocker
|
|
1562
1731
|
break
|
|
@@ -1575,4 +1744,5 @@ return {
|
|
|
1575
1744
|
blocker: blocker || `iteration cap reached (${CONFIG.maxIterations})`,
|
|
1576
1745
|
standardsNote,
|
|
1577
1746
|
copilotNote,
|
|
1747
|
+
reuseNote,
|
|
1578
1748
|
}
|
|
@@ -544,6 +544,48 @@ def check_database_column_string_magic(content: str, file_path: str) -> list[str
|
|
|
544
544
|
return issues
|
|
545
545
|
|
|
546
546
|
|
|
547
|
+
def is_test_path(file_path: str) -> bool:
|
|
548
|
+
"""Return True when *file_path* matches CODE_RULES.md test-file detection patterns.
|
|
549
|
+
|
|
550
|
+
Mirrors the test-file detection rule documented in CODE_RULES.md:
|
|
551
|
+
filename matches test_*.py OR *_test.py OR *.test.* OR *.spec.* OR
|
|
552
|
+
conftest.py, OR path contains the segment /tests/.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
file_path: Path string to classify; backslashes are normalized to
|
|
556
|
+
forward slashes before pattern matching.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
True when the path matches any test-file pattern; False otherwise.
|
|
560
|
+
"""
|
|
561
|
+
tests_path_segment = "/tests/"
|
|
562
|
+
conftest_filename = "conftest.py"
|
|
563
|
+
test_filename_prefix = "test_"
|
|
564
|
+
all_test_filename_suffixes = ("_test.py",)
|
|
565
|
+
all_test_filename_glob_suffixes = (".test.", ".spec.")
|
|
566
|
+
normalized_posix = file_path.replace("\\", "/")
|
|
567
|
+
filename_only = normalized_posix.rsplit("/", maxsplit=1)[-1]
|
|
568
|
+
if tests_path_segment in normalized_posix:
|
|
569
|
+
return True
|
|
570
|
+
if filename_only == conftest_filename:
|
|
571
|
+
return True
|
|
572
|
+
if filename_only.startswith(test_filename_prefix) and filename_only.endswith(
|
|
573
|
+
PYTHON_FILE_EXTENSION
|
|
574
|
+
):
|
|
575
|
+
return True
|
|
576
|
+
if any(
|
|
577
|
+
filename_only.endswith(each_suffix)
|
|
578
|
+
for each_suffix in all_test_filename_suffixes
|
|
579
|
+
):
|
|
580
|
+
return True
|
|
581
|
+
if any(
|
|
582
|
+
each_glob_suffix in filename_only
|
|
583
|
+
for each_glob_suffix in all_test_filename_glob_suffixes
|
|
584
|
+
):
|
|
585
|
+
return True
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
|
|
547
589
|
def _iter_calls_excluding_nested_functions(node: ast.AST) -> Iterator[ast.Call]:
|
|
548
590
|
for each_child in ast.iter_child_nodes(node):
|
|
549
591
|
if isinstance(each_child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -636,11 +678,11 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
|
|
|
636
678
|
Args:
|
|
637
679
|
content: File content as a single string for AST parsing.
|
|
638
680
|
file_path: Repository-relative POSIX path of the file (used to
|
|
639
|
-
skip non-Python code extensions early).
|
|
681
|
+
skip non-Python code extensions and test files early).
|
|
640
682
|
|
|
641
683
|
Returns:
|
|
642
|
-
List of violation strings, one per dropped optional kwarg.
|
|
643
|
-
|
|
684
|
+
List of violation strings, one per dropped optional kwarg. Empty for
|
|
685
|
+
a non-Python file, a test file, or a file with a syntax error.
|
|
644
686
|
"""
|
|
645
687
|
non_python_code_extensions = ALL_CODE_FILE_EXTENSIONS - {PYTHON_FILE_EXTENSION}
|
|
646
688
|
lowercase_file_path = file_path.lower()
|
|
@@ -649,6 +691,8 @@ def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
|
|
|
649
691
|
for each_extension in non_python_code_extensions
|
|
650
692
|
):
|
|
651
693
|
return []
|
|
694
|
+
if is_test_path(file_path):
|
|
695
|
+
return []
|
|
652
696
|
try:
|
|
653
697
|
tree = ast.parse(content)
|
|
654
698
|
except SyntaxError:
|
|
@@ -790,6 +790,40 @@ def test_check_wrapper_plumb_through_flags_name_call_dropping_kwarg() -> None:
|
|
|
790
790
|
)
|
|
791
791
|
|
|
792
792
|
|
|
793
|
+
def test_check_wrapper_plumb_through_exempts_test_files() -> None:
|
|
794
|
+
"""A test_* function in a test-file path that calls a module-level helper
|
|
795
|
+
exposing an optional kwarg is an ordinary pytest case, not a wrapper; the
|
|
796
|
+
bugteam gate must exempt test files and emit zero findings."""
|
|
797
|
+
source = (
|
|
798
|
+
"def _helper(name, *, clean_name=None):\n"
|
|
799
|
+
" return (name, clean_name)\n"
|
|
800
|
+
"\n"
|
|
801
|
+
"def test_uses_helper():\n"
|
|
802
|
+
" return _helper('a', clean_name='b')\n"
|
|
803
|
+
)
|
|
804
|
+
issues = gate_module.check_wrapper_plumb_through(source, "pkg/test_thing.py")
|
|
805
|
+
assert issues == [], (
|
|
806
|
+
f"a wrapper shape in a test file must yield no findings; got {issues!r}"
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def test_check_wrapper_plumb_through_still_flags_non_test_path_with_test_shape() -> None:
|
|
811
|
+
"""The test-file exemption is scoped to test paths only; the same wrapper
|
|
812
|
+
shape on a non-test path must still be flagged."""
|
|
813
|
+
source = (
|
|
814
|
+
"def _helper(name, *, clean_name=None):\n"
|
|
815
|
+
" return (name, clean_name)\n"
|
|
816
|
+
"\n"
|
|
817
|
+
"def test_uses_helper():\n"
|
|
818
|
+
" return _helper('a', clean_name='b')\n"
|
|
819
|
+
)
|
|
820
|
+
issues = gate_module.check_wrapper_plumb_through(source, "pkg/module.py")
|
|
821
|
+
assert any(
|
|
822
|
+
"test_uses_helper" in each_issue and "clean_name" in each_issue
|
|
823
|
+
for each_issue in issues
|
|
824
|
+
), f"the same wrapper shape on a non-test path must still flag; got {issues!r}"
|
|
825
|
+
|
|
826
|
+
|
|
793
827
|
def test_check_wrapper_plumb_through_ignores_calls_nested_inside_delegate_arguments() -> None:
|
|
794
828
|
"""A callee nested as an argument (``delegate(helper(x))``) is not a
|
|
795
829
|
separate call site; only the enclosing call is inspected, matching the
|