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.
- package/_shared/pr-loop/scripts/CLAUDE.md +3 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
- package/audit-rubrics/CLAUDE.md +3 -2
- package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
- package/audit-rubrics/prompts/CLAUDE.md +2 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_annotations_length.py +59 -11
- package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
- package/hooks/blocking/code_rules_command_dispatch.py +140 -0
- package/hooks/blocking/code_rules_docstrings.py +93 -0
- package/hooks/blocking/code_rules_enforcer.py +58 -4
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_js_conventions.py +246 -0
- package/hooks/blocking/code_rules_naming_collection.py +5 -0
- package/hooks/blocking/code_rules_test_assertions.py +3 -0
- package/hooks/blocking/code_rules_unused_imports.py +7 -66
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
- package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +10 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
- package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
- package/hooks/hooks_constants/js_conventions_constants.py +54 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +3 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
- package/skills/autoconverge/SKILL.md +28 -7
- package/skills/autoconverge/reference/convergence.md +3 -3
- package/skills/autoconverge/reference/stop-conditions.md +1 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +235 -247
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- 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,
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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("
|
|
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.
|
|
117
|
-
|
|
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
|
|
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
|
|
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
|
|
86
|
-
|
|
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
|
|
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
|
|
44
|
-
const body = functionBody('
|
|
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, /
|
|
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, /
|
|
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/);
|