claude-dev-env 1.57.2 → 1.59.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 +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +317 -54
- package/bin/install.test.mjs +478 -3
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/intent_only_ending_blocker.py +155 -0
- package/hooks/blocking/session_handoff_blocker.py +190 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
- package/hooks/blocking/test_session_handoff_blocker.py +312 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/messages.py +4 -0
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/hooks/workflow/auto_formatter.py +26 -1
- package/hooks/workflow/test_auto_formatter.py +134 -0
- package/package.json +1 -1
- package/rules/conservative-action.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/long-horizon-autonomy.md +43 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +68 -6
- package/skills/autoconverge/reference/closing-report.md +44 -0
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -38
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
- package/skills/autoconverge/workflow/render_report.py +903 -0
- package/skills/autoconverge/workflow/test_render_report.py +484 -0
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -15,7 +15,7 @@ export const meta = {
|
|
|
15
15
|
whenToUse: 'Launched by the /autoconverge skill after it resolves PR scope, enters a worktree, and grants project .claude permissions.',
|
|
16
16
|
phases: [
|
|
17
17
|
{ title: 'Converge', detail: 'Bugbot + code-review + bug-audit in parallel each round; one clean-coder applies all fixes; loop until all three are clean on a stable HEAD' },
|
|
18
|
-
{ title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge' },
|
|
18
|
+
{ title: 'Copilot gate', detail: 'Request Copilot review and poll up to three times; route findings back into Converge; when Copilot is down or out of quota, log a notice and mark the PR ready with the gate bypassed' },
|
|
19
19
|
{ title: 'Finalize', detail: 'Run check_convergence.py; mark draft=false on a full pass' },
|
|
20
20
|
],
|
|
21
21
|
}
|
|
@@ -28,6 +28,24 @@ const CONFIG = {
|
|
|
28
28
|
bugteamRubric: '$HOME/.claude/skills/bugteam/reference/audit-contract.md',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const HEADLESS_SAFETY_PREAMBLE =
|
|
32
|
+
'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' +
|
|
33
|
+
'- 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' +
|
|
34
|
+
'- 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' +
|
|
35
|
+
'- Keep scratch files and cleanup inside the OS temp dir or $CLAUDE_JOB_DIR/tmp (auto-allowed as ephemeral); never target a repository or worktree path with rm -rf.\n' +
|
|
36
|
+
'- 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'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Spawn a workflow agent with the headless-safety preamble prepended to its
|
|
40
|
+
* prompt. Every agent in this convergence loop runs unattended, so each one is
|
|
41
|
+
* routed through here to inherit the same no-confirmation-prompt guidance.
|
|
42
|
+
* @param {string} prompt the agent's role-specific instruction body
|
|
43
|
+
* @param {object} options the agent() options (label, phase, schema, agentType, model)
|
|
44
|
+
* @returns {Promise<*>} the agent() result
|
|
45
|
+
*/
|
|
46
|
+
const convergeAgent = (prompt, options) =>
|
|
47
|
+
agent(`${HEADLESS_SAFETY_PREAMBLE}${prompt}`, options)
|
|
48
|
+
|
|
31
49
|
const LENS_SCHEMA = {
|
|
32
50
|
type: 'object',
|
|
33
51
|
additionalProperties: false,
|
|
@@ -62,10 +80,10 @@ const COPILOT_SCHEMA = {
|
|
|
62
80
|
properties: {
|
|
63
81
|
sha: { type: 'string' },
|
|
64
82
|
clean: { type: 'boolean' },
|
|
83
|
+
down: { type: 'boolean', description: 'true when Copilot is down or out of quota — it posts an out-of-usage notice or never surfaces a review on HEAD after the poll cap; the gate is bypassed and the run proceeds to mark-ready' },
|
|
65
84
|
findings: LENS_SCHEMA.properties.findings,
|
|
66
|
-
blocker: { type: ['string', 'null'], description: 'non-null when Copilot never surfaced a review after the poll cap' },
|
|
67
85
|
},
|
|
68
|
-
required: ['sha', 'clean', '
|
|
86
|
+
required: ['sha', 'clean', 'down', 'findings'],
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
const HEAD_SCHEMA = {
|
|
@@ -324,22 +342,44 @@ function classifyReadyOutcome(readyResult) {
|
|
|
324
342
|
* Classify a Copilot gate result into the loop's next action. A dead gate agent
|
|
325
343
|
* (null result) is a retry rather than an approval, mirroring the converge
|
|
326
344
|
* lenses' dead-agent convention so a failed gate is never mistaken for a clean
|
|
327
|
-
* Copilot review. A
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
345
|
+
* Copilot review. A down result — Copilot out of quota or unreachable, so it
|
|
346
|
+
* posts an out-of-usage notice or never surfaces a review after the poll cap —
|
|
347
|
+
* routes to the 'down' kind, which logs a notice and proceeds to mark-ready with
|
|
348
|
+
* the Copilot gate bypassed, the same way a down Bugbot lens is bypassed; this is
|
|
349
|
+
* checked first so an outage proceeds rather than waiting on a review that will
|
|
350
|
+
* not arrive. Findings route to a fix step. The gate otherwise approves only when
|
|
351
|
+
* it explicitly reports clean:true with no findings — a clean:false result with
|
|
352
|
+
* zero findings is an unreliable or malformed gate response and retries rather
|
|
353
|
+
* than advancing to Finalize, so a PR never goes ready on a HEAD Copilot did not
|
|
354
|
+
* call clean.
|
|
332
355
|
* @param {object|null|undefined} copilot the Copilot gate result, or null on agent failure
|
|
333
|
-
* @returns {{kind: string,
|
|
356
|
+
* @returns {{kind: string, findings?: Array<object>}} the next action
|
|
334
357
|
*/
|
|
335
358
|
function classifyCopilotOutcome(copilot) {
|
|
336
359
|
if (copilot == null) return { kind: 'retry' }
|
|
337
|
-
if (copilot.
|
|
360
|
+
if (copilot.down === true) return { kind: 'down' }
|
|
338
361
|
if (copilot.findings.length > 0) return { kind: 'fix', findings: copilot.findings }
|
|
339
362
|
if (copilot.clean === true) return { kind: 'approved' }
|
|
340
363
|
return { kind: 'retry' }
|
|
341
364
|
}
|
|
342
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Decide whether the Copilot review gate is bypassed for this COPILOT pass from
|
|
368
|
+
* the gate outcome, mirroring resolveBugbotDown so the flag is recomputed every
|
|
369
|
+
* pass rather than left sticky. Only a 'down' outcome (Copilot out of quota or
|
|
370
|
+
* unreachable after the poll cap) bypasses the convergence Copilot gate; an
|
|
371
|
+
* 'approved', 'fix', or 'retry' outcome means Copilot answered this pass, so the
|
|
372
|
+
* gate must be evaluated against its review and is never bypassed. Recomputing
|
|
373
|
+
* from the current outcome is what lets a recovered Copilot — one that returns
|
|
374
|
+
* standards-only findings after an earlier down pass — reach FINALIZE without
|
|
375
|
+
* the stale bypass that would skip its non-clean review.
|
|
376
|
+
* @param {{kind: string}} copilotOutcome a classifyCopilotOutcome result
|
|
377
|
+
* @returns {boolean} true only when this pass's Copilot gate is bypassed
|
|
378
|
+
*/
|
|
379
|
+
function resolveCopilotDown(copilotOutcome) {
|
|
380
|
+
return copilotOutcome.kind === 'down'
|
|
381
|
+
}
|
|
382
|
+
|
|
343
383
|
/**
|
|
344
384
|
* Classify a convergence-check result into the loop's next action. A dead check
|
|
345
385
|
* agent (null/undefined result) is a retry rather than a failure: with no FAIL
|
|
@@ -412,7 +452,7 @@ const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNum
|
|
|
412
452
|
* @returns {Promise<string>} the 40-char HEAD SHA
|
|
413
453
|
*/
|
|
414
454
|
async function resolveHead() {
|
|
415
|
-
const head = await
|
|
455
|
+
const head = await convergeAgent(
|
|
416
456
|
`Print the current HEAD SHA of ${prCoordinates}. Run exactly:\n` +
|
|
417
457
|
`gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .head.sha\n` +
|
|
418
458
|
`Return the full 40-character SHA in the sha field. Do not modify any files.`,
|
|
@@ -430,7 +470,7 @@ async function resolveHead() {
|
|
|
430
470
|
* @returns {Promise<string>} agent transcript (unused)
|
|
431
471
|
*/
|
|
432
472
|
function prefetchMainForRound() {
|
|
433
|
-
return
|
|
473
|
+
return convergeAgent(
|
|
434
474
|
`Refresh the base ref for ${prCoordinates} so the parallel review lenses can diff against an up-to-date origin/main without each running its own fetch. Run exactly:\n` +
|
|
435
475
|
`git fetch origin main\n` +
|
|
436
476
|
`Do not edit, commit, push, rebase, or modify any files — fetch only.`,
|
|
@@ -448,7 +488,7 @@ function runBugbotLens(head) {
|
|
|
448
488
|
if (input.bugbotDisabled) {
|
|
449
489
|
return Promise.resolve({ sha: head, clean: true, down: true, findings: [] })
|
|
450
490
|
}
|
|
451
|
-
return
|
|
491
|
+
return convergeAgent(
|
|
452
492
|
`You are the Cursor Bugbot lens for ${prCoordinates}, HEAD ${head}. Cursor Bugbot participates this run.\n\n` +
|
|
453
493
|
`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` +
|
|
454
494
|
`Procedure (use the existing scripts; each step below shows the exact flags that script accepts):\n` +
|
|
@@ -474,7 +514,7 @@ function runBugbotLens(head) {
|
|
|
474
514
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
475
515
|
*/
|
|
476
516
|
function runCodeReviewLens(head) {
|
|
477
|
-
return
|
|
517
|
+
return convergeAgent(
|
|
478
518
|
`You are the code-review lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
479
519
|
`Review the FULL origin/main...HEAD diff — every file the PR touches. Do NOT delta-scope to recent commits or to a single file. The workflow already fetched origin/main this round, so do NOT run git fetch; run git diff --name-only origin/main...HEAD to enumerate the changed files, then review the complete diff of each.\n\n` +
|
|
480
520
|
`Apply correctness-focused review: real bugs, broken logic, incorrect error handling, data-loss or security risks, contract mismatches, and reuse/simplification problems. Report only defensible findings with concrete file:line evidence.\n\n` +
|
|
@@ -490,7 +530,7 @@ function runCodeReviewLens(head) {
|
|
|
490
530
|
* @returns {Promise<object>} LENS_SCHEMA result
|
|
491
531
|
*/
|
|
492
532
|
function runAuditLens(head) {
|
|
493
|
-
return
|
|
533
|
+
return convergeAgent(
|
|
494
534
|
`You are the second-opinion bug-audit lens for ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
495
535
|
`Read the audit rubric at ${CONFIG.bugteamRubric} and apply its categories (A through P) against the FULL origin/main...HEAD diff — every file the PR touches, never a delta cut. The workflow already fetched origin/main this round, so do NOT run git fetch; run git diff --name-only origin/main...HEAD first to enumerate scope.\n\n` +
|
|
496
536
|
`This is a clean-room audit: assume nothing from other lenses. Report only findings backed by concrete file:line evidence. Do NOT edit, commit, or push.\n\n` +
|
|
@@ -520,7 +560,7 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
520
560
|
const threadIds = findings
|
|
521
561
|
.flatMap((each) => collectFindingThreadIds(each))
|
|
522
562
|
.filter((each) => typeof each === 'number')
|
|
523
|
-
return
|
|
563
|
+
return convergeAgent(
|
|
524
564
|
`You are fixing ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}.\n\n` +
|
|
525
565
|
`Findings:\n${findingsBlock}\n\n` +
|
|
526
566
|
`Rules:\n` +
|
|
@@ -544,7 +584,7 @@ function applyFixes(head, findings, sourceLabel) {
|
|
|
544
584
|
* @returns {Promise<string>} agent transcript (unused)
|
|
545
585
|
*/
|
|
546
586
|
function postCleanAudit(head) {
|
|
547
|
-
return
|
|
587
|
+
return convergeAgent(
|
|
548
588
|
`Post a CLEAN bugteam audit review on ${prCoordinates} at commit ${head}. All review lenses are clean on this HEAD.\n\n` +
|
|
549
589
|
`Write an empty findings file: create a temp file containing exactly [] (an empty JSON array). Then run:\n` +
|
|
550
590
|
`python "${CONFIG.prLoopScripts}/post_audit_thread.py" --skill bugteam --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --commit ${head} --state CLEAN --findings-json <temp-file>\n` +
|
|
@@ -555,19 +595,25 @@ function postCleanAudit(head) {
|
|
|
555
595
|
|
|
556
596
|
/**
|
|
557
597
|
* Copilot gate: request a Copilot review on HEAD and poll until it lands or the
|
|
558
|
-
* poll cap is hit; return Copilot's findings or a
|
|
598
|
+
* poll cap is hit; return Copilot's findings or a down signal. Copilot is down
|
|
599
|
+
* when it posts an out-of-usage notice (the requester hit their quota) rather
|
|
600
|
+
* than a review, or surfaces no review at all after the poll cap; the gate
|
|
601
|
+
* reports either as down so the run logs a notice and proceeds to mark-ready with
|
|
602
|
+
* the gate bypassed rather than waiting on a review that will not arrive.
|
|
559
603
|
* @param {string} head converged PR HEAD SHA
|
|
560
604
|
* @returns {Promise<object>} COPILOT_SCHEMA result
|
|
561
605
|
*/
|
|
562
606
|
function runCopilotGate(head) {
|
|
563
|
-
return
|
|
607
|
+
return convergeAgent(
|
|
564
608
|
`You are the Copilot gate for ${prCoordinates}, HEAD ${head}. Do not edit code, commit, or push.\n\n` +
|
|
565
|
-
`
|
|
609
|
+
`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` +
|
|
610
|
+
`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` +
|
|
566
611
|
` gh api --method POST repos/${input.owner}/${input.repo}/pulls/${input.prNumber}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n` +
|
|
567
612
|
`2. Poll for Copilot's review on HEAD ${head}: up to ${CONFIG.copilotMaxPolls} attempts, 360 seconds apart (delay each attempt with "sleep 360", or the PowerShell alternative "Start-Sleep -Seconds 360"). 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` +
|
|
568
|
-
` -
|
|
569
|
-
` - Copilot
|
|
570
|
-
` -
|
|
613
|
+
` - Out-of-usage notice on HEAD -> return the down result above (clean:true, down:true) and stop.\n` +
|
|
614
|
+
` - Copilot review present and clean/approved on HEAD -> return {sha:${'`'}${head}${'`'}, clean:true, down:false, findings:[]}.\n` +
|
|
615
|
+
` - 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` +
|
|
616
|
+
` - No review after ${CONFIG.copilotMaxPolls} attempts -> Copilot is down for this run (unreachable, or silently out of quota with no notice): return {sha:${'`'}${head}${'`'}, clean:false, down:true, findings:[]}.\n\n` +
|
|
571
617
|
`Return strictly the schema.`,
|
|
572
618
|
{ label: 'copilot-gate', phase: 'Copilot gate', schema: COPILOT_SCHEMA },
|
|
573
619
|
)
|
|
@@ -576,13 +622,15 @@ function runCopilotGate(head) {
|
|
|
576
622
|
/**
|
|
577
623
|
* Run the authoritative convergence gate.
|
|
578
624
|
* @param {boolean} bugbotDown pass --bugbot-down when Bugbot is opted out or proved unreachable this run
|
|
625
|
+
* @param {boolean} copilotDown pass --copilot-down when Copilot is down or out of quota this run
|
|
579
626
|
* @returns {Promise<object>} CONVERGENCE_SCHEMA result
|
|
580
627
|
*/
|
|
581
|
-
function checkConvergence(bugbotDown) {
|
|
628
|
+
function checkConvergence(bugbotDown, copilotDown) {
|
|
582
629
|
const bugbotDownFlag = bugbotDown ? ' --bugbot-down' : ''
|
|
583
|
-
|
|
630
|
+
const copilotDownFlag = copilotDown ? ' --copilot-down' : ''
|
|
631
|
+
return convergeAgent(
|
|
584
632
|
`Run the convergence gate for ${prCoordinates} and report the result. Do not edit code.\n\n` +
|
|
585
|
-
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}\n\n` +
|
|
633
|
+
`Run: python "${CONFIG.sharedScripts}/check_convergence.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber}${bugbotDownFlag}${copilotDownFlag}\n\n` +
|
|
586
634
|
`Exit 0 -> every gate passed: return {pass:true, failures:[]}.\n` +
|
|
587
635
|
`Exit 1 -> return {pass:false, failures:[<each printed FAIL line verbatim>]}.\n` +
|
|
588
636
|
`Exit 2 -> retry once; if it still errors, return {pass:false, failures:["check_convergence gh error"]}.`,
|
|
@@ -592,12 +640,22 @@ function checkConvergence(bugbotDown) {
|
|
|
592
640
|
|
|
593
641
|
/**
|
|
594
642
|
* Mark the PR ready for review (draft=false) and confirm the transition landed.
|
|
643
|
+
* When Copilot is down this run, the mark-ready agent first opts the
|
|
644
|
+
* independent mark-ready blocker hook out of the Copilot gate by exporting
|
|
645
|
+
* the Copilot token into CLAUDE_REVIEWS_DISABLED: that hook re-runs
|
|
646
|
+
* check_convergence.py without --copilot-down, so the env token is the only
|
|
647
|
+
* channel a genuine Copilot outage has to pass its Copilot review gate.
|
|
595
648
|
* @param {string} head converged PR HEAD SHA
|
|
649
|
+
* @param {boolean} copilotDown true when the Copilot gate was bypassed for an outage this run
|
|
596
650
|
* @returns {Promise<object>} READY_SCHEMA result
|
|
597
651
|
*/
|
|
598
|
-
function markReady(head) {
|
|
599
|
-
|
|
652
|
+
function markReady(head, copilotDown) {
|
|
653
|
+
const copilotOptOut = copilotDown
|
|
654
|
+
? `0. Copilot is down this run, so opt the independent mark-ready blocker hook out of the Copilot gate before step 1. Export the token in the same shell session as step 1 so the hook's convergence re-check inherits it:\n bash: export CLAUDE_REVIEWS_DISABLED="copilot" (PowerShell: $env:CLAUDE_REVIEWS_DISABLED = "copilot")\n`
|
|
655
|
+
: ''
|
|
656
|
+
return convergeAgent(
|
|
600
657
|
`All convergence gates pass for ${prCoordinates} on HEAD ${head}. Mark the PR ready, then confirm it left draft state. Do not edit code.\n\n` +
|
|
658
|
+
copilotOptOut +
|
|
601
659
|
`1. Run: gh pr ready ${input.prNumber} --repo ${input.owner}/${input.repo}\n` +
|
|
602
660
|
`2. Re-query the draft state: gh api repos/${input.owner}/${input.repo}/pulls/${input.prNumber} --jq .draft\n` +
|
|
603
661
|
`Return {ready:true} only when step 2 prints false (the PR is no longer a draft). If step 1 errors or step 2 still prints true, return {ready:false}.`,
|
|
@@ -617,7 +675,7 @@ function repairConvergence(head, failures) {
|
|
|
617
675
|
const failureBlock = failures.length
|
|
618
676
|
? failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
619
677
|
: 'none reported'
|
|
620
|
-
return
|
|
678
|
+
return convergeAgent(
|
|
621
679
|
`The convergence check for ${prCoordinates} failed these gates on HEAD ${head}:\n${failureBlock}\n\n` +
|
|
622
680
|
`Address only the failing gates:\n` +
|
|
623
681
|
`- Unresolved bot review threads: fetch the threads where isResolved is false (gh api graphql, or the github MCP pull_request_read get_review_comments), then keep only the bot-authored ones — a thread whose root comment author login contains "cursor", "claude", or "copilot" (case-insensitive substring). Explicitly skip every human reviewer thread; the convergence gate counts only unresolved bot threads, so touching a human thread is out of scope. For each bot thread, verify the concern against current code; if it still applies, fix it test-first; either way post an inline reply and resolve the thread.\n` +
|
|
@@ -661,7 +719,7 @@ function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
|
661
719
|
return `${position + 1}. [${each.severity}] ${each.file}:${each.line} — ${each.title}\n ${each.detail}${threadNote}`
|
|
662
720
|
})
|
|
663
721
|
.join('\n')
|
|
664
|
-
return
|
|
722
|
+
return convergeAgent(
|
|
665
723
|
`A review round on ${prCoordinates}, HEAD ${head}, surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact). The convergence run treats the round as passed and defers these to follow-up work, which you now create. Do NOT commit or push to the PR's own branch.\n\n` +
|
|
666
724
|
`Findings:\n${findingsBlock}\n\n` +
|
|
667
725
|
`1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR.\n` +
|
|
@@ -678,6 +736,8 @@ let rounds = 0
|
|
|
678
736
|
let iterations = 0
|
|
679
737
|
let blocker = null
|
|
680
738
|
let bugbotDown = input.bugbotDisabled || false
|
|
739
|
+
let copilotDown = false
|
|
740
|
+
let copilotNote = null
|
|
681
741
|
let standardsNote = null
|
|
682
742
|
|
|
683
743
|
while (iterations < CONFIG.maxIterations) {
|
|
@@ -722,7 +782,6 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
722
782
|
: `fix lens landed no push for ${findings.length} finding(s) on HEAD ${head}`
|
|
723
783
|
break
|
|
724
784
|
}
|
|
725
|
-
head = fixProgress.newSha
|
|
726
785
|
continue
|
|
727
786
|
}
|
|
728
787
|
if (!roundOutcome.roundClean) {
|
|
@@ -738,19 +797,26 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
738
797
|
if (phase === 'COPILOT') {
|
|
739
798
|
const copilot = await runCopilotGate(head)
|
|
740
799
|
const copilotOutcome = classifyCopilotOutcome(copilot)
|
|
800
|
+
copilotDown = resolveCopilotDown(copilotOutcome)
|
|
801
|
+
copilotNote = null
|
|
741
802
|
if (copilotOutcome.kind === 'retry') {
|
|
742
803
|
log('Copilot gate agent died or returned an unreliable not-clean result with no findings — re-running the gate on the same HEAD')
|
|
743
804
|
continue
|
|
744
805
|
}
|
|
745
|
-
if (copilotOutcome.kind === '
|
|
746
|
-
|
|
747
|
-
|
|
806
|
+
if (copilotOutcome.kind === 'down') {
|
|
807
|
+
log('Copilot gate: Copilot is down or out of quota — no review on HEAD after the poll cap. Logging a notice and proceeding to mark-ready with the Copilot gate bypassed.')
|
|
808
|
+
copilotDown = true
|
|
809
|
+
copilotNote = 'Copilot was down or out of quota — the Copilot gate was bypassed and the PR was marked ready without a Copilot review'
|
|
810
|
+
phase = 'FINALIZE'
|
|
811
|
+
continue
|
|
748
812
|
}
|
|
749
813
|
if (copilotOutcome.kind === 'fix') {
|
|
750
814
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
751
815
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
752
816
|
await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
753
817
|
standardsNote = `${copilotOutcome.findings.length} code-standard finding(s) deferred to a follow-up fix issue plus an environment-hardening PR — verify both land`
|
|
818
|
+
copilotDown = false
|
|
819
|
+
copilotNote = null
|
|
754
820
|
phase = 'FINALIZE'
|
|
755
821
|
continue
|
|
756
822
|
}
|
|
@@ -764,26 +830,27 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
764
830
|
: `copilot fix lens landed no push for ${copilotOutcome.findings.length} finding(s) on HEAD ${head}`
|
|
765
831
|
break
|
|
766
832
|
}
|
|
767
|
-
head = fixProgress.newSha
|
|
768
833
|
phase = 'CONVERGE'
|
|
769
834
|
continue
|
|
770
835
|
}
|
|
836
|
+
copilotDown = false
|
|
837
|
+
copilotNote = null
|
|
771
838
|
phase = 'FINALIZE'
|
|
772
839
|
continue
|
|
773
840
|
}
|
|
774
841
|
|
|
775
842
|
if (phase === 'FINALIZE') {
|
|
776
|
-
const convergence = await checkConvergence(bugbotDown)
|
|
843
|
+
const convergence = await checkConvergence(bugbotDown, copilotDown)
|
|
777
844
|
const convergenceOutcome = classifyConvergenceOutcome(convergence)
|
|
778
845
|
if (convergenceOutcome.kind === 'retry') {
|
|
779
846
|
log('Convergence check agent died or returned no FAIL lines — re-running the check on the same HEAD')
|
|
780
847
|
continue
|
|
781
848
|
}
|
|
782
849
|
if (convergenceOutcome.kind === 'ready') {
|
|
783
|
-
const readyResult = await markReady(head)
|
|
850
|
+
const readyResult = await markReady(head, copilotDown)
|
|
784
851
|
const readyOutcome = classifyReadyOutcome(readyResult)
|
|
785
852
|
if (readyOutcome.converged) {
|
|
786
|
-
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote }
|
|
853
|
+
return { converged: true, rounds, finalSha: head, blocker: null, standardsNote, copilotNote }
|
|
787
854
|
}
|
|
788
855
|
blocker = readyOutcome.blocker
|
|
789
856
|
break
|
|
@@ -801,4 +868,5 @@ return {
|
|
|
801
868
|
finalSha: head,
|
|
802
869
|
blocker: blocker || `iteration cap reached (${CONFIG.maxIterations})`,
|
|
803
870
|
standardsNote,
|
|
871
|
+
copilotNote,
|
|
804
872
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a11d903476b803493-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a11d903476b803493-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a11d903476b803493", "name": "StructuredOutput", "input": {"sha": "7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4", "clean": false, "findings": [{"file": "src/exports/core/export_runner.py", "line": 96, "severity": "P2", "title": "New inline log string literal flagged by Magic-values rule", "detail": "Copilot (CodingGuidelineID 1000000, \"Magic values & configuration\"): the new log message at this line introduces a new string literal in production code. Prefer reusing an existing configured UI message (e.g. web_editor_config.ui_messages.msg_detected_version) instead of adding a new inline literal.", "replyToCommentId": 3407912224}, {"file": "src/web/services/os_generation_ssot.py", "line": 265, "severity": "P2", "title": "New inline warning string literal flagged by Magic-values rule", "detail": "Copilot (CodingGuidelineID 1000000, \"Magic values & configuration\"): read_current_version_name_or_none() adds a new inline warning string in production code. Per repo convention, extract this message into a shared config constant (in src/shared/web_automation/config/) so future callers reuse it consistently.", "replyToCommentId": 3407912234}], "blocker": null}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a26213978adeef6fb-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a26213978adeef6fb-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a26213978adeef6fb", "name": "StructuredOutput", "input": {"newSha": "620382f766c4731ba92eefeb30bdbb9d0bcd4c90", "pushed": true, "resolvedWithoutCommit": false, "summary": "Fixed all 11 Copilot findings in one commit (620382f7): renamed banned `result` to `skip_path` in test_resume_skip_export.py and added `-> None` return annotations to the test functions across test_resume_skip_export.py, test_stp_path_selection.py, test_version_export_guards.py, and test_import_isolation.py; all 9 affected tests pass; pushed to fix/example-feature, posted 11 inline replies, and all 11 review threads are resolved."}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a3def0d15ed9d9110-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a3def0d15ed9d9110-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a3def0d15ed9d9110", "name": "StructuredOutput", "input": {"sha": "6a5169ef635ddb841e6f0465a309691ff77f033e", "clean": false, "blocker": null, "findings": [{"file": "src/exports/tests/test_resume_skip_export.py", "line": 35, "severity": "P2", "title": "Banned identifier `result` in test", "detail": "Naming guideline (CodingGuidelineID 1000000): newly introduced identifier `result` is banned; rename to a descriptive name such as `existing_apk_path` or `skip_path`.", "replyToCommentId": 3407883689}, {"file": "src/exports/tests/test_resume_skip_export.py", "line": 47, "severity": "P2", "title": "Banned identifier `result` in test", "detail": "Naming guideline (CodingGuidelineID 1000000): newly introduced identifier `result` is banned; rename to a descriptive name such as `existing_apk_path` or `skip_path`.", "replyToCommentId": 3407883707}, {"file": "src/logging/tests/test_import_isolation.py", "line": 33, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883713}, {"file": "src/logging/tests/test_import_isolation.py", "line": 43, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883717}, {"file": "src/exports/tests/test_version_export_guards.py", "line": 37, "severity": "P2", "title": "Missing return type annotation on async test", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added async test is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883723}, {"file": "src/exports/tests/test_version_export_guards.py", "line": 74, "severity": "P2", "title": "Missing return type annotation on async test", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added async test is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883727}, {"file": "src/exports/tests/test_stp_path_selection.py", "line": 19, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883731}, {"file": "src/exports/tests/test_stp_path_selection.py", "line": 31, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883736}, {"file": "src/exports/tests/test_stp_path_selection.py", "line": 43, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883740}, {"file": "src/exports/tests/test_resume_skip_export.py", "line": 21, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883749}, {"file": "src/exports/tests/test_resume_skip_export.py", "line": 38, "severity": "P2", "title": "Missing return type annotation on test function", "detail": "Repository guideline (CodingGuidelineID 1000000 \u2014 Types): newly added test function is missing an explicit return type annotation. Add `-> None`.", "replyToCommentId": 3407883755}]}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a41f41b1b708ee3b7-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a41f41b1b708ee3b7-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a41f41b1b708ee3b7", "name": "StructuredOutput", "input": {"sha": "7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4", "clean": true, "findings": [], "blocker": null}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a758b880abecc3ff7-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a758b880abecc3ff7-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a758b880abecc3ff7", "name": "StructuredOutput", "input": {"newSha": "7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4", "pushed": false, "resolvedWithoutCommit": true, "summary": "Both Copilot magic-value findings were already fixed at HEAD 7c2f420c (export_runner.py uses web_editor_config.ui_messages.msg_detected_version; os_generation_ssot.py uses os_generation_ssot_constants.unavailable_warning_template); posted inline replies and resolved both review threads (PRRT_kwDORiaSZc6JUqSw, PRRT_kwDORiaSZc6JUqS6)."}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "a8897b89656b1bd16-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "a8897b89656b1bd16-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_a8897b89656b1bd16", "name": "StructuredOutput", "input": {"newSha": "7c2f420c4d5b7c83aa47f93d99a0f1420e3373c4", "pushed": true, "resolvedWithoutCommit": false, "summary": "Fixed both Copilot magic-value findings on PR #211: export_runner.py now reuses web_editor_config.ui_messages.msg_detected_version, and os_generation_ssot.py references a new os_generation_ssot_constants.unavailable_warning_template. Added a behavioral TDD test, committed (7c2f420c) and pushed; posted inline replies to both comments (3407912224, 3407912234) and resolved both review threads (PRRT_kwDORiaSZc6JUqSw, PRRT_kwDORiaSZc6JUqS6)."}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "abd463d744a1437bc-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "abd463d744a1437bc-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_abd463d744a1437bc", "name": "StructuredOutput", "input": {"sha": "6a5169ef635ddb841e6f0465a309691ff77f033e", "clean": true, "down": false, "findings": []}}]}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"type": "user", "uuid": "ad19d027ae8ee1816-u", "message": {"role": "user", "content": "Return the structured result."}}
|
|
2
|
+
{"type": "assistant", "uuid": "ad19d027ae8ee1816-a", "message": {"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_ad19d027ae8ee1816", "name": "StructuredOutput", "input": {"sha": "620382f766c4731ba92eefeb30bdbb9d0bcd4c90", "clean": false, "findings": [{"file": "src/exports/core/export_runner.py", "line": 94, "severity": "P2", "title": "New inline log string literal in production code", "detail": "Copilot (CodingGuidelineID 1000000, Magic values & configuration): the new log message at this line introduces a new string literal in production code. Prefer reusing an existing configured UI message (e.g., web_editor_config.ui_messages.msg_detected_version) instead of adding a new inline literal here.", "replyToCommentId": 3407912224}, {"file": "src/web/services/os_generation_ssot.py", "line": 266, "severity": "P2", "title": "New inline warning string literal in read_current_version_name_or_none()", "detail": "Copilot (CodingGuidelineID 1000000, Magic values & configuration): read_current_version_name_or_none() adds a new inline warning string in production code. Per repo convention, extract this message into a shared config constant (in src/shared/web_automation/config/) so future callers reuse it consistently.", "replyToCommentId": 3407912234}], "blocker": null}}]}}
|