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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 comment, finding comments, fix replies)
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
- - **No PR or upstream diff.** Respond exactly: `No PR or upstream diff. /bugteam needs a target.` and stop.
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` never re-prompts the user mid-cycle.
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 that is NOT in `[A-Za-z0-9._-]` with `-`. This whitelist covers safe portable filename characters and rejects all OS-reserved or shell-special chars including `/ \ : * ? < > | "` and ASCII control chars (0x00–0x1F). Example: `feat/foo*bar` → `feat-foo-bar`; team_name becomes `bugteam-feat-foo-bar-<YYYYMMDDHHMMSS>`. Apply this sanitization BEFORE the team_name is captured, not after every downstream use of `team_name` (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 do NOT add it again here. Avoid hand-rolled env var chains. Capture the resolved absolute path as `<team_temp_dir>` and pass that literal path to every shell command that follows. Shell-side parameter expansion (`${TMPDIR:-/tmp}`) is forbidden because cmd.exe and PowerShell do not expand it.
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 (start simple)
126
+ ### Step 2.5: PR comment lifecycle (one review per loop)
127
127
 
128
- The team narrates its work to the PR via GitHub comments so a reviewer can scan `/bugteam` activity inline with the code. **Teammates own all PR comment posting** — bugfind posts audit comments, bugfix posts fix replies. The lead never calls `gh pr comment` or `gh api repos/.../comments`. The lead's only PR-write action is the final description rewrite at Step 4.5 (via `pr-description-writer` agent).
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
- - **Loop comment** — one top-level PR issue comment per loop. Posted by the bugfind teammate at the start of each loop. Body: short header naming the loop and the action. Example body:
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
- Clean-room audit on PR diff. Finding comments will appear below
136
- this line.
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
- New loop comment per loop, not one across loops keeps each loop's section self-contained.
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
- - **Finding comments** inline review comments anchored to file:line in the diff. Posted by the bugfind teammate, one per P0/P1/P2 finding. Body: severity, category, description, and a `From /bugteam audit loop <N>` footer.
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
- - **Fix replies** replies to each 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.
185
+ <if any findings could not be anchored to a diff line, include this section:>
186
+ ### Findings without a diff anchor
144
187
 
145
- This is the **simplest** comment shape that links findings and fixes inline. Do not add cross-loop threading, comment editing, thread resolution, batched reviews, or comment summarization in this version. Build out from observed behavior later.
188
+ - **[severity] title** <file>:<line> <one-line description>
189
+ ```
146
190
 
147
- CLI shapes (teammate runs these):
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
- - Loop comment: `gh pr comment <number> -R <owner>/<repo> --body-file <tmp>` returns the comment URL on stdout. (GitHub API name: issue comment.)
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
- `<head_sha_at_post_time>` = the SHA at the moment the finding comment is posted (run `git rev-parse HEAD` in the teammate's working dir immediately before the POST). Each loop's audit anchors its finding comments to the head SHA at audit time, which is the SHA before this loop's fix lands.
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
- Use `--body-file` everywhere, not `--body` — the existing `gh-body-backtick-guard` hook blocks inline bodies that contain backticks, and bug descriptions almost always contain code excerpts.
197
+ **GitHub REST endpoints the teammate POSTs to:**
156
198
 
157
- **Finding-comment failure fallback (teammate handles).** If the finding-comment POST fails (rate limit, line not in the diff, malformed payload, network), the bugfind teammate falls back to a top-level issue comment with the file:line in the body text and prefixes the body with `**Inline failed for <file>:<line>** finding follows below.` The teammate logs the fallback in its outcome XML so the lead's final report can count fallbacks. Cycle continues; no single-comment failure aborts the loop.
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` runs do not collide. Use the literal `<team_temp_dir>` resolved once in Step 2 — do NOT rewrite the path with shell expansion:
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 because the shell never has to interpret `${TMPDIR:-/tmp}` or `%TEMP%`.
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. **Forbid all conversation references** in the spawn prompt. No "as we discussed," "the earlier issue," "fix from the prior loop," "you previously identified." Each loop's audit teammate has no idea other loops happened.
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. Post the loop comment for this loop FIRST, before auditing. Use
229
- the Step 2.5 loop-comment CLI shape with this body:
230
-
231
- ## /bugteam loop N: audit running
232
-
233
- Clean-room audit on PR diff. Finding comments will appear below
234
- this line.
235
-
236
- 2. Audit the diff against the 10 categories above.
237
- 3. For each finding, post a finding comment via the Step 2.5
238
- finding-comment CLI shape. Body:
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
- On POST failure (rate limit, line not in diff, malformed payload,
247
- network), fall back to a top-level issue comment per Step 2.5.
248
- 4. Assign each finding a stable finding_id of exactly the form
249
- `loopN-K` where K is 1-based within this loop.
250
- 5. Use --body-file (never --body) to avoid the gh-body-backtick-guard hook.
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>" loop_comment_url="<url>">
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, inline OR fallback issue comment 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, loop_comment_url=<url>, finding_count=<n>, fallback_count=<n>)` to `audit_log`.
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 (NEVER --force, NEVER --force-with-lease).
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 --body-file (the existing gh-body-backtick-guard hook blocks --body).
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
- - Do NOT rebase, amend, --force, --force-with-lease, or change the PR base.
358
- - Do NOT skip git hooks.
359
- - git add by explicit path; never `git add .` or `git add -A`.
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 do NOT defer to the shell, and never use shell `${TMPDIR:-/tmp}` or `%TEMP%` expansion at this step either.
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. Do NOT mention `/bugteam`, audit loops, fix commits, finding counts, or any process metadata. Those belong in the finding comments, not the description. The description is for the merge audience.
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.** No mid-loop AskUserQuestion. The `/bugteam` invocation IS the authorization.
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.** Never pass conversation context, prior findings, prior commits, or prior loop history into a bugfind teammate's spawn prompt.
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
- - **No clean-room exception for fix.** The fix teammate legitimately needs the findings; that is not anchoring bias, that is the input contract.
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
- - **No `--force`, no `--amend`, no rebase, no base change** at any point.
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; teammates never call cleanup.
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 the loop comment and finding comments (with issue-comment fallback). Bugfix posts the fix replies after committing. The lead never calls `gh pr comment` or `gh api repos/.../comments` for these.
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
- - **Loop comment per loop, fresh finding comments per loop.** No cross-loop comment threading, no comment editing in place, no thread resolution in this version. Each loop's section on the PR is self-contained.
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")