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.
- package/CLAUDE.md +16 -13
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +5 -0
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +1 -1
- package/skills/autoconverge/SKILL.md +10 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
- package/skills/autoconverge/workflow/converge.mjs +97 -33
- 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
|
|
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 }`
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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(
|
|
312
|
-
|
|
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
|
|
753
|
-
*
|
|
754
|
-
* run
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
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
|
|
762
|
-
if (
|
|
763
|
-
if (
|
|
764
|
-
return
|
|
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
|
|
1019
|
-
* pass rather than left sticky. Only
|
|
1020
|
-
* unreachable after the poll cap)
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
* @
|
|
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 =
|
|
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
|
|
1689
|
-
log('Copilot gate: the
|
|
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
|