claude-dev-env 1.62.1 → 1.64.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/code-advisor.md +22 -0
- package/agents/code-verifier.md +42 -0
- package/bin/install.mjs +1 -1
- package/hooks/blocking/code_rules_dead_argparse_argument.py +554 -0
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/config/verified_commit_constants.py +16 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +534 -0
- package/hooks/blocking/test_verification_verdict_store.py +232 -0
- package/hooks/blocking/test_verified_commit_gate.py +43 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +139 -0
- package/hooks/blocking/verification_verdict_store.py +165 -10
- package/hooks/blocking/verified_commit_gate.py +8 -2
- package/hooks/blocking/verifier_verdict_minter.py +59 -9
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +28 -0
- package/package.json +1 -1
- package/skills/autoconverge/SKILL.md +26 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +82 -18
- package/skills/autoconverge/workflow/converge.mjs +46 -18
- package/skills/verified-build/SKILL.md +38 -0
|
@@ -29,12 +29,16 @@ from config.verified_commit_constants import (
|
|
|
29
29
|
AGENT_META_SIDECAR_SUFFIX,
|
|
30
30
|
AGENT_META_TYPE_KEY,
|
|
31
31
|
AGENT_TRANSCRIPT_GLOB,
|
|
32
|
+
BRANCH_REFERENCE_PREFIX,
|
|
33
|
+
BRANCH_WORKTREE_ABSENT_MESSAGE,
|
|
32
34
|
CLAUDE_HOME_DIRECTORY_NAME,
|
|
33
35
|
CONFTEST_FILE_NAME,
|
|
34
36
|
DOCS_ONLY_EXTENSIONS,
|
|
35
37
|
ALL_FALLBACK_BASE_REFERENCES,
|
|
38
|
+
EMPTY_SURFACE_GUARD_MESSAGE,
|
|
36
39
|
GIT_TIMEOUT_SECONDS,
|
|
37
40
|
MANIFEST_HASH_CLI_FLAG,
|
|
41
|
+
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG,
|
|
38
42
|
MINIMUM_STATUS_FIELD_COUNT,
|
|
39
43
|
MINTING_AGENT_TYPE,
|
|
40
44
|
PYTHON_EXTENSION,
|
|
@@ -52,10 +56,13 @@ from config.verified_commit_constants import (
|
|
|
52
56
|
TRANSCRIPT_TEXT_KEY,
|
|
53
57
|
VERDICT_DIRECTORY_NAME,
|
|
54
58
|
VERDICT_FENCE_PATTERN,
|
|
59
|
+
VERDICT_FILE_GLOB,
|
|
55
60
|
VERDICT_JSON_INDENT,
|
|
56
61
|
VERDICT_KEY_ALL_PASS,
|
|
57
62
|
VERDICT_KEY_FINDINGS,
|
|
58
63
|
VERDICT_KEY_MANIFEST_SHA256,
|
|
64
|
+
WORKTREE_LIST_BRANCH_PREFIX,
|
|
65
|
+
WORKTREE_LIST_PATH_PREFIX,
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
|
|
@@ -243,6 +250,65 @@ def manifest_sha256(surface_manifest_text: str) -> str:
|
|
|
243
250
|
return hashlib.sha256(surface_manifest_text.encode("utf-8")).hexdigest()
|
|
244
251
|
|
|
245
252
|
|
|
253
|
+
def empty_surface_hash() -> str:
|
|
254
|
+
"""Return the manifest hash that represents an empty change surface.
|
|
255
|
+
|
|
256
|
+
A work tree whose HEAD equals the merge base has no changed or untracked
|
|
257
|
+
files, so ``branch_surface_manifest`` returns ``""`` and this hash is what
|
|
258
|
+
the minter or store CLI would produce for it. Comparing an attested hash
|
|
259
|
+
against this value lets the minter refuse to mint for a verifier that ran
|
|
260
|
+
in the wrong work tree.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The hex sha256 digest of the empty surface manifest (the empty string).
|
|
264
|
+
"""
|
|
265
|
+
return manifest_sha256("")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def worktree_path_for_branch(repo_directory: str, branch_name: str) -> str | None:
|
|
269
|
+
"""Find the work-tree directory that has a given branch checked out.
|
|
270
|
+
|
|
271
|
+
Parses the porcelain output of ``git worktree list --porcelain`` and
|
|
272
|
+
returns the ``worktree <path>`` line whose block carries a
|
|
273
|
+
``branch refs/heads/<branch_name>`` line. Returns None when git fails
|
|
274
|
+
or no work tree holds the branch.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
repo_directory: Any directory inside the repository.
|
|
278
|
+
branch_name: The short branch name to locate (without ``refs/heads/``).
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The absolute path of the work tree that has the branch checked out,
|
|
282
|
+
or None when no work tree holds the branch or git fails.
|
|
283
|
+
"""
|
|
284
|
+
porcelain_output = run_git(repo_directory, "worktree", "list", "--porcelain")
|
|
285
|
+
if porcelain_output is None:
|
|
286
|
+
return None
|
|
287
|
+
target_branch_reference = f"{BRANCH_REFERENCE_PREFIX}{branch_name}"
|
|
288
|
+
current_worktree_path: str | None = None
|
|
289
|
+
for each_line in porcelain_output.splitlines():
|
|
290
|
+
if each_line.startswith(WORKTREE_LIST_PATH_PREFIX):
|
|
291
|
+
current_worktree_path = each_line[len(WORKTREE_LIST_PATH_PREFIX):]
|
|
292
|
+
elif each_line.startswith(WORKTREE_LIST_BRANCH_PREFIX):
|
|
293
|
+
branch_reference = each_line[len(WORKTREE_LIST_BRANCH_PREFIX):]
|
|
294
|
+
if branch_reference == target_branch_reference and current_worktree_path:
|
|
295
|
+
return current_worktree_path
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def verdict_directory() -> Path:
|
|
300
|
+
"""Return the shared directory holding every work tree's verdict file.
|
|
301
|
+
|
|
302
|
+
Verdicts live outside any repository (under the user's Claude home) so no
|
|
303
|
+
repo accumulates untracked verdict files; every work tree's verdict shares
|
|
304
|
+
this one directory, distinguished by file name.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
The verdict directory under the user's Claude home.
|
|
308
|
+
"""
|
|
309
|
+
return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / VERDICT_DIRECTORY_NAME
|
|
310
|
+
|
|
311
|
+
|
|
246
312
|
def verdict_path_for_repo(repo_root: str) -> Path:
|
|
247
313
|
"""Derive the verdict file path for a repository work tree.
|
|
248
314
|
|
|
@@ -258,9 +324,7 @@ def verdict_path_for_repo(repo_root: str) -> Path:
|
|
|
258
324
|
"""
|
|
259
325
|
normalized_root = str(Path(repo_root).resolve()).replace("\\", "/").lower()
|
|
260
326
|
root_key = hashlib.sha256(normalized_root.encode("utf-8")).hexdigest()[:ROOT_KEY_HEX_LENGTH]
|
|
261
|
-
return (
|
|
262
|
-
Path.home() / CLAUDE_HOME_DIRECTORY_NAME / VERDICT_DIRECTORY_NAME / f"{root_key}.json"
|
|
263
|
-
)
|
|
327
|
+
return verdict_directory() / f"{root_key}.json"
|
|
264
328
|
|
|
265
329
|
|
|
266
330
|
def load_valid_verdict(repo_root: str, expected_manifest_sha256: str) -> dict | None:
|
|
@@ -289,6 +353,44 @@ def load_valid_verdict(repo_root: str, expected_manifest_sha256: str) -> dict |
|
|
|
289
353
|
return verdict_record
|
|
290
354
|
|
|
291
355
|
|
|
356
|
+
def minted_verdict_covers_surface(expected_manifest_sha256: str) -> bool:
|
|
357
|
+
"""Decide whether any minted verdict covers the live surface, keyed by hash.
|
|
358
|
+
|
|
359
|
+
A verdict's bound ``manifest_sha256`` commits to the exact set of surface
|
|
360
|
+
file paths and their byte contents; the work tree's location never enters
|
|
361
|
+
the hash. A clean verdict minted while verifying one work tree therefore
|
|
362
|
+
proves the same change surface in a sibling work tree of the same branch,
|
|
363
|
+
even though each work tree files its verdict under its own path-keyed name.
|
|
364
|
+
Scanning every verdict file by bound hash lets that verdict clear the
|
|
365
|
+
sibling's commit, while a verdict bound to a different hash — different
|
|
366
|
+
code — never matches. The path-keyed ``load_valid_verdict`` stays the fast
|
|
367
|
+
same-work-tree lookup; this is the cross-work-tree fallback.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
expected_manifest_sha256: Hash of the live surface manifest the verdict
|
|
371
|
+
must match exactly.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True as soon as one verdict file reports ``all_pass`` true and binds to
|
|
375
|
+
the expected hash; False when none match or the directory is absent.
|
|
376
|
+
"""
|
|
377
|
+
verdict_dir = verdict_directory()
|
|
378
|
+
if not verdict_dir.is_dir():
|
|
379
|
+
return False
|
|
380
|
+
for each_verdict_file in sorted(verdict_dir.glob(VERDICT_FILE_GLOB)):
|
|
381
|
+
try:
|
|
382
|
+
verdict_record = json.loads(each_verdict_file.read_text(encoding="utf-8"))
|
|
383
|
+
except (OSError, json.JSONDecodeError):
|
|
384
|
+
continue
|
|
385
|
+
if not isinstance(verdict_record, dict):
|
|
386
|
+
continue
|
|
387
|
+
if verdict_record.get(VERDICT_KEY_ALL_PASS) is not True:
|
|
388
|
+
continue
|
|
389
|
+
if verdict_record.get(VERDICT_KEY_MANIFEST_SHA256) == expected_manifest_sha256:
|
|
390
|
+
return True
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
|
|
292
394
|
def _subagents_directory_for_transcript(transcript_path: str) -> Path | None:
|
|
293
395
|
"""Locate the live session's subagents directory from a transcript path.
|
|
294
396
|
|
|
@@ -647,14 +749,18 @@ def _print_live_manifest_hash(repo_directory: str) -> int:
|
|
|
647
749
|
"""Print the live surface manifest hash for a repo, for a workflow verifier.
|
|
648
750
|
|
|
649
751
|
A workflow code-verifier runs this to learn the exact hash to bind its
|
|
650
|
-
verdict to, so stdout carries only the hash and nothing else.
|
|
752
|
+
verdict to, so stdout carries only the hash and nothing else. When the
|
|
753
|
+
work tree has no changed or untracked files (empty change surface), this
|
|
754
|
+
prints the empty-surface guard message to stderr and returns nonzero —
|
|
755
|
+
an empty surface means the verifier is pointed at the wrong work tree.
|
|
651
756
|
|
|
652
757
|
Args:
|
|
653
758
|
repo_directory: A directory inside the work tree to bind the verdict to.
|
|
654
759
|
|
|
655
760
|
Returns:
|
|
656
|
-
0 after printing the hash; nonzero with no stdout when the repo root
|
|
657
|
-
merge base cannot be resolved
|
|
761
|
+
0 after printing the hash; nonzero with no stdout when the repo root,
|
|
762
|
+
merge base, or manifest cannot be resolved, or when the change surface
|
|
763
|
+
is empty (wrong work tree).
|
|
658
764
|
"""
|
|
659
765
|
repo_root = resolve_repo_root(repo_directory)
|
|
660
766
|
if repo_root is None:
|
|
@@ -665,20 +771,69 @@ def _print_live_manifest_hash(repo_directory: str) -> int:
|
|
|
665
771
|
surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
|
|
666
772
|
if surface_manifest_text is None:
|
|
667
773
|
return 1
|
|
774
|
+
if surface_manifest_text == "":
|
|
775
|
+
print(EMPTY_SURFACE_GUARD_MESSAGE.format(repo_root=repo_root), file=sys.stderr)
|
|
776
|
+
return 1
|
|
668
777
|
print(manifest_sha256(surface_manifest_text))
|
|
669
778
|
return 0
|
|
670
779
|
|
|
671
780
|
|
|
781
|
+
def _print_branch_manifest_hash(branch_name: str) -> int:
|
|
782
|
+
"""Print the manifest hash for the work tree that holds a given branch.
|
|
783
|
+
|
|
784
|
+
Resolves the repository root from the current working directory, then
|
|
785
|
+
finds the work tree that has ``branch_name`` checked out, and delegates
|
|
786
|
+
to ``_print_live_manifest_hash`` for that work tree. This mode is immune
|
|
787
|
+
to the verifier's own cwd: it always hashes the work tree that holds the
|
|
788
|
+
branch under review, regardless of where the verifier itself is running.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
branch_name: The short branch name to locate (without ``refs/heads/``).
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
0 after printing the hash; nonzero with a stderr message when the
|
|
795
|
+
repo root cannot be resolved, no work tree holds the branch, or the
|
|
796
|
+
located work tree has an empty change surface.
|
|
797
|
+
"""
|
|
798
|
+
repo_root = resolve_repo_root(str(Path.cwd()))
|
|
799
|
+
if repo_root is None:
|
|
800
|
+
print("ERROR: Current directory is not inside a git repository.", file=sys.stderr)
|
|
801
|
+
return 1
|
|
802
|
+
branch_worktree_path = worktree_path_for_branch(repo_root, branch_name)
|
|
803
|
+
if branch_worktree_path is None:
|
|
804
|
+
print(
|
|
805
|
+
BRANCH_WORKTREE_ABSENT_MESSAGE.format(branch=branch_name),
|
|
806
|
+
file=sys.stderr,
|
|
807
|
+
)
|
|
808
|
+
return 1
|
|
809
|
+
return _print_live_manifest_hash(branch_worktree_path)
|
|
810
|
+
|
|
811
|
+
|
|
672
812
|
def main() -> None:
|
|
673
813
|
"""Run the verdict-store CLI: compute the live surface-manifest hash.
|
|
674
814
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
815
|
+
Two modes:
|
|
816
|
+
|
|
817
|
+
``--manifest-hash <work-tree-dir>``
|
|
818
|
+
Print the live ``manifest_sha256`` for the given work tree directory
|
|
819
|
+
so a workflow code-verifier can bind its verdict to the exact surface
|
|
820
|
+
the gate checks. Fails with a stderr message when the change surface
|
|
821
|
+
is empty (wrong work tree).
|
|
822
|
+
|
|
823
|
+
``--manifest-hash-for-branch <branch>``
|
|
824
|
+
Resolve the work tree that has ``<branch>`` checked out (via
|
|
825
|
+
``git worktree list --porcelain``) and print its manifest hash.
|
|
826
|
+
Immune to the verifier's own cwd — always targets the branch's own
|
|
827
|
+
work tree. Fails when no work tree holds the branch or the surface
|
|
828
|
+
is empty.
|
|
829
|
+
|
|
830
|
+
Exits nonzero with no stdout on any other argument shape or when the
|
|
831
|
+
surface cannot be resolved.
|
|
679
832
|
"""
|
|
680
833
|
if len(sys.argv) == 3 and sys.argv[1] == MANIFEST_HASH_CLI_FLAG:
|
|
681
834
|
sys.exit(_print_live_manifest_hash(sys.argv[2]))
|
|
835
|
+
if len(sys.argv) == 3 and sys.argv[1] == MANIFEST_HASH_FOR_BRANCH_CLI_FLAG:
|
|
836
|
+
sys.exit(_print_branch_manifest_hash(sys.argv[2]))
|
|
682
837
|
sys.exit(1)
|
|
683
838
|
|
|
684
839
|
|
|
@@ -13,8 +13,11 @@ and allows the command only when one of these holds:
|
|
|
13
13
|
- the surface is mechanically exempt (docs/images by extension, pytest
|
|
14
14
|
test files by name convention, Python files whose docstring-stripped
|
|
15
15
|
AST is unchanged), or
|
|
16
|
-
- a verdict
|
|
17
|
-
|
|
16
|
+
- a passing verifier verdict binds to the exact live manifest hash —
|
|
17
|
+
matched by content hash, not by work-tree location, so a verdict
|
|
18
|
+
``verifier_verdict_minter.py`` minted while verifying any work tree of
|
|
19
|
+
the surface clears the commit, as does one a workflow ``code-verifier``
|
|
20
|
+
emitted in its own transcript.
|
|
18
21
|
|
|
19
22
|
The surface binds every changed and untracked file's content, so slicing
|
|
20
23
|
work into small commits or staging files cannot move the hash, while any
|
|
@@ -57,6 +60,7 @@ from verification_verdict_store import (
|
|
|
57
60
|
is_verification_exempt_diff,
|
|
58
61
|
load_valid_verdict,
|
|
59
62
|
manifest_sha256,
|
|
63
|
+
minted_verdict_covers_surface,
|
|
60
64
|
resolve_merge_base,
|
|
61
65
|
resolve_repo_root,
|
|
62
66
|
workflow_verdict_covers_surface,
|
|
@@ -500,6 +504,8 @@ def deny_reason_for_directory(target_directory: str, transcript_path: str) -> st
|
|
|
500
504
|
live_manifest_sha256 = manifest_sha256(surface_manifest_text)
|
|
501
505
|
if load_valid_verdict(repo_root, live_manifest_sha256) is not None:
|
|
502
506
|
return None
|
|
507
|
+
if minted_verdict_covers_surface(live_manifest_sha256):
|
|
508
|
+
return None
|
|
503
509
|
if workflow_verdict_covers_surface(transcript_path, live_manifest_sha256):
|
|
504
510
|
return None
|
|
505
511
|
hash_preview = live_manifest_sha256[:HASH_PREVIEW_LENGTH]
|
|
@@ -35,9 +35,13 @@ blocking_directory = str(Path(__file__).resolve().parent)
|
|
|
35
35
|
if blocking_directory not in sys.path:
|
|
36
36
|
sys.path.insert(0, blocking_directory)
|
|
37
37
|
|
|
38
|
-
from config.verified_commit_constants import
|
|
38
|
+
from config.verified_commit_constants import (
|
|
39
|
+
MINTING_AGENT_TYPE,
|
|
40
|
+
VERDICT_KEY_MANIFEST_SHA256,
|
|
41
|
+
)
|
|
39
42
|
from verification_verdict_store import (
|
|
40
43
|
branch_surface_manifest,
|
|
44
|
+
empty_surface_hash,
|
|
41
45
|
manifest_sha256,
|
|
42
46
|
resolve_merge_base,
|
|
43
47
|
resolve_repo_root,
|
|
@@ -169,6 +173,53 @@ def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
|
|
|
169
173
|
)
|
|
170
174
|
|
|
171
175
|
|
|
176
|
+
def _attested_or_recomputed_hash(verdict_record: dict, repo_root: str) -> str | None:
|
|
177
|
+
"""Choose the surface hash the minted verdict binds to.
|
|
178
|
+
|
|
179
|
+
A code-verifier that verifies a work tree other than the stop event's cwd
|
|
180
|
+
attests the surface it checked by emitting ``manifest_sha256`` in its
|
|
181
|
+
verdict fence (computed against the verified work tree via the
|
|
182
|
+
``--manifest-hash`` CLI). Binding the minted verdict to that attested hash
|
|
183
|
+
keeps the verdict tied to the code actually verified rather than the
|
|
184
|
+
subagent's cwd, so a verdict earned for one work tree clears a commit in a
|
|
185
|
+
sibling work tree of the same surface. When the fence attests no hash, the
|
|
186
|
+
minter recomputes one from the cwd work tree, which is correct whenever the
|
|
187
|
+
verifier ran in the work tree it verified.
|
|
188
|
+
|
|
189
|
+
Returns None (mints nothing) in two empty-surface cases:
|
|
190
|
+
|
|
191
|
+
- The attested hash equals ``empty_surface_hash()`` — the verifier called
|
|
192
|
+
the store CLI on a wrong (empty) work tree and the hash it got back
|
|
193
|
+
represents nothing.
|
|
194
|
+
- The recompute branch produces an empty manifest — the cwd work tree also
|
|
195
|
+
has no changed or untracked files versus the merge base.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
verdict_record: The parsed verdict fence from the verifier transcript.
|
|
199
|
+
repo_root: The work-tree root resolved from the stop event's cwd, used
|
|
200
|
+
for the recompute fallback.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The attested ``manifest_sha256`` when the fence carries a non-empty
|
|
204
|
+
string one that is not the empty-surface sentinel; the cwd work tree's
|
|
205
|
+
recomputed surface hash when the fence attests nothing and the surface
|
|
206
|
+
is non-empty; or None when the attested hash is the empty-surface
|
|
207
|
+
sentinel, the surface manifest is empty, or no upstream base resolves.
|
|
208
|
+
"""
|
|
209
|
+
attested_manifest_sha256 = verdict_record.get(VERDICT_KEY_MANIFEST_SHA256)
|
|
210
|
+
if isinstance(attested_manifest_sha256, str) and attested_manifest_sha256:
|
|
211
|
+
if attested_manifest_sha256 == empty_surface_hash():
|
|
212
|
+
return None
|
|
213
|
+
return attested_manifest_sha256
|
|
214
|
+
merge_base_sha = resolve_merge_base(repo_root)
|
|
215
|
+
if merge_base_sha is None:
|
|
216
|
+
return None
|
|
217
|
+
surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
|
|
218
|
+
if not surface_manifest_text:
|
|
219
|
+
return None
|
|
220
|
+
return manifest_sha256(surface_manifest_text)
|
|
221
|
+
|
|
222
|
+
|
|
172
223
|
def mint_for_payload(subagent_stop_payload: dict) -> Path | None:
|
|
173
224
|
"""Mint a verdict file for a code-verifier stop event.
|
|
174
225
|
|
|
@@ -177,8 +228,10 @@ def mint_for_payload(subagent_stop_payload: dict) -> Path | None:
|
|
|
177
228
|
|
|
178
229
|
Returns:
|
|
179
230
|
The verdict file path when minted; None when the payload is not a
|
|
180
|
-
code-verifier stop, the transcript holds no verdict,
|
|
181
|
-
|
|
231
|
+
code-verifier stop, the transcript holds no verdict, the cwd is not a
|
|
232
|
+
work tree, or — for a verdict that attests no ``manifest_sha256`` of
|
|
233
|
+
its own — that work tree has no upstream base to recompute the surface
|
|
234
|
+
hash from.
|
|
182
235
|
"""
|
|
183
236
|
if resolved_subagent_type(subagent_stop_payload) != MINTING_AGENT_TYPE:
|
|
184
237
|
return None
|
|
@@ -191,15 +244,12 @@ def mint_for_payload(subagent_stop_payload: dict) -> Path | None:
|
|
|
191
244
|
repo_root = resolve_repo_root(subagent_stop_payload.get("cwd", "."))
|
|
192
245
|
if repo_root is None:
|
|
193
246
|
return None
|
|
194
|
-
|
|
195
|
-
if
|
|
196
|
-
return None
|
|
197
|
-
surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
|
|
198
|
-
if surface_manifest_text is None:
|
|
247
|
+
bound_manifest_sha256 = _attested_or_recomputed_hash(verdict_record, repo_root)
|
|
248
|
+
if bound_manifest_sha256 is None:
|
|
199
249
|
return None
|
|
200
250
|
return write_verdict(
|
|
201
251
|
repo_root,
|
|
202
|
-
|
|
252
|
+
bound_manifest_sha256,
|
|
203
253
|
verdict_record["all_pass"],
|
|
204
254
|
verdict_record["findings"],
|
|
205
255
|
str(subagent_stop_payload.get("agent_id", "")),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Constants for the dead argparse-argument detector in ``code_rules_enforcer``.
|
|
2
|
+
|
|
3
|
+
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
+
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
+
requirement and share a home with the other hook-tree configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
ADD_ARGUMENT_METHOD_NAME: str = "add_argument"
|
|
9
|
+
ALL_PARSE_METHOD_NAMES: frozenset[str] = frozenset({"parse_args", "parse_known_args"})
|
|
10
|
+
DEST_KEYWORD_NAME: str = "dest"
|
|
11
|
+
NAMESPACE_KEYWORD_NAME: str = "namespace"
|
|
12
|
+
ACTION_KEYWORD_NAME: str = "action"
|
|
13
|
+
ALL_SUPPRESSED_ACTION_NAMES: frozenset[str] = frozenset({"help", "version"})
|
|
14
|
+
GETATTR_FUNCTION_NAME: str = "getattr"
|
|
15
|
+
GETATTR_NAME_ARGUMENT_MINIMUM: int = 2
|
|
16
|
+
ATTRGETTER_FUNCTION_NAME: str = "attrgetter"
|
|
17
|
+
NAMESPACE_DICT_ATTRIBUTE_NAME: str = "__dict__"
|
|
18
|
+
OPTION_PREFIX: str = "-"
|
|
19
|
+
LONG_OPTION_PREFIX: str = "--"
|
|
20
|
+
DEST_WORD_SEPARATOR: str = "-"
|
|
21
|
+
DEST_WORD_JOINER: str = "_"
|
|
22
|
+
EXPORTED_NAMES_ATTRIBUTE: str = "__all__"
|
|
23
|
+
MAX_DEAD_ARGPARSE_ARGUMENT_ISSUES: int = 25
|
|
24
|
+
DEAD_ARGPARSE_ARGUMENT_GUIDANCE: str = (
|
|
25
|
+
"optional CLI flag whose parsed value is never read in this file - remove the"
|
|
26
|
+
" add_argument call (argparse silently accepts an unused flag), or read the"
|
|
27
|
+
" parsed value where it is needed (CODE_RULES §9.8)"
|
|
28
|
+
)
|
package/package.json
CHANGED
|
@@ -34,7 +34,9 @@ PR's owner.
|
|
|
34
34
|
|
|
35
35
|
1. **Enter a worktree.** Call `EnterWorktree` with no arguments before any
|
|
36
36
|
`gh`, `git`, file read, or edit. `gh`/`git` Bash calls do not auto-isolate,
|
|
37
|
-
so this is mandatory. If it fails, report and stop.
|
|
37
|
+
so this is mandatory. If it fails, report and stop. A bare `EnterWorktree`
|
|
38
|
+
branches from `origin/main`; step 2 positions the worktree on the PR's head
|
|
39
|
+
ref, which the workflow needs.
|
|
38
40
|
|
|
39
41
|
2. **Resolve PR scope.** When the user passed a PR URL or number, parse owner,
|
|
40
42
|
repo, and number from it. Otherwise read the current branch's PR:
|
|
@@ -43,6 +45,18 @@ PR's owner.
|
|
|
43
45
|
ready, mark it draft first (`gh pr ready <n> --repo <o>/<r> --undo`) so the
|
|
44
46
|
loop owns the ready transition.
|
|
45
47
|
|
|
48
|
+
**Position the worktree on the PR branch.** The workflow reviews
|
|
49
|
+
`git diff origin/main...HEAD` against this worktree's local `HEAD` and pushes
|
|
50
|
+
each fix to the PR branch, so the worktree sits on the PR's head ref at the PR
|
|
51
|
+
HEAD before the workflow launches. A worktree fresh off `origin/main` has
|
|
52
|
+
`HEAD == origin/main`, shows an empty diff, and reports a false convergence
|
|
53
|
+
with zero findings. When a local worktree already tracks the PR branch, enter
|
|
54
|
+
that one by passing its path to `EnterWorktree`; otherwise put the entered
|
|
55
|
+
worktree on the branch with `gh pr checkout <number> --repo <owner>/<repo>`
|
|
56
|
+
(or `git fetch origin <headRefName>` then `git switch <headRefName>`). Confirm
|
|
57
|
+
before launching: `git rev-parse --abbrev-ref HEAD` equals the PR's head ref
|
|
58
|
+
and local `HEAD` equals the PR head SHA.
|
|
59
|
+
|
|
46
60
|
3. **Verify the worktree is the PR's repo (strict pre-flight).** Run
|
|
47
61
|
`python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <owner> --repo <repo> --mode strict`.
|
|
48
62
|
It confirms the working directory is a checkout of the PR's own repo and
|
|
@@ -56,6 +70,17 @@ PR's owner.
|
|
|
56
70
|
4. **Grant project permissions.**
|
|
57
71
|
`python "$HOME/.claude/skills/bugteam/scripts/grant_project_claude_permissions.py"`
|
|
58
72
|
|
|
73
|
+
In auto-mode the classifier blocks this grant as an unrequested change to the
|
|
74
|
+
permission allowlist: the `/autoconverge` invocation alone does not meet its
|
|
75
|
+
bar for an explicitly requested permission change. When it is blocked, keep
|
|
76
|
+
the run alive — surface the grant to the user through `AskUserQuestion` with
|
|
77
|
+
the exact command and ask them to approve it or run it themselves with the `!`
|
|
78
|
+
prefix:
|
|
79
|
+
`! python "$HOME/.claude/skills/bugteam/scripts/grant_project_claude_permissions.py"`.
|
|
80
|
+
Continue once the grant lands. A user who wants future runs to skip this
|
|
81
|
+
prompt can add a standing Bash permission allow-rule for that script in their
|
|
82
|
+
settings.
|
|
83
|
+
|
|
59
84
|
## Run the workflow
|
|
60
85
|
|
|
61
86
|
Call the `Workflow` tool against the colocated script:
|
|
@@ -220,37 +220,61 @@ test('the fix flow spawns a code-verifier step between the edit step and the com
|
|
|
220
220
|
);
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
assert.notEqual(constantStart, -1, `expected ${constantName} to exist`);
|
|
226
|
-
const nextConstantStart = convergeSource.indexOf('\nconst ', constantStart + 1);
|
|
227
|
-
const constantEnd = nextConstantStart === -1 ? convergeSource.length : nextConstantStart;
|
|
228
|
-
return convergeSource.slice(constantStart, constantEnd);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
test('the shared verdict-fence steps name the binding-hash command and the verdict fence', () => {
|
|
232
|
-
const fenceSteps = constantBody('VERDICT_FENCE_STEPS');
|
|
233
|
-
assert.match(fenceSteps, /--manifest-hash/, 'expected the binding-hash command to be named');
|
|
223
|
+
test('the shared verdict-fence builder names the binding-hash command and the verdict fence', () => {
|
|
224
|
+
const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
|
|
234
225
|
assert.match(
|
|
235
|
-
|
|
226
|
+
fenceBuilder,
|
|
227
|
+
/--manifest-hash-for-branch/,
|
|
228
|
+
'expected the binding-hash command to use --manifest-hash-for-branch (cwd-immune)',
|
|
229
|
+
);
|
|
230
|
+
assert.doesNotMatch(
|
|
231
|
+
fenceBuilder,
|
|
232
|
+
/--manifest-hash(?!-for-branch)/,
|
|
233
|
+
'expected the old --manifest-hash <REPO> form to be removed in favour of --manifest-hash-for-branch',
|
|
234
|
+
);
|
|
235
|
+
assert.match(
|
|
236
|
+
fenceBuilder,
|
|
236
237
|
/verification_verdict_store\.py/,
|
|
237
238
|
'expected the verdict-store script that computes the binding hash to be named',
|
|
238
239
|
);
|
|
239
|
-
assert.match(
|
|
240
|
-
assert.match(
|
|
240
|
+
assert.match(fenceBuilder, /```verdict/, 'expected the verdict fence to be specified');
|
|
241
|
+
assert.match(fenceBuilder, /manifest_sha256/, 'expected the verdict fence to carry manifest_sha256');
|
|
242
|
+
assert.match(
|
|
243
|
+
fenceBuilder,
|
|
244
|
+
/gh pr view/,
|
|
245
|
+
'expected buildVerdictFenceSteps to resolve the head branch via gh pr view (cwd-immune)',
|
|
246
|
+
);
|
|
247
|
+
assert.match(
|
|
248
|
+
fenceBuilder,
|
|
249
|
+
/headRefName/,
|
|
250
|
+
'expected buildVerdictFenceSteps to extract the headRefName from gh pr view output',
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('the verdict-fence binding does not self-resolve a cwd via git rev-parse for the manifest hash', () => {
|
|
255
|
+
const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
|
|
256
|
+
assert.doesNotMatch(
|
|
257
|
+
fenceBuilder,
|
|
258
|
+
/git rev-parse --show-toplevel/,
|
|
259
|
+
'expected the binding hash to be cwd-immune (no git rev-parse in the binding step)',
|
|
260
|
+
);
|
|
241
261
|
});
|
|
242
262
|
|
|
243
|
-
test('every verify step
|
|
263
|
+
test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
|
|
244
264
|
for (const verifyFunctionName of [
|
|
245
265
|
'verifyFixesInWorkingTree',
|
|
246
266
|
'verifyRepairChanges',
|
|
247
|
-
'verifyHardeningChanges',
|
|
248
267
|
]) {
|
|
249
268
|
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
250
269
|
assert.match(
|
|
251
270
|
verifyBody,
|
|
252
|
-
/
|
|
253
|
-
`expected ${verifyFunctionName} to
|
|
271
|
+
/buildVerdictFenceSteps\(/,
|
|
272
|
+
`expected ${verifyFunctionName} to call buildVerdictFenceSteps (cwd-immune branch binding)`,
|
|
273
|
+
);
|
|
274
|
+
assert.doesNotMatch(
|
|
275
|
+
verifyBody,
|
|
276
|
+
/VERDICT_FENCE_STEPS(?!\s*\))/,
|
|
277
|
+
`expected ${verifyFunctionName} not to reference the removed VERDICT_FENCE_STEPS constant`,
|
|
254
278
|
);
|
|
255
279
|
assert.match(
|
|
256
280
|
verifyBody,
|
|
@@ -270,6 +294,46 @@ test('every verify step reuses the shared verdict-fence steps, uses code-verifie
|
|
|
270
294
|
}
|
|
271
295
|
});
|
|
272
296
|
|
|
297
|
+
test('verifyHardeningChanges uses --manifest-hash-for-branch with the hardening branch, uses code-verifier, and forbids edits', () => {
|
|
298
|
+
const verifyBody = lensPromptBody('verifyHardeningChanges');
|
|
299
|
+
assert.match(
|
|
300
|
+
verifyBody,
|
|
301
|
+
/--manifest-hash-for-branch/,
|
|
302
|
+
'expected verifyHardeningChanges to bind by hardening branch (cwd-immune)',
|
|
303
|
+
);
|
|
304
|
+
assert.doesNotMatch(
|
|
305
|
+
verifyBody,
|
|
306
|
+
/--manifest-hash(?!-for-branch)/,
|
|
307
|
+
'expected verifyHardeningChanges not to use the old --manifest-hash <REPO> form',
|
|
308
|
+
);
|
|
309
|
+
assert.match(
|
|
310
|
+
verifyBody,
|
|
311
|
+
/agentType:\s*'code-verifier'/,
|
|
312
|
+
'expected verifyHardeningChanges to spawn the code-verifier agent type',
|
|
313
|
+
);
|
|
314
|
+
assert.doesNotMatch(
|
|
315
|
+
verifyBody,
|
|
316
|
+
/schema:/,
|
|
317
|
+
'expected verifyHardeningChanges to pass no schema so its verdict fence stays as assistant text',
|
|
318
|
+
);
|
|
319
|
+
assert.match(
|
|
320
|
+
verifyBody,
|
|
321
|
+
/do no edits|make no edits|not edit|no file edits/i,
|
|
322
|
+
'expected verifyHardeningChanges to be told to make no edits',
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('verifyFixesInWorkingTree and verifyRepairChanges pass input.owner, input.repo, input.prNumber to buildVerdictFenceSteps', () => {
|
|
327
|
+
for (const verifyFunctionName of ['verifyFixesInWorkingTree', 'verifyRepairChanges']) {
|
|
328
|
+
const verifyBody = lensPromptBody(verifyFunctionName);
|
|
329
|
+
assert.match(
|
|
330
|
+
verifyBody,
|
|
331
|
+
/buildVerdictFenceSteps\(input\.owner,\s*input\.repo,\s*input\.prNumber\)/,
|
|
332
|
+
`expected ${verifyFunctionName} to pass PR coordinates to buildVerdictFenceSteps`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
273
337
|
test('the commit step is instructed to make no further file edits', () => {
|
|
274
338
|
const commitBody = lensPromptBody('commitVerifiedFixes');
|
|
275
339
|
assert.match(
|