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
|
@@ -13,13 +13,23 @@ Search files instantly on Windows using the Everything command-line interface (e
|
|
|
13
13
|
|
|
14
14
|
## Hard limits
|
|
15
15
|
|
|
16
|
-
Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds
|
|
16
|
+
Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds. Narrow the search to what you need.
|
|
17
17
|
|
|
18
18
|
When `es.exe` fails or returns nothing, self-heal first: fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents), and report the outage so the reader knows the index was unavailable. When self-healing also fails, ask the user through `AskUserQuestion` with a short analysis and next-step options.
|
|
19
19
|
|
|
20
20
|
## Registry tokens
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Run project-name searches through this command:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The command reads `~/.claude/project-paths.json`. An exact project name or `{project-name}` argument becomes one absolute path. Unknown names, absolute paths, flags, and spaces stay as they are. The command then starts `es.exe` without a shell.
|
|
29
|
+
|
|
30
|
+
Put the search scope first. The first argument names a project, path, file, extension, date, or size. An options-only request exits before `es.exe` starts. Use `-version` alone to read the installed Everything version.
|
|
31
|
+
|
|
32
|
+
Path and operator searches run when the registry file is missing. Malformed registry content, an empty search, a missing `es.exe`, or a process start failure returns a nonzero exit. After a successful start, the command returns the `es.exe` exit status.
|
|
23
33
|
|
|
24
34
|
## Instructions
|
|
25
35
|
|
|
@@ -131,7 +141,7 @@ Run separate searches for each extension type:
|
|
|
131
141
|
|
|
132
142
|
- Everything must be running (system tray) for es.exe to work
|
|
133
143
|
- Run one extension per search for cleaner results
|
|
134
|
-
- Use `dm:`
|
|
144
|
+
- Use `dm:` to filter recent files by the index timestamp
|
|
135
145
|
- Combine with path to narrow scope
|
|
136
146
|
- Results return instantly regardless of drive size
|
|
137
147
|
|
|
@@ -152,3 +162,8 @@ Everything indexes REAL paths only, not junctions or mapped drives.
|
|
|
152
162
|
**Common drive mappings:**
|
|
153
163
|
- `Z:\Projects\` -> `Y:\Work\Projects\`
|
|
154
164
|
- When in Z:\, translate to Y:\ equivalent for Everything searches
|
|
165
|
+
|
|
166
|
+
## Files
|
|
167
|
+
|
|
168
|
+
- `scripts/everything_search.py` runs one Everything search without a shell.
|
|
169
|
+
- `scripts/test_everything_search.py` verifies registry expansion and command failures.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
9
|
+
from typing import TextIO
|
|
10
|
+
|
|
11
|
+
from everything_search_command_constants.config.constants import (
|
|
12
|
+
CLAUDE_DIRECTORY_NAME,
|
|
13
|
+
EXECUTABLE_NAME,
|
|
14
|
+
EXECUTION_ERROR_EXIT_CODE,
|
|
15
|
+
INFORMATIONAL_ARGUMENT,
|
|
16
|
+
INVALID_INPUT_EXIT_CODE,
|
|
17
|
+
PROJECT_PATHS_FILE_NAME,
|
|
18
|
+
REGISTRY_META_KEY,
|
|
19
|
+
SEARCH_SCOPE_REQUIRED_MESSAGE,
|
|
20
|
+
UTF8_ENCODING,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegistryRunFatal(ValueError):
|
|
25
|
+
"""Stop the run when the project-path registry is unreadable or invalid."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_registry(registry_path: Path) -> dict[str, str]:
|
|
29
|
+
"""Read project names and absolute paths from a registry file.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
registry_path: Registry file to read.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The registered paths keyed by project name.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
RegistryRunFatal: The file is unreadable or its content is invalid.
|
|
39
|
+
"""
|
|
40
|
+
if not registry_path.is_file():
|
|
41
|
+
return {}
|
|
42
|
+
try:
|
|
43
|
+
all_registry_entries = json.loads(
|
|
44
|
+
registry_path.read_text(encoding=UTF8_ENCODING)
|
|
45
|
+
)
|
|
46
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
47
|
+
raise RegistryRunFatal(f"Malformed registry: {error}") from error
|
|
48
|
+
return _validated_registry_entries(all_registry_entries)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _validated_registry_entries(all_registry_entries: object) -> dict[str, str]:
|
|
52
|
+
if not isinstance(all_registry_entries, dict):
|
|
53
|
+
raise RegistryRunFatal("Invalid registry: expected an object")
|
|
54
|
+
all_registered_paths_by_name: dict[str, str] = {}
|
|
55
|
+
for each_registry_name, each_registered_path in all_registry_entries.items():
|
|
56
|
+
if each_registry_name == REGISTRY_META_KEY:
|
|
57
|
+
continue
|
|
58
|
+
if not isinstance(each_registry_name, str) or not isinstance(
|
|
59
|
+
each_registered_path, str
|
|
60
|
+
):
|
|
61
|
+
raise RegistryRunFatal("Invalid registry: names and paths must be strings")
|
|
62
|
+
if not _is_absolute_path(each_registered_path):
|
|
63
|
+
raise RegistryRunFatal("Invalid registry: paths must be absolute")
|
|
64
|
+
all_registered_paths_by_name[each_registry_name] = each_registered_path
|
|
65
|
+
return all_registered_paths_by_name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_absolute_path(search_argument: str) -> bool:
|
|
69
|
+
try:
|
|
70
|
+
return (
|
|
71
|
+
PureWindowsPath(search_argument).is_absolute()
|
|
72
|
+
or PurePosixPath(search_argument).is_absolute()
|
|
73
|
+
)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _registry_name(search_argument: str) -> str:
|
|
79
|
+
if search_argument.startswith("{") and search_argument.endswith("}"):
|
|
80
|
+
return search_argument[1:-1]
|
|
81
|
+
return search_argument
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def expand_search_arguments(
|
|
85
|
+
all_search_arguments: Sequence[str],
|
|
86
|
+
all_registered_paths_by_name: Mapping[str, str],
|
|
87
|
+
) -> list[str]:
|
|
88
|
+
"""Return the registered path for an exact project name or `{project-name}` token.
|
|
89
|
+
|
|
90
|
+
Unknown names, absolute paths, flags, and spaces stay as their own arguments.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
all_search_arguments: Everything search arguments.
|
|
94
|
+
all_registered_paths_by_name: Absolute paths keyed by project name.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Arguments with exact project names as their registered paths.
|
|
98
|
+
"""
|
|
99
|
+
all_expanded_arguments: list[str] = []
|
|
100
|
+
for each_search_argument in all_search_arguments:
|
|
101
|
+
registry_name = _registry_name(each_search_argument)
|
|
102
|
+
if (
|
|
103
|
+
not _is_absolute_path(each_search_argument)
|
|
104
|
+
and registry_name in all_registered_paths_by_name
|
|
105
|
+
):
|
|
106
|
+
all_expanded_arguments.append(all_registered_paths_by_name[registry_name])
|
|
107
|
+
else:
|
|
108
|
+
all_expanded_arguments.append(each_search_argument)
|
|
109
|
+
return all_expanded_arguments
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _run_search(
|
|
113
|
+
executable_path: str,
|
|
114
|
+
all_search_arguments: Sequence[str],
|
|
115
|
+
search_stdout: TextIO,
|
|
116
|
+
search_stderr: TextIO,
|
|
117
|
+
) -> int:
|
|
118
|
+
try:
|
|
119
|
+
search_run = subprocess.run(
|
|
120
|
+
[executable_path, *all_search_arguments],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
check=False,
|
|
123
|
+
encoding=UTF8_ENCODING,
|
|
124
|
+
shell=False,
|
|
125
|
+
text=True,
|
|
126
|
+
)
|
|
127
|
+
except OSError as error:
|
|
128
|
+
search_stderr.write(f"{error}\n")
|
|
129
|
+
return EXECUTION_ERROR_EXIT_CODE
|
|
130
|
+
search_stdout.write(search_run.stdout)
|
|
131
|
+
search_stderr.write(search_run.stderr)
|
|
132
|
+
return search_run.returncode
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _run_registered_search(
|
|
136
|
+
all_search_arguments: Sequence[str],
|
|
137
|
+
registry_path: Path,
|
|
138
|
+
executable_path: str | None,
|
|
139
|
+
search_stdout: TextIO,
|
|
140
|
+
search_stderr: TextIO,
|
|
141
|
+
) -> int:
|
|
142
|
+
try:
|
|
143
|
+
all_registered_paths_by_name = load_registry(registry_path)
|
|
144
|
+
except RegistryRunFatal as error:
|
|
145
|
+
search_stderr.write(f"{error}\n")
|
|
146
|
+
return INVALID_INPUT_EXIT_CODE
|
|
147
|
+
selected_executable_path = executable_path or shutil.which(EXECUTABLE_NAME)
|
|
148
|
+
if selected_executable_path is None:
|
|
149
|
+
search_stderr.write(f"{EXECUTABLE_NAME} was not found.\n")
|
|
150
|
+
return EXECUTION_ERROR_EXIT_CODE
|
|
151
|
+
all_expanded_arguments = expand_search_arguments(
|
|
152
|
+
all_search_arguments, all_registered_paths_by_name
|
|
153
|
+
)
|
|
154
|
+
return _run_search(
|
|
155
|
+
selected_executable_path,
|
|
156
|
+
all_expanded_arguments,
|
|
157
|
+
search_stdout,
|
|
158
|
+
search_stderr,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _has_search_scope(all_search_arguments: Sequence[str]) -> bool:
|
|
163
|
+
first_argument = all_search_arguments[0]
|
|
164
|
+
return bool(first_argument.strip()) and not first_argument.startswith("-")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_informational_operation(all_search_arguments: Sequence[str]) -> bool:
|
|
168
|
+
return (
|
|
169
|
+
len(all_search_arguments) == 1
|
|
170
|
+
and all_search_arguments[0].casefold() == INFORMATIONAL_ARGUMENT
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main(all_search_arguments: Sequence[str]) -> int:
|
|
175
|
+
"""Expand project names and run one Everything search.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
all_search_arguments: Arguments passed to Everything.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The Everything process exit status or a command error status.
|
|
182
|
+
"""
|
|
183
|
+
if not all_search_arguments:
|
|
184
|
+
sys.stderr.write("At least one search argument is required.\n")
|
|
185
|
+
return INVALID_INPUT_EXIT_CODE
|
|
186
|
+
if not _has_search_scope(all_search_arguments) and not _is_informational_operation(
|
|
187
|
+
all_search_arguments
|
|
188
|
+
):
|
|
189
|
+
sys.stderr.write(SEARCH_SCOPE_REQUIRED_MESSAGE)
|
|
190
|
+
return INVALID_INPUT_EXIT_CODE
|
|
191
|
+
selected_registry_path = (
|
|
192
|
+
Path.home() / CLAUDE_DIRECTORY_NAME / PROJECT_PATHS_FILE_NAME
|
|
193
|
+
)
|
|
194
|
+
return _run_registered_search(
|
|
195
|
+
all_search_arguments,
|
|
196
|
+
selected_registry_path,
|
|
197
|
+
None,
|
|
198
|
+
sys.stdout,
|
|
199
|
+
sys.stderr,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
raise SystemExit(main(sys.argv[1:]))
|
package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Constants for the Everything search command."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration for the Everything search command."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Configuration constants for the Everything search command."""
|
|
2
|
+
|
|
3
|
+
CLAUDE_DIRECTORY_NAME = ".claude"
|
|
4
|
+
EXECUTABLE_NAME = "es.exe"
|
|
5
|
+
EXECUTION_ERROR_EXIT_CODE = 1
|
|
6
|
+
INFORMATIONAL_ARGUMENT = "-version"
|
|
7
|
+
INVALID_INPUT_EXIT_CODE = 2
|
|
8
|
+
PROJECT_PATHS_FILE_NAME = "project-paths.json"
|
|
9
|
+
REGISTRY_META_KEY = "_meta"
|
|
10
|
+
SEARCH_SCOPE_REQUIRED_MESSAGE = "The first search argument must define a path, project, name, extension, date, or size scope.\n"
|
|
11
|
+
UTF8_ENCODING = "utf-8"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
13
|
+
|
|
14
|
+
search = importlib.import_module("everything_search")
|
|
15
|
+
|
|
16
|
+
KNOWN_PATH = "Y:\\Projects\\my-repo"
|
|
17
|
+
REGISTRY = {"my-repo": KNOWN_PATH}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_expand_search_arguments_rewrites_bare_registry_token() -> None:
|
|
21
|
+
assert search.expand_search_arguments(["my-repo", "config.py"], REGISTRY) == [
|
|
22
|
+
KNOWN_PATH,
|
|
23
|
+
"config.py",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_expand_search_arguments_rewrites_exact_placeholder_token() -> None:
|
|
28
|
+
assert search.expand_search_arguments(["{my-repo}", "config.py"], REGISTRY) == [
|
|
29
|
+
KNOWN_PATH,
|
|
30
|
+
"config.py",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_expand_search_arguments_preserves_unknown_tokens_and_spaces() -> None:
|
|
35
|
+
all_arguments = ["unknown-name", "foo bar", "config.py"]
|
|
36
|
+
assert search.expand_search_arguments(all_arguments, REGISTRY) == all_arguments
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
"absolute_path",
|
|
41
|
+
[
|
|
42
|
+
"C:/Users/example/file.py",
|
|
43
|
+
"\\\\server\\share\\folder",
|
|
44
|
+
"/mnt/c/Projects/repo",
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_expand_search_arguments_preserves_absolute_paths(absolute_path: str) -> None:
|
|
48
|
+
assert search.expand_search_arguments([absolute_path], REGISTRY) == [absolute_path]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_expand_search_arguments_preserves_embedded_flag_tokens() -> None:
|
|
52
|
+
all_arguments = ["--flag={my-repo}", "--regex=^{my-repo}$"]
|
|
53
|
+
assert search.expand_search_arguments(all_arguments, REGISTRY) == all_arguments
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_load_registry_returns_empty_mapping_for_missing_file(tmp_path: Path) -> None:
|
|
57
|
+
assert search.load_registry(tmp_path / "missing.json") == {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize(
|
|
61
|
+
"registry_bytes",
|
|
62
|
+
[
|
|
63
|
+
b"\xff",
|
|
64
|
+
b"{not valid",
|
|
65
|
+
json.dumps({"my-repo": 42}).encode(),
|
|
66
|
+
json.dumps({"my-repo": ""}).encode(),
|
|
67
|
+
json.dumps({"my-repo": "relative/path"}).encode(),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
def test_load_registry_reports_invalid_input(
|
|
71
|
+
tmp_path: Path, registry_bytes: bytes
|
|
72
|
+
) -> None:
|
|
73
|
+
registry_path = tmp_path / "project-paths.json"
|
|
74
|
+
registry_path.write_bytes(registry_bytes)
|
|
75
|
+
with pytest.raises(search.RegistryRunFatal, match="(Malformed|Invalid)"):
|
|
76
|
+
search.load_registry(registry_path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_main_runs_direct_search_without_registry_or_shell() -> None:
|
|
80
|
+
completed_process = search.subprocess.CompletedProcess(
|
|
81
|
+
args=["es.exe", "foo bar"],
|
|
82
|
+
returncode=0,
|
|
83
|
+
stdout="found path\n",
|
|
84
|
+
stderr="",
|
|
85
|
+
)
|
|
86
|
+
captured_stdout = io.StringIO()
|
|
87
|
+
with (
|
|
88
|
+
patch.object(search, "load_registry", return_value={}),
|
|
89
|
+
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
90
|
+
patch.object(
|
|
91
|
+
search.subprocess, "run", return_value=completed_process
|
|
92
|
+
) as run_process,
|
|
93
|
+
patch.object(search.sys, "stdout", captured_stdout),
|
|
94
|
+
):
|
|
95
|
+
exit_code = search.main(["foo bar"])
|
|
96
|
+
assert exit_code == 0
|
|
97
|
+
assert captured_stdout.getvalue() == "found path\n"
|
|
98
|
+
run_process.assert_called_once_with(
|
|
99
|
+
["es.exe", "foo bar"],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
check=False,
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
shell=False,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_main_passes_through_child_exit_status() -> None:
|
|
109
|
+
completed_process = search.subprocess.CompletedProcess(
|
|
110
|
+
args=["es.exe", "config.py"], returncode=7, stdout="", stderr="failed\n"
|
|
111
|
+
)
|
|
112
|
+
captured_stderr = io.StringIO()
|
|
113
|
+
with (
|
|
114
|
+
patch.object(search, "load_registry", return_value={}),
|
|
115
|
+
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
116
|
+
patch.object(search.subprocess, "run", return_value=completed_process),
|
|
117
|
+
patch.object(search.sys, "stderr", captured_stderr),
|
|
118
|
+
):
|
|
119
|
+
exit_code = search.main(["config.py"])
|
|
120
|
+
assert exit_code == 7
|
|
121
|
+
assert captured_stderr.getvalue() == "failed\n"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_main_reports_missing_executable() -> None:
|
|
125
|
+
captured_stderr = io.StringIO()
|
|
126
|
+
with (
|
|
127
|
+
patch.object(search, "load_registry", return_value={}),
|
|
128
|
+
patch.object(search.shutil, "which", return_value=None),
|
|
129
|
+
patch.object(search.sys, "stderr", captured_stderr),
|
|
130
|
+
):
|
|
131
|
+
exit_code = search.main(["config.py"])
|
|
132
|
+
assert exit_code != 0
|
|
133
|
+
assert "es.exe" in captured_stderr.getvalue()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_main_reports_empty_search_arguments() -> None:
|
|
137
|
+
captured_stderr = io.StringIO()
|
|
138
|
+
with patch.object(search.sys, "stderr", captured_stderr):
|
|
139
|
+
exit_code = search.main([])
|
|
140
|
+
assert exit_code == search.INVALID_INPUT_EXIT_CODE
|
|
141
|
+
assert "search argument" in captured_stderr.getvalue()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.parametrize("all_arguments", [["-n", "50"], [""], [" "]])
|
|
145
|
+
def test_main_rejects_unscoped_search_without_launching_child(
|
|
146
|
+
all_arguments: list[str],
|
|
147
|
+
) -> None:
|
|
148
|
+
captured_stderr = io.StringIO()
|
|
149
|
+
with (
|
|
150
|
+
patch.object(search.subprocess, "run") as run_process,
|
|
151
|
+
patch.object(search.sys, "stderr", captured_stderr),
|
|
152
|
+
):
|
|
153
|
+
exit_code = search.main(all_arguments)
|
|
154
|
+
assert exit_code == search.INVALID_INPUT_EXIT_CODE
|
|
155
|
+
assert "first search argument" in captured_stderr.getvalue().lower()
|
|
156
|
+
run_process.assert_not_called()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_main_allows_version_information_operation() -> None:
|
|
160
|
+
completed_process = search.subprocess.CompletedProcess(
|
|
161
|
+
args=["es.exe", "-version"], returncode=0, stdout="1.1.0.30\n", stderr=""
|
|
162
|
+
)
|
|
163
|
+
with (
|
|
164
|
+
patch.object(search, "load_registry", return_value={}),
|
|
165
|
+
patch.object(search.shutil, "which", return_value="es.exe"),
|
|
166
|
+
patch.object(
|
|
167
|
+
search.subprocess, "run", return_value=completed_process
|
|
168
|
+
) as run_process,
|
|
169
|
+
):
|
|
170
|
+
exit_code = search.main(["-version"])
|
|
171
|
+
assert exit_code == 0
|
|
172
|
+
run_process.assert_called_once()
|
|
@@ -60,6 +60,12 @@ Search open and closed issues on the target repo before create. Open twin -> upd
|
|
|
60
60
|
|
|
61
61
|
Prefer GitHub MCP. Fall back to `gh` on the same REST endpoints. Full map and `.id` rule: [reference/operation-matrix.md](reference/operation-matrix.md).
|
|
62
62
|
|
|
63
|
+
Before issue creation, a full issue body edit, or an issue comment, write the
|
|
64
|
+
body to a UTF-8 file and run
|
|
65
|
+
`../_shared/pr-loop/scripts/durable_post_lint.py`. Use `issue-create`,
|
|
66
|
+
`issue-edit`, or `issue-comment` as the action. Send the server write only
|
|
67
|
+
after exit code `0`.
|
|
68
|
+
|
|
63
69
|
## Handoff input
|
|
64
70
|
|
|
65
71
|
Optional issue-candidate JSON from orchestrator or closeout. Full schema and consume path: [reference/handoff-schema.md](reference/handoff-schema.md).
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pull-request
|
|
3
|
+
description: >-
|
|
4
|
+
Validate and publish GitHub pull request actions. Triggers: create PR, open
|
|
5
|
+
pull request, edit PR, update PR body, comment on PR, review PR, publish a
|
|
6
|
+
draft PR, scoped GitHub author.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pull request
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Principle](#principle)
|
|
14
|
+
- [When this applies](#when-this-applies)
|
|
15
|
+
- [Constraints](#constraints)
|
|
16
|
+
- [Dependencies](#dependencies)
|
|
17
|
+
- [Sub-skills](#sub-skills)
|
|
18
|
+
- [Task seeding](#task-seeding)
|
|
19
|
+
- [Process](#process)
|
|
20
|
+
- [Exit handling](#exit-handling)
|
|
21
|
+
- [Examples](#examples)
|
|
22
|
+
- [Gotchas](#gotchas)
|
|
23
|
+
- [File index](#file-index)
|
|
24
|
+
- [Folder map](#folder-map)
|
|
25
|
+
|
|
26
|
+
## Principle
|
|
27
|
+
|
|
28
|
+
Publish one GitHub pull request action from validated local files and a
|
|
29
|
+
process-local author value. Keep the parent environment unchanged.
|
|
30
|
+
|
|
31
|
+
## When this applies
|
|
32
|
+
|
|
33
|
+
Use this skill for one pull request action:
|
|
34
|
+
|
|
35
|
+
- Create a draft pull request.
|
|
36
|
+
- Edit a pull request title or body.
|
|
37
|
+
- Add a pull request comment.
|
|
38
|
+
- Submit a pull request review.
|
|
39
|
+
- Recover one selected legacy author record before the action.
|
|
40
|
+
|
|
41
|
+
Send issue create, edit, and comment work to `issue-tracker`. Send review
|
|
42
|
+
convergence to `pr-cleanup` or `autoconverge`. Send commits to
|
|
43
|
+
`source-command-commit`.
|
|
44
|
+
|
|
45
|
+
Require one repository and one action target. Stop before author lookup or
|
|
46
|
+
network work when the repository, pull request, action, author, or required
|
|
47
|
+
local file is missing or ambiguous.
|
|
48
|
+
|
|
49
|
+
Use these exact refusal responses when the request belongs to another skill:
|
|
50
|
+
|
|
51
|
+
- Issue action: `Use issue-tracker for this GitHub issue action.`
|
|
52
|
+
- Commit or push: `Use source-command-commit for this commit or push.`
|
|
53
|
+
- Missing target: `Provide one repository and one pull request action target.`
|
|
54
|
+
|
|
55
|
+
## Constraints
|
|
56
|
+
|
|
57
|
+
- Run local validation and privacy checks before account lookup or network work.
|
|
58
|
+
- Use a body file. Do not send inline body text through the shell.
|
|
59
|
+
- Keep `GITHUB_DEFAULT_ACCOUNT`, the parent environment, and global GitHub CLI
|
|
60
|
+
state unchanged.
|
|
61
|
+
- Run one create, edit, comment, review, or recovery action per command.
|
|
62
|
+
- Use global account switching only for one explicitly selected legacy record.
|
|
63
|
+
- Read back the remote state before reporting success.
|
|
64
|
+
|
|
65
|
+
## Dependencies
|
|
66
|
+
|
|
67
|
+
The commands use Python 3.11 or later and the Python standard library. GitHub
|
|
68
|
+
actions also require an authenticated `gh` executable. The linter and artifact
|
|
69
|
+
uploader ship in this package. Set `GITHUB_DEFAULT_ACCOUNT` to one exact GitHub
|
|
70
|
+
CLI login when the action needs a selected author. Leave it unset to use the
|
|
71
|
+
current process environment.
|
|
72
|
+
|
|
73
|
+
## Sub-skills
|
|
74
|
+
|
|
75
|
+
| Skill | When | Produces | If missing |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `pr-description-writer` | Before create or a full body rewrite | A reviewed title and body file | Stop the create or rewrite and report that authoring is required |
|
|
78
|
+
| `pr-title-description` | Optional title and body review | Review findings for the writer's output | Continue with the required writer output |
|
|
79
|
+
| `privacy-hygiene` | Before any durable GitHub post | A clean body and repository privacy sweep | Stop before publication and report the missing gate |
|
|
80
|
+
| `issue-tracker` | Issue create, edit, or comment requests | Issue state and issue URLs | Route the request there. |
|
|
81
|
+
| `pr-cleanup` | Placement, naming, sizing, or cleanup convergence | Cleanup findings or a focused PR boundary | Route the request there. |
|
|
82
|
+
| `autoconverge` | Autonomous PR review and fix loops | A converged draft or ready-state decision | Route the request there. |
|
|
83
|
+
| `source-command-commit` | Commit or push requests | A verified commit or pushed branch | Route the request there. |
|
|
84
|
+
|
|
85
|
+
## Task seeding
|
|
86
|
+
|
|
87
|
+
At the start of the work, register every item in
|
|
88
|
+
`reference/publication-tasks.md` through `TaskCreate`, `TodoWrite`, or the host
|
|
89
|
+
task equivalent when one is available. If no task tool is available, use the
|
|
90
|
+
catalog and the running work ledger. Mark each task with `PASS`, `FAIL` plus
|
|
91
|
+
file and line evidence, or `N/A` plus a reason for a conditional task.
|
|
92
|
+
|
|
93
|
+
## Process
|
|
94
|
+
|
|
95
|
+
### 1. Scope the action
|
|
96
|
+
|
|
97
|
+
Resolve the repository, action, and selected GitHub author. For create, resolve
|
|
98
|
+
the source branch, base, and head. For other actions, resolve one existing pull
|
|
99
|
+
request target. Create actions publish drafts. Record the target before any write.
|
|
100
|
+
|
|
101
|
+
### 2. Author the title and body
|
|
102
|
+
|
|
103
|
+
For create and full body rewrite, invoke the installed
|
|
104
|
+
`pr-description-writer`. Require its title and body file as the authoring
|
|
105
|
+
output. `pr-title-description` may review that output. A comment or review may
|
|
106
|
+
use a supplied body file when it does not rewrite the pull request description.
|
|
107
|
+
|
|
108
|
+
### 3. Run the local linter
|
|
109
|
+
|
|
110
|
+
Run `_shared/pr-loop/scripts/durable_post_lint.py` with the matching action and
|
|
111
|
+
title or body file. Use `pr-create`, `pr-edit`, `pr-comment`, or `pr-review`.
|
|
112
|
+
The linter owns the action-specific body, title, and volatile-path rules.
|
|
113
|
+
|
|
114
|
+
Exit code `0` continues to the next gate. Any non-zero exit stops the action. Fix
|
|
115
|
+
the named local input and rerun the linter. Do not look up the author or make a
|
|
116
|
+
network call while validation fails.
|
|
117
|
+
|
|
118
|
+
### 4. Run privacy and artifact checks
|
|
119
|
+
|
|
120
|
+
Invoke `privacy-hygiene` before the GitHub post. Upload binary evidence with
|
|
121
|
+
`scripts/gh_artifact_upload.py` and replace local artifact references with
|
|
122
|
+
permanent URLs. Rerun the linter after changing the body.
|
|
123
|
+
|
|
124
|
+
### 5. Resolve the process-local author
|
|
125
|
+
|
|
126
|
+
Run `.agents/skills/pull-request/scripts/pull_request.py` with one structured
|
|
127
|
+
action and the selected author. The command runs the linter again, resolves
|
|
128
|
+
the author value, and passes it only to the child `gh` process. It never
|
|
129
|
+
changes the parent environment or calls `gh auth switch`.
|
|
130
|
+
|
|
131
|
+
If the selected author has no usable value, stop and report the account lookup
|
|
132
|
+
failure. Never print the value.
|
|
133
|
+
|
|
134
|
+
### 6. Recover a selected legacy record
|
|
135
|
+
|
|
136
|
+
Run `.agents/skills/pull-request/scripts/recover_legacy_author.py
|
|
137
|
+
<exact-state-file> --confirm-inactive` only when the user selects one legacy
|
|
138
|
+
state file. The command checks the record's age, secure file metadata, and
|
|
139
|
+
contents. The confirmation flag records that the caller verified inactivity.
|
|
140
|
+
Leave every other record untouched. Delete the selected record only after a
|
|
141
|
+
successful restore. Mark this step `N/A` when no legacy state file is selected.
|
|
142
|
+
|
|
143
|
+
### 7. Run the action
|
|
144
|
+
|
|
145
|
+
Run `pull_request.py` for exactly one of `create`, `edit`, `comment`, or
|
|
146
|
+
`review`. Supply the validated body file path. Treat a non-zero exit as a
|
|
147
|
+
failed publication and preserve the local inputs for retry.
|
|
148
|
+
|
|
149
|
+
### 8. Read back the remote state
|
|
150
|
+
|
|
151
|
+
Use the same selected process-local author for a read-only remote readback.
|
|
152
|
+
Confirm the pull request URL and number, title, body, head SHA, and draft state.
|
|
153
|
+
For comments and reviews, confirm the new remote entry. Report artifact URLs
|
|
154
|
+
without exposing author values.
|
|
155
|
+
|
|
156
|
+
## Exit handling
|
|
157
|
+
|
|
158
|
+
Stop before network work when authoring, local validation, privacy, or author
|
|
159
|
+
selection fails. A failed action preserves its body and any recovery record.
|
|
160
|
+
Report the generic error and the next required input. A successful action ends
|
|
161
|
+
only after remote readback proves the requested state.
|
|
162
|
+
|
|
163
|
+
## Examples
|
|
164
|
+
|
|
165
|
+
Create example: the writer produces `pr-body.md`. The linter exits `0` for
|
|
166
|
+
`pr-create`. `pull_request.py create` publishes one draft pull request. The
|
|
167
|
+
readback matches the title, body, head SHA, and draft state.
|
|
168
|
+
|
|
169
|
+
Rejected comment example: the comment body names a worktree file. The linter
|
|
170
|
+
returns a non-zero exit. No account lookup or GitHub request runs. Replace the
|
|
171
|
+
path with inline text or a permanent artifact URL, then rerun.
|
|
172
|
+
|
|
173
|
+
Recovery example: one old record remains after an interrupted legacy account
|
|
174
|
+
swap. The user selects that exact file and confirms its session is inactive.
|
|
175
|
+
The recovery command restores the named account and deletes only that record.
|
|
176
|
+
|
|
177
|
+
## Gotchas
|
|
178
|
+
|
|
179
|
+
- Relative body-file paths depend on the caller directory. Use an absolute path
|
|
180
|
+
when another process or worktree starts the command.
|
|
181
|
+
- A record older than 30 minutes can still belong to a live session. Require
|
|
182
|
+
`--confirm-inactive` before recovery.
|
|
183
|
+
- Normal pull request actions never call `gh auth switch`. If that command
|
|
184
|
+
appears outside explicit recovery, stop.
|
|
185
|
+
- GitHub CLI output can contain account data. The command captures account
|
|
186
|
+
lookup and recovery output and prints only generic errors.
|
|
187
|
+
|
|
188
|
+
## File index
|
|
189
|
+
|
|
190
|
+
| Path | Purpose |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `SKILL.md` | Hub for pull request publication, validation, author selection, recovery, and readback |
|
|
193
|
+
| `reference/publication-tasks.md` | Ordered tasks for every publication gate |
|
|
194
|
+
| `scripts/pull_request.py` | Command for author selection and create, edit, comment, and review actions |
|
|
195
|
+
| `scripts/github_pr_command_constants/` | Constants for pull request actions, recovery, exit codes, and generic messages |
|
|
196
|
+
| `scripts/test_pull_request.py` | Tests for `pull_request.py` |
|
|
197
|
+
| `scripts/recover_legacy_author.py` | Command for one selected legacy author record |
|
|
198
|
+
| `scripts/test_github_pr_command_constants.py` | Tests for the command and recovery constants |
|
|
199
|
+
| `scripts/test_recover_legacy_author.py` | Tests for `recover_legacy_author.py` |
|
|
200
|
+
| `_shared/pr-loop/scripts/durable_post_lint.py` | Shared action-aware title, body, and path validator |
|
|
201
|
+
| `_shared/pr-loop/scripts/test_durable_post_lint.py` | Tests for the shared validator |
|
|
202
|
+
| `scripts/gh_artifact_upload.py` | Helper for permanent GitHub binary evidence URLs |
|
|
203
|
+
| `scripts/tests/test_gh_artifact_upload.py` | Tests for binary evidence upload |
|
|
204
|
+
|
|
205
|
+
## Folder map
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
pull-request/
|
|
209
|
+
├── SKILL.md
|
|
210
|
+
├── reference/
|
|
211
|
+
│ └── publication-tasks.md
|
|
212
|
+
└── scripts/
|
|
213
|
+
├── pull_request.py
|
|
214
|
+
├── github_pr_command_constants/
|
|
215
|
+
├── recover_legacy_author.py
|
|
216
|
+
├── test_pull_request.py
|
|
217
|
+
└── test_recover_legacy_author.py
|
|
218
|
+
```
|