claude-dev-env 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
|
|
40
40
|
- **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
|
|
41
41
|
|
|
42
|
+
## Tool Policies
|
|
43
|
+
- **context7:** Before writing code using any library/framework/SDK/API, call `resolve-library-id` then `query-docs` via Context7 MCP. Use the fetched docs to write code. Applies to all libs including React, Next.js, Django, Express, Prisma.
|
|
44
|
+
|
|
42
45
|
## Compaction
|
|
43
46
|
When compacting, always preserve:
|
|
44
47
|
- Active task and current goal
|
package/package.json
CHANGED
package/skills/bugteam/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ This file is 400+ lines. The list below is for the LLM reading this skill — pa
|
|
|
29
29
|
- Step 0 — Grant project permissions
|
|
30
30
|
- Step 1 — Resolve PR scope
|
|
31
31
|
- Step 2 — Create the agent team
|
|
32
|
-
- Step 2.5 — PR comment lifecycle (loop
|
|
32
|
+
- Step 2.5 — PR comment lifecycle (per-loop review with child finding comments, fix replies)
|
|
33
33
|
- Step 3 — The cycle (AUDIT ↔ FIX, decision table, exit conditions)
|
|
34
34
|
- Step 4 — Tear down the team and clean working tree
|
|
35
35
|
- Step 4.5 — Finalize the PR description (via pr-description-writer)
|
|
@@ -47,7 +47,7 @@ Refusal cases — check in order; first match short-circuits and stops:
|
|
|
47
47
|
|
|
48
48
|
- **Agent teams not enabled.** Check `claude config get env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` and `~/.claude/settings.json`. If neither sets it to `"1"`, respond: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 not set. /bugteam requires the agent teams feature. See https://code.claude.com/docs/en/agent-teams#enable-agent-teams.` and stop.
|
|
49
49
|
- **Claude Code version too old.** Run `claude --version`. If older than v2.1.32, respond: `Claude Code v<version> is older than the v2.1.32 minimum for agent teams. Upgrade first.` and stop.
|
|
50
|
-
- **
|
|
50
|
+
- **Missing PR or upstream diff.** Respond exactly: `No PR or upstream diff. /bugteam needs a target.` and stop.
|
|
51
51
|
- **Working tree dirty with uncommitted changes the user did not stage.** Respond: `Uncommitted changes detected. Stash, commit, or revert before /bugteam.` and stop. Reason: the fix teammate will commit the working tree, mixing user-uncommitted work into automated fixes.
|
|
52
52
|
- **Required subagents not installed.** Before Step 0, verify `code-quality-agent` and `clean-coder` subagent types exist in the available agents list. If either is missing, respond: `Required subagent type <name> not installed. /bugteam needs both code-quality-agent and clean-coder available.` and stop.
|
|
53
53
|
|
|
@@ -88,7 +88,7 @@ Same resolution path as `/findbugs`:
|
|
|
88
88
|
2. Fall back to `git merge-base HEAD origin/<default>` then `git diff <merge-base>...HEAD`.
|
|
89
89
|
3. Neither → refuse per the refusal cases above.
|
|
90
90
|
|
|
91
|
-
Capture: `<owner>/<repo>`, head branch, base branch, PR number, PR URL. This scope persists across every loop — `/bugteam`
|
|
91
|
+
Capture: `<owner>/<repo>`, head branch, base branch, PR number, PR URL. This scope persists across every loop — `/bugteam` runs to completion from the single up-front confirmation.
|
|
92
92
|
|
|
93
93
|
### Step 2: Create the agent team
|
|
94
94
|
|
|
@@ -97,8 +97,8 @@ This session is the **team lead**. Create a team using the agent teams feature.
|
|
|
97
97
|
Team specification:
|
|
98
98
|
|
|
99
99
|
- **Team name:** `bugteam-pr-<number>-<YYYYMMDDHHMMSS>` (or `bugteam-<sanitized-head-branch>-<YYYYMMDDHHMMSS>` if no PR). The timestamp is captured at team-creation time from the lead session and prevents two concurrent invocations on the same PR from colliding.
|
|
100
|
-
- **Branch-name sanitization (no-PR fallback only):** Before substituting `<head-branch>` into the team_name template, replace every character
|
|
101
|
-
- **Per-team temp directory (resolved once, reused everywhere):** After team_name is captured, resolve a portable absolute path with a Claude-side lookup using Python's `tempfile.gettempdir()`, which honors `TMPDIR`, `TEMP`, and `TMP` in the platform-correct order and falls back to `C:\Users\<user>\AppData\Local\Temp` on Windows or `/tmp` on Unix: `Path(tempfile.gettempdir()) / team_name` (requires `import tempfile`). The `team_name` value already carries the `bugteam-` prefix, so
|
|
100
|
+
- **Branch-name sanitization (no-PR fallback only):** Before substituting `<head-branch>` into the team_name template, replace every character outside `[A-Za-z0-9._-]` with `-`. The whitelist keeps safe portable filename characters only; OS-reserved and shell-special characters (`/ \ : * ? < > | "` plus ASCII control chars 0x00–0x1F) fall outside the whitelist and become `-`. Example: `feat/foo*bar` → `feat-foo-bar`; team_name becomes `bugteam-feat-foo-bar-<YYYYMMDDHHMMSS>`. Apply the sanitization when team_name is first assembled so every downstream use (team creation, scoped temp dir, cleanup) sees the safe form.
|
|
101
|
+
- **Per-team temp directory (resolved once, reused everywhere):** After team_name is captured, resolve a portable absolute path with a Claude-side lookup using Python's `tempfile.gettempdir()`, which honors `TMPDIR`, `TEMP`, and `TMP` in the platform-correct order and falls back to `C:\Users\<user>\AppData\Local\Temp` on Windows or `/tmp` on Unix: `Path(tempfile.gettempdir()) / team_name` (requires `import tempfile`). The `team_name` value already carries the `bugteam-` prefix, so keep it as-is here. Let `tempfile.gettempdir()` do the lookup; use its result directly. Capture the resolved absolute path as `<team_temp_dir>` and pass that literal path to every shell command that follows. Claude performs all temp-root resolution, so every shell (bash, cmd.exe, PowerShell) receives the same literal absolute value.
|
|
102
102
|
- **Roles defined up front (spawned per loop, not at team creation):**
|
|
103
103
|
- `bugfind` — uses teammate role `code-quality-agent`, model sonnet
|
|
104
104
|
- `bugfix` — uses teammate role `clean-coder`, model sonnet
|
|
@@ -123,38 +123,82 @@ loop_comment_index="" # reset at every AUDIT, see scope note be
|
|
|
123
123
|
|
|
124
124
|
Each entry: `{loop, finding_id, finding_comment_id, finding_comment_url, used_fallback, fix_status}`. Populated by AUDIT, consumed by FIX.
|
|
125
125
|
|
|
126
|
-
### Step 2.5: PR comment lifecycle (
|
|
126
|
+
### Step 2.5: PR comment lifecycle (one review per loop)
|
|
127
127
|
|
|
128
|
-
The team narrates its work to the PR via GitHub
|
|
128
|
+
The team narrates its work to the PR via a **GitHub pull-request review** per loop so findings render as a tree under a single parent review (like Cursor Bugbot). **Teammates own all PR comment posting** — bugfind posts the review (parent body + child finding comments in one batched POST), bugfix posts fix replies. All comment, review, and reply POSTs belong to the teammates. The lead's single PR-write action is the final description rewrite at Step 4.5 (via `pr-description-writer` agent).
|
|
129
129
|
|
|
130
|
-
- **
|
|
130
|
+
- **Per-loop review** — one `POST /pulls/<number>/reviews` per loop, posted by the bugfind teammate AFTER auditing. The review body is the loop header (with audit counts); the review's `comments[]` array holds one anchored finding per P0/P1/P2 finding. GitHub renders this as a single collapsible thread with each finding as a child comment — the tree shape Cursor Bugbot produces.
|
|
131
131
|
|
|
132
|
+
- **Fix replies** — replies to each child finding comment. Posted by the bugfix teammate after the commit lands. Body: `Fixed in <commit_sha>` if addressed, or `Could not address this loop: <one-line reason>` if not. The `/pulls/<number>/comments/<id>/replies` endpoint works on any review comment, including those created as part of a review, so this shape is unchanged.
|
|
133
|
+
|
|
134
|
+
**Ordering:** bugfind audits FIRST, buffers the findings, validates anchors against the captured diff, then posts the review ONCE at the end. The review body names the finding count authoritatively. Keep all posting bunched into that single end-of-loop review POST.
|
|
135
|
+
|
|
136
|
+
CLI shapes (teammate runs these). All three POSTs use the same robust pattern: build the JSON payload with `jq` (pulling file contents in with `--rawfile` or `-Rs` so markdown with backticks, newlines, and quotes survives intact), then pipe the JSON to `gh api ... --input -` on stdin. This avoids every shell-quoting edge case.
|
|
137
|
+
|
|
138
|
+
- **Per-loop review (one POST creates the parent review AND all child finding comments).** Build the `comments[]` array programmatically from the buffered, diff-anchored findings. The shape per finding is `{path, line, side: "RIGHT", body: <finding markdown>}` for single-line anchors; use `{path, start_line, start_side: "RIGHT", line, side: "RIGHT", body: ...}` for multi-line ranges (all four fields required).
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
jq -n \
|
|
142
|
+
--rawfile review_body <tmp_review_body.md> \
|
|
143
|
+
--arg commit_id "$(git rev-parse HEAD)" \
|
|
144
|
+
--rawfile finding_body_1 <tmp_finding_1.md> \
|
|
145
|
+
--arg path_1 "<file_1>" \
|
|
146
|
+
--argjson line_1 <line_1> \
|
|
147
|
+
[... one finding_body_K / path_K / line_K triple per anchored finding ...] \
|
|
148
|
+
'{
|
|
149
|
+
commit_id: $commit_id,
|
|
150
|
+
event: "COMMENT",
|
|
151
|
+
body: $review_body,
|
|
152
|
+
comments: [
|
|
153
|
+
{path: $path_1, line: $line_1, side: "RIGHT", body: $finding_body_1}
|
|
154
|
+
[, ... one object per anchored finding ...]
|
|
155
|
+
]
|
|
156
|
+
}' \
|
|
157
|
+
| gh api repos/<owner>/<repo>/pulls/<number>/reviews -X POST --input -
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Response JSON carries the parent review `id` / `html_url` plus a `comments` array of child comments, each with its own `id` and `html_url`. Harvest the child entries in index order and match them to the finding list the teammate posted.
|
|
161
|
+
|
|
162
|
+
- **Fix reply** — replying to a child finding comment only needs `body`:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
jq -Rs '{body: .}' < <tmp_reply.md> \
|
|
166
|
+
| gh api repos/<owner>/<repo>/pulls/<number>/comments/<finding_comment_id>/replies -X POST --input -
|
|
132
167
|
```
|
|
133
|
-
## /bugteam loop <N>: audit running
|
|
134
168
|
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
- **Review-POST failure fallback** — top-level PR comment via the issue-comments endpoint (`{issue_number}` is the PR number):
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
jq -Rs '{body: .}' < <tmp_fallback.md> \
|
|
173
|
+
| gh api repos/<owner>/<repo>/issues/<number>/comments -X POST --input -
|
|
137
174
|
```
|
|
138
175
|
|
|
139
|
-
|
|
176
|
+
`<head_sha_at_post_time>` = the SHA at the moment the review is posted (run `git rev-parse HEAD` in the teammate's working dir immediately before the POST). The review anchors its finding comments to the head SHA at audit time, which is the SHA before this loop's fix lands.
|
|
177
|
+
|
|
178
|
+
Write each body (review body and every per-finding body) to its own temp file before running the jq pipeline. The `jq --rawfile` / `jq -Rs` pattern loads file contents as a single string into the JSON payload, which preserves backticks, newlines, and quotes intact. The body stays inside the file the jq pipeline reads — it reaches GitHub as part of the JSON payload — which keeps it compatible with the `gh-body-backtick-guard` hook that scans command-line `--body` arguments.
|
|
140
179
|
|
|
141
|
-
|
|
180
|
+
**Review body shape** (content of `<tmp_review_body.md>`):
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
## /bugteam loop <N> audit: <P0>P0 / <P1>P1 / <P2>P2
|
|
142
184
|
|
|
143
|
-
|
|
185
|
+
<if any findings could not be anchored to a diff line, include this section:>
|
|
186
|
+
### Findings without a diff anchor
|
|
144
187
|
|
|
145
|
-
|
|
188
|
+
- **[severity] title** — <file>:<line> — <one-line description>
|
|
189
|
+
```
|
|
146
190
|
|
|
147
|
-
|
|
191
|
+
If the audit returns zero findings, the teammate still posts ONE review with `event=COMMENT`, an empty `comments[]`, and body `## /bugteam loop <N> audit: 0P0 / 0P1 / 0P2 → clean`. This keeps every loop's section self-contained on the PR.
|
|
148
192
|
|
|
149
|
-
-
|
|
150
|
-
- Finding comment: `gh api repos/<owner>/<repo>/pulls/<number>/comments -X POST -f body=@<tmp> -f commit_id=<head_sha_at_post_time> -f path=<file> -F line=<line> -f side=RIGHT` → returns JSON; capture `id` and `html_url`. (GitHub API name: pull-request review comment.)
|
|
151
|
-
- Fix reply: `gh api repos/<owner>/<repo>/pulls/<number>/comments/<finding_comment_id>/replies -X POST -f body=@<tmp>` → returns JSON.
|
|
193
|
+
**Anchor-validation fallback (teammate handles).** GitHub rejects the entire review POST if any `comments[]` entry targets a line not in the diff. Before posting, the bugfind teammate validates every finding's `(file, line)` against the captured diff. Findings whose anchor is not in the diff are NOT added to `comments[]`; they are listed in the review body under `### Findings without a diff anchor`. The outcome XML records `used_fallback="true"` for each such finding, with `finding_comment_id=""` and `finding_comment_url=<review_url>` (the parent review URL, since no child comment exists for it). The teammate logs the fallback count in its outcome XML so the lead's final report can count fallbacks. Cycle continues; no anchor failure aborts the loop.
|
|
152
194
|
|
|
153
|
-
|
|
195
|
+
**Review POST failure fallback.** If the review POST itself fails (rate limit, network, malformed payload), the teammate falls back to a single top-level issue comment containing the review body plus every finding inline (severity, file:line, description). Every finding in that run carries `used_fallback="true"` and the issue-comment URL as `finding_comment_url`. Use the Review-POST failure fallback CLI shape above (`jq -Rs | gh api .../issues/<number>/comments --input -`).
|
|
154
196
|
|
|
155
|
-
|
|
197
|
+
**GitHub REST endpoints the teammate POSTs to:**
|
|
156
198
|
|
|
157
|
-
|
|
199
|
+
- Per-loop batched review: `POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews` (required: `body`, `event=COMMENT`, `commit_id`; optional `comments[]` — each entry needs `path`, `body`, `line`, `side`)
|
|
200
|
+
- Fix reply: `POST /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies` (required: `body`)
|
|
201
|
+
- Review-POST failure fallback: `POST /repos/{owner}/{repo}/issues/{issue_number}/comments` (required: `body`; `{issue_number}` is the PR number)
|
|
158
202
|
|
|
159
203
|
### Step 3: The cycle
|
|
160
204
|
|
|
@@ -175,18 +219,18 @@ Repeat until an exit condition fires:
|
|
|
175
219
|
|
|
176
220
|
### AUDIT action (clean-room teammate, fresh per loop)
|
|
177
221
|
|
|
178
|
-
Capture a fresh PR diff for this loop into the per-team scoped directory so concurrent `/bugteam`
|
|
222
|
+
Capture a fresh PR diff for this loop into the per-team scoped directory so each concurrent `/bugteam` run keeps its patches isolated. Use the literal `<team_temp_dir>` resolved once in Step 2 — Claude resolves the absolute path, and every shell receives the same literal value:
|
|
179
223
|
|
|
180
224
|
```
|
|
181
225
|
mkdir -p "<team_temp_dir>"
|
|
182
226
|
gh pr diff <number> -R <owner>/<repo> > "<team_temp_dir>/loop-<N>.patch"
|
|
183
227
|
```
|
|
184
228
|
|
|
185
|
-
`<team_temp_dir>` is the absolute path captured in Step 2 (already includes the sanitized team_name and timestamp suffix, and `team_name` itself is already prefixed with `bugteam-`). Claude resolves the portable temp root once via `Path(tempfile.gettempdir()) / team_name` (requires `import tempfile`) and passes the literal absolute path to every shell command. `tempfile.gettempdir()` honors `TMPDIR`, `TEMP`, and `TMP` in the platform-correct order and falls back to `C:\Users\<user>\AppData\Local\Temp` on Windows or `/tmp` on Unix, so this works identically on macOS, Linux, Windows cmd.exe, and PowerShell
|
|
229
|
+
`<team_temp_dir>` is the absolute path captured in Step 2 (already includes the sanitized team_name and timestamp suffix, and `team_name` itself is already prefixed with `bugteam-`). Claude resolves the portable temp root once via `Path(tempfile.gettempdir()) / team_name` (requires `import tempfile`) and passes the literal absolute path to every shell command. `tempfile.gettempdir()` honors `TMPDIR`, `TEMP`, and `TMP` in the platform-correct order and falls back to `C:\Users\<user>\AppData\Local\Temp` on Windows or `/tmp` on Unix, so this works identically on macOS, Linux, Windows cmd.exe, and PowerShell: Claude resolves the literal path once and every shell receives the same absolute value.
|
|
186
230
|
|
|
187
231
|
Spawn a NEW `bugfind` teammate for this loop using the `code-quality-agent` subagent type. The teammate is fresh: no prior loop's findings, no chat history, no inherited audit context. Per the docs: *"The lead's conversation history does not carry over."* — and we further guarantee independence by spawning a new teammate per loop rather than reusing one.
|
|
188
232
|
|
|
189
|
-
The teammate's spawn prompt is the full XML below — copy it verbatim with the placeholders substituted. **
|
|
233
|
+
The teammate's spawn prompt is the full XML below — copy it verbatim with the placeholders substituted. **Keep the spawn prompt context-free.** Reference only the PR scope, audit rubric, and this loop number. Write each instruction as a standalone statement so the teammate treats the prompt as a fresh brief — every audit starts from first principles.
|
|
190
234
|
|
|
191
235
|
```xml
|
|
192
236
|
<context>
|
|
@@ -225,17 +269,17 @@ The teammate's spawn prompt is the full XML below — copy it verbatim with the
|
|
|
225
269
|
</constraints>
|
|
226
270
|
|
|
227
271
|
<comment_posting>
|
|
228
|
-
1.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
272
|
+
1. Audit the diff against the 10 categories above. Buffer the findings
|
|
273
|
+
in memory; all posting happens at step 6 once anchors are validated.
|
|
274
|
+
2. Assign each finding a stable finding_id of exactly the form `loopN-K`
|
|
275
|
+
where K is 1-based within this loop.
|
|
276
|
+
3. Validate every finding's (file, line) against the captured diff. Split
|
|
277
|
+
findings into two buckets: anchored (line is in the diff) and
|
|
278
|
+
unanchored (line is not in the diff — goes into the review body's
|
|
279
|
+
"Findings without a diff anchor" section per Step 2.5).
|
|
280
|
+
4. Build the review body per Step 2.5's review-body shape, filling in the
|
|
281
|
+
P0/P1/P2 counts and the unanchored-findings list (if any).
|
|
282
|
+
5. For each anchored finding, write its body to its own temp file:
|
|
239
283
|
|
|
240
284
|
**[severity] one-line title**
|
|
241
285
|
Category: <letter> (<category name>)
|
|
@@ -243,11 +287,16 @@ The teammate's spawn prompt is the full XML below — copy it verbatim with the
|
|
|
243
287
|
|
|
244
288
|
_From /bugteam audit loop N._
|
|
245
289
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
290
|
+
6. Post ONE review via Step 2.5's per-loop review CLI shape. Harvest the
|
|
291
|
+
parent review `html_url` from the response JSON and the `comments[]`
|
|
292
|
+
child entries (each with its own `id` and `html_url`). Match child
|
|
293
|
+
entries to anchored findings in index order.
|
|
294
|
+
7. If the review POST itself fails, use Step 2.5's Review POST failure
|
|
295
|
+
fallback (single issue comment with full body and all findings inline).
|
|
296
|
+
8. Write every body (review body, each finding body, any fallback body)
|
|
297
|
+
to its own temp file. Load each file into the JSON payload via jq's
|
|
298
|
+
`--rawfile` or `-Rs`, then pipe the jq output to `gh api ... --input -`
|
|
299
|
+
so every body reaches GitHub as file contents inside the JSON payload.
|
|
251
300
|
</comment_posting>
|
|
252
301
|
|
|
253
302
|
<output_format>
|
|
@@ -259,15 +308,15 @@ The teammate's spawn prompt is the full XML below — copy it verbatim with the
|
|
|
259
308
|
Outcome XML schema (bugfind writes this):
|
|
260
309
|
|
|
261
310
|
```xml
|
|
262
|
-
<bugteam_audit loop="<N>"
|
|
311
|
+
<bugteam_audit loop="<N>" review_url="<url>">
|
|
263
312
|
<finding
|
|
264
313
|
finding_id="loop<N>-<index>"
|
|
265
314
|
severity="P0|P1|P2"
|
|
266
315
|
category="<letter>"
|
|
267
316
|
file="<path>"
|
|
268
317
|
line="<int>"
|
|
269
|
-
finding_comment_id="<gh comment id, or empty if fallback>"
|
|
270
|
-
finding_comment_url="<url,
|
|
318
|
+
finding_comment_id="<gh child comment id, or empty if unanchored/review-fallback>"
|
|
319
|
+
finding_comment_url="<url of child comment, OR review_url if unanchored, OR fallback issue comment URL>"
|
|
271
320
|
used_fallback="true|false"
|
|
272
321
|
>
|
|
273
322
|
<title>one-line title</title>
|
|
@@ -281,7 +330,9 @@ Outcome XML schema (bugfind writes this):
|
|
|
281
330
|
|
|
282
331
|
After the teammate writes the XML and returns, the lead reads `.bugteam-loop-<N>.outcomes.xml`, parses it, and populates `loop_comment_index` from `<finding>` elements. Then **shut down the bugfind teammate**: `Ask the bugfind teammate to shut down`. Per the docs: *"The lead sends a shutdown request. The teammate can approve, exiting gracefully, or reject with an explanation."* If the teammate rejects shutdown, force-shut by failing the team and starting Step 5 cleanup with exit reason = `error: bugfind teammate refused shutdown`.
|
|
283
332
|
|
|
284
|
-
`last_action = "audited"`. `last_findings = parsed`. Append `(loop=N, action="audit", counts={P0,P1,P2}, sha=current_HEAD,
|
|
333
|
+
`last_action = "audited"`. `last_findings = parsed`. Append `(loop=N, action="audit", counts={P0,P1,P2}, sha=current_HEAD, review_url=<url>, finding_count=<n>, fallback_count=<n>)` to `audit_log`.
|
|
334
|
+
|
|
335
|
+
**Parallel auditors from loop 4 onward (`loop_count >= 4`).** Once the cycle has made it through three full audit/fix rounds without converging, the next audit spawns THREE bugfind teammates in parallel — named `bugfind-loop-<N>-a`, `bugfind-loop-<N>-b`, `bugfind-loop-<N>-c` — each with an identical spawn prompt (same diff path, same rubric, same loop number). `a` is the post-owner; `b` and `c` write their outcome XML to `<team_temp_dir>/loop-<N>-b.outcomes.xml` and `...-c.outcomes.xml` respectively, then shut down. `a` reads all three outcome XML files, merges findings by `(file, line, category_letter)` (same tuple collapses to one finding, keeping the longest description and the highest severity of the group), re-assigns merged-finding IDs as `loopN-K`, and posts the single per-loop review per the standard posting protocol above. The lead shuts down `b` and `c` first, then `a` after its post completes.
|
|
285
336
|
|
|
286
337
|
### FIX action (fresh teammate, only sees latest audit)
|
|
287
338
|
|
|
@@ -326,13 +377,13 @@ Prompt skeleton:
|
|
|
326
377
|
capture the hook's stderr, write status=hook_blocked for every finding in this loop
|
|
327
378
|
(the commit was atomic; if it failed, no finding was applied), populate hook_output
|
|
328
379
|
on each outcome, and return WITHOUT retrying. The lead will treat this loop as no-progress.
|
|
329
|
-
5. git push (
|
|
380
|
+
5. git push with a plain fast-forward push (the default, no flag overrides).
|
|
330
381
|
6. For each bug, post a fix reply to its finding_comment_id via the
|
|
331
382
|
Step 2.5 reply CLI shape:
|
|
332
383
|
- "Fixed in <commit_sha>" if the bug was addressed by your commit
|
|
333
384
|
- "Could not address this loop: <one-line reason>" if you skipped or failed it
|
|
334
385
|
- "Hook blocked the fix commit: <one-line summary>" if the commit was hook-blocked
|
|
335
|
-
Use
|
|
386
|
+
Use the Fix reply CLI shape from Step 2.5 (`jq -Rs | gh api .../comments/<id>/replies --input -`). Write every reply body to a temp file first.
|
|
336
387
|
7. Write `.bugteam-loop-<N>.outcomes.xml` (schema below) and return its path.
|
|
337
388
|
</execution>
|
|
338
389
|
|
|
@@ -354,9 +405,10 @@ Prompt skeleton:
|
|
|
354
405
|
<constraints>
|
|
355
406
|
- Modify only files referenced in bugs_to_fix.
|
|
356
407
|
- One commit on the existing branch, then push.
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
-
|
|
408
|
+
- Keep the branch linear and the PR base fixed; append one new commit per
|
|
409
|
+
loop and fast-forward push only.
|
|
410
|
+
- Let every git hook run on every commit.
|
|
411
|
+
- git add by explicit path — name each file being staged.
|
|
360
412
|
- Preserve existing comments on lines you do not modify.
|
|
361
413
|
- Type hints on every signature you touch.
|
|
362
414
|
</constraints>
|
|
@@ -376,7 +428,7 @@ If `git rev-parse HEAD` did not change, exit reason = `stuck — bugfix teammate
|
|
|
376
428
|
When the cycle exits (any reason):
|
|
377
429
|
|
|
378
430
|
1. **Clean up the team as the lead.** Per the docs: *"When you're done, ask the lead to clean up: 'Clean up the team'. This removes the shared team resources. When the lead runs cleanup, it checks for active teammates and fails if any are still running, so shut them down first."* The lead is THIS session — call cleanup directly. If any teammate is still alive (e.g., from an aborted shutdown), shut it down first.
|
|
379
|
-
2. Delete the per-team scoped temp directory using Python: `shutil.rmtree(team_temp_dir, ignore_errors=True)` (requires `import shutil`). This works on every platform without OS-detection branching. Pass the literal absolute path Claude resolved at Step 2
|
|
431
|
+
2. Delete the per-team scoped temp directory using Python: `shutil.rmtree(team_temp_dir, ignore_errors=True)` (requires `import shutil`). This works on every platform without OS-detection branching. Pass the literal absolute path Claude resolved at Step 2; Claude performs the path resolution so every shell receives the same literal value at cleanup time.
|
|
380
432
|
|
|
381
433
|
### Step 4.5: Finalize the PR description (mandatory)
|
|
382
434
|
|
|
@@ -392,7 +444,7 @@ Steps:
|
|
|
392
444
|
2. Capture the original body: `gh pr view <number> -R <owner>/<repo> --json body --jq .body > .bugteam-original-body.md`.
|
|
393
445
|
3. Invoke the `pr-description-writer` agent (or `general-purpose` fallback) with this brief:
|
|
394
446
|
- **Inputs:** the diff path, the original body path, the head branch name, the base branch name.
|
|
395
|
-
- **Constraint:** describe what the PR delivers based on the cumulative diff
|
|
447
|
+
- **Constraint:** describe what the PR delivers based on the cumulative diff — code behavior, user-facing effect, and merge rationale. Process metadata (audit loops, fix commit counts, finding counts) lives in the finding comments. The description speaks to the merge audience.
|
|
396
448
|
- **Preservation rule:** if the original body contains sections that look manually curated (linked issues, screenshots, a populated test plan, "Risk Assessment" sections), preserve those verbatim and only rewrite the prose narrative around them.
|
|
397
449
|
- **Output:** the new body markdown.
|
|
398
450
|
4. Write the agent's returned body to `.bugteam-final-body.md`.
|
|
@@ -435,20 +487,20 @@ If exit = `cap reached`, name the remaining bug count and recommend `/findbugs`
|
|
|
435
487
|
- **Agent teams required, not parallel subagents.** The skill MUST use Claude Code's agent teams feature (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`). Spawning `code-quality-agent` and `clean-coder` as parallel subagents from the lead's context = fail; the clean-room property requires independent teammate sessions.
|
|
436
488
|
- **Grant before any spawn, revoke before any return.** Step 0 grants project `.claude/**` permissions; Step 5 revokes. Both are mandatory. Revoke runs on every exit path including error, cap-reached, and stuck.
|
|
437
489
|
- **Fresh teammate per loop.** Both bugfind and bugfix are spawned new each loop and shut down after their action. Reusing a teammate across loops accumulates context inside that teammate's window — defeats clean-room.
|
|
438
|
-
- **One up-front confirmation = whole cycle.**
|
|
490
|
+
- **One up-front confirmation = whole cycle.** The `/bugteam` invocation authorizes the entire cycle; every subsequent decision runs on that single authorization.
|
|
439
491
|
- **10-loop hard cap.** Counted as audits performed. Worst case = 10 audits + 10 fixes = 20 teammate spawns + 20 shutdowns.
|
|
440
|
-
- **Clean-room audits, every loop.**
|
|
492
|
+
- **Clean-room audits, every loop.** Each bugfind teammate's spawn prompt contains only the PR scope, audit rubric, and the current loop number. Prior loop history stays in the lead.
|
|
441
493
|
- **Targeted fixes.** Each fix teammate sees ONLY the most recent audit's findings. Prior loops are invisible to the fix teammate.
|
|
442
494
|
- **Sonnet for both teammates.** Predictable cost, fits-purpose for code work.
|
|
443
|
-
- **
|
|
495
|
+
- **Fix teammate receives the latest audit as its input contract.** Passing the audit's findings to the fix teammate is the input contract — each loop's fix run operates on the current audit's output and only that.
|
|
444
496
|
- **One commit per fix action.** Loops produce one commit per loop, not one per bug.
|
|
445
|
-
- **
|
|
446
|
-
- **Lead-only cleanup.** Per the docs: *"Always use the lead to clean up. Teammates should not run cleanup because their team context may not resolve correctly, potentially leaving resources in an inconsistent state."* This session is the lead
|
|
497
|
+
- **Linear branch, fixed PR base.** Every loop appends one forward-only commit; existing commits and the PR base stay intact throughout the cycle.
|
|
498
|
+
- **Lead-only cleanup.** Per the docs: *"Always use the lead to clean up. Teammates should not run cleanup because their team context may not resolve correctly, potentially leaving resources in an inconsistent state."* This session is the lead, and cleanup runs here only.
|
|
447
499
|
- **Cleanup the per-team scoped temp directory on exit.** The resolved `<team_temp_dir>` (absolute literal captured in Step 2) is deleted entirely so no loop patches leak between runs.
|
|
448
500
|
- **Cleanup all `.bugteam-*` files on exit.** `.bugteam-loop-*.patch`, `.bugteam-loop-*.outcomes.xml`, `.bugteam-final.diff`, `.bugteam-original-body.md`, `.bugteam-final-body.md`. Working directory ends clean.
|
|
449
|
-
- **Teammates own audit/fix comment posting.** Bugfind posts
|
|
501
|
+
- **Teammates own audit/fix comment posting.** Bugfind posts ONE per-loop review (parent body + child finding comments in a single batched POST, with review-fallback to a top-level issue comment). Bugfix posts the fix replies after committing. All comment, review, and reply POSTs belong to the teammates; the lead's single PR-write action is the final description rewrite at Step 4.5.
|
|
450
502
|
- **Lead owns the final PR description rewrite only** (Step 4.5), and only via the `pr-description-writer` agent. The lead does not compose the description inline.
|
|
451
|
-
- **
|
|
503
|
+
- **One review per loop, findings as child comments of that review.** Each loop posts a single pull-request review whose body is the loop header and whose `comments[]` are the anchored findings. Each loop's review stands alone — one review created per loop, fully self-contained on the PR conversation.
|
|
452
504
|
- **PR description rewrite on every exit.** Step 4.5 runs on `converged`, `cap reached`, and `stuck`. On `error`, the rewrite is best-effort; if it fails, surface the error in the final report and continue to revoke.
|
|
453
505
|
- **Outcome XML, not JSON.** Both teammates write structured outcome data (findings or fix outcomes) to `.bugteam-loop-<N>.outcomes.xml`. The lead reads these files between actions. XML chosen for parser robustness against multi-line, special-character, and quoted reason fields.
|
|
454
506
|
|
|
@@ -53,10 +53,6 @@ def path_contains_glob_metacharacters(candidate_path: str) -> bool:
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def path_contains_whitespace(candidate_path: str) -> bool:
|
|
57
|
-
return any(each_character.isspace() for each_character in candidate_path)
|
|
58
|
-
|
|
59
|
-
|
|
60
56
|
def get_current_project_path() -> str:
|
|
61
57
|
normalized_project_path = str(Path.cwd()).replace("\\", "/")
|
|
62
58
|
if path_contains_glob_metacharacters(normalized_project_path):
|
|
@@ -64,11 +60,6 @@ def get_current_project_path() -> str:
|
|
|
64
60
|
f"Current directory path contains glob metacharacters and cannot "
|
|
65
61
|
f"be used to build permission rules safely: {normalized_project_path}"
|
|
66
62
|
)
|
|
67
|
-
if path_contains_whitespace(normalized_project_path):
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Current directory path contains whitespace and cannot be used "
|
|
70
|
-
f"to build permission rules safely: {normalized_project_path}"
|
|
71
|
-
)
|
|
72
63
|
return normalized_project_path
|
|
73
64
|
|
|
74
65
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
7
|
+
|
|
8
|
+
from _claude_permissions_common import (
|
|
9
|
+
build_permission_rule,
|
|
10
|
+
get_current_project_path,
|
|
11
|
+
path_contains_glob_metacharacters,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_return_normalized_path_when_cwd_contains_spaces(
|
|
16
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
17
|
+
) -> None:
|
|
18
|
+
directory_with_spaces = tmp_path / "dir with spaces"
|
|
19
|
+
directory_with_spaces.mkdir()
|
|
20
|
+
monkeypatch.chdir(directory_with_spaces)
|
|
21
|
+
returned_project_path = get_current_project_path()
|
|
22
|
+
expected_suffix = "/dir with spaces"
|
|
23
|
+
assert returned_project_path.endswith(expected_suffix)
|
|
24
|
+
assert "\\" not in returned_project_path
|
|
25
|
+
built_rule = build_permission_rule("Edit", returned_project_path)
|
|
26
|
+
assert built_rule.startswith("Edit(")
|
|
27
|
+
assert built_rule.endswith("/.claude/**)")
|
|
28
|
+
assert "dir with spaces" in built_rule
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_raise_when_cwd_contains_glob_metacharacters(
|
|
32
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
33
|
+
) -> None:
|
|
34
|
+
directory_with_star = tmp_path / "weird[dir]"
|
|
35
|
+
directory_with_star.mkdir()
|
|
36
|
+
monkeypatch.chdir(directory_with_star)
|
|
37
|
+
with pytest.raises(ValueError, match="glob metacharacters"):
|
|
38
|
+
get_current_project_path()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_flag_glob_metacharacters_in_any_position() -> None:
|
|
42
|
+
assert path_contains_glob_metacharacters("/home/user/[dir]/project")
|
|
43
|
+
assert path_contains_glob_metacharacters("/home/user/project*")
|
|
44
|
+
assert not path_contains_glob_metacharacters("/home/user/dir with spaces")
|