claude-dev-env 1.79.0 → 1.81.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.
Files changed (76) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +3 -1
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
  3. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
  10. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
  12. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  13. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  14. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
  15. package/audit-rubrics/CLAUDE.md +3 -2
  16. package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
  17. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
  18. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
  19. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
  20. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
  21. package/audit-rubrics/prompts/CLAUDE.md +2 -1
  22. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
  23. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
  24. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
  25. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
  26. package/docs/CODE_RULES.md +2 -2
  27. package/hooks/blocking/CLAUDE.md +2 -0
  28. package/hooks/blocking/code_rules_annotations_length.py +59 -11
  29. package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
  30. package/hooks/blocking/code_rules_command_dispatch.py +140 -0
  31. package/hooks/blocking/code_rules_docstrings.py +93 -0
  32. package/hooks/blocking/code_rules_enforcer.py +58 -4
  33. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  34. package/hooks/blocking/code_rules_js_conventions.py +246 -0
  35. package/hooks/blocking/code_rules_naming_collection.py +5 -0
  36. package/hooks/blocking/code_rules_test_assertions.py +3 -0
  37. package/hooks/blocking/code_rules_unused_imports.py +7 -66
  38. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
  39. package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
  40. package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
  41. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
  42. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
  43. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
  45. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  46. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
  47. package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
  48. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  49. package/hooks/hooks_constants/CLAUDE.md +2 -0
  50. package/hooks/hooks_constants/blocking_check_limits.py +10 -0
  51. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  52. package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
  53. package/hooks/hooks_constants/js_conventions_constants.py +54 -0
  54. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  55. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  56. package/package.json +1 -1
  57. package/rules/docstring-prose-matches-implementation.md +3 -1
  58. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
  59. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
  60. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
  61. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
  62. package/skills/autoconverge/SKILL.md +28 -7
  63. package/skills/autoconverge/reference/convergence.md +3 -3
  64. package/skills/autoconverge/reference/stop-conditions.md +1 -1
  65. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  66. package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
  67. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
  68. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  69. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  70. package/skills/autoconverge/workflow/converge.mjs +235 -247
  71. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  72. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  73. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  74. package/skills/pr-converge/SKILL.md +28 -2
  75. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  76. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -45,8 +45,8 @@ test('bug-audit lens prompt no longer instructs a per-lens git fetch', () => {
45
45
  });
46
46
 
47
47
  test('a single round-level prefetch step fetches origin/main before the parallel lenses', () => {
48
- assert.ok(convergeSource.includes("resumeGitAgent(gitAgentId, 'prefetch-main')"));
49
- const prefetchCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'prefetch-main')");
48
+ assert.ok(convergeSource.includes("runGitTask('prefetch-main')"));
49
+ const prefetchCallIndex = convergeSource.indexOf("runGitTask('prefetch-main')");
50
50
  const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
51
51
  assert.notEqual(prefetchCallIndex, -1, 'expected prefetch to be invoked');
52
52
  assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
@@ -85,7 +85,7 @@ test('gotchas doc states parallel lenses must avoid concurrent git operations',
85
85
  });
86
86
 
