claude-dev-env 1.81.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 +3 -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/conventional_pr_title_gate.py +444 -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_conventional_pr_title_gate.py +640 -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 +10 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -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/conventional_pr_title_gate_constants.py +58 -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 +96 -5
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/workflow/converge.contract.test.mjs +228 -27
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +154 -21
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +153 -49
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
- package/skills/everything-search/SKILL.md +5 -0
|
@@ -29,13 +29,19 @@ 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' +
|
|
35
37
|
'- When a commit message, or a PR / issue / review-comment body, must describe destructive-command behavior, write that text to a file and pass it by path (git commit -F <file>, gh ... --body-file <file>); never inline it with git commit -m or gh ... -b, where the literal lands in the Bash command and stalls you.\n' +
|
|
36
38
|
'- Keep scratch files and cleanup inside the OS temp dir; never target a repository or worktree path.\n' +
|
|
37
39
|
'- rm shape rules — the hook grants several rm auto-allow paths. The simplest one accepts a standalone Bash call whose target resolves inside the ephemeral namespace (/tmp, /temp, the OS temp root, or the run worktree); a compound path accepts an rm joined with benign reporting segments when every rm target is an absolute ephemeral path. Both of those paths fail closed on $(...) command substitution and on backtick subshells. The compound path additionally fails closed on any $ in the target — including $CLAUDE_JOB_DIR. The standalone path declines a $-bearing target only when the literal path is not already under an ephemeral root, so it does not by itself stop a $VAR that expands inside an ephemeral root. A third, broad path matches only when the command itself declares an ephemeral working directory (it cds into one, or runs under one): that cwd-scoped path resolves the target against the declared cwd, fails closed on $(...) , backticks, and unknown variables, and resolves the known temporary variables TEMP, TMP, TMPDIR, and CLAUDE_JOB_DIR to the OS temp root, so under that declared ephemeral cwd a bare $CLAUDE_JOB_DIR/tmp/<name> target and a relative target after a cd are auto-allowed. Even so, prefer a Python helper for any cleanup whose path is variable-built or whose setup/teardown spans multiple steps: author the helper file and run it as python <file>.py, which keeps every destructive literal out of a Bash command string entirely and never depends on which auto-allow path matches.\n' +
|
|
38
|
-
'- If a step appears to require a real destructive command, use a non-destructive equivalent or report it as a blocker instead of running it.\n\n'
|
|
40
|
+
'- If a step appears to require a real destructive command, use a non-destructive equivalent or report it as a blocker instead of running it.\n\n' +
|
|
41
|
+
'WAITS AND POLLS — foreground sleep is blocked in this headless harness: a bare Bash "sleep N" or a PowerShell "Start-Sleep" is denied, and a wait you move to a background process — then end your turn to await it — never resumes, because a schema-bearing agent runs for a single turn. Therefore:\n' +
|
|
42
|
+
'- Perform every required delay or poll-interval wait inside this same turn by pairing the Monitor tool with a bounded until-loop: the Monitor tool streams its events to you while you keep working, and the until-loop re-runs the step condition on the interval the step names, up to the attempt budget the step names, exiting the moment the condition holds or the budget is spent — consume the Monitor notifications as they arrive rather than ending your turn to await them. Never end your turn to wait for something to finish.\n' +
|
|
43
|
+
'- Size the Monitor to the whole wait: its timeout_ms defaults to 300000 (300 seconds) and the Monitor is killed the moment that elapses, so a poll whose interval or whose interval-times-attempts span runs longer registers a false time-out. Before you arm a Monitor, set timeout_ms to at least the poll interval times the attempt count the step names — up to the 3600000ms ceiling — or pass persistent: true so the Monitor is never timed out. A 360-second interval, or a many-minute total, exceeds the 300000 default, so arming the Monitor with the default truncates the wait; set timeout_ms to the step span instead.\n' +
|
|
44
|
+
'- When your run was given a result schema, your final action is always the StructuredOutput call. If the poll budget is spent before the awaited signal arrives, call StructuredOutput with the whole time-out result the step documents — for the Copilot gate, the full down result {sha, clean:false, down:true, findings:[]}, never a bare down flag — rather than ending the turn without a result.\n\n'
|
|
39
45
|
|
|
40
46
|
let activeRepoPath = null
|
|
41
47
|
|
|
@@ -102,7 +108,7 @@ function runGitTask(task, head) {
|
|
|
102
108
|
`Report whether ${prCoordinates} (HEAD ${head}) has merge conflicts with its base branch. Do not edit, commit, push, or rebase — read only.\n\n` +
|
|
103
109
|
`GitHub computes mergeability asynchronously, so .mergeable is null right after a push until it finishes. Poll until it resolves: run\n` +
|
|
104
110
|
` gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq '{mergeable: .mergeable, state: .mergeable_state}'\n` +
|
|
105
|
-
`up to 5 times, 5 seconds apart (
|
|
111
|
+
`up to 5 times, 5 seconds apart (wait each 5-second interval inside this turn with the Monitor tool, per the WAITS AND POLLS rule above), stopping as soon as mergeable is true or false.\n\n` +
|
|
106
112
|
`Return conflicting:true when mergeable is false or state is "dirty" (the branch conflicts with the base). Return conflicting:false when mergeable is true, or when mergeable stays null after the full poll budget — mergeability is unknown, so let the bug checks proceed rather than rebase on a guess.`,
|
|
107
113
|
{ label: 'git-utility', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },
|
|
108
114
|
)
|
|
@@ -205,7 +211,7 @@ async function fixerWithRecovery(head, findings, sourceLabel) {
|
|
|
205
211
|
* Each task carries its own edit instructions.
|
|
206
212
|
* @param {string} task the short task name
|
|
207
213
|
* @param {object} context task-specific context
|
|
208
|
-
* @returns {Promise<object|string>} the structured output, or the transcript string when a schema-less task ('
|
|
214
|
+
* @returns {Promise<object|string>} the structured output, or the transcript string when a schema-less task ('standards-resolve-threads') runs
|
|
209
215
|
*/
|
|
210
216
|
function runCodeEditorTask(task, context) {
|
|
211
217
|
const label = `code-editor:${task}`
|
|
@@ -312,9 +318,10 @@ function runCodeEditorTask(task, context) {
|
|
|
312
318
|
`You are the COMMIT step opening the environment-hardening PR (${context.sourceLabel}) for the change staged in ${context.hardeningRepoPath} on branch ${context.hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree. Do NOT touch the PR's own branch.\n\n` +
|
|
313
319
|
`Rules:\n` +
|
|
314
320
|
`- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Only commit and push what is already there.\n` +
|
|
315
|
-
`- In ${context.hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${context.hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${context.issueUrl || '(none)'} and states the PR hardens the environment so the deferred violation classes are blocked at Write/Edit time. Honor the gh-body-file rule: write a BOM-free temp file and pass --body-file.\n
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
`- In ${context.hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${context.hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${context.issueUrl || '(none)'} and states the PR hardens the environment so the deferred violation classes are blocked at Write/Edit time. Honor the gh-body-file rule: write a BOM-free temp file and pass --body-file.\n` +
|
|
322
|
+
`- Title the PR as a Conventional Commit — a type prefix (feat, fix, chore, docs, refactor, perf, ci, style, test, build, revert), an optional scope in parentheses, then a colon and a short summary, e.g. "feat(hooks): block the deferred violation class". The target repo's CI validates the PR title as a semantic commit and rejects a non-conforming title.\n\n` +
|
|
323
|
+
`Return the full https URL of the DRAFT hardening PR in hardeningPrUrl (empty string when no PR was opened) and a one-line summary.`,
|
|
324
|
+
{ label, phase: 'Converge', schema: HARDENING_COMMIT_SCHEMA, agentType: 'clean-coder' },
|
|
318
325
|
)
|
|
319
326
|
}
|
|
320
327
|
if (task === 'commit-recover') {
|
|
@@ -459,6 +466,27 @@ function runConvergenceCheck(context) {
|
|
|
459
466
|
)
|
|
460
467
|
}
|
|
461
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
|
+
|
|
462
490
|
const PRE_COMMIT_GATE_STEP =
|
|
463
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` +
|
|
464
492
|
` python "${CONFIG.prLoopScripts}/code_rules_gate.py" --repo-root "<that root>" --staged\n` +
|
|
@@ -504,6 +532,32 @@ const COPILOT_SCHEMA = {
|
|
|
504
532
|
required: ['sha', 'clean', 'down', 'findings'],
|
|
505
533
|
}
|
|
506
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
|
+
|
|
507
561
|
const HEAD_SCHEMA = {
|
|
508
562
|
type: 'object',
|
|
509
563
|
additionalProperties: false,
|
|
@@ -583,6 +637,16 @@ const STANDARDS_EDIT_SCHEMA = {
|
|
|
583
637
|
required: ['issueUrl', 'hardeningRepoPath', 'hardeningBranch', 'hardeningEdited', 'summary'],
|
|
584
638
|
}
|
|
585
639
|
|
|
640
|
+
const HARDENING_COMMIT_SCHEMA = {
|
|
641
|
+
type: 'object',
|
|
642
|
+
additionalProperties: false,
|
|
643
|
+
properties: {
|
|
644
|
+
hardeningPrUrl: { type: 'string', description: 'the full https URL of the DRAFT environment-hardening PR the commit step opened, or an empty string when no PR was opened' },
|
|
645
|
+
summary: { type: 'string' },
|
|
646
|
+
},
|
|
647
|
+
required: ['hardeningPrUrl', 'summary'],
|
|
648
|
+
}
|
|
649
|
+
|
|
586
650
|
/**
|
|
587
651
|
* Build the verdict-fence step instructions for a verify agent, binding the
|
|
588
652
|
* surface hash by branch name rather than by a self-resolved cwd. Resolving
|
|
@@ -734,19 +798,27 @@ function isMoreSevere(candidateSeverity, currentSeverity) {
|
|
|
734
798
|
}
|
|
735
799
|
|
|
736
800
|
/**
|
|
737
|
-
* Decide whether
|
|
738
|
-
*
|
|
739
|
-
* run
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
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
|
|
745
817
|
*/
|
|
746
|
-
function
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
749
|
-
return
|
|
818
|
+
function resolveReviewerDown(reviewerAvailabilityEntry, isReviewerDisabledByInput) {
|
|
819
|
+
if (isReviewerDisabledByInput) return true
|
|
820
|
+
if (reviewerAvailabilityEntry == null) return false
|
|
821
|
+
return reviewerAvailabilityEntry.down === true
|
|
750
822
|
}
|
|
751
823
|
|
|
752
824
|
/**
|
|
@@ -1000,9 +1072,10 @@ function classifyCopilotOutcome(copilot) {
|
|
|
1000
1072
|
|
|
1001
1073
|
/**
|
|
1002
1074
|
* Decide whether the Copilot review gate is bypassed for this COPILOT pass from
|
|
1003
|
-
* the gate outcome, mirroring
|
|
1004
|
-
* pass rather than left sticky. Only
|
|
1005
|
-
* 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
|
|
1006
1079
|
* 'approved', 'fix', or 'retry' outcome means Copilot answered this pass, so the
|
|
1007
1080
|
* gate must be evaluated against its review and is never bypassed. Recomputing
|
|
1008
1081
|
* from the current outcome is what lets a recovered Copilot — one that returns
|
|
@@ -1090,9 +1163,6 @@ const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNum
|
|
|
1090
1163
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
1091
1164
|
*/
|
|
1092
1165
|
function runBugbotLens(head) {
|
|
1093
|
-
if (input.bugbotDisabled) {
|
|
1094
|
-
return Promise.resolve({ sha: head, clean: true, down: true, findings: [] })
|
|
1095
|
-
}
|
|
1096
1166
|
return convergeAgent(
|
|
1097
1167
|
`You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
|
|
1098
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` +
|
|
@@ -1105,7 +1175,7 @@ function runBugbotLens(head) {
|
|
|
1105
1175
|
` Only count entries whose commit_id starts with ${head}.\n` +
|
|
1106
1176
|
` - If findings exist on HEAD -> return them (each with its inline comment id in replyToCommentId when present, else null).\n` +
|
|
1107
1177
|
` - If a clean review exists on HEAD -> return clean.\n` +
|
|
1108
|
-
`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 (
|
|
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` +
|
|
1109
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` +
|
|
1110
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.`,
|
|
1111
1181
|
{ label: 'lens:bugbot', phase: 'Converge', schema: LENS_SCHEMA },
|
|
@@ -1299,8 +1369,8 @@ function runCopilotGate(head) {
|
|
|
1299
1369
|
`You are the Copilot gate for ${prCoordinates}, HEAD ${head}. Do not edit code, commit, or push.\n\n` +
|
|
1300
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` +
|
|
1301
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` +
|
|
1302
|
-
` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
|
|
1303
|
-
`2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (
|
|
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` +
|
|
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` +
|
|
1304
1374
|
` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
|
|
1305
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` +
|
|
1306
1376
|
` - Copilot findings on HEAD -> return them (each with its inline comment id in replyToCommentId; category 'code-standard' for pure CODE_RULES/style violations with no behavioral impact, 'bug' otherwise), clean:false, down:false.\n` +
|
|
@@ -1436,6 +1506,25 @@ function standardsDeferralNote(findingsCount, wasHardeningPrOpened) {
|
|
|
1436
1506
|
: `${base} — verify it lands (no environment-hardening PR was opened for this run)`
|
|
1437
1507
|
}
|
|
1438
1508
|
|
|
1509
|
+
/**
|
|
1510
|
+
* Parse a GitHub pull-request URL into the owner, repo, and number a recursive
|
|
1511
|
+
* converge run needs to address it.
|
|
1512
|
+
*
|
|
1513
|
+
* A hardening PR the commit step opens returns its URL as
|
|
1514
|
+
* `https://github.com/<owner>/<repo>/pull/<number>`; this reads those three
|
|
1515
|
+
* coordinates back out so the self-closing orchestrator can converge the
|
|
1516
|
+
* deferred PR in turn. A blank or non-matching string yields null, so a commit
|
|
1517
|
+
* step that opened no PR contributes no deferred coordinate.
|
|
1518
|
+
* @param {string} prUrl the hardening PR's https URL, or an empty string
|
|
1519
|
+
* @returns {{owner: string, repo: string, prNumber: number}|null} the parsed coordinates, or null when the URL does not match
|
|
1520
|
+
*/
|
|
1521
|
+
function parseDeferredPr(prUrl) {
|
|
1522
|
+
if (typeof prUrl !== 'string') return null
|
|
1523
|
+
const match = prUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?(?:[?#].*)?$/)
|
|
1524
|
+
if (!match) return null
|
|
1525
|
+
return { owner: match[1], repo: match[2], prNumber: Number(match[3]) }
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1439
1528
|
/**
|
|
1440
1529
|
* Defer a standards-only round: edit (clean-coder files the follow-up fix issue,
|
|
1441
1530
|
* stages an environment-hardening hooks/rules change in the config repo's
|
|
@@ -1451,28 +1540,35 @@ function standardsDeferralNote(findingsCount, wasHardeningPrOpened) {
|
|
|
1451
1540
|
* @param {Array<object>} findings deduped code-standard-only findings
|
|
1452
1541
|
* @param {string} sourceLabel short description of where the findings came from
|
|
1453
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
|
|
1454
|
-
* @
|
|
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
|
|
1455
1545
|
*/
|
|
1456
|
-
async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened) {
|
|
1546
|
+
async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningPrAlreadyOpened, deferredReviewerFlags) {
|
|
1457
1547
|
const editResult = await runCodeEditorTask('standards-edit', { head, findings, sourceLabel })
|
|
1458
1548
|
const followUpIssueFiled = typeof editResult?.issueUrl === 'string' && editResult.issueUrl.length > 0
|
|
1459
1549
|
const followUpIssueUrl = followUpIssueFiled ? editResult.issueUrl : ''
|
|
1460
1550
|
if (hasHardeningPrAlreadyOpened === true) {
|
|
1461
|
-
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
|
|
1551
|
+
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
|
|
1462
1552
|
}
|
|
1463
1553
|
if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
|
|
1464
|
-
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
|
|
1554
|
+
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
|
|
1465
1555
|
}
|
|
1466
1556
|
const verifyTranscript = await runVerifierTask('hardening-verify', {
|
|
1467
1557
|
head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch,
|
|
1468
1558
|
})
|
|
1469
1559
|
if (!verdictPassed(verifyTranscript)) {
|
|
1470
|
-
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false }
|
|
1560
|
+
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
|
|
1471
1561
|
}
|
|
1472
|
-
await runCodeEditorTask('hardening-commit', {
|
|
1562
|
+
const commitResult = await runCodeEditorTask('hardening-commit', {
|
|
1473
1563
|
head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch, issueUrl: editResult.issueUrl,
|
|
1474
1564
|
})
|
|
1475
|
-
|
|
1565
|
+
const parsedDeferredPr = parseDeferredPr(commitResult?.hardeningPrUrl)
|
|
1566
|
+
const deferredPr =
|
|
1567
|
+
parsedDeferredPr === null
|
|
1568
|
+
? null
|
|
1569
|
+
: { ...parsedDeferredPr, copilotDisabled: deferredReviewerFlags.copilotDisabled, bugbotDisabled: deferredReviewerFlags.bugbotDisabled }
|
|
1570
|
+
const hardeningPrOpened = typeof commitResult?.hardeningPrUrl === 'string' && commitResult.hardeningPrUrl.length > 0
|
|
1571
|
+
return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened, deferredPr }
|
|
1476
1572
|
}
|
|
1477
1573
|
|
|
1478
1574
|
/**
|
|
@@ -1525,21 +1621,22 @@ async function resolveStandardsThreadsForBatch(head, findings, sourceLabel) {
|
|
|
1525
1621
|
* @param {string} head PR HEAD SHA the findings were raised against
|
|
1526
1622
|
* @param {Array<object>} findings deduped code-standard-only findings
|
|
1527
1623
|
* @param {string} sourceLabel short description of where the findings came from
|
|
1528
|
-
* @
|
|
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
|
|
1529
1626
|
*/
|
|
1530
|
-
async function openStandardsFollowUpOnce(head, findings, sourceLabel) {
|
|
1627
|
+
async function openStandardsFollowUpOnce(head, findings, sourceLabel, deferredReviewerFlags) {
|
|
1531
1628
|
if (!shouldOpenStandardsFollowUp(hasStandardsFollowUpFiled)) {
|
|
1532
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'}`)
|
|
1533
1630
|
await resolveStandardsThreadsForBatch(head, findings, sourceLabel)
|
|
1534
|
-
return wasStandardsHardeningPrOpened
|
|
1631
|
+
return { hardeningPrOpened: wasStandardsHardeningPrOpened, deferredPr: null }
|
|
1535
1632
|
}
|
|
1536
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened)
|
|
1633
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, findings, sourceLabel, wasStandardsHardeningPrOpened, deferredReviewerFlags)
|
|
1537
1634
|
hasStandardsFollowUpFiled = standardsOutcome?.followUpIssueFiled === true
|
|
1538
1635
|
if (hasStandardsFollowUpFiled) {
|
|
1539
1636
|
standardsFollowUpIssueUrl = standardsOutcome.issueUrl
|
|
1540
1637
|
}
|
|
1541
1638
|
wasStandardsHardeningPrOpened = wasStandardsHardeningPrOpened || standardsOutcome?.hardeningPrOpened === true
|
|
1542
|
-
return wasStandardsHardeningPrOpened
|
|
1639
|
+
return { hardeningPrOpened: wasStandardsHardeningPrOpened, deferredPr: standardsOutcome?.deferredPr ?? null }
|
|
1543
1640
|
}
|
|
1544
1641
|
|
|
1545
1642
|
let phase = 'CONVERGE'
|
|
@@ -1555,6 +1652,8 @@ let hasStandardsFollowUpFiled = false
|
|
|
1555
1652
|
let wasStandardsHardeningPrOpened = false
|
|
1556
1653
|
let standardsFollowUpIssueUrl = ''
|
|
1557
1654
|
let reuseNote = null
|
|
1655
|
+
let reviewerAvailability = null
|
|
1656
|
+
const deferredPrs = []
|
|
1558
1657
|
|
|
1559
1658
|
const preflightHead = await runGitTask('resolve-head')
|
|
1560
1659
|
if (isResolvedHeadUsable(preflightHead?.sha)) {
|
|
@@ -1592,13 +1691,15 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1592
1691
|
continue
|
|
1593
1692
|
}
|
|
1594
1693
|
await runGitTask('prefetch-main')
|
|
1694
|
+
reviewerAvailability = await runReviewerAvailabilityCheck()
|
|
1695
|
+
const isBugbotDownPreSpawn = resolveReviewerDown(reviewerAvailability?.bugbot, input.bugbotDisabled || false)
|
|
1595
1696
|
log(`Round ${rounds}: parallel Bugbot + code-review + bug-audit on ${head?.slice(0, 7)}`)
|
|
1596
1697
|
const lenses = await parallel([
|
|
1597
|
-
() => runBugbotLens(head),
|
|
1698
|
+
() => (isBugbotDownPreSpawn ? Promise.resolve({ sha: head, clean: true, down: true, findings: [] }) : runBugbotLens(head)),
|
|
1598
1699
|
() => runCodeReviewLens(head),
|
|
1599
1700
|
() => runAuditLens(head),
|
|
1600
1701
|
])
|
|
1601
|
-
bugbotDown =
|
|
1702
|
+
bugbotDown = lenses[0] == null ? true : resolveReviewerDown(lenses[0], input.bugbotDisabled || false)
|
|
1602
1703
|
const roundOutcome = resolveRoundOutcome(lenses)
|
|
1603
1704
|
if (roundOutcome.allLensesDead) {
|
|
1604
1705
|
log(`Round ${rounds}: every lens agent died — retrying without posting a clean artifact`)
|
|
@@ -1607,8 +1708,9 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1607
1708
|
const findings = roundOutcome.findings
|
|
1608
1709
|
if (isStandardsOnlyRound(findings)) {
|
|
1609
1710
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
1610
|
-
const
|
|
1611
|
-
standardsNote = standardsDeferralNote(findings.length,
|
|
1711
|
+
const standardsOutcome = await openStandardsFollowUpOnce(head, findings, 'converge-round', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
|
|
1712
|
+
standardsNote = standardsDeferralNote(findings.length, standardsOutcome.hardeningPrOpened)
|
|
1713
|
+
if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
|
|
1612
1714
|
const auditResult = await runGeneralUtilityTask('post-clean-audit', { head })
|
|
1613
1715
|
if (!auditResult?.posted) {
|
|
1614
1716
|
blocker = cleanAuditBlocker(head, auditResult)
|
|
@@ -1645,10 +1747,10 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1645
1747
|
}
|
|
1646
1748
|
|
|
1647
1749
|
if (phase === 'COPILOT') {
|
|
1648
|
-
if (input.copilotDisabled) {
|
|
1750
|
+
if (resolveReviewerDown(reviewerAvailability?.copilot, input.copilotDisabled || false)) {
|
|
1649
1751
|
copilotDown = true
|
|
1650
|
-
copilotNote = 'Copilot was
|
|
1651
|
-
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.')
|
|
1652
1754
|
phase = 'FINALIZE'
|
|
1653
1755
|
continue
|
|
1654
1756
|
}
|
|
@@ -1670,8 +1772,9 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1670
1772
|
if (copilotOutcome.kind === 'fix') {
|
|
1671
1773
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
1672
1774
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
1673
|
-
const
|
|
1674
|
-
standardsNote = standardsDeferralNote(copilotOutcome.findings.length,
|
|
1775
|
+
const standardsOutcome = await openStandardsFollowUpOnce(head, copilotOutcome.findings, 'copilot', { copilotDisabled: copilotDown, bugbotDisabled: bugbotDown })
|
|
1776
|
+
standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome.hardeningPrOpened)
|
|
1777
|
+
if (standardsOutcome?.deferredPr) deferredPrs.push(standardsOutcome.deferredPr)
|
|
1675
1778
|
copilotDown = false
|
|
1676
1779
|
copilotNote = null
|
|
1677
1780
|
phase = 'FINALIZE'
|
|
@@ -1707,7 +1810,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1707
1810
|
const readyResult = await runGeneralUtilityTask('mark-ready', { head, copilotDown })
|
|
1708
1811
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
1709
1812
|
if (readyOutcome.converged) {
|
|
1710
|
-
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote }
|
|
1813
|
+
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote, reuseNote, deferredPrs }
|
|
1711
1814
|
}
|
|
1712
1815
|
blocker = readyOutcome.blocker
|
|
1713
1816
|
break
|
|
@@ -1727,4 +1830,5 @@ return {
|
|
|
1727
1830
|
standardsNote,
|
|
1728
1831
|
copilotNote,
|
|
1729
1832
|
reuseNote,
|
|
1833
|
+
deferredPrs,
|
|
1730
1834
|
}
|
|
@@ -126,7 +126,7 @@ function childRunInput(prEntry) {
|
|
|
126
126
|
|
|
127
127
|
const multiInput = classifyMultiInput(args)
|
|
128
128
|
if (multiInput.blocker) {
|
|
129
|
-
return { converged: false, prCount: 0, convergedCount: 0, results: [], blocker: multiInput.blocker }
|
|
129
|
+
return { converged: false, prCount: 0, convergedCount: 0, results: [], allDeferredPrs: [], blocker: multiInput.blocker }
|
|
130
130
|
}
|
|
131
131
|
const input = multiInput.input
|
|
132
132
|
|
|
@@ -147,6 +147,7 @@ const childResults = await parallel(
|
|
|
147
147
|
rounds: childOutcome && childOutcome.rounds !== undefined ? childOutcome.rounds : null,
|
|
148
148
|
finalSha: childOutcome && childOutcome.finalSha !== undefined ? childOutcome.finalSha : null,
|
|
149
149
|
blocker: childOutcome && childOutcome.blocker !== undefined ? childOutcome.blocker : null,
|
|
150
|
+
deferredPrs: childOutcome && Array.isArray(childOutcome.deferredPrs) ? childOutcome.deferredPrs : [],
|
|
150
151
|
}
|
|
151
152
|
}),
|
|
152
153
|
)
|
|
@@ -161,17 +162,20 @@ const results = childResults.map((eachResult, eachIndex) =>
|
|
|
161
162
|
rounds: null,
|
|
162
163
|
finalSha: null,
|
|
163
164
|
blocker: 'child run threw or was skipped before returning an outcome',
|
|
165
|
+
deferredPrs: [],
|
|
164
166
|
}
|
|
165
167
|
: eachResult,
|
|
166
168
|
)
|
|
167
169
|
|
|
168
170
|
const convergedCount = results.filter((eachResult) => eachResult.converged).length
|
|
169
|
-
|
|
171
|
+
const allDeferredPrs = results.flatMap((eachResult) => eachResult.deferredPrs)
|
|
172
|
+
log(`autoconverge multi-PR done: ${convergedCount}/${results.length} PR(s) converged, ${allDeferredPrs.length} deferred hardening PR(s) opened`)
|
|
170
173
|
|
|
171
174
|
return {
|
|
172
175
|
converged: convergedCount === results.length,
|
|
173
176
|
prCount: results.length,
|
|
174
177
|
convergedCount,
|
|
175
178
|
results,
|
|
179
|
+
allDeferredPrs,
|
|
176
180
|
blocker: null,
|
|
177
181
|
}
|
|
@@ -21,6 +21,19 @@ const productionModule = new Function(
|
|
|
21
21
|
)();
|
|
22
22
|
const { normalizeMultiInput, classifyMultiInput, childRunInput } = productionModule;
|
|
23
23
|
|
|
24
|
+
const AsyncFunction = async function () {}.constructor;
|
|
25
|
+
const workflowBodySource = multiSource.slice(multiSource.indexOf('function normalizeMultiInput('));
|
|
26
|
+
const runMultiWorkflowBody = new AsyncFunction(
|
|
27
|
+
'args',
|
|
28
|
+
'phase',
|
|
29
|
+
'log',
|
|
30
|
+
'parallel',
|
|
31
|
+
'workflow',
|
|
32
|
+
workflowBodySource,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
function ignoreCall() {}
|
|
36
|
+
|
|
24
37
|
const SCRIPT_PATH = '/abs/skills/autoconverge/workflow/converge.mjs';
|
|
25
38
|
|
|
26
39
|
function validEntry(prNumber) {
|
|
@@ -124,3 +137,16 @@ test('childRunInput forwards bugbotDisabled true when the entry opts out', () =>
|
|
|
124
137
|
test('childRunInput defaults bugbotDisabled to false when the entry omits it', () => {
|
|
125
138
|
assert.equal(childRunInput(validEntry(398)).bugbotDisabled, false);
|
|
126
139
|
});
|
|
140
|
+
|
|
141
|
+
test('the malformed-input blocker return carries an empty allDeferredPrs list', async () => {
|
|
142
|
+
const blockerOutcome = await runMultiWorkflowBody(
|
|
143
|
+
'not json at all',
|
|
144
|
+
ignoreCall,
|
|
145
|
+
ignoreCall,
|
|
146
|
+
ignoreCall,
|
|
147
|
+
ignoreCall,
|
|
148
|
+
);
|
|
149
|
+
assert.notEqual(blockerOutcome.blocker, null);
|
|
150
|
+
assert.equal(blockerOutcome.prCount, 0);
|
|
151
|
+
assert.deepEqual(blockerOutcome.allDeferredPrs, []);
|
|
152
|
+
});
|
|
@@ -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
|