claude-dev-env 1.35.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/bin/install.mjs +28 -8
  3. package/bin/install.test.mjs +9 -1
  4. package/docs/CODE_RULES.md +3 -0
  5. package/docs/agents-md-alignment-plan.md +123 -0
  6. package/hooks/blocking/code_rules_enforcer.py +451 -39
  7. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  8. package/hooks/blocking/test_code_rules_enforcer.py +182 -0
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  10. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
  12. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
  13. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  14. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
  16. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  17. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  18. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  19. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  20. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  21. package/hooks/config/banned_identifiers_constants.py +24 -0
  22. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  23. package/hooks/config/hook_log_extractor_constants.py +1 -1
  24. package/hooks/config/pre_tool_use_stdin.py +48 -0
  25. package/hooks/config/setup_project_paths_constants.py +4 -0
  26. package/hooks/config/stuttering_check_config.py +14 -0
  27. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  28. package/hooks/config/sys_path_insert_constants.py +4 -0
  29. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  30. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  31. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  32. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  33. package/hooks/config/unused_module_import_constants.py +7 -0
  34. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  35. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  36. package/hooks/git-hooks/config.py +3 -3
  37. package/hooks/git-hooks/test_gate_utils.py +10 -10
  38. package/hooks/mypy.ini +2 -0
  39. package/package.json +1 -1
  40. package/rules/gh-paginate.md +125 -0
  41. package/skills/bugteam/CONSTRAINTS.md +12 -6
  42. package/skills/bugteam/SKILL.md +77 -91
  43. package/skills/bugteam/SKILL_EVALS.md +25 -23
  44. package/skills/bugteam/reference/README.md +2 -0
  45. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  46. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  47. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  48. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  49. package/skills/bugteam/test_skill_additions.py +13 -4
  50. package/skills/bugteam/test_team_lifecycle.py +94 -0
  51. package/skills/findbugs/SKILL.md +3 -3
  52. package/skills/fixbugs/SKILL.md +4 -4
  53. package/skills/monitor-open-prs/SKILL.md +32 -2
  54. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  55. package/skills/pr-converge/SKILL.md +562 -97
  56. package/skills/pr-converge/scripts/README.md +145 -0
  57. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  58. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  59. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  60. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  61. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  65. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  66. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  68. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  70. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  71. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  72. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  74. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  75. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  76. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  77. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  78. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  79. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  80. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  81. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  82. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  83. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  84. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  85. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  86. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  87. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  88. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  89. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  90. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  91. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  92. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  93. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  94. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  95. package/skills/pr-converge/test_team_lifecycle.py +47 -0
  96. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  97. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  98. package/skills/qbug/SKILL.md +4 -4
  99. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  100. package/skills/resume-review/SKILL.md +261 -0
  101. package/skills/bugteam/scripts/README.md +0 -58
  102. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  103. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  104. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  105. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  106. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  107. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  108. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  109. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  110. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  111. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  112. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  113. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,11 @@
