claude-dev-env 1.74.0 → 1.75.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/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/hedging_language_blocker.py +1 -13
- package/hooks/blocking/intent_only_ending_blocker.py +1 -15
- package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
- package/hooks/blocking/question_to_user_enforcer.py +1 -11
- package/hooks/blocking/session_handoff_blocker.py +1 -15
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_mypy_validator.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/autoconverge/SKILL.md +6 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +308 -132
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +36 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +598 -606
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
|
@@ -40,8 +40,9 @@ test('cleanAuditBlocker falls back to a no-result reason when the post agent die
|
|
|
40
40
|
assert.match(message, /the post agent returned no result/);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
test('
|
|
44
|
-
const body = functionBody('
|
|
43
|
+
test('the post-clean-audit task in resumeGeneralUtilityAgent returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
|
|
44
|
+
const body = functionBody('resumeGeneralUtilityAgent');
|
|
45
|
+
assert.match(body, /task === 'post-clean-audit'/);
|
|
45
46
|
assert.match(body, /schema: CLEAN_AUDIT_SCHEMA/);
|
|
46
47
|
assert.doesNotMatch(body, /agent transcript \(unused\)/);
|
|
47
48
|
});
|
|
@@ -58,7 +59,7 @@ test('the standards-only call site breaks with a clean-audit blocker when the po
|
|
|
58
59
|
convergeSource.indexOf('if (isStandardsOnlyRound(findings)) {'),
|
|
59
60
|
convergeSource.indexOf('if (findings.length > 0) {'),
|
|
60
61
|
);
|
|
61
|
-
assert.match(branch, /
|
|
62
|
+
assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
|
|
62
63
|
assert.match(branch, /if \(!auditResult\?\.posted\)/);
|
|
63
64
|
assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
|
|
64
65
|
assert.match(branch, /\bbreak\b/);
|
|
@@ -69,7 +70,7 @@ test('the all-clean call site breaks with a clean-audit blocker when the post do
|
|
|
69
70
|
convergeSource.indexOf('all lenses clean on'),
|
|
70
71
|
convergeSource.indexOf("if (phase === 'COPILOT') {"),
|
|
71
72
|
);
|
|
72
|
-
assert.match(branch, /
|
|
73
|
+
assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
|
|
73
74
|
assert.match(branch, /if \(!auditResult\?\.posted\)/);
|
|
74
75
|
assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
|
|
75
76
|
assert.match(branch, /\bbreak\b/);
|
|
@@ -16,14 +16,26 @@ const skillSource = readFileSync(
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
function lensPromptBody(builderName) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let builderStart = convergeSource.indexOf(`function ${builderName}(`);
|
|
20
|
+
if (builderStart === -1) {
|
|
21
|
+
builderStart = convergeSource.indexOf(`const ${builderName} =`);
|
|
22
|
+
assert.notEqual(builderStart, -1, `expected ${builderName} to exist as a function or const`);
|
|
23
|
+
}
|
|
21
24
|
const nextBuilderMatch = /\n(?:async )?function /.exec(convergeSource.slice(builderStart + 1));
|
|
22
25
|
const builderEnd =
|
|
23
26
|
nextBuilderMatch === null ? convergeSource.length : builderStart + 1 + nextBuilderMatch.index;
|
|
24
27
|
return convergeSource.slice(builderStart, builderEnd);
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function functionSource(functionName) {
|
|
31
|
+
const functionStart = convergeSource.indexOf(`function ${functionName}(`);
|
|
32
|
+
assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
|
|
33
|
+
const nextMatch = /\n(?:async )?function /.exec(convergeSource.slice(functionStart + 1));
|
|
34
|
+
const functionEnd =
|
|
35
|
+
nextMatch === null ? convergeSource.length : functionStart + 1 + nextMatch.index;
|
|
36
|
+
return convergeSource.slice(functionStart, functionEnd);
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
test('code-review lens prompt no longer instructs a per-lens git fetch', () => {
|
|
28
40
|
assert.doesNotMatch(lensPromptBody('runCodeReviewLens'), /git fetch origin main/);
|
|
29
41
|
});
|
|
@@ -33,10 +45,10 @@ test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
|
|
|
33
45
|
});
|
|
34
46
|
|
|
35
47
|
test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
|
|
36
|
-
assert.
|
|
37
|
-
const prefetchCallIndex = convergeSource.indexOf(
|
|
48
|
+
assert.ok(convergeSource.includes("resumeGitAgent(gitAgentId, 'prefetch-main')"));
|
|
49
|
+
const prefetchCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'prefetch-main')");
|
|
38
50
|
const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
|
|
39
|
-
assert.notEqual(prefetchCallIndex, -1, 'expected
|
|
51
|
+
assert.notEqual(prefetchCallIndex, -1, 'expected prefetch to be invoked');
|
|
40
52
|
assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
|
|
41
53
|
assert.ok(
|
|
42
54
|
prefetchCallIndex < parallelLensIndex,
|
|
@@ -73,7 +85,7 @@ test('gotchas doc states parallel lenses must avoid concurrent git operations',
|
|
|
73
85
|
});
|
|
74
86
|
|
|
75
87
|
test('repair-convergence edit step filters unresolved threads to bot authors and skips human threads', () => {
|
|
76
|
-
const repairPrompt =
|
|
88
|
+
const repairPrompt = functionSource('resumeCodeEditorAgent');
|
|
77
89
|
assert.match(
|
|
78
90
|
repairPrompt,
|
|
79
91
|
/cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
|
|
@@ -87,7 +99,7 @@ test('repair-convergence edit step filters unresolved threads to bot authors and
|
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
test('repair-convergence edit step no longer instructs resolving every unresolved thread without an author filter', () => {
|
|
90
|
-
const repairPrompt =
|
|
102
|
+
const repairPrompt = functionSource('resumeCodeEditorAgent');
|
|
91
103
|
assert.doesNotMatch(
|
|
92
104
|
repairPrompt,
|
|
93
105
|
/fetch every thread where isResolved is false/,
|
|
@@ -183,12 +195,12 @@ test('the COPILOT fix branch does not re-assign head from the fix before re-conv
|
|
|
183
195
|
test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
|
|
184
196
|
const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
|
|
185
197
|
assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
|
|
186
|
-
const
|
|
187
|
-
assert.notEqual(
|
|
198
|
+
const headResolveCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'resolve-head')", convergeBranchStart);
|
|
199
|
+
assert.notEqual(headResolveCallIndex, -1, 'expected CONVERGE to re-resolve HEAD via resumeGitAgent');
|
|
188
200
|
});
|
|
189
201
|
|
|
190
202
|
test('fix edit prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
|
|
191
|
-
const editPrompt =
|
|
203
|
+
const editPrompt = functionSource('resumeCodeEditorAgent');
|
|
192
204
|
assert.match(editPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
|
|
193
205
|
assert.match(
|
|
194
206
|
editPrompt,
|
|
@@ -204,24 +216,16 @@ test('fix edit prompt resolves threads by PRRT thread node id looked up from the
|
|
|
204
216
|
|
|
205
217
|
test('fix edit prompt does not pass the numeric comment id straight to resolve_thread', () => {
|
|
206
218
|
assert.doesNotMatch(
|
|
207
|
-
|
|
219
|
+
functionSource('resumeCodeEditorAgent'),
|
|
208
220
|
/then resolve that thread \(use the github MCP pull_request_review_write/,
|
|
209
221
|
'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
|
|
210
222
|
);
|
|
211
223
|
});
|
|
212
224
|
|
|
213
|
-
test('the fix flow spawns a
|
|
225
|
+
test('the fix flow spawns a fixer agent and runs fixerWithRecovery after the edit step', () => {
|
|
214
226
|
const applyFixesBody = lensPromptBody('applyFixes');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const commitIndex = applyFixesBody.indexOf('commitVerifiedFixes(');
|
|
218
|
-
assert.notEqual(editIndex, -1, 'expected applyFixes to call the edit step');
|
|
219
|
-
assert.notEqual(verifyIndex, -1, 'expected applyFixes to call the verify step');
|
|
220
|
-
assert.notEqual(commitIndex, -1, 'expected applyFixes to call the commit step');
|
|
221
|
-
assert.ok(
|
|
222
|
-
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
223
|
-
'expected the order edit -> verify -> commit so the verifier verdict binds the fixed working tree',
|
|
224
|
-
);
|
|
227
|
+
assert.match(applyFixesBody, /spawnFixerAgent\(/, 'expected applyFixes to call spawnFixerAgent');
|
|
228
|
+
assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
|
|
225
229
|
});
|
|
226
230
|
|
|
227
231
|
test('the shared verdict-fence builder names the binding-hash command and the verdict fence', () => {
|
|
@@ -265,21 +269,13 @@ test('the verdict-fence binding does not self-resolve a cwd via git rev-parse fo
|
|
|
265
269
|
});
|
|
266
270
|
|
|
267
271
|
test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
|
|
268
|
-
for (const verifyFunctionName of [
|
|
269
|
-
'verifyFixesInWorkingTree',
|
|
270
|
-
'verifyRepairChanges',
|
|
271
|
-
]) {
|
|
272
|
+
for (const verifyFunctionName of ['resumeVerifierAgent']) {
|
|
272
273
|
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
273
274
|
assert.match(
|
|
274
275
|
verifyBody,
|
|
275
276
|
/buildVerdictFenceSteps\(/,
|
|
276
277
|
`expected ${verifyFunctionName} to call buildVerdictFenceSteps (cwd-immune branch binding)`,
|
|
277
278
|
);
|
|
278
|
-
assert.doesNotMatch(
|
|
279
|
-
verifyBody,
|
|
280
|
-
/VERDICT_FENCE_STEPS(?!\s*\))/,
|
|
281
|
-
`expected ${verifyFunctionName} not to reference the removed VERDICT_FENCE_STEPS constant`,
|
|
282
|
-
);
|
|
283
279
|
assert.match(
|
|
284
280
|
verifyBody,
|
|
285
281
|
/agentType:\s*'code-verifier'/,
|
|
@@ -298,125 +294,91 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
|
|
|
298
294
|
}
|
|
299
295
|
});
|
|
300
296
|
|
|
301
|
-
test('
|
|
302
|
-
const
|
|
297
|
+
test('resumeFixerAgent verify-commit path uses code-verifier and calls buildVerdictFenceSteps', () => {
|
|
298
|
+
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
299
|
+
assert.match(fixerBody, /buildVerdictFenceSteps\(/, 'expected resumeFixerAgent to call buildVerdictFenceSteps');
|
|
300
|
+
assert.match(fixerBody, /agentType:\s*'code-verifier'/, 'expected resumeFixerAgent to use code-verifier');
|
|
301
|
+
assert.match(fixerBody, /do no edits|make no edits|not edit|no file edits/i, 'expected resumeFixerAgent to forbid edits');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
|
|
305
|
+
const verifyBody = lensPromptBody('resumeVerifierAgent');
|
|
303
306
|
assert.match(
|
|
304
307
|
verifyBody,
|
|
305
308
|
/--manifest-hash-for-branch/,
|
|
306
|
-
'expected
|
|
307
|
-
);
|
|
308
|
-
assert.doesNotMatch(
|
|
309
|
-
verifyBody,
|
|
310
|
-
/--manifest-hash(?!-for-branch)/,
|
|
311
|
-
'expected verifyHardeningChanges not to use the old --manifest-hash <REPO> form',
|
|
312
|
-
);
|
|
313
|
-
assert.match(
|
|
314
|
-
verifyBody,
|
|
315
|
-
/agentType:\s*'code-verifier'/,
|
|
316
|
-
'expected verifyHardeningChanges to spawn the code-verifier agent type',
|
|
317
|
-
);
|
|
318
|
-
assert.doesNotMatch(
|
|
319
|
-
verifyBody,
|
|
320
|
-
/schema:/,
|
|
321
|
-
'expected verifyHardeningChanges to pass no schema so its verdict fence stays as assistant text',
|
|
309
|
+
'expected the verifier to bind by hardening branch (cwd-immune)',
|
|
322
310
|
);
|
|
323
311
|
assert.match(
|
|
324
312
|
verifyBody,
|
|
325
313
|
/do no edits|make no edits|not edit|no file edits/i,
|
|
326
|
-
'expected
|
|
314
|
+
'expected the verifier to be told to make no edits',
|
|
327
315
|
);
|
|
328
316
|
});
|
|
329
317
|
|
|
330
|
-
test('
|
|
331
|
-
for (const verifyFunctionName of ['
|
|
318
|
+
test('resumeVerifierAgent and resumeFixerAgent pass PR coordinates to buildVerdictFenceSteps', () => {
|
|
319
|
+
for (const verifyFunctionName of ['resumeFixerAgent', 'resumeVerifierAgent']) {
|
|
332
320
|
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
333
321
|
assert.match(
|
|
334
322
|
verifyBody,
|
|
335
|
-
/buildVerdictFenceSteps\(
|
|
336
|
-
`expected ${verifyFunctionName} to
|
|
323
|
+
/buildVerdictFenceSteps\(/,
|
|
324
|
+
`expected ${verifyFunctionName} to call buildVerdictFenceSteps`,
|
|
337
325
|
);
|
|
338
326
|
}
|
|
339
327
|
});
|
|
340
328
|
|
|
341
|
-
test('the commit
|
|
342
|
-
const
|
|
329
|
+
test('the commit path in resumeFixerAgent forbids further edits and uses clean-coder', () => {
|
|
330
|
+
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
343
331
|
assert.match(
|
|
344
|
-
|
|
332
|
+
fixerBody,
|
|
345
333
|
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
346
|
-
'expected the commit
|
|
334
|
+
'expected the commit path to forbid further edits',
|
|
347
335
|
);
|
|
348
336
|
assert.match(
|
|
349
|
-
|
|
337
|
+
fixerBody,
|
|
350
338
|
/agentType:\s*'clean-coder'/,
|
|
351
|
-
'expected the commit
|
|
339
|
+
'expected the commit path to use clean-coder',
|
|
352
340
|
);
|
|
353
341
|
});
|
|
354
342
|
|
|
355
|
-
test('the repair flow
|
|
343
|
+
test('the repair flow uses resume helpers for edit, verify, and commit', () => {
|
|
356
344
|
const repairBody = lensPromptBody('repairConvergence');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
assert.notEqual(editIndex, -1, 'expected repairConvergence to call the edit step');
|
|
361
|
-
assert.notEqual(verifyIndex, -1, 'expected repairConvergence to call the verify step');
|
|
362
|
-
assert.notEqual(commitIndex, -1, 'expected repairConvergence to call the commit step');
|
|
363
|
-
assert.ok(
|
|
364
|
-
editIndex < verifyIndex && verifyIndex < commitIndex,
|
|
365
|
-
'expected edit -> verify -> commit so the verifier verdict binds the repaired working tree',
|
|
366
|
-
);
|
|
367
|
-
assert.match(
|
|
368
|
-
repairBody,
|
|
369
|
-
/verdictPassed\(/,
|
|
370
|
-
'expected the verify verdict to gate the repair commit step',
|
|
371
|
-
);
|
|
345
|
+
assert.match(repairBody, /spawnCodeEditorAgent\(/, 'expected repairConvergence to spawn a code-editor');
|
|
346
|
+
assert.match(repairBody, /spawnVerifierAgent\(/, 'expected repairConvergence to spawn a verifier');
|
|
347
|
+
assert.match(repairBody, /verdictPassed\(/, 'expected the verify verdict to gate the repair commit step');
|
|
372
348
|
});
|
|
373
349
|
|
|
374
|
-
test('the standards-deferral flow
|
|
350
|
+
test('the standards-deferral flow uses resume helpers for edit, verify, and commit', () => {
|
|
375
351
|
const standardsBody = lensPromptBody('spawnStandardsFollowUp');
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
352
|
+
assert.match(standardsBody, /spawnCodeEditorAgent\(/, 'expected spawnStandardsFollowUp to spawn a code-editor');
|
|
353
|
+
assert.match(standardsBody, /spawnVerifierAgent\(/, 'expected spawnStandardsFollowUp to spawn a verifier');
|
|
354
|
+
assert.match(standardsBody, /verdictPassed\(/, 'expected the verify verdict to gate the hardening commit step');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('repair-commit and hardening-commit paths use clean-coder and forbid edits', () => {
|
|
358
|
+
const codeEditorBody = lensPromptBody('resumeCodeEditorAgent');
|
|
359
|
+
assert.match(
|
|
360
|
+
codeEditorBody,
|
|
361
|
+
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
362
|
+
'expected the commit paths to forbid further edits',
|
|
385
363
|
);
|
|
386
364
|
assert.match(
|
|
387
|
-
|
|
388
|
-
/
|
|
389
|
-
'expected the
|
|
365
|
+
codeEditorBody,
|
|
366
|
+
/agentType:\s*'clean-coder'/,
|
|
367
|
+
'expected the commit paths to use clean-coder',
|
|
390
368
|
);
|
|
391
369
|
});
|
|
392
370
|
|
|
393
|
-
test('the
|
|
394
|
-
|
|
395
|
-
const commitBody = lensPromptBody(commitFunctionName);
|
|
396
|
-
assert.match(
|
|
397
|
-
commitBody,
|
|
398
|
-
/no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
|
|
399
|
-
`expected ${commitFunctionName} to forbid further edits so the verified surface stays bound`,
|
|
400
|
-
);
|
|
401
|
-
assert.match(
|
|
402
|
-
commitBody,
|
|
403
|
-
/agentType:\s*'clean-coder'/,
|
|
404
|
-
`expected ${commitFunctionName} to use clean-coder`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
test('the standards-deferral edit step stages the hardening change without committing', () => {
|
|
410
|
-
const editBody = lensPromptBody('standardsFollowUpEdit');
|
|
371
|
+
test('the code-editor standards-edit path stages hardening without committing and uses clean-coder', () => {
|
|
372
|
+
const editBody = lensPromptBody('resumeCodeEditorAgent');
|
|
411
373
|
assert.match(
|
|
412
374
|
editBody,
|
|
413
375
|
/do not commit and do not push|NO commit and NO push|Do NOT commit/i,
|
|
414
|
-
'expected the standards edit
|
|
376
|
+
'expected the standards edit path to leave the hardening change uncommitted',
|
|
415
377
|
);
|
|
416
378
|
assert.match(
|
|
417
379
|
editBody,
|
|
418
380
|
/agentType:\s*'clean-coder'/,
|
|
419
|
-
'expected the
|
|
381
|
+
'expected the edit paths to use clean-coder',
|
|
420
382
|
);
|
|
421
383
|
});
|
|
422
384
|
|
|
@@ -534,20 +496,31 @@ test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES
|
|
|
534
496
|
);
|
|
535
497
|
});
|
|
536
498
|
|
|
537
|
-
const
|
|
538
|
-
'applyFixesEdit',
|
|
539
|
-
'recoverCommitBlockEdit',
|
|
540
|
-
'recoverVerifyFailEdit',
|
|
541
|
-
'repairConvergenceEdit',
|
|
542
|
-
'standardsFollowUpEdit',
|
|
543
|
-
];
|
|
499
|
+
const editStepResumeHelpers = ['resumeCodeEditorAgent', 'resumeFixerAgent'];
|
|
544
500
|
|
|
545
|
-
for (const
|
|
546
|
-
test(`${
|
|
501
|
+
for (const helperName of editStepResumeHelpers) {
|
|
502
|
+
test(`${helperName} appends the pre-commit gate step to its edit prompts`, () => {
|
|
547
503
|
assert.match(
|
|
548
|
-
|
|
504
|
+
functionSource(helperName),
|
|
549
505
|
/\+\s*PRE_COMMIT_GATE_STEP/,
|
|
550
|
-
`expected ${
|
|
506
|
+
`expected ${helperName} to append PRE_COMMIT_GATE_STEP to its edit-task prompts`,
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const editStepResumeTasks = [
|
|
512
|
+
['resumeCodeEditorAgent', 'fix-edit'],
|
|
513
|
+
['resumeCodeEditorAgent', 'repair-edit'],
|
|
514
|
+
['resumeCodeEditorAgent', 'standards-edit'],
|
|
515
|
+
['resumeCodeEditorAgent', 'commit-recover'],
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
for (const [helperName, taskName] of editStepResumeTasks) {
|
|
519
|
+
test(`${helperName} routes the ${taskName} task to a pre-commit-gated edit prompt`, () => {
|
|
520
|
+
assert.match(
|
|
521
|
+
functionSource(helperName),
|
|
522
|
+
new RegExp(`task === '${taskName}'`),
|
|
523
|
+
`expected ${helperName} to handle the ${taskName} task`,
|
|
551
524
|
);
|
|
552
525
|
});
|
|
553
526
|
}
|
|
@@ -567,11 +540,15 @@ test('preamble prescribes authoring a Python helper for variable-built or multi-
|
|
|
567
540
|
);
|
|
568
541
|
});
|
|
569
542
|
|
|
570
|
-
test('preamble does not claim
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
543
|
+
test('preamble does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
544
|
+
const text = preambleText().replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
545
|
+
const overstatesStandalone =
|
|
546
|
+
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
547
|
+
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
548
|
+
assert.equal(
|
|
549
|
+
overstatesStandalone,
|
|
550
|
+
false,
|
|
551
|
+
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
575
552
|
);
|
|
576
553
|
});
|
|
577
554
|
|
|
@@ -591,11 +568,15 @@ test('preamble scopes its rm-shape claim to the narrowest auto-allow path, not t
|
|
|
591
568
|
);
|
|
592
569
|
});
|
|
593
570
|
|
|
594
|
-
test('SKILL.md does not claim
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
571
|
+
test('SKILL.md does not claim the standalone or both rm auto-allow paths fail closed on any $', () => {
|
|
572
|
+
const text = skillSource.replace(/`/g, '').replace(/\$\(\.\.\.\)/g, 'SUBSHELL').replace(/\s+/g, ' ');
|
|
573
|
+
const overstatesStandalone =
|
|
574
|
+
/\b(?:both|standalone|neither)\b[^.]*fail closed[^.]*any \$/i.test(text) ||
|
|
575
|
+
/\b(?:both|standalone|neither)\b[^.]*any \$[^.]*fail closed/i.test(text);
|
|
576
|
+
assert.equal(
|
|
577
|
+
overstatesStandalone,
|
|
578
|
+
false,
|
|
579
|
+
'only the compound path fails closed on any $ in the target; the standalone path accepts a $-bearing target whose literal path already sits under an ephemeral root',
|
|
599
580
|
);
|
|
600
581
|
});
|
|
601
582
|
|
|
@@ -611,7 +592,7 @@ test('preamble does not attribute the known-temp-var resolution to the standalon
|
|
|
611
592
|
assert.doesNotMatch(
|
|
612
593
|
preambleText().replace(/\s+/g, ' '),
|
|
613
594
|
/Across these paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
614
|
-
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths
|
|
595
|
+
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
615
596
|
);
|
|
616
597
|
});
|
|
617
598
|
|
|
@@ -635,7 +616,7 @@ test('SKILL.md does not attribute the known-temp-var resolution to the standalon
|
|
|
635
616
|
assert.doesNotMatch(
|
|
636
617
|
skillSource.replace(/\s+/g, ' '),
|
|
637
618
|
/Across those paths[\s\S]*?CLAUDE_JOB_DIR/i,
|
|
638
|
-
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths
|
|
619
|
+
'the temp-var resolution lives only in the broad cwd-scoped path; the standalone and compound paths do not resolve known temp variables',
|
|
639
620
|
);
|
|
640
621
|
});
|
|
641
622
|
|
|
@@ -653,3 +634,198 @@ test('SKILL.md attributes the known-temp-var resolution to the cwd-scoped auto-a
|
|
|
653
634
|
'expected the temp-var resolution to be tied to the cwd-scoped path that declares an ephemeral working directory, not the standalone or compound paths',
|
|
654
635
|
);
|
|
655
636
|
});
|
|
637
|
+
|
|
638
|
+
test('convergeAgent prepends HEADLESS_SAFETY_PREAMBLE and worktree directive on fresh spawn', () => {
|
|
639
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
640
|
+
assert.match(
|
|
641
|
+
convergeAgentBody,
|
|
642
|
+
/HEADLESS_SAFETY_PREAMBLE.*worktreeDirective/,
|
|
643
|
+
'expected fresh-spawn path to prepend both preamble and worktree directive',
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test('convergeAgent checks isResume before prepending the preamble', () => {
|
|
648
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
649
|
+
assert.match(convergeAgentBody, /isResume/, 'expected an isResume guard in convergeAgent');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const newSpawnResumeHelpers = [
|
|
653
|
+
{ name: 'spawnGitAgent', isAsync: true },
|
|
654
|
+
{ name: 'resumeGitAgent', isAsync: false },
|
|
655
|
+
{ name: 'spawnFixerAgent', isAsync: true },
|
|
656
|
+
{ name: 'resumeFixerAgent', isAsync: false },
|
|
657
|
+
{ name: 'fixerWithRecovery', isAsync: true },
|
|
658
|
+
{ name: 'spawnCodeEditorAgent', isAsync: true },
|
|
659
|
+
{ name: 'resumeCodeEditorAgent', isAsync: false },
|
|
660
|
+
{ name: 'spawnVerifierAgent', isAsync: true },
|
|
661
|
+
{ name: 'resumeVerifierAgent', isAsync: false },
|
|
662
|
+
{ name: 'spawnGeneralUtilityAgent', isAsync: true },
|
|
663
|
+
{ name: 'resumeGeneralUtilityAgent', isAsync: false },
|
|
664
|
+
{ name: 'spawnConvergenceCheckAgent', isAsync: true },
|
|
665
|
+
{ name: 'resumeConvergenceCheckAgent', isAsync: false },
|
|
666
|
+
{ name: 'extractVerdict', isAsync: false },
|
|
667
|
+
];
|
|
668
|
+
|
|
669
|
+
for (const { name, isAsync } of newSpawnResumeHelpers) {
|
|
670
|
+
const prefix = isAsync ? 'async ' : '';
|
|
671
|
+
test(`function ${prefix}${name} exists in converge.mjs`, () => {
|
|
672
|
+
const needle = isAsync ? `async function ${name}(` : `function ${name}(`;
|
|
673
|
+
assert.ok(convergeSource.includes(needle), `expected ${name} to exist`);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const spawnHelperNames = [
|
|
678
|
+
'spawnGitAgent',
|
|
679
|
+
'spawnFixerAgent',
|
|
680
|
+
'spawnCodeEditorAgent',
|
|
681
|
+
'spawnVerifierAgent',
|
|
682
|
+
'spawnGeneralUtilityAgent',
|
|
683
|
+
'spawnConvergenceCheckAgent',
|
|
684
|
+
];
|
|
685
|
+
|
|
686
|
+
for (const spawnName of spawnHelperNames) {
|
|
687
|
+
test(`${spawnName} actually spawns an agent via convergeAgent`, () => {
|
|
688
|
+
const spawnBody = functionSource(spawnName);
|
|
689
|
+
assert.match(
|
|
690
|
+
spawnBody,
|
|
691
|
+
/await convergeAgent\(/,
|
|
692
|
+
`expected ${spawnName} to spawn a real agent through convergeAgent rather than return a hardcoded label`,
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test(`${spawnName} returns the spawned agent's runtime id`, () => {
|
|
697
|
+
const spawnBody = functionSource(spawnName);
|
|
698
|
+
assert.match(
|
|
699
|
+
spawnBody,
|
|
700
|
+
/return\s+result\?\.agentId/,
|
|
701
|
+
`expected ${spawnName} to return result?.agentId so resume targets the real session`,
|
|
702
|
+
);
|
|
703
|
+
assert.doesNotMatch(
|
|
704
|
+
spawnBody,
|
|
705
|
+
/return\s+['"`]/,
|
|
706
|
+
`expected ${spawnName} not to return a hardcoded label string`,
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
test('spawnGitAgent returns result?.agentId without a tautological ternary', () => {
|
|
712
|
+
const spawnBody = functionSource('spawnGitAgent');
|
|
713
|
+
assert.doesNotMatch(
|
|
714
|
+
spawnBody,
|
|
715
|
+
/\?\s*'git-utility'\s*:\s*'git-utility'/,
|
|
716
|
+
'expected the identical-branch ternary that discards the spawn outcome to be gone',
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('resume helpers fall back to a fresh spawn when no agentId is available', () => {
|
|
721
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
722
|
+
assert.match(
|
|
723
|
+
convergeAgentBody,
|
|
724
|
+
/options\?\.resume.*length\s*>\s*0|length\s*>\s*0.*resume/s,
|
|
725
|
+
'expected convergeAgent to treat a missing agentId as a fresh spawn (isResume false), restoring the preamble and worktree directive',
|
|
726
|
+
);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('resumeGeneralUtilityAgent only handles the two tasks it is called with', () => {
|
|
730
|
+
const generalBody = functionSource('resumeGeneralUtilityAgent');
|
|
731
|
+
assert.doesNotMatch(
|
|
732
|
+
generalBody,
|
|
733
|
+
/task === 'bugbot-lens'/,
|
|
734
|
+
'the live Bugbot lens is runBugbotLens; the dead bugbot-lens branch must be removed',
|
|
735
|
+
);
|
|
736
|
+
assert.doesNotMatch(
|
|
737
|
+
generalBody,
|
|
738
|
+
/Copilot can run out of usage/,
|
|
739
|
+
'the live Copilot gate is runCopilotGate; the dead copilot-gate branch must be removed',
|
|
740
|
+
);
|
|
741
|
+
assert.doesNotMatch(
|
|
742
|
+
generalBody,
|
|
743
|
+
/convergence summary/,
|
|
744
|
+
'the convergence-summary producer was removed; the dead branch must not return',
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const orphanedHelperNames = [
|
|
749
|
+
'applyFixesEdit',
|
|
750
|
+
'recoverCommitBlockEdit',
|
|
751
|
+
'recoverVerifyFailEdit',
|
|
752
|
+
'checkConvergence',
|
|
753
|
+
'markReady',
|
|
754
|
+
'repairConvergenceEdit',
|
|
755
|
+
'verifyRepairChanges',
|
|
756
|
+
'commitRepairFixes',
|
|
757
|
+
'resolveConflictsEdit',
|
|
758
|
+
'standardsFollowUpEdit',
|
|
759
|
+
'verifyHardeningChanges',
|
|
760
|
+
'commitHardeningPr',
|
|
761
|
+
'postCleanAudit',
|
|
762
|
+
'spawnConvergenceSummary',
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
for (const orphanName of orphanedHelperNames) {
|
|
766
|
+
test(`${orphanName} is removed — its behavior lives in a resume helper`, () => {
|
|
767
|
+
assert.ok(
|
|
768
|
+
!convergeSource.includes(`function ${orphanName}(`),
|
|
769
|
+
`expected the orphaned ${orphanName} definition to be deleted (CODE_RULES 9.8)`,
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
test('convergeAgent omits the preamble when options.resume is a non-empty string', () => {
|
|
775
|
+
const convergeAgentBody = lensPromptBody('convergeAgent');
|
|
776
|
+
assert.match(
|
|
777
|
+
convergeAgentBody,
|
|
778
|
+
/isResume\s*\?\s*prompt/,
|
|
779
|
+
'expected the resume path to pass prompt through without the preamble',
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
|
|
784
|
+
const parseModule = new Function(
|
|
785
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
786
|
+
'return { parseLastVerdictFence };',
|
|
787
|
+
)();
|
|
788
|
+
const result = parseModule.parseLastVerdictFence('```verdict\n{"all_pass":true,"findings":[],"manifest_sha256":"abc"}\n```');
|
|
789
|
+
assert.notEqual(result, null);
|
|
790
|
+
assert.equal(result.all_pass, true);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test('parseLastVerdictFence returns null for non-string input', () => {
|
|
794
|
+
const parseModule = new Function(
|
|
795
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
796
|
+
'return { parseLastVerdictFence };',
|
|
797
|
+
)();
|
|
798
|
+
assert.equal(parseModule.parseLastVerdictFence(null), null);
|
|
799
|
+
assert.equal(parseModule.parseLastVerdictFence(undefined), null);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test('parseLastVerdictFence returns null when no verdict fence is present', () => {
|
|
803
|
+
const parseModule = new Function(
|
|
804
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
805
|
+
'return { parseLastVerdictFence };',
|
|
806
|
+
)();
|
|
807
|
+
assert.equal(parseModule.parseLastVerdictFence('plain text with no fence'), null);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
|
|
811
|
+
const parseModule = new Function(
|
|
812
|
+
`${functionSource('parseLastVerdictFence')}\n` +
|
|
813
|
+
'return { parseLastVerdictFence };',
|
|
814
|
+
)();
|
|
815
|
+
assert.equal(parseModule.parseLastVerdictFence('```verdict\nnot json\n```'), null);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test('verdictPassed calls parseLastVerdictFence', () => {
|
|
819
|
+
const verdictBody = lensPromptBody('verdictPassed');
|
|
820
|
+
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected verdictPassed to call the shared parser');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
824
|
+
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
825
|
+
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test('extractVerdict calls parseLastVerdictFence', () => {
|
|
829
|
+
const verdictBody = lensPromptBody('extractVerdict');
|
|
830
|
+
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected extractVerdict to call the shared parser');
|
|
831
|
+
});
|