claude-dev-env 1.36.2 → 1.37.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 (70) hide show
  1. package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
  2. package/_shared/pr-loop/scripts/preflight.py +242 -20
  3. package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
  4. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
  5. package/hooks/blocking/code_rules_enforcer.py +269 -23
  6. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
  7. package/hooks/config/test_unused_module_import_constants.py +48 -0
  8. package/hooks/config/unused_module_import_constants.py +41 -0
  9. package/package.json +1 -1
  10. package/skills/bg-agent/SKILL.md +69 -0
  11. package/skills/bugteam/CONSTRAINTS.md +10 -19
  12. package/skills/bugteam/PROMPTS.md +3 -3
  13. package/skills/bugteam/SKILL.md +103 -202
  14. package/skills/bugteam/SKILL_EVALS.md +75 -114
  15. package/skills/bugteam/reference/README.md +2 -4
  16. package/skills/bugteam/reference/design-rationale.md +3 -8
  17. package/skills/bugteam/reference/team-setup.md +11 -19
  18. package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
  19. package/skills/bugteam/scripts/config/__init__.py +0 -0
  20. package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
  21. package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
  22. package/skills/bugteam/sources.md +1 -25
  23. package/skills/bugteam/test_skill_additions.py +4 -13
  24. package/skills/fresh-branch/SKILL.md +71 -0
  25. package/skills/gotcha/SKILL.md +73 -0
  26. package/skills/monitor-open-prs/SKILL.md +4 -37
  27. package/skills/monitor-open-prs/test_skill_contract.py +0 -5
  28. package/skills/pr-converge/SKILL.md +60 -1298
  29. package/skills/pr-converge/reference/convergence-gates.md +118 -0
  30. package/skills/pr-converge/reference/examples.md +76 -0
  31. package/skills/pr-converge/reference/fix-protocol.md +54 -0
  32. package/skills/pr-converge/reference/ground-rules.md +13 -0
  33. package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
  34. package/skills/pr-converge/reference/per-tick.md +201 -0
  35. package/skills/pr-converge/reference/state-schema.md +19 -0
  36. package/skills/pr-converge/reference/stop-conditions.md +26 -0
  37. package/skills/pr-converge/scripts/README.md +36 -9
  38. package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
  39. package/skills/pr-converge/scripts/config/pr_converge_constants.py +58 -5
  40. package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
  41. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
  42. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
  43. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
  44. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
  45. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
  46. package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
  47. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
  48. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
  49. package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
  50. package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
  51. package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
  52. package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
  53. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
  54. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
  55. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
  56. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
  57. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
  58. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
  59. package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
  60. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
  61. package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
  62. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
  63. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
  64. package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
  65. package/skills/bugteam/test_team_lifecycle.py +0 -103
  66. package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
  67. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
  68. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
  69. package/skills/pr-converge/test_team_lifecycle.py +0 -56
  70. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
