claude-dev-env 1.35.0 → 1.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/clean-coder.md +109 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +451 -39
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +182 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/SKILL.md +364 -154
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +103 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +1206 -131
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +56 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -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,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
|
-
|
|
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
|
|
119
|
+
assert WINDOWS_CREATE_NO_WINDOW_FLAG > 0
|
|
120
120
|
assert WINDOWS_CREATE_NEW_PROCESS_GROUP_FLAG > 0
|
|
121
121
|
assert (
|
|
122
|
-
|
|
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"
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
24
|
-
"
|
|
23
|
+
"_shared",
|
|
24
|
+
"pr-loop",
|
|
25
25
|
"scripts",
|
|
26
|
-
"
|
|
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 / "
|
|
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
|
-
/ "
|
|
67
|
-
/ "
|
|
66
|
+
/ "_shared"
|
|
67
|
+
/ "pr-loop"
|
|
68
68
|
/ "scripts"
|
|
69
|
-
/ "
|
|
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
|
|
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 / "
|
|
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" / "
|
|
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" / "
|
|
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 / "
|
|
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 / "
|
|
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
package/package.json
CHANGED
|
@@ -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
|
-
- **
|
|
6
|
-
- **
|
|
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/
|
|
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
|
|
20
|
-
- **
|
|
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
|
-
- **
|
|
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.
|