claude-dev-env 4.1.0 → 4.3.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/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
- package/.agents/skills/everything-search/SKILL.md +18 -3
- package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
- package/.agents/skills/issue-tracker/SKILL.md +6 -0
- package/.agents/skills/pull-request/SKILL.md +218 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.mjs +9 -1
- package/bin/install.prune.test.mjs +149 -0
- package/bin/install.test.mjs +24 -7
- package/hooks/blocking/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
- package/hooks/gh_post_body_texts.py +195 -0
- package/hooks/hooks.json +0 -15
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -16
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -20
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- package/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +31 -20
- package/rules/filesystem-search.md +5 -5
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/tests/test_adapter_configuration.py +54 -0
- package/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/es_exe_path_rewriter.py +0 -159
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- package/hooks/test__gh_pr_author_swap_utils.py +0 -333
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Tests for GitHub post body extraction constants."""
|
|
2
|
+
|
|
3
|
+
from hooks_constants import gh_post_body_texts_constants as constants
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_post_actions_cover_pull_requests_and_issues() -> None:
|
|
7
|
+
assert constants.ALL_GH_POST_SUBCOMMANDS == {
|
|
8
|
+
"issue": frozenset({"comment", "create", "edit"}),
|
|
9
|
+
"pr": frozenset({"comment", "create", "edit", "review"}),
|
|
10
|
+
}
|
|
11
|
+
assert constants.ALL_MCP_BODY_PARAM_NAMES == ("body", "comment")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parser_constants_are_stable() -> None:
|
|
15
|
+
assert constants.BODY_FILE_ENCODING == "utf-8"
|
|
16
|
+
assert constants.BODY_FLAG_WITH_VALUE_STEP == 2
|
|
17
|
+
assert constants.GH_COMMAND_NAME == "gh"
|
|
18
|
+
assert constants.MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT == 2
|
|
19
|
+
assert constants.TOKEN_JOIN_SEPARATOR == " "
|
|
@@ -101,13 +101,13 @@ try:
|
|
|
101
101
|
for each_bootstrap_directory in (_hooks_root_directory, _blocking_directory):
|
|
102
102
|
if each_bootstrap_directory not in sys.path:
|
|
103
103
|
sys.path.insert(0, each_bootstrap_directory)
|
|
104
|
-
from hooks_constants.gh_pr_author_swap_constants import BASH_TOOL_NAME
|
|
105
104
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
106
105
|
from hooks_constants.test_failure_recorder_constants import (
|
|
107
106
|
ALL_CHAINING_OPERATOR_SUBSTRINGS,
|
|
108
107
|
ALL_PYTEST_MENTION_SUBSTRINGS,
|
|
109
108
|
ALL_PYTEST_PROGRAM_BASENAMES,
|
|
110
109
|
ALL_REDIRECTION_TOKENS,
|
|
110
|
+
BASH_TOOL_NAME,
|
|
111
111
|
EXIT_CODE_ERROR_PREFIX,
|
|
112
112
|
MODULE_RUN_FLAG,
|
|
113
113
|
NODE_ID_FILE_PATH_SEPARATOR,
|
package/hooks/pyproject.toml
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for durable GitHub post body extraction."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from gh_post_body_texts import (
|
|
8
|
+
extract_gh_post_body_texts_for_privacy_gate,
|
|
9
|
+
extract_mcp_body_texts,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
("command", "expected_text"),
|
|
15
|
+
[
|
|
16
|
+
('gh pr create --body "pull request text"', "pull request text"),
|
|
17
|
+
('gh pr comment 12 --body="comment text"', "comment text"),
|
|
18
|
+
('gh issue edit 7 -b "issue text"', "issue text"),
|
|
19
|
+
('GH_HOST=github.example gh pr review 8 --body "review text"', "review text"),
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
def test_inline_post_bodies_are_extracted(command: str, expected_text: str) -> None:
|
|
23
|
+
all_body_texts, deny_reason = extract_gh_post_body_texts_for_privacy_gate(command)
|
|
24
|
+
assert all_body_texts == [expected_text]
|
|
25
|
+
assert deny_reason is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_relative_body_file_uses_supplied_working_directory(tmp_path: Path) -> None:
|
|
29
|
+
body_file = tmp_path / "body.md"
|
|
30
|
+
body_file.write_text("body file text", encoding="utf-8")
|
|
31
|
+
all_body_texts, deny_reason = extract_gh_post_body_texts_for_privacy_gate(
|
|
32
|
+
"gh pr comment 12 --body-file body.md",
|
|
33
|
+
str(tmp_path),
|
|
34
|
+
)
|
|
35
|
+
assert all_body_texts == ["body file text"]
|
|
36
|
+
assert deny_reason is None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize("body_file_text", ["missing.md", "$BODY_FILE", "-"])
|
|
40
|
+
def test_unreadable_body_file_fails_closed(body_file_text: str) -> None:
|
|
41
|
+
all_body_texts, deny_reason = extract_gh_post_body_texts_for_privacy_gate(
|
|
42
|
+
f"gh pr comment 12 --body-file {body_file_text}"
|
|
43
|
+
)
|
|
44
|
+
assert all_body_texts == []
|
|
45
|
+
assert deny_reason is not None
|
|
46
|
+
assert "could not be read" in deny_reason
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_invalid_utf8_body_file_fails_closed(tmp_path: Path) -> None:
|
|
50
|
+
body_file = tmp_path / "body.md"
|
|
51
|
+
body_file.write_bytes(b"\xff")
|
|
52
|
+
all_body_texts, deny_reason = extract_gh_post_body_texts_for_privacy_gate(
|
|
53
|
+
f'gh issue create --body-file "{body_file}"'
|
|
54
|
+
)
|
|
55
|
+
assert all_body_texts == []
|
|
56
|
+
assert deny_reason is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize(
|
|
60
|
+
"command",
|
|
61
|
+
[
|
|
62
|
+
"gh pr view 12",
|
|
63
|
+
'echo "gh pr comment 12 --body text"',
|
|
64
|
+
"git status",
|
|
65
|
+
"",
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
def test_non_post_commands_have_no_body(command: str) -> None:
|
|
69
|
+
assert extract_gh_post_body_texts_for_privacy_gate(command) == ([], None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_mcp_body_and_comment_values_are_extracted() -> None:
|
|
73
|
+
assert extract_mcp_body_texts(
|
|
74
|
+
{"body": "body text", "comment": "comment text", "title": "ignored"}
|
|
75
|
+
) == ["body text", "comment text"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_mcp_non_string_and_empty_values_are_ignored() -> None:
|
|
79
|
+
assert extract_mcp_body_texts({"body": "", "comment": 7}) == []
|
package/package.json
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GitHub post input rules
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## When this applies
|
|
4
|
+
|
|
5
|
+
Use this rule for a GitHub issue, pull request, comment, or review created
|
|
6
|
+
through `gh` or a GitHub MCP post tool.
|
|
4
7
|
|
|
5
8
|
## Rule
|
|
6
9
|
|
|
7
|
-
A post
|
|
10
|
+
A post remains after the job ends. Job scratch directories, worktrees, and
|
|
11
|
+
system temp folders do not. Do not put a path from one of those directories in
|
|
12
|
+
a post.
|
|
8
13
|
|
|
9
|
-
Handle
|
|
14
|
+
Handle text and binary content differently:
|
|
10
15
|
|
|
11
|
-
- **Text data**
|
|
12
|
-
|
|
16
|
+
- **Text data** such as logs, tables, diffs, and stack traces belongs inline in
|
|
17
|
+
the post body. Do not link a scratch file that holds text data.
|
|
18
|
+
- **Binary artifacts** such as images, screenshots, and archives belong in the
|
|
19
|
+
repository's durable `artifacts` release. Use the helper:
|
|
13
20
|
|
|
14
21
|
```
|
|
15
22
|
python3 ~/.claude/scripts/gh_artifact_upload.py <file-path> <owner/repo>
|
|
16
23
|
```
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
The helper creates the `artifacts` prerelease when needed, uploads the file
|
|
26
|
+
under a `YYYYMMDD_HHMMSS_<name>` asset name, and prints a permanent download
|
|
27
|
+
URL. Put that URL in the post.
|
|
19
28
|
|
|
20
29
|
## Volatile paths that must not appear in a post body
|
|
21
30
|
|
|
22
|
-
- A job scratch directory
|
|
23
|
-
- A worktree
|
|
24
|
-
- A system temp location
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Both slash directions count.
|
|
28
|
-
|
|
29
|
-
The worktree and job-scratch entries count as a path when either:
|
|
31
|
+
- A job scratch directory: `.claude-profile-a/jobs/`
|
|
32
|
+
- A worktree: `.claude/worktrees/`
|
|
33
|
+
- A system temp location: `AppData\\Local\\Temp`, `%TEMP%`, `$env:TEMP`, or
|
|
34
|
+
`/tmp/`
|
|
35
|
+
- The job scratch environment variable: `$CLAUDE_JOB_DIR`
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
Both slash directions count. The path rule applies when a slash or backslash
|
|
38
|
+
precedes a marker, or when a path segment follows it. A standalone directory
|
|
39
|
+
name does not form a path.
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
## Validation
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
Run `.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py` before the
|
|
44
|
+
server write. Pass the matching action and body file. Use `pr-create`,
|
|
45
|
+
`pr-edit`, `pr-comment`, `pr-review`, `issue-create`, `issue-edit`,
|
|
46
|
+
`issue-comment`, or `github-mcp-post`.
|
|
37
47
|
|
|
38
|
-
The
|
|
48
|
+
The linter reads the body file and reports a volatile local path without
|
|
49
|
+
printing the body. Fix the body and rerun the linter before posting.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Filesystem Search
|
|
2
2
|
|
|
3
|
-
**When this applies:** Any search for files by name, path, extension, size, or date
|
|
3
|
+
**When this applies:** Any search for files by name, path, extension, size, or date. That includes `es.exe`, a shell `find`, a recursive `Get-ChildItem` / `gci` / `dir` / `ls -R`, or the harness Grep and Glob tools.
|
|
4
4
|
|
|
5
5
|
## The scope invariant
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ Three tools are equally sanctioned; pick by what you know:
|
|
|
14
14
|
|
|
15
15
|
| You know | Use |
|
|
16
16
|
|---|---|
|
|
17
|
-
| The exact path | `Read
|
|
17
|
+
| The exact path | `Read`. Do not search. |
|
|
18
18
|
| A name, extension, or date, on Windows | `es.exe` with a path scope |
|
|
19
19
|
| A name or path pattern | The harness `Glob` tool |
|
|
20
20
|
| Text inside files | The harness `Grep` tool |
|
|
@@ -39,13 +39,13 @@ When `es.exe` fails or returns nothing, fall back to `Glob` or `Grep` without pa
|
|
|
39
39
|
| Git Bash drive root | `find /c -name '*.py'` |
|
|
40
40
|
| Windows drive root | `find C:\ -name foo` / `Get-ChildItem C:\ -Recurse` |
|
|
41
41
|
| Bare home | `find ~ -name README.md` / `find $HOME -type f` |
|
|
42
|
-
| Network share root | `find //server/share -name x
|
|
42
|
+
| Network share root | `find //server/share -name x`. A path under the share (`//server/share/project/src`) is allowed |
|
|
43
43
|
|
|
44
44
|
## Shell batching
|
|
45
45
|
|
|
46
46
|
Issue one shell search at a time when the walk is large. Parallel full-tree searches contend for the shell and can lock the host. Harness `Grep` and `Glob` calls carry no such cost and run in parallel freely.
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## Search controls
|
|
49
49
|
|
|
50
50
|
- `unscoped_search_blocker` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a walk from an unscoped root and returns the scoped alternative.
|
|
51
|
-
-
|
|
51
|
+
- For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
|
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
# gh CLI
|
|
1
|
+
# gh CLI conventions
|
|
2
2
|
|
|
3
|
-
Two
|
|
3
|
+
Two `gh` call shapes need explicit handling.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Put body content in a file
|
|
6
6
|
|
|
7
|
-
Every `gh` command
|
|
7
|
+
Every `gh` command that carries markdown body content uses
|
|
8
|
+
`--body-file <path>`. This applies to `gh pr create`, `gh pr edit`,
|
|
9
|
+
`gh pr comment`, `gh pr review`, `gh issue create`, `gh issue edit`, and
|
|
10
|
+
`gh issue comment`. Never pass a `--body` or `-b` string. Write the file as
|
|
11
|
+
BOM-free UTF-8:
|
|
8
12
|
|
|
9
13
|
```powershell
|
|
10
14
|
[IO.File]::WriteAllText($bodyPath, $body, [Text.UTF8Encoding]::new($false))
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
MCP GitHub tools take `body` as a structured parameter
|
|
17
|
+
MCP GitHub tools take `body` as a structured parameter. Write the same body to
|
|
18
|
+
a UTF-8 file and run the shared linter before sending that parameter.
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
For pull requests, use
|
|
21
|
+
`.agents/skills/pull-request/scripts/pull_request.py`. It passes
|
|
22
|
+
`--body-file` to `gh` after the action-aware linter succeeds. For issues and
|
|
23
|
+
GitHub MCP posts, run the linter directly with `issue-create`, `issue-edit`,
|
|
24
|
+
`issue-comment`, or `github-mcp-post` as the action.
|
|
16
25
|
|
|
17
26
|
## Paginated reads slurp before they filter
|
|
18
27
|
|
|
19
|
-
Every `gh api` read of a paginated GitHub list endpoint
|
|
28
|
+
Every `gh api` read of a paginated GitHub list endpoint uses
|
|
29
|
+
`--paginate --slurp` and pipes the result to external `jq`. This applies to PR
|
|
30
|
+
reviews, comments, and files, plus issue comments, pulls, and issues. The
|
|
31
|
+
built-in `--jq` runs once per page and can produce a wrong cross-page result.
|
|
20
32
|
|
|
21
|
-
Single-object endpoints
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- [`destructive-commands.md`](destructive-commands.md) — why a body describing `rm -rf` must travel by file path.
|
|
26
|
-
- [`durable-post-artifacts.md`](durable-post-artifacts.md) — what a post body may reference.
|
|
33
|
+
Single-object endpoints such as `pulls/<n>` and `issues/<n>` do not need
|
|
34
|
+
pagination and may use `--jq` directly. For a newest-first walk, sort the
|
|
35
|
+
slurped array and take the last element. For one page, cap the request with a
|
|
36
|
+
`per_page` query parameter.
|
package/rules/git-workflow.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Git
|
|
1
|
+
# Git workflow
|
|
2
2
|
|
|
3
|
-
User-level rule: applies to **every** git repo that uses GitHub with `gh
|
|
3
|
+
User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small or non-primary repos follow the same rule unless the user says otherwise in the session.
|
|
4
4
|
|
|
5
|
-
## Workflow
|
|
5
|
+
## Workflow decision tree
|
|
6
6
|
|
|
7
7
|
**When to use stacked PRs:** Feature B depends on Feature A's implementation
|
|
8
8
|
|
|
@@ -13,19 +13,32 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh` (no ex
|
|
|
13
13
|
2. Get reviewed and MERGE infrastructure first
|
|
14
14
|
3. Launch parallel feature PRs that use merged infrastructure
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Pull request submission rules
|
|
17
17
|
|
|
18
18
|
**ALWAYS create PRs as DRAFT:** Use `gh pr create --draft` for ALL PRs
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Use the `pr-description-writer` agent before creating a pull request or
|
|
21
|
+
rewriting its full description. Publish its title and body file through
|
|
22
|
+
`.agents/skills/pull-request/scripts/pull_request.py`. The
|
|
23
|
+
`pr-title-description` skill may review the writer's output but cannot replace
|
|
24
|
+
the required writer.
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
Run `_shared/pr-loop/scripts/durable_post_lint.py` before any pull request,
|
|
27
|
+
issue, or GitHub MCP post. The linter checks the action-specific title, body,
|
|
28
|
+
and volatile-path rules before credential lookup or network access.
|
|
29
|
+
|
|
30
|
+
Use `.agents/skills/pull-request/scripts/recover_legacy_author.py
|
|
31
|
+
<exact-state-file> --confirm-inactive` only for one explicitly selected legacy
|
|
32
|
+
author record. Do not infer a record from age alone. Keep every other record
|
|
33
|
+
untouched.
|
|
34
|
+
|
|
35
|
+
## Git golden rules
|
|
23
36
|
|
|
24
37
|
1. **DRAFT BEFORE PUSH**: When pushing ANYTHING to a PR, it MUST be in draft state first
|
|
25
38
|
- Before push: `gh pr ready --undo`
|
|
26
39
|
- After review approved: `gh pr ready`
|
|
27
40
|
|
|
28
|
-
## Never
|
|
41
|
+
## Never commit working documents or images
|
|
29
42
|
|
|
30
43
|
**NEVER commit these files to the repo:**
|
|
31
44
|
|
|
@@ -49,4 +62,4 @@ An image a PR needs as visual evidence is not an exception to that row. Upload i
|
|
|
49
62
|
|
|
50
63
|
Repair only reported findings, then re-verify after every repair.
|
|
51
64
|
|
|
52
|
-
Every `gh` post in this workflow uses `--body-file` per `gh-cli-conventions.md` and keeps volatile scratch paths out per `durable-post-artifacts.md
|
|
65
|
+
Every `gh` post in this workflow uses `--body-file` per `gh-cli-conventions.md` and keeps volatile scratch paths out per `durable-post-artifacts.md`. Stage session edits per `re-stage-before-commit.md` before each commit.
|
|
@@ -23,30 +23,69 @@ def hook_configuration_diagnostics(
|
|
|
23
23
|
adapter_support._document_path(repository_root, document)
|
|
24
24
|
if document.path.name != "hooks.json":
|
|
25
25
|
return ()
|
|
26
|
-
all_messages = _hook_configuration_messages(document.text)
|
|
26
|
+
all_messages = _hook_configuration_messages(document.text, document.prior_text)
|
|
27
27
|
return adapter_support._diagnostics_for_messages(
|
|
28
28
|
document, "hook-configuration", all_messages
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def
|
|
32
|
+
def _parsed_configuration(
|
|
33
|
+
configuration_text: str,
|
|
34
|
+
invalid_json_message: str,
|
|
35
|
+
invalid_object_message: str,
|
|
36
|
+
) -> dict[object, object]:
|
|
33
37
|
try:
|
|
34
38
|
parsed_configuration = json.loads(configuration_text)
|
|
35
|
-
except json.JSONDecodeError:
|
|
36
|
-
|
|
39
|
+
except json.JSONDecodeError as error:
|
|
40
|
+
raise ValueError(invalid_json_message) from error
|
|
37
41
|
if not isinstance(parsed_configuration, dict):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
raise TypeError(invalid_object_message)
|
|
43
|
+
return parsed_configuration
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _action_boundary_strings(
|
|
47
|
+
all_configuration_entries: dict[object, object],
|
|
48
|
+
) -> frozenset[str]:
|
|
49
|
+
return frozenset(
|
|
50
|
+
each_string
|
|
51
|
+
for each_string in _registered_hook_strings(all_configuration_entries)
|
|
52
|
+
if _contains_action_boundary_marker(each_string)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _hook_configuration_messages(
|
|
57
|
+
configuration_text: str,
|
|
58
|
+
prior_configuration_text: str | None,
|
|
59
|
+
) -> tuple[str, ...]:
|
|
60
|
+
try:
|
|
61
|
+
current_configuration = _parsed_configuration(
|
|
62
|
+
configuration_text,
|
|
63
|
+
"Hook configuration is not valid JSON",
|
|
64
|
+
"Hook configuration must be an object",
|
|
65
|
+
)
|
|
66
|
+
all_current_action_boundary_strings = _action_boundary_strings(
|
|
67
|
+
current_configuration
|
|
68
|
+
)
|
|
69
|
+
if prior_configuration_text is None:
|
|
70
|
+
all_prior_action_boundary_strings: frozenset[str] = frozenset()
|
|
71
|
+
else:
|
|
72
|
+
prior_configuration = _parsed_configuration(
|
|
73
|
+
prior_configuration_text,
|
|
74
|
+
"Prior hook configuration is not valid JSON",
|
|
75
|
+
"Prior hook configuration must be an object",
|
|
76
|
+
)
|
|
77
|
+
all_prior_action_boundary_strings = _action_boundary_strings(
|
|
78
|
+
prior_configuration
|
|
79
|
+
)
|
|
80
|
+
except (TypeError, ValueError) as error:
|
|
81
|
+
return (str(error),)
|
|
82
|
+
if all_current_action_boundary_strings - all_prior_action_boundary_strings:
|
|
44
83
|
return ("Hook configuration registers a deny, block, or ask policy path",)
|
|
45
84
|
return ()
|
|
46
85
|
|
|
47
86
|
|
|
48
87
|
def _registered_hook_strings(
|
|
49
|
-
all_configuration_entries: dict[object, object]
|
|
88
|
+
all_configuration_entries: dict[object, object],
|
|
50
89
|
) -> tuple[str, ...]:
|
|
51
90
|
all_registered_strings: list[str] = []
|
|
52
91
|
for each_key, each_entry in all_configuration_entries.items():
|
|
@@ -62,7 +101,9 @@ def _registered_strings_for_entry(
|
|
|
62
101
|
all_registered_strings: list[str] = []
|
|
63
102
|
if each_key in constants.ALL_HOOK_REGISTRATION_KEYS and isinstance(each_entry, str):
|
|
64
103
|
all_registered_strings.append(each_entry)
|
|
65
|
-
if each_key in constants.ALL_DECISION_REGISTRATION_KEYS and isinstance(
|
|
104
|
+
if each_key in constants.ALL_DECISION_REGISTRATION_KEYS and isinstance(
|
|
105
|
+
each_entry, str
|
|
106
|
+
):
|
|
66
107
|
all_registered_strings.append(each_entry)
|
|
67
108
|
all_registered_strings.extend(_registered_nested_strings(each_entry))
|
|
68
109
|
return tuple(all_registered_strings)
|
|
@@ -131,11 +131,6 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
|
|
|
131
131
|
"home-path",
|
|
132
132
|
"09f83c224a02c370d3d63de5c330e1a830fc92866330fc7963b5b5fd20ffcb91",
|
|
133
133
|
),
|
|
134
|
-
(
|
|
135
|
-
"packages/claude-dev-env/hooks/blocking/volatile_path_in_post_blocker.py",
|
|
136
|
-
"home-path",
|
|
137
|
-
"ed9203c8078e4c6b832600085f28f24656d679aa8dca82ecc2557afc3df25036",
|
|
138
|
-
),
|
|
139
134
|
(
|
|
140
135
|
"packages/claude-dev-env/hooks/hooks_constants/hardcoded_user_path_constants.py",
|
|
141
136
|
"home-path",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Incremental migration tests for the hook-configuration lint rule."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from policy_lint import adapters
|
|
10
|
+
from policy_lint.model import Document
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _hook_document(all_commands: list[str], prior_text: str) -> Document:
|
|
14
|
+
current_text = json.dumps(
|
|
15
|
+
{
|
|
16
|
+
"hooks": {
|
|
17
|
+
"PreToolUse": [
|
|
18
|
+
{
|
|
19
|
+
"hooks": [
|
|
20
|
+
{"command": each_command} for each_command in all_commands
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
return replace(
|
|
28
|
+
Document.from_text("hooks/hooks.json", current_text),
|
|
29
|
+
prior_text=prior_text,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_incremental_blocker_removal_is_clean(tmp_path: Path) -> None:
|
|
34
|
+
prior_text = _hook_document(
|
|
35
|
+
["hooks/blocking/first.py", "hooks/blocking/second.py"],
|
|
36
|
+
'{"hooks": {}}',
|
|
37
|
+
).text
|
|
38
|
+
current_document = _hook_document(
|
|
39
|
+
["hooks/blocking/second.py"],
|
|
40
|
+
prior_text,
|
|
41
|
+
)
|
|
42
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_new_blocker_in_staged_change_is_rejected(tmp_path: Path) -> None:
|
|
46
|
+
current_document = _hook_document(
|
|
47
|
+
["hooks/blocking/new.py"],
|
|
48
|
+
'{"hooks": {}}',
|
|
49
|
+
)
|
|
50
|
+
all_diagnostics = adapters.hook_configuration_diagnostics(
|
|
51
|
+
current_document, tmp_path
|
|
52
|
+
)
|
|
53
|
+
assert len(all_diagnostics) == 1
|
|
54
|
+
assert all_diagnostics[0].rule_id == "hook-configuration"
|