@@ -0,0 +1,107 @@
1
+ """Tests for reviewer_specs.
2
+
3
+ Covers:
4
+ - each ReviewerSpec instance carries the documented login_filter_substring
5
+ - bugbot_spec.classify_review uses the dirty-body regex
6
+ - copilot_spec.classify_review dispatches off review state plus body
7
+ - claude_spec.classify_review dispatches off review state plus body
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+
17
+ def _load_module() -> ModuleType:
18
+ module_path = Path(__file__).parent / "reviewer_specs.py"
19
+ spec = importlib.util.spec_from_file_location("reviewer_specs", module_path)
20
+ assert spec is not None
21
+ assert spec.loader is not None
22
+ module = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(module)
24
+ return module
25
+
26
+
27
+ reviewer_specs_module = _load_module()
28
+
29
+
30
+ def test_bugbot_spec_uses_cursor_login_filter_substring() -> None:
31
+ assert reviewer_specs_module.bugbot_spec.login_filter_substring == "cursor"
32
+
33
+
34
+ def test_copilot_spec_uses_copilot_login_filter_substring() -> None:
35
+ assert reviewer_specs_module.copilot_spec.login_filter_substring == "copilot"
36
+
37
+
38
+ def test_claude_spec_uses_claude_login_filter_substring() -> None:
39
+ assert reviewer_specs_module.claude_spec.login_filter_substring == "claude"
40
+
41
+
42
+ def test_bugbot_classify_returns_dirty_when_body_matches_findings_pattern() -> None:
43
+ review_payload = {
44
+ "body": "Cursor Bugbot has reviewed your changes and found 2 potential issues.",
45
+ }
46
+ assert reviewer_specs_module.bugbot_spec.classify_review(review_payload) == "dirty"
47
+
48
+
49
+ def test_bugbot_classify_returns_clean_when_body_lacks_findings_pattern() -> None:
50
+ review_payload = {
51
+ "body": "Bugbot reviewed your changes and found no new issues!",
52
+ }
53
+ assert reviewer_specs_module.bugbot_spec.classify_review(review_payload) == "clean"
54
+
55
+
56
+ def test_copilot_classify_returns_clean_when_state_is_approved() -> None:
57
+ review_payload = {"state": "APPROVED", "body": "lgtm"}
58
+ assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
59
+
60
+
61
+ def test_copilot_classify_returns_dirty_when_state_is_changes_requested() -> None:
62
+ review_payload = {"state": "CHANGES_REQUESTED", "body": "fix this"}
63
+ assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "dirty"
64
+
65
+
66
+ def test_copilot_classify_returns_dirty_when_state_is_commented_with_body() -> None:
67
+ review_payload = {"state": "COMMENTED", "body": "minor nit"}
68
+ assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "dirty"
69
+
70
+
71
+ def test_copilot_classify_returns_clean_when_state_is_commented_with_empty_body() -> (
72
+ None
73
+ ):
74
+ review_payload = {"state": "COMMENTED", "body": ""}
75
+ assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
76
+
77
+
78
+ def test_copilot_classify_returns_clean_when_state_is_unknown() -> None:
79
+ review_payload = {"state": "DISMISSED", "body": "ignored"}
80
+ assert reviewer_specs_module.copilot_spec.classify_review(review_payload) == "clean"
81
+
82
+
83
+ def test_claude_classify_returns_clean_when_state_is_approved() -> None:
84
+ review_payload = {"state": "APPROVED", "body": "lgtm"}
85
+ assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
86
+
87
+
88
+ def test_claude_classify_returns_dirty_when_state_is_changes_requested() -> None:
89
+ review_payload = {"state": "CHANGES_REQUESTED", "body": "fix this"}
90
+ assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "dirty"
91
+
92
+
93
+ def test_claude_classify_returns_dirty_when_state_is_commented_with_body() -> None:
94
+ review_payload = {"state": "COMMENTED", "body": "minor nit"}
95
+ assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "dirty"
96
+
97
+
98
+ def test_claude_classify_returns_clean_when_state_is_commented_with_empty_body() -> (
99
+ None
100
+ ):
101
+ review_payload = {"state": "COMMENTED", "body": ""}
102
+ assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
103
+
104
+
105
+ def test_claude_classify_returns_clean_when_state_is_unknown() -> None:
106
+ review_payload = {"state": "DISMISSED", "body": "ignored"}
107
+ assert reviewer_specs_module.claude_spec.classify_review(review_payload) == "clean"
@@ -1,37 +1,39 @@
1
1
  # ScheduleWakeup loop pacing (pr-converge)
2
2
 
3
- Load this document when **`ScheduleWakeup` is available** in the parent harness session and you use it for converge **loop pacing** (primary /
4
- orchestrated-teams path). Follow it for **every** instruction below that depends on that choice. Shared bugbot / bugteam / Fix protocol steps
5
- stay in the main `SKILL.md`.
3
+ Load this document for converge **loop pacing**. The pre-flight in `SKILL.md`
4
+ guarantees `ScheduleWakeup` is available before any tick runs. Shared bugbot
5
+ / bugteam / Fix protocol steps stay in the main `SKILL.md`.
6
6
 
7
7
  ## Session behavior
8
8
 
9
- Call `ScheduleWakeup` from this same session so the next tick fires back into **this** transcript with the prior tick's state line and PR
10
- context still addressable.
9
+ Call `ScheduleWakeup` from this same session so the next tick fires back into **this** transcript with the prior tick's state line and PR context still addressable.
11
10
 
12
- ## Step 4 — `ScheduleWakeup` branch
11
+ ## Calling ScheduleWakeup
13
12
 
14
- At end of tick (unless convergence or another stop condition already omitted pacing), call `ScheduleWakeup` with:
13
+ At end of tick (unless convergence or another stop condition already
14
+ omitted pacing), call `ScheduleWakeup` with:
15
15
 
16
- - `delaySeconds: 270` whenever bugbot was just re-triggered (whether by Step 3 directly, by the Fix protocol's mandatory re-trigger, or by
17
- BUGTEAM branch 1's same-tick re-trigger). Bugbot finishes a review in 1–4 minutes, so 270s stays under the 5-minute prompt-cache TTL while
18
- giving a margin past bugbot's typical upper bound. The single exception is the BUGBOT inline-lag branch in Step 2 of the main skill, which
19
- uses `delaySeconds: 60` because no re-trigger fired and the only thing being awaited is GitHub's inline-comments API catching up.
20
- - `reason`: one short sentence on what is being awaited, including the current `phase` and `bugbot_clean_at` SHA when set.
21
- - `prompt: "/pr-converge"` re-enters this skill on the next firing with default loop semantics (no need for the user to type `/loop`). If
22
- the parent harness requires the `/loop` wrapper for wakeups to execute, `prompt: "/loop /pr-converge"` is equivalent.
16
+ - `delaySeconds: 270` whenever bugbot was just re-triggered (by the
17
+ bugbot re-trigger in `../reference/per-tick.md`, by Fix protocol's
18
+ mandatory re-trigger, or by BUGTEAM's same-tick re-trigger). Bugbot
19
+ finishes a review in 1–4 minutes, so 270s stays under the 5-minute
20
+ prompt-cache TTL with margin past bugbot's typical upper bound. The
21
+ exception is the BUGBOT inline-lag branch (see below).
22
+ - `reason`: one short sentence on what is being awaited, including the
23
+ current `phase` and `bugbot_clean_at` SHA when set.
24
+ - `prompt: "/pr-converge"` — re-enters this skill on the next firing.
23
25
 
24
- ## BUGBOT inline-lag (this path only)
26
+ ## BUGBOT inline-lag
25
27
 
26
- When Step 2 BUGBOT branch c routes to API lag and you are on **this** pacing path: complete Step 4 with `ScheduleWakeup` using `delaySeconds:
27
- 60` (lag is short-lived).
28
+ See [`../reference/per-tick.md`](../reference/per-tick.md) the BUGBOT
29
+ inline-lag branch (review body says findings, inline API returns zero
30
+ matching for `current_head`) uses `delaySeconds: 90` because no
31
+ re-trigger fired and only GitHub's inline-comments API needs to catch up.
28
32
 
29
33
  ## Convergence
30
34
 
31
- On back-to-back clean: **omit** further `ScheduleWakeup` calls. Do not start the AHK auto-typer for loop pacing when this path is the active
32
- pacer.
35
+ On convergence: **omit** further `ScheduleWakeup` calls.
33
36
 
34
- ## Stop / safety (this path)
37
+ ## Stop / safety
35
38
 
36
- On hard blockers or user stop: omit `ScheduleWakeup` per main skill **Stop conditions**. If the session never used AHK for pacing,
37
- skip AHK shutdown commands in the companion AHK workflow.
39
+ On hard blockers or user stop: omit `ScheduleWakeup` per main skill **Stop conditions**.
@@ -1,113 +0,0 @@
1
- # Bugteam — Path A workflow (orchestrated teams)
2
-
3
- Load when bugteam `SKILL.md` **Path routing** selects **Path A** (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` equals **`1`** after trim). Execute **after** shared `SKILL.md` steps through **Step 2 loop state**; then use this file for **harness-only** steps. Shared gate scripts, Step 3 cycle numbering, Step 2.5 `jq`/`gh` payload shapes, `PROMPTS.md`, and outcome XML rules remain in **`SKILL.md`**.
4
-
5
- ## Step 2 harness — lifecycle, `TeamCreate`, and team metadata
6
-
7
- **This session is the lead.** Step 2 first resolves the team lifecycle from the [Team lifecycle](../SKILL.md#team-lifecycle-path-a-only) table in `SKILL.md`, then either creates or attaches to a team.
8
-
9
- **Lifecycle resolution (read once, then apply):**
10
-
11
- ```python
12
- import os
13
- import re
14
-
15
- mode = os.environ.get("BUGTEAM_TEAM_LIFECYCLE", "auto").strip().lower() or "auto"
16
- attached_team_name = os.environ.get("BUGTEAM_TEAM_NAME", "").strip()
17
-
18
- if mode == "attach" and not attached_team_name:
19
- raise RuntimeError(
20
- "BUGTEAM_TEAM_LIFECYCLE=attach requires BUGTEAM_TEAM_NAME to name an existing team."
21
- )
22
- ```
23
-
24
- The mode then drives Step 2's `TeamCreate` decision:
25
-
26
- - **`mode == "attach"`** — skip `TeamCreate` entirely. Set `team_name = attached_team_name`, `team_owned = false`. Continue to teammate spawns using that `team_name`.
27
-
28
- - **`mode == "owned"`** — call `TeamCreate(team_name=<computed_team_name>, ...)` (form below). On the runtime error matching `Already leading team "(.+)"\.` → **fail** with `Already leading team <existing>; rerun with BUGTEAM_TEAM_LIFECYCLE=attach BUGTEAM_TEAM_NAME=<existing>`. On success, set `team_owned = true`.
29
-
30
- - **`mode == "auto"`** (default) — call `TeamCreate(team_name=<computed_team_name>, ...)`. On the same runtime error, parse the existing team name with the regex `r'Already leading team "([^"]+)"'`, set `team_name = <existing>`, `team_owned = false`, and continue without retrying `TeamCreate`. On success, set `team_owned = true`. Both branches honor §Team name below.
31
-
32
- **`TeamCreate` call shape (only when `mode != "attach"` and the auto branch did not already attach):**
33
-
34
- ```
35
- TeamCreate(
36
- team_name="<computed_team_name>",
37
- description="Bugteam audit/fix loop for PR <number> (<owner>/<repo>)",
38
- agent_type="team-lead"
39
- )
40
- ```
41
-
42
- **Team name:** For a single-PR invocation use `bugteam-pr-<number>-<YYYYMMDDHHMMSS>`. For a multi-PR invocation use `bugteam-<YYYYMMDDHHMMSS>`. The timestamp is captured once at team-creation time. Apply the no-PR fallback (`bugteam-<sanitized-head>-<YYYYMMDDHHMMSS>`) only when no PR resolves at all. `TeamCreate` implements natural-language team creation ([`sources.md`](../sources.md) § Team creation in natural language).
43
-
44
- **Sanitize head branch (no-PR only):** replace characters outside `[A-Za-z0-9._-]` with `-` (e.g. `feat/foo*bar` → `feat-foo-bar`). Apply once; reuse everywhere below.
45
-
46
- **`<team_temp_dir>`:** `Path(tempfile.gettempdir()) / team_name` (lead resolves once to an absolute path; every shell gets that literal string).
47
-
48
- **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).
49
-
50
- **Optional Groq-backed FIX path (explicit opt-in 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.
51
-
52
- **`--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.
53
-
54
- ## Step 2.5 — who posts (Path A)
55
-
56
- **Bugfind** posts one `POST .../pulls/<n>/reviews` per loop after audit. **Bugfix** posts `.../comments/<id>/replies` after push. The **lead’s** only PR write before Step 4.5 is unchanged from `SKILL.md` (Step 4.5 body edit).
57
-
58
- ## AUDIT spawn (Path A)
59
-
60
- After shared setup in `SKILL.md` (`mkdir`, `gh pr diff`):
61
-
62
- ```
63
- Agent(
64
- subagent_type="code-quality-agent",
65
- name="bugfind-pr<N>-loop<L>",
66
- team_name="<team_name>",
67
- model="opus",
68
- description="Bugfind audit PR <N> loop <L>",
69
- prompt="<audit XML; see PROMPTS.md>"
70
- )
71
- ```
72
-
73
- Fresh `Agent` each loop; teammate context excludes lead history ([`sources.md`](../sources.md) § Teammate context isolation). [`PROMPTS.md`](../PROMPTS.md): XML + outcome schema. Lead reads `.bugteam-pr<N>-loop<L>.outcomes.xml`, fills `loop_comment_index` per `SKILL.md`.
74
-
75
- **Shutdown:** If `Agent` returned and the teammate already ended, skip. Otherwise:
76
-
77
- ```
78
- SendMessage(
79
- to="bugfind-pr<N>-loop<L>",
80
- message={"type": "shutdown_request", "reason": "audit PR <N> loop <L> complete; outcome XML captured"}
81
- )
82
- ```
83
-
84
- `approve: false` → `error: bugfind teammate refused shutdown` → Step 4 then 5 per `SKILL.md`.
85
-
86
- **Parallel auditors (`loop_count >= 4`):** after three full audit/fix rounds without convergence, issue three `Agent` calls in one assistant message with `team_name="<team_name>"` per `SKILL.md` § parallel variant naming (`-a` / `-b` / `-c`). Shutdown: parallel `SendMessage` to `b` and `c`, then `a`.
87
-
88
- ## FIX spawn (Path A)
89
-
90
- ```
91
- Agent(
92
- subagent_type="clean-coder",
93
- name="bugfix-pr<N>-loop<L>",
94
- team_name="<team_name>",
95
- model="opus",
96
- description="Bugfix PR <N> loop <L>",
97
- prompt="<fix XML; see PROMPTS.md>"
98
- )
99
- ```
100
-
101
- **Shutdown:** same pattern as AUDIT; `SendMessage(to="bugfix-pr<N>-loop<L>", message={"type": "shutdown_request", "reason": "fix PR <N> loop <L> complete; commit <sha7> pushed"})`. `approve: false` → `error: bugfix teammate refused shutdown` → Step 4 then 5.
102
-
103
- Verify and outcome handling: unchanged from `SKILL.md` § FIX action (**Verify**, stuck message, [`PROMPTS.md`](../PROMPTS.md)).
104
-
105
- ## Step 4 harness — after worktree remove, before shared `rmtree`
106
-
107
- Run **after** `SKILL.md` § Step 4 step 1 (`git worktree remove` for each PR).
108
-
109
- 1. For each live teammate **spawned by this invocation**: `SendMessage(to="<name>", message={"type": "shutdown_request", "reason": "bugteam cycle ending"})`. `approve: false` on cleanup → log and continue. Teammates that pre-existed an attached team (mode `attach` or `auto` after the attach branch) are owned by the orchestrator, not this invocation — leave them alone.
110
-
111
- 2. `TeamDelete()` — **only when `team_owned=true`** (set in Step 2 lifecycle resolution). When `team_owned=false`, **skip `TeamDelete`** and log `cleanup: skipped TeamDelete (team not owned by this invocation; lifecycle=<mode>)`. The orchestrator that originally created the team owns teardown — see [Team lifecycle](../SKILL.md#team-lifecycle-path-a-only).
112
-
113
- Then continue with `SKILL.md` § Step 4 step 3 (`<team_temp_dir>` cleanup, also gated on `team_owned`).
@@ -1,48 +0,0 @@
1
- # Bugteam — Path B workflow (Task harness)
2
-
3
- Load when bugteam `SKILL.md` **Path routing** selects **Path B** (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is not exactly **`1`** after trim — typical Cursor IDE). Execute **after** shared `SKILL.md` steps through **Step 2 loop state**. **Do not** run `TeamCreate` or `TeamDelete`. Harness substitutions vs Path A:
4
-
5
- | Path A | Path B |
6
- | --- | --- |
7
- | `TeamCreate(...)` | **Omit.** |
8
- | `Agent(..., team_name=..., ...)` | **`Task`** (or host-equivalent) with the same `model` and prompt contracts as Path A — **omit** `team_name`. `subagent_type` follows this file's **AUDIT** / **FIX spawn** sections (Claude Code: `code-quality-agent` / `clean-coder`; Cursor: `code-quality-agent` / `generalPurpose` + mandatory `clean-coder.md` **Read** on FIX when the enum rejects `clean-coder`). |
9
- | `SendMessage` shutdown | **Omit.** Await Task completion. |
10
- | `TeamDelete()` | **Omit.** |
11
- | Three parallel `Agent` (`loop_count >= 4`) | Three parallel **`Task`** with `subagent_type="code-quality-agent"`; merge outcomes in the lead like Path A `-a`/`-b`/`-c`. |
12
-
13
- ## Step 2 harness — Path B
14
-
15
- No `TeamCreate`. After shared `SKILL.md` **Step 1** completes (PR scope **and** `<team_temp_dir>/pr-<N>/` per Step 1 items 1–3 there), use the same `team_name` string only as a **logical label** for paths under that `<team_temp_dir>`; do not pass `team_name` into spawns.
16
-
17
- **`--bugbot-retrigger` flag:** same as Path A [`workflow-path-a-orchestrated-teams.md`](workflow-path-a-orchestrated-teams.md) § Step 2 harness — the **lead** posts the issue comment after each successful FIX push when the flag is present.
18
-
19
- ## Step 2.5 — who posts (Path B)
20
-
21
- The **lead** runs the **same** `gh api` / `jq` sequences as `SKILL.md` **Step 2.5** after reading each **`Task`** handoff / outcome XML — same JSON shapes and anchor rules; only **who executes the shell** changes.
22
-
23
- ## AUDIT spawn (Path B)
24
-
25
- After shared setup in `SKILL.md` (`mkdir`, `gh pr diff`):
26
-
27
- `Task` with `subagent_type="code-quality-agent"`, **omit** `team_name`, same `model`, `description`, and `prompt="<audit XML; see PROMPTS.md>"` as Path A [`workflow-path-a-orchestrated-teams.md`](workflow-path-a-orchestrated-teams.md) § AUDIT spawn.
28
-
29
- Fresh **Task** each loop (clean-room intent: do not reuse prior Task transcript as audit input). Lead reads `.bugteam-pr<N>-loop<L>.outcomes.xml`, fills `loop_comment_index` per `SKILL.md`.
30
-
31
- **Parallel auditors (`loop_count >= 4`):** three **`Task`** calls with `subagent_type="code-quality-agent"` in one assistant message; merge outcomes in the lead exactly as Path A documents for variants `-a`/`-b`/`-c`. Await all three Tasks — no `SendMessage`.
32
-
33
- ## FIX spawn (Path B)
34
-
35
- **Hosts that accept `clean-coder` as a `Task` subtype (typical Claude Code):** `Task` with `subagent_type="clean-coder"` (or `subagent_type="groq-coder"` when `BUGTEAM_FIX_IMPLEMENTER=groq-coder` per Path A optional Groq rules), **omit** `team_name`, same fields otherwise as Path A [`workflow-path-a-orchestrated-teams.md`](workflow-path-a-orchestrated-teams.md) § FIX spawn. Await Task completion — no `SendMessage`.
36
-
37
- **Cursor and other hosts with a fixed `Task` enum (no `clean-coder` value):** use `Task` with `subagent_type: "generalPurpose"` and put the **same** FIX obligations from Path A into the **`prompt`**, after a mandatory first step to **Read** the clean-coder agent markdown: macOS/Linux `$HOME/.claude/agents/clean-coder.md`, Windows `%USERPROFILE%\.claude\agents\clean-coder.md`. State that file is binding for naming, TDD when behavior changes, hooks, one commit, and scope. Do **not** use a bare `generalPurpose` prompt without that Read. Same bundle as [`../../pr-converge/SKILL.md`](../../pr-converge/SKILL.md) Fix protocol **Implement**. If `Task` cannot run, stop and notify the user.
38
-
39
- Verify and outcome handling: unchanged from `SKILL.md` § FIX action.
40
- ## Step 4 harness — after worktree remove, before shared `rmtree`
41
-
42
- Run **after** `SKILL.md` § Step 4 step 1 (`git worktree remove` for each PR).
43
-
44
- **Omit** teammate `SendMessage` rounds and **`TeamDelete()`**. Then continue with `SKILL.md` § Step 4 step 3 (shared `rmtree` on `<team_temp_dir>`).
45
-
46
- ## Clean-room note
47
-
48
- Path B approximates Path A isolation by spawning a **new** Task per AUDIT (and per FIX) with the same prompt contract as Path A, without reusing prior Task context as audit input.
@@ -1,103 +0,0 @@
1
- """Markdown assertion tests for bugteam team-lifecycle decoupling.
2
-
3
- Locks in the contract that the bugteam skill must:
4
- - support three team lifecycle modes (`owned`, `attach`, `auto`)
5
- - default to `auto` (back-compat for solo invocations, safe for nested ones)
6
- - skip `TeamDelete` when the invocation did not create the team
7
- - parse the runtime's `Already leading team "<name>"` error and attach
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import pathlib
13
-
14
-
15
- def _read(relative_path: str) -> str:
16
- here = pathlib.Path(__file__).parent
17
- return (here / relative_path).read_text(encoding="utf-8")
18
-
19
-
20
- def _skill_text() -> str:
21
- return _read("SKILL.md")
22
-
23
-
24
- def _path_a_text() -> str:
25
- return _read("reference/workflow-path-a-orchestrated-teams.md")
26
-
27
-
28
- def _constraints_text() -> str:
29
- return _read("CONSTRAINTS.md")
30
-
31
-
32
- def test_skill_documents_three_team_lifecycle_modes():
33
- skill_text = _skill_text()
34
- assert "BUGTEAM_TEAM_LIFECYCLE" in skill_text
35
- assert "owned" in skill_text
36
- assert "attach" in skill_text
37
- assert "auto" in skill_text
38
-
39
-
40
- def test_skill_documents_auto_as_default_lifecycle():
41
- skill_text = _skill_text()
42
- assert "default" in skill_text.lower()
43
- assert "BUGTEAM_TEAM_LIFECYCLE" in skill_text
44
- auto_default_phrases = [
45
- "default: `auto`",
46
- "default `auto`",
47
- "defaults to `auto`",
48
- "defaults to auto",
49
- "default to `auto`",
50
- ]
51
- assert any(phrase in skill_text for phrase in auto_default_phrases)
52
-
53
-
54
- def test_skill_documents_BUGTEAM_TEAM_NAME_env_for_attach_mode():
55
- skill_text = _skill_text()
56
- assert "BUGTEAM_TEAM_NAME" in skill_text
57
-
58
-
59
- def test_path_a_workflow_handles_already_leading_team_error():
60
- workflow_text = _path_a_text()
61
- assert 'Already leading team "' in workflow_text
62
- assert "team_owned" in workflow_text
63
-
64
-
65
- def test_path_a_workflow_step_4_skips_team_delete_when_not_owned():
66
- workflow_text = _path_a_text()
67
- assert "TeamDelete" in workflow_text
68
- assert "team_owned" in workflow_text
69
- skip_phrases = [
70
- "skip `TeamDelete`",
71
- "skip TeamDelete",
72
- "omit `TeamDelete`",
73
- "omit TeamDelete",
74
- "do not call `TeamDelete`",
75
- "do not call TeamDelete",
76
- ]
77
- assert any(phrase in workflow_text for phrase in skip_phrases)
78
-
79
-
80
- def test_path_a_workflow_documents_attach_mode_reuses_team_name():
81
- workflow_text = _path_a_text()
82
- assert "BUGTEAM_TEAM_NAME" in workflow_text
83
- assert "attach" in workflow_text
84
-
85
-
86
- def test_constraints_lead_only_cleanup_includes_team_owned():
87
- constraints_text = _constraints_text()
88
- assert "team_owned" in constraints_text
89
-
90
-
91
- def test_constraints_warn_against_owned_mode_inside_orchestrator():
92
- constraints_text = _constraints_text()
93
- assert "orchestrator" in constraints_text.lower()
94
- assert "attach" in constraints_text
95
-
96
-
97
- def test_skill_md_physical_lines_fit_eighty_column_limit():
98
- skill_text = _skill_text()
99
- for each_line_number, each_physical_line in enumerate(skill_text.splitlines(), 1):
100
- assert len(each_physical_line) <= 80, (
101
- "SKILL.md line %s exceeds 80 columns (%s chars)"
102
- % (each_line_number, len(each_physical_line))
103
- )
@@ -1,46 +0,0 @@
1
- """Markdown assertion tests for monitor-open-prs team lifecycle.
2
-
3
- Locks in the contract that the sweep must:
4
- - own a single long-lived team for every dispatched /bugteam
5
- - pass attach mode + the team name into each per-PR /bugteam dispatch
6
- - tear down only after all PR polling completes
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import pathlib
12
-
13
-
14
- def _skill_text() -> str:
15
- here = pathlib.Path(__file__).parent
16
- return (here / "SKILL.md").read_text(encoding="utf-8")
17
-
18
-
19
- def test_skill_creates_one_team_for_the_whole_sweep():
20
- skill_text = _skill_text()
21
- assert "TeamCreate" in skill_text
22
- sweep_phrases = [
23
- "one team for the whole sweep",
24
- "single team for the sweep",
25
- "single long-lived team",
26
- ]
27
- assert any(phrase in skill_text for phrase in sweep_phrases)
28
-
29
-
30
- def test_skill_passes_attach_lifecycle_to_each_bugteam_dispatch():
31
- skill_text = _skill_text()
32
- assert "BUGTEAM_TEAM_LIFECYCLE" in skill_text
33
- assert "attach" in skill_text
34
- assert "BUGTEAM_TEAM_NAME" in skill_text
35
-
36
-
37
- def test_skill_tears_down_team_after_polling_completes():
38
- skill_text = _skill_text()
39
- assert "TeamDelete" in skill_text
40
- teardown_phrases = [
41
- "after every PR has exited polling",
42
- "after polling completes",
43
- "after the sweep",
44
- "after all polling",
45
- ]
46
- assert any(phrase in skill_text for phrase in teardown_phrases)
@@ -1,136 +0,0 @@
1
- """Open a follow-up draft PR addressing Copilot findings from the parent PR.
2
-
3
- Subprocess sequence:
4
-
5
- 1. ``gh pr view <parent_number> --json baseRefName`` to resolve the parent's base ref.
6
- 2. ``git fetch origin <head_sha>`` to make the SHA available locally.
7
- 3. ``git switch -c <new_branch> <head_sha>`` to create the follow-up branch off ``head_sha``.
8
- 4. ``git push -u origin <new_branch>`` to publish it.
9
- 5. ``gh pr create --draft --base <base_ref> --head <new_branch> --title <...> --body-file <findings_file>``
10
- per the gh-body-file rule.
11
-
12
- Returns the trimmed PR URL emitted by ``gh pr create`` on stdout.
13
- """
14
-
15
- import argparse
16
- import json
17
- import subprocess
18
- import sys
19
- from pathlib import Path
20
-
21
- if str(Path(__file__).resolve().parent) not in sys.path:
22
- sys.path.insert(0, str(Path(__file__).resolve().parent))
23
-
24
- from evict_cached_config_modules import evict_cached_config_modules
25
-
26
- evict_cached_config_modules()
27
-
28
- from config.pr_converge_constants import (
29
- COPILOT_FOLLOWUP_BRANCH_TEMPLATE,
30
- COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE,
31
- COPILOT_FOLLOWUP_SHORT_SHA_LENGTH,
32
- GH_REPO_ARG_TEMPLATE,
33
- PR_BASE_REF_FIELDS,
34
- )
35
-
36
-
37
- def open_followup_copilot_pr(
38
- *,
39
- owner: str,
40
- repo: str,
41
- parent_number: int,
42
- head: str,
43
- findings_file: Path,
44
- ) -> str:
45
- """Create the follow-up branch + draft PR; return the new PR URL."""
46
- repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
47
- parent_base_ref = _resolve_parent_base_ref(
48
- parent_number=parent_number, repo_arg=repo_arg
49
- )
50
- short_sha = head[:COPILOT_FOLLOWUP_SHORT_SHA_LENGTH]
51
- new_branch_name = COPILOT_FOLLOWUP_BRANCH_TEMPLATE.format(
52
- parent_number=parent_number, short_sha=short_sha
53
- )
54
- _run_checked(["git", "fetch", "origin", head])
55
- _run_checked(["git", "switch", "-c", new_branch_name, head])
56
- _run_checked(["git", "push", "-u", "origin", new_branch_name])
57
- pr_title = COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE.format(parent_number=parent_number)
58
- completed = _run_checked(
59
- [
60
- "gh",
61
- "pr",
62
- "create",
63
- "--repo",
64
- repo_arg,
65
- "--draft",
66
- "--base",
67
- parent_base_ref,
68
- "--head",
69
- new_branch_name,
70
- "--title",
71
- pr_title,
72
- "--body-file",
73
- str(findings_file),
74
- ]
75
- )
76
- return completed.stdout.strip()
77
-
78
-
79
- def _resolve_parent_base_ref(*, parent_number: int, repo_arg: str) -> str:
80
- completed = _run_checked(
81
- [
82
- "gh",
83
- "pr",
84
- "view",
85
- str(parent_number),
86
- "--repo",
87
- repo_arg,
88
- "--json",
89
- PR_BASE_REF_FIELDS,
90
- ]
91
- )
92
- parent_pr_metadata = json.loads(completed.stdout)
93
- base_ref_name_field = parent_pr_metadata.get("baseRefName")
94
- if not isinstance(base_ref_name_field, str):
95
- raise TypeError(
96
- f"gh pr view baseRefName field is not str: {type(base_ref_name_field).__name__}"
97
- )
98
- return base_ref_name_field
99
-
100
-
101
- def _run_checked(all_command_arguments: list[str]) -> subprocess.CompletedProcess:
102
- return subprocess.run(
103
- all_command_arguments,
104
- capture_output=True,
105
- check=True,
106
- text=True,
107
- encoding="utf-8",
108
- errors="replace",
109
- )
110
-
111
-
112
- def main() -> int:
113
- parser = argparse.ArgumentParser(description=__doc__)
114
- parser.add_argument("--owner", required=True)
115
- parser.add_argument("--repo", required=True)
116
- parser.add_argument(
117
- "--parent-number", required=True, type=int, dest="parent_number"
118
- )
119
- parser.add_argument("--head", required=True)
120
- parser.add_argument(
121
- "--findings-file", required=True, type=Path, dest="findings_file"
122
- )
123
- parsed_arguments = parser.parse_args()
124
- new_pr_url = open_followup_copilot_pr(
125
- owner=parsed_arguments.owner,
126
- repo=parsed_arguments.repo,
127
- parent_number=parsed_arguments.parent_number,
128
- head=parsed_arguments.head,
129
- findings_file=parsed_arguments.findings_file,
130
- )
131
- sys.stdout.write(f"{new_pr_url}\n")
132
- return 0
133
-
134
-
135
- if __name__ == "__main__":
136
- sys.exit(main())