claude-dev-env 1.35.0 → 1.36.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/agents/clean-coder.md +109 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +451 -39
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +182 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/SKILL.md +364 -154
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +103 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +1206 -131
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +56 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -14,23 +14,23 @@ Evals are split into two layers. Both layers run against the same trace but carr
|
|
|
14
14
|
|
|
15
15
|
## Ironclad invariants (Layer A, apply to every eval)
|
|
16
16
|
|
|
17
|
-
Each invariant cites the normative section or companion file it derives from.
|
|
17
|
+
Each invariant cites the normative section or companion file it derives from. **Path A vs Path B:** `SKILL.md` **Path routing** splits harnesses. Invariants **I-1, I-3, I-4, I-7, I-9, I-11 (teammate shutdown → `TeamDelete` prefix), I-13** apply to **Path A only**. **Path B** substitutes per [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md): no `TeamCreate` / `TeamDelete`; parallel auditors use parallel **`Task`** calls; **I-12 Path B** — the **lead** runs Step 2.5 `gh api` posts. **I-2, I-5, I-6, I-8, I-10** apply to **both** paths (revoke once; fresh spawn per loop; `model="opus"` on audit/fix workers; cap; read outcome XML).
|
|
18
18
|
|
|
19
19
|
| # | Invariant | Citation |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| I-1 | `Bash` invoking `scripts/grant_project_claude_permissions.py` precedes every `TeamCreate`. | `SKILL.md` § Step 0 |
|
|
22
|
-
| I-2 | `Bash` invoking `scripts/revoke_project_claude_permissions.py` runs exactly once per invocation, after
|
|
23
|
-
| I-3 | Exactly one `TeamCreate` and exactly one `TeamDelete` per invocation. | `SKILL.md` § Step 2; § Step 4 |
|
|
24
|
-
| I-4 | Before `TeamDelete`, no teammate remains active without cleanup
|
|
25
|
-
| I-5 | `Agent` calls are fresh per loop
|
|
26
|
-
| I-6 | Both audit and fix
|
|
27
|
-
| I-7 | `TeamDelete()` is called with no arguments.
|
|
21
|
+
| I-1 | **Path A:** `Bash` invoking `scripts/grant_project_claude_permissions.py` precedes every `TeamCreate`. **Path B:** grant precedes first audit **`Task`**. | `SKILL.md` § Step 0; § Path routing |
|
|
22
|
+
| I-2 | `Bash` invoking `scripts/revoke_project_claude_permissions.py` runs exactly once per invocation on every exit path, **after** teardown that applies to that path (`TeamDelete` only on Path A). | `SKILL.md` § Step 5 |
|
|
23
|
+
| I-3 | **Path A:** Exactly one `TeamCreate` and exactly one `TeamDelete` per invocation. **Path B:** zero `TeamCreate` / `TeamDelete`. | `SKILL.md` § Step 2; § Step 4 |
|
|
24
|
+
| I-4 | **Path A:** Before `TeamDelete`, no teammate remains active without cleanup (self-terminated `Agent` or `SendMessage` shutdown). | `SKILL.md` § AUDIT/FIX shutdown; § Step 4 |
|
|
25
|
+
| I-5 | **Path A:** `Agent` calls are fresh per loop. **Path B:** `Task` calls for audit/fix are fresh per loop (same `name` discipline where the host exposes naming). | `CONSTRAINTS.md` — **Fresh teammate per loop**; deltas **Clean-room note** |
|
|
26
|
+
| I-6 | Both paths: audit and fix worker spawns pass `model="opus"` on `Agent` **or** `Task` as documented in `SKILL.md` § AUDIT/FIX. | `SKILL.md` § Step 2 (**Roles**); `CONSTRAINTS.md` — **Opus 4.7 at xhigh effort for both teammates** |
|
|
27
|
+
| I-7 | **Path A:** `TeamDelete()` is called with no arguments. **Path B:** omit. | `SKILL.md` § Step 4 |
|
|
28
28
|
| I-8 | Loop count ≤ 10 audits. 11th audit never fires. | `SKILL.md` YAML `description` (10-loop cap); § Step 3 (**Pre-audit** / **FIX** increment rules) |
|
|
29
|
-
| I-9 | From loop 4 onward without convergence,
|
|
29
|
+
| I-9 | **Path A:** From loop 4 onward without convergence, three parallel `Agent` calls in one message. **Path B:** three parallel **`Task`** calls. | `SKILL.md` § AUDIT action (**Parallel auditors**); `reference/workflow-path-b-task-harness.md` |
|
|
30
30
|
| I-10 | Lead reads `.bugteam-loop-<N>.outcomes.xml` with the `Read` tool after each audit, before the next action. | `SKILL.md` § AUDIT action |
|
|
31
|
-
| I-11 |
|
|
32
|
-
| I-12 | Lead never posts PR review
|
|
33
|
-
| I-13 | Only the
|
|
31
|
+
| I-11 | **Path A:** `git worktree remove` each PR → teammate shutdowns → `TeamDelete` → `rmtree` `<team_temp_dir>` → Step 4.5 → revoke. **Path B:** `git worktree remove` each PR → (omit shutdown / `TeamDelete`) → `rmtree` → Step 4.5 → revoke. | `SKILL.md` § Step 4; § Step 4.5; § Step 5; `reference/workflow-path-a-orchestrated-teams.md` § Step 4; `reference/workflow-path-b-task-harness.md` § Step 4 |
|
|
32
|
+
| I-12 | **Path A:** Lead never posts PR review / finding / fix replies except Step 4.5 body. **Path B:** Lead performs Step 2.5 posts per deltas; Step 4.5 unchanged. | `CONSTRAINTS.md` — **Audit/fix comment posting** |
|
|
33
|
+
| I-13 | **Path A:** Only the lead invokes `TeamCreate`; every teammate `Agent(..., team_name=...)`. **Path B:** no `TeamCreate`; `Task` spawns omit `team_name`. | `CONSTRAINTS.md` — **Path A — orchestrator-only `TeamCreate`**; `reference/workflow-path-b-task-harness.md` |
|
|
34
34
|
|
|
35
35
|
Any eval failing one or more Layer A invariants fails the run.
|
|
36
36
|
|
|
@@ -46,23 +46,25 @@ The harness does not yet exist; this document defines its contract.
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
-
## Eval 1 —
|
|
49
|
+
## Eval 1 — Path B: agent teams env unset (Task harness, not a refusal)
|
|
50
50
|
|
|
51
51
|
**Scenario.** `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is unset in both `claude config` and `~/.claude/settings.json`.
|
|
52
52
|
|
|
53
53
|
**Trigger.** `/bugteam`
|
|
54
54
|
|
|
55
|
-
**Layer A invariants.**
|
|
55
|
+
**Layer A invariants.** Path B subset (I-2, I-5, I-6, I-8, I-10; I-1/I-3/I-4/I-7/I-9/I-11/I-13 N/A or Path-B-shaped).
|
|
56
56
|
|
|
57
|
-
**Layer B predicted trace.**
|
|
57
|
+
**Layer B predicted trace (Path B smoke).**
|
|
58
58
|
1. `Bash("claude config get env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS")` → empty.
|
|
59
|
-
2. `
|
|
60
|
-
3. No
|
|
59
|
+
2. `Bash("python .../grant_project_claude_permissions.py")` runs (Step 0).
|
|
60
|
+
3. **No** `TeamCreate`.
|
|
61
|
+
4. At least one `Task(subagent_type="code-quality-agent", ...)` or host-equivalent for AUDIT; FIX rounds use the host-appropriate FIX `Task` from `workflow-path-b-task-harness.md` § **FIX spawn** (`clean-coder` subtype on Claude Code when accepted; `generalPurpose` + clean-coder **Read** preamble on Cursor when the enum rejects `clean-coder`).
|
|
62
|
+
5. `Bash("python .../revoke_project_claude_permissions.py")` on exit.
|
|
61
63
|
|
|
62
64
|
**Pass criteria.**
|
|
63
|
-
-
|
|
64
|
-
- Zero `TeamCreate`, `
|
|
65
|
-
-
|
|
65
|
+
- **No** refusal string about missing agent teams.
|
|
66
|
+
- Zero `TeamCreate`, zero `TeamDelete`, zero teammate `SendMessage` shutdowns.
|
|
67
|
+
- Non-zero `Task` (or `Agent` without `team_name` only if the host maps Path B that way) carrying **`code-quality-agent`** / **fix worker under the clean-coder contract** (subtype `clean-coder` where accepted, else `generalPurpose` + `clean-coder.md` Read per `workflow-path-b-task-harness.md`).
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
@@ -95,11 +97,11 @@ The harness does not yet exist; this document defines its contract.
|
|
|
95
97
|
|
|
96
98
|
---
|
|
97
99
|
|
|
98
|
-
## Eval 5 — Happy path: converges in 2 loops
|
|
100
|
+
## Eval 5 — Happy path: converges in 2 loops (Path A fixture)
|
|
99
101
|
|
|
100
|
-
**Scenario.** PR #42 contains three P1 bugs all addressable by the mock fix teammate. Loop 1 audit returns 3 findings; loop 1 fix commits cleanly; loop 2 audit returns zero findings.
|
|
102
|
+
**Scenario.** PR #42 contains three P1 bugs all addressable by the mock fix teammate. Loop 1 audit returns 3 findings; loop 1 fix commits cleanly; loop 2 audit returns zero findings. **`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`** (Path A — `TeamCreate` + teammate `Agent`).
|
|
101
103
|
|
|
102
|
-
**Layer A invariants.** I-1, I-2, I-3, I-4, I-5, I-6, I-7, I-10, I-11, I-12.
|
|
104
|
+
**Layer A invariants.** Path A: I-1, I-2, I-3, I-4, I-5, I-6, I-7, I-10, I-11, I-12, I-13.
|
|
103
105
|
|
|
104
106
|
**Layer B predicted trace.**
|
|
105
107
|
|
|
@@ -4,6 +4,8 @@ Expanded material that used to live inline in `SKILL.md`. Load a file when the o
|
|
|
4
4
|
|
|
5
5
|
| File | Domain |
|
|
6
6
|
|------|--------|
|
|
7
|
+
| [`workflow-path-a-orchestrated-teams.md`](workflow-path-a-orchestrated-teams.md) | **Path A only** — `TeamCreate`, `Agent` + `team_name`, `SendMessage`, `TeamDelete`, who posts Step 2.5 |
|
|
8
|
+
| [`workflow-path-b-task-harness.md`](workflow-path-b-task-harness.md) | **Path B only** — `Task` harness (no `TeamCreate` / `TeamDelete`, lead Step 2.5 `gh api`, Step 4 omissions) |
|
|
7
9
|
| [`design-rationale.md`](design-rationale.md) | Why agent teams (clean-room), table-of-contents habit, when `/bugteam` applies, refusal reasons |
|
|
8
10
|
| [`team-setup.md`](team-setup.md) | Permissions grant (`CLAUDE_SKILL_DIR`), PR scope, `TeamCreate`, team name / sanitization / temp dir / roles / loop state |
|
|
9
11
|
| [`github-pr-reviews.md`](github-pr-reviews.md) | Per-loop reviews, `jq` + `gh api` payloads, anchors, fallbacks, REST endpoints |
|
|
@@ -16,10 +16,10 @@ Repeat until an exit condition fires.
|
|
|
16
16
|
1. From the repository root, run the gate script (align `--base` with the PR base branch from Step 1, e.g. `origin/main` or `origin/develop`):
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
python "${CLAUDE_SKILL_DIR}/scripts/
|
|
19
|
+
python "${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/code_rules_gate.py" --base origin/<baseRefName>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`git merge-base` + `git diff --name-only` live inside the script; see [
|
|
22
|
+
`git merge-base` + `git diff --name-only` live inside the script; see [`../../../_shared/pr-loop/scripts/README.md`](../../../_shared/pr-loop/scripts/README.md) for what lives under this directory, and [`../../../_shared/pr-loop/code-rules-gate.md`](../../../_shared/pr-loop/code-rules-gate.md) for gate-only merge-base / invocation semantics. The lead runs this (not a teammate).
|
|
23
23
|
|
|
24
24
|
2. If exit code **0** → continue to step 2.5 (AUDIT spawn) below.
|
|
25
25
|
3. If exit code **non-zero** → spawn a new **clean-coder** teammate — **standards-fix pass** — with instructions: read the script’s stderr, edit the repo until a **re-run** of the **same** gate command exits **0**, then one commit, `git push`, shutdown. Repeat standards-fix spawns until the gate exits **0** or **5** failed gate rounds (each round = one teammate session after a non-zero gate). If still non-zero after 5 rounds → exit reason = `error: code rules gate failed pre-audit`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Utility scripts (progressive disclosure)
|
|
4
4
|
|
|
5
|
-
Fragile or repeatable shell sequences belong in `scripts/`. Anthropic: [Progressive disclosure patterns](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#progressive-disclosure-patterns) — utility scripts are **executed**, not loaded into context as primary reading.
|
|
5
|
+
Fragile or repeatable shell sequences belong in `_shared/pr-loop/scripts/`. Anthropic: [Progressive disclosure patterns](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#progressive-disclosure-patterns) — utility scripts are **executed**, not loaded into context as primary reading. Script inventory and entry points: [`../../../_shared/pr-loop/scripts/README.md`](../../../_shared/pr-loop/scripts/README.md). Gate-only merge-base / diff semantics: [`../../../_shared/pr-loop/code-rules-gate.md`](../../../_shared/pr-loop/code-rules-gate.md).
|
|
6
6
|
|
|
7
7
|
### Pre-flight (recommended before Step 0)
|
|
8
8
|
|
|
@@ -0,0 +1,113 @@
|
|
|
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`).
|
|
@@ -0,0 +1,48 @@
|
|
|
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.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Reflow packages/claude-dev-env/skills/bugteam/SKILL.md to 80 columns.
|
|
2
|
+
|
|
3
|
+
Merge soft line breaks outside fenced blocks (space join; URL path fragments
|
|
4
|
+
joined without a space only inside unfinished markdown link targets), then
|
|
5
|
+
wrap with textwrap. Preserves fenced blocks verbatim.
|
|
6
|
+
|
|
7
|
+
Same algorithm as ``packages/claude-dev-env/skills/pr-converge/scripts/reflow_skill_md.py``
|
|
8
|
+
from https://github.com/jl-cmd/claude-code-config/pull/349 (branch
|
|
9
|
+
``cursor/pr-converge-skill-line-wrap-ecd1``); ``SKILL_PATH`` points at bugteam
|
|
10
|
+
``SKILL.md``. Link reference definitions (``[id]: url``) are treated as logical
|
|
11
|
+
line starts so they are not merged with prior paragraphs.
|
|
12
|
+
|
|
13
|
+
Run: python3 packages/claude-dev-env/skills/bugteam/scripts/reflow_skill_md.py
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import textwrap
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
MAX_WIDTH = 80
|
|
23
|
+
SKILL_PATH = Path(__file__).resolve().parent.parent / "SKILL.md"
|
|
24
|
+
|
|
25
|
+
ORDERED_RE = re.compile(r"^(\s*)(\d+\.\s)(.*)$")
|
|
26
|
+
BULLET_RE = re.compile(r"^(\s*)([-*]\s)(.*)$")
|
|
27
|
+
UNFINISHED_MD_LINK_TARGET = re.compile(r"\]\([^)]*$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def wrap_paragraph_plain(text: str) -> list[str]:
|
|
31
|
+
collapsed = " ".join(text.split())
|
|
32
|
+
if not collapsed:
|
|
33
|
+
return []
|
|
34
|
+
return textwrap.fill(
|
|
35
|
+
collapsed,
|
|
36
|
+
width=MAX_WIDTH,
|
|
37
|
+
break_long_words=False,
|
|
38
|
+
break_on_hyphens=False,
|
|
39
|
+
).splitlines()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def wrap_list_item(lead_ws: str, marker: str, body: str) -> list[str]:
|
|
43
|
+
collapsed = " ".join(body.split())
|
|
44
|
+
if not collapsed:
|
|
45
|
+
return [lead_ws + marker.rstrip()]
|
|
46
|
+
prefix = lead_ws + marker
|
|
47
|
+
subsequent = lead_ws + (" " * len(marker))
|
|
48
|
+
return textwrap.fill(
|
|
49
|
+
collapsed,
|
|
50
|
+
width=MAX_WIDTH,
|
|
51
|
+
initial_indent=prefix,
|
|
52
|
+
subsequent_indent=subsequent,
|
|
53
|
+
break_long_words=False,
|
|
54
|
+
break_on_hyphens=False,
|
|
55
|
+
).splitlines()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reflow_yaml_description_block(lines: list[str], body_start: int) -> tuple[list[str], int]:
|
|
59
|
+
body_parts: list[str] = []
|
|
60
|
+
index = body_start
|
|
61
|
+
while index < len(lines):
|
|
62
|
+
line = lines[index]
|
|
63
|
+
if line.strip() == "---":
|
|
64
|
+
index += 1
|
|
65
|
+
break
|
|
66
|
+
stripped = line.lstrip()
|
|
67
|
+
if stripped:
|
|
68
|
+
body_parts.append(stripped)
|
|
69
|
+
index += 1
|
|
70
|
+
merged = " ".join(body_parts)
|
|
71
|
+
wrapped = textwrap.fill(
|
|
72
|
+
merged,
|
|
73
|
+
width=MAX_WIDTH,
|
|
74
|
+
initial_indent=" ",
|
|
75
|
+
subsequent_indent=" ",
|
|
76
|
+
break_long_words=False,
|
|
77
|
+
break_on_hyphens=False,
|
|
78
|
+
)
|
|
79
|
+
return wrapped.splitlines(), index
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_table_line(line: str) -> bool:
|
|
83
|
+
return line.lstrip().startswith("|")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_link_reference_definition(stripped: str) -> bool:
|
|
87
|
+
return bool(re.match(r"^\[[^\]]+\]:\s+\S", stripped))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_new_logical_line(stripped: str) -> bool:
|
|
91
|
+
if not stripped:
|
|
92
|
+
return False
|
|
93
|
+
if stripped.startswith("```"):
|
|
94
|
+
return True
|
|
95
|
+
if stripped.startswith("#"):
|
|
96
|
+
return True
|
|
97
|
+
if stripped == "---":
|
|
98
|
+
return True
|
|
99
|
+
if is_table_line(stripped):
|
|
100
|
+
return True
|
|
101
|
+
if stripped.startswith("<example>") or stripped.startswith("</example>"):
|
|
102
|
+
return True
|
|
103
|
+
if is_link_reference_definition(stripped):
|
|
104
|
+
return True
|
|
105
|
+
if ORDERED_RE.match(stripped) or BULLET_RE.match(stripped):
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def merge_without_space(buffer: str, continuation: str) -> bool:
|
|
111
|
+
"""Join without space only for split markdown link URL paths."""
|
|
112
|
+
base = buffer.rstrip()
|
|
113
|
+
stripped = continuation.lstrip()
|
|
114
|
+
if not base or not stripped:
|
|
115
|
+
return False
|
|
116
|
+
if stripped.startswith("/") and UNFINISHED_MD_LINK_TARGET.search(base):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def merge_soft_breaks(lines: list[str]) -> list[str]:
|
|
122
|
+
output: list[str] = []
|
|
123
|
+
index = 0
|
|
124
|
+
in_fence = False
|
|
125
|
+
while index < len(lines):
|
|
126
|
+
raw = lines[index]
|
|
127
|
+
line = raw.rstrip("\n")
|
|
128
|
+
if line.lstrip().startswith("```"):
|
|
129
|
+
in_fence = not in_fence
|
|
130
|
+
output.append(line)
|
|
131
|
+
index += 1
|
|
132
|
+
continue
|
|
133
|
+
if in_fence:
|
|
134
|
+
output.append(line)
|
|
135
|
+
index += 1
|
|
136
|
+
continue
|
|
137
|
+
if line.strip() == "":
|
|
138
|
+
output.append(line)
|
|
139
|
+
index += 1
|
|
140
|
+
continue
|
|
141
|
+
buffer_line = line
|
|
142
|
+
index += 1
|
|
143
|
+
while index < len(lines):
|
|
144
|
+
next_raw = lines[index].rstrip("\n")
|
|
145
|
+
if next_raw.strip() == "":
|
|
146
|
+
break
|
|
147
|
+
if next_raw.lstrip().startswith("```"):
|
|
148
|
+
break
|
|
149
|
+
stripped_next = next_raw.lstrip()
|
|
150
|
+
if is_new_logical_line(stripped_next):
|
|
151
|
+
break
|
|
152
|
+
if merge_without_space(buffer_line, stripped_next):
|
|
153
|
+
buffer_line = buffer_line.rstrip() + stripped_next
|
|
154
|
+
else:
|
|
155
|
+
buffer_line = f"{buffer_line.rstrip()} {stripped_next}"
|
|
156
|
+
index += 1
|
|
157
|
+
output.append(buffer_line)
|
|
158
|
+
return output
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def reflow_merged_line(line: str) -> list[str]:
|
|
162
|
+
stripped = line.strip()
|
|
163
|
+
if stripped == "":
|
|
164
|
+
return [""]
|
|
165
|
+
if stripped.startswith("```"):
|
|
166
|
+
return [line]
|
|
167
|
+
if stripped.startswith("#"):
|
|
168
|
+
if len(stripped) <= MAX_WIDTH:
|
|
169
|
+
return [stripped]
|
|
170
|
+
title = stripped.lstrip("#").strip()
|
|
171
|
+
level = len(stripped) - len(stripped.lstrip("#"))
|
|
172
|
+
prefix = "#" * level + " "
|
|
173
|
+
return textwrap.fill(
|
|
174
|
+
title,
|
|
175
|
+
width=MAX_WIDTH,
|
|
176
|
+
initial_indent=prefix,
|
|
177
|
+
subsequent_indent=prefix,
|
|
178
|
+
break_long_words=False,
|
|
179
|
+
break_on_hyphens=False,
|
|
180
|
+
).splitlines()
|
|
181
|
+
if stripped == "---":
|
|
182
|
+
return ["---"]
|
|
183
|
+
if is_link_reference_definition(stripped):
|
|
184
|
+
return [stripped]
|
|
185
|
+
if is_table_line(stripped):
|
|
186
|
+
return [stripped]
|
|
187
|
+
if stripped.startswith("</example>"):
|
|
188
|
+
return [stripped]
|
|
189
|
+
if stripped.startswith("<example>"):
|
|
190
|
+
inner = stripped[len("<example>") :].strip()
|
|
191
|
+
if not inner:
|
|
192
|
+
return ["<example>"]
|
|
193
|
+
tag = "<example> "
|
|
194
|
+
subsequent = " " * len(tag)
|
|
195
|
+
return textwrap.fill(
|
|
196
|
+
" ".join(inner.split()),
|
|
197
|
+
width=MAX_WIDTH,
|
|
198
|
+
initial_indent=tag,
|
|
199
|
+
subsequent_indent=subsequent,
|
|
200
|
+
break_long_words=False,
|
|
201
|
+
break_on_hyphens=False,
|
|
202
|
+
).splitlines()
|
|
203
|
+
|
|
204
|
+
ordered = ORDERED_RE.match(line)
|
|
205
|
+
if ordered:
|
|
206
|
+
return wrap_list_item(ordered.group(1), ordered.group(2), ordered.group(3))
|
|
207
|
+
|
|
208
|
+
bullet = BULLET_RE.match(line)
|
|
209
|
+
if bullet:
|
|
210
|
+
return wrap_list_item(bullet.group(1), bullet.group(2), bullet.group(3))
|
|
211
|
+
|
|
212
|
+
return wrap_paragraph_plain(stripped)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def reflow_markdown_body(lines: list[str]) -> list[str]:
|
|
216
|
+
merged = merge_soft_breaks(lines)
|
|
217
|
+
output: list[str] = []
|
|
218
|
+
for each_line in merged:
|
|
219
|
+
if each_line.strip() == "":
|
|
220
|
+
output.append("")
|
|
221
|
+
continue
|
|
222
|
+
output.extend(reflow_merged_line(each_line))
|
|
223
|
+
return output
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def wrap_long_bash_fence_lines(lines: list[str]) -> list[str]:
|
|
227
|
+
"""Hard-wrap only ```bash fence bodies that still exceed MAX_WIDTH."""
|
|
228
|
+
output: list[str] = []
|
|
229
|
+
in_bash_fence = False
|
|
230
|
+
for line in lines:
|
|
231
|
+
stripped = line.lstrip()
|
|
232
|
+
if stripped.startswith("```"):
|
|
233
|
+
if not in_bash_fence:
|
|
234
|
+
lang = stripped[3:].strip().lower()
|
|
235
|
+
in_bash_fence = lang == "bash"
|
|
236
|
+
else:
|
|
237
|
+
in_bash_fence = False
|
|
238
|
+
output.append(line)
|
|
239
|
+
continue
|
|
240
|
+
if in_bash_fence and len(line) > MAX_WIDTH:
|
|
241
|
+
indent_len = len(line) - len(line.lstrip())
|
|
242
|
+
indent = line[:indent_len]
|
|
243
|
+
content = line.lstrip()
|
|
244
|
+
wrapped_segments: list[str] = []
|
|
245
|
+
rest = content
|
|
246
|
+
while len(rest) > MAX_WIDTH - len(indent):
|
|
247
|
+
room = MAX_WIDTH - len(indent) - 2
|
|
248
|
+
window = rest[:room]
|
|
249
|
+
break_at = window.rfind(" ")
|
|
250
|
+
if break_at <= 0:
|
|
251
|
+
break_at = room
|
|
252
|
+
piece = rest[:break_at].rstrip()
|
|
253
|
+
rest = rest[break_at:].lstrip()
|
|
254
|
+
wrapped_segments.append(indent + piece + " \\")
|
|
255
|
+
if rest:
|
|
256
|
+
wrapped_segments.append(indent + (" " if wrapped_segments else "") + rest)
|
|
257
|
+
output.extend(wrapped_segments)
|
|
258
|
+
else:
|
|
259
|
+
output.append(line)
|
|
260
|
+
return output
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main() -> None:
|
|
264
|
+
raw = SKILL_PATH.read_text(encoding="utf-8")
|
|
265
|
+
lines = raw.splitlines()
|
|
266
|
+
if not lines or lines[0].strip() != "---":
|
|
267
|
+
raise SystemExit("expected YAML front matter starting with ---")
|
|
268
|
+
|
|
269
|
+
out: list[str] = ["---"]
|
|
270
|
+
index = 1
|
|
271
|
+
while index < len(lines):
|
|
272
|
+
line = lines[index]
|
|
273
|
+
if line.startswith("description: >-"):
|
|
274
|
+
out.append(line)
|
|
275
|
+
index += 1
|
|
276
|
+
desc_lines, index = reflow_yaml_description_block(lines, index)
|
|
277
|
+
out.extend(desc_lines)
|
|
278
|
+
out.append("---")
|
|
279
|
+
break
|
|
280
|
+
out.append(line)
|
|
281
|
+
index += 1
|
|
282
|
+
|
|
283
|
+
body = reflow_markdown_body(lines[index:])
|
|
284
|
+
body = wrap_long_bash_fence_lines(body)
|
|
285
|
+
|
|
286
|
+
text = "\n".join(out + body) + "\n"
|
|
287
|
+
SKILL_PATH.write_text(text, encoding="utf-8", newline="\n")
|
|
288
|
+
|
|
289
|
+
all_lines = text.splitlines()
|
|
290
|
+
long_rows = [(i, len(ln)) for i, ln in enumerate(all_lines, 1) if len(ln) > MAX_WIDTH]
|
|
291
|
+
print("SKILL.md reflowed; lines:", len(all_lines))
|
|
292
|
+
print("lines longer than %d: %d" % (MAX_WIDTH, len(long_rows)))
|
|
293
|
+
if long_rows[:20]:
|
|
294
|
+
print("first long:", long_rows[:20])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
main()
|
|
@@ -10,9 +10,16 @@ def _read_skill_text() -> str:
|
|
|
10
10
|
return skill_path.read_text(encoding="utf-8")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _read_path_a_workflow_text() -> str:
|
|
14
|
+
workflow_path = (
|
|
15
|
+
pathlib.Path(__file__).parent / "reference" / "workflow-path-a-orchestrated-teams.md"
|
|
16
|
+
)
|
|
17
|
+
return workflow_path.read_text(encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
def test_skill_references_fix_implementer_env_var():
|
|
14
|
-
|
|
15
|
-
assert "BUGTEAM_FIX_IMPLEMENTER" in
|
|
21
|
+
workflow_text = _read_path_a_workflow_text()
|
|
22
|
+
assert "BUGTEAM_FIX_IMPLEMENTER" in workflow_text
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
def test_skill_names_default_implementer_subagent_type():
|
|
@@ -21,10 +28,12 @@ def test_skill_names_default_implementer_subagent_type():
|
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
def test_skill_names_optional_groq_implementer_subagent_type():
|
|
24
|
-
|
|
25
|
-
assert "groq-coder" in
|
|
31
|
+
workflow_text = _read_path_a_workflow_text()
|
|
32
|
+
assert "groq-coder" in workflow_text
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
def test_skill_documents_bugbot_retrigger_flag():
|
|
29
36
|
skill_text = _read_skill_text()
|
|
37
|
+
workflow_text = _read_path_a_workflow_text()
|
|
30
38
|
assert "--bugbot-retrigger" in skill_text
|
|
39
|
+
assert "--bugbot-retrigger" in workflow_text
|