claude-dev-env 1.26.3 → 1.26.5

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.
@@ -11,6 +11,9 @@ import os
11
11
  import re
12
12
  import sys
13
13
 
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "config"))
15
+ from messages import USER_FACING_NOTICE
16
+
14
17
  PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
18
 
16
19
  RESEARCH_MODE_SKILL_SEARCH_PATHS = [
@@ -99,14 +102,19 @@ def main() -> None:
99
102
 
100
103
  formatted_term_list = ", ".join(f'"{term}"' for term in found_hedging_terms)
101
104
 
102
- research_mode_content = "(Could not load research-mode skill file)"
105
+ resolved_skill_path: str | None = None
103
106
  for each_skill_path in RESEARCH_MODE_SKILL_SEARCH_PATHS:
104
- try:
105
- with open(each_skill_path, encoding="utf-8") as skill_file:
106
- research_mode_content = skill_file.read()
107
- break
108
- except OSError:
109
- continue
107
+ if os.path.exists(each_skill_path):
108
+ resolved_skill_path = each_skill_path
109
+ break
110
+
111
+ if resolved_skill_path is not None:
112
+ skill_reference = f"under the research-mode constraints defined in:\n\n{resolved_skill_path}"
113
+ else:
114
+ skill_reference = (
115
+ "under research-mode constraints "
116
+ "(no research-mode skill installed; verify with sources or reply 'I don't know')"
117
+ )
110
118
 
111
119
  block_response = {
112
120
  "decision": "block",
@@ -114,12 +122,13 @@ def main() -> None:
114
122
  f"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language: "
115
123
  f"{formatted_term_list}. "
116
124
  f"These words signal unverified claims. You MUST rewrite your response "
117
- f"with these constraints active:\n\n"
118
- f"{research_mode_content}\n\n"
125
+ f"{skill_reference}\n\n"
119
126
  f"Do NOT simply remove the hedging word and keep the unverified claim. "
120
127
  f"Either VERIFY it with a source or replace it with 'I don't know'.\n\n"
121
128
  f"You MUST re-output the complete, revised response with the corrections applied."
122
129
  ),
130
+ "systemMessage": USER_FACING_NOTICE,
131
+ "suppressOutput": True,
123
132
  }
124
133
 
125
134
  print(json.dumps(block_response))
