claude-dev-env 8.31.1 → 8.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pull-request/SKILL.md +4 -3
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +5 -2
- package/.agents/skills/pull-request/scripts/test_pull_request.py +32 -0
- package/commands/fix-followups.md +3 -3
- package/commands/sr-loop.md +4 -3
- package/hooks/advisory/test_pr_done_reminder.py +2 -1
- package/hooks/blocking/block_main_commit.py +2 -10
- package/hooks/blocking/test_block_main_commit.py +13 -0
- package/hooks/hooks_constants/block_main_commit_constants.py +9 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +1 -1
- package/hooks/pyproject.toml +0 -1
- package/hooks/validators/README.md +0 -1
- package/hooks/validators/health_check.py +0 -1
- package/hooks/validators/run_all_validators.py +0 -13
- package/hooks/validators/test_run_all_validators.py +5 -36
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +1 -1
- package/rules/flag-non-breaking-findings.md +1 -1
- package/rules/git-workflow.md +2 -7
- package/rules/review-closure-is-a-check.md +1 -1
- package/scripts/dev_env_scripts_constants/followup_constants.py +1 -1
- package/scripts/test_followup_cli.py +2 -1
- package/hooks/validators/git_checks.py +0 -110
- package/hooks/validators/test_git_checks.py +0 -112
|
@@ -32,7 +32,7 @@ process-local author value. Keep the parent environment unchanged.
|
|
|
32
32
|
|
|
33
33
|
Use this skill for one pull request action:
|
|
34
34
|
|
|
35
|
-
- Create a draft
|
|
35
|
+
- Create a pull request, ready for review by default or a draft with `--draft`.
|
|
36
36
|
- Edit a pull request title or body.
|
|
37
37
|
- Add a pull request comment.
|
|
38
38
|
- Submit a pull request review.
|
|
@@ -91,7 +91,8 @@ Process steps in order.
|
|
|
91
91
|
|
|
92
92
|
Resolve the repository, action, and selected GitHub author. For create, resolve
|
|
93
93
|
the source branch, base, and head. For other actions, resolve one existing pull
|
|
94
|
-
request target. Create
|
|
94
|
+
request target. Create publishes a ready pull request, and `--draft` publishes a
|
|
95
|
+
draft. Record the target before any write.
|
|
95
96
|
|
|
96
97
|
### 2. Author the title and body
|
|
97
98
|
|
|
@@ -185,7 +186,7 @@ only after remote readback proves the requested state.
|
|
|
185
186
|
## Examples
|
|
186
187
|
|
|
187
188
|
Create example: the writer produces `pr-body.md`. The linter exits `0` for
|
|
188
|
-
`pr-create`. `pull_request.py create` publishes one
|
|
189
|
+
`pr-create`. `pull_request.py create` publishes one ready pull request. The
|
|
189
190
|
readback matches the title, body, head SHA, and draft state.
|
|
190
191
|
|
|
191
192
|
Rejected comment example: the comment body names a worktree file. The linter
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
When the session exposes a task tool, register each line as one session task
|
|
4
4
|
before starting the process. This file is a task seed catalog.
|
|
5
5
|
|
|
6
|
-
1. Resolve one repository, action, and selected author. For create, resolve the source branch, base, and head. For other actions, resolve one existing pull request target. Create
|
|
6
|
+
1. Resolve one repository, action, and selected author. For create, resolve the source branch, base, and head. For other actions, resolve one existing pull request target. Create publishes a ready pull request, and `--draft` publishes a draft.
|
|
7
7
|
2. Record the title and body file for create or full body rewrite, written by you or drafted by `pstack:poteto-agent` with a PR-writing brief. Mark `N/A` for comment or review bodies that do not rewrite the pull request description.
|
|
8
8
|
3. Validate every title, body, artifact, and local path input. Reject inline body input.
|
|
9
9
|
4. Resolve the active managed root and run `<managed-root>/scripts/durable_post_lint.py` for the matching action. Record exit code `0` before credential lookup or network work.
|
|
@@ -43,6 +43,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
43
43
|
create.add_argument("--head", required=True)
|
|
44
44
|
create.add_argument("--title", required=True)
|
|
45
45
|
create.add_argument("--body-file", required=True, type=Path)
|
|
46
|
+
create.add_argument("--draft", dest="is_draft", action="store_true")
|
|
46
47
|
edit = all_actions.add_parser(ACTION_EDIT, allow_abbrev=False)
|
|
47
48
|
edit.add_argument("--repo", required=True)
|
|
48
49
|
edit.add_argument("--number", required=True)
|
|
@@ -105,11 +106,10 @@ def _linter_arguments(
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
def _create_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
108
|
-
|
|
109
|
+
all_arguments = [
|
|
109
110
|
"gh",
|
|
110
111
|
"pr",
|
|
111
112
|
"create",
|
|
112
|
-
"--draft",
|
|
113
113
|
"--repo",
|
|
114
114
|
arguments.repo,
|
|
115
115
|
"--base",
|
|
@@ -121,6 +121,9 @@ def _create_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
|
121
121
|
"--body-file",
|
|
122
122
|
str(arguments.body_file),
|
|
123
123
|
]
|
|
124
|
+
if arguments.is_draft:
|
|
125
|
+
all_arguments.append("--draft")
|
|
126
|
+
return all_arguments
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
def _edit_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
@@ -47,7 +47,38 @@ ACTION_CASES = [
|
|
|
47
47
|
"gh",
|
|
48
48
|
"pr",
|
|
49
49
|
"create",
|
|
50
|
+
"--repo",
|
|
51
|
+
REPOSITORY,
|
|
52
|
+
"--base",
|
|
53
|
+
"main",
|
|
54
|
+
"--head",
|
|
55
|
+
"feature/post",
|
|
56
|
+
"--title",
|
|
57
|
+
PR_TITLE,
|
|
58
|
+
"--body-file",
|
|
59
|
+
BODY_FILENAME,
|
|
60
|
+
],
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
[
|
|
64
|
+
"create",
|
|
65
|
+
"--repo",
|
|
66
|
+
REPOSITORY,
|
|
67
|
+
"--base",
|
|
68
|
+
"main",
|
|
69
|
+
"--head",
|
|
70
|
+
"feature/post",
|
|
71
|
+
"--title",
|
|
72
|
+
PR_TITLE,
|
|
73
|
+
"--body-file",
|
|
74
|
+
BODY_FILENAME,
|
|
50
75
|
"--draft",
|
|
76
|
+
],
|
|
77
|
+
"pr-create",
|
|
78
|
+
[
|
|
79
|
+
"gh",
|
|
80
|
+
"pr",
|
|
81
|
+
"create",
|
|
51
82
|
"--repo",
|
|
52
83
|
REPOSITORY,
|
|
53
84
|
"--base",
|
|
@@ -58,6 +89,7 @@ ACTION_CASES = [
|
|
|
58
89
|
PR_TITLE,
|
|
59
90
|
"--body-file",
|
|
60
91
|
BODY_FILENAME,
|
|
92
|
+
"--draft",
|
|
61
93
|
],
|
|
62
94
|
),
|
|
63
95
|
(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Fix every non-breaking finding the follow-up ledger holds and open a
|
|
2
|
+
description: Fix every non-breaking finding the follow-up ledger holds and open a PR for it
|
|
3
3
|
argument-hint: [repository root, or blank for the current repository]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Fix the non-breaking findings a local gate recorded rather than blocked on,
|
|
7
|
-
and deliver them as one
|
|
7
|
+
and deliver them as one pull request.
|
|
8
8
|
|
|
9
9
|
The repository root is `$ARGUMENTS`, or the current repository when that is
|
|
10
10
|
blank.
|
|
@@ -50,7 +50,7 @@ finding names a generated file, regenerate it with the repository's tooling.
|
|
|
50
50
|
Branch from the repository's default branch. Commit each group on its own,
|
|
51
51
|
with a message naming the check identifier and the files.
|
|
52
52
|
|
|
53
|
-
Open the pull request
|
|
53
|
+
Open the pull request ready for review. Write the body as:
|
|
54
54
|
|
|
55
55
|
- One `Before:` paragraph saying which findings stood open.
|
|
56
56
|
- One `After:` paragraph saying what the tree now holds.
|
package/commands/sr-loop.md
CHANGED
|
@@ -39,7 +39,7 @@ stack's design, intent, and changes.
|
|
|
39
39
|
code, not the full repo suite), commit once (`git commit -F <file>`, body
|
|
40
40
|
written with the Write tool, Co-Authored-By line, 10-minute timeout — the
|
|
41
41
|
pre-commit gate runs its own tests), and push to the PR head branch. The PR
|
|
42
|
-
|
|
42
|
+
keeps its draft or ready state.
|
|
43
43
|
3. Repeat the invocation only after substantive findings or a user request.
|
|
44
44
|
**Skips are sticky:** carry every adjudicated skip
|
|
45
45
|
forward into the next pass as "already adjudicated — do not re-report: ..."
|
|
@@ -64,8 +64,9 @@ stack's design, intent, and changes.
|
|
|
64
64
|
|
|
65
65
|
## Finish
|
|
66
66
|
|
|
67
|
-
When both phases are clean, or only fixed nits remain,
|
|
68
|
-
with `gh pr ready`. Confirm that it
|
|
67
|
+
When both phases are clean, or only fixed nits remain, a target PR that is a
|
|
68
|
+
draft goes ready with `gh pr ready`. Confirm that it now reads ready. A
|
|
69
|
+
ready target PR stays as it is.
|
|
69
70
|
|
|
70
71
|
Report: passes run per phase, commits pushed with hashes, fixes applied, and
|
|
71
72
|
the standing skip list with reasons.
|
|
@@ -242,7 +242,8 @@ def test_main_should_remind_to_open_a_pr_when_the_branch_has_none(
|
|
|
242
242
|
context = _additional_context(_run_main(monkeypatch, capsys, _payload("git push"), no_pr))
|
|
243
243
|
|
|
244
244
|
assert "No open pull request found" in context
|
|
245
|
-
assert "gh pr create
|
|
245
|
+
assert "Open a pull request with gh pr create." in context
|
|
246
|
+
assert "--draft" not in context
|
|
246
247
|
|
|
247
248
|
|
|
248
249
|
def test_main_should_stay_quiet_on_any_other_gh_failure(
|
|
@@ -23,6 +23,7 @@ if _hooks_dir not in sys.path:
|
|
|
23
23
|
|
|
24
24
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
25
25
|
|
|
26
|
+
from hooks_constants.block_main_commit_constants import PULL_REQUEST_INSTRUCTION
|
|
26
27
|
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
27
28
|
|
|
28
29
|
GIT_COMMAND_TIMEOUT_SECONDS = 5
|
|
@@ -164,20 +165,11 @@ def parse_bash_command_from_stdin() -> str:
|
|
|
164
165
|
return bash_command
|
|
165
166
|
|
|
166
167
|
|
|
167
|
-
DRAFT_PR_INSTRUCTION = (
|
|
168
|
-
" Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
|
|
169
|
-
"(2) commit your changes there, "
|
|
170
|
-
"(3) push with `git push -u origin <branch-name>`, "
|
|
171
|
-
"(4) create a draft PR with `gh pr create --draft`. "
|
|
172
|
-
"If you must commit to main, the user needs to approve explicitly."
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
168
|
def build_denial_response(branch_name: str, repo_dir: str | None) -> dict:
|
|
177
169
|
location = f" in {repo_dir}" if repo_dir else ""
|
|
178
170
|
denial_reason = (
|
|
179
171
|
f"BLOCKED: Direct commit to '{branch_name}'{location} is not allowed."
|
|
180
|
-
+
|
|
172
|
+
+ PULL_REQUEST_INSTRUCTION
|
|
181
173
|
)
|
|
182
174
|
|
|
183
175
|
return {
|
|
@@ -74,6 +74,19 @@ def test_blocks_commit_on_protected_branch_from_hook_event_cwd(tmp_path: Path) -
|
|
|
74
74
|
assert str(repository) in hook_response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def test_denial_should_open_a_ready_pull_request(tmp_path: Path) -> None:
|
|
78
|
+
repository = _create_repository(tmp_path, "main")
|
|
79
|
+
process_repository = _create_repository(tmp_path, "agent-owned-change")
|
|
80
|
+
|
|
81
|
+
completed_process = _run_commit_gate(repository, process_repository)
|
|
82
|
+
|
|
83
|
+
denial_reason = json.loads(completed_process.stdout)["hookSpecificOutput"][
|
|
84
|
+
"permissionDecisionReason"
|
|
85
|
+
]
|
|
86
|
+
assert "open a pull request with `gh pr create`" in denial_reason
|
|
87
|
+
assert "--draft" not in denial_reason
|
|
88
|
+
|
|
89
|
+
|
|
77
90
|
def test_allows_commit_on_owned_branch_from_hook_event_cwd(tmp_path: Path) -> None:
|
|
78
91
|
repository = _create_repository(tmp_path, "agent-owned-change")
|
|
79
92
|
process_repository = _create_repository(tmp_path, "main")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Constants for the protected-branch commit gate."""
|
|
2
|
+
|
|
3
|
+
PULL_REQUEST_INSTRUCTION = (
|
|
4
|
+
" Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
|
|
5
|
+
"(2) commit your changes there, "
|
|
6
|
+
"(3) push with `git push -u origin <branch-name>`, "
|
|
7
|
+
"(4) open a pull request with `gh pr create`. "
|
|
8
|
+
"If you must commit to main, the user needs to approve explicitly."
|
|
9
|
+
)
|
|
@@ -87,7 +87,7 @@ VERDICT_DONE: str = "DONE. Add the label now:"
|
|
|
87
87
|
VERDICT_NOT_DONE: str = "NOT DONE. Do not add the done label yet."
|
|
88
88
|
NO_PULL_REQUEST_REMINDER: str = (
|
|
89
89
|
"=== PR DONE CHECKLIST (context reminder, never a block) ===\n"
|
|
90
|
-
"No open pull request found for this branch. Open a
|
|
90
|
+
"No open pull request found for this branch. Open a pull request with gh pr create.\n"
|
|
91
91
|
"A branch is done only when its PR has clean CI, no merge conflicts, and the done label."
|
|
92
92
|
)
|
|
93
93
|
RECHECK_COMMAND_TEMPLATE: str = (
|
package/hooks/pyproject.toml
CHANGED
|
@@ -14,7 +14,6 @@ The checks live in `python_style_checks.py`. Shared source-line splitting, funct
|
|
|
14
14
|
| `exempt_paths.py` | Shared config, test, and hook-infrastructure path exemptions |
|
|
15
15
|
| `fast_save_validators.py` | In-process Write/Edit save-path validator roster |
|
|
16
16
|
| `file_structure_checks.py` | File structure checks for pre-PR validation |
|
|
17
|
-
| `git_checks.py` | Git and GitHub checks for pre-push review |
|
|
18
17
|
| `health_check.py` | Validator availability, dependency, and version checks |
|
|
19
18
|
| `hook_timing_harness.py` | Hook runtime measurement and threshold report |
|
|
20
19
|
| `magic_value_checks.py` | Hardcoded magic-number checks |
|
|
@@ -402,18 +402,6 @@ def run_react_checks(files: List[Path]) -> ValidatorResult:
|
|
|
402
402
|
)
|
|
403
403
|
|
|
404
404
|
|
|
405
|
-
def run_git_checks() -> ValidatorResult:
|
|
406
|
-
"""Run git/GitHub checks."""
|
|
407
|
-
result = invoke_validator_module("git_checks", [])
|
|
408
|
-
|
|
409
|
-
return ValidatorResult(
|
|
410
|
-
name="Git/PR Workflow",
|
|
411
|
-
checks="23,24",
|
|
412
|
-
passed=result.returncode == 0,
|
|
413
|
-
output=result.stdout or result.stderr or "All checks passed",
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
|
|
417
405
|
def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
418
406
|
"""Report that comment policy runs through the explicit diff-aware linter."""
|
|
419
407
|
return ValidatorResult(
|
|
@@ -1693,7 +1681,6 @@ def main() -> int:
|
|
|
1693
1681
|
|
|
1694
1682
|
validators.extend([
|
|
1695
1683
|
("File Structure", run_file_structure_checks),
|
|
1696
|
-
("Git/PR", run_git_checks),
|
|
1697
1684
|
])
|
|
1698
1685
|
|
|
1699
1686
|
for i, (name, validator_func) in enumerate(validators, 1):
|
|
@@ -19,7 +19,6 @@ from .run_all_validators import (
|
|
|
19
19
|
format_timing_report,
|
|
20
20
|
main,
|
|
21
21
|
print_header,
|
|
22
|
-
run_git_checks,
|
|
23
22
|
run_python_style_checks,
|
|
24
23
|
run_with_fallback,
|
|
25
24
|
validate_proposed_file,
|
|
@@ -164,11 +163,9 @@ class TestFixFlag:
|
|
|
164
163
|
def test_fix_flag_is_accepted(self) -> None:
|
|
165
164
|
"""Verify --fix flag is recognized without error."""
|
|
166
165
|
with patch("validators.run_all_validators.get_changed_files") as mock_get_files, \
|
|
167
|
-
patch("validators.run_all_validators.run_file_structure_checks") as mock_file
|
|
168
|
-
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
166
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
|
|
169
167
|
mock_get_files.return_value = []
|
|
170
168
|
mock_file.return_value = _passing_mock_result()
|
|
171
|
-
mock_git.return_value = _passing_mock_result()
|
|
172
169
|
|
|
173
170
|
assert _run_main_with_argv(["run_all_validators.py", "--fix"]) == 0
|
|
174
171
|
|
|
@@ -180,11 +177,10 @@ class TestFixFlag:
|
|
|
180
177
|
patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
|
|
181
178
|
patch("validators.run_all_validators.run_react_checks") as mock_react, \
|
|
182
179
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
183
|
-
patch("validators.run_all_validators.run_file_structure_checks") as mock_file
|
|
184
|
-
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
180
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
|
|
185
181
|
mock_get_files.return_value = [Path("test.py")]
|
|
186
182
|
mock_fix.return_value = ["test.py"]
|
|
187
|
-
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file
|
|
183
|
+
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file):
|
|
188
184
|
each_mock.return_value = _passing_mock_result()
|
|
189
185
|
|
|
190
186
|
_run_main_with_argv(["run_all_validators.py", "--fix"])
|
|
@@ -199,10 +195,9 @@ class TestFixFlag:
|
|
|
199
195
|
patch("validators.run_all_validators.run_test_safety_checks") as mock_test, \
|
|
200
196
|
patch("validators.run_all_validators.run_react_checks") as mock_react, \
|
|
201
197
|
patch("validators.run_all_validators.run_comment_checks") as mock_comment, \
|
|
202
|
-
patch("validators.run_all_validators.run_file_structure_checks") as mock_file
|
|
203
|
-
patch("validators.run_all_validators.run_git_checks") as mock_git:
|
|
198
|
+
patch("validators.run_all_validators.run_file_structure_checks") as mock_file:
|
|
204
199
|
mock_get_files.return_value = [Path("test.py")]
|
|
205
|
-
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file
|
|
200
|
+
for each_mock in (mock_style, mock_test, mock_react, mock_comment, mock_file):
|
|
206
201
|
each_mock.return_value = _passing_mock_result()
|
|
207
202
|
|
|
208
203
|
_run_main_with_argv(["run_all_validators.py"])
|
|
@@ -302,32 +297,6 @@ class TestStderrSurfacing:
|
|
|
302
297
|
|
|
303
298
|
assert "ImportError" in validator_result.output
|
|
304
299
|
|
|
305
|
-
def test_git_check_surfaces_stderr_when_stdout_empty(self) -> None:
|
|
306
|
-
"""When git validator crashes with no stdout, stderr must appear in output."""
|
|
307
|
-
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
308
|
-
crashed_result = MagicMock()
|
|
309
|
-
crashed_result.returncode = 1
|
|
310
|
-
crashed_result.stdout = ""
|
|
311
|
-
crashed_result.stderr = "SyntaxError: invalid syntax in git_checks.py"
|
|
312
|
-
mock_invoke.return_value = crashed_result
|
|
313
|
-
|
|
314
|
-
validator_result = run_git_checks()
|
|
315
|
-
|
|
316
|
-
assert "SyntaxError" in validator_result.output
|
|
317
|
-
|
|
318
|
-
def test_output_falls_back_to_all_checks_passed_when_both_empty(self) -> None:
|
|
319
|
-
"""When both stdout and stderr are empty and returncode is 0, use fallback."""
|
|
320
|
-
with patch("validators.run_all_validators.invoke_validator_module") as mock_invoke:
|
|
321
|
-
clean_result = MagicMock()
|
|
322
|
-
clean_result.returncode = 0
|
|
323
|
-
clean_result.stdout = ""
|
|
324
|
-
clean_result.stderr = ""
|
|
325
|
-
mock_invoke.return_value = clean_result
|
|
326
|
-
|
|
327
|
-
validator_result = run_git_checks()
|
|
328
|
-
|
|
329
|
-
assert validator_result.output == "All checks passed"
|
|
330
|
-
|
|
331
300
|
|
|
332
301
|
class TestTimingMetrics:
|
|
333
302
|
def test_create_timing_metrics_empty(self) -> None:
|
package/package.json
CHANGED
|
@@ -50,6 +50,6 @@ Delete nothing by hand. The repository deletes the head branch on merge.
|
|
|
50
50
|
|
|
51
51
|
| Rule | Role |
|
|
52
52
|
|---|---|
|
|
53
|
-
| [`git-workflow.md`](git-workflow.md) |
|
|
53
|
+
| [`git-workflow.md`](git-workflow.md) | Open ready for review, and confirm each required context fired after the push |
|
|
54
54
|
| [`ci-owns-the-gate.md`](ci-owns-the-gate.md) | The gate runs once, and it runs on CI |
|
|
55
55
|
| [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
|
|
@@ -84,7 +84,7 @@ escalates rather than opening one more quiet pull request. The number is the
|
|
|
84
84
|
repository's setting, and raising or lowering it is one edit there.
|
|
85
85
|
|
|
86
86
|
The `/fix-followups` command drives the whole pass: it reads the brief, fixes
|
|
87
|
-
each rule group, opens a
|
|
87
|
+
each rule group, opens a pull request, and clears the ledger.
|
|
88
88
|
|
|
89
89
|
## Worked example
|
|
90
90
|
|
package/rules/git-workflow.md
CHANGED
|
@@ -15,7 +15,8 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small
|
|
|
15
15
|
|
|
16
16
|
## Pull request submission rules
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
**Open every pull request ready for review.** Pass `--draft` only when the owner asks
|
|
19
|
+
for a draft.
|
|
19
20
|
|
|
20
21
|
**A release bot's PR body is machine input. Leave it alone.** Release automation reads
|
|
21
22
|
back the body of its own merged pull request to decide it owns that merge. Rewriting the
|
|
@@ -45,12 +46,6 @@ Use `.agents/skills/pull-request/scripts/recover_legacy_author.py
|
|
|
45
46
|
author record. Do not infer a record from age alone. Keep every other record
|
|
46
47
|
untouched.
|
|
47
48
|
|
|
48
|
-
## Git golden rules
|
|
49
|
-
|
|
50
|
-
1. **Draft before push.** Put a pull request in draft state before you push to it.
|
|
51
|
-
- Before push: `gh pr ready --undo`
|
|
52
|
-
- After review approved: `gh pr ready`
|
|
53
|
-
|
|
54
49
|
## Confirm the required checks fired, and let CI run them
|
|
55
50
|
|
|
56
51
|
The gate runs once, and it runs on CI. Push the branch and read its verdict.
|
|
@@ -50,5 +50,5 @@ A repository that merges through a merge queue also runs the check on `merge_gro
|
|
|
50
50
|
| Rule | Role |
|
|
51
51
|
|---|---|
|
|
52
52
|
| [`agent-merges-its-own-green-pull-request.md`](agent-merges-its-own-green-pull-request.md) | The agent that drives a pull request merges it once its gate passes |
|
|
53
|
-
| [`git-workflow.md`](git-workflow.md) |
|
|
53
|
+
| [`git-workflow.md`](git-workflow.md) | Open ready for review, and confirm each required context fired after the push |
|
|
54
54
|
| [`correction-lens.md`](correction-lens.md) | A correction becomes a control at the highest layer that can hold it |
|
|
@@ -72,7 +72,7 @@ USAGE_TEXT: str = (
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
BRIEF_HEADER: str = (
|
|
75
|
-
"Fix every finding below in one change, then open a
|
|
75
|
+
"Fix every finding below in one change, then open a pull request "
|
|
76
76
|
"for it. Each finding is non-breaking, so the change that raised it "
|
|
77
77
|
"already shipped. Keep the fixes mechanical, touch no behavior, and run "
|
|
78
78
|
"the repository's own checks before pushing."
|
|
@@ -130,7 +130,8 @@ def test_brief_names_every_recorded_finding_and_the_repository(tmp_path: Path) -
|
|
|
130
130
|
assert exit_code == 0
|
|
131
131
|
assert "instruction-git-mode" in output_text
|
|
132
132
|
assert "wrong mode" in output_text
|
|
133
|
-
assert "
|
|
133
|
+
assert "open a pull request for it" in output_text
|
|
134
|
+
assert "draft pull request" not in output_text
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
def test_brief_asks_for_no_work_on_an_empty_ledger(tmp_path: Path) -> None:
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""Git and GitHub validation checks for pre-push review."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import List
|
|
8
|
-
|
|
9
|
-
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
10
|
-
|
|
11
|
-
SUBPROCESS_TIMEOUT_SECONDS = 30
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class Violation:
|
|
16
|
-
"""Represents a validation violation."""
|
|
17
|
-
file: str
|
|
18
|
-
line: int
|
|
19
|
-
message: str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_current_branch() -> str:
|
|
23
|
-
"""Get the current git branch name."""
|
|
24
|
-
try:
|
|
25
|
-
result = subprocess.run(
|
|
26
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
check=True,
|
|
30
|
-
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
31
|
-
creationflags=hidden_window_creation_flags(),
|
|
32
|
-
)
|
|
33
|
-
return result.stdout.strip()
|
|
34
|
-
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
35
|
-
return ""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def check_draft_pr_state() -> List[Violation]:
|
|
39
|
-
"""
|
|
40
|
-
Check that PR is in draft state.
|
|
41
|
-
|
|
42
|
-
Returns empty list if:
|
|
43
|
-
- No PR exists for current branch
|
|
44
|
-
- gh CLI not available
|
|
45
|
-
- PR is in draft state
|
|
46
|
-
|
|
47
|
-
Returns violation if:
|
|
48
|
-
- PR is not in draft state
|
|
49
|
-
"""
|
|
50
|
-
branch = get_current_branch()
|
|
51
|
-
if not branch:
|
|
52
|
-
return []
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
pr_info = subprocess.run(
|
|
56
|
-
["gh", "pr", "list", "--head", branch, "--json", "number,isDraft"],
|
|
57
|
-
capture_output=True,
|
|
58
|
-
text=True,
|
|
59
|
-
check=True,
|
|
60
|
-
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
61
|
-
creationflags=hidden_window_creation_flags(),
|
|
62
|
-
)
|
|
63
|
-
except FileNotFoundError:
|
|
64
|
-
return []
|
|
65
|
-
except subprocess.CalledProcessError:
|
|
66
|
-
return []
|
|
67
|
-
except subprocess.TimeoutExpired:
|
|
68
|
-
return []
|
|
69
|
-
|
|
70
|
-
if not pr_info.stdout.strip():
|
|
71
|
-
return []
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
pr_data = json.loads(pr_info.stdout)
|
|
75
|
-
except json.JSONDecodeError:
|
|
76
|
-
return []
|
|
77
|
-
|
|
78
|
-
if not pr_data:
|
|
79
|
-
return []
|
|
80
|
-
|
|
81
|
-
is_draft = pr_data[0].get("isDraft", False)
|
|
82
|
-
|
|
83
|
-
if is_draft:
|
|
84
|
-
return []
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
Violation(
|
|
88
|
-
file="",
|
|
89
|
-
line=0,
|
|
90
|
-
message="PR must be in draft state before pushing. Run: gh pr ready --undo",
|
|
91
|
-
)
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def main() -> None:
|
|
96
|
-
"""Run all git checks and exit with appropriate code."""
|
|
97
|
-
violations: List[Violation] = []
|
|
98
|
-
|
|
99
|
-
violations.extend(check_draft_pr_state())
|
|
100
|
-
|
|
101
|
-
if violations:
|
|
102
|
-
for violation in violations:
|
|
103
|
-
print(violation.message)
|
|
104
|
-
sys.exit(1)
|
|
105
|
-
|
|
106
|
-
sys.exit(0)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
main()
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
"""Tests for git and GitHub validation checks."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
from unittest.mock import MagicMock, patch
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from .git_checks import (
|
|
9
|
-
Violation,
|
|
10
|
-
check_draft_pr_state,
|
|
11
|
-
main,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestDraftPrState:
|
|
16
|
-
"""Test that PR is in draft state when pushing review fixes."""
|
|
17
|
-
|
|
18
|
-
@patch("validators.git_checks.subprocess.run")
|
|
19
|
-
def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
|
|
20
|
-
"""When no PR exists, check should return empty list."""
|
|
21
|
-
mock_run.return_value = MagicMock(returncode=0, stdout="[]", stderr="")
|
|
22
|
-
|
|
23
|
-
violations = check_draft_pr_state()
|
|
24
|
-
|
|
25
|
-
assert violations == []
|
|
26
|
-
|
|
27
|
-
@patch("validators.git_checks.subprocess.run")
|
|
28
|
-
def test_draft_pr_passes(self, mock_run: MagicMock) -> None:
|
|
29
|
-
"""Draft PR should pass."""
|
|
30
|
-
mock_run.return_value = MagicMock(
|
|
31
|
-
returncode=0,
|
|
32
|
-
stdout='[{"number": 123, "isDraft": true}]',
|
|
33
|
-
stderr=""
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
violations = check_draft_pr_state()
|
|
37
|
-
|
|
38
|
-
assert violations == []
|
|
39
|
-
|
|
40
|
-
@patch("validators.git_checks.subprocess.run")
|
|
41
|
-
def test_non_draft_pr_fails(self, mock_run: MagicMock) -> None:
|
|
42
|
-
"""Non-draft PR should fail."""
|
|
43
|
-
mock_run.return_value = MagicMock(
|
|
44
|
-
returncode=0,
|
|
45
|
-
stdout='[{"number": 123, "isDraft": false}]',
|
|
46
|
-
stderr=""
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
violations = check_draft_pr_state()
|
|
50
|
-
|
|
51
|
-
assert len(violations) == 1
|
|
52
|
-
assert violations[0].file == ""
|
|
53
|
-
assert violations[0].line == 0
|
|
54
|
-
assert "draft" in violations[0].message.lower()
|
|
55
|
-
assert "gh pr ready --undo" in violations[0].message
|
|
56
|
-
|
|
57
|
-
@patch("validators.git_checks.subprocess.run")
|
|
58
|
-
def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
|
|
59
|
-
"""When gh CLI not available, should return empty (warning, not failure)."""
|
|
60
|
-
mock_run.side_effect = FileNotFoundError("gh not found")
|
|
61
|
-
|
|
62
|
-
violations = check_draft_pr_state()
|
|
63
|
-
|
|
64
|
-
assert violations == []
|
|
65
|
-
|
|
66
|
-
@patch("validators.git_checks.subprocess.run")
|
|
67
|
-
def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
|
|
68
|
-
"""When gh CLI times out, should return empty (warning, not failure)."""
|
|
69
|
-
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30)
|
|
70
|
-
|
|
71
|
-
violations = check_draft_pr_state()
|
|
72
|
-
|
|
73
|
-
assert violations == []
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class TestMain:
|
|
77
|
-
"""Test main function integration."""
|
|
78
|
-
|
|
79
|
-
@patch("validators.git_checks.check_draft_pr_state")
|
|
80
|
-
def test_main_no_violations_exits_zero(
|
|
81
|
-
self,
|
|
82
|
-
mock_draft: MagicMock,
|
|
83
|
-
capsys,
|
|
84
|
-
) -> None:
|
|
85
|
-
"""main() should exit 0 when no violations found."""
|
|
86
|
-
mock_draft.return_value = []
|
|
87
|
-
|
|
88
|
-
with pytest.raises(SystemExit) as exc_info:
|
|
89
|
-
main()
|
|
90
|
-
|
|
91
|
-
assert exc_info.value.code == 0
|
|
92
|
-
captured = capsys.readouterr()
|
|
93
|
-
assert captured.out == ""
|
|
94
|
-
|
|
95
|
-
@patch("validators.git_checks.check_draft_pr_state")
|
|
96
|
-
def test_main_prints_violations_without_file_line(
|
|
97
|
-
self,
|
|
98
|
-
mock_draft: MagicMock,
|
|
99
|
-
capsys,
|
|
100
|
-
) -> None:
|
|
101
|
-
"""main() should print git violations without file:line: prefix."""
|
|
102
|
-
mock_draft.return_value = [
|
|
103
|
-
Violation(file="", line=0, message="PR must be in draft state")
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
with pytest.raises(SystemExit) as exc_info:
|
|
107
|
-
main()
|
|
108
|
-
|
|
109
|
-
assert exc_info.value.code == 1
|
|
110
|
-
captured = capsys.readouterr()
|
|
111
|
-
assert captured.out == "PR must be in draft state\n"
|
|
112
|
-
assert ":0:" not in captured.out
|