claude-dev-env 2.2.0 → 2.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.
Files changed (60) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/advisor/advisor-protocol.md +2 -2
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
  8. package/_shared/pr-loop/scripts/pyproject.toml +22 -0
  9. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
  10. package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
  11. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  12. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
  13. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
  14. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
  15. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
  16. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
  17. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
  18. package/_shared/pr-loop/worker-spawn.md +1 -1
  19. package/agents/code-verifier.md +1 -1
  20. package/hooks/blocking/code_rules_shared.py +4 -2
  21. package/hooks/blocking/config/verified_commit_constants.py +11 -0
  22. package/hooks/blocking/pii_payload_scan.py +78 -20
  23. package/hooks/blocking/pii_prevention_blocker.py +3 -1
  24. package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
  25. package/hooks/blocking/test_pii_write_surface_ephemeral.py +221 -0
  26. package/hooks/blocking/test_verification_verdict_store.py +93 -0
  27. package/hooks/blocking/verification_verdict_store.py +91 -0
  28. package/hooks/validators/ruff_integration.py +2 -1
  29. package/hooks/validators/run_all_validators.py +460 -24
  30. package/hooks/validators/test_ruff_integration.py +21 -0
  31. package/hooks/validators/test_run_all_validators_pretooluse.py +223 -3
  32. package/package.json +1 -1
  33. package/rules/CLAUDE.md +1 -0
  34. package/rules/cleanup-command-forms.md +23 -0
  35. package/scripts/CLAUDE.md +2 -1
  36. package/scripts/check.ps1 +32 -6
  37. package/scripts/claude-chain.example.json +15 -3
  38. package/scripts/claude_chain_runner.py +130 -27
  39. package/scripts/claude_chain_usage.py +346 -0
  40. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
  41. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +13 -1
  42. package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
  43. package/scripts/dev_env_scripts_constants/code_review_constants.py +1 -1
  44. package/scripts/dev_env_scripts_constants/timing.py +1 -1
  45. package/scripts/test_claude_chain_runner.py +412 -6
  46. package/scripts/test_claude_chain_usage.py +534 -0
  47. package/skills/orchestrator/SKILL.md +1 -20
  48. package/skills/orchestrator-refresh/SKILL.md +1 -1
  49. package/skills/pr-converge/SKILL.md +3 -3
  50. package/skills/pr-converge/reference/examples.md +3 -3
  51. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  52. package/skills/pr-converge/reference/ground-rules.md +3 -3
  53. package/skills/pr-converge/reference/per-tick.md +5 -5
  54. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  55. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  56. package/skills/team-advisor/SKILL.md +3 -2
  57. package/skills/usage-pause/SKILL.md +5 -4
  58. package/skills/usage-pause/scripts/resolve_usage_window.py +68 -13
  59. package/skills/usage-pause/scripts/test_resolve_usage_window.py +121 -9
  60. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +7 -3
@@ -329,7 +329,9 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
329
329
  raw_tool_input = payload_by_key.get("tool_input", {})
330
330
  all_tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
331
331
  if tool_name in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
332
- return evaluate_write_edit_payload(tool_name, all_tool_input)
332
+ return evaluate_write_edit_payload(
333
+ tool_name, all_tool_input, hook_payload=payload_by_key
334
+ )
333
335
  if tool_name in ALL_SHELL_TOOL_NAMES:
334
336
  return _evaluate_shell_tool(all_tool_input, payload_by_key)
335
337
  if tool_name.startswith(MCP_GITHUB_TOOL_PREFIX):
