claude-dev-env 1.58.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
const workflowDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
|
|
9
|
-
const gotchasSource = readFileSync(
|
|
10
|
-
join(workflowDirectory, '..', 'reference', 'gotchas.md'),
|
|
11
|
-
'utf8',
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
function lensPromptBody(builderName) {
|
|
15
|
-
const builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
16
|
-
assert.notEqual(builderStart, -1, `expected ${builderName} to exist`);
|
|
17
|
-
const nextBuilderStart = convergeSource.indexOf('\nfunction ', builderStart + 1);
|
|
18
|
-
const builderEnd = nextBuilderStart === -1 ? convergeSource.length : nextBuilderStart;
|
|
19
|
-
return convergeSource.slice(builderStart, builderEnd);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
23
|
-
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
27
|
-
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
31
|
-
assert.match(convergeSource, /function prefetchMainForRound\(/);
|
|
32
|
-
const prefetchCallIndex = convergeSource.indexOf('await prefetchMainForRound(');
|
|
33
|
-
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
34
|
-
assert.notEqual(prefetchCallIndex, -1, 'expected prefetchMainForRound to be invoked');
|
|
35
|
-
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
36
|
-
assert.ok(
|
|
37
|
-
prefetchCallIndex < parallelLensIndex,
|
|
38
|
-
'expected the round prefetch to run before the parallel lenses spawn',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
43
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
44
|
-
assert.doesNotMatch(
|
|
45
|
-
bugbotPrompt,
|
|
46
|
-
/use the existing scripts; pass --owner/,
|
|
47
|
-
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
52
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
53
|
-
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
54
|
-
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
55
|
-
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
56
|
-
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
57
|
-
assert.match(invocationLine, /--reviewer bugbot/);
|
|
58
|
-
assert.doesNotMatch(
|
|
59
|
-
invocationLine,
|
|
60
|
-
/--owner|--repo/,
|
|
61
|
-
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
66
|
-
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
67
|
-
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('repair-convergence filters unresolved threads to bot authors and skips human threads', () => {
|
|
71
|
-
const repairPrompt = lensPromptBody('repairConvergence');
|
|
72
|
-
assert.match(
|
|
73
|
-
repairPrompt,
|
|
74
|
-
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
75
|
-
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
76
|
-
);
|
|
77
|
-
assert.match(
|
|
78
|
-
repairPrompt,
|
|
79
|
-
/skip.*human|human.*skip/is,
|
|
80
|
-
'expected an explicit instruction to skip human reviewer threads',
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('repair-convergence no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
85
|
-
const repairPrompt = lensPromptBody('repairConvergence');
|
|
86
|
-
assert.doesNotMatch(
|
|
87
|
-
repairPrompt,
|
|
88
|
-
/fetch every thread where isResolved is false/,
|
|
89
|
-
'the unfiltered instruction could resolve human reviewer threads',
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('bugbot lens delay instructions are shell-agnostic with PowerShell as an alternative', () => {
|
|
94
|
-
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
95
|
-
assert.match(bugbotPrompt, /sleep 60/, 'expected a shell-agnostic 60-second poll delay');
|
|
96
|
-
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
97
|
-
assert.match(
|
|
98
|
-
bugbotPrompt,
|
|
99
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
100
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
101
|
-
);
|
|
102
|
-
assert.doesNotMatch(
|
|
103
|
-
bugbotPrompt,
|
|
104
|
-
/wait 8 seconds(?!,)/,
|
|
105
|
-
'the vague "wait 8 seconds" phrasing must carry a concrete command',
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('copilot gate delay instruction is shell-agnostic with PowerShell as an alternative', () => {
|
|
110
|
-
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
111
|
-
assert.match(copilotPrompt, /sleep 360/, 'expected a shell-agnostic 360-second poll delay');
|
|
112
|
-
assert.match(
|
|
113
|
-
copilotPrompt,
|
|
114
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
115
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('gotchas doc describes the reviewer wait as shell-agnostic', () => {
|
|
120
|
-
assert.match(
|
|
121
|
-
gotchasSource,
|
|
122
|
-
/\bsleep\b/i,
|
|
123
|
-
'expected the wait guidance to name a shell-agnostic sleep',
|
|
124
|
-
);
|
|
125
|
-
assert.doesNotMatch(
|
|
126
|
-
gotchasSource,
|
|
127
|
-
/a single PowerShell\s*`?Start-Sleep`?\s*loop/i,
|
|
128
|
-
'PowerShell Start-Sleep must be an alternative, not the sole mechanism',
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
function finalizeRepairBranch() {
|
|
133
|
-
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
134
|
-
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
135
|
-
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
136
|
-
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
137
|
-
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
138
|
-
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
139
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
140
|
-
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
144
|
-
assert.doesNotMatch(
|
|
145
|
-
finalizeRepairBranch(),
|
|
146
|
-
/head\s*=\s*repair/,
|
|
147
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
function fixBranchAfter(branchLabel) {
|
|
152
|
-
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
153
|
-
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
154
|
-
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
155
|
-
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
156
|
-
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
157
|
-
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
158
|
-
const branchEnd = continueIndex + 'continue'.length;
|
|
159
|
-
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
163
|
-
assert.doesNotMatch(
|
|
164
|
-
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
165
|
-
/head\s*=\s*fixProgress/,
|
|
166
|
-
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
171
|
-
assert.doesNotMatch(
|
|
172
|
-
fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
|
|
173
|
-
/head\s*=\s*fixProgress/,
|
|
174
|
-
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
179
|
-
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
180
|
-
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
181
|
-
const resolveHeadIndex = convergeSource.indexOf('head = await resolveHead()', convergeBranchStart);
|
|
182
|
-
assert.notEqual(resolveHeadIndex, -1, 'expected CONVERGE to re-resolve HEAD via resolveHead()');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('fix prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
186
|
-
const fixPrompt = lensPromptBody('applyFixes');
|
|
187
|
-
assert.match(fixPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
188
|
-
assert.match(
|
|
189
|
-
fixPrompt,
|
|
190
|
-
/databaseId/,
|
|
191
|
-
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
192
|
-
);
|
|
193
|
-
assert.match(
|
|
194
|
-
fixPrompt,
|
|
195
|
-
/not the numeric comment id/,
|
|
196
|
-
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('fix prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
201
|
-
assert.doesNotMatch(
|
|
202
|
-
lensPromptBody('applyFixes'),
|
|
203
|
-
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
204
|
-
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
205
|
-
);
|
|
206
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const workflowDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
|
|
9
|
+
const gotchasSource = readFileSync(
|
|
10
|
+
join(workflowDirectory, '..', 'reference', 'gotchas.md'),
|
|
11
|
+
'utf8',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function lensPromptBody(builderName) {
|
|
15
|
+
const builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
16
|
+
assert.notEqual(builderStart, -1, `expected ${builderName} to exist`);
|
|
17
|
+
const nextBuilderStart = convergeSource.indexOf('\nfunction ', builderStart + 1);
|
|
18
|
+
const builderEnd = nextBuilderStart === -1 ? convergeSource.length : nextBuilderStart;
|
|
19
|
+
return convergeSource.slice(builderStart, builderEnd);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
23
|
+
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
27
|
+
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
31
|
+
assert.match(convergeSource, /function prefetchMainForRound\(/);
|
|
32
|
+
const prefetchCallIndex = convergeSource.indexOf('await prefetchMainForRound(');
|
|
33
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
34
|
+
assert.notEqual(prefetchCallIndex, -1, 'expected prefetchMainForRound to be invoked');
|
|
35
|
+
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
36
|
+
assert.ok(
|
|
37
|
+
prefetchCallIndex < parallelLensIndex,
|
|
38
|
+
'expected the round prefetch to run before the parallel lenses spawn',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
43
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
44
|
+
assert.doesNotMatch(
|
|
45
|
+
bugbotPrompt,
|
|
46
|
+
/use the existing scripts; pass --owner/,
|
|
47
|
+
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
52
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
53
|
+
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
54
|
+
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
55
|
+
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
56
|
+
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
57
|
+
assert.match(invocationLine, /--reviewer bugbot/);
|
|
58
|
+
assert.doesNotMatch(
|
|
59
|
+
invocationLine,
|
|
60
|
+
/--owner|--repo/,
|
|
61
|
+
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
66
|
+
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
67
|
+
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('repair-convergence filters unresolved threads to bot authors and skips human threads', () => {
|
|
71
|
+
const repairPrompt = lensPromptBody('repairConvergence');
|
|
72
|
+
assert.match(
|
|
73
|
+
repairPrompt,
|
|
74
|
+
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
75
|
+
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
76
|
+
);
|
|
77
|
+
assert.match(
|
|
78
|
+
repairPrompt,
|
|
79
|
+
/skip.*human|human.*skip/is,
|
|
80
|
+
'expected an explicit instruction to skip human reviewer threads',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('repair-convergence no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
85
|
+
const repairPrompt = lensPromptBody('repairConvergence');
|
|
86
|
+
assert.doesNotMatch(
|
|
87
|
+
repairPrompt,
|
|
88
|
+
/fetch every thread where isResolved is false/,
|
|
89
|
+
'the unfiltered instruction could resolve human reviewer threads',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('bugbot lens delay instructions are shell-agnostic with PowerShell as an alternative', () => {
|
|
94
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
95
|
+
assert.match(bugbotPrompt, /sleep 60/, 'expected a shell-agnostic 60-second poll delay');
|
|
96
|
+
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
97
|
+
assert.match(
|
|
98
|
+
bugbotPrompt,
|
|
99
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
100
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
101
|
+
);
|
|
102
|
+
assert.doesNotMatch(
|
|
103
|
+
bugbotPrompt,
|
|
104
|
+
/wait 8 seconds(?!,)/,
|
|
105
|
+
'the vague "wait 8 seconds" phrasing must carry a concrete command',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('copilot gate delay instruction is shell-agnostic with PowerShell as an alternative', () => {
|
|
110
|
+
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
111
|
+
assert.match(copilotPrompt, /sleep 360/, 'expected a shell-agnostic 360-second poll delay');
|
|
112
|
+
assert.match(
|
|
113
|
+
copilotPrompt,
|
|
114
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
115
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('gotchas doc describes the reviewer wait as shell-agnostic', () => {
|
|
120
|
+
assert.match(
|
|
121
|
+
gotchasSource,
|
|
122
|
+
/\bsleep\b/i,
|
|
123
|
+
'expected the wait guidance to name a shell-agnostic sleep',
|
|
124
|
+
);
|
|
125
|
+
assert.doesNotMatch(
|
|
126
|
+
gotchasSource,
|
|
127
|
+
/a single PowerShell\s*`?Start-Sleep`?\s*loop/i,
|
|
128
|
+
'PowerShell Start-Sleep must be an alternative, not the sole mechanism',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function finalizeRepairBranch() {
|
|
133
|
+
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
134
|
+
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
135
|
+
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
136
|
+
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
137
|
+
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
138
|
+
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
139
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
140
|
+
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
144
|
+
assert.doesNotMatch(
|
|
145
|
+
finalizeRepairBranch(),
|
|
146
|
+
/head\s*=\s*repair/,
|
|
147
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function fixBranchAfter(branchLabel) {
|
|
152
|
+
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
153
|
+
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
154
|
+
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
155
|
+
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
156
|
+
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
157
|
+
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
158
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
159
|
+
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
163
|
+
assert.doesNotMatch(
|
|
164
|
+
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
165
|
+
/head\s*=\s*fixProgress/,
|
|
166
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
171
|
+
assert.doesNotMatch(
|
|
172
|
+
fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
|
|
173
|
+
/head\s*=\s*fixProgress/,
|
|
174
|
+
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
179
|
+
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
180
|
+
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
181
|
+
const resolveHeadIndex = convergeSource.indexOf('head = await resolveHead()', convergeBranchStart);
|
|
182
|
+
assert.notEqual(resolveHeadIndex, -1, 'expected CONVERGE to re-resolve HEAD via resolveHead()');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('fix prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
186
|
+
const fixPrompt = lensPromptBody('applyFixes');
|
|
187
|
+
assert.match(fixPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
188
|
+
assert.match(
|
|
189
|
+
fixPrompt,
|
|
190
|
+
/databaseId/,
|
|
191
|
+
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
192
|
+
);
|
|
193
|
+
assert.match(
|
|
194
|
+
fixPrompt,
|
|
195
|
+
/not the numeric comment id/,
|
|
196
|
+
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('fix prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
201
|
+
assert.doesNotMatch(
|
|
202
|
+
lensPromptBody('applyFixes'),
|
|
203
|
+
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
204
|
+
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
205
|
+
);
|
|
206
|
+
});
|