claude-dev-env 1.82.0 → 1.83.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 (67) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  3. package/_shared/pr-loop/scripts/README.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  7. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  10. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  11. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  12. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/code_rules_constants_config.py +159 -1
  15. package/hooks/blocking/code_rules_docstrings.py +312 -9
  16. package/hooks/blocking/code_rules_enforcer.py +29 -0
  17. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  18. package/hooks/blocking/code_rules_naming_collection.py +141 -0
  19. package/hooks/blocking/code_rules_string_magic.py +68 -0
  20. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  21. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  22. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  23. package/hooks/blocking/state_description_blocker.py +96 -5
  24. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  25. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  26. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  27. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  28. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  29. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  30. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  31. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  32. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  33. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  34. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  35. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  36. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  37. package/hooks/blocking/test_state_description_blocker.py +135 -0
  38. package/hooks/hooks.json +5 -0
  39. package/hooks/hooks_constants/CLAUDE.md +3 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +43 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  44. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  45. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  46. package/package.json +1 -1
  47. package/rules/CLAUDE.md +4 -1
  48. package/rules/claude-md-orphan-file.md +5 -0
  49. package/rules/docstring-prose-matches-implementation.md +10 -1
  50. package/rules/env-var-table-code-drift.md +5 -0
  51. package/rules/es-exe-file-search.md +17 -0
  52. package/rules/no-historical-clutter.md +12 -1
  53. package/rules/orphan-css-class.md +5 -0
  54. package/rules/package-inventory-stale-entry.md +10 -0
  55. package/rules/paired-test-coverage.md +5 -0
  56. package/rules/plain-illustrative-docstrings.md +5 -0
  57. package/rules/verify-before-asking.md +7 -0
  58. package/rules/verify-runtime-state.md +40 -0
  59. package/rules/windows-filesystem-safe.md +8 -0
  60. package/rules/workers-done-before-complete.md +33 -0
  61. package/rules/workflow-substitution-slots.md +5 -0
  62. package/skills/CLAUDE.md +1 -1
  63. package/skills/autoconverge/SKILL.md +10 -4
  64. package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
  65. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
  66. package/skills/autoconverge/workflow/converge.mjs +97 -33
  67. package/skills/everything-search/SKILL.md +5 -0
@@ -133,7 +133,9 @@ on completion. Watch live progress with `/workflows`.
133
133
  The workflow returns
134
134
  `{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote, deferredPrs }`.
135
135
  `deferredPrs` is the list of draft environment-hardening PRs the standards-deferral