@@ -0,0 +1,28 @@
1
+ """Contract: code-verifier cannot message a warm session-advisor."""
2
+
3
+ from pathlib import Path
4
+
5
+ _PACKAGE_ROOT = Path(__file__).resolve().parents[2]
6
+ _CODE_VERIFIER_AGENT_PATH = _PACKAGE_ROOT / "agents" / "code-verifier.md"
7
+ _FRONTMATTER_OPEN = "---\n"
8
+ _FRONTMATTER_CLOSE = "\n---\n"
9
+ _TOOLS_PREFIX = "tools:"
10
+ _SEND_MESSAGE_TOOL_NAME = "SendMessage"
11
+
12
+
13
+ def test_code_verifier_tools_exclude_sendmessage() -> None:
14
+ """tools frontmatter must not list SendMessage (warm-advisor path closed)."""
15
+ agent_text = _CODE_VERIFIER_AGENT_PATH.read_text(encoding="utf-8")
16
+ assert agent_text.startswith(_FRONTMATTER_OPEN), "missing frontmatter open"
17
+ frontmatter_end = agent_text.find(_FRONTMATTER_CLOSE, len(_FRONTMATTER_OPEN))
18
+ assert frontmatter_end > 0, "missing frontmatter close"
19
+ frontmatter_body = agent_text[len(_FRONTMATTER_OPEN) : frontmatter_end]
20
+ all_tools_lines = [
21
+ each_line
22
+ for each_line in frontmatter_body.splitlines()
23
+ if each_line.startswith(_TOOLS_PREFIX)
24
+ ]
25
+ assert len(all_tools_lines) == 1, all_tools_lines
26
+ tools_value = all_tools_lines[0].removeprefix(_TOOLS_PREFIX).strip()
27
+ all_tool_names = {each_name.strip() for each_name in tools_value.split(",")}
28
+ assert _SEND_MESSAGE_TOOL_NAME not in all_tool_names, all_tool_names
@@ -0,0 +1,221 @@
1
+ """Behavior tests for the ephemeral-path skip on the Write/Edit PII surface.
2
+
3
+ Sub-defect (ii): the Write/Edit surface scanned files under an ephemeral scratch
4
+ root that sit outside every git repository. Such a draft only becomes durable
5
+ through ``gh --body-file`` or a staged commit, and each of those surfaces keeps
6
+ its own PII scan, so scanning the draft at write time is a pure false positive.
7
+
8
+ The write surface consults the shared ``is_ephemeral_path`` predicate directly.
9
+ These tests drive that real predicate on genuine ephemeral shapes: a harness
10
+ session scratchpad under the user temp directory, and a ``CLAUDE_JOB_DIR``
11
+ scratch tree. A write inside a git repository keeps full scanning, and the
12
+ durable-post and staged-commit surfaces stay gated even from ephemeral sources.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import uuid
21
+ from collections.abc import Iterator
22
+ from pathlib import Path
23
+
24
+ import pytest
25
+
26
+ _HOOK_DIR = Path(__file__).parent
27
+ _HOOKS_DIR = _HOOK_DIR.parent
28
+ if str(_HOOK_DIR) not in sys.path:
29
+ sys.path.insert(0, str(_HOOK_DIR))
30
+ if str(_HOOKS_DIR) not in sys.path:
31
+ sys.path.insert(0, str(_HOOKS_DIR))
32
+
33
+ import tempfile # noqa: E402
34
+
35
+ from pii_payload_scan import evaluate_write_edit_payload # noqa: E402
36
+ from pii_prevention_blocker import evaluate, evaluate_bash_command # noqa: E402
37
+
38
+ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
39
+ CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME,
40
+ CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY,
41
+ EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME,
42
+ )
43
+ from hooks_constants.harness_scratchpad_constants import ( # noqa: E402
44
+ HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
45
+ HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME,
46
+ HOOK_PAYLOAD_SESSION_ID_KEY,
47
+ )
48
+
49
+
50
+ def _real_pii_email() -> str:
51
+ return "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
52
+
53
+
54
+ def _bot_trailer_address() -> str:
55
+ return "noreply" + "@" + "anthropic" + ".com"
56
+
57
+
58
+ def _write_deny_reason(
59
+ target_path: Path,
60
+ sensitive_text: str,
61
+ hook_payload: dict[str, object] | None = None,
62
+ ) -> str | None:
63
+ content = "owner contact " + sensitive_text + "\n"
64
+ return evaluate_write_edit_payload(
65
+ "Write",
66
+ {"file_path": str(target_path), "content": content},
67
+ hook_payload=hook_payload,
68
+ )
69
+
70
+
71
+ def _enable_job_dir_ephemeral_root(
72
+ monkeypatch: pytest.MonkeyPatch, job_directory: Path
73
+ ) -> Path:
74
+ monkeypatch.delenv(EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME, raising=False)
75
+ monkeypatch.setenv(CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME, str(job_directory))
76
+ ephemeral_root = job_directory / CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY
77
+ ephemeral_root.mkdir(parents=True, exist_ok=True)
78
+ return ephemeral_root
79
+
80
+
81
+ @pytest.fixture
82
+ def harness_scratchpad() -> Iterator[tuple[Path, str]]:
83
+ session_id = "session-" + uuid.uuid4().hex
84
+ mangled_working_directory = "cwd-" + uuid.uuid4().hex
85
+ temp_root = Path(tempfile.gettempdir())
86
+ user_root = temp_root / HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME
87
+ scratchpad_root = (
88
+ user_root
89
+ / mangled_working_directory
90
+ / session_id
91
+ / HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME
92
+ )
93
+ scratchpad_root.mkdir(parents=True, exist_ok=True)
94
+ try:
95
+ yield scratchpad_root, session_id
96
+ finally:
97
+ try:
98
+ shutil.rmtree(user_root / mangled_working_directory)
99
+ except OSError:
100
+ pass
101
+
102
+
103
+ def _init_repo_with_github_origin(repository_root: Path, origin_slug: str) -> None:
104
+ repository_root.mkdir(parents=True, exist_ok=True)
105
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
106
+ origin_url = "https://github.com/" + origin_slug + ".git"
107
+ subprocess.run(
108
+ ["git", "remote", "add", "origin", origin_url],
109
+ cwd=repository_root,
110
+ check=True,
111
+ )
112
+
113
+
114
+ def _init_repo_with_staged_email(repository_root: Path, staged_email: str) -> None:
115
+ repository_root.mkdir(parents=True, exist_ok=True)
116
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
117
+ subprocess.run(
118
+ ["git", "config", "user.email", "dev@example.com"],
119
+ cwd=repository_root,
120
+ check=True,
121
+ )
122
+ subprocess.run(
123
+ ["git", "config", "user.name", "Fixture Dev"],
124
+ cwd=repository_root,
125
+ check=True,
126
+ )
127
+ tracked_file = repository_root / "notes.md"
128
+ tracked_file.write_text("owner email " + staged_email + "\n", encoding="utf-8")
129
+ subprocess.run(["git", "add", "notes.md"], cwd=repository_root, check=True)
130
+
131
+
132
+ def test_windows_shaped_scratchpad_write_is_skipped(
133
+ harness_scratchpad: tuple[Path, str],
134
+ ) -> None:
135
+ scratchpad_root, session_id = harness_scratchpad
136
+ scratch_target = scratchpad_root / "blocker-matrix.txt"
137
+ payload = {HOOK_PAYLOAD_SESSION_ID_KEY: session_id}
138
+ assert _write_deny_reason(scratch_target, _bot_trailer_address(), payload) is None
139
+
140
+
141
+ def test_write_with_no_scannable_texts_returns_none(tmp_path: Path) -> None:
142
+ target_path = tmp_path / "empty-write.md"
143
+ assert (
144
+ evaluate_write_edit_payload(
145
+ "Write",
146
+ {"file_path": str(target_path), "content": ""},
147
+ )
148
+ is None
149
+ )
150
+
151
+
152
+ def test_entry_point_threads_payload_session_id_to_scratchpad_skip(
153
+ monkeypatch: pytest.MonkeyPatch,
154
+ harness_scratchpad: tuple[Path, str],
155
+ ) -> None:
156
+ scratchpad_root, session_id = harness_scratchpad
157
+ monkeypatch.delenv("CLAUDE_CODE_SESSION_ID", raising=False)
158
+ scratch_target = scratchpad_root / "blocker-matrix.txt"
159
+ content = "owner contact " + _real_pii_email() + "\n"
160
+ payload = {
161
+ "tool_name": "Write",
162
+ "tool_input": {"file_path": str(scratch_target), "content": content},
163
+ HOOK_PAYLOAD_SESSION_ID_KEY: session_id,
164
+ }
165
+ assert evaluate(payload) is None
166
+
167
+
168
+ def test_job_dir_ephemeral_write_outside_repo_is_skipped(
169
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
170
+ ) -> None:
171
+ ephemeral_root = _enable_job_dir_ephemeral_root(monkeypatch, tmp_path)
172
+ scratch_target = ephemeral_root / "drafts" / "draft.md"
173
+ scratch_target.parent.mkdir(parents=True)
174
+ assert _write_deny_reason(scratch_target, _real_pii_email()) is None
175
+
176
+
177
+ def test_write_inside_ordinary_repository_is_still_denied(
178
+ tmp_path: Path,
179
+ ) -> None:
180
+ repository_root = tmp_path / "repo"
181
+ _init_repo_with_github_origin(repository_root, "SomeOwner/some-repo")
182
+ deny_reason = _write_deny_reason(repository_root / "notes.md", _real_pii_email())
183
+ assert deny_reason is not None
184
+ assert "email" in deny_reason
185
+
186
+
187
+ def test_ephemeral_path_inside_a_repository_is_still_denied(
188
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
189
+ ) -> None:
190
+ ephemeral_root = _enable_job_dir_ephemeral_root(monkeypatch, tmp_path)
191
+ repository_root = ephemeral_root / "repo"
192
+ _init_repo_with_github_origin(repository_root, "SomeOwner/some-repo")
193
+ deny_reason = _write_deny_reason(repository_root / "notes.md", _real_pii_email())
194
+ assert deny_reason is not None
195
+ assert "email" in deny_reason
196
+
197
+
198
+ def test_gh_body_file_from_ephemeral_path_is_still_scanned(
199
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
200
+ ) -> None:
201
+ ephemeral_root = _enable_job_dir_ephemeral_root(monkeypatch, tmp_path)
202
+ body_path = ephemeral_root / "pr_body.md"
203
+ body_path.write_text("contact " + _real_pii_email() + "\n", encoding="utf-8")
204
+ command = 'gh pr comment 12 --body-file "' + str(body_path) + '"'
205
+ deny_reason = evaluate_bash_command(command, working_directory=None)
206
+ assert deny_reason is not None
207
+ assert "email" in deny_reason
208
+
209
+
210
+ def test_staged_commit_from_ephemeral_tree_is_still_scanned(
211
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
212
+ ) -> None:
213
+ ephemeral_root = _enable_job_dir_ephemeral_root(monkeypatch, tmp_path)
214
+ repository_root = ephemeral_root / "repo"
215
+ _init_repo_with_staged_email(repository_root, _real_pii_email())
216
+ deny_reason = evaluate_bash_command(
217
+ "git commit -m test", working_directory=str(repository_root)
218
+ )
219
+ assert deny_reason is not None
220
+ assert "email" in deny_reason
221
+ assert "staged commit" in deny_reason
@@ -808,3 +808,96 @@ def test_store_imports_under_foreign_config_shadow(
808
808
  )
809
809
  assert completed_probe.returncode == 0, completed_probe.stderr
810
810
  assert "IMPORT_OK" in completed_probe.stdout
811
+
812
+
813
+ def _ledger_path(fake_home: pathlib.Path) -> pathlib.Path:
814
+ return (
815
+ fake_home
816
+ / constants_module.CLAUDE_HOME_DIRECTORY_NAME
817
+ / constants_module.LOGS_DIRECTORY_NAME
818
+ / constants_module.VERIFIER_VERDICTS_JSONL_FILENAME
819
+ )
820
+
821
+
822
+ def _read_ledger_rows(fake_home: pathlib.Path) -> list[dict]:
823
+ ledger_file = _ledger_path(fake_home)
824
+ all_rows: list[dict] = []
825
+ for each_line in ledger_file.read_text(encoding="utf-8").splitlines():
826
+ if each_line.strip():
827
+ all_rows.append(json.loads(each_line))
828
+ return all_rows
829
+
830
+
831
+ def test_write_verdict_appends_failing_row_that_survives_later_clean_mint(
832
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
833
+ ) -> None:
834
+ """A failing mint's check names stay readable after a later clean mint."""
835
+ fake_home = tmp_path / "home"
836
+ fake_home.mkdir()
837
+ _isolate_home(monkeypatch, fake_home)
838
+ monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
839
+ work_dir = _make_repo_with_origin(tmp_path)
840
+ failing_findings = [
841
+ {"check": "gates.import", "detail": "ImportError: boom"},
842
+ {"check": "task.item-2", "detail": "missing hunk"},
843
+ ]
844
+ write_verdict(
845
+ str(work_dir),
846
+ MATCHING_MANIFEST_SHA256,
847
+ False,
848
+ failing_findings,
849
+ "agent-fail",
850
+ )
851
+ write_verdict(
852
+ str(work_dir),
853
+ MATCHING_MANIFEST_SHA256,
854
+ True,
855
+ [],
856
+ "agent-pass",
857
+ )
858
+ all_rows = _read_ledger_rows(fake_home)
859
+ assert len(all_rows) == 2
860
+ failing_row = all_rows[0]
861
+ clean_row = all_rows[1]
862
+ assert failing_row[constants_module.LEDGER_KEY_ALL_PASS] is False
863
+ assert failing_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 2
864
+ assert failing_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == [
865
+ "gates.import",
866
+ "task.item-2",
867
+ ]
868
+ assert failing_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-fail"
869
+ assert failing_row[constants_module.LEDGER_KEY_MANIFEST_HASH] == MATCHING_MANIFEST_SHA256
870
+ assert failing_row[constants_module.LEDGER_KEY_REPO_ROOT] == str(work_dir)
871
+ assert failing_row[constants_module.LEDGER_KEY_BRANCH] == "main"
872
+ assert clean_row[constants_module.LEDGER_KEY_ALL_PASS] is True
873
+ assert clean_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 0
874
+ assert clean_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == []
875
+ assert clean_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-pass"
876
+
877
+
878
+ def test_write_verdict_returns_path_when_ledger_append_fails(
879
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
880
+ ) -> None:
881
+ """A ledger IO failure never changes the mint path or verdict file write."""
882
+ fake_home = tmp_path / "home"
883
+ fake_home.mkdir()
884
+ _isolate_home(monkeypatch, fake_home)
885
+ monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
886
+ logs_as_file = (
887
+ fake_home
888
+ / constants_module.CLAUDE_HOME_DIRECTORY_NAME
889
+ / constants_module.LOGS_DIRECTORY_NAME
890
+ )
891
+ logs_as_file.parent.mkdir(parents=True, exist_ok=True)
892
+ logs_as_file.write_text("not-a-directory", encoding="utf-8")
893
+ work_dir = _make_repo_with_origin(tmp_path)
894
+ verdict_file = write_verdict(
895
+ str(work_dir),
896
+ MATCHING_MANIFEST_SHA256,
897
+ True,
898
+ [],
899
+ "agent-x",
900
+ )
901
+ assert verdict_file.is_file()
902
+ verdict_record = json.loads(verdict_file.read_text(encoding="utf-8"))
903
+ assert verdict_record[constants_module.VERDICT_KEY_ALL_PASS] is True
@@ -13,6 +13,7 @@ and reading/writing verdict files.
13
13
  from __future__ import annotations