1
+ """Constants for the stuttering ``all_``/``ALL_`` AST import-binding scan.
2
+
3
+ Lives under the hooks-tree ``config/`` package so module-level
4
+ UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
5
+ requirement and share a home with the other hook-tree configuration
6
+ (``messages``, ``stuttering_check_config``, ``project_paths_reader``).
7
+ """
8
+
9
+ WILDCARD_IMPORT_SENTINEL = "*"
10
+ MODULE_PATH_SEPARATOR = "."
11
+ AST_LINENO_ATTRIBUTE = "lineno"
@@ -0,0 +1,4 @@
1
+ """Constants for the sys.path.insert deduplication guard checker."""
2
+
3
+ MAX_SYS_PATH_INSERT_ISSUES: int = 25
4
+ SYS_PATH_INSERT_GUIDANCE: str = "guard with `if <path> not in sys.path:` to avoid pushing the same entry on every reload"
@@ -0,0 +1,48 @@
1
+ """Behavior tests for banned-identifier configuration constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
9
+ if str(_HOOKS_ROOT) not in sys.path:
10
+ sys.path.insert(0, str(_HOOKS_ROOT))
11
+
12
+ from config.banned_identifiers_constants import (
13
+ ALL_BANNED_IDENTIFIERS,
14
+ BANNED_IDENTIFIER_MESSAGE_SUFFIX,
15
+ BANNED_IDENTIFIER_SKIP_ADVISORY,
16
+ MAX_BANNED_IDENTIFIER_ISSUES,
17
+ )
18
+
19
+
20
+ def test_all_banned_identifiers_includes_canonical_offenders() -> None:
21
+ canonical_offenders = {
22
+ "result",
23
+ "data",
24
+ "output",
25
+ "response",
26
+ "value",
27
+ "item",
28
+ "temp",
29
+ "argv",
30
+ "args",
31
+ "kwargs",
32
+ "argc",
33
+ }
34
+ assert canonical_offenders <= ALL_BANNED_IDENTIFIERS
35
+
36
+
37
+ def test_max_banned_identifier_issues_is_positive_cap() -> None:
38
+ assert MAX_BANNED_IDENTIFIER_ISSUES > 0
39
+
40
+
41
+ def test_banned_identifier_message_suffix_references_naming_section() -> None:
42
+ assert "CODE_RULES" in BANNED_IDENTIFIER_MESSAGE_SUFFIX
43
+ assert "Naming" in BANNED_IDENTIFIER_MESSAGE_SUFFIX
44
+
45
+
46
+ def test_banned_identifier_skip_advisory_explains_skip_reason() -> None:
47
+ assert "skipped" in BANNED_IDENTIFIER_SKIP_ADVISORY
48
+ assert "parse" in BANNED_IDENTIFIER_SKIP_ADVISORY
@@ -0,0 +1,78 @@
1
+ """Behavior tests for hardcoded user-path detection constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
9
+ if str(_HOOKS_ROOT) not in sys.path:
10
+ sys.path.insert(0, str(_HOOKS_ROOT))
11
+
12
+ from config.hardcoded_user_path_constants import HARDCODED_USER_PATH_PATTERN
13
+
14
+
15
+ def test_pattern_matches_windows_user_home() -> None:
16
+ match = HARDCODED_USER_PATH_PATTERN.search("C:/Users/jon/notes")
17
+ assert match is not None
18
+ assert match.group(0) == "C:/Users/jon"
19
+
20
+
21
+ def test_pattern_matches_macos_user_home() -> None:
22
+ match = HARDCODED_USER_PATH_PATTERN.search("/Users/bob/Documents")
23
+ assert match is not None
24
+ assert match.group(0) == "/Users/bob"
25
+
26
+
27
+ def test_pattern_matches_linux_user_home() -> None:
28
+ match = HARDCODED_USER_PATH_PATTERN.search("/home/alice/data")
29
+ assert match is not None
30
+ assert match.group(0) == "/home/alice"
31
+
32
+
33
+ def test_pattern_excludes_windows_public_shared_folder() -> None:
34
+ assert HARDCODED_USER_PATH_PATTERN.search("C:/Users/Public/Documents") is None
35
+
36
+
37
+ def test_pattern_excludes_windows_shared_folder() -> None:
38
+ assert HARDCODED_USER_PATH_PATTERN.search("C:/Users/Shared/data") is None
39
+
40
+
41
+ def test_pattern_excludes_windows_all_users_folder() -> None:
42
+ assert HARDCODED_USER_PATH_PATTERN.search("C:/Users/All Users/AppData") is None
43
+
44
+
45
+ def test_pattern_excludes_macos_shared_folder() -> None:
46
+ assert HARDCODED_USER_PATH_PATTERN.search("/Users/Shared/data") is None
47
+
48
+
49
+ def test_pattern_excludes_macos_public_shared_folder() -> None:
50
+ assert HARDCODED_USER_PATH_PATTERN.search("/Users/Public/Documents") is None
51
+
52
+
53
+ def test_pattern_excludes_windows_lowercase_public_shared_folder() -> None:
54
+ assert HARDCODED_USER_PATH_PATTERN.search("c:/users/public/Documents") is None
55
+
56
+
57
+ def test_pattern_excludes_windows_lowercase_shared_folder() -> None:
58
+ assert HARDCODED_USER_PATH_PATTERN.search("c:/users/shared/data") is None
59
+
60
+
61
+ def test_pattern_excludes_windows_lowercase_all_users_folder() -> None:
62
+ assert HARDCODED_USER_PATH_PATTERN.search("c:/users/all users/AppData") is None
63
+
64
+
65
+ def test_pattern_excludes_windows_mixed_case_public_shared_folder() -> None:
66
+ assert HARDCODED_USER_PATH_PATTERN.search("C:/Users/PuBlIc/Documents") is None
67
+
68
+
69
+ def test_pattern_excludes_windows_uppercase_public_shared_folder() -> None:
70
+ assert HARDCODED_USER_PATH_PATTERN.search("C:/Users/PUBLIC/Documents") is None
71
+
72
+
73
+ def test_pattern_excludes_macos_lowercase_shared_folder() -> None:
74
+ assert HARDCODED_USER_PATH_PATTERN.search("/Users/shared/data") is None
75
+
76
+
77
+ def test_pattern_excludes_macos_lowercase_public_shared_folder() -> None:
78
+ assert HARDCODED_USER_PATH_PATTERN.search("/Users/public/Documents") is None
@@ -24,7 +24,7 @@ from config.hook_log_extractor_constants import (
24
24
  STOP_WRAPPER_DEBOUNCE_SECONDS,
25
25
  STOP_WRAPPER_LAST_RUN_TIMESTAMP_FILE,
26
26
  WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG,
27
- WINDOWS_DETACHED_PROCESS_FLAG,
27
+ WINDOWS_CREATE_NO_WINDOW_FLAG,
28
28
  )