136
- path opened this run, each as `{ owner, repo, prNumber }` the seed the
136
+ path opened this run, each as `{ owner, repo, prNumber, copilotDisabled, bugbotDisabled }`.
137
+ The two flags carry this run's Copilot and Bugbot availability, so the next generation
138
+ skips a reviewer that is down or out of quota without re-probing. This list is the seed the
137
139
  [self-closing loop](#self-closing-loop-converge-the-deferred-prs) converges next.
138
140
 
139
141
  ## Budget-aware round boundaries
@@ -435,8 +437,8 @@ so there is nothing to converge. Report that and stop.
435
437
 
436
438
  ### Each generation
437
439
 
438
- Given a non-empty list of deferred PRs `{ owner, repo, prNumber }` (a generation
439
- may span more than one repository — a hardening PR lands in whichever repo owns
440
+ Given a non-empty list of deferred PRs `{ owner, repo, prNumber, copilotDisabled, bugbotDisabled }`
441
+ (a generation may span more than one repository — a hardening PR lands in whichever repo owns
440
442
  the surface that blocks the deferred class, so `JonEcho/llm-settings` for hooks
441
443
  and `jl-cmd/claude-code-config` for rules and skills both appear):
442
444
 
@@ -449,7 +451,11 @@ and `jl-cmd/claude-code-config` for rules and skills both appear):
449
451
  Grant project permissions once per repository the generation spans.
450
452
  2. **Converge the generation.** Launch `workflow/converge_multi.mjs` with one
451
453
  entry per checked-out deferred PR, exactly as the
452
- [multi-PR launch](#launch-the-multi-pr-workflow) describes.
454
+ [multi-PR launch](#launch-the-multi-pr-workflow) describes. Each child run
455
+ re-checks Copilot and Bugbot availability every round through the workflow's own
456
+ pre-spawn probe, so a reviewer that is down or out of quota is never spawned in any
457
+ generation; the `copilotDisabled`/`bugbotDisabled` flags each deferred PR carries
458
+ seed that check for the first round.
453
459
  3. **Tear down.** Run the [multi-PR teardown](#multi-pr-teardown-on-workflow-completion)
454
460
  over the generation's `results`, and revoke project permissions once per
455
461
  repository.
@@ -56,6 +56,75 @@ test('a single round-level prefetch step fetches origin/main before the parallel
56
56
  );
57
57
  });
58
58
 
59
+ test('the CONVERGE round spawns a single shared reviewer-availability probe before the parallel lenses', () => {
60
+ const prefetchCallIndex = convergeSource.indexOf("runGitTask('prefetch-main')");
61
+ const probeCallIndex = convergeSource.indexOf('reviewerAvailability = await runReviewerAvailabilityCheck()');
62
+ const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
63
+ assert.notEqual(probeCallIndex, -1, 'expected the CONVERGE round to spawn the shared reviewer-availability probe');
64
+ assert.ok(
65
+ prefetchCallIndex < probeCallIndex && probeCallIndex < parallelLensIndex,
66
+ 'expected the probe to run after the round prefetch and before the parallel lenses spawn',
67
+ );
68
+ const probeBody = functionSource('runReviewerAvailabilityCheck');
69
+ assert.match(probeBody, /reviewer_availability\.py/);
70
+ assert.match(probeBody, /--reviewer copilot/);
71
+ assert.match(probeBody, /--reviewer bugbot/);
72
+ assert.match(probeBody, /schema:\s*REVIEWER_AVAILABILITY_SCHEMA/);
73
+ });
74
+
75
+ test('the Bugbot lens is not spawned pre-spawn when the shared gate reports Bugbot down', () => {
76
+ const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
77
+ assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
78
+ const isBugbotDownPreSpawnIndex = convergeSource.indexOf('const isBugbotDownPreSpawn = resolveReviewerDown(');
79
+ assert.notEqual(isBugbotDownPreSpawnIndex, -1, 'expected a pre-spawn Bugbot-down decision computed before the lens array');
80
+ assert.ok(
81
+ isBugbotDownPreSpawnIndex < parallelLensIndex,
82
+ 'expected the pre-spawn Bugbot-down decision to be computed before the parallel lens array',
83
+ );
84
+ const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
85
+ const lensArray = convergeSource.slice(parallelLensIndex, lensArrayEnd);
86
+ assert.match(
87
+ lensArray,
88
+ /isBugbotDownPreSpawn \? Promise\.resolve\(\{ sha: head, clean: true, down: true, findings: \[\] \}\) : runBugbotLens\(head\)/,
89
+ 'expected the Bugbot lens slot to substitute a resolved down placeholder instead of spawning runBugbotLens when isBugbotDownPreSpawn is true',
90
+ );
91
+ });
92
+
93
+ test('the post-round bugbotDown verdict treats a dead Bugbot lens as down, not fail-open', () => {
94
+ const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
95
+ assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
96
+ const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
97
+ const bugbotDownAssignIndex = convergeSource.indexOf('bugbotDown = ', lensArrayEnd);
98
+ assert.notEqual(bugbotDownAssignIndex, -1, 'expected the post-round bugbotDown assignment after the lens array');
99
+ const bugbotDownAssignLineEnd = convergeSource.indexOf('\n', bugbotDownAssignIndex);
100
+ const bugbotDownAssignLine = convergeSource.slice(bugbotDownAssignIndex, bugbotDownAssignLineEnd);
101
+ assert.match(
102
+ bugbotDownAssignLine,
103
+ /bugbotDown = lenses\[0\] == null \? true : resolveReviewerDown\(lenses\[0\], input\.bugbotDisabled \|\| false\)/,
104
+ 'expected a dead (null) Bugbot lens result to report bugbotDown:true — resolveReviewerDown alone is fail-open on null, which would wrongly report bugbotDown:false for a lens agent that actually spawned and died, leaving check_convergence.py to look for a bugbot review that was never posted',
105
+ );
106
+ });
107
+
108
+ test('both reviewer trigger commands carry the CLAUDE_REVIEWER_GATE=autoconverge sentinel', () => {
109
+ assert.match(
110
+ convergeSource,
111
+ /const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '/,
112
+ 'expected the shared sentinel constant to exist',
113
+ );
114
+ const copilotPrompt = lensPromptBody('runCopilotGate');
115
+ assert.match(
116
+ copilotPrompt,
117
+ /\$\{REVIEWER_GATE_SENTINEL\}gh api --method POST[^\n]*requested_reviewers/,
118
+ 'expected the Copilot requested_reviewers trigger to carry the sentinel prefix',
119
+ );
120
+ const bugbotPrompt = lensPromptBody('runBugbotLens');
121
+ assert.match(
122
+ bugbotPrompt,
123
+ /\$\{REVIEWER_GATE_SENTINEL\}python "\$\{CONFIG\.sharedScripts\}\/post_fix_reply\.py"[^\n]*--body "bugbot run"/,
124
+ 'expected the Bugbot rerun-comment trigger to carry the sentinel prefix',
125
+ );
126
+ });
127
+
59
128
  test('bugbot lens preamble does not blanket-instruct passing --owner/--repo to every script', () => {
60
129
  const bugbotPrompt = lensPromptBody('runBugbotLens');
61
130
  assert.doesNotMatch(
@@ -18,9 +18,10 @@ function functionBody(functionName) {
18
18
  const productionModule = new Function(
19
19
  `${functionBody('classifyCopilotOutcome')}\n` +
20
20
  `${functionBody('resolveCopilotDown')}\n` +
21
- 'return { classifyCopilotOutcome, resolveCopilotDown };',
21
+ `${functionBody('resolveReviewerDown')}\n` +
22
+ 'return { classifyCopilotOutcome, resolveCopilotDown, resolveReviewerDown };',
22
23
  )();
23
- const { classifyCopilotOutcome, resolveCopilotDown } = productionModule;
24
+ const { classifyCopilotOutcome, resolveCopilotDown, resolveReviewerDown } = productionModule;
24
25
 
25
26
  function copilotResult(overrides) {
26
27
  return {
@@ -281,7 +282,7 @@ test('the mark-ready task in runGeneralUtilityTask opts the unflagged convergenc
281
282
  );
282
283
  });
283
284
 
284
- test('the COPILOT phase short-circuits on input.copilotDisabled before spawning the gate agent', () => {
285
+ test('the COPILOT phase short-circuits on the unified reviewer-down gate before spawning the gate agent', () => {
285
286
  const copilotPhaseStart = convergeSource.indexOf("if (phase === 'COPILOT') {");
286
287
  assert.notEqual(copilotPhaseStart, -1, 'expected a COPILOT phase block');
287
288
  const gateCallIndex = convergeSource.indexOf('await runCopilotGate(head)', copilotPhaseStart);
@@ -289,8 +290,8 @@ test('the COPILOT phase short-circuits on input.copilotDisabled before spawning
289
290
  const beforeGate = convergeSource.slice(copilotPhaseStart, gateCallIndex);
290
291
  assert.match(
291
292
  beforeGate,
292
- /if \(input\.copilotDisabled\)/,
293
- 'expected the quota pre-check bypass to guard the COPILOT phase before any gate agent spawns',
293
+ /if \(resolveReviewerDown\(reviewerAvailability\?\.copilot, input\.copilotDisabled \|\| false\)\)/,
294
+ 'expected the unified resolveReviewerDown gate — fed by the reviewer-availability probe or the input override — to guard the COPILOT phase before any gate agent spawns',
294
295
  );
295
296
  assert.match(beforeGate, /copilotDown = true/, 'expected the bypass to mark copilotDown');
296
297
  assert.match(beforeGate, /copilotNote =/, 'expected the bypass to set a copilotNote');
@@ -298,6 +299,16 @@ test('the COPILOT phase short-circuits on input.copilotDisabled before spawning
298
299
  assert.match(beforeGate, /continue/, 'expected the bypass to continue without spawning the gate agent');
299
300
  });
300
301
 
302
+ test('the COPILOT phase pre-spawn gate skips the gate agent via input.copilotDisabled with no probe entry', () => {
303
+ const isDownFromInput = resolveReviewerDown(undefined, true);
304
+ assert.equal(isDownFromInput, true, 'expected the input override alone to report Copilot down with no probe entry');
305
+ });
306
+
307
+ test('the COPILOT phase pre-spawn gate skips the gate agent via a probe down entry with no input override', () => {
308
+ const isDownFromProbe = resolveReviewerDown({ down: true, reason: 'copilot-quota: out of premium-interaction quota' }, false);
309
+ assert.equal(isDownFromProbe, true, 'expected a probe entry reporting down to skip the gate agent with no input override set');
310
+ });
311
+
301
312
  test('copilotDown initializes from input.copilotDisabled so the pre-check decision carries into the loop', () => {
302
313
  assert.match(
303
314
  convergeSource,
@@ -308,8 +319,11 @@ test('copilotDown initializes from input.copilotDisabled so the pre-check decisi
308
319
 
309
320
  test('a copilotDisabled run reaches FINALIZE with --copilot-down', () => {
310
321
  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');
322
+ const bypassStart = convergeSource.indexOf(
323
+ 'if (resolveReviewerDown(reviewerAvailability?.copilot, input.copilotDisabled || false))',
324
+ copilotPhaseStart,
325
+ );
326
+ assert.notEqual(bypassStart, -1, 'expected the unified resolveReviewerDown bypass in the COPILOT phase');
313
327
  const bypassBlock = convergeSource.slice(bypassStart, bypassStart + 800);
314
328
  assert.match(bypassBlock, /copilotDown = true/, 'expected the bypass to set copilotDown');
315
329
  assert.match(bypassBlock, /phase = 'FINALIZE'/, 'expected the bypass to advance to FINALIZE');
@@ -373,7 +387,7 @@ test('openStandardsFollowUpOnce gates spawnStandardsFollowUp behind the run-once
373
387
  );
374
388
  assert.match(
375
389
  onceBody,
376
- /await spawnStandardsFollowUp\(head, findings, sourceLabel, wasStandardsHardeningPrOpened\)/,
390
+ /await spawnStandardsFollowUp\(head, findings, sourceLabel, wasStandardsHardeningPrOpened, deferredReviewerFlags\)/,
377
391
  'expected the helper to pass the remembered hardening state into the spawn so an already-opened PR is never re-opened',
378
392
  );
379
393
  assert.match(
@@ -511,8 +525,8 @@ test('a second standards-only round never re-opens a hardening PR after the firs
511
525
  };
512
526
  const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged);
513
527
 
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');
528
+ const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round', { copilotDisabled: false, bugbotDisabled: false });
529
+ const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot', { copilotDisabled: false, bugbotDisabled: false });
516
530
 
517
531
  const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
518
532
  assert.equal(
@@ -545,8 +559,8 @@ test('a hardening-commit that opens a PR but returns an unparseable URL still la
545
559
  const unparseableUrlHardeningCommitResult = { hardeningPrUrl: 'draft-hardening-pr-opened', summary: 'opened' };
546
560
  const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged, unparseableUrlHardeningCommitResult);
547
561
 
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');
562
+ const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round', { copilotDisabled: false, bugbotDisabled: false });
563
+ const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot', { copilotDisabled: false, bugbotDisabled: false });
550
564
 
551
565
  const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
552
566
  assert.equal(
@@ -575,8 +589,8 @@ test('a hardening-commit that opens no PR (empty hardeningPrUrl) leaves the run-
575
589
  const noPrHardeningCommitResult = { hardeningPrUrl: '', summary: 'no PR opened' };
576
590
  const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFailedHardeningStaged, noPrHardeningCommitResult);
577
591
 
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');
592
+ const firstRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'converge-round', { copilotDisabled: false, bugbotDisabled: false });
593
+ const secondRoundHardeningPr = await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1 }], 'copilot', { copilotDisabled: false, bugbotDisabled: false });
580
594
 
581
595
  const hardeningCommitCalls = recordedCalls.filter((call) => call.task === 'hardening-commit').length;
582
596
  assert.equal(
@@ -603,8 +617,8 @@ test('a later standards-only round resolves its own review threads after the fol
603
617
  };
604
618
  const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
605
619
 
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');
620
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round', { copilotDisabled: false, bugbotDisabled: false });
621
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: 42 }], 'copilot', { copilotDisabled: false, bugbotDisabled: false });
608
622
 
609
623
  const standardsEditCalls = recordedCalls.filter((call) => call.task === 'standards-edit');
610
624
  assert.equal(standardsEditCalls.length, 1, 'expected the follow-up issue to be filed exactly once across the run');
@@ -633,8 +647,8 @@ test('a reuse-path standards round carrying no review threads spawns no thread-r
633
647
  };
634
648
  const runtime = loadStandardsFollowUpRuntime(recordedCalls, issueFiledNoHardening);
635
649
 
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');
650
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'a.py', line: 1, replyToCommentId: null }], 'converge-round', { copilotDisabled: false, bugbotDisabled: false });
651
+ await runtime.openStandardsFollowUpOnce('sha1', [{ file: 'b.py', line: 2, replyToCommentId: null }], 'copilot', { copilotDisabled: false, bugbotDisabled: false });
638
652
 
639
653
  const resolveCalls = recordedCalls.filter((call) => call.task === 'standards-resolve-threads');
640
654
  assert.equal(
@@ -644,3 +658,25 @@ test('a reuse-path standards round carrying no review threads spawns no thread-r
644
658
  );
645
659
  });
646
660
 
661
+ test('resolveReviewerDown is the single reviewer-down gate; resolveBugbotDown no longer exists', () => {
662
+ assert.doesNotMatch(
663
+ convergeSource,
664
+ /function resolveBugbotDown\(/,
665
+ 'expected resolveBugbotDown to be removed in favor of the shared resolveReviewerDown gate',
666
+ );
667
+ assert.match(convergeSource, /function resolveReviewerDown\(/, 'expected the shared resolveReviewerDown gate to exist');
668
+ });
669
+
670
+ test('resolveReviewerDown reports down when the input override is set, even with an available probe entry', () => {
671
+ assert.equal(resolveReviewerDown({ down: false, reason: 'available' }, true), true);
672
+ });
673
+
674
+ test('resolveReviewerDown reports available (fail-open) when the probe entry is missing and no input override is set', () => {
675
+ assert.equal(resolveReviewerDown(null, false), false);
676
+ assert.equal(resolveReviewerDown(undefined, false), false);
677
+ });
678
+
679
+ test('resolveReviewerDown reports available when the probe entry explicitly reports available', () => {
680
+ assert.equal(resolveReviewerDown({ down: false, reason: 'available' }, false), false);
681
+ });
682
+
@@ -29,6 +29,8 @@ const CONFIG = {
29
29
  bugteamRubric: '$HOME/.claude/skills/bugteam/reference/audit-contract.md',
30
30
  }
31
31
 
32
+ const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '
33
+
32
34
  const HEADLESS_SAFETY_PREAMBLE =
33
35
  'HEADLESS RUN — you run unattended: no human can answer a permission or confirmation prompt, and any such prompt stalls the entire convergence run. The destructive_command_blocker hook matches dangerous patterns (rm -rf, git reset --hard, dd, mkfs, chmod -R, fork bombs) as raw text anywhere in a Bash command, with no quote-awareness — so a destructive string stalls you even when it is only data you never execute. Therefore:\n' +
34
36
  '- Never place a destructive-command literal inside a Bash command — not in echo, not in a heredoc, and not as an argument to python -c, node -e, or awk. To exercise or verify destructive_command_blocker (or any hook) behavior, run the committed test suite, e.g. python -m pytest <test_file>, which passes the command strings as in-language data rather than as a shell command.\n' +
@@ -464,6 +466,27 @@ function runConvergenceCheck(context) {
464
466
  )
465
467
  }
466
468
 
469
+ /**
470
+ * Spawn a single reviewer-availability Explore agent once per CONVERGE round,
471
+ * before either reviewer's own agent is spawned. It runs the shared
472
+ * reviewer_availability.py CLI for both Copilot and Bugbot and reports each
473
+ * one's down state and reason, so resolveReviewerDown can decide whether to
474
+ * spawn the Bugbot lens and the Copilot gate without depending on an
475
+ * externally pre-set input flag.
476
+ * @returns {Promise<object>} REVIEWER_AVAILABILITY_SCHEMA result
477
+ */
478
+ function runReviewerAvailabilityCheck() {
479
+ return convergeAgent(
480
+ `Check whether GitHub Copilot and Cursor Bugbot are available to review ${prCoordinates} this round, before either reviewer's agent is spawned. Do not edit code, commit, or push.\n\n` +
481
+ `Run exactly:\n` +
482
+ `python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer copilot\n` +
483
+ `python "${CONFIG.prLoopScripts}/reviewer_availability.py" --reviewer bugbot\n` +
484
+ `Each run exits 0 when that reviewer is available and non-zero when it is down, and prints one line naming the reason (stdout when available, stderr when down) — capture that line.\n\n` +
485
+ `Return strictly the schema: copilot.down and bugbot.down report whether that reviewer's run exited non-zero, and copilot.reason / bugbot.reason carry its printed line.`,
486
+ { label: 'reviewer-availability', phase: 'Converge', schema: REVIEWER_AVAILABILITY_SCHEMA, agentType: 'Explore' },
487
+ )
488
+ }
489
+
467
490
  const PRE_COMMIT_GATE_STEP =
468
491
  `\n\nFINAL STEP — pre-commit gate check (do NOT commit): before your turn ends, prove your working-tree changes CAN be committed by dry-running the CODE_RULES commit gate that gates git commit (precommit_code_rules_gate). From inside the checkout that holds your changes, resolve its root with git rev-parse --show-toplevel, stage your changes with git add -A, then run exactly:\n` +
469
492
  ` python "${CONFIG.prLoopScripts}/code_rules_gate.py" --repo-root "<that root>" --staged\n` +
@@ -509,6 +532,32 @@ const COPILOT_SCHEMA = {
509
532
  required: ['sha', 'clean', 'down', 'findings'],
510
533
  }
511
534
 
535
+ const REVIEWER_AVAILABILITY_SCHEMA = {
536
+ type: 'object',
537
+ additionalProperties: false,
538
+ properties: {
539
+ copilot: {
540
+ type: 'object',
541
+ additionalProperties: false,
542
+ properties: {
543
+ down: { type: 'boolean', description: 'true when reviewer_availability.py --reviewer copilot exited non-zero (opted out or out of premium-request quota)' },
544
+ reason: { type: 'string', description: 'the one-line reason reviewer_availability.py printed for Copilot' },
545
+ },
546
+ required: ['down', 'reason'],
547
+ },
548
+ bugbot: {
549
+ type: 'object',
550
+ additionalProperties: false,
551
+ properties: {
552
+ down: { type: 'boolean', description: 'true when reviewer_availability.py --reviewer bugbot exited non-zero (opted out via CLAUDE_REVIEWS_DISABLED)' },
553
+ reason: { type: 'string', description: 'the one-line reason reviewer_availability.py printed for Bugbot' },
554
+ },
555
+ required: ['down', 'reason'],
556
+ },
557
+ },
558
+ required: ['copilot', 'bugbot'],
559
+ }
560
+
512
561
  const HEAD_SCHEMA = {
513
562
  type: 'object',
514
563
  additionalProperties: false,
@@ -749,19 +798,27 @@ function isMoreSevere(candidateSeverity, currentSeverity) {
749
798
  }
750
799
 
751
800
  /**
752
- * Decide whether the convergence check should bypass the Bugbot gate this round,
753
- * recomputed from the current Bugbot lens result rather than latched across the
754
- * run, so a recovered Bugbot re-arms the gate. A dead lens agent (null/undefined
755
- * result) produces no Bugbot verdict on this HEAD, so it is treated as down to
756
- * keep the convergence gate from demanding a verdict that cannot exist.
757
- * @param {object|null|undefined} bugbotLens the current round's Bugbot lens result
758
- * @param {boolean} bugbotDisabled true when Bugbot is opted out for the whole run
759
- * @returns {boolean} true when the Bugbot gate is bypassed for the current HEAD
801
+ * Decide whether a reviewer is skipped this round — the one shared gate both
802
+ * Bugbot and Copilot consult before either reviewer's agent is spawned. The
803
+ * run's own disable flag always wins, so a deferred PR seeded with
804
+ * copilotDisabled or bugbotDisabled skips the reviewer without a probe.
805
+ * Otherwise the decision comes from the carried entry's down field, read from
806
+ * the round-start availability probe for a pre-spawn decision. A missing entry
807
+ * (a dead probe agent, or no result yet) reads as available rather than down,
808
+ * so an outage in the probe itself never wedges convergence the reviewer's
809
+ * own runtime detection still runs and can report down on its own. This
810
+ * fail-open null handling suits a pre-spawn decision; a caller computing a
811
+ * post-round verdict from a reviewer's own lens/gate result — where a dead
812
+ * agent has no verdict to report, not an available one — guards that null
813
+ * case itself before calling this function.
814
+ * @param {{down: boolean}|null|undefined} reviewerAvailabilityEntry the probe's per-reviewer entry, or a reviewer's own result, carrying a down field
815
+ * @param {boolean} isReviewerDisabledByInput true when the run input opts this reviewer out for the whole run
816
+ * @returns {boolean} true when the reviewer is treated as down this round
760
817
  */
761
- function resolveBugbotDown(bugbotLens, bugbotDisabled) {
762
- if (bugbotDisabled) return true
763
- if (bugbotLens == null) return true
764
- return bugbotLens.down === true
818
+ function resolveReviewerDown(reviewerAvailabilityEntry, isReviewerDisabledByInput) {
819
+ if (isReviewerDisabledByInput) return true
820
+ if (reviewerAvailabilityEntry == null) return false
821
+ return reviewerAvailabilityEntry.down === true
765
822
  }
766
823
 
767
824
  /**
@@ -1015,9 +1072,10 @@ function classifyCopilotOutcome(copilot) {
1015
1072
 
1016
1073
  /**
1017
1074
  * Decide whether the Copilot review gate is bypassed for this COPILOT pass from
1018
- * the gate outcome, mirroring resolveBugbotDown so the flag is recomputed every
1019
- * pass rather than left sticky. Only a 'down' outcome (Copilot out of quota or
1020
- * unreachable after the poll cap) bypasses the convergence Copilot gate; an
1075
+ * the gate outcome, mirroring resolveReviewerDown's post-round bugbotDown
1076
+ * recompute so the flag is recomputed every pass rather than left sticky. Only
1077
+ * a 'down' outcome (Copilot out of quota or unreachable after the poll cap)
1078
+ * bypasses the convergence Copilot gate; an
1021
1079
  * 'approved', 'fix', or 'retry' outcome means Copilot answered this pass, so the
1022
1080
  * gate must be evaluated against its review and is never bypassed. Recomputing
1023
1081
  * from the current outcome is what lets a recovered Copilot — one that returns
@@ -1105,9 +1163,6 @@ const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNum
1105
1163
  * @returns {Promise<object>} LENS_SCHEMA result
1106
1164
  */
1107
1165
  function runBugbotLens(head) {
1108
- if (input.bugbotDisabled) {
1109
- return Promise.resolve({ sha: head, clean: true, down: true, findings: [] })
1110
- }
1111
1166
  return convergeAgent(
1112
1167
  `You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
1113
1168
  `Goal: return Bugbot's verdict on HEAD ${head}. Do not edit code, commit, or push. You may post the literal trigger comment described below.\n\n` +
@@ -1120,7 +1175,7 @@ function runBugbotLens(head) {
1120
1175
  ` Only count entries whose commit_id starts with ${head}.\n` +
1121
1176
  ` - If findings exist on HEAD -> return them (each with its inline comment id in replyToCommentId when present, else null).\n` +
1122
1177
  ` - If a clean review exists on HEAD -> return clean.\n` +
1123
- `4. No review yet on HEAD: check_bugbot_ci.py --check-active. If active (exit 0), poll: repeat check_bugbot_ci.py --check-clean / --check-active every 60 seconds (wait each 60-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above) for up to 25 iterations, then re-fetch the review. If not active (exit 1), post the literal comment "bugbot run" (no @mention, no other text) via python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --body "bugbot run", wait 8 seconds inside this turn with the Monitor tool (per the WAITS AND POLLS rule above), then poll as above.\n` +
1178
+ `4. No review yet on HEAD: check_bugbot_ci.py --check-active. If active (exit 0), poll: repeat check_bugbot_ci.py --check-clean / --check-active every 60 seconds (wait each 60-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above) for up to 25 iterations, then re-fetch the review. If not active (exit 1), post the literal comment "bugbot run" (no @mention, no other text) via ${REVIEWER_GATE_SENTINEL}python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --body "bugbot run", wait 8 seconds inside this turn with the Monitor tool (per the WAITS AND POLLS rule above), then poll as above.\n` +
1124
1179
  `5. If after the full poll budget Bugbot has neither a check run nor a review on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} (treat as down).\n\n` +
1125
1180
  `Scope is the whole PR; you are only reading Bugbot's own output here. For each finding set category: 'code-standard' when it is a pure CODE_RULES/style violation (naming, comments, type hints, magic values, structure) with no behavioral impact; 'bug' otherwise. Return strictly the schema.`,
1126
1181
  { label: 'lens:bugbot', phase: 'Converge', schema: LENS_SCHEMA },
@@ -1314,7 +1369,7 @@ function runCopilotGate(head) {
1314
1369
  `You are the Copilot gate for ${prCoordinates}, HEAD ${head}. Do not edit code, commit, or push.\n\n` +
1315
1370
  `Copilot can run out of usage. When the newest Copilot review on HEAD carries an out-of-usage notice — a body stating Copilot was unable to review because the user who requested the review has reached their quota limit, or any equivalent quota / premium-request / usage-limit exhaustion message rather than an actual code review — Copilot is down for this run: return {sha:${'`'}${head}${'`'}, clean:true, down:true, findings:[]} and stop. Do NOT re-request a review, do NOT keep polling, and do NOT treat the notice as a finding.\n\n` +
1316
1371
  `1. Read any existing Copilot review on HEAD first: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}. This lists every Copilot review across all commits newest-first; only count entries whose commit_id starts with ${head}. If the newest such HEAD-scoped Copilot review is the out-of-usage notice above -> return the down result and stop. A notice on any earlier commit is NOT down: ignore it and continue. With no Copilot review on HEAD, skip a duplicate request: python "${CONFIG.sharedScripts}/check_pending_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --user copilot. Exit 0 means a request is already pending; otherwise request one:\n` +
1317
- ` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
1372
+ ` ${REVIEWER_GATE_SENTINEL}gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
1318
1373
  `2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (wait each 360-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above; if the attempt budget is spent with no review on HEAD, return down: true). Each attempt: python "${CONFIG.sharedScripts}/fetch_copilot_reviews.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} for the top-level review state, plus gh api "repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/comments" --paginate --slurp for inline comment ids (Copilot's login contains "copilot", case-insensitive). Only count entries whose commit_id starts with ${head}.\n` +
1319
1374
  ` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
1320
1375
  ` - Copilot review present on HEAD whose state is APPROVED, or COMMENTED with no inline findings -> a clean pass: return {sha:${'`'}${head}${'`'}, clean:true, down:false, findings:[]}.\n` +
@@ -1485,9 +1540,10 @@ function parseDeferredPr(prUrl) {
1485
1540
  * @param {Array<object>} findings deduped code-standard-only findings
1486
1541
  * @param {string} sourceLabel short description of where the findings came from
1487
1542
  * @param {boolean} hasHardeningPrAlreadyOpened true when an earlier round already opened the environment-hardening PR for this run, so the verify and commit steps are skipped and no second PR opens while the edit retries the issue filing
1488
- * @returns {Promise<object>} `{ followUpIssueFiled, issueUrl, hardeningPrOpened, deferredPr }` followUpIssueFiled true when the standards-edit step returned a non-empty issue URL, issueUrl that filed URL (empty string when the filing failed) so a later reuse-path round can reference it when resolving its own threads, hardeningPrOpened true when the hardening-commit step returned a non-empty hardeningPrUrl (a PR opened) so the run-once latch holds even when that URL does not parse into coordinates, and false when the commit step returned an empty URL (no PR opened) so a later round retries the open, and deferredPr the opened PR's `{owner, repo, prNumber}` (null when no PR was opened or the committed URL does not parse) so the self-closing orchestrator can converge it in turn
1543
+ * @param {{copilotDisabled: boolean, bugbotDisabled: boolean}} deferredReviewerFlags this run's latest resolved reviewer-down state, carried onto the deferred PR so a later generation converging it seeds the known-unavailable state instead of re-learning it
1544
+ * @returns {Promise<object>} `{ followUpIssueFiled, issueUrl, hardeningPrOpened, deferredPr }` — followUpIssueFiled true when the standards-edit step returned a non-empty issue URL, issueUrl that filed URL (empty string when the filing failed) so a later reuse-path round can reference it when resolving its own threads, hardeningPrOpened true when the hardening-commit step returned a non-empty hardeningPrUrl (a PR opened) so the run-once latch holds even when that URL does not parse into coordinates, and false when the commit step returned an empty URL (no PR opened) so a later round retries the open, and deferredPr the opened PR's `{owner, repo, prNumber, copilotDisabled, bugbotDisabled}` (null when no PR was opened or the committed URL does not parse) so the self-closing orchestrator can converge it in turn
1489
1545
  */
1490
- async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened) {
1546
+ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened, deferredReviewerFlags) {
1491
1547
  const editResult = await runCodeEditorTask('standards-edit', { head, findings, sourceLabel })
1492
1548
  const followUpIssueFiled = typeof editResult?.issueUrl === 'string' && editResult.issueUrl.length > 0
1493
1549
  const followUpIssueUrl = followUpIssueFiled ? editResult.issueUrl : ''
@@ -1506,7 +1562,11 @@ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningP
1506
1562
  const commitResult = await runCodeEditorTask('hardening-commit', {
1507
1563
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
1508
1564
  })
1509
- const deferredPr = parseDeferredPr(commitResult?.hardeningPrUrl)
1565
+ const parsedDeferredPr = parseDeferredPr(commitResult?.hardeningPrUrl)
1566
+ const deferredPr =
1567
+ parsedDeferredPr === null
1568
+ ? null
1569
+ : { ...parsedDeferredPr, copilotDisabled: deferredReviewerFlags.copilotDisabled, bugbotDisabled: deferredReviewerFlags.bugbotDisabled }
1510
1570
  const hardeningPrOpened = typeof commitResult?.hardeningPrUrl === 'string' && commitResult.hardeningPrUrl.length > 0
1511
1571
  return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened, deferredPr }
1512
1572
  }
@@ -1561,15 +1621,16 @@ async function resolveStandardsThreadsForBatch(head, findings, sourceLabel) {
1561
1621
  * @param {string} head PR HEAD SHA the findings were raised against
1562
1622
  * @param {Array<object>} findings deduped code-standard-only findings
1563
1623
  * @param {string} sourceLabel short description of where the findings came from
1564
- * @returns {Promise<object>} `{ hardeningPrOpened, deferredPr }` hardeningPrOpened true when a hardening PR was opened for this run, and deferredPr the opened PR's `{owner, repo, prNumber}` when this call opened it (null otherwise) so the self-closing orchestrator can converge it in turn
1624
+ * @param {{copilotDisabled: boolean, bugbotDisabled: boolean}} deferredReviewerFlags this run's latest resolved reviewer-down state, carried onto the deferred PR so a later generation converging it seeds the known-unavailable state instead of re-learning it
1625
+ * @returns {Promise<object>} `{ hardeningPrOpened, deferredPr }` — hardeningPrOpened true when a hardening PR was opened for this run, and deferredPr the opened PR's `{owner, repo, prNumber, copilotDisabled, bugbotDisabled}` when this call opened it (null otherwise) so the self-closing orchestrator can converge it in turn
1565
1626
  */
1566
- async function openStandardsFollowUpOnce(head, findings, sourceLabel) {
1627
+ async function openStandardsFollowUpOnce(head, findings, sourceLabel, deferredReviewerFlags) {
1567
1628
  if (!shouldOpenStandardsFollowUp(hasStandardsFollowUpFiled)) {
1568
1629
  log(`Standards deferral (${sourceLabel}): reusing the follow-up fix issue already filed for this run rather than filing a duplicate; environment-hardening PR ${wasStandardsHardeningPrOpened ? 'was opened for this run' : 'was not opened for this run'}`)
1569
1630
  await resolveStandardsThreadsForBatch(head, findings, sourceLabel)
1570
1631
  return { hardeningPrOpened: wasStandardsHardeningPrOpened, deferredPr: null }
1571
1632
  }
1572
- const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened)
1633
+ const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened, deferredReviewerFlags)
1573
1634
  hasStandardsFollowUpFiled = standardsOutcome?.followUpIssueFiled === true
1574
1635
  if (hasStandardsFollowUpFiled) {
1575
1636
  standardsFollowUpIssueUrl = standardsOutcome.issueUrl
@@ -1591,6 +1652,7 @@ let hasStandardsFollowUpFiled = false
1591
1652
  let wasStandardsHardeningPrOpened = false
1592
1653
  let standardsFollowUpIssueUrl = ''
1593
1654
  let reuseNote = null
1655
+ let reviewerAvailability = null
1594
1656
  const deferredPrs = []
1595
1657
 
1596
1658
  const preflightHead = await runGitTask('resolve-head')
@@ -1629,13 +1691,15 @@ while (iterations < CONFIG.maxIterations) {
1629
1691
  continue
1630
1692
  }
1631
1693
  await runGitTask('prefetch-main')
1694
+ reviewerAvailability = await runReviewerAvailabilityCheck()
1695
+ const isBugbotDownPreSpawn = resolveReviewerDown(reviewerAvailability?.bugbot, input.bugbotDisabled || false)
1632
1696
  log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
1633
1697
  const lenses = await parallel([
1634
- () => runBugbotLens(head),
1698
+ () => (isBugbotDownPreSpawn ? Promise.resolve({ sha: head, clean: true, down: true, findings: [] }) : runBugbotLens(head)),
1635
1699
  () => runCodeReviewLens(head),
1636
1700
  () => runAuditLens(head),
1637
1701
  ])
1638
- bugbotDown = resolveBugbotDown(lenses[0], input.bugbotDisabled || false)
1702
+ bugbotDown = lenses[0] == null ? true : resolveReviewerDown(lenses[0], input.bugbotDisabled || false)
1639
1703
  const roundOutcome = resolveRoundOutcome(lenses)
1640
1704
  if (roundOutcome.allLensesDead) {
1641
1705
  log(`Round ${rounds}: every lens agent died — retrying without posting a clean artifact`)
@@ -1644,7 +1708,7 @@ while (iterations < CONFIG.maxIterations) {
1644
1708
  const findings = roundOutcome.findings
1645
1709
  if (isStandardsOnlyRound(findings)) {
1646
1710
  log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
1647
- const standardsOutcome = await openStandardsFollowUpOnce(head, findings, 'converge-round')
1711
+ const standardsOutcome = await openStandardsFollowUpOnce(head, findings, 'converge-round', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
1648
1712
  standardsNote = standardsDeferralNote(findings.length, standardsOutcome.hardeningPrOpened)
1649
1713
  if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
1650
1714
  const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
@@ -1683,10 +1747,10 @@ while (iterations < CONFIG.maxIterations) {
1683
1747
  }
1684
1748
 
1685
1749
  if (phase === 'COPILOT') {
1686
- if (input.copilotDisabled) {
1750
+ if (resolveReviewerDown(reviewerAvailability?.copilot, input.copilotDisabled || false)) {
1687
1751
  copilotDown = true
1688
- copilotNote = 'Copilot was skipped by the quota pre-check (out of premium-request quota, unreachable, or unconfigured) — the Copilot gate was bypassed and the PR was marked ready without a Copilot review'
1689
- log('Copilot gate: the quota pre-check reported Copilot unavailable — skipping the Copilot gate with no agent spawned and proceeding to mark-ready with the gate bypassed.')
1752
+ copilotNote = 'Copilot was unavailable or out of premium-request quota this round — the Copilot gate was bypassed with no agent spawned and the PR was marked ready without a Copilot review'
1753
+ log('Copilot gate: the shared reviewer-availability probe (or the run input) reported Copilot unavailable — skipping the Copilot gate with no agent spawned and proceeding to mark-ready with the gate bypassed.')
1690
1754
  phase = 'FINALIZE'
1691
1755
  continue
1692
1756
  }
@@ -1708,7 +1772,7 @@ while (iterations < CONFIG.maxIterations) {
1708
1772
  if (copilotOutcome.kind === 'fix') {
1709
1773
  if (isStandardsOnlyRound(copilotOutcome.findings)) {
1710
1774
  log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
1711
- const standardsOutcome = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot')
1775
+ const standardsOutcome = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
1712
1776
  standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome.hardeningPrOpened)
1713
1777
  if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
1714
1778
  copilotDown = false
@@ -11,6 +11,11 @@ Search files instantly on Windows using the Everything command-line interface (e
11
11
 
12
12
  **Announce at start:** "I'm using the everything-search skill to find files."
13
13
 
14
+ ## Hard limits
15
+
16
+ - **Scope every search.** Pass a project path, an `ext:`/`dm:`/`size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds.
17
+ - **Fall back on failure.** When `es.exe` fails or returns nothing, try to start it. if that doesn't work, prompt the user to decide next steps: grep, debug further, etc.
18
+
14
19
  ## Instructions
15
20
 
16
21
  ### Command Syntax