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.
Files changed (115) 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 +364 -154
  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/scripts/reflow_skill_md.py +298 -0
  50. package/skills/bugteam/test_skill_additions.py +13 -4
  51. package/skills/bugteam/test_team_lifecycle.py +103 -0
  52. package/skills/findbugs/SKILL.md +3 -3
  53. package/skills/fixbugs/SKILL.md +4 -4
  54. package/skills/monitor-open-prs/SKILL.md +32 -2
  55. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  56. package/skills/pr-converge/SKILL.md +1206 -131
  57. package/skills/pr-converge/scripts/README.md +145 -0
  58. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  59. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  60. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  61. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  65. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  66. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  68. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  70. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  71. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  72. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  74. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  75. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  76. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  77. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  78. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  79. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  80. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  81. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  82. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  83. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  84. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  85. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  86. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  87. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  88. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  89. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  90. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  91. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  92. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  93. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  94. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  95. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  96. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  97. package/skills/pr-converge/test_team_lifecycle.py +56 -0
  98. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  99. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  100. package/skills/qbug/SKILL.md +4 -4
  101. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  102. package/skills/resume-review/SKILL.md +261 -0
  103. package/skills/bugteam/scripts/README.md +0 -58
  104. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  105. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  106. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  107. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  108. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  109. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  110. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  111. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  112. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  113. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  114. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  115. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,139 @@
