claude-dev-env 1.79.0 → 1.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +3 -1
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
  3. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
  10. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
  12. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  13. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  14. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
  15. package/audit-rubrics/CLAUDE.md +3 -2
  16. package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
  17. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
  18. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
  19. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
  20. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
  21. package/audit-rubrics/prompts/CLAUDE.md +2 -1
  22. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
  23. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
  24. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
  25. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
  26. package/docs/CODE_RULES.md +2 -2
  27. package/hooks/blocking/CLAUDE.md +2 -0
  28. package/hooks/blocking/code_rules_annotations_length.py +59 -11
  29. package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
  30. package/hooks/blocking/code_rules_command_dispatch.py +140 -0
  31. package/hooks/blocking/code_rules_docstrings.py +93 -0
  32. package/hooks/blocking/code_rules_enforcer.py +58 -4
  33. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  34. package/hooks/blocking/code_rules_js_conventions.py +246 -0
  35. package/hooks/blocking/code_rules_naming_collection.py +5 -0
  36. package/hooks/blocking/code_rules_test_assertions.py +3 -0
  37. package/hooks/blocking/code_rules_unused_imports.py +7 -66
  38. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
  39. package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
  40. package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
  41. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
  42. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
  43. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
  45. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  46. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
  47. package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
  48. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  49. package/hooks/hooks_constants/CLAUDE.md +2 -0
  50. package/hooks/hooks_constants/blocking_check_limits.py +10 -0
  51. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  52. package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
  53. package/hooks/hooks_constants/js_conventions_constants.py +54 -0
  54. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  55. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  56. package/package.json +1 -1
  57. package/rules/docstring-prose-matches-implementation.md +3 -1
  58. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
  59. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
  60. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
  61. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
  62. package/skills/autoconverge/SKILL.md +28 -7
  63. package/skills/autoconverge/reference/convergence.md +3 -3
  64. package/skills/autoconverge/reference/stop-conditions.md +1 -1
  65. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  66. package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
  67. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
  68. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  69. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  70. package/skills/autoconverge/workflow/converge.mjs +235 -247
  71. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  72. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  73. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  74. package/skills/pr-converge/SKILL.md +28 -2
  75. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  76. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -3,7 +3,8 @@
3
3
  Builds <context> and <scope> from CLI args; <bug_categories>,
4
4
  <rubric_reference>, and <constraints> come from the shared constants in
5
5
  skills_pr_loop_constants; <comment_posting> and <output_format> are built
6
- inline.
6
+ inline. <pr_description> carries the PR body text read from --pr-body-file,
7
+ and stays empty when that argument is absent or the file cannot be read.
7
8
 
8
9
  Usage:
9
10
  python scripts/build_audit_prompt.py --owner jl-cmd --repo claude-code-config --pr-number 422 --loop 1 --head-ref feat/branch --base-ref main --worktree-path <PATH> --run-temp-dir <PATH>
@@ -28,6 +29,38 @@ from skills_pr_loop_constants.path_resolver_constants import (
28
29
  )
29
30
 
30
31
 