29
29
 
30
30
 
@@ -116,8 +116,8 @@ def test_stop_wrapper_last_run_timestamp_file_is_under_claude_home() -> None:
116
116
 
117
117
 
118
118
  def test_windows_creation_flags_are_distinct_nonzero_bits() -> None:
119
- assert WINDOWS_DETACHED_PROCESS_FLAG > 0
119
+ assert WINDOWS_CREATE_NO_WINDOW_FLAG > 0
120
120
  assert WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG > 0
121
121
  assert (
122
- WINDOWS_DETACHED_PROCESS_FLAG & WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG
122
+ WINDOWS_CREATE_NO_WINDOW_FLAG & WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG
123
123
  ) == 0
@@ -0,0 +1,80 @@
1
+ """Tests for PreToolUse stdin JSON parsing helper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import io
7
+ import json
8
+ import sys
9
+ from unittest.mock import patch
10
+
11
+ from config import pre_tool_use_stdin
12
+ from config.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
13
+
14
+
15
+ def test_pre_tool_use_stdin_uses_shared_encoding_and_decode_constants() -> None:
16
+ """Pin: stdin parsing must use shared constants, not duplicate literals."""
17
+ module_source = inspect.getsource(pre_tool_use_stdin)
18
+ assert "UTF8_ENCODING" in module_source
19
+ assert "DECODE_ERRORS_POLICY" in module_source
20
+ assert "UTF8_BYTE_ORDER_MARK" in module_source
21
+ assert '.decode("utf-8"' not in module_source
22
+ assert 'errors="replace"' not in module_source
23
+ assert "stdin_parse_constants" not in module_source
24
+
25
+
26
+ def test_read_returns_none_for_empty_stdin() -> None:
27
+ with patch("sys.stdin", io.StringIO("")):
28
+ assert read_hook_input_dictionary_from_stdin() is None
29
+
30
+
31
+ def test_read_returns_none_for_whitespace_only_stdin() -> None:
32
+ with patch("sys.stdin", io.StringIO(" \n\t ")):
33
+ assert read_hook_input_dictionary_from_stdin() is None
34
+
35
+
36
+ def test_read_returns_none_for_invalid_json() -> None:
37
+ with patch("sys.stdin", io.StringIO("not json")):
38
+ assert read_hook_input_dictionary_from_stdin() is None
39
+
40
+
41
+ def test_read_returns_none_for_json_array_root() -> None:
42
+ with patch("sys.stdin", io.StringIO("[1, 2]")):
43
+ assert read_hook_input_dictionary_from_stdin() is None
44
+
45
+
46
+ def test_read_strips_bom_and_returns_dict() -> None:
47
+ payload = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
48
+ with patch("sys.stdin", io.StringIO("\ufeff" + json.dumps(payload))):
49
+ parsed = read_hook_input_dictionary_from_stdin()
50
+ assert parsed == payload
51
+
52
+
53
+ def test_read_returns_dict_for_valid_json_object() -> None:
54
+ payload = {"tool_name": "Read", "tool_input": {"file_path": "/tmp/x"}}
55
+ with patch("sys.stdin", io.StringIO(json.dumps(payload))):
56
+ parsed = read_hook_input_dictionary_from_stdin()
57
+ assert parsed == payload
58
+
59
+
60
+ def test_read_uses_buffer_when_present() -> None:
61
+ payload = {"tool_name": "Bash", "tool_input": {}}
62
+ raw_bytes = json.dumps(payload).encode("utf-8")
63
+ binary_stream = io.BytesIO(raw_bytes)
64
+ text_wrapper = io.TextIOWrapper(binary_stream, encoding="utf-8")
65
+ with patch("sys.stdin", text_wrapper):
66
+ parsed = read_hook_input_dictionary_from_stdin()
67
+ assert parsed == payload
68
+
69
+
70
+ def test_read_returns_none_when_buffer_and_text_read_raise_attribute_error() -> None:
71
+ class BrokenStandardInput:
72
+ @property
73
+ def buffer(self) -> object:
74
+ raise AttributeError("no buffer")
75
+
76
+ def read(self, size: int = -1) -> str:
77
+ raise AttributeError("no read")
78
+
79
+ with patch("sys.stdin", BrokenStandardInput()):
80
+ assert read_hook_input_dictionary_from_stdin() is None
@@ -0,0 +1,7 @@
1
+ """Constants for the unused module-level import scan in ``code_rules_enforcer``."""
2
+
3
+ MAX_UNUSED_IMPORT_ISSUES: int = 25
4
+ UNUSED_IMPORT_GUIDANCE: str = (
5
+ "remove unused import; if kept for side effects, mark with `# noqa: F401`"
6
+ )
7
+ TYPE_CHECKING_IDENTIFIER: str = "TYPE_CHECKING"
@@ -0,0 +1,3 @@
1
+ """Configuration constants for the windows_rmtree_blocker PreToolUse hook."""
2
+
3
+ PYTHON_FILE_EXTENSION: str = ".py"
@@ -9,8 +9,11 @@ stay near zero. The wrapper:
9
9
  path: a small file read, well under 10ms).
10
10
  2. Otherwise records the current timestamp, then launches the extractor
11
11
  as a fully detached background process (no stdio, separate process
12
- group on POSIX or DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP on
13
- Windows) and returns without waiting for it.
12
+ group on POSIX or CREATE_NO_WINDOW|CREATE_NEW_PROCESS_GROUP on
13
+ Windows) and returns without waiting for it. CREATE_NO_WINDOW
14
+ prevents a console flash on Windows even when the wrapper goes
15
+ through ``bws run`` first; DETACHED_PROCESS would let the
16
+ grandchild python.exe allocate a fresh console.
14
17
 
15
18
  Bitwarden injection: when both ``bws`` is on PATH and
16
19
  ``BWS_ACCESS_TOKEN`` is set, the extractor is launched via
@@ -45,7 +48,7 @@ from config.hook_log_extractor_constants import (
45
48
  STOP_WRAPPER_EXTRACTOR_SCRIPT_NAME,
46
49
  STOP_WRAPPER_LAST_RUN_TIMESTAMP_FILE,
47
50
  WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG,
48
- WINDOWS_DETACHED_PROCESS_FLAG,
51
+ WINDOWS_CREATE_NO_WINDOW_FLAG,
49
52
  WINDOWS_OS_NAME,
50
53
  )
51
54
 
@@ -113,7 +116,7 @@ def _detached_spawn_keyword_arguments() -> dict[str, object]:
113
116
  }
114
117
  if os.name == WINDOWS_OS_NAME:
115
118
  spawn_arguments["creationflags"] = (
116
- WINDOWS_DETACHED_PROCESS_FLAG
119
+ WINDOWS_CREATE_NO_WINDOW_FLAG
117
120
  | WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG
118
121
  )
119
122
  startup_info = subprocess.STARTUPINFO()
@@ -20,10 +20,10 @@ GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_RULES_GATE_PATH"
20
20
  CLAUDE_HOME_ENV_VAR: str = "CLAUDE_HOME"
21
21
  CLAUDE_HOME_DEFAULT_SUBDIRECTORY: str = ".claude"
22
22
  GATE_SCRIPT_RELATIVE_PATH: tuple[str, ...] = (
23
- "skills",
24
- "bugteam",
23
+ "_shared",
24
+ "pr-loop",
25
25
  "scripts",
26
- "bugteam_code_rules_gate.py",
26
+ "code_rules_gate.py",
27
27
  )
28
28
  GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE: int = 2
29
29
  GATE_SCRIPT_NOT_FOUND_MESSAGE: str = (
@@ -44,7 +44,7 @@ def test_resolve_gate_script_path_defaults_to_claude_home_when_env_var_set(
44
44
  resolved_path, exact_allowed = gate_utils.resolve_gate_script_path()
45
45
 
46
46
  expected_path = (
47
- tmp_path / "skills" / "bugteam" / "scripts" / "bugteam_code_rules_gate.py"
47
+ tmp_path / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py"
48
48
  )
49
49
  assert resolved_path == expected_path
50
50
  assert exact_allowed is None
@@ -63,10 +63,10 @@ def test_resolve_gate_script_path_falls_back_to_home_dot_claude_when_no_env_vars
63
63
  expected_path = (
64
64
  tmp_path
65
65
  / ".claude"
66
- / "skills"
67
- / "bugteam"
66
+ / "_shared"
67
+ / "pr-loop"
68
68
  / "scripts"
69
- / "bugteam_code_rules_gate.py"
69
+ / "code_rules_gate.py"
70
70
  )
71
71
  assert resolved_path == expected_path
72
72
  assert exact_allowed is None
@@ -106,13 +106,13 @@ def test_is_safe_regular_file_accepts_exact_override_path(
106
106
  assert is_safe
107
107
 
108
108
 
109
- def test_is_safe_regular_file_rejects_claude_home_override_outside_home_dot_claude(
109
+ def test_is_safe_regular_file_rejects_gate_outside_home_dot_claude_tree(
110
110
  tmp_path: Path,
111
111
  monkeypatch: pytest.MonkeyPatch,
112
112
  ) -> None:
113
113
  attacker_home = tmp_path / "attacker_home"
114
114
  gate_under_attacker_home = (
115
- attacker_home / "skills" / "bugteam" / "scripts" / "bugteam_code_rules_gate.py"
115
+ attacker_home / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py"
116
116
  )
117
117
  gate_under_attacker_home.parent.mkdir(parents=True)
118
118
  gate_under_attacker_home.write_text("", encoding="utf-8")
@@ -130,7 +130,7 @@ def test_is_safe_regular_file_accepts_gate_inside_home_dot_claude(
130
130
  ) -> None:
131
131
  home_dir = tmp_path / "real_home"
132
132
  gate_path = (
133
- home_dir / ".claude" / "skills" / "bugteam" / "scripts" / "bugteam_code_rules_gate.py"
133
+ home_dir / ".claude" / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py"
134
134
  )
135
135
  gate_path.parent.mkdir(parents=True)
136
136
  gate_path.write_text("", encoding="utf-8")
@@ -148,7 +148,7 @@ def test_is_safe_regular_file_rejects_nonexistent_path_under_trusted_prefix(
148
148
  home_dir = tmp_path / "real_home"
149
149
  (home_dir / ".claude").mkdir(parents=True)
150
150
  missing_gate_path = (
151
- home_dir / ".claude" / "skills" / "bugteam" / "scripts" / "missing_gate.py"
151
+ home_dir / ".claude" / "_shared" / "pr-loop" / "scripts" / "missing_gate.py"
152
152
  )
153
153
  monkeypatch.setattr(Path, "home", staticmethod(lambda: home_dir))
154
154
 
@@ -182,7 +182,7 @@ def test_is_safe_regular_file_uses_claude_home_env_as_trust_root(
182
182
  ) -> None:
183
183
  custom_claude_home = tmp_path / "custom_claude"
184
184
  gate_path = (
185
- custom_claude_home / "skills" / "bugteam" / "scripts" / "bugteam_code_rules_gate.py"
185
+ custom_claude_home / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py"
186
186
  )
187
187
  gate_path.parent.mkdir(parents=True)
188
188
  gate_path.write_text("", encoding="utf-8")
@@ -202,7 +202,7 @@ def test_resolve_gate_script_path_snapshot_is_consistent_with_is_safe_regular_fi
202
202
  ) -> None:
203
203
  custom_claude_home = tmp_path / "custom_claude"
204
204
  gate_path = (
205
- custom_claude_home / "skills" / "bugteam" / "scripts" / "bugteam_code_rules_gate.py"
205
+ custom_claude_home / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py"
206
206
  )
207
207
  gate_path.parent.mkdir(parents=True)
208
208
  gate_path.write_text("", encoding="utf-8")
package/hooks/mypy.ini ADDED
@@ -0,0 +1,2 @@
1
+ [mypy]
2
+ mypy_path = .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,125 @@
1
+ # gh API Pagination Rule
2
+
3
+ **Root cause:** The GitHub REST API returns 30 items per page by default. `gh api repos/<owner>/<repo>/pulls/<number>/reviews` and `gh api repos/<owner>/<repo>/pulls/<number>/comments` silently truncate at 30 results without warning. PRs that have accumulated more than 30 reviews or inline comments — common on long PR-loop cycles where bugbot, copilot, or the in-house bugteam each post repeatedly — return only the **oldest** 30, hiding the most recent reviews and findings entirely. A `sort_by(.submitted_at) | last` (or `| reverse`) on a truncated array picks the latest entry **within the first 30**, not the actual latest, which produces a stale-but-confident report that then drives wrong decisions (e.g., re-triggering bugbot when it has already posted a CLEAN review on a later page).
4
+
5
+ **Rule:** All `gh api` calls that read `pulls/<number>/reviews`, `pulls/<number>/comments`, `issues/<number>/comments`, or any other paginated GitHub list endpoint **must** request the full set of pages AND apply any cross-page jq operation through external `jq`, not through `gh`'s built-in `--jq`. Use `--paginate --slurp | jq` (preferred — see [Safe patterns](#safe-patterns)). Never call these endpoints with their default pagination, and never use `gh`'s `--jq` for cross-page operations like `sort_by | last` or `| reverse | .[0]`.
6
+
7
+ ## Two defects, one rule
8
+
9
+ This rule guards against two distinct silent-truncation defects that compound:
10
+
11
+ 1. **Default 30-item page.** Without `--paginate`, only the first page is fetched. On long PRs this hides the most recent reviews entirely.
12
+ 2. **`--jq` runs per-page, not on the concatenated result.** Per [GitHub CLI #10459](https://github.com/cli/cli/issues/10459), `gh api --paginate --jq '<filter>'` applies `<filter>` to each page **separately** and emits one output per page. Cross-page operations like `sort_by(.submitted_at) | last` therefore operate within each page independently, not across the merged result set. On PRs with more than 100 reviews this still produces a wrong-but-confident "latest" review even when `--paginate` is set.
13
+
14
+ The safe patterns below fix both defects together: `--paginate --slurp` walks every page AND emits a single merged structure, and an **external** `jq` then runs cross-page operations on that merged structure.
15
+
16
+ ## Affected endpoints
17
+
18
+ The rule applies to every paginated read from the GitHub REST API. Common offenders in this repo's PR-loop skills:
19
+
20
+ - `gh api repos/<owner>/<repo>/pulls/<number>/reviews`
21
+ - `gh api repos/<owner>/<repo>/pulls/<number>/comments`
22
+ - `gh api repos/<owner>/<repo>/pulls/<number>/files`
23
+ - `gh api repos/<owner>/<repo>/issues/<number>/comments`
24
+ - `gh api repos/<owner>/<repo>/pulls`
25
+ - `gh api repos/<owner>/<repo>/issues`
26
+
27
+ The same rule applies to any other endpoint documented as paginated by GitHub (see [GitHub REST API pagination](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api)).
28
+
29
+ Single-object endpoints (e.g., `repos/<owner>/<repo>/pulls/<number>` returning one PR object) are not paginated — `?per_page=...` is silently ignored, and neither `--paginate` nor external `jq` is required. Use `gh`'s `--jq` directly on those endpoints.
30
+
31
+ ## Safe patterns
32
+
33
+ ### Preferred — `--paginate --slurp` piped to external `jq`
34
+
35
+ `gh --paginate --slurp` walks every page and emits a single merged JSON array of page-arrays (`[[page1_items...], [page2_items...], ...]`). Pipe to external `jq` to flatten and filter across the full result set:
36
+
37
+ ```bash
38
+ gh api 'repos/<owner>/<repo>/pulls/<number>/reviews?per_page=100' --paginate --slurp \
39
+ | jq '[.[][] | select(.user.login=="cursor[bot]")] | sort_by(.submitted_at) | last'
40
+ ```
41
+
42
+ The `.[][]` flattens the array-of-pages into one stream of items before the cross-page operators (`sort_by`, `last`, `reverse`) run. Combine with `?per_page=100` so each page fetches 100 items instead of 30, reducing round-trips on long PRs without changing correctness.
43
+
44
+ `gh`'s `--jq` flag and `--slurp` flag are mutually exclusive (gh CLI rejects `--paginate --slurp --jq` with `the --slurp option is not supported with --jq or --template`), which is why the filter must run in an external `jq` invocation.
45
+
46
+ ### Acceptable — single-page bound on a paginated list endpoint when result fits
47
+
48
+ When you have an explicit reason to read at most one page from a **paginated** list endpoint (e.g., a known-small list), document the bound in a comment and use `?per_page=100` without `--paginate`. Cross-page operators are not in play here, so `gh`'s `--jq` is safe:
49
+
50
+ ```bash
51
+ # Bound: a freshly created issue is expected to have <= 100 comments.
52
+ gh api 'repos/<owner>/<repo>/issues/<number>/comments?per_page=100' \
53
+ --jq '[.[] | select(.user.login=="cursor[bot]")] | length'
54
+ ```
55
+
56
+ This pattern is only safe when the endpoint is confirmed to return a list smaller than 100 entries. Lists that grow over the PR's lifetime (reviews, comments) must use `--paginate --slurp` plus external `jq`.
57
+
58
+ ### Single-object endpoints — no pagination needed
59
+
60
+ Endpoints that return a single object (e.g., `pulls/<number>`, `issues/<number>`) are not paginated. `?per_page=...`, `--paginate`, and `--slurp` are all unnecessary. Use `gh`'s built-in `--jq` directly:
61
+
62
+ ```bash
63
+ gh api 'repos/<owner>/<repo>/pulls/<number>' --jq '.head.sha'
64
+ ```
65
+
66
+ ### Newest-first walk
67
+
68
+ Pair pagination with explicit reverse-sort so the consumer reads newest-first regardless of the API's internal order:
69
+
70
+ ```bash
71
+ gh api 'repos/<owner>/<repo>/pulls/<number>/reviews?per_page=100' --paginate --slurp \
72
+ | jq '[.[][] | select(.user.login=="cursor[bot]")] | sort_by(.submitted_at) | reverse'
73
+ ```
74
+
75
+ This is the canonical pattern for the bugbot ↔ bugteam convergence loop: walk newest-first, stop at the first clean review.
76
+
77
+ ## What NOT to do
78
+
79
+ ```bash
80
+ # BAD — default 30-item page silently truncates on long PRs
81
+ gh api repos/<owner>/<repo>/pulls/<number>/reviews \
82
+ --jq '[.[] | select(.user.login=="cursor[bot]")] | sort_by(.submitted_at) | last'
83
+
84
+ # BAD — `?per_page=100` alone caps at 100 items; PRs with 100+ reviews still truncate
85
+ gh api 'repos/<owner>/<repo>/pulls/<number>/reviews?per_page=100' \
86
+ --jq '[.[] | select(.user.login=="cursor[bot]")] | sort_by(.submitted_at) | last'
87
+
88
+ # BAD — --paginate fetches every page, but `--jq` runs PER-PAGE (gh CLI #10459).
89
+ # `sort_by(.submitted_at) | last` operates within each page independently and
90
+ # emits one "latest" per page, not the actual latest across the full result set.
91
+ gh api 'repos/<owner>/<repo>/pulls/<number>/reviews?per_page=100' --paginate \
92
+ --jq '[.[] | select(.user.login=="cursor[bot]")] | sort_by(.submitted_at) | last'
93
+
94
+ # BAD — taking `| last` on an unpaginated read returns the latest of the first 30,
95
+ # not the actual latest. Same defect for `| reverse | .[0]`.
96
+ ```
97
+
98
+ ## Why both defects matter
99
+
100
+ `gh api`'s default page is the FIRST page of results, ordered oldest-to-newest by the GitHub API. When the result set exceeds 30 items, page 1 contains the OLDEST 30 — not the newest. A jq `| last` after `sort_by(.submitted_at)` picks the latest entry within those 30 oldest items, producing output that looks correct but reports a state from days or weeks ago.
101
+
102
+ `--paginate` alone does NOT fix this when paired with `--jq`: gh applies the jq filter to each page separately and emits one result per page. A consumer reading "the last line of output" still gets the latest within a single page, not the latest across all pages. The skill that consumes this output then makes decisions (re-trigger bugbot, mark a finding stale, report convergence) against an obsolete view of the PR.
103
+
104
+ `--paginate --slurp | jq` fixes both defects: every page is fetched, every page is merged into one structure before any jq operator runs, and cross-page operations see the full result set.
105
+
106
+ ## Consumers
107
+
108
+ Skills and scripts in this repo that read paginated endpoints and must therefore use `--paginate --slurp` plus external `jq`:
109
+
110
+ - `pr-converge` — bugbot review walk (BUGBOT phase, Step 2.a) and inline-comments fetch (Step 2.b).
111
+ - `bugteam` — review threads, inline comments, audit-loop history.
112
+ - `qbug` — same as bugteam, scoped to a single subagent loop.
113
+ - `pr-review-responder` — review comments fetch (already enforced; this rule extends the same constraint to reviews and other endpoints).
114
+ - `monitor-many` — open-PR enumeration and per-PR review/comment scans.
115
+ - `babysit-pr` — review-comment polling.
116
+
117
+ Updating any of these to read paginated endpoints requires `--paginate --slurp` plus external `jq` (or a documented single-page bound on a small list).
118
+
119
+ ## Enforcement
120
+
121
+ This rule is documentation-only at present. A future PreToolUse hook may pattern-match `Bash` invocations of `gh api repos/.../pulls/<n>/(reviews|comments)` without `--paginate --slurp` (or with `--paginate --jq` doing cross-page operations) and return a corrective message. Until that hook lands, treat this rule as binding by review and rely on it during skill authoring.
122
+
123
+ ## Precedent
124
+
125
+ The `pr-review-responder` skill predated this rule and forbids default pagination on `pulls/<n>/comments` reads (`packages/claude-dev-env/skills/pr-review-responder/SKILL.md` Rule 1). This file generalizes that constraint to every paginated GitHub endpoint, adds the `--jq` per-page defect (gh CLI #10459) discovered while reviewing this rule, and centralizes the safe patterns so additional skills inherit the rule by reference instead of restating it.
@@ -1,25 +1,31 @@
1
1
  # Bugteam — invariants and design rationale
2
2
 
3
+ ## Path A vs Path B
4
+
5
+ **Path A** (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`): the constraints below apply as written — `TeamCreate`, isolated teammate sessions, lead-only `TeamDelete`. **Path B** (Task harness): read [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md) for harness-only steps; **agent types** (`code-quality-agent`, `clean-coder`), **models**, **one commit per fix**, **gate-before-AUDIT**, **10-loop cap**, and **outcome XML** remain identical to `SKILL.md`. Path B intentionally uses **`Task`** from the lead instead of teammate isolation — see that file **Clean-room note**.
6
+
3
7
  ## Constraints
4
8
 
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
- - **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.
9
+ - **Path A — agent teams required, not parallel subagents from the lead without `TeamCreate`.** On Path A, 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 **Agents** with `team_name` from the lead is the supported pattern. Spawning ad-hoc `generalPurpose` workers in place of those roles = fail. **Path B** does not use `TeamCreate`; it uses **`Task`** carrying the same Path A spawn contracts per [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md) (AUDIT/FIX spawn; when the host rejects `subagent_type="clean-coder"`, apply **Path B Cursor `Task` registry**). Not a substitute for skipping `code-quality-agent` / `clean-coder` work.
10
+ - **Path B Cursor `Task` registry.** When the host `Task` tool rejects `subagent_type="clean-coder"`, Path B FIX MUST use `subagent_type: "generalPurpose"` plus the mandatory **Read** of `clean-coder.md` in the FIX prompt per [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md) (FIX spawn, Cursor host split). This is the documented shim, not an ad-hoc `generalPurpose` bypass of the clean-coder contract.
11
+ - **Path A — 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. The Step 2 lifecycle resolution in [Team lifecycle](SKILL.md#team-lifecycle-path-a-only) parses this exact error in `auto` mode to attach to the existing team rather than fail. **Path B:** no `TeamCreate`; parallel work uses parallel **`Task`** calls per [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md).
7
12
  - **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.
8
13
  - **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.
9
14
  - **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.
10
15
  - **One up-front confirmation = whole cycle.** The `/bugteam` invocation authorizes the entire cycle; every subsequent decision runs on that single authorization.
11
16
  - **10-loop hard cap.** Counted as **AUDIT** completions (increment in Step 3). Standards-fix passes before an audit do not advance `loop_count`. Worst case includes extra clean-coder spawns for the code-rules gate.
12
- - **Code rules gate before every AUDIT.** Run `scripts/bugteam_code_rules_gate.py` until exit **0** before spawning **bugfind**. Same `validate_content` logic as `hooks/blocking/code_rules_enforcer.py`.
17
+ - **Code rules gate before every AUDIT.** Run `_shared/pr-loop/scripts/code_rules_gate.py` (resolved via `${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/code_rules_gate.py`) until exit **0** before spawning **bugfind**. Same `validate_content` logic as `hooks/blocking/code_rules_enforcer.py`.
13
18
  - **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.
14
19
  - **Targeted fixes.** Each fix teammate sees ONLY the most recent audit's findings. Prior loops are invisible to the fix teammate.
15
20
  - **Opus 4.7 at xhigh effort for both teammates.** Both `Agent(...)` spawns pass `model="opus"`, which resolves to Opus 4.7 on the Anthropic API. Opus 4.7's default effort level in Claude Code is `xhigh` (https://code.claude.com/docs/en/model-config — *"On Opus 4.7, the default effort is `xhigh` for all plans and providers."*), so no `effort` override is needed at spawn time. Effort is set per-subagent in YAML frontmatter, not via the `Agent` tool's parameters; `code-quality-agent` and `clean-coder` rely on the model default. The trade vs Sonnet is higher per-loop cost in exchange for deeper audit recall and stronger fix correctness on bug-hunting work, which the per-PR loop economics tolerate (10-loop hard cap bounds total spend).
16
21
  - **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.
17
22
  - **One commit per fix action.** Loops produce one commit per loop, not one per bug.
18
23
  - **Linear branch, fixed PR base.** Every loop appends one forward-only commit; existing commits and the PR base stay intact throughout the cycle.
19
- - **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.
20
- - **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.
24
+ - **Lead-only cleanup, gated by `team_owned`.** 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. Step 4 calls `TeamDelete` **only when `team_owned == true`** (this invocation called `TeamCreate` itself). When `team_owned == false` (lifecycle `attach`, or `auto` after the runtime's `Already leading team` fallback), the orchestrator that originally created the team owns teardown — see [Team lifecycle](SKILL.md#team-lifecycle-path-a-only).
25
+ - **Orchestrators must use `attach` mode, not `owned`.** When `/bugteam` runs inside an orchestrator that is itself managing a long-lived team across PRs (`pr-converge` multi-PR mode, `monitor-open-prs`), the orchestrator passes `BUGTEAM_TEAM_LIFECYCLE=attach` and `BUGTEAM_TEAM_NAME=<existing>`. `owned` mode under such an orchestrator would either error out (the session already leads a team) or, worse, tear down the orchestrator's team mid-sweep on the first invocation's Step 4. `auto` is the safe default for ambiguous callers; `attach` is the explicit-orchestrator contract.
26
+ - **Cleanup the per-team scoped temp directory on exit, gated by `team_owned`.** When `team_owned == true`, the resolved `<team_temp_dir>` is removed entirely so no loop patches leak between runs. When `team_owned == false`, only this invocation's per-PR subfolders (`<team_temp_dir>/pr-<N>/`) are removed; the orchestrator-owned parent stays so the next attached invocation can write its own per-PR subfolders without colliding.
21
27
  - **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.
22
- - **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.
28
+ - **Audit/fix comment posting.** **Path A:** 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. **Path B:** the **lead** performs the same POSTs after Task handoffs (`SKILL.md` Step 2.5 + [`reference/workflow-path-b-task-harness.md`](reference/workflow-path-b-task-harness.md) § Step 2.5).
23
29
  - **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.
24
30
  - **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.
25
31
  - **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.