claude-dev-env 1.59.0 → 1.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +4 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +38 -15
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +118 -0
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +213 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +490 -0
- package/hooks/blocking/test_verified_commit_gate.py +495 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +686 -0
- package/hooks/blocking/verified_commit_gate.py +535 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +221 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/orphan-css-class.md +23 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
- package/skills/autoconverge/workflow/converge.mjs +520 -57
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +518 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -1,206 +1,395 @@
|
|
|
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
|
|
18
|
-
const builderEnd =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
assert.notEqual(
|
|
36
|
-
assert.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
assert.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
assert.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert.match(bugbotPrompt, /sleep
|
|
97
|
-
assert.match(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
assert.match(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
assert.match(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 nextBuilderMatch = /\n(?:async )?function /.exec(convergeSource.slice(builderStart + 1));
|
|
18
|
+
const builderEnd =
|
|
19
|
+
nextBuilderMatch === null ? convergeSource.length : builderStart + 1 + nextBuilderMatch.index;
|
|
20
|
+
return convergeSource.slice(builderStart, builderEnd);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
24
|
+
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
28
|
+
assert.doesNotMatch(lensPromptBody('runAuditLens'), /git fetch origin main/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
32
|
+
assert.match(convergeSource, /function prefetchMainForRound\(/);
|
|
33
|
+
const prefetchCallIndex = convergeSource.indexOf('await prefetchMainForRound(');
|
|
34
|
+
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
35
|
+
assert.notEqual(prefetchCallIndex, -1, 'expected prefetchMainForRound to be invoked');
|
|
36
|
+
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
37
|
+
assert.ok(
|
|
38
|
+
prefetchCallIndex < parallelLensIndex,
|
|
39
|
+
'expected the round prefetch to run before the parallel lenses spawn',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
|
|
44
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
45
|
+
assert.doesNotMatch(
|
|
46
|
+
bugbotPrompt,
|
|
47
|
+
/use the existing scripts; pass --owner/,
|
|
48
|
+
'the blanket clause breaks reviews_disabled.py, which accepts only --reviewer',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('bugbot lens invokes reviews_disabled.py with only --reviewer', () => {
|
|
53
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
54
|
+
const reviewsDisabledIndex = bugbotPrompt.indexOf('reviews_disabled.py');
|
|
55
|
+
assert.notEqual(reviewsDisabledIndex, -1, 'expected reviews_disabled.py invocation');
|
|
56
|
+
const invocationLineEnd = bugbotPrompt.indexOf('\\n', reviewsDisabledIndex);
|
|
57
|
+
const invocationLine = bugbotPrompt.slice(reviewsDisabledIndex, invocationLineEnd);
|
|
58
|
+
assert.match(invocationLine, /--reviewer bugbot/);
|
|
59
|
+
assert.doesNotMatch(
|
|
60
|
+
invocationLine,
|
|
61
|
+
/--owner|--repo/,
|
|
62
|
+
'reviews_disabled.py argparse rejects --owner/--repo with SystemExit(2)',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('gotchas doc states parallel lenses must avoid concurrent git operations', () => {
|
|
67
|
+
assert.doesNotMatch(gotchasSource, /cannot race on git state/);
|
|
68
|
+
assert.match(gotchasSource, /fetch.*once.*before/i);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('repair-convergence edit step filters unresolved threads to bot authors and skips human threads', () => {
|
|
72
|
+
const repairPrompt = lensPromptBody('repairConvergenceEdit');
|
|
73
|
+
assert.match(
|
|
74
|
+
repairPrompt,
|
|
75
|
+
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
76
|
+
'expected the bot-author allowlist (Cursor/Claude/Copilot) to be named',
|
|
77
|
+
);
|
|
78
|
+
assert.match(
|
|
79
|
+
repairPrompt,
|
|
80
|
+
/skip.*human|human.*skip/is,
|
|
81
|
+
'expected an explicit instruction to skip human reviewer threads',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('repair-convergence edit step no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
86
|
+
const repairPrompt = lensPromptBody('repairConvergenceEdit');
|
|
87
|
+
assert.doesNotMatch(
|
|
88
|
+
repairPrompt,
|
|
89
|
+
/fetch every thread where isResolved is false/,
|
|
90
|
+
'the unfiltered instruction could resolve human reviewer threads',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('bugbot lens delay instructions are shell-agnostic with PowerShell as an alternative', () => {
|
|
95
|
+
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
96
|
+
assert.match(bugbotPrompt, /sleep 60/, 'expected a shell-agnostic 60-second poll delay');
|
|
97
|
+
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
98
|
+
assert.match(
|
|
99
|
+
bugbotPrompt,
|
|
100
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
101
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
102
|
+
);
|
|
103
|
+
assert.doesNotMatch(
|
|
104
|
+
bugbotPrompt,
|
|
105
|
+
/wait 8 seconds(?!,)/,
|
|
106
|
+
'the vague "wait 8 seconds" phrasing must carry a concrete command',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('copilot gate delay instruction is shell-agnostic with PowerShell as an alternative', () => {
|
|
111
|
+
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
112
|
+
assert.match(copilotPrompt, /sleep 360/, 'expected a shell-agnostic 360-second poll delay');
|
|
113
|
+
assert.match(
|
|
114
|
+
copilotPrompt,
|
|
115
|
+
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
116
|
+
'expected PowerShell to be named only as an allowed alternative',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('gotchas doc describes the reviewer wait as shell-agnostic', () => {
|
|
121
|
+
assert.match(
|
|
122
|
+
gotchasSource,
|
|
123
|
+
/\bsleep\b/i,
|
|
124
|
+
'expected the wait guidance to name a shell-agnostic sleep',
|
|
125
|
+
);
|
|
126
|
+
assert.doesNotMatch(
|
|
127
|
+
gotchasSource,
|
|
128
|
+
/a single PowerShell\s*`?Start-Sleep`?\s*loop/i,
|
|
129
|
+
'PowerShell Start-Sleep must be an alternative, not the sole mechanism',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function finalizeRepairBranch() {
|
|
134
|
+
const repairCallIndex = convergeSource.indexOf('await repairConvergence(');
|
|
135
|
+
assert.notEqual(repairCallIndex, -1, 'expected the FINALIZE repair call to exist');
|
|
136
|
+
const transitionIndex = convergeSource.indexOf("phase = 'CONVERGE'", repairCallIndex);
|
|
137
|
+
assert.notEqual(transitionIndex, -1, 'expected a CONVERGE transition after the repair call');
|
|
138
|
+
const continueIndex = convergeSource.indexOf('continue', transitionIndex);
|
|
139
|
+
assert.notEqual(continueIndex, -1, 'expected a continue statement to close the FINALIZE repair branch');
|
|
140
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
141
|
+
return convergeSource.slice(repairCallIndex, branchEnd);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test('the FINALIZE repair branch does not re-assign head from the repair before re-converging', () => {
|
|
145
|
+
assert.doesNotMatch(
|
|
146
|
+
finalizeRepairBranch(),
|
|
147
|
+
/head\s*=\s*repair/,
|
|
148
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the repair SHA here is dead',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function fixBranchAfter(branchLabel) {
|
|
153
|
+
const labelIndex = convergeSource.indexOf(branchLabel);
|
|
154
|
+
assert.notEqual(labelIndex, -1, `expected the ${branchLabel} marker to exist`);
|
|
155
|
+
const applyFixesIndex = convergeSource.indexOf('await applyFixes(', labelIndex);
|
|
156
|
+
assert.notEqual(applyFixesIndex, -1, `expected an applyFixes call after ${branchLabel}`);
|
|
157
|
+
const continueIndex = convergeSource.indexOf('continue', applyFixesIndex);
|
|
158
|
+
assert.notEqual(continueIndex, -1, `expected a continue statement to close the ${branchLabel} branch`);
|
|
159
|
+
const branchEnd = continueIndex + 'continue'.length;
|
|
160
|
+
return convergeSource.slice(applyFixesIndex, branchEnd);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
test('the CONVERGE fix branch does not re-assign head from the fix before re-converging', () => {
|
|
164
|
+
assert.doesNotMatch(
|
|
165
|
+
fixBranchAfter('${findings.length} finding(s) — applying fixes'),
|
|
166
|
+
/head\s*=\s*fixProgress/,
|
|
167
|
+
'the next CONVERGE pass re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the COPILOT fix branch does not re-assign head from the fix before re-converging', () => {
|
|
172
|
+
assert.doesNotMatch(
|
|
173
|
+
fixBranchAfter('${copilotOutcome.findings.length} finding(s) — fixing and re-converging'),
|
|
174
|
+
/head\s*=\s*fixProgress/,
|
|
175
|
+
'the CONVERGE pass it transitions to re-resolves HEAD from GitHub, so assigning the fix SHA here is dead',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
180
|
+
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
181
|
+
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
182
|
+
const resolveHeadIndex = convergeSource.indexOf('head = await resolveHead()', convergeBranchStart);
|
|
183
|
+
assert.notEqual(resolveHeadIndex, -1, 'expected CONVERGE to re-resolve HEAD via resolveHead()');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('fix edit prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
187
|
+
const editPrompt = lensPromptBody('applyFixesEdit');
|
|
188
|
+
assert.match(editPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
189
|
+
assert.match(
|
|
190
|
+
editPrompt,
|
|
191
|
+
/databaseId/,
|
|
192
|
+
'expected the GraphQL lookup matching comment databaseId to be named',
|
|
193
|
+
);
|
|
194
|
+
assert.match(
|
|
195
|
+
editPrompt,
|
|
196
|
+
/not the numeric comment id/,
|
|
197
|
+
'expected an explicit guard against passing the numeric comment id to resolve_thread',
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('fix edit prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
202
|
+
assert.doesNotMatch(
|
|
203
|
+
lensPromptBody('applyFixesEdit'),
|
|
204
|
+
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
205
|
+
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the fix flow spawns a code-verifier step between the edit step and the commit step', () => {
|
|
210
|
+
const applyFixesBody = lensPromptBody('applyFixes');
|
|
211
|
+
const editIndex = applyFixesBody.indexOf('applyFixesEdit(');
|
|
212
|
+
const verifyIndex = applyFixesBody.indexOf('verifyFixesInWorkingTree(');
|
|
213
|
+
const commitIndex = applyFixesBody.indexOf('commitVerifiedFixes(');
|
|
214
|
+
assert.notEqual(editIndex, -1, 'expected applyFixes to call the edit step');
|
|
215
|
+
assert.notEqual(verifyIndex, -1, 'expected applyFixes to call the verify step');
|
|
216
|
+
assert.notEqual(commitIndex, -1, 'expected applyFixes to call the commit step');
|
|
217
|
+
assert.ok(
|
|
218
|
+
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
219
|
+
'expected the order edit -> verify -> commit so the verifier verdict binds the fixed working tree',
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
function constantBody(constantName) {
|
|
224
|
+
const constantStart = convergeSource.indexOf(`const ${constantName} =`);
|
|
225
|
+
assert.notEqual(constantStart, -1, `expected ${constantName} to exist`);
|
|
226
|
+
const nextConstantStart = convergeSource.indexOf('\nconst ', constantStart + 1);
|
|
227
|
+
const constantEnd = nextConstantStart === -1 ? convergeSource.length : nextConstantStart;
|
|
228
|
+
return convergeSource.slice(constantStart, constantEnd);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test('the shared verdict-fence steps name the binding-hash command and the verdict fence', () => {
|
|
232
|
+
const fenceSteps = constantBody('VERDICT_FENCE_STEPS');
|
|
233
|
+
assert.match(fenceSteps, /--manifest-hash/, 'expected the binding-hash command to be named');
|
|
234
|
+
assert.match(
|
|
235
|
+
fenceSteps,
|
|
236
|
+
/verification_verdict_store\.py/,
|
|
237
|
+
'expected the verdict-store script that computes the binding hash to be named',
|
|
238
|
+
);
|
|
239
|
+
assert.match(fenceSteps, /```verdict/, 'expected the verdict fence to be specified');
|
|
240
|
+
assert.match(fenceSteps, /manifest_sha256/, 'expected the verdict fence to carry manifest_sha256');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('every verify step reuses the shared verdict-fence steps, uses code-verifier, and forbids edits', () => {
|
|
244
|
+
for (const verifyFunctionName of [
|
|
245
|
+
'verifyFixesInWorkingTree',
|
|
246
|
+
'verifyRepairChanges',
|
|
247
|
+
'verifyHardeningChanges',
|
|
248
|
+
]) {
|
|
249
|
+
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
250
|
+
assert.match(
|
|
251
|
+
verifyBody,
|
|
252
|
+
/VERDICT_FENCE_STEPS/,
|
|
253
|
+
`expected ${verifyFunctionName} to reuse the shared VERDICT_FENCE_STEPS`,
|
|
254
|
+
);
|
|
255
|
+
assert.match(
|
|
256
|
+
verifyBody,
|
|
257
|
+
/agentType:\s*'code-verifier'/,
|
|
258
|
+
`expected ${verifyFunctionName} to spawn the code-verifier agent type`,
|
|
259
|
+
);
|
|
260
|
+
assert.doesNotMatch(
|
|
261
|
+
verifyBody,
|
|
262
|
+
/schema:/,
|
|
263
|
+
`expected ${verifyFunctionName} to pass no schema so its verdict fence stays as assistant text`,
|
|
264
|
+
);
|
|
265
|
+
assert.match(
|
|
266
|
+
verifyBody,
|
|
267
|
+
/do no edits|make no edits|not edit|no file edits/i,
|
|
268
|
+
`expected ${verifyFunctionName} to be told to make no edits`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('the commit step is instructed to make no further file edits', () => {
|
|
274
|
+
const commitBody = lensPromptBody('commitVerifiedFixes');
|
|
275
|
+
assert.match(
|
|
276
|
+
commitBody,
|
|
277
|
+
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
278
|
+
'expected the commit step to forbid further edits so the verified surface stays bound',
|
|
279
|
+
);
|
|
280
|
+
assert.match(
|
|
281
|
+
commitBody,
|
|
282
|
+
/agentType:\s*'clean-coder'/,
|
|
283
|
+
'expected the commit step to use clean-coder',
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('the repair flow spawns a code-verifier step between the edit step and the commit step', () => {
|
|
288
|
+
const repairBody = lensPromptBody('repairConvergence');
|
|
289
|
+
const editIndex = repairBody.indexOf('repairConvergenceEdit(');
|
|
290
|
+
const verifyIndex = repairBody.indexOf('verifyRepairChanges(');
|
|
291
|
+
const commitIndex = repairBody.indexOf('commitRepairFixes(');
|
|
292
|
+
assert.notEqual(editIndex, -1, 'expected repairConvergence to call the edit step');
|
|
293
|
+
assert.notEqual(verifyIndex, -1, 'expected repairConvergence to call the verify step');
|
|
294
|
+
assert.notEqual(commitIndex, -1, 'expected repairConvergence to call the commit step');
|
|
295
|
+
assert.ok(
|
|
296
|
+
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
297
|
+
'expected edit -> verify -> commit so the verifier verdict binds the repaired working tree',
|
|
298
|
+
);
|
|
299
|
+
assert.match(
|
|
300
|
+
repairBody,
|
|
301
|
+
/verdictPassed\(/,
|
|
302
|
+
'expected the verify verdict to gate the repair commit step',
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('the standards-deferral flow spawns a code-verifier step between the edit step and the commit step', () => {
|
|
307
|
+
const standardsBody = lensPromptBody('spawnStandardsFollowUp');
|
|
308
|
+
const editIndex = standardsBody.indexOf('standardsFollowUpEdit(');
|
|
309
|
+
const verifyIndex = standardsBody.indexOf('verifyHardeningChanges(');
|
|
310
|
+
const commitIndex = standardsBody.indexOf('commitHardeningPr(');
|
|
311
|
+
assert.notEqual(editIndex, -1, 'expected spawnStandardsFollowUp to call the edit step');
|
|
312
|
+
assert.notEqual(verifyIndex, -1, 'expected spawnStandardsFollowUp to call the verify step');
|
|
313
|
+
assert.notEqual(commitIndex, -1, 'expected spawnStandardsFollowUp to call the commit step');
|
|
314
|
+
assert.ok(
|
|
315
|
+
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
316
|
+
'expected edit -> verify -> commit so the verifier verdict binds the hardening working tree',
|
|
317
|
+
);
|
|
318
|
+
assert.match(
|
|
319
|
+
standardsBody,
|
|
320
|
+
/verdictPassed\(/,
|
|
321
|
+
'expected the verify verdict to gate the hardening commit step',
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('the repair and hardening commit steps forbid further edits and use clean-coder', () => {
|
|
326
|
+
for (const commitFunctionName of ['commitRepairFixes', 'commitHardeningPr']) {
|
|
327
|
+
const commitBody = lensPromptBody(commitFunctionName);
|
|
328
|
+
assert.match(
|
|
329
|
+
commitBody,
|
|
330
|
+
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
331
|
+
`expected ${commitFunctionName} to forbid further edits so the verified surface stays bound`,
|
|
332
|
+
);
|
|
333
|
+
assert.match(
|
|
334
|
+
commitBody,
|
|
335
|
+
/agentType:\s*'clean-coder'/,
|
|
336
|
+
`expected ${commitFunctionName} to use clean-coder`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('the standards-deferral edit step stages the hardening change without committing', () => {
|
|
342
|
+
const editBody = lensPromptBody('standardsFollowUpEdit');
|
|
343
|
+
assert.match(
|
|
344
|
+
editBody,
|
|
345
|
+
/do not commit and do not push|NO commit and NO push|Do NOT commit/i,
|
|
346
|
+
'expected the standards edit step to leave the hardening change uncommitted',
|
|
347
|
+
);
|
|
348
|
+
assert.match(
|
|
349
|
+
editBody,
|
|
350
|
+
/agentType:\s*'clean-coder'/,
|
|
351
|
+
'expected the standards edit step to use clean-coder',
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('spawnStandardsFollowUp reports whether a hardening PR opened on every path', () => {
|
|
356
|
+
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
357
|
+
const falseReturns = body.match(/hardeningPrOpened:\s*false/g) || [];
|
|
358
|
+
assert.ok(
|
|
359
|
+
falseReturns.length >= 2,
|
|
360
|
+
'expected both skip paths (no hardening staged, verify failed) to return hardeningPrOpened:false',
|
|
361
|
+
);
|
|
362
|
+
assert.match(
|
|
363
|
+
body,
|
|
364
|
+
/hardeningPrOpened:\s*true/,
|
|
365
|
+
'expected the commit path to return hardeningPrOpened:true',
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('the standards-deferral note names the hardening PR only when one opened', () => {
|
|
370
|
+
const noteBody = lensPromptBody('standardsDeferralNote');
|
|
371
|
+
assert.match(
|
|
372
|
+
noteBody,
|
|
373
|
+
/environment-hardening PR/,
|
|
374
|
+
'expected the opened-PR branch to name the hardening PR',
|
|
375
|
+
);
|
|
376
|
+
assert.match(
|
|
377
|
+
noteBody,
|
|
378
|
+
/no environment-hardening PR/i,
|
|
379
|
+
'expected the skip branch to state no hardening PR was opened',
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('both standards-deferral call sites build standardsNote from the spawnStandardsFollowUp outcome', () => {
|
|
384
|
+
const callSiteUses = convergeSource.match(/standardsNote = standardsDeferralNote\(/g) || [];
|
|
385
|
+
assert.equal(
|
|
386
|
+
callSiteUses.length,
|
|
387
|
+
2,
|
|
388
|
+
'expected both standards-deferral call sites to build standardsNote via standardsDeferralNote(...)',
|
|
389
|
+
);
|
|
390
|
+
assert.doesNotMatch(
|
|
391
|
+
convergeSource,
|
|
392
|
+
/standardsNote = `\$\{[^}]+\} code-standard finding\(s\) deferred to a follow-up fix issue plus an environment-hardening PR/,
|
|
393
|
+
'expected no unconditional hardening-PR claim in standardsNote',
|
|
394
|
+
);
|
|
395
|
+
});
|