87
87
  test('repair-convergence edit step filters unresolved threads to bot authors and skips human threads', () => {
88
- const repairPrompt = functionSource('resumeCodeEditorAgent');
88
+ const repairPrompt = functionSource('runCodeEditorTask');
89
89
  assert.match(
90
90
  repairPrompt,
91
91
  /cursor.*claude.*copilot|copilot.*cursor.*claude|claude.*cursor.*copilot/is,
@@ -99,7 +99,7 @@ test('repair-convergence edit step filters unresolved threads to bot authors and
99
99
  });
100
100
 
101
101
  test('repair-convergence edit step no longer instructs resolving every unresolved thread without an author filter', () => {
102
- const repairPrompt = functionSource('resumeCodeEditorAgent');
102
+ const repairPrompt = functionSource('runCodeEditorTask');
103
103
  assert.doesNotMatch(
104
104
  repairPrompt,
105
105
  /fetch every thread where isResolved is false/,
@@ -195,12 +195,12 @@ test('the COPILOT fix branch does not re-assign head from the fix before re-conv
195
195
  test('the CONVERGE branch re-resolves HEAD from GitHub on every entry', () => {
196
196
  const convergeBranchStart = convergeSource.indexOf("if (phase === 'CONVERGE')");
197
197
  assert.notEqual(convergeBranchStart, -1, 'expected the CONVERGE branch to exist');
198
- const headResolveCallIndex = convergeSource.indexOf("resumeGitAgent(gitAgentId, 'resolve-head')", convergeBranchStart);
199
- assert.notEqual(headResolveCallIndex, -1, 'expected CONVERGE to re-resolve HEAD via resumeGitAgent');
198
+ const headResolveCallIndex = convergeSource.indexOf("runGitTask('resolve-head')", convergeBranchStart);
199
+ assert.notEqual(headResolveCallIndex, -1, 'expected CONVERGE to re-resolve HEAD via runGitTask');
200
200
  });
201
201
 
202
202
  test('fix edit prompt resolves threads by PRRT thread node id looked up from the comment databaseId', () => {
203
- const editPrompt = functionSource('resumeCodeEditorAgent');
203
+ const editPrompt = functionSource('runCodeEditorTask');
204
204
  assert.match(editPrompt, /PRRT/, 'expected the thread node id form (PRRT_...) to be named');
205
205
  assert.match(
206
206
  editPrompt,
@@ -216,15 +216,15 @@ test('fix edit prompt resolves threads by PRRT thread node id looked up from the
216
216
 
217
217
  test('fix edit prompt does not pass the numeric comment id straight to resolve_thread', () => {
218
218
  assert.doesNotMatch(
219
- functionSource('resumeCodeEditorAgent'),
219
+ functionSource('runCodeEditorTask'),
220
220
  /then resolve that thread \(use the github MCP pull_request_review_write/,
221
221
  'resolve_thread and resolveReviewThread require a PRRT_... thread node id, not the comment id',
222
222
  );
223
223
  });
224
224
 
225
- test('the fix flow spawns a fixer agent and runs fixerWithRecovery after the edit step', () => {
225
+ test('the fix flow runs the edit task then fixerWithRecovery after the edit step', () => {
226
226
  const applyFixesBody = lensPromptBody('applyFixes');
227
- assert.match(applyFixesBody, /spawnFixerAgent\(/, 'expected applyFixes to call spawnFixerAgent');
227
+ assert.match(applyFixesBody, /runCodeEditorTask\('fix-edit'/, "expected applyFixes to call runCodeEditorTask('fix-edit')");
228
228
  assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
229
229
  });
230
230
 
@@ -269,7 +269,7 @@ test('the verdict-fence binding does not self-resolve a cwd via git rev-parse fo
269
269
  });
270
270
 
271
271
  test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
272
- for (const verifyFunctionName of ['resumeVerifierAgent']) {
272
+ for (const verifyFunctionName of ['runVerifierTask']) {
273
273
  const verifyBody = lensPromptBody(verifyFunctionName);
274
274
  assert.match(
275
275
  verifyBody,
@@ -294,15 +294,15 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
294
294
  }
295
295
  });
296
296
 
297
- test('resumeFixerAgent never verifies its own session — verification belongs to the separate verifier', () => {
298
- const fixerBody = lensPromptBody('resumeFixerAgent');
297
+ test('runFixerTask never verifies — verification belongs to the separate verifier', () => {
298
+ const fixerBody = lensPromptBody('runFixerTask');
299
299
  assert.doesNotMatch(fixerBody, /buildVerdictFenceSteps\(/, 'expected the fixer to not emit a verdict fence — the separate verifier does');
300
- assert.doesNotMatch(fixerBody, /agentType:\s*'code-verifier'/, 'expected the fixer session to be clean-coder only');
300
+ assert.doesNotMatch(fixerBody, /agentType:\s*'code-verifier'/, 'expected the fixer to be clean-coder only');
301
301
  assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
302
302
  });
303
303
 
304
- test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
305
- const verifyBody = lensPromptBody('resumeVerifierAgent');
304
+ test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
305
+ const verifyBody = lensPromptBody('runVerifierTask');
306
306
  assert.match(
307
307
  verifyBody,
308
308
  /--manifest-hash-for-branch/,
@@ -315,18 +315,18 @@ test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening bra
315
315
  );
316
316
  });
317
317
 
318
- test('resumeVerifierAgent passes PR coordinates to buildVerdictFenceSteps for the fix-verify and repair-verify tasks', () => {
319
- const verifyBody = lensPromptBody('resumeVerifierAgent');
320
- assert.match(verifyBody, /task === 'fix-verify'/, 'expected resumeVerifierAgent to carry the fix-path verify task');
318
+ test('runVerifierTask passes PR coordinates to buildVerdictFenceSteps for the fix-verify and repair-verify tasks', () => {
319
+ const verifyBody = lensPromptBody('runVerifierTask');
320
+ assert.match(verifyBody, /task === 'fix-verify'/, 'expected runVerifierTask to carry the fix-path verify task');
321
321
  assert.match(
322
322
  verifyBody,
323
323
  /buildVerdictFenceSteps\(input\.owner, input\.repo, input\.prNumber\)/,
324
- 'expected resumeVerifierAgent to pass PR coordinates to buildVerdictFenceSteps',
324
+ 'expected runVerifierTask to pass PR coordinates to buildVerdictFenceSteps',
325
325
  );
326
326
  });
327
327
 
328
- test('the commit path in resumeFixerAgent forbids further edits and uses clean-coder', () => {
329
- const fixerBody = lensPromptBody('resumeFixerAgent');
328
+ test('the commit path in runFixerTask forbids further edits and uses clean-coder', () => {
329
+ const fixerBody = lensPromptBody('runFixerTask');
330
330
  assert.match(
331
331
  fixerBody,
332
332
  /no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
@@ -339,22 +339,22 @@ test('the commit path in resumeFixerAgent forbids further edits and uses clean-c
339
339
  );
340
340
  });
341
341
 
342
- test('the repair flow uses resume helpers for edit, verify, and commit', () => {
342
+ test('the repair flow uses the direct-spawn task helpers for edit, verify, and commit', () => {
343
343
  const repairBody = lensPromptBody('repairConvergence');
344
- assert.match(repairBody, /spawnCodeEditorAgent\(/, 'expected repairConvergence to spawn a code-editor');
345
- assert.match(repairBody, /spawnVerifierAgent\(/, 'expected repairConvergence to spawn a verifier');
344
+ assert.match(repairBody, /runCodeEditorTask\(/, 'expected repairConvergence to call runCodeEditorTask');
345
+ assert.match(repairBody, /runVerifierTask\(/, 'expected repairConvergence to call runVerifierTask');
346
346
  assert.match(repairBody, /verdictPassed\(/, 'expected the verify verdict to gate the repair commit step');
347
347
  });
348
348
 
349
- test('the standards-deferral flow uses resume helpers for edit, verify, and commit', () => {
349
+ test('the standards-deferral flow uses the direct-spawn task helpers for edit, verify, and commit', () => {
350
350
  const standardsBody = lensPromptBody('spawnStandardsFollowUp');
351
- assert.match(standardsBody, /spawnCodeEditorAgent\(/, 'expected spawnStandardsFollowUp to spawn a code-editor');
352
- assert.match(standardsBody, /spawnVerifierAgent\(/, 'expected spawnStandardsFollowUp to spawn a verifier');
351
+ assert.match(standardsBody, /runCodeEditorTask\(/, 'expected spawnStandardsFollowUp to call runCodeEditorTask');
352
+ assert.match(standardsBody, /runVerifierTask\(/, 'expected spawnStandardsFollowUp to call runVerifierTask');
353
353
  assert.match(standardsBody, /verdictPassed\(/, 'expected the verify verdict to gate the hardening commit step');
354
354
  });
355
355
 
356
356
  test('repair-commit and hardening-commit paths use clean-coder and forbid edits', () => {
357
- const codeEditorBody = lensPromptBody('resumeCodeEditorAgent');
357
+ const codeEditorBody = lensPromptBody('runCodeEditorTask');
358
358
  assert.match(
359
359
  codeEditorBody,
360
360
  /no (?:further |additional )?(?:file )?edits|do not edit|make no edits/i,
@@ -368,7 +368,7 @@ test('repair-commit and hardening-commit paths use clean-coder and forbid edits'
368
368
  });
369
369
 
370
370
  test('the code-editor standards-edit path stages hardening without committing and uses clean-coder', () => {
371
- const editBody = lensPromptBody('resumeCodeEditorAgent');
371
+ const editBody = lensPromptBody('runCodeEditorTask');
372
372
  assert.match(
373
373
  editBody,
374
374
  /do not commit and do not push|NO commit and NO push|Do NOT commit/i,
@@ -385,8 +385,8 @@ test('spawnStandardsFollowUp reports whether a hardening PR opened on every path
385
385
  const body = lensPromptBody('spawnStandardsFollowUp');
386
386
  const falseReturns = body.match(/hardeningPrOpened:\s*false/g) || [];
387
387
  assert.ok(
388
- falseReturns.length >= 2,
389
- 'expected both skip paths (no hardening staged, verify failed) to return hardeningPrOpened:false',
388
+ falseReturns.length >= 3,
389
+ 'expected every skip path (hardening PR already opened, no hardening staged, verify failed) to return hardeningPrOpened:false',
390
390
  );
391
391
  assert.match(
392
392
  body,
@@ -495,9 +495,9 @@ test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES
495
495
  );
496
496
  });
497
497
 
498
- const editStepResumeHelpers = ['resumeCodeEditorAgent', 'resumeFixerAgent'];
498
+ const editStepTaskDispatchers = ['runCodeEditorTask', 'runFixerTask'];
499
499
 
500
- for (const helperName of editStepResumeHelpers) {
500
+ for (const helperName of editStepTaskDispatchers) {
501
501
  test(`${helperName} appends the pre-commit gate step to its edit prompts`, () => {
502
502
  assert.match(
503
503
  functionSource(helperName),
@@ -507,14 +507,14 @@ for (const helperName of editStepResumeHelpers) {
507
507
  });
508
508
  }
509
509
 
510
- const editStepResumeTasks = [
511
- ['resumeCodeEditorAgent', 'fix-edit'],
512
- ['resumeCodeEditorAgent', 'repair-edit'],
513
- ['resumeCodeEditorAgent', 'standards-edit'],
514
- ['resumeCodeEditorAgent', 'commit-recover'],
510
+ const editStepTasks = [
511
+ ['runCodeEditorTask', 'fix-edit'],
512
+ ['runCodeEditorTask', 'repair-edit'],
513
+ ['runCodeEditorTask', 'standards-edit'],
514
+ ['runCodeEditorTask', 'commit-recover'],
515
515
  ];
516
516
 
517
- for (const [helperName, taskName] of editStepResumeTasks) {
517
+ for (const [helperName, taskName] of editStepTasks) {
518
518
  test(`${helperName} routes the ${taskName} task to a pre-commit-gated edit prompt`, () => {
519
519
  assert.match(
520
520
  functionSource(helperName),
@@ -634,37 +634,26 @@ test('SKILL.md attributes the known-temp-var resolution to the cwd-scoped auto-a
634
634
  );
635
635
  });
636
636
 
637
- test('convergeAgent prepends HEADLESS_SAFETY_PREAMBLE and worktree directive on fresh spawn', () => {
637
+ test('convergeAgent prepends HEADLESS_SAFETY_PREAMBLE and the worktree directive to every prompt', () => {
638
638
  const convergeAgentBody = lensPromptBody('convergeAgent');
639
639
  assert.match(
640
640
  convergeAgentBody,
641
641
  /HEADLESS_SAFETY_PREAMBLE.*worktreeDirective/,
642
- 'expected fresh-spawn path to prepend both preamble and worktree directive',
642
+ 'expected convergeAgent to prepend both preamble and worktree directive',
643
643
  );
644
644
  });
645
645
 
646
- test('convergeAgent checks isResume before prepending the preamble', () => {
647
- const convergeAgentBody = lensPromptBody('convergeAgent');
648
- assert.match(convergeAgentBody, /isResume/, 'expected an isResume guard in convergeAgent');
649
- });
650
-
651
- const newSpawnResumeHelpers = [
652
- { name: 'spawnGitAgent', isAsync: true },
653
- { name: 'resumeGitAgent', isAsync: false },
654
- { name: 'spawnFixerAgent', isAsync: true },
655
- { name: 'resumeFixerAgent', isAsync: false },
646
+ const taskDispatchers = [
647
+ { name: 'runGitTask', isAsync: false },
648
+ { name: 'runFixerTask', isAsync: false },
656
649
  { name: 'fixerWithRecovery', isAsync: true },
657
- { name: 'spawnCodeEditorAgent', isAsync: true },
658
- { name: 'resumeCodeEditorAgent', isAsync: false },
659
- { name: 'spawnVerifierAgent', isAsync: true },
660
- { name: 'resumeVerifierAgent', isAsync: false },
661
- { name: 'spawnGeneralUtilityAgent', isAsync: true },
662
- { name: 'resumeGeneralUtilityAgent', isAsync: false },
663
- { name: 'spawnConvergenceCheckAgent', isAsync: true },
664
- { name: 'resumeConvergenceCheckAgent', isAsync: false },
650
+ { name: 'runCodeEditorTask', isAsync: false },
651
+ { name: 'runVerifierTask', isAsync: false },
652
+ { name: 'runGeneralUtilityTask', isAsync: false },
653
+ { name: 'runConvergenceCheck', isAsync: false },
665
654
  ];
666
655
 
667
- for (const { name, isAsync } of newSpawnResumeHelpers) {
656
+ for (const { name, isAsync } of taskDispatchers) {
668
657
  const prefix = isAsync ? 'async ' : '';
669
658
  test(`function ${prefix}${name} exists in converge.mjs`, () => {
670
659
  const needle = isAsync ? `async function ${name}(` : `function ${name}(`;
@@ -672,60 +661,8 @@ for (const { name, isAsync } of newSpawnResumeHelpers) {
672
661
  });
673
662
  }
674
663
 
675
- const spawnHelperNames = [
676
- 'spawnGitAgent',
677
- 'spawnFixerAgent',
678
- 'spawnCodeEditorAgent',
679
- 'spawnVerifierAgent',
680
- 'spawnGeneralUtilityAgent',
681
- 'spawnConvergenceCheckAgent',
682
- ];
683
-
684
- for (const spawnName of spawnHelperNames) {
685
- test(`${spawnName} actually spawns an agent via convergeAgent`, () => {
686
- const spawnBody = functionSource(spawnName);
687
- assert.match(
688
- spawnBody,
689
- /await convergeAgent\(/,
690
- `expected ${spawnName} to spawn a real agent through convergeAgent rather than return a hardcoded label`,
691
- );
692
- });
693
-
694
- test(`${spawnName} returns the spawned agent's runtime id`, () => {
695
- const spawnBody = functionSource(spawnName);
696
- assert.match(
697
- spawnBody,
698
- /return\s+result\?\.agentId/,
699
- `expected ${spawnName} to return result?.agentId so resume targets the real session`,
700
- );
701
- assert.doesNotMatch(
702
- spawnBody,
703
- /return\s+['"`]/,
704
- `expected ${spawnName} not to return a hardcoded label string`,
705
- );
706
- });
707
- }
708
-
709
- test('spawnGitAgent returns result?.agentId without a tautological ternary', () => {
710
- const spawnBody = functionSource('spawnGitAgent');
711
- assert.doesNotMatch(
712
- spawnBody,
713
- /\?\s*'git-utility'\s*:\s*'git-utility'/,
714
- 'expected the identical-branch ternary that discards the spawn outcome to be gone',
715
- );
716
- });
717
-
718
- test('resume helpers fall back to a fresh spawn when no agentId is available', () => {
719
- const convergeAgentBody = lensPromptBody('convergeAgent');
720
- assert.match(
721
- convergeAgentBody,
722
- /options\?\.resume.*length\s*>\s*0|length\s*>\s*0.*resume/s,
723
- 'expected convergeAgent to treat a missing agentId as a fresh spawn (isResume false), restoring the preamble and worktree directive',
724
- );
725
- });
726
-
727
- test('resumeGeneralUtilityAgent only handles the two tasks it is called with', () => {
728
- const generalBody = functionSource('resumeGeneralUtilityAgent');
664
+ test('runGeneralUtilityTask only handles the two tasks it is called with', () => {
665
+ const generalBody = functionSource('runGeneralUtilityTask');
729
666
  assert.doesNotMatch(
730
667
  generalBody,
731
668
  /task === 'bugbot-lens'/,
@@ -761,7 +698,7 @@ const orphanedHelperNames = [
761
698
  ];
762
699
 
763
700
  for (const orphanName of orphanedHelperNames) {
764
- test(`${orphanName} is removed — its behavior lives in a resume helper`, () => {
701
+ test(`${orphanName} is removed — its behavior lives in a direct-spawn task dispatcher`, () => {
765
702
  assert.ok(
766
703
  !convergeSource.includes(`function ${orphanName}(`),
767
704
  `expected the orphaned ${orphanName} definition to be deleted (CODE_RULES 9.8)`,
@@ -769,12 +706,11 @@ for (const orphanName of orphanedHelperNames) {
769
706
  });
770
707
  }
771
708
 
772
- test('convergeAgent omits the preamble when options.resume is a non-empty string', () => {
773
- const convergeAgentBody = lensPromptBody('convergeAgent');
774
- assert.match(
775
- convergeAgentBody,
776
- /isResume\s*\?\s*prompt/,
777
- 'expected the resume path to pass prompt through without the preamble',
709
+ test('the whole priming spawn-agent family is removed every dispatcher spawns fresh', () => {
710
+ assert.doesNotMatch(
711
+ convergeSource,
712
+ /function\s+spawn\w+Agent\s*\(/,
713
+ 'expected no spawn<Role>Agent priming function to survive — each task dispatcher spawns a fresh agent',
778
714
  );
779
715
  });
780
716
 
@@ -129,12 +129,30 @@ 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('resumeConvergenceCheckAgent wires the --copilot-down flag from the copilotDown context', () => {
133
- const checkConvergenceBody = functionBody('resumeConvergenceCheckAgent');
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
+
150
+ test('runConvergenceCheck wires the --copilot-down flag from the copilotDown context', () => {
151
+ const checkConvergenceBody = functionBody('runConvergenceCheck');
134
152
  assert.match(
135
153
  checkConvergenceBody,
136
154
  /context\.copilotDown \? ' --copilot-down' : ''/,
137
- 'expected resumeConvergenceCheckAgent to append --copilot-down when copilotDown is set',
155
+ 'expected runConvergenceCheck to append --copilot-down when copilotDown is set',
138
156
  );
139
157
  assert.match(
140
158
  checkConvergenceBody,
@@ -244,8 +262,8 @@ test('markReady receives copilotDown so it can opt the unflagged hook out of the
244
262
  );
245
263
  });
246
264
 
247
- test('the mark-ready task in resumeGeneralUtilityAgent opts the unflagged convergence hook out of Copilot when copilotDown', () => {
248
- const markReadyBody = functionBody('resumeGeneralUtilityAgent');
265
+ test('the mark-ready task in runGeneralUtilityTask opts the unflagged convergence hook out of Copilot when copilotDown', () => {
266
+ const markReadyBody = functionBody('runGeneralUtilityTask');
249
267
  assert.match(
250
268
  markReadyBody,
251
269
  /context\.copilotDown/,
@@ -263,3 +281,269 @@ test('the mark-ready task in resumeGeneralUtilityAgent opts the unflagged conver
263
281
  );
264
282
  });
265
283
 
284
+ test('the COPILOT phase short-circuits on input.copilotDisabled before spawning the gate agent', () => {
285
+ const copilotPhaseStart = convergeSource.indexOf("if (phase === 'COPILOT') {");
286
+ assert.notEqual(copilotPhaseStart, -1, 'expected a COPILOT phase block');
287
+ const gateCallIndex = convergeSource.indexOf('await runCopilotGate(head)', copilotPhaseStart);
288
+ assert.notEqual(gateCallIndex, -1, 'expected the COPILOT phase to call runCopilotGate when Copilot is enabled');
289
+ const beforeGate = convergeSource.slice(copilotPhaseStart, gateCallIndex);
290
+ assert.match(
291
+ beforeGate,
292
+ /if \(input\.copilotDisabled\)/,
293
+ 'expected the quota pre-check bypass to guard the COPILOT phase before any gate agent spawns',
294
+ );
295
+ assert.match(beforeGate, /copilotDown = true/, 'expected the bypass to mark copilotDown');
296
+ assert.match(beforeGate, /copilotNote =/, 'expected the bypass to set a copilotNote');
297
+ assert.match(beforeGate, /phase = 'FINALIZE'/, 'expected the bypass to advance to FINALIZE');
298
+ assert.match(beforeGate, /continue/, 'expected the bypass to continue without spawning the gate agent');
299
+ });
300
+
301
+ test('copilotDown initializes from input.copilotDisabled so the pre-check decision carries into the loop', () => {
302
+ assert.match(
303
+ convergeSource,
304
+ /let copilotDown = input\.copilotDisabled \|\| false/,
305
+ 'expected copilotDown to seed from the copilotDisabled run input',
306
+ );
307
+ });
308
+
309
+ test('a copilotDisabled run reaches FINALIZE with --copilot-down', () => {
310
+ const copilotPhaseStart = convergeSource.indexOf("if (phase === 'COPILOT') {");
311
+ const bypassStart = convergeSource.indexOf('if (input.copilotDisabled)', copilotPhaseStart);
312
+ assert.notEqual(bypassStart, -1, 'expected an input.copilotDisabled bypass in the COPILOT phase');
313
+ const bypassBlock = convergeSource.slice(bypassStart, bypassStart + 800);
314
+ assert.match(bypassBlock, /copilotDown = true/, 'expected the bypass to set copilotDown');
315
+ assert.match(bypassBlock, /phase = 'FINALIZE'/, 'expected the bypass to advance to FINALIZE');
316
+ const convergenceCheckBody = functionBody('runConvergenceCheck');
317
+ assert.match(
318
+ convergenceCheckBody,
319
+ /context\.copilotDown \? ' --copilot-down' : ''/,
320
+ 'expected the convergence check to pass --copilot-down when the bypassed copilotDown reaches FINALIZE',
321
+ );
322
+ });
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
+ /return wasStandardsHardeningPrOpened/,
392
+ 'expected the skip path to return the cached hardening outcome',
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
+ function loadStandardsFollowUpRuntime(recordedCalls, standardsEditResult) {
435
+ const runtimeSource =
436
+ 'let hasStandardsFollowUpFiled = false;\n' +
437
+ 'let wasStandardsHardeningPrOpened = false;\n' +
438
+ "let standardsFollowUpIssueUrl = '';\n" +
439
+ 'async function runCodeEditorTask(taskName, context) {\n' +
440
+ ' recordedCalls.push({ task: taskName, context });\n' +
441
+ " return taskName === 'standards-edit' ? standardsEditResult : {};\n" +
442
+ '}\n' +
443
+ 'async function runVerifierTask() {\n' +
444
+ ' return { passed: true };\n' +
445
+ '}\n' +
446
+ 'function verdictPassed() {\n' +
447
+ ' return true;\n' +
448
+ '}\n' +
449
+ 'function log() {}\n' +
450
+ `${extractCallableSource('collectFindingThreadIds')}\n` +
451
+ `${extractCallableSource('findingsCarryThreads')}\n` +
452
+ `${extractCallableSource('shouldOpenStandardsFollowUp')}\n` +
453
+ `${extractCallableSource('spawnStandardsFollowUp')}\n` +
454
+ `${extractCallableSource('resolveStandardsThreadsForBatch')}\n` +
455
+ `${extractCallableSource('openStandardsFollowUpOnce')}\n` +
456
+ 'return {\n' +
457
+ ' openStandardsFollowUpOnce,\n' +
458
+ ' guards: () => ({ hasStandardsFollowUpFiled, wasStandardsHardeningPrOpened, standardsFollowUpIssueUrl }),\n' +
459
+ '};';
460
+ return new Function('recordedCalls', 'standardsEditResult', runtimeSource)(
461
+ recordedCalls,
462
+ standardsEditResult,
463
+ );
464
+ }
465
+
466
+ 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 () => {
467
+ const recordedCalls = [];
468
+ const issueFailedHardeningStaged = {
469
+ issueUrl: '',
470
+ hardeningEdited: true,
471
+ hardeningRepoPath: '/tmp/hardening',
472
+ hardeningBranch: 'harden-standards',
473
+ };
474
+ const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged);
475
+
476
+ const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round');
477
+ const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot');
478
+
479
+ const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
480
+ assert.equal(
481
+ hardeningCommitCalls,
482
+ 1,
483
+ 'expected the hardening PR to be committed exactly once even when the follow-up issue filing must retry on the second round',
484
+ );
485
+ assert.equal(firstRoundHardeningPr, true, 'expected the first round to open the hardening PR');
486
+ assert.equal(secondRoundHardeningPr, true, 'expected the second round to report the hardening PR as opened for this run');
487
+ assert.equal(
488
+ runtime.guards().wasStandardsHardeningPrOpened,
489
+ true,
490
+ 'expected the hardening guard to stay latched across rounds',
491
+ );
492
+ assert.equal(
493
+ runtime.guards().hasStandardsFollowUpFiled,
494
+ false,
495
+ 'expected the issue guard to stay clear so the filing keeps retrying',
496
+ );
497
+ });
498
+
499
+ test('a later standards-only round resolves its own review threads after the follow-up issue was already filed', async () => {
500
+ const recordedCalls = [];
501
+ const issueFiledNoHardening = {
502
+ issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/900',
503
+ hardeningEdited: false,
504
+ hardeningRepoPath: '',
505
+ hardeningBranch: '',
506
+ };
507
+ const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
508
+
509
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round');
510
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: 42 }], 'copilot');
511
+
512
+ const standardsEditCalls = recordedCalls.filter((call) => call.task === 'standards-edit');
513
+ assert.equal(standardsEditCalls.length, 1, 'expected the follow-up issue to be filed exactly once across the run');
514
+
515
+ const resolveCalls = recordedCalls.filter((call) => call.task === 'standards-resolve-threads');
516
+ assert.equal(resolveCalls.length, 1, 'expected the reuse-path round to resolve its own batch review threads');
517
+ assert.deepEqual(
518
+ resolveCalls[0].context.findings,
519
+ [{ file: 'b.py', line: 2, replyToCommentId: 42 }],
520
+ 'expected the resolve step to receive the reuse-path batch findings so their threads get replied-to and resolved',
521
+ );
522
+ assert.equal(
523
+ resolveCalls[0].context.issueUrl,
524
+ 'https://github.com/jl-cmd/claude-code-config/issues/900',
525
+ 'expected the resolve step to reference the already-filed follow-up issue in its inline reply',
526
+ );
527
+ });
528
+
529
+ test('a reuse-path standards round carrying no review threads spawns no thread-resolution agent', async () => {
530
+ const recordedCalls = [];
531
+ const issueFiledNoHardening = {
532
+ issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/901',
533
+ hardeningEdited: false,
534
+ hardeningRepoPath: '',
535
+ hardeningBranch: '',
536
+ };
537
+ const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
538
+
539
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round');
540
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: null }], 'copilot');
541
+
542
+ const resolveCalls = recordedCalls.filter((call) => call.task === 'standards-resolve-threads');
543
+ assert.equal(
544
+ resolveCalls.length,
545
+ 0,
546
+ 'expected no thread-resolution agent when the reuse-path batch of in-memory findings carries no review threads',
547
+ );
548
+ });
549
+