32
+ def read_pr_body_text(pr_body_file: Path | None) -> str | None:
33
+ """Read the PR description body from a file when given and readable.
34
+
35
+ Args:
36
+ pr_body_file: Path to a file holding the PR description body, or None.
37
+
38
+ Returns:
39
+ The file's text when the path is given and readable, otherwise None.
40
+ """
41
+ if pr_body_file is None:
42
+ return None
43
+ try:
44
+ return pr_body_file.read_text(encoding="utf-8")
45
+ except OSError:
46
+ return None
47
+
48
+
49
+ def _append_pr_description(root: Element, pr_body_text: str | None) -> None:
50
+ """Append a <pr_description> element carrying the PR body text.
51
+
52
+ Args:
53
+ root: Root element the <pr_description> child is appended to.
54
+ pr_body_text: PR description body text, or None when unavailable.
55
+
56
+ Returns:
57
+ None.
58
+ """
59
+ pr_description = SubElement(root, "pr_description")
60
+ if pr_body_text:
61
+ pr_description.text = pr_body_text
62
+
63
+
31
64
  def build_audit_prompt_xml(
32
65
  *,
33
66
  owner: str,
@@ -38,6 +71,7 @@ def build_audit_prompt_xml(
38
71
  base_ref: str,
39
72
  worktree_path: Path,
40
73
  run_temp_dir: Path,
74
+ pr_body_text: str | None = None,
41
75
  ) -> Element:
42
76
  """Build the complete AUDIT spawn prompt XML.
43
77
 
@@ -50,6 +84,7 @@ def build_audit_prompt_xml(
50
84
  base_ref: Base branch ref.
51
85
  worktree_path: Path to the git worktree.
52
86
  run_temp_dir: Path to the run temp directory.
87
+ pr_body_text: PR description body text, or None when unavailable.
53
88
 
54
89
  Returns:
55
90
  Root <spawn_prompt> element.
@@ -72,6 +107,8 @@ def build_audit_prompt_xml(
72
107
  f"bugs, and anti-patterns. Work in {worktree_path.as_posix()}."
73
108
  )
74
109
 
110
+ _append_pr_description(root, pr_body_text)
111
+
75
112
  bug_categories = SubElement(root, "bug_categories")
76
113
  for each_category_id, each_category_label in ALL_AUDIT_CATEGORY_ENTRIES:
77
114
  cat_elem = SubElement(bug_categories, "category", {"id": each_category_id})
@@ -110,6 +147,7 @@ def emit_audit_prompt(
110
147
  base_ref: str,
111
148
  worktree_path: Path,
112
149
  run_temp_dir: Path,
150
+ pr_body_text: str | None = None,
113
151
  ) -> str:
114
152
  """Build and serialize the AUDIT spawn prompt to a pretty-printed XML string.
115
153
 
@@ -122,6 +160,7 @@ def emit_audit_prompt(
122
160
  base_ref: Base branch ref.
123
161
  worktree_path: Path to the git worktree.
124
162
  run_temp_dir: Path to the run temp directory.
163
+ pr_body_text: PR description body text, or None when unavailable.
125
164
 
126
165
  Returns:
127
166
  Pretty-printed XML string.
@@ -135,6 +174,7 @@ def emit_audit_prompt(
135
174
  base_ref=base_ref,
136
175
  worktree_path=worktree_path,
137
176
  run_temp_dir=run_temp_dir,
177
+ pr_body_text=pr_body_text,
138
178
  )
139
179
  return emit_pretty_xml(root)
140
180
 
@@ -157,6 +197,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
157
197
  parser.add_argument("--base-ref", required=True)
158
198
  parser.add_argument("--worktree-path", type=Path, required=True)
159
199
  parser.add_argument("--run-temp-dir", type=Path, required=True)
200
+ parser.add_argument("--pr-body-file", type=Path, default=None)
160
201
  return parser.parse_args(all_argv)
161
202
 
162
203
 
@@ -179,6 +220,7 @@ def main(all_arguments: list[str]) -> int:
179
220
  base_ref=arguments.base_ref,
180
221
  worktree_path=arguments.worktree_path,
181
222
  run_temp_dir=arguments.run_temp_dir,
223
+ pr_body_text=read_pr_body_text(arguments.pr_body_file),
182
224
  )
183
225
  sys.stdout.write(xml_output)
184
226
  return 0
@@ -7,7 +7,7 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
7
7
  | File | Role |
8
8
  |---|---|
9
9
  | `__init__.py` | Package marker. |
10
- | `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns, loop state filename, fix status values, audit constraint texts, audit category entries, fix execution steps, fix constraint texts, and XML serialization settings. |
10
+ | `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns, fix status values, audit constraint texts, audit category entries, fix execution steps, and fix constraint texts. |
11
11
  | `preflight_constants.py` | Constants used by `preflight_worktree.py` for exit codes and output marker strings. |
12
12
 
13
13
  ## Usage
@@ -15,5 +15,5 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
15
15
  Scripts import directly from the package:
16
16
 
17
17
  ```python
18
- from skills_pr_loop_constants.path_resolver_constants import LOOP_STATE_FILENAME
18
+ from skills_pr_loop_constants.path_resolver_constants import PER_PR_WORKSPACE_TEMPLATE
19
19
  ```
@@ -9,7 +9,6 @@ WORKTREE_DIRNAME = "worktree"
9
9
  DIFF_PATCH_TEMPLATE = "loop-{loop}.patch"
10
10
  OUTCOME_XML_TEMPLATE = ".bugteam-pr{number}-loop{loop}.outcomes.xml"
11
11
  FIX_OUTCOME_XML_TEMPLATE = ".bugteam-pr{number}-loop{loop}.fix-outcomes.xml"
12
- LOOP_STATE_FILENAME = "loop-state.json"
13
12
  SLUGIFY_SAFE_CHARS = re.compile(r"[^A-Za-z0-9._-]")
14
13
  SLUGIFY_REPLACEMENT = "-"
15
14
  MULTI_PR_SLUG_TEMPLATE = "{owner}-{repo}-pr-{number}"
@@ -47,6 +46,7 @@ ALL_AUDIT_CATEGORY_ENTRIES = [
47
46
  ("N", "Test-name scenario verifier"),
48
47
  ("O", "Docstring / fixture-prose vs implementation drift"),
49
48
  ("P", "Name / regex / word-list vs behavior-contract precision"),
49
+ ("Q", "Cross-surface claim consistency (terminology, PR-description claims, message-vs-guard)"),
50
50
  ]
51
51
 
52
52
  AUDIT_RUBRIC_REFERENCE_TEXT = (
@@ -77,9 +77,6 @@ ALL_FIX_CONSTRAINT_TEXTS = [
77
77
  "forward slashes (e.g. C:/Users/...), even on Windows.",
78
78
  ]
79
79
 
80
- XML_PRETTY_INDENT = " "
81
- XML_SERIALIZE_ENCODING = "unicode"
82
- XML_OUTPUT_ENCODING = "utf-8"
83
80
  ALL_PYTHON_ONEXC_VERSION = (3, 12)
84
81
 
85
82
  ALL_FINDING_BODY_ELEMENT_KEYS: tuple[str, ...] = (
@@ -1,4 +1,4 @@
1
- """Tests pinning build_audit_prompt's emitted A-P category taxonomy."""
1
+ """Tests pinning build_audit_prompt's emitted A-Q category taxonomy."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -18,7 +18,7 @@ from skills_pr_loop_constants.path_resolver_constants import (
18
18
  )
19
19
 
20
20
  _CATEGORY_RUBRICS_DIR = _SCRIPTS_DIR.parents[3] / "audit-rubrics" / "category_rubrics"
21
- _HEADING_PATTERN = re.compile(r"^# Category ([A-P]) — (.+)$")
21
+ _HEADING_PATTERN = re.compile(r"^# Category ([A-Q]) — (.+)$")
22
22
 
23
23
 
24
24
  def _load_build_audit_prompt() -> ModuleType:
@@ -84,12 +84,12 @@ def test_context_and_scope_render_paths_with_forward_slashes() -> None:
84
84
  assert "C:/Users/jon/AppData/Local/Temp/bugteam-pr-376/worktree" in scope.text
85
85
 
86
86
 
87
- def test_bug_categories_carry_ids_a_through_p_in_order() -> None:
87
+ def test_bug_categories_carry_ids_a_through_q_in_order() -> None:
88
88
  root = _build_audit_root()
89
89
  bug_categories = root.find("bug_categories")
90
90
  assert bug_categories is not None
91
91
  all_emitted_ids = [each_category.get("id") for each_category in bug_categories]
92
- all_expected_ids = list("ABCDEFGHIJKLMNOP")
92
+ all_expected_ids = list("ABCDEFGHIJKLMNOPQ")
93
93
  assert all_emitted_ids == all_expected_ids
94
94
 
95
95
 
@@ -161,3 +161,99 @@ def test_prompt_skeleton_sub_bucket_counts_match_rubric_rows() -> None:
161
161
  f"{each_letter}{each_walk_match.group(1)} but rubric has "
162
162
  f"{sub_bucket_row_count} rows"
163
163
  )
164
+
165
+
166
+ def test_bug_categories_include_category_q() -> None:
167
+ root = _build_audit_root()
168
+ bug_categories = root.find("bug_categories")
169
+ assert bug_categories is not None
170
+ label_by_id = {
171
+ each_category.get("id"): each_category.text for each_category in bug_categories
172
+ }
173
+ assert label_by_id["Q"] == (
174
+ "Cross-surface claim consistency "
175
+ "(terminology, PR-description claims, message-vs-guard)"
176
+ )
177
+
178
+
179
+ def test_pr_description_carries_body_text_when_supplied() -> None:
180
+ root = build_audit_prompt.build_audit_prompt_xml(
181
+ owner="jl-cmd",
182
+ repo="claude-code-config",
183
+ pr_number=422,
184
+ loop=1,
185
+ head_ref="feat/branch",
186
+ base_ref="main",
187
+ worktree_path=Path("/tmp/bugteam-pr-422/worktree"),
188
+ run_temp_dir=Path("/tmp/bugteam-pr-422"),
189
+ pr_body_text="## Summary\nCloses the gate.",
190
+ )
191
+ pr_description = root.find("pr_description")
192
+ assert pr_description is not None
193
+ assert pr_description.text == "## Summary\nCloses the gate."
194
+
195
+
196
+ def test_pr_description_empty_when_body_absent() -> None:
197
+ root = _build_audit_root()
198
+ pr_description = root.find("pr_description")
199
+ assert pr_description is not None
200
+ assert pr_description.text is None
201
+
202
+
203
+ def test_read_pr_body_text_returns_file_contents(tmp_path: Path) -> None:
204
+ body_file = tmp_path / "pr-body.md"
205
+ body_file.write_text("body from disk", encoding="utf-8")
206
+ assert build_audit_prompt.read_pr_body_text(body_file) == "body from disk"
207
+
208
+
209
+ def test_read_pr_body_text_returns_none_when_path_absent() -> None:
210
+ assert build_audit_prompt.read_pr_body_text(None) is None
211
+
212
+
213
+ def test_read_pr_body_text_returns_none_when_file_missing(tmp_path: Path) -> None:
214
+ missing_file = tmp_path / "does-not-exist.md"
215
+ assert build_audit_prompt.read_pr_body_text(missing_file) is None
216
+
217
+
218
+ def test_emit_audit_prompt_embeds_pr_body_text_in_pr_description() -> None:
219
+ xml_text = build_audit_prompt.emit_audit_prompt(
220
+ owner="jl-cmd",
221
+ repo="claude-code-config",
222
+ pr_number=422,
223
+ loop=1,
224
+ head_ref="feat/branch",
225
+ base_ref="main",
226
+ worktree_path=Path("/tmp/bugteam-pr-422/worktree"),
227
+ run_temp_dir=Path("/tmp/bugteam-pr-422"),
228
+ pr_body_text="body via emit",
229
+ )
230
+ assert "body via emit" in xml_text
231
+
232
+
233
+ def test_parse_arguments_defaults_pr_body_file_to_none() -> None:
234
+ arguments = build_audit_prompt.parse_arguments([
235
+ "--owner", "jl-cmd",
236
+ "--repo", "claude-code-config",
237
+ "--pr-number", "422",
238
+ "--loop", "1",
239
+ "--head-ref", "feat/branch",
240
+ "--base-ref", "main",
241
+ "--worktree-path", "/tmp/wt",
242
+ "--run-temp-dir", "/tmp/rt",
243
+ ])
244
+ assert arguments.pr_body_file is None
245
+
246
+
247
+ def test_parse_arguments_reads_pr_body_file_path() -> None:
248
+ arguments = build_audit_prompt.parse_arguments([
249
+ "--owner", "jl-cmd",
250
+ "--repo", "claude-code-config",
251
+ "--pr-number", "422",
252
+ "--loop", "1",
253
+ "--head-ref", "feat/branch",
254
+ "--base-ref", "main",
255
+ "--worktree-path", "/tmp/wt",
256
+ "--run-temp-dir", "/tmp/rt",
257
+ "--pr-body-file", "/tmp/body.md",
258
+ ])
259
+ assert arguments.pr_body_file == Path("/tmp/body.md")
@@ -97,6 +97,18 @@ PR's owner.
97
97
  prompt can add a standing Bash permission allow-rule for that script in their
98
98
  settings.
99
99
 
100
+ 5. **Copilot quota pre-check.** Before the `Workflow` call, run once:
101
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`
102
+ It reads the account's remaining Copilot premium-request quota via
103
+ `gh api copilot_internal/user` and prints one line — log that line. Exit 0
104
+ means Copilot has quota to run, so pass `copilotDisabled: false`. Any non-zero
105
+ exit means skip Copilot for this run — the account is out of quota, the quota
106
+ API or account access is down, or no account is set — so pass
107
+ `copilotDisabled: true`; the workflow then skips the Copilot gate with no agent
108
+ spawned. The account comes from the `COPILOT_QUOTA_ACCOUNT` environment
109
+ variable or a git-ignored `.env` file, and the no-account line names the exact
110
+ `.env` path and key to set.
111
+
100
112
  ## Run the workflow
101
113
 
102
114
  Call the `Workflow` tool against the colocated script:
@@ -104,7 +116,7 @@ Call the `Workflow` tool against the colocated script:
104
116
  ```
105
117
  Workflow({
106
118
  scriptPath: "<this skill dir>/workflow/converge.mjs",
107
- args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false }
119
+ args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false }
108
120
  })
109
121
  ```
110
122
 
@@ -113,8 +125,10 @@ own directory (on this install,
113
125
  `<home>/.claude/skills/autoconverge/workflow/converge.mjs`). Set
114
126
  `bugbotDisabled: true` only when the user has opted Cursor Bugbot out for the
115
127
  run; otherwise the workflow detects an opt-out or an unreachable Bugbot on its
116
- own. The workflow runs in the background and notifies this session on
117
- completion. Watch live progress with `/workflows`.
128
+ own. Set `copilotDisabled: true` when the step 5 quota pre-check exits non-zero,
129
+ and `false` when it exits 0; on `true` the workflow skips the Copilot gate with
130
+ no agent spawned. The workflow runs in the background and notifies this session
131
+ on completion. Watch live progress with `/workflows`.
118
132
 
119
133
  The workflow returns
120
134
  `{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
@@ -298,10 +312,10 @@ matches.
298
312
  (hooks/rules that block those violation classes at Write/Edit time), resolves
299
313
  any bot threads with a deferral note, and reports the deferral in
300
314
  `standardsNote`.
301
- - **Copilot gate:** request a Copilot review, poll up to three times; findings
315
+ - **Copilot gate:** request a Copilot review, poll up to the configured cap; findings
302
316
  route back into Converge. When Copilot is down or out of quota — it posts an
303
317
  out-of-usage notice (the requester hit their quota) on the HEAD, or surfaces no
304
- review at all after the cap — the gate logs a notice and the run marks the PR
318
+ review at all after the configured cap — the gate logs a notice and the run marks the PR
305
319
  ready with the Copilot gate bypassed. `copilotNote` records the bypass.
306
320
  - **Convergence check:** `check_convergence.py` is the authoritative gate; on a
307
321
  full pass the workflow marks `draft=false`.
@@ -341,6 +355,13 @@ each PR its own checkout with `git worktree add`. For each PR the user named:
341
355
  4. **Grant project permissions once per repository** — the single-PR pre-flight
342
356
  step 4 grant covers every worktree of the same repo, so run it one time for
343
357
  the repo the PRs live in.
358
+ 5. **Copilot quota pre-check once for the whole run** — run the single-PR
359
+ pre-flight step 5 check one time:
360
+ `python "$HOME/.claude/_shared/pr-loop/scripts/copilot_quota.py"`. Every PR in
361
+ the run shares one account's Copilot premium-request quota, so one check covers
362
+ them all. Exit 0 sets `copilotDisabled: false` on every PR entry below; any
363
+ non-zero exit sets `copilotDisabled: true` on every entry, so each child skips
364
+ the Copilot gate with no agent spawned.
344
365
 
345
366
  ### Launch the multi-PR workflow
346
367
 
@@ -353,8 +374,8 @@ Workflow({
353
374
  args: {
354
375
  convergeScriptPath: "<this skill dir>/workflow/converge.mjs",
355
376
  prs: [
356
- { owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false },
357
- { owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false }
377
+ { owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false, copilotDisabled: false },
378
+ { owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false, copilotDisabled: false }
358
379
  ]
359
380
  }
360
381
  })
@@ -79,11 +79,11 @@ tracks CONVERGE passes only and is never the cap.
79
79
  **COPILOT** gate:
80
80
 
81
81
  - Request a Copilot review on HEAD (skipping a duplicate request), then poll up
82
- to three times, 360 seconds apart.
82
+ to the configured cap, 360 seconds apart.
83
83
  - Copilot findings → fix them and return to CONVERGE on the new HEAD.
84
84
  - Copilot clean or approved → move to the convergence check.
85
- - Copilot down or out of quota (an out-of-usage notice, or no review after three
86
- polls) → log a notice and move to the convergence check with the Copilot gate
85
+ - Copilot down or out of quota (an out-of-usage notice, or no review after the
86
+ configured cap) → log a notice and move to the convergence check with the Copilot gate
87
87
  bypassed.
88
88
 
89
89
  **Convergence check**:
@@ -47,7 +47,7 @@ skill still runs teardown (revoke permissions, final report).
47
47
  Bugbot gate is bypassed.
48
48
  - **Copilot down or out of quota** — when Copilot posts an out-of-usage notice on
49
49
  the current HEAD (the user who requested the review reached their quota limit)
50
- rather than a code review, or surfaces no review at all after three polls, the
50
+ rather than a code review, or surfaces no review at all after the configured cap, the
51
51
  Copilot gate returns `down: true`. The run logs a notice, runs the convergence
52
52
  check with `--copilot-down` (the Copilot review gate and the
53
53
  pending-requested-reviews gate bypassed), and marks the PR ready. `copilotNote`
@@ -40,8 +40,8 @@ test('cleanAuditBlocker falls back to a no-result reason when the post agent die
40
40
  assert.match(message, /the post agent returned no result/);
41
41
  });
42
42
 
43
- test('the post-clean-audit task in resumeGeneralUtilityAgent returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
44
- const body = functionBody('resumeGeneralUtilityAgent');
43
+ test('the post-clean-audit task in runGeneralUtilityTask returns the CLEAN_AUDIT_SCHEMA result rather than an unused transcript', () => {
44
+ const body = functionBody('runGeneralUtilityTask');
45
45
  assert.match(body, /task === 'post-clean-audit'/);
46
46
  assert.match(body, /schema: CLEAN_AUDIT_SCHEMA/);
47
47
  assert.doesNotMatch(body, /agent transcript \(unused\)/);
@@ -59,7 +59,7 @@ test('the standards-only call site breaks with a clean-audit blocker when the po
59
59
  convergeSource.indexOf('if (isStandardsOnlyRound(findings)) {'),
60
60
  convergeSource.indexOf('if (findings.length > 0) {'),
61
61
  );
62
- assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
62
+ assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
63
63
  assert.match(branch, /if \(!auditResult\?\.posted\)/);
64
64
  assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
65
65
  assert.match(branch, /\bbreak\b/);
@@ -70,7 +70,7 @@ test('the all-clean call site breaks with a clean-audit blocker when the post do
70
70
  convergeSource.indexOf('all lenses clean on'),
71
71
  convergeSource.indexOf("if (phase === 'COPILOT') {"),
72
72
  );
73
- assert.match(branch, /resumeGeneralUtilityAgent\(.*'post-clean-audit'/);
73
+ assert.match(branch, /runGeneralUtilityTask\(.*'post-clean-audit'/);
74
74
  assert.match(branch, /if \(!auditResult\?\.posted\)/);
75
75
  assert.match(branch, /blocker = cleanAuditBlocker\(head, auditResult\)/);
76
76
  assert.match(branch, /\bbreak\b/);