14
14
 
15
15
  import ast
16
+ import datetime
16
17
  import hashlib
17
18
  import json
18
19
  import re
@@ -20,6 +21,7 @@ import subprocess
20
21
  import sys
21
22
  import time
22
23
  from pathlib import Path
24
+ from collections.abc import Mapping
23
25
 
24
26
  blocking_directory = str(Path(__file__).resolve().parent)
25
27
  if blocking_directory not in sys.path:
@@ -45,6 +47,15 @@ from config.verified_commit_constants import (
45
47
  ALL_FALLBACK_BASE_REFERENCES,
46
48
  EMPTY_SURFACE_GUARD_MESSAGE,
47
49
  GIT_TIMEOUT_SECONDS,
50
+ LEDGER_KEY_ALL_PASS,
51
+ LEDGER_KEY_BRANCH,
52
+ LEDGER_KEY_FINDING_CHECK_NAMES,
53
+ LEDGER_KEY_FINDING_COUNT,
54
+ LEDGER_KEY_MANIFEST_HASH,
55
+ LEDGER_KEY_MINTED_FROM_AGENT_ID,
56
+ LEDGER_KEY_REPO_ROOT,
57
+ LEDGER_KEY_TIMESTAMP,
58
+ LOGS_DIRECTORY_NAME,
48
59
  MANIFEST_HASH_CLI_FLAG,
49
60
  MANIFEST_HASH_FOR_BRANCH_CLI_FLAG,
50
61
  MINIMUM_STATUS_FIELD_COUNT,
@@ -65,10 +76,12 @@ from config.verified_commit_constants import (
65
76
  VERDICT_DIRECTORY_NAME,
66
77
  VERDICT_FENCE_PATTERN,
67
78
  VERDICT_FILE_GLOB,
79
+ VERDICT_FINDING_CHECK_KEY,
68
80
  VERDICT_JSON_INDENT,
69
81
  VERDICT_KEY_ALL_PASS,
70
82
  VERDICT_KEY_FINDINGS,
71
83
  VERDICT_KEY_MANIFEST_SHA256,
84
+ VERIFIER_VERDICTS_JSONL_FILENAME,
72
85
  WORKTREE_LIST_BRANCH_PREFIX,
73
86
  WORKTREE_LIST_PATH_PREFIX,
74
87
  )
@@ -654,6 +667,74 @@ def workflow_verdict_covers_surface(
654
667
  return False
655
668
 
656
669
 
670
+ def _finding_check_names(all_findings: list) -> list[str]:
671
+ """Collect the ``check`` name from each finding mapping that carries one.
672
+
673
+ Args:
674
+ all_findings: The verifier's findings list (empty when clean).
675
+
676
+ Returns:
677
+ Ordered list of non-empty check name strings; entries without a
678
+ string ``check`` field are omitted.
679
+ """
680
+ all_check_names: list[str] = []
681
+ for each_finding in all_findings:
682
+ if not isinstance(each_finding, Mapping):
683
+ continue
684
+ check_name = each_finding.get(VERDICT_FINDING_CHECK_KEY)
685
+ if isinstance(check_name, str) and check_name:
686
+ all_check_names.append(check_name)
687
+ return all_check_names
688
+
689
+
690
+ def _append_verdict_ledger_row(
691
+ repo_root: str,
692
+ bound_manifest_sha256: str,
693
+ is_all_pass: bool,
694
+ all_findings: list,
695
+ minted_from_agent_id: str,
696
+ ) -> None:
697
+ """Append one fail-safe JSONL row for a minted verdict.
698
+
699
+ Writes to ``~/.claude/logs/verifier-verdicts.jsonl``. Swallows home
700
+ resolution and IO errors so a ledger failure never changes the mint.
701
+
702
+ Args:
703
+ repo_root: The repository top-level directory.
704
+ bound_manifest_sha256: Hash of the surface the verdict covers.
705
+ is_all_pass: Whether the verifier reported a clean verdict.
706
+ all_findings: The verifier's findings list (empty when clean).
707
+ minted_from_agent_id: The subagent invocation id, kept for audit.
708
+ """
709
+ try:
710
+ home_directory = Path.home()
711
+ except RuntimeError:
712
+ return
713
+ try:
714
+ ledger_path = (
715
+ home_directory
716
+ / CLAUDE_HOME_DIRECTORY_NAME
717
+ / LOGS_DIRECTORY_NAME
718
+ / VERIFIER_VERDICTS_JSONL_FILENAME
719
+ )
720
+ ledger_path.parent.mkdir(parents=True, exist_ok=True)
721
+ branch_name = _head_branch_name(repo_root)
722
+ ledger_row = {
723
+ LEDGER_KEY_TIMESTAMP: datetime.datetime.now(datetime.timezone.utc).isoformat(),
724
+ LEDGER_KEY_REPO_ROOT: repo_root,
725
+ LEDGER_KEY_BRANCH: branch_name,
726
+ LEDGER_KEY_MANIFEST_HASH: bound_manifest_sha256,
727
+ LEDGER_KEY_ALL_PASS: is_all_pass,
728
+ LEDGER_KEY_FINDING_COUNT: len(all_findings),
729
+ LEDGER_KEY_FINDING_CHECK_NAMES: _finding_check_names(all_findings),
730
+ LEDGER_KEY_MINTED_FROM_AGENT_ID: minted_from_agent_id,
731
+ }
732
+ with ledger_path.open("a", encoding="utf-8") as ledger_file:
733
+ ledger_file.write(json.dumps(ledger_row) + "\n")
734
+ except OSError:
735
+ return
736
+
737
+
657
738
  def write_verdict(
658
739
  repo_root: str,
659
740
  bound_manifest_sha256: str,
@@ -663,6 +744,9 @@ def write_verdict(
663
744
  ) -> Path:
664
745
  """Write a verdict file binding a verification outcome to a surface hash.
665
746
 
747
+ Also appends one durable JSONL ledger row under the Claude logs directory.
748
+ A ledger write failure never changes the mint outcome.
749
+
666
750
  Args:
667
751
  repo_root: The repository top-level directory.
668
752
  bound_manifest_sha256: Hash of the surface manifest the verdict covers.
@@ -686,6 +770,13 @@ def write_verdict(
686
770
  verdict_file.write_text(
687
771
  json.dumps(verdict_record, indent=VERDICT_JSON_INDENT), encoding="utf-8"
688
772
  )
773
+ _append_verdict_ledger_row(
774
+ repo_root,
775
+ bound_manifest_sha256,
776
+ is_all_pass,
777
+ all_findings,
778
+ minted_from_agent_id,
779
+ )
689
780
  return verdict_file
690
781
 
691
782
 
@@ -38,8 +38,9 @@ def run_ruff_check(files: list[Path]) -> RuffResult:
38
38
  if not py_files:
39
39
  return RuffResult(passed=True, output="No Python files", fixed_count=0)
40
40
 
41
+ concise_output_arguments = ["--output-format", "concise"]
41
42
  result = subprocess.run(
42
- ["ruff", "check"] + py_files,
43
+ ["ruff", "check", *concise_output_arguments] + py_files,
43
44
  check=False,
44
45
  capture_output=True,
45
46
  text=True,