1
+ """Tests for trigger_bugbot.
2
+
3
+ Covers:
4
+ - gh pr comment is invoked with --body-file (per gh-body-file rule)
5
+ - the body file written contains the literal phrase "bugbot run\\n"
6
+ - the comment URL emitted by gh is returned
7
+ - the temp body file is cleaned up
8
+ - subprocess errors propagate
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import subprocess
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+
21
+
22
+ def _load_module() -> ModuleType:
23
+ module_path = Path(__file__).parent / "trigger_bugbot.py"
24
+ spec = importlib.util.spec_from_file_location("trigger_bugbot", module_path)
25
+ assert spec is not None
26
+ assert spec.loader is not None
27
+ module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ trigger_bugbot_module = _load_module()
33
+
34
+
35
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
36
+ process = MagicMock(spec=subprocess.CompletedProcess)
37
+ process.stdout = stdout
38
+ process.returncode = 0
39
+ return process
40
+
41
+
42
+ def test_should_invoke_gh_pr_comment_with_body_file_flag() -> None:
43
+ captured_body_paths: list[str] = []
44
+
45
+ def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
46
+ invoked_argv = subprocess_args[0]
47
+ assert "--body-file" in invoked_argv
48
+ body_file_path = invoked_argv[invoked_argv.index("--body-file") + 1]
49
+ captured_body_paths.append(body_file_path)
50
+ return _completed("https://github.com/acme/widget/issues/42#issuecomment-99\n")
51
+
52
+ with patch("subprocess.run", side_effect=capture_body_file_contents) as mock_run:
53
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
54
+ invoked_argv = mock_run.call_args[0][0]
55
+ assert invoked_argv[0:3] == ["gh", "pr", "comment"]
56
+ assert "42" in invoked_argv
57
+ assert "--repo" in invoked_argv
58
+ assert "acme/widget" in invoked_argv
59
+
60
+
61
+ def test_should_write_literal_bugbot_run_phrase_into_body_file() -> None:
62
+ captured_body_contents: list[str] = []
63
+
64
+ def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
65
+ invoked_argv = subprocess_args[0]
66
+ body_file_path = Path(invoked_argv[invoked_argv.index("--body-file") + 1])
67
+ captured_body_contents.append(body_file_path.read_text(encoding="utf-8"))
68
+ return _completed("https://example.com\n")
69
+
70
+ with patch("subprocess.run", side_effect=capture_body_file_contents):
71
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
72
+ assert len(captured_body_contents) == 1
73
+ assert captured_body_contents[0] == "bugbot run\n"
74
+
75
+
76
+ def test_should_return_comment_url_from_gh_stdout() -> None:
77
+ expected_url = "https://github.com/acme/widget/pull/42#issuecomment-12345"
78
+ with patch("subprocess.run") as mock_run:
79
+ mock_run.return_value = _completed(f"{expected_url}\n")
80
+ comment_url = trigger_bugbot_module.trigger_bugbot(
81
+ owner="acme", repo="widget", number=42
82
+ )
83
+ assert comment_url == expected_url
84
+
85
+
86
+ def test_should_remove_temp_body_file_after_invocation() -> None:
87
+ captured_body_paths: list[Path] = []
88
+
89
+ def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
90
+ invoked_argv = subprocess_args[0]
91
+ captured_body_paths.append(
92
+ Path(invoked_argv[invoked_argv.index("--body-file") + 1])
93
+ )
94
+ return _completed("https://example.com\n")
95
+
96
+ with patch("subprocess.run", side_effect=capture_body_file_contents):
97
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
98
+ assert len(captured_body_paths) == 1
99
+ assert not captured_body_paths[0].exists()
100
+
101
+
102
+ def test_should_raise_when_gh_subprocess_fails() -> None:
103
+ failure = subprocess.CalledProcessError(
104
+ returncode=1, cmd=["gh"], stderr="auth failure"
105
+ )
106
+ with patch("subprocess.run", side_effect=failure):
107
+ with pytest.raises(subprocess.CalledProcessError):
108
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
109
+
110
+
111
+ def test_should_write_imported_constant_directly_without_local_alias() -> None:
112
+ captured_body_contents: list[str] = []
113
+
114
+ def capture_body_file_contents(*subprocess_args, **_subprocess_kwargs):
115
+ invoked_argv = subprocess_args[0]
116
+ body_file_path = Path(invoked_argv[invoked_argv.index("--body-file") + 1])
117
+ captured_body_contents.append(body_file_path.read_text(encoding="utf-8"))
118
+ return _completed("https://example.com\n")
119
+
120
+ with patch("subprocess.run", side_effect=capture_body_file_contents):
121
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=99)
122
+ assert len(captured_body_contents) == 1
123
+ assert (
124
+ captured_body_contents[0]
125
+ == trigger_bugbot_module.BUGBOT_RUN_TRIGGER_PHRASE
126
+ )
127
+
128
+
129
+ def test_should_render_repo_arg_via_named_template_constant() -> None:
130
+ with patch("subprocess.run") as mock_run:
131
+ mock_run.return_value = _completed("https://example.com\n")
132
+ trigger_bugbot_module.trigger_bugbot(owner="acme", repo="widget", number=42)
133
+ invoked_argv = mock_run.call_args[0][0]
134
+ expected_repo_arg = trigger_bugbot_module.GH_REPO_ARG_TEMPLATE.format(
135
+ owner="acme", repo="widget"
136
+ )
137
+ assert expected_repo_arg == "acme/widget"
138
+ repo_flag_index = invoked_argv.index("--repo")
139
+ assert invoked_argv[repo_flag_index + 1] == expected_repo_arg
@@ -0,0 +1,111 @@
1
+ """Tests for view_pr_context.
2
+
3
+ Covers:
4
+ - gh pr view is invoked with the documented --json field list
5
+ - the parsed JSON object is returned
6
+ - subprocess errors propagate
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import json
13
+ import subprocess
14
+ from pathlib import Path
15
+ from types import ModuleType
16
+ from unittest.mock import MagicMock, patch
17
+
18
+ import pytest
19
+
20
+
21
+ def _load_module() -> ModuleType:
22
+ module_path = Path(__file__).parent / "view_pr_context.py"
23
+ spec = importlib.util.spec_from_file_location("view_pr_context", module_path)
24
+ assert spec is not None
25
+ assert spec.loader is not None
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ view_pr_context_module = _load_module()
32
+
33
+
34
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
35
+ process = MagicMock(spec=subprocess.CompletedProcess)
36
+ process.stdout = stdout
37
+ process.returncode = 0
38
+ return process
39
+
40
+
41
+ def test_should_invoke_gh_pr_view_with_documented_field_list() -> None:
42
+ payload = json.dumps(
43
+ {
44
+ "number": 42,
45
+ "url": "https://github.com/acme/widget/pull/42",
46
+ "headRefOid": "abc123",
47
+ "baseRefName": "main",
48
+ "headRefName": "feat/x",
49
+ "isDraft": True,
50
+ }
51
+ )
52
+ with patch("subprocess.run") as mock_run:
53
+ mock_run.return_value = _completed(payload)
54
+ view_pr_context_module.view_pr_context()
55
+ invoked_argv = mock_run.call_args[0][0]
56
+ assert invoked_argv[0:3] == ["gh", "pr", "view"]
57
+ assert "--json" in invoked_argv
58
+ fields_arg = invoked_argv[invoked_argv.index("--json") + 1]
59
+ for required_field in (
60
+ "number",
61
+ "url",
62
+ "headRefOid",
63
+ "baseRefName",
64
+ "headRefName",
65
+ "isDraft",
66
+ ):
67
+ assert required_field in fields_arg
68
+
69
+
70
+ def test_should_return_parsed_json_object() -> None:
71
+ payload = {
72
+ "number": 42,
73
+ "url": "https://github.com/acme/widget/pull/42",
74
+ "headRefOid": "abc123",
75
+ "baseRefName": "main",
76
+ "headRefName": "feat/x",
77
+ "isDraft": True,
78
+ }
79
+ with patch("subprocess.run") as mock_run:
80
+ mock_run.return_value = _completed(json.dumps(payload))
81
+ pr_context = view_pr_context_module.view_pr_context()
82
+ assert pr_context == payload
83
+
84
+
85
+ def test_should_raise_when_gh_subprocess_fails() -> None:
86
+ failure = subprocess.CalledProcessError(
87
+ returncode=1, cmd=["gh"], stderr="auth failure"
88
+ )
89
+ with patch("subprocess.run", side_effect=failure):
90
+ with pytest.raises(subprocess.CalledProcessError):
91
+ view_pr_context_module.view_pr_context()
92
+
93
+
94
+ def test_should_pass_imported_constant_directly_without_local_alias() -> None:
95
+ payload = json.dumps(
96
+ {
97
+ "number": 7,
98
+ "url": "https://github.com/acme/widget/pull/7",
99
+ "headRefOid": "deadbeef",
100
+ "baseRefName": "main",
101
+ "headRefName": "feat/y",
102
+ "isDraft": False,
103
+ }
104
+ )
105
+ with patch("subprocess.run") as mock_run:
106
+ mock_run.return_value = _completed(payload)
107
+ view_pr_context_module.view_pr_context()
108
+ invoked_argv = mock_run.call_args[0][0]
109
+ fields_arg = invoked_argv[invoked_argv.index("--json") + 1]
110
+ expected_fields = view_pr_context_module.PR_CONTEXT_FIELDS
111
+ assert fields_arg is expected_fields
@@ -0,0 +1,77 @@
1
+ """Post a `bugbot run` comment to re-trigger a Cursor Bugbot review.
2
+
3
+ Writes the literal trigger phrase to a temp file (per the gh-body-file rule —
4
+ `gh pr comment --body "..."` may corrupt backticks), invokes
5
+ `gh pr comment --body-file`, and removes the temp file on success or failure.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ if str(Path(__file__).resolve().parent) not in sys.path:
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+
18
+ from evict_cached_config_modules import evict_cached_config_modules
19
+
20
+ evict_cached_config_modules()
21
+
22
+ from config.pr_converge_constants import (
23
+ BUGBOT_RUN_TEMPFILE_PREFIX,
24
+ BUGBOT_RUN_TEMPFILE_SUFFIX,
25
+ BUGBOT_RUN_TRIGGER_PHRASE,
26
+ GH_REPO_ARG_TEMPLATE,
27
+ )
28
+
29
+
30
+ def trigger_bugbot(*, owner: str, repo: str, number: int) -> str:
31
+ """Post the bugbot re-trigger comment, return the comment URL gh emits."""
32
+ file_descriptor, raw_path = tempfile.mkstemp(
33
+ suffix=BUGBOT_RUN_TEMPFILE_SUFFIX, prefix=BUGBOT_RUN_TEMPFILE_PREFIX
34
+ )
35
+ try:
36
+ os.close(file_descriptor)
37
+ body_file_path = Path(raw_path)
38
+ body_file_path.write_text(BUGBOT_RUN_TRIGGER_PHRASE, encoding="utf-8")
39
+ repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
40
+ gh_command: list[str] = [
41
+ "gh",
42
+ "pr",
43
+ "comment",
44
+ str(number),
45
+ "--repo",
46
+ repo_arg,
47
+ "--body-file",
48
+ str(body_file_path),
49
+ ]
50
+ completed = subprocess.run(
51
+ gh_command,
52
+ capture_output=True,
53
+ check=True,
54
+ text=True,
55
+ encoding="utf-8",
56
+ errors="replace",
57
+ )
58
+ return completed.stdout.strip()
59
+ finally:
60
+ Path(raw_path).unlink(missing_ok=True)
61
+
62
+
63
+ def main() -> int:
64
+ parser = argparse.ArgumentParser(description=__doc__)
65
+ parser.add_argument("--owner", required=True)
66
+ parser.add_argument("--repo", required=True)
67
+ parser.add_argument("--number", required=True, type=int)
68
+ parsed_arguments = parser.parse_args()
69
+ comment_url = trigger_bugbot(
70
+ owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number
71
+ )
72
+ sys.stdout.write(f"{comment_url}\n")
73
+ return 0
74
+
75
+
76
+ if __name__ == "__main__":
77
+ sys.exit(main())
@@ -0,0 +1,47 @@
1
+ """Resolve the per-tick PR context (number, url, head sha, branch names, draft state).
2
+
3
+ Wraps `gh pr view --json ...` so the skill body emits one script invocation
4
+ instead of repeating the field list inline.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ if str(Path(__file__).resolve().parent) not in sys.path:
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+
16
+ from evict_cached_config_modules import evict_cached_config_modules
17
+
18
+ evict_cached_config_modules()
19
+
20
+ from config.pr_converge_constants import PR_CONTEXT_FIELDS
21
+
22
+
23
+ def view_pr_context() -> dict[str, object]:
24
+ """Return the parsed JSON object from `gh pr view --json <fields>`."""
25
+ gh_command: list[str] = ["gh", "pr", "view", "--json", PR_CONTEXT_FIELDS]
26
+ completed = subprocess.run(
27
+ gh_command,
28
+ capture_output=True,
29
+ check=True,
30
+ text=True,
31
+ encoding="utf-8",
32
+ errors="replace",
33
+ )
34
+ return json.loads(completed.stdout)
35
+
36
+
37
+ def main() -> int:
38
+ parser = argparse.ArgumentParser(description=__doc__)
39
+ parser.parse_args()
40
+ pr_context = view_pr_context()
41
+ json.dump(pr_context, sys.stdout)
42
+ sys.stdout.write("\n")
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ sys.exit(main())
@@ -0,0 +1,56 @@
1
+ """Markdown assertion tests for pr-converge orchestrator team lifecycle.
2
+
3
+ Locks in the contract that pr-converge multi-PR orchestration must:
4
+ - own a single long-lived team for the whole sweep
5
+ - pass that team to every bugteam invocation via attach mode
6
+ - tear down only when every PR reaches `converged` or `blocked`
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import pathlib
12
+
13
+
14
+ def _skill_text() -> str:
15
+ here = pathlib.Path(__file__).parent
16
+ return (here / "SKILL.md").read_text(encoding="utf-8")
17
+
18
+
19
+ def test_skill_documents_orchestrator_owned_team_in_multi_pr_mode():
20
+ skill_text = _skill_text()
21
+ assert "team_name" in skill_text
22
+ assert "TeamCreate" in skill_text
23
+ assert "orchestrator" in skill_text.lower()
24
+
25
+
26
+ def test_skill_passes_attach_mode_to_bugteam_invocations():
27
+ skill_text = _skill_text()
28
+ assert "BUGTEAM_TEAM_LIFECYCLE" in skill_text
29
+ assert "attach" in skill_text
30
+ assert "BUGTEAM_TEAM_NAME" in skill_text
31
+
32
+
33
+ def test_skill_tears_down_team_only_on_full_convergence():
34
+ skill_text = _skill_text()
35
+ assert "TeamDelete" in skill_text
36
+ convergence_phrases = [
37
+ "every PR",
38
+ "all PRs",
39
+ "fully converged",
40
+ "every prs[",
41
+ ]
42
+ assert any(phrase in skill_text for phrase in convergence_phrases)
43
+
44
+
45
+ def test_state_schema_includes_team_name_field():
46
+ skill_text = _skill_text()
47
+ assert '"team_name"' in skill_text or "team_name:" in skill_text
48
+
49
+
50
+ def test_skill_md_physical_lines_fit_eighty_column_limit():
51
+ skill_text = _skill_text()
52
+ for each_line_number, each_physical_line in enumerate(skill_text.splitlines(), 1):
53
+ assert len(each_physical_line) <= 80, (
54
+ "SKILL.md line %s exceeds 80 columns (%s chars)"
55
+ % (each_line_number, len(each_physical_line))
56
+ )
@@ -0,0 +1,108 @@
1
+ # AHK auto-continue loop pacing (pr-converge)
2
+
3
+ Load this document when **`ScheduleWakeup` is not available** in this session (orchestrated teams disabled, restricted tool registry, Cursor
4
+ without that primitive, or the user wants a visibly-running pacer). Follow it for **every** instruction below that depends on that choice.
5
+ Shared bugbot / bugteam / Fix protocol steps stay in the main `SKILL.md`.
6
+
7
+ ## Session behavior
8
+
9
+ Keep ticks in the **same** window the auto-typer targets so each `continue` re-enters here and reads the same state line and `gh` context.
10
+
11
+ ## Why this path exists
12
+
13
+ It is not a separate "mode" the user must remember — bare `/pr-converge` already implies loop-until-done; when the primary wakeup tool is
14
+ missing, fall through to AHK automatically. The per-tick work is unchanged; what changes is who fires the next tick. Instead of
15
+ `ScheduleWakeup` re-entering the skill, an external AutoHotkey utility auto-types `continue` into the active Claude Code window every 5
16
+ minutes, and the model treats each `continue` as the next tick trigger.
17
+
18
+ **AHK is loop pacing only:** every `phase == BUGTEAM` tick still runs **`/bugteam`** via the bugteam skill per Step 2 of the main skill —
19
+ nothing here replaces that audit.
20
+
21
+ **Fix protocol** commits use **`Task`** with **`subagent_type: "generalPurpose"`** and the **clean-coder preamble** from the main
22
+ [`SKILL.md` Fix protocol](../SKILL.md#fix-protocol) section (same as ScheduleWakeup pacing — Cursor has no `clean-coder` `subagent_type`).
23
+
24
+ Ensure `~/.claude/agents/clean-coder.md` exists (Windows: `%USERPROFILE%\.claude\agents\clean-coder.md`). Optionally also copy it to
25
+ `.cursor/agents/clean-coder.md` in the repo when you want the file co-located with the checkout; the spawn **prompt** must still name the
26
+ absolute path the subagent should **Read** first.
27
+
28
+ ### One-time setup at the start of the loop
29
+
30
+ The skill bundles its driver scripts under `scripts/` and resolves them at runtime via `$HOME\.claude\skills\pr-converge\scripts\…` (the same
31
+ convention `/logifix` uses). The bundled `.cmd` launchers locate their siblings via `%~dp0`, so they need no path arguments — only the AHK
32
+ target PID.
33
+
34
+ Run these two commands in order (PowerShell-friendly Bash escaping):
35
+
36
+ 1. Resolve the PID of the GUI ancestor that hosts this Claude Code session:
37
+ ```bash
38
+ pwsh -NoProfile -ExecutionPolicy Bypass -File "$HOME\.claude\skills\pr-converge\scripts\caller-window-pid.ps1"
39
+ ```
40
+ Capture the printed integer as `caller_pid`. Verify it points at the right window before continuing:
41
+ ```bash
42
+ pwsh -NoProfile -Command "Get-Process -Id $caller_pid | Select-Object Id,ProcessName,MainWindowTitle"
43
+ ```
44
+ 2. Launch the auto-typer attached to that PID with auto-start enabled. The bundled launcher accepts the PID as its first arg and the
45
+ `--start-on` flag is forwarded to the AHK script:
46
+ ```bash
47
+ "$HOME\.claude\skills\pr-converge\scripts\cursor-agents-continue.cmd" $caller_pid --start-on
48
+ ```
49
+ AutoHotkey v2 must be installed at `C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe`.
50
+
51
+ ### Per-tick behavior under this driver
52
+
53
+ - Run Steps 1–3 of **Per-tick work** in the main `SKILL.md` exactly as written.
54
+ - In **Step 4**, do **not** call `ScheduleWakeup` — the auto-typer is the pacer (this is the fallback branch of Step 4 in the main skill).
55
+ - End every assistant response with the literal sentence `Awaiting next "continue" tick.` so the next iteration is unambiguously identifiable
56
+ in the transcript.
57
+ - When the next user message is `continue` (auto-typed by AHK) or any close paraphrase, treat it as the next tick of default-loop
58
+ `/pr-converge` and re-enter from Step 1 against the freshest PR state.
59
+
60
+ ### Convergence cleanup
61
+
62
+ On back-to-back clean (the existing convergence rule in the main skill), run `gh pr ready`, then kill the auto-typer when this session used
63
+ this AHK pacing path:
64
+
65
+ ```bash
66
+ pwsh -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='AutoHotkey64.exe'\" | Where-Object CommandLine -like '*cursor-agents-continue.ahk*' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
67
+ ```
68
+
69
+ Report convergence in the same one-sentence shape as the standard flow, plus a second sentence noting the auto-typer was stopped. The skill
70
+ returns; no next tick fires.
71
+
72
+ ### Gotchas
73
+
74
+ - **Resolver fallback semantics matter.** `caller-window-pid.ps1` walks up the parent process chain, terminates at `explorer.exe`, and falls
75
+ back to the foreground window when no GUI ancestor is found. Always verify `MainWindowTitle` after capture — if it isn't the Claude Code
76
+ session, the auto-typer will fire `continue` into the wrong window and the loop stalls silently.
77
+ - **Tick-duration vs. 5-minute cadence.** The auto-typer fires every 5 minutes regardless of model activity. A tick that runs longer than 5
78
+ minutes will receive a queued `continue` while still in flight; Claude Code processes these sequentially, so there's no corruption, but the
79
+ loop pace becomes irregular. Don't try to "fix" this by shortening the AHK interval — the `bugbot run` cadence already has its own pacing
80
+ baked into the standard flow.
81
+ - **AHK runs as `#SingleInstance Force`.** Re-running the launcher replaces the prior instance silently. Safe to re-issue if the loop appears
82
+ stalled.
83
+ - **`Stop-Process -Force` on `AutoHotkey64` is broad.** It kills every AHK instance, not just the one this skill started. When the user has
84
+ unrelated AHK utilities running, scope the kill by command-line match instead:
85
+ ```bash
86
+ pwsh -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='AutoHotkey64.exe'\" | Where-Object CommandLine -like '*cursor-agents-continue.ahk*' | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
87
+ ```
88
+ - **State-line responsibility is unchanged.** The state line (phase, bugbot_clean_at, inline_lag_streak, tick_count) is still emitted at the
89
+ end of every tick — it's how the next tick reads prior state. The auto-typer only fires `continue`; it does not preserve state for you.
90
+ - **No `tick_count` ceiling.** `tick_count` is observability-only (same as the main skill and `state-schema.md`). This path ends on convergence or **Stop conditions** in `SKILL.md`, not on a tick counter.
91
+ - **`/bugteam` is not optional for BUGTEAM ticks.** AHK only paces **when** the next tick runs; it does not replace the bugteam skill. Skipping
92
+ **`/bugteam`** after a clean Bugbot review breaks the back-to-back contract.
93
+ - **Fix protocol:** use **`Task` + `generalPurpose`** with the clean-coder **Read** preamble from the main [`SKILL.md` Fix protocol](../SKILL.md#fix-protocol)
94
+ (never a bare `generalPurpose` production edit). Ensure the clean-coder agent markdown exists at `~/.claude/agents/clean-coder.md` (Windows:
95
+ `%USERPROFILE%\.claude\agents\clean-coder.md`); copy into `.cursor/agents/` only if you want a repo-local duplicate.
96
+
97
+ ## BUGBOT inline-lag (this path only)
98
+
99
+ When Step 2 BUGBOT branch c routes to API lag and you are on **this** pacing path: complete Step 4 per **Per-tick behavior under this driver**
100
+ above (fixed AHK cadence — there is no 60s shortcut). The inline comments should appear on the next tick.
101
+
102
+ ## Convergence
103
+
104
+ On back-to-back clean: stop the auto-typer per **Convergence cleanup** above; omit `ScheduleWakeup` (not used on this path).
105
+
106
+ ## Stop / safety (this path)
107
+
108
+ On hard blockers or user stop: omit loop pacing and stop the AHK auto-typer if it was started, per main skill **Stop conditions**. Use the same **scoped** `Get-CimInstance` / `Stop-Process` command as **Convergence cleanup** (command-line match on `cursor-agents-continue.ahk`) so unrelated AutoHotkey instances are not killed.
@@ -0,0 +1,37 @@
1
+ # ScheduleWakeup loop pacing (pr-converge)
2
+
3
+ Load this document when **`ScheduleWakeup` is available** in the parent harness session and you use it for converge **loop pacing** (primary /
4
+ orchestrated-teams path). Follow it for **every** instruction below that depends on that choice. Shared bugbot / bugteam / Fix protocol steps
5
+ stay in the main `SKILL.md`.
6
+
7
+ ## Session behavior
8
+
9
+ Call `ScheduleWakeup` from this same session so the next tick fires back into **this** transcript with the prior tick's state line and PR
10
+ context still addressable.
11
+
12
+ ## Step 4 — `ScheduleWakeup` branch
13
+
14
+ At end of tick (unless convergence or another stop condition already omitted pacing), call `ScheduleWakeup` with:
15
+
16
+ - `delaySeconds: 270` whenever bugbot was just re-triggered (whether by Step 3 directly, by the Fix protocol's mandatory re-trigger, or by
17
+ BUGTEAM branch 1's same-tick re-trigger). Bugbot finishes a review in 1–4 minutes, so 270s stays under the 5-minute prompt-cache TTL while
18
+ giving a margin past bugbot's typical upper bound. The single exception is the BUGBOT inline-lag branch in Step 2 of the main skill, which
19
+ uses `delaySeconds: 60` because no re-trigger fired and the only thing being awaited is GitHub's inline-comments API catching up.
20
+ - `reason`: one short sentence on what is being awaited, including the current `phase` and `bugbot_clean_at` SHA when set.
21
+ - `prompt: "/pr-converge"` — re-enters this skill on the next firing with default loop semantics (no need for the user to type `/loop`). If
22
+ the parent harness requires the `/loop` wrapper for wakeups to execute, `prompt: "/loop /pr-converge"` is equivalent.
23
+
24
+ ## BUGBOT inline-lag (this path only)
25
+
26
+ When Step 2 BUGBOT branch c routes to API lag and you are on **this** pacing path: complete Step 4 with `ScheduleWakeup` using `delaySeconds:
27
+ 60` (lag is short-lived).
28
+
29
+ ## Convergence
30
+
31
+ On back-to-back clean: **omit** further `ScheduleWakeup` calls. Do not start the AHK auto-typer for loop pacing when this path is the active
32
+ pacer.
33
+
34
+ ## Stop / safety (this path)
35
+
36
+ On hard blockers or user stop: omit `ScheduleWakeup` per main skill **Stop conditions**. If the session never used AHK for pacing,
37
+ skip AHK shutdown commands in the companion AHK workflow.
@@ -19,8 +19,8 @@ description: >-
19
19
 
20
20
  Shared artifacts with /bugteam are referenced below by path, using the `${CLAUDE_SKILL_DIR}` host-substitution convention (both skills land under `~/.claude/skills/` after install):
21
21
 
22
- - Pre-flight script: `${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_preflight.py`
23
- - Code-rules gate script: `${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_code_rules_gate.py`
22
+ - Pre-flight script: `${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/preflight.py`
23
+ - Code-rules gate script: `${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/code_rules_gate.py`
24
24
  - Bug category rubric A–J: [`bugteam/PROMPTS.md`](../bugteam/PROMPTS.md#audit-spawn-prompt-xml-bugfind-teammate)
25
25
  - **Audit contract** (finding schema, proof-of-absence, adversarial pass, Haiku secondary, post-fix self-audit, diagnostics JSON): [`bugteam/reference/audit-contract.md`](../bugteam/reference/audit-contract.md)
26
26
  - PR comment lifecycle shape: [`bugteam/SKILL.md`](../bugteam/SKILL.md#step-25-pr-comments-one-review-per-loop)
@@ -48,7 +48,7 @@ Refusals — first match wins; respond with the quoted line exactly and stop:
48
48
  ## Step 0: Pre-flight
49
49
 
50
50
  ```bash
51
- python "${CLAUDE_SKILL_DIR}/../bugteam/scripts/bugteam_preflight.py"
51
+ python "${CLAUDE_SKILL_DIR}/../../_shared/pr-loop/scripts/preflight.py"
52
52
  ```
53
53
 
54
54
  `${CLAUDE_SKILL_DIR}` is host-substituted before the shell runs. Non-zero → fix before continuing. `BUGTEAM_PREFLIGHT_SKIP=1` is emergency only. Add `--pre-commit` when `.pre-commit-config.yaml` exists.
@@ -97,7 +97,7 @@ import os
97
97
  from pathlib import Path
98
98
 
99
99
  skill_dir = Path(os.environ["CLAUDE_SKILL_DIR"])
100
- gate_script_path = (skill_dir / ".." / "bugteam" / "scripts" / "bugteam_code_rules_gate.py").resolve()
100
+ gate_script_path = (skill_dir / ".." / ".." / "_shared" / "pr-loop" / "scripts" / "code_rules_gate.py").resolve()
101
101
  categories_file_path = (skill_dir / ".." / "bugteam" / "PROMPTS.md").resolve()
102
102
  ```
103
103
 
@@ -22,8 +22,8 @@ def _load_skill_text() -> str:
22
22
 
23
23
  def test_should_require_post_fix_gate_before_git_add() -> None:
24
24
  skill_text = _load_skill_text()
25
- assert "bugteam_code_rules_gate" in skill_text, (
26
- "FIX step must run bugteam_code_rules_gate against modified files"
25
+ assert "code_rules_gate" in skill_text, (
26
+ "FIX step must run code_rules_gate against modified files"
27
27
  )
28
28
  assert "post-fix" in skill_text.lower() or "post_fix" in skill_text.lower(), (
29
29
  "FIX step must reference a post-fix audit phase"