claude-dev-env 8.98.6 → 8.98.8
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/pr-lifecycle/SKILL.md +4 -4
- package/bin/install-pstack-plugin.mjs +71 -21
- package/bin/install-pstack-plugin.test.mjs +46 -9
- package/bin/install.test.mjs +9 -9
- package/docs/account-broker.md +1 -1
- package/hooks/blocking/reply_length_gate.py +11 -2
- package/hooks/blocking/resolve_thread_reply_gate.py +135 -0
- package/hooks/blocking/test_reply_length_gate.py +11 -0
- package/hooks/blocking/test_resolve_thread_reply_gate.py +199 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/artifact_template_pointer_constants.py +3 -1
- package/hooks/hooks_constants/resolve_thread_reply_gate_constants.py +22 -0
- package/hooks/hooks_constants/test_artifact_template_pointer_constants.py +12 -0
- package/hooks/hooks_constants/transcript_skill_scan_constants.py +2 -0
- package/hooks/session/artifact_template_pointer.py +26 -3
- package/hooks/session/test_artifact_template_pointer.py +16 -0
- package/hooks/test_transcript_skill_scan.py +30 -0
- package/hooks/transcript_skill_scan.py +72 -1
- package/package.json +1 -1
- package/scripts/Sync-RepoMain.ps1 +102 -4
- package/scripts/account_broker.py +12 -4
- package/scripts/durable_post_lint.py +47 -1
- package/scripts/durable_post_lint_config/config/constants.py +6 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +1 -0
- package/scripts/policy_lint/adapter_configuration.py +5 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_account_broker.py +35 -0
- package/scripts/test_durable_post_lint.py +176 -3
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +43 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_sync_repo_main_mirror.py +226 -0
|
@@ -58,6 +58,14 @@ def test_pull_request_lifecycle_gate_registration_is_clean(tmp_path: Path) -> No
|
|
|
58
58
|
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def test_new_resolve_thread_reply_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
62
|
+
current_document = _hook_document(
|
|
63
|
+
["hooks/blocking/resolve_thread_reply_gate.py"],
|
|
64
|
+
'{"hooks": {}}',
|
|
65
|
+
)
|
|
66
|
+
assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
|
|
67
|
+
|
|
68
|
+
|
|
61
69
|
def test_new_merge_offer_gate_registration_is_clean(tmp_path: Path) -> None:
|
|
62
70
|
current_document = _hook_document(
|
|
63
71
|
["hooks/blocking/merge_over_review_offer_gate.py"],
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Specifications for Sync-RepoMain returning a mirror left on another branch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SCRIPT_PATH = Path(__file__).resolve().parents[1] / "Sync-RepoMain.ps1"
|
|
10
|
+
_PWSH = shutil.which("pwsh") or "pwsh"
|
|
11
|
+
_SKIPPED_EXIT_CODE = 3
|
|
12
|
+
_SUCCESS_EXIT_CODE = 0
|
|
13
|
+
_FAILURE_EXIT_CODE = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _git(repository_path: Path, *arguments: str) -> str:
|
|
17
|
+
completed = subprocess.run(
|
|
18
|
+
[
|
|
19
|
+
"git",
|
|
20
|
+
"-C",
|
|
21
|
+
str(repository_path),
|
|
22
|
+
"-c",
|
|
23
|
+
"user.name=tester",
|
|
24
|
+
"-c",
|
|
25
|
+
"user.email=tester@example.com",
|
|
26
|
+
"-c",
|
|
27
|
+
"core.autocrlf=false",
|
|
28
|
+
*arguments,
|
|
29
|
+
],
|
|
30
|
+
check=True,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
)
|
|
34
|
+
return completed.stdout.strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mirror_on_feature_branch_behind_remote(
|
|
38
|
+
root_path: Path, should_advance_remote: bool = True
|
|
39
|
+
) -> tuple[Path, str, str]:
|
|
40
|
+
remote_path = root_path / "remote.git"
|
|
41
|
+
author_path = root_path / "author"
|
|
42
|
+
mirror_path = root_path / "mirror"
|
|
43
|
+
remote_path.mkdir()
|
|
44
|
+
author_path.mkdir()
|
|
45
|
+
_git(remote_path, "init", "-q", "--bare", "-b", "main")
|
|
46
|
+
_git(author_path, "init", "-q", "-b", "main")
|
|
47
|
+
(author_path / "tracked.txt").write_text("old\n", encoding="utf-8")
|
|
48
|
+
_git(author_path, "add", "-A")
|
|
49
|
+
_git(author_path, "commit", "-q", "-m", "first")
|
|
50
|
+
_git(author_path, "remote", "add", "origin", str(remote_path))
|
|
51
|
+
_git(author_path, "push", "-q", "origin", "main")
|
|
52
|
+
subprocess.run(
|
|
53
|
+
[
|
|
54
|
+
"git",
|
|
55
|
+
"-c",
|
|
56
|
+
"core.autocrlf=false",
|
|
57
|
+
"clone",
|
|
58
|
+
"-q",
|
|
59
|
+
str(remote_path),
|
|
60
|
+
str(mirror_path),
|
|
61
|
+
],
|
|
62
|
+
check=True,
|
|
63
|
+
)
|
|
64
|
+
_git(mirror_path, "config", "core.autocrlf", "false")
|
|
65
|
+
_git(mirror_path, "switch", "-q", "-c", "feature")
|
|
66
|
+
(mirror_path / "tracked.txt").write_text("feature\n", encoding="utf-8")
|
|
67
|
+
_git(mirror_path, "commit", "-q", "-am", "feature edit")
|
|
68
|
+
feature_tip = _git(mirror_path, "rev-parse", "HEAD")
|
|
69
|
+
if should_advance_remote:
|
|
70
|
+
(author_path / "tracked.txt").write_text("new\n", encoding="utf-8")
|
|
71
|
+
_git(author_path, "commit", "-q", "-am", "second")
|
|
72
|
+
_git(author_path, "push", "-q", "origin", "main")
|
|
73
|
+
remote_tip = _git(author_path, "rev-parse", "HEAD")
|
|
74
|
+
return mirror_path, feature_tip, remote_tip
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_sync(mirror_path: Path, log_path: Path, *switches: str) -> int:
|
|
78
|
+
completed = subprocess.run(
|
|
79
|
+
[
|
|
80
|
+
_PWSH,
|
|
81
|
+
"-NoProfile",
|
|
82
|
+
"-File",
|
|
83
|
+
str(_SCRIPT_PATH),
|
|
84
|
+
"-RepoPath",
|
|
85
|
+
str(mirror_path),
|
|
86
|
+
*switches,
|
|
87
|
+
"-LogPath",
|
|
88
|
+
str(log_path),
|
|
89
|
+
],
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
check=False,
|
|
93
|
+
)
|
|
94
|
+
return completed.returncode
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_skip_a_feature_branch_checkout_without_mirror(tmp_path: Path) -> None:
|
|
98
|
+
mirror_path, feature_tip, _ = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
99
|
+
log_path = tmp_path / "sync.log"
|
|
100
|
+
|
|
101
|
+
assert _run_sync(mirror_path, log_path, "-StashDirty") == _SKIPPED_EXIT_CODE
|
|
102
|
+
assert _git(mirror_path, "branch", "--show-current") == "feature"
|
|
103
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == feature_tip
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_return_a_feature_branch_checkout_to_main_with_mirror(
|
|
107
|
+
tmp_path: Path,
|
|
108
|
+
) -> None:
|
|
109
|
+
mirror_path, feature_tip, remote_tip = _mirror_on_feature_branch_behind_remote(
|
|
110
|
+
tmp_path
|
|
111
|
+
)
|
|
112
|
+
log_path = tmp_path / "sync.log"
|
|
113
|
+
|
|
114
|
+
assert (
|
|
115
|
+
_run_sync(mirror_path, log_path, "-StashDirty", "-Mirror")
|
|
116
|
+
== _SUCCESS_EXIT_CODE
|
|
117
|
+
)
|
|
118
|
+
assert _git(mirror_path, "branch", "--show-current") == "main"
|
|
119
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == remote_tip
|
|
120
|
+
assert (mirror_path / "tracked.txt").read_text(encoding="utf-8") == "new\n"
|
|
121
|
+
assert _git(mirror_path, "status", "--porcelain") == ""
|
|
122
|
+
assert _git(mirror_path, "rev-parse", "feature") == feature_tip
|
|
123
|
+
assert f"left branch 'feature' at {feature_tip}" in log_path.read_text(
|
|
124
|
+
encoding="utf-8"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_should_restore_main_files_when_main_is_already_current(
|
|
129
|
+
tmp_path: Path,
|
|
130
|
+
) -> None:
|
|
131
|
+
mirror_path, feature_tip, remote_tip = _mirror_on_feature_branch_behind_remote(
|
|
132
|
+
tmp_path, should_advance_remote=False
|
|
133
|
+
)
|
|
134
|
+
log_path = tmp_path / "sync.log"
|
|
135
|
+
|
|
136
|
+
assert (
|
|
137
|
+
_run_sync(mirror_path, log_path, "-StashDirty", "-Mirror")
|
|
138
|
+
== _SUCCESS_EXIT_CODE
|
|
139
|
+
)
|
|
140
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == remote_tip
|
|
141
|
+
assert (mirror_path / "tracked.txt").read_text(encoding="utf-8") == "old\n"
|
|
142
|
+
assert _git(mirror_path, "status", "--porcelain") == ""
|
|
143
|
+
assert f"tree of branch 'feature' at {feature_tip}" in _git(
|
|
144
|
+
mirror_path, "stash", "list"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_should_return_to_main_without_stash_dirty(tmp_path: Path) -> None:
|
|
149
|
+
mirror_path, _, remote_tip = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
150
|
+
log_path = tmp_path / "sync.log"
|
|
151
|
+
|
|
152
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _SUCCESS_EXIT_CODE
|
|
153
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == remote_tip
|
|
154
|
+
assert _git(mirror_path, "status", "--porcelain") == ""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_should_name_a_detached_head_it_leaves(tmp_path: Path) -> None:
|
|
158
|
+
mirror_path, feature_tip, remote_tip = _mirror_on_feature_branch_behind_remote(
|
|
159
|
+
tmp_path
|
|
160
|
+
)
|
|
161
|
+
_git(mirror_path, "switch", "-q", "--detach", feature_tip)
|
|
162
|
+
log_path = tmp_path / "sync.log"
|
|
163
|
+
|
|
164
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _SUCCESS_EXIT_CODE
|
|
165
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == remote_tip
|
|
166
|
+
assert f"left detached HEAD at {feature_tip}" in log_path.read_text(
|
|
167
|
+
encoding="utf-8"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_should_stash_a_file_only_the_left_checkout_tracks(tmp_path: Path) -> None:
|
|
172
|
+
mirror_path, _, remote_tip = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
173
|
+
(mirror_path / "feature-only.txt").write_text("feature only\n", encoding="utf-8")
|
|
174
|
+
_git(mirror_path, "add", "feature-only.txt")
|
|
175
|
+
_git(mirror_path, "commit", "-q", "-m", "feature-only file")
|
|
176
|
+
(mirror_path / "local-note.txt").write_text("untracked note\n", encoding="utf-8")
|
|
177
|
+
log_path = tmp_path / "sync.log"
|
|
178
|
+
|
|
179
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _SUCCESS_EXIT_CODE
|
|
180
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == remote_tip
|
|
181
|
+
assert not (mirror_path / "feature-only.txt").exists()
|
|
182
|
+
assert (mirror_path / "local-note.txt").exists()
|
|
183
|
+
assert _git(mirror_path, "status", "--porcelain") == "?? local-note.txt"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_should_stash_a_left_file_that_main_ignores(tmp_path: Path) -> None:
|
|
187
|
+
mirror_path, _, _ = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
188
|
+
(mirror_path / "build.out").write_text("feature build\n", encoding="utf-8")
|
|
189
|
+
_git(mirror_path, "add", "build.out")
|
|
190
|
+
_git(mirror_path, "commit", "-q", "-m", "feature build output")
|
|
191
|
+
(mirror_path / ".git" / "info" / "exclude").write_text(
|
|
192
|
+
"build.out\n", encoding="utf-8"
|
|
193
|
+
)
|
|
194
|
+
log_path = tmp_path / "sync.log"
|
|
195
|
+
|
|
196
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _SUCCESS_EXIT_CODE
|
|
197
|
+
assert not (mirror_path / "build.out").exists()
|
|
198
|
+
assert "tree of branch 'feature'" in _git(mirror_path, "stash", "list")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_should_put_head_back_when_the_return_cannot_reset(tmp_path: Path) -> None:
|
|
202
|
+
mirror_path, feature_tip, _ = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
203
|
+
lock_path = mirror_path / ".git" / "index.lock"
|
|
204
|
+
lock_path.write_text("", encoding="utf-8")
|
|
205
|
+
log_path = tmp_path / "sync.log"
|
|
206
|
+
|
|
207
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _FAILURE_EXIT_CODE
|
|
208
|
+
assert _git(mirror_path, "branch", "--show-current") == "feature"
|
|
209
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == feature_tip
|
|
210
|
+
assert "HEAD and index are back on branch 'feature'" in log_path.read_text(
|
|
211
|
+
encoding="utf-8"
|
|
212
|
+
)
|
|
213
|
+
lock_path.unlink()
|
|
214
|
+
assert _git(mirror_path, "status", "--porcelain") == ""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_should_skip_a_checkout_with_staged_changes(tmp_path: Path) -> None:
|
|
218
|
+
mirror_path, feature_tip, _ = _mirror_on_feature_branch_behind_remote(tmp_path)
|
|
219
|
+
(mirror_path / "tracked.txt").write_text("staged\n", encoding="utf-8")
|
|
220
|
+
_git(mirror_path, "add", "tracked.txt")
|
|
221
|
+
log_path = tmp_path / "sync.log"
|
|
222
|
+
|
|
223
|
+
assert _run_sync(mirror_path, log_path, "-Mirror") == _SKIPPED_EXIT_CODE
|
|
224
|
+
assert _git(mirror_path, "branch", "--show-current") == "feature"
|
|
225
|
+
assert _git(mirror_path, "rev-parse", "HEAD") == feature_tip
|
|
226
|
+
assert _git(mirror_path, "diff", "--cached", "--name-only") == "tracked.txt"
|