@@ -0,0 +1,135 @@
1
+ """Tests for hedging_language_blocker hook response shape."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+
10
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
11
+ _HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
12
+ _CONFIG_DIR = os.path.join(_HOOKS_DIR, "..", "config")
13
+ if _HOOKS_DIR not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIR)
15
+ if _CONFIG_DIR not in sys.path:
16
+ sys.path.insert(0, _CONFIG_DIR)
17
+ import hedging_language_blocker
18
+ from messages import USER_FACING_NOTICE
19
+
20
+ RESEARCH_MODE_SKILL_BODY_MARKER = "Three anti-hallucination constraints are ALWAYS active."
21
+ HEDGING_MESSAGE = "This is likely correct."
22
+ CLEAN_MESSAGE = "This is verified by the source document."
23
+ EMPTY_MESSAGE = ""
24
+
25
+
26
+ def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
27
+ hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
28
+ return subprocess.run(
29
+ [sys.executable, HOOK_SCRIPT_PATH],
30
+ input=hook_input_payload,
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+
36
+
37
+ def run_hook_with_patched_search_paths(
38
+ assistant_message: str,
39
+ search_paths: list[str],
40
+ ) -> subprocess.CompletedProcess:
41
+ """Run the hook with RESEARCH_MODE_SKILL_SEARCH_PATHS overridden via a wrapper script."""
42
+ wrapper_script = (
43
+ "import sys, json, os\n"
44
+ f"sys.path.insert(0, {repr(os.path.dirname(HOOK_SCRIPT_PATH))})\n"
45
+ "import hedging_language_blocker as blocker\n"
46
+ f"blocker.RESEARCH_MODE_SKILL_SEARCH_PATHS = {repr(search_paths)}\n"
47
+ "blocker.main()\n"
48
+ )
49
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as wrapper_file:
50
+ wrapper_file.write(wrapper_script)
51
+ wrapper_file_path = wrapper_file.name
52
+
53
+ hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
54
+ try:
55
+ completed_process = subprocess.run(
56
+ [sys.executable, wrapper_file_path],
57
+ input=hook_input_payload,
58
+ capture_output=True,
59
+ text=True,
60
+ check=False,
61
+ )
62
+ finally:
63
+ os.unlink(wrapper_file_path)
64
+ return completed_process
65
+
66
+
67
+ def test_user_facing_notice_importable_from_config_messages():
68
+ config_messages_path = os.path.join(_CONFIG_DIR, "messages.py")
69
+ specification = importlib.util.spec_from_file_location("messages", config_messages_path)
70
+ module = importlib.util.module_from_spec(specification)
71
+ specification.loader.exec_module(module)
72
+
73
+ assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
74
+
75
+
76
+ def test_hedging_message_emits_block_with_short_user_notice():
77
+ completed_process = run_hook_with_message(HEDGING_MESSAGE)
78
+
79
+ assert completed_process.returncode == 0
80
+ parsed_response = json.loads(completed_process.stdout)
81
+
82
+ assert parsed_response["decision"] == "block"
83
+ assert parsed_response["systemMessage"] == USER_FACING_NOTICE
84
+ assert parsed_response["suppressOutput"] is True
85
+ assert "likely" in parsed_response["reason"]
86
+
87
+
88
+ def test_hedging_reason_contains_not_installed_notice_when_skill_absent():
89
+ completed_process = run_hook_with_patched_search_paths(
90
+ HEDGING_MESSAGE,
91
+ ["/nonexistent/path/one/SKILL.md", "/nonexistent/path/two/SKILL.md"],
92
+ )
93
+
94
+ assert completed_process.returncode == 0
95
+ parsed_response = json.loads(completed_process.stdout)
96
+
97
+ assert parsed_response["decision"] == "block"
98
+ assert "no research-mode skill installed" in parsed_response["reason"]
99
+ assert "verify with sources or reply" in parsed_response["reason"]
100
+ assert "SKILL.md" not in parsed_response["reason"]
101
+ assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
102
+
103
+
104
+ def test_hedging_reason_contains_skill_path_when_skill_present():
105
+ with tempfile.TemporaryDirectory() as skill_dir:
106
+ skill_file_path = os.path.join(skill_dir, "SKILL.md")
107
+ with open(skill_file_path, "w") as skill_file:
108
+ skill_file.write("# Research Mode Skill\n")
109
+
110
+ completed_process = run_hook_with_patched_search_paths(
111
+ HEDGING_MESSAGE,
112
+ ["/nonexistent/path/SKILL.md", skill_file_path],
113
+ )
114
+
115
+ assert completed_process.returncode == 0
116
+ parsed_response = json.loads(completed_process.stdout)
117
+
118
+ assert parsed_response["decision"] == "block"
119
+ assert "SKILL.md" in parsed_response["reason"]
120
+ assert "no research-mode skill installed" not in parsed_response["reason"]
121
+ assert RESEARCH_MODE_SKILL_BODY_MARKER not in parsed_response["reason"]
122
+
123
+
124
+ def test_clean_message_passes_through_with_no_output():
125
+ completed_process = run_hook_with_message(CLEAN_MESSAGE)
126
+
127
+ assert completed_process.returncode == 0
128
+ assert completed_process.stdout == ""
129
+
130
+
131
+ def test_empty_message_passes_through_with_no_output():
132
+ completed_process = run_hook_with_message(EMPTY_MESSAGE)
133
+
134
+ assert completed_process.returncode == 0
135
+ assert completed_process.stdout == ""
@@ -219,7 +219,10 @@ def test_read_body_file_rejects_relative_path_traversal(tmp_path) -> None:
219
219
  import os, pytest
220
220
  sentinel_file = tmp_path / 'secret.txt'
221
221
  sentinel_file.write_text('secret')
222
- rel_path = os.path.relpath(str(sentinel_file))
222
+ try:
223
+ rel_path = os.path.relpath(str(sentinel_file))
224
+ except ValueError:
225
+ pytest.skip('tmp_path on different drive than cwd; relpath undefined on Windows')
223
226
  if '..' not in rel_path:
224
227
  pytest.skip('file is under cwd, not a traversal case')
225
228
  with pytest.raises(m.PathTraversalError):
@@ -0,0 +1 @@
1
+ # pragma: no-tdd-gate
@@ -0,0 +1,4 @@
1
+ # pragma: no-tdd-gate
2
+ """User-facing notice messages for blocking hooks."""
3
+
4
+ USER_FACING_NOTICE = "Agent was found guessing - sourcing opinions..."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.26.3",
3
+ "version": "1.26.5",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@
4
4
 
5
5
  - **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.
6
6
  - **Orchestrator-only `TeamCreate`.** Only the lead session (this session, when `/bugteam` is invoked) calls `TeamCreate`. Teammates never call `TeamCreate` — if a teammate's spawn prompt instructs it to, that is a skill defect. When additional parallel work is needed (e.g., parallel auditors from loop 4 onward, supplementary audit of adjacent files), the lead spawns additional teammates into the EXISTING team by passing the current `team_name` to every `Agent(...)` call. Multiple teammate "sets" live inside one team under one orchestrator. The runtime enforces this: `TeamCreate` called while the session already leads a team returns the error `Already leading team "<name>". A leader can only manage one team at a time. Use TeamDelete to end the current team before creating a new one.` — direct quote from the runtime's response when this invariant is violated.
7
+ - **One team per invocation, multi-PR supported.** All PRs in a single /bugteam invocation share one team created by the orchestrator. Per-PR identity lives in the teammate name prefix (`bugfind-pr<N>-loop<L>` / `bugfix-pr<N>-loop<L>`) and the `<team_temp_dir>/pr-<N>/` subfolder containing that PR's git worktree, diff patches, and outcome XML files.
7
8
  - **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.
8
9
  - **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.
9
10
  - **One up-front confirmation = whole cycle.** The `/bugteam` invocation authorizes the entire cycle; every subsequent decision runs on that single authorization.
@@ -11,10 +11,14 @@ Keep the spawn prompt self-contained: reference only the PR scope, audit rubric,
11
11
  <base_branch>base ref</base_branch>
12
12
  <pr_url>full URL</pr_url>
13
13
  <loop>N</loop>
14
+ <pr_number>N</pr_number>
15
+ <worktree_path>absolute path from Step 1 per-PR workspace</worktree_path>
14
16
  </context>
15
17
 
18
+ cd into `<worktree_path>` before any git, gh, or file operation.
19
+
16
20
  <scope>
17
- <diff_path>Absolute path to the loop-N patch file under team_temp_dir from Step 2 (same path as gh pr diff redirect in AUDIT)</diff_path>
21
+ <diff_path>Absolute path to the per-PR patch file: <team_temp_dir>/pr-<N>/loop-<L>.patch (same path as gh pr diff redirect in AUDIT)</diff_path>
18
22
  <scope_rule>Audit only lines added or modified in the diff. Pre-existing code on untouched lines is out of scope.</scope_rule>
19
23
  </scope>
20
24
 
@@ -72,8 +76,8 @@ Keep the spawn prompt self-contained: reference only the PR scope, audit rubric,
72
76
  </comment_posting>
73
77
 
74
78
  <output_format>
75
- Write the outcome XML below to .bugteam-loop-N.outcomes.xml in the
76
- working directory. Return only that path on stdout. The schema:
79
+ Write the outcome XML below to .bugteam-pr<N>-loop<L>.outcomes.xml inside
80
+ the PR's worktree directory (<worktree_path>). Return only that path on stdout. The schema:
77
81
  </output_format>
78
82
  ```
79
83
 
@@ -100,7 +104,7 @@ Keep the spawn prompt self-contained: reference only the PR scope, audit rubric,
100
104
  </bugteam_audit>
101
105
  ```
102
106
 
103
- After the teammate writes the XML and returns, the lead reads `.bugteam-loop-<N>.outcomes.xml` with the `Read` tool, parses it, and populates `loop_comment_index` from `<finding>` elements.
107
+ After the teammate writes the XML and returns, the lead reads `.bugteam-pr<N>-loop<L>.outcomes.xml` from the PR's worktree directory with the `Read` tool, parses it, and populates `loop_comment_index` from `<finding>` elements.
104
108
 
105
109
  ## FIX spawn-prompt XML (bugfix teammate)
106
110
 
@@ -111,8 +115,12 @@ After the teammate writes the XML and returns, the lead reads `.bugteam-loop-<N>
111
115
  <base_branch>base</base_branch>
112
116
  <pr_url>url</pr_url>
113
117
  <loop>N</loop>
118
+ <pr_number>N</pr_number>
119
+ <worktree_path>absolute path from Step 1 per-PR workspace</worktree_path>
114
120
  </context>
115
121
 
122
+ cd into `<worktree_path>` before any git, gh, or file operation.
123
+
116
124
  <bugs_to_fix>
117
125
  [for each P0/P1/P2 finding from last_findings:]
118
126
  <bug
@@ -144,7 +152,7 @@ After the teammate writes the XML and returns, the lead reads `.bugteam-loop-<N>
144
152
  - "Could not address this loop: <one-line reason>" if you skipped or failed it
145
153
  - "Hook blocked the fix commit: <one-line summary>" if the commit was hook-blocked
146
154
  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.
147
- 7. Write `.bugteam-loop-<N>.outcomes.xml` (schema below) and return its path.
155
+ 7. Write `.bugteam-pr<N>-loop<L>.outcomes.xml` inside `<worktree_path>` (schema below) and return its path.
148
156
  </execution>
149
157
 
150
158
  <outcome_xml_schema>
@@ -51,6 +51,7 @@ Refusals — first match wins; respond with the quoted line exactly and stop:
51
51
  - **No PR or upstream diff.** `No PR or upstream diff. /bugteam needs a target.`
52
52
  - **Dirty tree.** `Uncommitted changes detected. Stash, commit, or revert before /bugteam.`
53
53
  - **Missing subagents.** Before Step 0, confirm `code-quality-agent` and `clean-coder` exist. Else: `Required subagent type <name> not installed. /bugteam needs both code-quality-agent and clean-coder available.`
54
+ - **Lead role must be held by the orchestrator.** Run /bugteam in the session that received the user's command. The orchestrator session calls TeamCreate directly. Runtime confirms a single lead per team: `Already leading team "<name>". A leader can only manage one team at a time.`
54
55
 
55
56
  ## Utility scripts
56
57
 
@@ -89,17 +90,23 @@ python "${CLAUDE_SKILL_DIR}/scripts/grant_project_claude_permissions.py"
89
90
 
90
91
  ### Step 1: Resolve PR scope (once)
91
92
 
92
- Same as `/findbugs`:
93
-
94
- 1. `gh pr view --json number,baseRefName,headRefName,url`
95
- 2. Else `git merge-base HEAD origin/<default>` then `git diff <merge-base>...HEAD`
96
- 3. Else refuse above.
93
+ Accept one or more PR numbers from the invocation. For each PR, run `gh pr view --json number,baseRefName,headRefName,url` (falling back to the merge-base diff path when no PR exists). Capture `all_prs = [{number, owner, repo, baseRef, headRef, url}, ...]`. A single-PR invocation produces a one-element list and follows the same downstream rules.
97
94
 
98
95
  Keep: owner/repo, branches, PR number, URL — for all loops.
99
96
 
97
+ #### Per-PR workspace
98
+
99
+ For each PR in all_prs:
100
+
101
+ 1. Create `<team_temp_dir>/pr-<N>/`.
102
+ 2. Run `git worktree add "<team_temp_dir>/pr-<N>/worktree" origin/<headRef>`.
103
+ 3. Record the absolute worktree path alongside the PR's other fields.
104
+
105
+ Teammates spawned for a PR operate inside that PR's worktree. Step 4 teardown runs `git worktree remove "<team_temp_dir>/pr-<N>/worktree"` for each PR before `TeamDelete`.
106
+
100
107
  ### Step 2: Create the agent team
101
108
 
102
- Lead calls `TeamCreate`:
109
+ **This session is the lead.** The orchestrator calls `TeamCreate` directly:
103
110
 
104
111
  ```
105
112
  TeamCreate(
@@ -109,7 +116,7 @@ TeamCreate(
109
116
  )
110
117
  ```
111
118
 
112
- **Team name:** `bugteam-pr-<number>-<YYYYMMDDHHMMSS>` or `bugteam-<sanitized-head>-<YYYYMMDDHHMMSS>` if no PR. Timestamp avoids collisions. `TeamCreate` implements natural-language team creation ([`sources.md`](sources.md) § Team creation in natural language).
119
+ **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).
113
120
 
114
121
  **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.
115
122
 
@@ -186,7 +193,9 @@ jq -n \
186
193
 
187
194
  ### Step 3: The cycle
188
195
 
189
- Repeat until exit. **Gate:** `validate_content` / `hooks/blocking/code_rules_enforcer.py` on PR-scoped files before every AUDIT (`bugteam_code_rules_gate.py`). Lead runs gate; clean-coder clears failures; then bugfind audits.
196
+ Run the AUDIT-FIX cycle for each PR in all_prs, reusing the same team across PRs. The 10-loop cap applies per PR. Exit reasons (converged, cap reached, stuck, error) are tracked per PR; the final report lists one outcome line per PR.
197
+
198
+ **Gate:** `validate_content` / `hooks/blocking/code_rules_enforcer.py` on PR-scoped files before every AUDIT (`bugteam_code_rules_gate.py`). Lead runs gate; clean-coder clears failures; then bugfind audits.
190
199
 
191
200
  1. From `last_action` / `last_findings`:
192
201
  - `last_action == "audited"` and `last_findings.total == 0` → exit `converged`
@@ -213,29 +222,29 @@ First pass: pre-audit → AUDIT. After a FIX, the next pass runs pre-audit again
213
222
  ### AUDIT action
214
223
 
215
224
  ```bash
216
- mkdir -p "<team_temp_dir>"
217
- gh pr diff <number> -R <owner>/<repo> > "<team_temp_dir>/loop-<N>.patch"
225
+ mkdir -p "<team_temp_dir>/pr-<N>"
226
+ gh pr diff <N> -R <owner>/<repo> > "<team_temp_dir>/pr-<N>/loop-<L>.patch"
218
227
  ```
219
228
 
220
229
  ```
221
230
  Agent(
222
231
  subagent_type="code-quality-agent",
223
- name="bugfind",
232
+ name="bugfind-pr<N>-loop<L>",
224
233
  team_name="<team_name>",
225
234
  model="sonnet",
226
- description="Bugfind audit loop <N>",
235
+ description="Bugfind audit PR <N> loop <L>",
227
236
  prompt="<audit XML; see PROMPTS.md>"
228
237
  )
229
238
  ```
230
239
 
231
- 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-loop-<N>.outcomes.xml`, fills `loop_comment_index`.
240
+ 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`.
232
241
 
233
242
  **Shutdown:** If `Agent` returned and the teammate already ended, skip. Otherwise:
234
243
 
235
244
  ```
236
245
  SendMessage(
237
- to="bugfind",
238
- message={"type": "shutdown_request", "reason": "audit loop <N> complete; outcome XML captured"}
246
+ to="bugfind-pr<N>-loop<L>",
247
+ message={"type": "shutdown_request", "reason": "audit PR <N> loop <L> complete; outcome XML captured"}
239
248
  )
240
249
  ```
241
250
 
@@ -243,24 +252,24 @@ SendMessage(
243
252
 
244
253
  `last_action = "audited"`; append audit line to `audit_log`.
245
254
 
246
- **Parallel auditors (`loop_count >= 4`):** gate passes immediately before; after three full audit/fix rounds without convergence, issue three `Agent` calls in one assistant message (parallel). `-a` posts the review and merges outcomes from `-b`/`-c` (read `.bugteam-loop-<N>.outcomes.xml` plus `<team_temp_dir>/loop-<N>-b.outcomes.xml` and `...-c...`); merge key `(file, line, category_letter)`; re-id `loopN-K`. `-b`/`-c` write sibling XML only; prompts must pass literal absolute sibling paths. Shutdown: parallel `SendMessage` to `b` and `c`, then `a`.
255
+ **Parallel auditors (`loop_count >= 4`):** gate passes immediately before; after three full audit/fix rounds without convergence, issue three `Agent` calls in one assistant message (parallel). `-a` posts the review and merges outcomes from `-b`/`-c` (read `.bugteam-pr<N>-loop<L>.outcomes.xml` plus `<team_temp_dir>/pr-<N>/loop-<L>-b.outcomes.xml` and `...-c...`); merge key `(file, line, category_letter)`; re-id `loopN-K`. `-b`/`-c` write sibling XML only; prompts must pass literal absolute sibling paths. Shutdown: parallel `SendMessage` to `b` and `c`, then `a`.
247
256
 
248
257
  ### FIX action
249
258
 
250
259
  ```
251
260
  Agent(
252
261
  subagent_type="clean-coder",
253
- name="bugfix",
262
+ name="bugfix-pr<N>-loop<L>",
254
263
  team_name="<team_name>",
255
264
  model="sonnet",
256
- description="Bugfix loop <N>",
265
+ description="Bugfix PR <N> loop <L>",
257
266
  prompt="<fix XML; see PROMPTS.md>"
258
267
  )
259
268
  ```
260
269
 
261
270
  Pass finding comment URLs/ids from `loop_comment_index` in XML. Replies: `Fixed in <sha>` or `Could not address this loop: <reason>`.
262
271
 
263
- **Shutdown:** same as bugfind; else `SendMessage(to="bugfix", message={"type": "shutdown_request", "reason": "fix loop <N> complete; commit <sha7> pushed"})`. `approve: false` → `error: bugfix teammate refused shutdown` → Step 4 then 5.
272
+ **Shutdown:** same as bugfind; else `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.
264
273
 
265
274
  [`PROMPTS.md`](PROMPTS.md): fix XML + schema. Verify: `git rev-parse HEAD` advanced; `git fetch origin <branch> && git rev-parse origin/<branch>` matches `HEAD`. Unchanged HEAD → `stuck — bugfix teammate could not address findings`.
266
275