claude-dev-env 1.29.3 → 1.30.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 (41) hide show
  1. package/agents/code-quality-agent.md +279 -24
  2. package/agents/groq-coder.md +111 -0
  3. package/commands/plan.md +4 -5
  4. package/hooks/blocking/code_rules_enforcer.py +775 -8
  5. package/hooks/blocking/destructive_command_blocker.py +149 -12
  6. package/hooks/blocking/test_code_rules_enforcer.py +751 -0
  7. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
  8. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
  9. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
  10. package/hooks/blocking/test_destructive_command_blocker.py +281 -4
  11. package/hooks/git-hooks/test_config.py +9 -3
  12. package/hooks/git-hooks/test_gate_utils.py +9 -3
  13. package/hooks/git-hooks/test_pre_commit.py +9 -3
  14. package/hooks/git-hooks/test_pre_push.py +9 -3
  15. package/hooks/validators/run_all_validators.py +76 -3
  16. package/hooks/validators/test_output_formatter.py +4 -16
  17. package/hooks/validators/test_run_all_validators.py +22 -0
  18. package/hooks/validators/test_run_all_validators_integration.py +2 -11
  19. package/package.json +1 -1
  20. package/scripts/config/groq_bugteam_config.py +104 -0
  21. package/scripts/config/test_groq_bugteam_config.py +11 -0
  22. package/scripts/config/test_spec_implementer_prompt.py +36 -0
  23. package/scripts/groq_bugteam.README.md +2 -0
  24. package/scripts/groq_bugteam.py +74 -15
  25. package/scripts/groq_bugteam_dotenv.py +40 -0
  26. package/scripts/groq_bugteam_spec.py +226 -0
  27. package/scripts/test_groq_bugteam.py +143 -5
  28. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
  29. package/scripts/test_groq_bugteam_dotenv.py +66 -0
  30. package/scripts/test_groq_bugteam_spec.py +346 -0
  31. package/skills/bugteam/SKILL.md +4 -0
  32. package/skills/bugteam/reference/README.md +16 -0
  33. package/skills/bugteam/test_skill_additions.py +30 -0
  34. package/skills/monitor-open-prs/SKILL.md +104 -0
  35. package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
  36. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
  37. package/skills/monitor-open-prs/test_skill_contract.py +43 -0
  38. package/skills/pr-review-responder/SKILL.md +10 -8
  39. package/hooks/github-action/pre-push-review.yml +0 -27
  40. package/hooks/github-action/test_workflow.py +0 -33
  41. 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
@@ -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