claude-dev-env 1.29.3 → 1.30.1
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 +8 -0
- package/agents/code-quality-agent.md +279 -24
- package/agents/groq-coder.md +111 -0
- package/commands/plan.md +4 -5
- package/docs/CODE_RULES.md +40 -0
- package/hooks/blocking/code_rules_enforcer.py +775 -8
- package/hooks/blocking/destructive_command_blocker.py +149 -12
- package/hooks/blocking/test_code_rules_enforcer.py +751 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
- package/hooks/blocking/test_destructive_command_blocker.py +281 -4
- package/hooks/git-hooks/test_config.py +9 -3
- package/hooks/git-hooks/test_gate_utils.py +9 -3
- package/hooks/git-hooks/test_pre_commit.py +9 -3
- package/hooks/git-hooks/test_pre_push.py +9 -3
- package/hooks/validators/run_all_validators.py +76 -3
- package/hooks/validators/test_output_formatter.py +4 -16
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_integration.py +2 -11
- package/package.json +1 -1
- package/scripts/config/groq_bugteam_config.py +104 -0
- package/scripts/config/test_groq_bugteam_config.py +11 -0
- package/scripts/config/test_spec_implementer_prompt.py +36 -0
- package/scripts/groq_bugteam.README.md +2 -0
- package/scripts/groq_bugteam.py +74 -15
- package/scripts/groq_bugteam_dotenv.py +40 -0
- package/scripts/groq_bugteam_spec.py +226 -0
- package/scripts/test_groq_bugteam.py +143 -5
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
- package/scripts/test_groq_bugteam_dotenv.py +66 -0
- package/scripts/test_groq_bugteam_spec.py +346 -0
- package/skills/bugteam/SKILL.md +4 -0
- package/skills/bugteam/reference/README.md +16 -0
- package/skills/bugteam/test_skill_additions.py +30 -0
- package/skills/monitor-open-prs/SKILL.md +104 -0
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
- package/skills/monitor-open-prs/test_skill_contract.py +43 -0
- package/skills/pr-review-responder/SKILL.md +10 -8
- package/hooks/github-action/pre-push-review.yml +0 -27
- package/hooks/github-action/test_workflow.py +0 -33
- package/skills/pr-review-responder/update_skill.py +0 -297
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Coherence tests for groq_bugteam_spec module import surface.
|
|
2
|
+
|
|
3
|
+
The behavioral contract for apply_fix_from_spec lives in
|
|
4
|
+
test_groq_bugteam_apply_fix_from_spec.py; those tests pass whether the
|
|
5
|
+
function is defined in groq_bugteam.py directly or re-exported from the
|
|
6
|
+
spec module. This file exists solely so the spec module has a
|
|
7
|
+
same-named test companion for filename-based test pairing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import pathlib
|
|
16
|
+
import sys
|
|
17
|
+
import types
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_spec_module():
|
|
23
|
+
scripts_directory = pathlib.Path(__file__).parent
|
|
24
|
+
sys.path.insert(0, str(scripts_directory))
|
|
25
|
+
sys.modules.pop("groq_bugteam_spec", None)
|
|
26
|
+
module_path = scripts_directory / "groq_bugteam_spec.py"
|
|
27
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
28
|
+
"groq_bugteam_spec", module_path
|
|
29
|
+
)
|
|
30
|
+
loaded_module = importlib.util.module_from_spec(module_spec)
|
|
31
|
+
sys.modules["groq_bugteam_spec"] = loaded_module
|
|
32
|
+
module_spec.loader.exec_module(loaded_module)
|
|
33
|
+
return loaded_module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
groq_bugteam_spec = _load_spec_module()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_apply_fix_from_spec_is_callable():
|
|
40
|
+
assert callable(groq_bugteam_spec.apply_fix_from_spec)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_run_spec_mode_main_is_callable():
|
|
44
|
+
assert callable(groq_bugteam_spec.run_spec_mode_main)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_is_spec_mode_invocation_detects_flag_value_pair():
|
|
48
|
+
assert groq_bugteam_spec.is_spec_mode_invocation(["--mode", "spec"]) is True
|
|
49
|
+
assert groq_bugteam_spec.is_spec_mode_invocation(["--mode", "pipeline"]) is False
|
|
50
|
+
assert groq_bugteam_spec.is_spec_mode_invocation([]) is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _attach_required_groq_attributes(target_module: types.ModuleType) -> None:
|
|
54
|
+
target_module.call_groq_with_fallback = lambda *args, **kwargs: None
|
|
55
|
+
target_module.parse_json_object = lambda text: {}
|
|
56
|
+
target_module.preserve_trailing_newline = lambda original, updated: updated
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_resolver_prefers_registered_groq_bugteam_over_main(monkeypatch):
|
|
60
|
+
fake_groq_bugteam = types.ModuleType("groq_bugteam")
|
|
61
|
+
_attach_required_groq_attributes(fake_groq_bugteam)
|
|
62
|
+
monkeypatch.setitem(sys.modules, "groq_bugteam", fake_groq_bugteam)
|
|
63
|
+
|
|
64
|
+
resolved_module = groq_bugteam_spec.resolve_groq_bugteam_module()
|
|
65
|
+
|
|
66
|
+
assert resolved_module is fake_groq_bugteam
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_resolver_falls_back_to_main_when_groq_bugteam_absent(monkeypatch):
|
|
70
|
+
monkeypatch.delitem(sys.modules, "groq_bugteam", raising=False)
|
|
71
|
+
fake_main = types.ModuleType("__main__")
|
|
72
|
+
_attach_required_groq_attributes(fake_main)
|
|
73
|
+
monkeypatch.setitem(sys.modules, "__main__", fake_main)
|
|
74
|
+
|
|
75
|
+
resolved_module = groq_bugteam_spec.resolve_groq_bugteam_module()
|
|
76
|
+
|
|
77
|
+
assert resolved_module is fake_main
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_resolver_falls_back_to_main_when_registered_module_is_stub(monkeypatch):
|
|
81
|
+
stub_groq_bugteam = types.ModuleType("groq_bugteam")
|
|
82
|
+
stub_groq_bugteam.call_groq_with_fallback = lambda *args, **kwargs: None
|
|
83
|
+
monkeypatch.setitem(sys.modules, "groq_bugteam", stub_groq_bugteam)
|
|
84
|
+
complete_main = types.ModuleType("__main__")
|
|
85
|
+
_attach_required_groq_attributes(complete_main)
|
|
86
|
+
monkeypatch.setitem(sys.modules, "__main__", complete_main)
|
|
87
|
+
|
|
88
|
+
resolved_module = groq_bugteam_spec.resolve_groq_bugteam_module()
|
|
89
|
+
|
|
90
|
+
assert resolved_module is complete_main
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_resolver_raises_when_registered_module_missing_required_attributes(
|
|
94
|
+
monkeypatch,
|
|
95
|
+
):
|
|
96
|
+
stub_groq_bugteam = types.ModuleType("groq_bugteam")
|
|
97
|
+
stub_groq_bugteam.call_groq_with_fallback = lambda *args, **kwargs: None
|
|
98
|
+
monkeypatch.setitem(sys.modules, "groq_bugteam", stub_groq_bugteam)
|
|
99
|
+
monkeypatch.delitem(sys.modules, "__main__", raising=False)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
groq_bugteam_spec.resolve_groq_bugteam_module()
|
|
103
|
+
except RuntimeError as resolver_error:
|
|
104
|
+
resolver_error_text = str(resolver_error)
|
|
105
|
+
assert "parse_json_object" in resolver_error_text
|
|
106
|
+
assert "preserve_trailing_newline" in resolver_error_text
|
|
107
|
+
else:
|
|
108
|
+
raise AssertionError("resolver should have raised RuntimeError")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_resolver_raises_when_neither_module_available(monkeypatch):
|
|
112
|
+
monkeypatch.delitem(sys.modules, "groq_bugteam", raising=False)
|
|
113
|
+
placeholder_main = types.ModuleType("__main__")
|
|
114
|
+
monkeypatch.setitem(sys.modules, "__main__", placeholder_main)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
groq_bugteam_spec.resolve_groq_bugteam_module()
|
|
118
|
+
except RuntimeError as resolver_error:
|
|
119
|
+
resolver_error_text = str(resolver_error)
|
|
120
|
+
assert "groq_bugteam" in resolver_error_text
|
|
121
|
+
else:
|
|
122
|
+
raise AssertionError("resolver should have raised RuntimeError")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
FAKE_API_KEY = "gsk_test_placeholder_value"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _install_fake_groq_bugteam_module(monkeypatch, response_object):
|
|
129
|
+
"""Register a minimal fake groq_bugteam module for resolver lookup."""
|
|
130
|
+
|
|
131
|
+
fake_module = types.ModuleType("groq_bugteam")
|
|
132
|
+
|
|
133
|
+
def fake_call(api_key, messages, temperature, max_completion_tokens):
|
|
134
|
+
return types.SimpleNamespace(
|
|
135
|
+
content=json.dumps(response_object),
|
|
136
|
+
model="fake-model",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def fake_parse_json_object(text):
|
|
140
|
+
return json.loads(text)
|
|
141
|
+
|
|
142
|
+
def fake_preserve_trailing_newline(original, updated):
|
|
143
|
+
if original.endswith("\n") and not updated.endswith("\n"):
|
|
144
|
+
return updated + "\n"
|
|
145
|
+
if not original.endswith("\n") and updated.endswith("\n"):
|
|
146
|
+
return updated[:-1]
|
|
147
|
+
return updated
|
|
148
|
+
|
|
149
|
+
fake_module.call_groq_with_fallback = fake_call
|
|
150
|
+
fake_module.parse_json_object = fake_parse_json_object
|
|
151
|
+
fake_module.preserve_trailing_newline = fake_preserve_trailing_newline
|
|
152
|
+
monkeypatch.setitem(sys.modules, "groq_bugteam", fake_module)
|
|
153
|
+
monkeypatch.setenv("GROQ_API_KEY", FAKE_API_KEY)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_skipped_entry_missing_finding_index_does_not_crash(monkeypatch):
|
|
157
|
+
original_file = "alpha\nbeta\n"
|
|
158
|
+
spec_list = [
|
|
159
|
+
{
|
|
160
|
+
"finding_index": 4,
|
|
161
|
+
"severity": "P1",
|
|
162
|
+
"category": "J",
|
|
163
|
+
"file": "sample.py",
|
|
164
|
+
"target_line_start": 1,
|
|
165
|
+
"target_line_end": 1,
|
|
166
|
+
"intended_change": "rename alpha",
|
|
167
|
+
"replacement_code": "alpha_fixed",
|
|
168
|
+
"acceptance_criteria": ["alpha_fixed appears on line 1"],
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
patched_file = "alpha_fixed\nbeta\n"
|
|
172
|
+
fake_response = {
|
|
173
|
+
"updated_content": patched_file,
|
|
174
|
+
"applied_finding_indexes": [4],
|
|
175
|
+
"skipped": [{"reason": "malformed entry without finding_index"}],
|
|
176
|
+
"acceptance_checks": [
|
|
177
|
+
{
|
|
178
|
+
"finding_index": 4,
|
|
179
|
+
"criterion": "alpha_fixed appears on line 1",
|
|
180
|
+
"met": True,
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
_install_fake_groq_bugteam_module(monkeypatch, fake_response)
|
|
185
|
+
|
|
186
|
+
outcome = groq_bugteam_spec.apply_fix_from_spec(spec_list, original_file)
|
|
187
|
+
|
|
188
|
+
assert outcome["updated_content"] == patched_file
|
|
189
|
+
assert outcome["applied_finding_indexes"] == [4]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_null_updated_content_falls_back_to_current_content(monkeypatch):
|
|
193
|
+
original_file = "alpha\nbeta\n"
|
|
194
|
+
spec_list = [
|
|
195
|
+
{
|
|
196
|
+
"finding_index": 0,
|
|
197
|
+
"severity": "P2",
|
|
198
|
+
"category": "E",
|
|
199
|
+
"file": "sample.py",
|
|
200
|
+
"target_line_start": 1,
|
|
201
|
+
"target_line_end": 1,
|
|
202
|
+
"intended_change": "no-op fallback",
|
|
203
|
+
"replacement_code": "alpha",
|
|
204
|
+
"acceptance_criteria": ["alpha remains on line 1"],
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
fake_response = {
|
|
208
|
+
"updated_content": None,
|
|
209
|
+
"applied_finding_indexes": [],
|
|
210
|
+
"skipped": [
|
|
211
|
+
{
|
|
212
|
+
"finding_index": 0,
|
|
213
|
+
"reason": "Groq returned null updated_content",
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
"acceptance_checks": [],
|
|
217
|
+
}
|
|
218
|
+
_install_fake_groq_bugteam_module(monkeypatch, fake_response)
|
|
219
|
+
|
|
220
|
+
outcome = groq_bugteam_spec.apply_fix_from_spec(spec_list, original_file)
|
|
221
|
+
|
|
222
|
+
assert outcome["updated_content"] == original_file
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_null_collection_fields_coerce_to_empty_lists(monkeypatch):
|
|
226
|
+
original_file = "alpha\n"
|
|
227
|
+
spec_list = [
|
|
228
|
+
{
|
|
229
|
+
"finding_index": 1,
|
|
230
|
+
"severity": "P2",
|
|
231
|
+
"category": "E",
|
|
232
|
+
"file": "sample.py",
|
|
233
|
+
"target_line_start": 1,
|
|
234
|
+
"target_line_end": 1,
|
|
235
|
+
"intended_change": "no-op",
|
|
236
|
+
"replacement_code": "alpha",
|
|
237
|
+
"acceptance_criteria": ["alpha remains"],
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
fake_response = {
|
|
241
|
+
"updated_content": original_file,
|
|
242
|
+
"applied_finding_indexes": None,
|
|
243
|
+
"skipped": None,
|
|
244
|
+
"acceptance_checks": None,
|
|
245
|
+
}
|
|
246
|
+
_install_fake_groq_bugteam_module(monkeypatch, fake_response)
|
|
247
|
+
|
|
248
|
+
outcome = groq_bugteam_spec.apply_fix_from_spec(spec_list, original_file)
|
|
249
|
+
|
|
250
|
+
assert outcome["applied_finding_indexes"] == []
|
|
251
|
+
assert outcome["skipped"] == []
|
|
252
|
+
assert outcome["acceptance_checks"] == []
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_dict_collection_fields_coerce_to_empty_lists(monkeypatch):
|
|
256
|
+
original_file = "alpha\n"
|
|
257
|
+
spec_list = [
|
|
258
|
+
{
|
|
259
|
+
"finding_index": 2,
|
|
260
|
+
"severity": "P2",
|
|
261
|
+
"category": "E",
|
|
262
|
+
"file": "sample.py",
|
|
263
|
+
"target_line_start": 1,
|
|
264
|
+
"target_line_end": 1,
|
|
265
|
+
"intended_change": "no-op",
|
|
266
|
+
"replacement_code": "alpha",
|
|
267
|
+
"acceptance_criteria": ["alpha remains"],
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
fake_response = {
|
|
271
|
+
"updated_content": original_file,
|
|
272
|
+
"applied_finding_indexes": {"not": "a list"},
|
|
273
|
+
"skipped": {"0": "not a list either"},
|
|
274
|
+
"acceptance_checks": {"also": "a dict"},
|
|
275
|
+
}
|
|
276
|
+
_install_fake_groq_bugteam_module(monkeypatch, fake_response)
|
|
277
|
+
|
|
278
|
+
outcome = groq_bugteam_spec.apply_fix_from_spec(spec_list, original_file)
|
|
279
|
+
|
|
280
|
+
assert outcome["applied_finding_indexes"] == []
|
|
281
|
+
assert outcome["skipped"] == []
|
|
282
|
+
assert outcome["acceptance_checks"] == []
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_non_string_updated_content_falls_back_to_current_content(monkeypatch):
|
|
286
|
+
original_file = "alpha\nbeta\n"
|
|
287
|
+
spec_list = [
|
|
288
|
+
{
|
|
289
|
+
"finding_index": 0,
|
|
290
|
+
"severity": "P2",
|
|
291
|
+
"category": "E",
|
|
292
|
+
"file": "sample.py",
|
|
293
|
+
"target_line_start": 1,
|
|
294
|
+
"target_line_end": 1,
|
|
295
|
+
"intended_change": "no-op fallback",
|
|
296
|
+
"replacement_code": "alpha",
|
|
297
|
+
"acceptance_criteria": ["alpha remains on line 1"],
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
fake_response = {
|
|
301
|
+
"updated_content": {"unexpected": "dict instead of str"},
|
|
302
|
+
"applied_finding_indexes": [],
|
|
303
|
+
"skipped": [],
|
|
304
|
+
"acceptance_checks": [],
|
|
305
|
+
}
|
|
306
|
+
_install_fake_groq_bugteam_module(monkeypatch, fake_response)
|
|
307
|
+
|
|
308
|
+
outcome = groq_bugteam_spec.apply_fix_from_spec(spec_list, original_file)
|
|
309
|
+
|
|
310
|
+
assert outcome["updated_content"] == original_file
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_run_spec_mode_main_emits_error_json_on_missing_api_key(
|
|
314
|
+
monkeypatch, capsys
|
|
315
|
+
):
|
|
316
|
+
monkeypatch.delenv("GROQ_API_KEY", raising=False)
|
|
317
|
+
monkeypatch.setattr(
|
|
318
|
+
"groq_bugteam_dotenv.load_claude_dev_env_dotenv_file",
|
|
319
|
+
lambda: None,
|
|
320
|
+
)
|
|
321
|
+
spec_payload = {
|
|
322
|
+
"spec": [
|
|
323
|
+
{
|
|
324
|
+
"finding_index": 0,
|
|
325
|
+
"severity": "P1",
|
|
326
|
+
"category": "J",
|
|
327
|
+
"file": "sample.py",
|
|
328
|
+
"target_line_start": 1,
|
|
329
|
+
"target_line_end": 1,
|
|
330
|
+
"intended_change": "noop",
|
|
331
|
+
"replacement_code": "noop",
|
|
332
|
+
"acceptance_criteria": ["noop"],
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
"current_content": "noop\n",
|
|
336
|
+
}
|
|
337
|
+
monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(spec_payload)))
|
|
338
|
+
|
|
339
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
340
|
+
groq_bugteam_spec.run_spec_mode_main()
|
|
341
|
+
|
|
342
|
+
captured = capsys.readouterr()
|
|
343
|
+
emitted_outcome = json.loads(captured.out)
|
|
344
|
+
assert "error" in emitted_outcome
|
|
345
|
+
assert "GROQ_API_KEY" in emitted_outcome["error"]
|
|
346
|
+
assert exit_info.value.code != 0
|
package/skills/bugteam/SKILL.md
CHANGED
|
@@ -124,6 +124,10 @@ TeamCreate(
|
|
|
124
124
|
|
|
125
125
|
**Roles (spawned per loop, not here):** bugfind → `code-quality-agent` opus (4.7) at xhigh effort; bugfix → `clean-coder` opus (4.7) at xhigh effort. `model="opus"` resolves to Opus 4.7 on the Anthropic API and runs at the model's default `xhigh` effort level — see [`CONSTRAINTS.md`](CONSTRAINTS.md) § **Opus 4.7 at xhigh effort for both teammates** for rationale. **Display:** inherit `teammateMode` from `~/.claude.json`. Reference subagent types by name when spawning teammates ([`sources.md`](sources.md) § Referencing subagent types when spawning teammates).
|
|
126
126
|
|
|
127
|
+
**Optional Groq-backed FIX path (explicit opt-in only):** the default flow above always uses Opus teammates. A separate optional path exists only when the user explicitly sets `BUGTEAM_FIX_IMPLEMENTER=groq-coder` before invocation: spawn the FIX teammate with `subagent_type="groq-coder"`. Before Step 3, `groq_bugteam.py` loads `packages/claude-dev-env/.env` when that file exists (gitignored; start from `packages/claude-dev-env/.env.example`). If `GROQ_API_KEY` is still unset after that load, stop and prompt the user to create `packages/claude-dev-env/.env` from the example path above—do not continue the Groq path without a key. Any other `BUGTEAM_FIX_IMPLEMENTER` value (or unset) keeps `clean-coder` on Opus. The FIX spawn XML in [`PROMPTS.md`](PROMPTS.md) is identical for both implementers.
|
|
128
|
+
|
|
129
|
+
**`--bugbot-retrigger` flag:** when present on the `/bugteam` invocation, after every successful FIX push in Step 3, post an additional `bugbot run` issue comment via the Step 2.5 issue-comments fallback endpoint (`POST .../issues/{issue}/comments`) to re-trigger Cursor's bugbot on the new commit. Omit when the flag is absent.
|
|
130
|
+
|
|
127
131
|
**Loop state (lead; not a single script):**
|
|
128
132
|
|
|
129
133
|
```bash
|
|
@@ -11,3 +11,19 @@ Expanded material that used to live inline in `SKILL.md`. Load a file when the o
|
|
|
11
11
|
| [`teardown-publish-permissions.md`](teardown-publish-permissions.md) | Utility scripts note, teardown, PR description rewrite, revoke, final report |
|
|
12
12
|
|
|
13
13
|
Canonical documentation quotes: [`../sources.md`](../sources.md).
|
|
14
|
+
|
|
15
|
+
## Retired: pre-push-review skill
|
|
16
|
+
|
|
17
|
+
The `pre-push-review` skill was retired. Its mechanical checks are now covered automatically by the expanded code-rules enforcer and the git hooks installed via `npx claude-dev-env`.
|
|
18
|
+
|
|
19
|
+
**What replaced what:**
|
|
20
|
+
|
|
21
|
+
- **Mechanical pre-push checks** (magic values, boolean naming, imports, constants location, and other CODE_RULES checks) — handled by the `code_rules_enforcer.py` PreToolUse hook (blocks at write time) and by the git pre-push hook installed via `npx claude-dev-env`. The git pre-push hook is the gate that runs at `git push` time; no manual invocation is needed.
|
|
22
|
+
|
|
23
|
+
- **`/qbug`** — a full PR audit-fix cycle that spawns subagents, runs multiple audit loops, and produces a structured report. It is NOT a lightweight pre-push gate. Do not use `/qbug` as a substitute for `git push` (the hook fires automatically). Use `/qbug` when you want a thorough multi-loop review of a PR before requesting human review.
|
|
24
|
+
|
|
25
|
+
References updated:
|
|
26
|
+
- `skills/pr-review-responder/SKILL.md` — Rule 6 and checklist item updated to reference the git pre-push hook
|
|
27
|
+
- `commands/plan.md` — Phase 5 step 10 updated to reference the git pre-push hook
|
|
28
|
+
- `hooks/github-action/pre-push-review.yml` — deleted (workflow no longer needed)
|
|
29
|
+
- `hooks/github-action/test_workflow.py` — deleted alongside the workflow
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Markdown assertion tests for bugteam SKILL.md additive options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _read_skill_text() -> str:
|
|
9
|
+
skill_path = pathlib.Path(__file__).parent / "SKILL.md"
|
|
10
|
+
return skill_path.read_text(encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_skill_references_fix_implementer_env_var():
|
|
14
|
+
skill_text = _read_skill_text()
|
|
15
|
+
assert "BUGTEAM_FIX_IMPLEMENTER" in skill_text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_skill_names_default_implementer_subagent_type():
|
|
19
|
+
skill_text = _read_skill_text()
|
|
20
|
+
assert "clean-coder" in skill_text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_skill_names_optional_groq_implementer_subagent_type():
|
|
24
|
+
skill_text = _read_skill_text()
|
|
25
|
+
assert "groq-coder" in skill_text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_skill_documents_bugbot_retrigger_flag():
|
|
29
|
+
skill_text = _read_skill_text()
|
|
30
|
+
assert "--bugbot-retrigger" in skill_text
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monitor-open-prs
|
|
3
|
+
description: >-
|
|
4
|
+
Discover every open pull request across the jl-cmd/* and JonEcho/*
|
|
5
|
+
owner scopes, spawn /bugteam on each in parallel with the Groq-backed
|
|
6
|
+
FIX implementer (BUGTEAM_FIX_IMPLEMENTER=groq-coder) and the bugbot
|
|
7
|
+
re-trigger flag (--bugbot-retrigger), wrap the session in `bws run`
|
|
8
|
+
to inject GROQ_API_KEY, and poll Cursor's bugbot replies after
|
|
9
|
+
convergence so any post-Groq findings loop back through /bugteam.
|
|
10
|
+
Requires CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1. Triggers:
|
|
11
|
+
'/monitor-open-prs', 'sweep the open PRs', 'groq-bugteam the backlog'.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Monitor Open PRs
|
|
15
|
+
|
|
16
|
+
**Core principle:** One sweep covers every open PR across both owner scopes. Claude discovers PRs live via `gh search prs`, dispatches `/bugteam` per PR with `BUGTEAM_FIX_IMPLEMENTER=groq-coder` and `--bugbot-retrigger`, then polls Cursor's bugbot replies until each PR is quiet for a full backoff cycle.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- When this skill applies — refusal cases and trigger conditions
|
|
21
|
+
- Discovery — live `gh search prs` across both owner scopes
|
|
22
|
+
- Wrapping — `bws run` for GROQ_API_KEY injection
|
|
23
|
+
- Dispatch — `/bugteam <numbers...>` with groq-coder + retrigger
|
|
24
|
+
- Post-convergence polling — bugbot replies and re-invocation
|
|
25
|
+
- `scripts/discover_open_prs.py` — the discovery helper
|
|
26
|
+
|
|
27
|
+
## When this skill applies
|
|
28
|
+
|
|
29
|
+
`/monitor-open-prs` authorizes one full sweep over all open PRs in both owner scopes.
|
|
30
|
+
|
|
31
|
+
Refusals — first match wins; respond with the quoted line exactly and stop:
|
|
32
|
+
|
|
33
|
+
- **Agent teams not enabled.** Check `claude config get env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` and `~/.claude/settings.json`. If neither is `"1"`: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 not set. /monitor-open-prs requires the agent teams feature.`
|
|
34
|
+
- **bws not on PATH.** `bws not installed. /monitor-open-prs injects GROQ_API_KEY via Bitwarden Secrets Manager.`
|
|
35
|
+
- **gh not authenticated.** `gh auth status failed. /monitor-open-prs relies on existing gh credentials.`
|
|
36
|
+
- **Dirty tree on the caller's repo.** `Uncommitted changes detected. Stash, commit, or revert before /monitor-open-prs.`
|
|
37
|
+
- **Required subagents missing.** Confirm `code-quality-agent`, `clean-coder`, and `groq-coder` exist. Else: `Required subagent type <name> not installed.`
|
|
38
|
+
|
|
39
|
+
## Discovery
|
|
40
|
+
|
|
41
|
+
Call `scripts/discover_open_prs.discover_open_prs(all_owners=["jl-cmd", "JonEcho"])` to merge the live open-PR list across both scopes. The helper runs `gh search prs --owner <owner> --state open --json number,repository,url,headRefName,baseRefName` once per owner and flattens the result to a uniform dict shape with keys `number`, `owner`, `repo`, `head_ref`, `base_ref`, `url`. Empty scopes contribute empty lists; an entirely empty sweep returns `[]` and exits cleanly.
|
|
42
|
+
|
|
43
|
+
## Secret Wrapping
|
|
44
|
+
|
|
45
|
+
Every `/bugteam` dispatch runs inside `bws run` so `GROQ_API_KEY` is injected from Bitwarden Secrets Manager without touching the filesystem. The project and secret UUIDs are fixed for this skill:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bws run \
|
|
49
|
+
--project-id c69cedc5-aea1-4aa8-b350-b4300145d978 \
|
|
50
|
+
-- \
|
|
51
|
+
env BUGTEAM_FIX_IMPLEMENTER=groq-coder \
|
|
52
|
+
/bugteam --bugbot-retrigger <pr_numbers...>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `bws run` subshell resolves the project's secrets and exports them for the wrapped command. The `GROQ_API_KEY` secret's UUID inside that project is `b7e99a7f-2ecc-42b3-99a5-b434010622f9`. GitHub auth is not sourced through `bws` — existing `gh auth` credentials carry the session.
|
|
56
|
+
|
|
57
|
+
## Dispatch
|
|
58
|
+
|
|
59
|
+
For each discovered PR:
|
|
60
|
+
|
|
61
|
+
1. Resolve the PR's repo checkout (existing worktree or fresh `git clone`).
|
|
62
|
+
2. From that checkout, invoke `/bugteam <pr_number>` under the `bws run` wrapper above.
|
|
63
|
+
3. The `BUGTEAM_FIX_IMPLEMENTER=groq-coder` env var routes the FIX role to the `groq-coder` subagent. The `--bugbot-retrigger` flag tells bugteam to post `bugbot run` as an issue comment after every successful FIX push so Cursor's bugbot re-evaluates the new commit.
|
|
64
|
+
4. Bugteam runs its own 10-loop audit/fix cycle per PR; this skill waits for each bugteam invocation to return before dispatching the next (or fanning out — see below).
|
|
65
|
+
|
|
66
|
+
**Fan-out (optional):** when the discovered list has more than one PR, the skill may spawn `/bugteam` dispatches in parallel by issuing multiple `Agent` calls in a single assistant message. Each dispatch operates in its own per-PR worktree (bugteam Step 1.1). Serialize when the caller sets an explicit `--serial` flag.
|
|
67
|
+
|
|
68
|
+
## Post-Convergence Polling
|
|
69
|
+
|
|
70
|
+
After a `/bugteam` invocation returns (converged, cap reached, stuck, or error), the PR may accumulate new Cursor bugbot comments within minutes. Poll for them:
|
|
71
|
+
|
|
72
|
+
1. Baseline: capture `since_timestamp` as the PR's last commit timestamp.
|
|
73
|
+
2. Every 60 seconds, run `gh pr view <pr_number> --json comments --jq '.comments[] | select(.createdAt > "<since_timestamp>") | select(.author.login | test("bugbot|cursor";"i"))'`.
|
|
74
|
+
3. Back off: 60s → 120s → 240s → 480s → 960s. If five successive polls return empty, exit polling for this PR.
|
|
75
|
+
4. If bugbot posts a new finding in any poll, re-invoke `/bugteam <pr_number>` via the same `bws run` wrapper with the bugbot finding text seeded into the invocation's `bugs_to_fix` preamble. Reset the backoff.
|
|
76
|
+
|
|
77
|
+
### Polling Cost and Cadence
|
|
78
|
+
|
|
79
|
+
The five-step backoff sums to `60 + 120 + 240 + 480 + 960 = 1860` seconds (~31 minutes) of idle polling per PR before the skill declares bugbot quiet. A sweep over ten open PRs therefore retains up to ~5 wall-clock hours of bugbot-watch time beyond the active `/bugteam` work. Callers who need faster turnaround should pass `--serial` to disable fan-out (so polling starts only after the previous PR finishes) or accept the tradeoff: the backoff exists specifically to catch late bugbot analyses that can take several minutes to appear after a push.
|
|
80
|
+
|
|
81
|
+
## Final Report
|
|
82
|
+
|
|
83
|
+
After every PR has exited polling, emit:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/monitor-open-prs sweep summary
|
|
87
|
+
PRs discovered: <N>
|
|
88
|
+
jl-cmd/*: <count>
|
|
89
|
+
JonEcho/*: <count>
|
|
90
|
+
PRs converged clean: <count>
|
|
91
|
+
PRs hit 10-loop cap: <count>
|
|
92
|
+
PRs stuck: <count>
|
|
93
|
+
PRs errored: <count>
|
|
94
|
+
Bugbot re-triggers fired: <count>
|
|
95
|
+
Total Groq tokens consumed: <approx from /bugteam outcome summaries>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Non-Negotiable Guardrails
|
|
99
|
+
|
|
100
|
+
- Never run `/bugteam` without `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` active.
|
|
101
|
+
- Never source secrets outside `bws run` — no `.env` files, no shell history, no logs.
|
|
102
|
+
- Never pass `--no-verify` or `--no-gpg-sign` to git in any dispatched bugteam run.
|
|
103
|
+
- Never open a PR from this skill; only comment on existing ones.
|
|
104
|
+
- Never merge or close PRs; the skill is read + audit + patch only.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Discover every open pull request across a list of owner scopes.
|
|
2
|
+
|
|
3
|
+
Shells out to ``gh search prs --owner <owner> --state open --json ...`` once per
|
|
4
|
+
owner, parses the JSON output, and flattens each entry to a uniform dict shape.
|
|
5
|
+
The module is stateless, takes no filesystem side effects, and exposes a single
|
|
6
|
+
``discover_open_prs`` entry point consumed by the monitor-open-prs skill.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_gh_search_argv(owner: str) -> list[str]:
|
|
16
|
+
gh_command_name = "gh"
|
|
17
|
+
search_subcommand = ("search", "prs")
|
|
18
|
+
owner_flag = "--owner"
|
|
19
|
+
state_flag = "--state"
|
|
20
|
+
state_open_value = "open"
|
|
21
|
+
json_fields_flag = "--json"
|
|
22
|
+
json_fields_value = "number,repository,url,headRefName,baseRefName"
|
|
23
|
+
return [
|
|
24
|
+
gh_command_name,
|
|
25
|
+
*search_subcommand,
|
|
26
|
+
owner_flag,
|
|
27
|
+
owner,
|
|
28
|
+
state_flag,
|
|
29
|
+
state_open_value,
|
|
30
|
+
json_fields_flag,
|
|
31
|
+
json_fields_value,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def split_repository_name(name_with_owner: str) -> tuple[str, str]:
|
|
36
|
+
repo_separator = "/"
|
|
37
|
+
if repo_separator not in name_with_owner:
|
|
38
|
+
return "", name_with_owner
|
|
39
|
+
owner_segment, repository_segment = name_with_owner.split(repo_separator, 1)
|
|
40
|
+
return owner_segment, repository_segment
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def flatten_pr_entry(raw_pr_entry: dict) -> dict:
|
|
44
|
+
name_with_owner = raw_pr_entry.get("repository", {}).get("nameWithOwner", "")
|
|
45
|
+
owner_segment, repository_segment = split_repository_name(name_with_owner)
|
|
46
|
+
return {
|
|
47
|
+
"number": raw_pr_entry.get("number"),
|
|
48
|
+
"owner": owner_segment,
|
|
49
|
+
"repo": repository_segment,
|
|
50
|
+
"head_ref": raw_pr_entry.get("headRefName", ""),
|
|
51
|
+
"base_ref": raw_pr_entry.get("baseRefName", ""),
|
|
52
|
+
"url": raw_pr_entry.get("url", ""),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fetch_open_prs_for_owner(owner: str) -> list[dict]:
|
|
57
|
+
search_argv = build_gh_search_argv(owner)
|
|
58
|
+
completed_process = subprocess.run(
|
|
59
|
+
search_argv, check=True, capture_output=True, text=True
|
|
60
|
+
)
|
|
61
|
+
raw_pr_entries = json.loads(completed_process.stdout)
|
|
62
|
+
return [flatten_pr_entry(each_entry) for each_entry in raw_pr_entries]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def discover_open_prs(all_owners: list[str]) -> list[dict]:
|
|
66
|
+
all_discovered: list[dict] = []
|
|
67
|
+
for each_owner in all_owners:
|
|
68
|
+
all_discovered.extend(fetch_open_prs_for_owner(each_owner))
|
|
69
|
+
return all_discovered
|