claude-dev-env 1.80.0 → 1.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
- package/audit-rubrics/CLAUDE.md +3 -2
- package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
- package/audit-rubrics/prompts/CLAUDE.md +2 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/code_rules_annotations_length.py +59 -11
- package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
- package/hooks/blocking/code_rules_command_dispatch.py +140 -0
- package/hooks/blocking/code_rules_docstrings.py +93 -0
- package/hooks/blocking/code_rules_enforcer.py +54 -4
- package/hooks/blocking/code_rules_js_conventions.py +246 -0
- package/hooks/blocking/code_rules_naming_collection.py +5 -0
- package/hooks/blocking/code_rules_test_assertions.py +3 -0
- package/hooks/blocking/code_rules_unused_imports.py +0 -3
- package/hooks/blocking/conventional_pr_title_gate.py +444 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +16 -3
- package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
- package/hooks/blocking/test_conventional_pr_title_gate.py +640 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -0
- package/hooks/hooks_constants/blocking_check_limits.py +9 -0
- package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +58 -0
- package/hooks/hooks_constants/js_conventions_constants.py +54 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +2 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
- package/skills/autoconverge/SKILL.md +92 -7
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +161 -29
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +342 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +178 -32
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
|
@@ -107,42 +107,28 @@ test('repair-convergence edit step no longer instructs resolving every unresolve
|
|
|
107
107
|
);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test('bugbot lens
|
|
110
|
+
test('the bugbot lens waits through the Monitor tool, not a foreground sleep', () => {
|
|
111
111
|
const bugbotPrompt = lensPromptBody('runBugbotLens');
|
|
112
|
-
assert.match(bugbotPrompt, /
|
|
113
|
-
assert.match(bugbotPrompt, /sleep 8/, 'expected a concrete 8-second delay command');
|
|
114
|
-
assert.match(
|
|
115
|
-
bugbotPrompt,
|
|
116
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
117
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
118
|
-
);
|
|
112
|
+
assert.match(bugbotPrompt, /Monitor tool/, 'expected the bugbot poll to wait via the Monitor tool');
|
|
119
113
|
assert.doesNotMatch(
|
|
120
114
|
bugbotPrompt,
|
|
121
|
-
/
|
|
122
|
-
'
|
|
115
|
+
/sleep 60|sleep 8|Start-Sleep/,
|
|
116
|
+
'expected no foreground sleep in the bugbot poll delays',
|
|
123
117
|
);
|
|
124
118
|
});
|
|
125
119
|
|
|
126
|
-
test('copilot gate
|
|
120
|
+
test('the copilot gate waits through the Monitor tool, not a foreground sleep', () => {
|
|
127
121
|
const copilotPrompt = lensPromptBody('runCopilotGate');
|
|
128
|
-
assert.match(copilotPrompt, /
|
|
129
|
-
assert.
|
|
130
|
-
copilotPrompt,
|
|
131
|
-
/Start-Sleep[\s\S]*alternative|alternative[\s\S]*Start-Sleep/i,
|
|
132
|
-
'expected PowerShell to be named only as an allowed alternative',
|
|
133
|
-
);
|
|
122
|
+
assert.match(copilotPrompt, /Monitor tool/, 'expected the copilot poll to wait via the Monitor tool');
|
|
123
|
+
assert.doesNotMatch(copilotPrompt, /sleep 360|Start-Sleep/, 'expected no foreground sleep in the copilot poll delay');
|
|
134
124
|
});
|
|
135
125
|
|
|
136
|
-
test('gotchas doc describes the reviewer wait as
|
|
137
|
-
assert.match(
|
|
138
|
-
gotchasSource,
|
|
139
|
-
/\bsleep\b/i,
|
|
140
|
-
'expected the wait guidance to name a shell-agnostic sleep',
|
|
141
|
-
);
|
|
126
|
+
test('gotchas doc describes the reviewer wait as a Monitor poll, not a foreground sleep', () => {
|
|
127
|
+
assert.match(gotchasSource, /Monitor tool/, 'expected the gotcha to name the Monitor-based reviewer wait');
|
|
142
128
|
assert.doesNotMatch(
|
|
143
129
|
gotchasSource,
|
|
144
|
-
/
|
|
145
|
-
'
|
|
130
|
+
/shell-agnostic/i,
|
|
131
|
+
'the reviewer wait is a Monitor poll, not a shell-agnostic sleep loop',
|
|
146
132
|
);
|
|
147
133
|
});
|
|
148
134
|
|
|
@@ -385,13 +371,45 @@ test('spawnStandardsFollowUp reports whether a hardening PR opened on every path
|
|
|
385
371
|
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
386
372
|
const falseReturns = body.match(/hardeningPrOpened:\s*false/g) || [];
|
|
387
373
|
assert.ok(
|
|
388
|
-
falseReturns.length >=
|
|
389
|
-
'expected
|
|
374
|
+
falseReturns.length >= 3,
|
|
375
|
+
'expected every skip path (hardening PR already opened, no hardening staged, verify failed) to return hardeningPrOpened:false',
|
|
390
376
|
);
|
|
391
377
|
assert.match(
|
|
392
378
|
body,
|
|
393
|
-
/hardeningPrOpened
|
|
394
|
-
'expected the commit path to
|
|
379
|
+
/const hardeningPrOpened =\s*typeof commitResult\?\.hardeningPrUrl === 'string' && commitResult\.hardeningPrUrl\.length > 0/,
|
|
380
|
+
'expected the commit path to derive hardeningPrOpened from a non-empty hardeningPrUrl, so a PR that opened with an unparseable URL still latches the guard while an empty URL (no PR opened) leaves it clear to retry',
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('spawnStandardsFollowUp reports the deferred PR identity on every path', () => {
|
|
385
|
+
const body = lensPromptBody('spawnStandardsFollowUp');
|
|
386
|
+
const nullDeferred = body.match(/deferredPr:\s*null/g) || [];
|
|
387
|
+
assert.ok(
|
|
388
|
+
nullDeferred.length >= 2,
|
|
389
|
+
'expected both skip paths (no hardening staged, verify failed) to return deferredPr:null',
|
|
390
|
+
);
|
|
391
|
+
assert.match(
|
|
392
|
+
body,
|
|
393
|
+
/parseDeferredPr\(commitResult\?\.hardeningPrUrl\)/,
|
|
394
|
+
'expected the commit path to parse the deferred PR identity from the commit step hardeningPrUrl',
|
|
395
|
+
);
|
|
396
|
+
assert.doesNotMatch(
|
|
397
|
+
body,
|
|
398
|
+
/hardeningPrOpened:\s*deferredPr !== null/,
|
|
399
|
+
'expected the commit path to keep hardeningPrOpened separate from the parsed deferredPr so an unparseable URL still latches the guard',
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('the workflow return objects carry the accumulated deferredPrs list', () => {
|
|
404
|
+
const converged = convergeSource.match(/deferredPrs/g) || [];
|
|
405
|
+
assert.ok(
|
|
406
|
+
converged.length >= 4,
|
|
407
|
+
'expected deferredPrs to be declared, pushed at both deferral call sites, and returned',
|
|
408
|
+
);
|
|
409
|
+
assert.match(
|
|
410
|
+
convergeSource,
|
|
411
|
+
/if \(standardsOutcome\?\.deferredPr\) deferredPrs\.push\(standardsOutcome\.deferredPr\)/,
|
|
412
|
+
'expected each deferral call site to accumulate the deferred PR into deferredPrs',
|
|
395
413
|
);
|
|
396
414
|
});
|
|
397
415
|
|
|
@@ -758,3 +776,117 @@ test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
|
758
776
|
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
759
777
|
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
760
778
|
});
|
|
779
|
+
|
|
780
|
+
test('the headless preamble routes waits through the Monitor tool and forbids ending a turn to await work', () => {
|
|
781
|
+
const preambleStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
|
|
782
|
+
assert.notEqual(preambleStart, -1, 'expected a HEADLESS_SAFETY_PREAMBLE definition');
|
|
783
|
+
const preambleEnd = convergeSource.indexOf('\n\nlet activeRepoPath', preambleStart);
|
|
784
|
+
assert.notEqual(preambleEnd, -1, 'expected the preamble to end before activeRepoPath');
|
|
785
|
+
const preamble = convergeSource.slice(preambleStart, preambleEnd);
|
|
786
|
+
assert.match(preamble, /foreground sleep is blocked/i, 'expected the preamble to state foreground sleep is blocked');
|
|
787
|
+
assert.match(preamble, /Monitor tool/, 'expected the preamble to route waits through the Monitor tool');
|
|
788
|
+
assert.match(preamble, /StructuredOutput/, 'expected the preamble to require a schema agent to always call StructuredOutput');
|
|
789
|
+
assert.match(preamble, /never end your turn to wait/i, 'expected the preamble to forbid ending a turn to await background work');
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('the preamble describes the Monitor wait by its real contract: a bounded until-loop consuming notifications as they arrive, rather than an in-turn synchronous return', () => {
|
|
793
|
+
const blockStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
|
|
794
|
+
assert.ok(blockStart !== -1, 'expected a wait-safety preamble to exist');
|
|
795
|
+
const blockEnd = convergeSource.indexOf('\n\nlet activeRepoPath', blockStart);
|
|
796
|
+
assert.ok(blockEnd !== -1, 'expected the preamble to end before activeRepoPath');
|
|
797
|
+
const preamble = convergeSource.slice(blockStart, blockEnd);
|
|
798
|
+
assert.doesNotMatch(
|
|
799
|
+
preamble,
|
|
800
|
+
/never move a wait to a background process/i,
|
|
801
|
+
'the Monitor tool is itself a background monitor, so the preamble must avoid forbidding a wait that moves to a background process',
|
|
802
|
+
);
|
|
803
|
+
assert.doesNotMatch(
|
|
804
|
+
preamble,
|
|
805
|
+
/return to you when the condition holds/i,
|
|
806
|
+
'the Monitor tool streams notifications across turns rather than returning in-turn, so the preamble must avoid describing an in-turn synchronous return',
|
|
807
|
+
);
|
|
808
|
+
assert.match(
|
|
809
|
+
preamble,
|
|
810
|
+
/until-loop/i,
|
|
811
|
+
'expected the preamble to describe the wait as a bounded until-loop paired with the Monitor tool',
|
|
812
|
+
);
|
|
813
|
+
assert.match(
|
|
814
|
+
preamble,
|
|
815
|
+
/notifications? as they arrive/i,
|
|
816
|
+
'expected the preamble to say Monitor notifications are consumed as they arrive',
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('the background-wait clause gives "await" an explicit object, not a dangling "await never resumes"', () => {
|
|
821
|
+
const text = preambleText();
|
|
822
|
+
assert.doesNotMatch(
|
|
823
|
+
text,
|
|
824
|
+
/end your turn to await never resumes/i,
|
|
825
|
+
'expected the background-wait clause to name what is awaited, not read as "await never resumes" with no object',
|
|
826
|
+
);
|
|
827
|
+
assert.match(
|
|
828
|
+
text,
|
|
829
|
+
/end your turn to await it/i,
|
|
830
|
+
'expected the background-wait clause to name the wait as the object of "await"',
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('the wait guidance names the full down-result schema, not a bare down flag', () => {
|
|
835
|
+
const text = preambleText();
|
|
836
|
+
assert.doesNotMatch(
|
|
837
|
+
text,
|
|
838
|
+
/Copilot gate, down: true\)/,
|
|
839
|
+
'expected the down signal to name the full schema, not a bare down flag that reads as a partial object',
|
|
840
|
+
);
|
|
841
|
+
assert.match(
|
|
842
|
+
text,
|
|
843
|
+
/down:true, findings:\[\]/,
|
|
844
|
+
'expected the wait guidance to name the full down-result fields',
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test('no agent prompt instructs a foreground sleep as the poll delay', () => {
|
|
849
|
+
assert.doesNotMatch(
|
|
850
|
+
convergeSource,
|
|
851
|
+
/delay each (?:attempt|iteration|retry) with "sleep/,
|
|
852
|
+
'expected no poll directive to instruct a foreground sleep as the between-attempt delay',
|
|
853
|
+
);
|
|
854
|
+
assert.doesNotMatch(
|
|
855
|
+
convergeSource,
|
|
856
|
+
/Start-Sleep -Seconds/,
|
|
857
|
+
'expected no agent prompt to instruct a foreground PowerShell Start-Sleep',
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
test('the wait guidance sizes the Monitor timeout_ms above the 300s default', () => {
|
|
862
|
+
const text = preambleText();
|
|
863
|
+
assert.match(text, /timeout_ms/, 'names the timeout_ms parameter the wait rule tells the agent to set');
|
|
864
|
+
assert.match(text, /persistent/, 'offers persistent as an alternative to a sized timeout_ms');
|
|
865
|
+
assert.match(text, /300000|300 second/, 'names the 300000 default that truncates a long wait');
|
|
866
|
+
assert.match(
|
|
867
|
+
text,
|
|
868
|
+
/interval[\s\S]{0,60}attempt|interval-times-attempts/i,
|
|
869
|
+
'sizes timeout_ms to the interval-times-attempts span the step names',
|
|
870
|
+
);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test('the Monitor ceiling the guidance names covers the longest interval-times-attempts wait the steps prescribe', () => {
|
|
874
|
+
const cap = /(\d{7})\s*(?:ms|millisecond)/i.exec(preambleText());
|
|
875
|
+
assert.ok(cap !== null, 'the guidance names a seven-digit Monitor ceiling in milliseconds');
|
|
876
|
+
const ceiling = Number(cap[1]);
|
|
877
|
+
|
|
878
|
+
const copilot = lensPromptBody('runCopilotGate');
|
|
879
|
+
const bugbot = lensPromptBody('runBugbotLens');
|
|
880
|
+
const apart = Number(/(\d+)\s*seconds apart/.exec(copilot)[1]);
|
|
881
|
+
const polls = Number(/copilotMaxPolls:\s*(\d+)/.exec(convergeSource)[1]);
|
|
882
|
+
const seconds = Number(/every\s*(\d+)\s*seconds/.exec(bugbot)[1]);
|
|
883
|
+
const iterations = Number(/up to\s*(\d+)\s*iterations/.exec(bugbot)[1]);
|
|
884
|
+
const spans = [apart * polls * 1000, seconds * iterations * 1000];
|
|
885
|
+
|
|
886
|
+
for (const span of spans) {
|
|
887
|
+
assert.ok(
|
|
888
|
+
span <= ceiling,
|
|
889
|
+
`a ${span}ms wait span exceeds the ${ceiling}ms Monitor ceiling the guidance names`,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
@@ -129,6 +129,24 @@ test('a Copilot no-show after the poll cap returns a down result rather than a b
|
|
|
129
129
|
);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test('the Copilot gate accepts a COMMENTED review with no inline findings as a clean pass', () => {
|
|
133
|
+
const copilotPrompt = functionBody('runCopilotGate');
|
|
134
|
+
assert.match(
|
|
135
|
+
copilotPrompt,
|
|
136
|
+
/COMMENTED with no inline findings/,
|
|
137
|
+
'expected the clean-pass branch to accept a COMMENTED review with no inline findings, matching the canonical ALL_COPILOT_CLEAN_REVIEW_STATES rule where a Copilot COMMENTED review is a clean state',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the Copilot gate polls long enough to catch a normal-latency Copilot review', () => {
|
|
142
|
+
const budgetMatch = convergeSource.match(/copilotMaxPolls:\s*(\d+)/);
|
|
143
|
+
assert.notEqual(budgetMatch, null, 'expected copilotMaxPolls in CONFIG');
|
|
144
|
+
assert.ok(
|
|
145
|
+
Number(budgetMatch[1]) >= 6,
|
|
146
|
+
`expected copilotMaxPolls >= 6 (>= ~36 min at 360s per attempt) so a normal-latency Copilot review lands before the no-show fallback triggers, got ${budgetMatch[1]}`,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
132
150
|
test('runConvergenceCheck wires the --copilot-down flag from the copilotDown context', () => {
|
|
133
151
|
const checkConvergenceBody = functionBody('runConvergenceCheck');
|
|
134
152
|
assert.match(
|
|
@@ -295,7 +313,7 @@ test('a copilotDisabled run reaches FINALIZE with --copilot-down', () => {
|
|
|
295
313
|
const bypassBlock = convergeSource.slice(bypassStart, bypassStart + 800);
|
|
296
314
|
assert.match(bypassBlock, /copilotDown = true/, 'expected the bypass to set copilotDown');
|
|
297
315
|
assert.match(bypassBlock, /phase = 'FINALIZE'/, 'expected the bypass to advance to FINALIZE');
|
|
298
|
-
const convergenceCheckBody = functionBody('
|
|
316
|
+
const convergenceCheckBody = functionBody('runConvergenceCheck');
|
|
299
317
|
assert.match(
|
|
300
318
|
convergenceCheckBody,
|
|
301
319
|
/context\.copilotDown \? ' --copilot-down' : ''/,
|
|
@@ -303,3 +321,326 @@ test('a copilotDisabled run reaches FINALIZE with --copilot-down', () => {
|
|
|
303
321
|
);
|
|
304
322
|
});
|
|
305
323
|
|
|
324
|
+
function loadStandardsFollowUpDecision() {
|
|
325
|
+
return new Function(
|
|
326
|
+
`${functionBody('shouldOpenStandardsFollowUp')}\n` +
|
|
327
|
+
'return shouldOpenStandardsFollowUp;',
|
|
328
|
+
)();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
test('the deferred follow-up create runs exactly once across a three-round standards run', () => {
|
|
332
|
+
const shouldOpenStandardsFollowUp = loadStandardsFollowUpDecision();
|
|
333
|
+
let alreadyOpened = false;
|
|
334
|
+
let createCount = 0;
|
|
335
|
+
for (let round = 0; round < 3; round += 1) {
|
|
336
|
+
if (shouldOpenStandardsFollowUp(alreadyOpened)) {
|
|
337
|
+
createCount += 1;
|
|
338
|
+
alreadyOpened = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
assert.equal(
|
|
342
|
+
createCount,
|
|
343
|
+
1,
|
|
344
|
+
'expected the deferred follow-up issue and hardening PR to be created exactly once across a multi-round run',
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('shouldOpenStandardsFollowUp opens on the first standards-only round and skips afterward', () => {
|
|
349
|
+
const shouldOpenStandardsFollowUp = loadStandardsFollowUpDecision();
|
|
350
|
+
assert.equal(shouldOpenStandardsFollowUp(false), true);
|
|
351
|
+
assert.equal(shouldOpenStandardsFollowUp(true), false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('the run seeds the standards follow-up flags to false so the first standards-only round opens the follow-up', () => {
|
|
355
|
+
assert.match(
|
|
356
|
+
convergeSource,
|
|
357
|
+
/let hasStandardsFollowUpFiled = false/,
|
|
358
|
+
'expected a run-scoped hasStandardsFollowUpFiled flag seeded false',
|
|
359
|
+
);
|
|
360
|
+
assert.match(
|
|
361
|
+
convergeSource,
|
|
362
|
+
/let wasStandardsHardeningPrOpened = false/,
|
|
363
|
+
'expected a run-scoped wasStandardsHardeningPrOpened flag seeded false',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('openStandardsFollowUpOnce gates spawnStandardsFollowUp behind the run-once flag and remembers the outcome', () => {
|
|
368
|
+
const onceBody = extractCallableSource('openStandardsFollowUpOnce');
|
|
369
|
+
assert.match(
|
|
370
|
+
onceBody,
|
|
371
|
+
/shouldOpenStandardsFollowUp\(hasStandardsFollowUpFiled\)/,
|
|
372
|
+
'expected the helper to consult the run-once decision on the current flag',
|
|
373
|
+
);
|
|
374
|
+
assert.match(
|
|
375
|
+
onceBody,
|
|
376
|
+
/await spawnStandardsFollowUp\(head, findings, sourceLabel, wasStandardsHardeningPrOpened\)/,
|
|
377
|
+
'expected the helper to pass the remembered hardening state into the spawn so an already-opened PR is never re-opened',
|
|
378
|
+
);
|
|
379
|
+
assert.match(
|
|
380
|
+
onceBody,
|
|
381
|
+
/hasStandardsFollowUpFiled = standardsOutcome\?\.followUpIssueFiled === true/,
|
|
382
|
+
'expected the helper to latch the flag only when the follow-up issue was actually filed so a transient failure retries on a later round',
|
|
383
|
+
);
|
|
384
|
+
assert.match(
|
|
385
|
+
onceBody,
|
|
386
|
+
/wasStandardsHardeningPrOpened = wasStandardsHardeningPrOpened \|\| standardsOutcome\?\.hardeningPrOpened === true/,
|
|
387
|
+
'expected the hardening guard to latch the moment a hardening PR opens and stay latched across rounds so a later issue-filing retry never re-opens it',
|
|
388
|
+
);
|
|
389
|
+
assert.match(
|
|
390
|
+
onceBody,
|
|
391
|
+
/hardeningPrOpened: wasStandardsHardeningPrOpened/,
|
|
392
|
+
'expected the helper to return the cached hardening outcome as hardeningPrOpened',
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('both standards-deferral call sites route the create through openStandardsFollowUpOnce', () => {
|
|
397
|
+
const onceCalls = convergeSource.match(/await openStandardsFollowUpOnce\(/g) || [];
|
|
398
|
+
assert.equal(
|
|
399
|
+
onceCalls.length,
|
|
400
|
+
2,
|
|
401
|
+
'expected the converge-round and copilot standards call sites to both defer to openStandardsFollowUpOnce',
|
|
402
|
+
);
|
|
403
|
+
const directCreates = convergeSource.match(/await spawnStandardsFollowUp\(/g) || [];
|
|
404
|
+
assert.equal(
|
|
405
|
+
directCreates.length,
|
|
406
|
+
1,
|
|
407
|
+
'expected spawnStandardsFollowUp to be invoked once from openStandardsFollowUpOnce, never directly at a call site',
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
function extractCallableSource(functionName) {
|
|
412
|
+
const asyncStart = convergeSource.indexOf(`async function ${functionName}(`);
|
|
413
|
+
const plainStart = convergeSource.indexOf(`function ${functionName}(`);
|
|
414
|
+
const declarationStart = asyncStart !== -1 ? asyncStart : plainStart;
|
|
415
|
+
assert.notEqual(declarationStart, -1, `expected ${functionName} to exist`);
|
|
416
|
+
const bodyStart = convergeSource.indexOf('{', declarationStart);
|
|
417
|
+
let depth = 0;
|
|
418
|
+
let index = bodyStart;
|
|
419
|
+
for (; index < convergeSource.length; index += 1) {
|
|
420
|
+
const character = convergeSource[index];
|
|
421
|
+
if (character === '{') {
|
|
422
|
+
depth += 1;
|
|
423
|
+
} else if (character === '}') {
|
|
424
|
+
depth -= 1;
|
|
425
|
+
if (depth === 0) {
|
|
426
|
+
index += 1;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return convergeSource.slice(declarationStart, index);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const parseableHardeningCommitResult = { hardeningPrUrl: 'https://github.com/owner/repo/pull/7', summary: 'opened' };
|
|
435
|
+
|
|
436
|
+
function loadStandardsFollowUpRuntime(recordedCalls, standardsEditResult, hardeningCommitResult = parseableHardeningCommitResult) {
|
|
437
|
+
const runtimeSource =
|
|
438
|
+
'let hasStandardsFollowUpFiled = false;\n' +
|
|
439
|
+
'let wasStandardsHardeningPrOpened = false;\n' +
|
|
440
|
+
"let standardsFollowUpIssueUrl = '';\n" +
|
|
441
|
+
'async function runCodeEditorTask(taskName, context) {\n' +
|
|
442
|
+
' recordedCalls.push({ task: taskName, context });\n' +
|
|
443
|
+
" if (taskName === 'standards-edit') return standardsEditResult;\n" +
|
|
444
|
+
" if (taskName === 'hardening-commit') return hardeningCommitResult;\n" +
|
|
445
|
+
' return {};\n' +
|
|
446
|
+
'}\n' +
|
|
447
|
+
'async function runVerifierTask() {\n' +
|
|
448
|
+
' return { passed: true };\n' +
|
|
449
|
+
'}\n' +
|
|
450
|
+
'function verdictPassed() {\n' +
|
|
451
|
+
' return true;\n' +
|
|
452
|
+
'}\n' +
|
|
453
|
+
'function log() {}\n' +
|
|
454
|
+
`${extractCallableSource('collectFindingThreadIds')}\n` +
|
|
455
|
+
`${extractCallableSource('findingsCarryThreads')}\n` +
|
|
456
|
+
`${extractCallableSource('shouldOpenStandardsFollowUp')}\n` +
|
|
457
|
+
`${extractCallableSource('parseDeferredPr')}\n` +
|
|
458
|
+
`${extractCallableSource('spawnStandardsFollowUp')}\n` +
|
|
459
|
+
`${extractCallableSource('resolveStandardsThreadsForBatch')}\n` +
|
|
460
|
+
`${extractCallableSource('openStandardsFollowUpOnce')}\n` +
|
|
461
|
+
'return {\n' +
|
|
462
|
+
' openStandardsFollowUpOnce,\n' +
|
|
463
|
+
' guards: () => ({ hasStandardsFollowUpFiled, wasStandardsHardeningPrOpened, standardsFollowUpIssueUrl }),\n' +
|
|
464
|
+
'};';
|
|
465
|
+
return new Function('recordedCalls', 'standardsEditResult', 'hardeningCommitResult', runtimeSource)(
|
|
466
|
+
recordedCalls,
|
|
467
|
+
standardsEditResult,
|
|
468
|
+
hardeningCommitResult,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function loadParseDeferredPr() {
|
|
473
|
+
return new Function(`${extractCallableSource('parseDeferredPr')}\nreturn parseDeferredPr;`)();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
test('parseDeferredPr parses a full canonical hardening PR URL into its coordinates', () => {
|
|
477
|
+
const parseDeferredPr = loadParseDeferredPr();
|
|
478
|
+
assert.deepEqual(
|
|
479
|
+
parseDeferredPr('https://github.com/jl-cmd/claude-code-config/pull/824'),
|
|
480
|
+
{ owner: 'jl-cmd', repo: 'claude-code-config', prNumber: 824 },
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('parseDeferredPr accepts a trailing slash, query string, or fragment on the PR URL', () => {
|
|
485
|
+
const parseDeferredPr = loadParseDeferredPr();
|
|
486
|
+
assert.equal(parseDeferredPr('https://github.com/owner/repo/pull/7/').prNumber, 7);
|
|
487
|
+
assert.equal(parseDeferredPr('https://github.com/owner/repo/pull/7?w=1').prNumber, 7);
|
|
488
|
+
assert.equal(parseDeferredPr('https://github.com/owner/repo/pull/7#issuecomment-42').prNumber, 7);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('parseDeferredPr rejects a PR URL embedded in surrounding log text so it never parses the wrong number', () => {
|
|
492
|
+
const parseDeferredPr = loadParseDeferredPr();
|
|
493
|
+
assert.equal(
|
|
494
|
+
parseDeferredPr('opened https://github.com/owner/repo/pull/7 then https://github.com/owner/repo/pull/9'),
|
|
495
|
+
null,
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('parseDeferredPr rejects a deep-linked pull path so a non-canonical URL parses no coordinate', () => {
|
|
500
|
+
const parseDeferredPr = loadParseDeferredPr();
|
|
501
|
+
assert.equal(parseDeferredPr('https://github.com/owner/repo/pull/7/files'), null);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('a second standards-only round never re-opens a hardening PR after the first round opened one but failed to file the issue', async () => {
|
|
505
|
+
const recordedCalls = [];
|
|
506
|
+
const issueFailedHardeningStaged = {
|
|
507
|
+
issueUrl: '',
|
|
508
|
+
hardeningEdited: true,
|
|
509
|
+
hardeningRepoPath: '/tmp/hardening',
|
|
510
|
+
hardeningBranch: 'harden-standards',
|
|
511
|
+
};
|
|
512
|
+
const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged);
|
|
513
|
+
|
|
514
|
+
const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round');
|
|
515
|
+
const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot');
|
|
516
|
+
|
|
517
|
+
const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
|
|
518
|
+
assert.equal(
|
|
519
|
+
hardeningCommitCalls,
|
|
520
|
+
1,
|
|
521
|
+
'expected the hardening PR to be committed exactly once even when the follow-up issue filing must retry on the second round',
|
|
522
|
+
);
|
|
523
|
+
assert.equal(firstRoundHardeningPr.hardeningPrOpened, true, 'expected the first round to open the hardening PR');
|
|
524
|
+
assert.equal(secondRoundHardeningPr.hardeningPrOpened, true, 'expected the second round to report the hardening PR as opened for this run');
|
|
525
|
+
assert.equal(
|
|
526
|
+
runtime.guards().wasStandardsHardeningPrOpened,
|
|
527
|
+
true,
|
|
528
|
+
'expected the hardening guard to stay latched across rounds',
|
|
529
|
+
);
|
|
530
|
+
assert.equal(
|
|
531
|
+
runtime.guards().hasStandardsFollowUpFiled,
|
|
532
|
+
false,
|
|
533
|
+
'expected the issue guard to stay clear so the filing keeps retrying',
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('a hardening-commit that opens a PR but returns an unparseable URL still latches the run-once guard', async () => {
|
|
538
|
+
const recordedCalls = [];
|
|
539
|
+
const issueFailedHardeningStaged = {
|
|
540
|
+
issueUrl: '',
|
|
541
|
+
hardeningEdited: true,
|
|
542
|
+
hardeningRepoPath: '/tmp/hardening',
|
|
543
|
+
hardeningBranch: 'harden-standards',
|
|
544
|
+
};
|
|
545
|
+
const unparseableUrlHardeningCommitResult = { hardeningPrUrl: 'draft-hardening-pr-opened', summary: 'opened' };
|
|
546
|
+
const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged, unparseableUrlHardeningCommitResult);
|
|
547
|
+
|
|
548
|
+
const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round');
|
|
549
|
+
const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot');
|
|
550
|
+
|
|
551
|
+
const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
|
|
552
|
+
assert.equal(
|
|
553
|
+
hardeningCommitCalls,
|
|
554
|
+
1,
|
|
555
|
+
'expected the non-empty-URL commit to latch the guard so a second round opens no duplicate hardening PR',
|
|
556
|
+
);
|
|
557
|
+
assert.equal(firstRoundHardeningPr.hardeningPrOpened, true, 'expected a non-empty (though unparseable) URL to report the hardening PR as opened');
|
|
558
|
+
assert.equal(firstRoundHardeningPr.deferredPr, null, 'expected the unparseable URL to contribute no deferred coordinate');
|
|
559
|
+
assert.equal(secondRoundHardeningPr.hardeningPrOpened, true, 'expected the second round to report the hardening PR as opened for this run');
|
|
560
|
+
assert.equal(
|
|
561
|
+
runtime.guards().wasStandardsHardeningPrOpened,
|
|
562
|
+
true,
|
|
563
|
+
'expected the hardening guard to latch even though the returned URL never parsed',
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('a hardening-commit that opens no PR (empty hardeningPrUrl) leaves the run-once guard clear so a later round retries the open', async () => {
|
|
568
|
+
const recordedCalls = [];
|
|
569
|
+
const issueFailedHardeningStaged = {
|
|
570
|
+
issueUrl: '',
|
|
571
|
+
hardeningEdited: true,
|
|
572
|
+
hardeningRepoPath: '/tmp/hardening',
|
|
573
|
+
hardeningBranch: 'harden-standards',
|
|
574
|
+
};
|
|
575
|
+
const noPrHardeningCommitResult = { hardeningPrUrl: '', summary: 'no PR opened' };
|
|
576
|
+
const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged, noPrHardeningCommitResult);
|
|
577
|
+
|
|
578
|
+
const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round');
|
|
579
|
+
const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot');
|
|
580
|
+
|
|
581
|
+
const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
|
|
582
|
+
assert.equal(
|
|
583
|
+
hardeningCommitCalls,
|
|
584
|
+
2,
|
|
585
|
+
'expected the empty-URL commit (no PR opened) to leave the guard clear so a later round retries the open',
|
|
586
|
+
);
|
|
587
|
+
assert.equal(firstRoundHardeningPr.hardeningPrOpened, false, 'expected an empty URL to report no hardening PR opened');
|
|
588
|
+
assert.equal(secondRoundHardeningPr.hardeningPrOpened, false, 'expected the retry round to still report no hardening PR opened');
|
|
589
|
+
assert.equal(
|
|
590
|
+
runtime.guards().wasStandardsHardeningPrOpened,
|
|
591
|
+
false,
|
|
592
|
+
'expected the hardening guard to stay clear when no PR opened so the open keeps retrying',
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test('a later standards-only round resolves its own review threads after the follow-up issue was already filed', async () => {
|
|
597
|
+
const recordedCalls = [];
|
|
598
|
+
const issueFiledNoHardening = {
|
|
599
|
+
issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/900',
|
|
600
|
+
hardeningEdited: false,
|
|
601
|
+
hardeningRepoPath: '',
|
|
602
|
+
hardeningBranch: '',
|
|
603
|
+
};
|
|
604
|
+
const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
|
|
605
|
+
|
|
606
|
+
await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round');
|
|
607
|
+
await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: 42 }], 'copilot');
|
|
608
|
+
|
|
609
|
+
const standardsEditCalls = recordedCalls.filter((call) => call.task === 'standards-edit');
|
|
610
|
+
assert.equal(standardsEditCalls.length, 1, 'expected the follow-up issue to be filed exactly once across the run');
|
|
611
|
+
|
|
612
|
+
const resolveCalls = recordedCalls.filter((call) => call.task === 'standards-resolve-threads');
|
|
613
|
+
assert.equal(resolveCalls.length, 1, 'expected the reuse-path round to resolve its own batch review threads');
|
|
614
|
+
assert.deepEqual(
|
|
615
|
+
resolveCalls[0].context.findings,
|
|
616
|
+
[{ file: 'b.py', line: 2, replyToCommentId: 42 }],
|
|
617
|
+
'expected the resolve step to receive the reuse-path batch findings so their threads get replied-to and resolved',
|
|
618
|
+
);
|
|
619
|
+
assert.equal(
|
|
620
|
+
resolveCalls[0].context.issueUrl,
|
|
621
|
+
'https://github.com/jl-cmd/claude-code-config/issues/900',
|
|
622
|
+
'expected the resolve step to reference the already-filed follow-up issue in its inline reply',
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('a reuse-path standards round carrying no review threads spawns no thread-resolution agent', async () => {
|
|
627
|
+
const recordedCalls = [];
|
|
628
|
+
const issueFiledNoHardening = {
|
|
629
|
+
issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/901',
|
|
630
|
+
hardeningEdited: false,
|
|
631
|
+
hardeningRepoPath: '',
|
|
632
|
+
hardeningBranch: '',
|
|
633
|
+
};
|
|
634
|
+
const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
|
|
635
|
+
|
|
636
|
+
await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round');
|
|
637
|
+
await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: null }], 'copilot');
|
|
638
|
+
|
|
639
|
+
const resolveCalls = recordedCalls.filter((call) => call.task === 'standards-resolve-threads');
|
|
640
|
+
assert.equal(
|
|
641
|
+
resolveCalls.length,
|
|
642
|
+
0,
|
|
643
|
+
'expected no thread-resolution agent when the reuse-path batch of in-memory findings carries no review threads',
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
@@ -31,7 +31,7 @@ test('isMergeConflicting reports a conflict only when the check returned conflic
|
|
|
31
31
|
assert.equal(isMergeConflicting({ conflicting: false }), false);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test('the git agent handles merge-conflict checks with
|
|
34
|
+
test('the git agent handles merge-conflict checks with a Monitor-based poll', () => {
|
|
35
35
|
const gitBody = functionBody('runGitTask');
|
|
36
36
|
assert.match(gitBody, /mergeable/, 'expected the git agent to read the PR mergeable field');
|
|
37
37
|
assert.match(
|
|
@@ -40,7 +40,7 @@ test('the git agent handles merge-conflict checks with shell-agnostic polling',
|
|
|
40
40
|
'expected the git agent merge check to be read-only',
|
|
41
41
|
);
|
|
42
42
|
assert.match(gitBody, /MERGE_CONFLICT_SCHEMA/, 'expected the git agent to return MERGE_CONFLICT_SCHEMA');
|
|
43
|
-
assert.match(gitBody, /
|
|
43
|
+
assert.match(gitBody, /Monitor tool/, 'expected a Monitor-based poll delay, not a foreground sleep');
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
test('runCodeEditorTask conflict-edit path rebases onto origin/main and makes no push', () => {
|