claude-dev-env 1.60.0 → 1.61.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/CLAUDE.md +4 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
- package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/code_rules_annotations_length.py +189 -10
- package/hooks/blocking/code_rules_enforcer.py +8 -0
- package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
- package/hooks/blocking/config/verified_commit_constants.py +14 -2
- package/hooks/blocking/destructive_command_blocker.py +483 -61
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
- package/hooks/blocking/test_destructive_command_blocker.py +213 -0
- package/hooks/blocking/test_verification_verdict_store.py +212 -0
- package/hooks/blocking/test_verified_commit_gate.py +127 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +74 -95
- package/hooks/blocking/verification_verdict_store.py +240 -0
- package/hooks/blocking/verified_commit_gate.py +20 -8
- package/hooks/blocking/verifier_verdict_minter.py +46 -124
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
- package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
- package/hooks/validation/mypy_validator.py +59 -7
- package/hooks/validation/test_mypy_validator.py +94 -0
- package/package.json +1 -1
- package/rules/orphan-css-class.md +23 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +202 -13
- package/skills/autoconverge/workflow/converge.mjs +392 -51
- package/skills/autoconverge/workflow/test_render_report.py +30 -0
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
"""Tests for the agent-type gate in verifier_verdict_minter.
|
|
2
2
|
|
|
3
|
-
The minter mints a verdict only for a code-verifier stop event. The
|
|
4
|
-
SubagentStop payload names the stopping subagent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
minter
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
can mint a passing verdict.
|
|
3
|
+
The minter mints a verdict only for a code-verifier stop event. The
|
|
4
|
+
SubagentStop payload names the stopping subagent's own transcript
|
|
5
|
+
(``agent_transcript_path``), which sits beside a harness-written
|
|
6
|
+
``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. These
|
|
7
|
+
tests build that sidecar and assert the minter gates on the resolved type and
|
|
8
|
+
on the shared MINTING_AGENT_TYPE constant, so a rename in config propagates to
|
|
9
|
+
the minter without a second edit. A malformed or non-string sidecar resolves
|
|
10
|
+
nothing, and an absent sidecar mints nothing — the main session writes neither
|
|
11
|
+
the transcript nor the sidecar, so it cannot forge a passing verdict. A
|
|
12
|
+
further test holds the shipped settings.json to the minter docstring's
|
|
13
|
+
anti-forgery claim: the main session is denied writes to the verdict
|
|
14
|
+
directory, so only this hook can mint a passing verdict.
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
import importlib.util
|
|
@@ -21,8 +20,6 @@ import pathlib
|
|
|
21
20
|
import subprocess
|
|
22
21
|
import sys
|
|
23
22
|
|
|
24
|
-
import pytest
|
|
25
|
-
|
|
26
23
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
27
24
|
if str(_HOOK_DIR) not in sys.path:
|
|
28
25
|
sys.path.insert(0, str(_HOOK_DIR))
|
|
@@ -51,100 +48,83 @@ constants_spec.loader.exec_module(constants_module)
|
|
|
51
48
|
MINTING_AGENT_TYPE = constants_module.MINTING_AGENT_TYPE
|
|
52
49
|
|
|
53
50
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"type": "tool_use",
|
|
61
|
-
"name": "Task",
|
|
62
|
-
"input": {"subagent_type": agent_type, "description": "Verify"},
|
|
63
|
-
"agentId": agent_id,
|
|
64
|
-
"agentType": agent_type,
|
|
65
|
-
"content": [{"type": "text", "text": "verification complete"}],
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
transcript_file.write_text(json.dumps(spawn_record) + "\n", encoding="utf-8")
|
|
51
|
+
def _write_sidecar(agent_transcript_file: pathlib.Path, agent_type: str) -> None:
|
|
52
|
+
sidecar_file = agent_transcript_file.with_name(f"{agent_transcript_file.stem}.meta.json")
|
|
53
|
+
sidecar_file.write_text(
|
|
54
|
+
json.dumps({"agentType": agent_type, "description": "Verify"}) + "\n",
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
)
|
|
71
57
|
|
|
72
58
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
def test_resolves_subagent_type_from_sidecar(tmp_path: pathlib.Path) -> None:
|
|
60
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
61
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
62
|
+
_write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
|
|
63
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
77
64
|
assert resolved_subagent_type(payload) == MINTING_AGENT_TYPE
|
|
78
65
|
|
|
79
66
|
|
|
80
|
-
def
|
|
81
|
-
tmp_path
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
_write_parent_transcript(transcript_file, "agent-7", MINTING_AGENT_TYPE)
|
|
85
|
-
payload = {"agent_id": "different-agent", "transcript_path": str(transcript_file)}
|
|
67
|
+
def test_resolves_none_when_sidecar_absent(tmp_path: pathlib.Path) -> None:
|
|
68
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
69
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
70
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
86
71
|
assert resolved_subagent_type(payload) is None
|
|
87
72
|
|
|
88
73
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
transcript_file = tmp_path / "parent.jsonl"
|
|
93
|
-
transcript_file.write_text("", encoding="utf-8")
|
|
74
|
+
def test_resolves_none_when_agent_transcript_path_empty() -> None:
|
|
75
|
+
assert resolved_subagent_type({"agent_transcript_path": ""}) is None
|
|
76
|
+
assert resolved_subagent_type({}) is None
|
|
94
77
|
|
|
95
|
-
def write_record_on_first_sleep(_seconds: float) -> None:
|
|
96
|
-
if transcript_file.read_text(encoding="utf-8"):
|
|
97
|
-
return
|
|
98
|
-
_write_parent_transcript(transcript_file, "agent-7", MINTING_AGENT_TYPE)
|
|
99
78
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
79
|
+
def test_resolves_none_when_sidecar_names_no_string_type(tmp_path: pathlib.Path) -> None:
|
|
80
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
81
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
82
|
+
sidecar_file = agent_transcript.with_name("agent-7.meta.json")
|
|
83
|
+
sidecar_file.write_text(json.dumps({"agentType": 123}), encoding="utf-8")
|
|
84
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
85
|
+
assert resolved_subagent_type(payload) is None
|
|
103
86
|
|
|
104
87
|
|
|
105
|
-
def
|
|
106
|
-
tmp_path
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
88
|
+
def test_unparseable_sidecar_resolves_nothing(tmp_path: pathlib.Path) -> None:
|
|
89
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
90
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
91
|
+
sidecar_file = agent_transcript.with_name("agent-7.meta.json")
|
|
92
|
+
sidecar_file.write_text("{not valid json", encoding="utf-8")
|
|
93
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
94
|
+
assert resolved_subagent_type(payload) is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_invalid_utf8_sidecar_resolves_nothing(tmp_path: pathlib.Path) -> None:
|
|
98
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
99
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
100
|
+
sidecar_file = agent_transcript.with_name("agent-7.meta.json")
|
|
101
|
+
sidecar_file.write_bytes(b'{"agentType": "\xff\xfe bad"}')
|
|
102
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
103
|
+
assert resolved_subagent_type(payload) is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_non_object_json_sidecar_resolves_nothing(tmp_path: pathlib.Path) -> None:
|
|
107
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
108
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
109
|
+
sidecar_file = agent_transcript.with_name("agent-7.meta.json")
|
|
110
|
+
sidecar_file.write_text(json.dumps(["agentType", "code-verifier"]), encoding="utf-8")
|
|
111
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
122
112
|
assert resolved_subagent_type(payload) is None
|
|
123
113
|
|
|
124
114
|
|
|
125
115
|
def test_non_verifier_agent_type_mints_nothing(tmp_path: pathlib.Path) -> None:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"transcript_path": str(transcript_file),
|
|
131
|
-
"agent_transcript_path": "",
|
|
132
|
-
"cwd": ".",
|
|
133
|
-
}
|
|
116
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
117
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
118
|
+
_write_sidecar(agent_transcript, "general-purpose")
|
|
119
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
134
120
|
assert mint_for_payload(payload) is None
|
|
135
121
|
|
|
136
122
|
|
|
137
|
-
def
|
|
138
|
-
tmp_path
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
payload = {
|
|
143
|
-
"agent_id": "agent-7",
|
|
144
|
-
"transcript_path": str(transcript_file),
|
|
145
|
-
"agent_transcript_path": "",
|
|
146
|
-
"cwd": ".",
|
|
147
|
-
}
|
|
123
|
+
def test_verifier_type_without_a_verdict_mints_nothing(tmp_path: pathlib.Path) -> None:
|
|
124
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
125
|
+
agent_transcript.write_text("", encoding="utf-8")
|
|
126
|
+
_write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
|
|
127
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
148
128
|
assert mint_for_payload(payload) is None
|
|
149
129
|
|
|
150
130
|
|
|
@@ -165,9 +145,7 @@ def test_clean_verifier_verdict_mints_a_verdict_file(tmp_path: pathlib.Path) ->
|
|
|
165
145
|
repo_root = tmp_path / "repo"
|
|
166
146
|
repo_root.mkdir()
|
|
167
147
|
_init_repo_with_upstream_and_edit(repo_root)
|
|
168
|
-
|
|
169
|
-
_write_parent_transcript(transcript_file, "agent-7", MINTING_AGENT_TYPE)
|
|
170
|
-
agent_transcript = tmp_path / "agent.jsonl"
|
|
148
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
171
149
|
agent_transcript.write_text(
|
|
172
150
|
json.dumps(
|
|
173
151
|
{
|
|
@@ -185,17 +163,18 @@ def test_clean_verifier_verdict_mints_a_verdict_file(tmp_path: pathlib.Path) ->
|
|
|
185
163
|
+ "\n",
|
|
186
164
|
encoding="utf-8",
|
|
187
165
|
)
|
|
166
|
+
_write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
|
|
188
167
|
payload = {
|
|
189
|
-
"agent_id": "agent-7",
|
|
190
|
-
"transcript_path": str(transcript_file),
|
|
191
168
|
"agent_transcript_path": str(agent_transcript),
|
|
192
169
|
"cwd": str(repo_root),
|
|
170
|
+
"agent_id": "a02b9583eedc74093",
|
|
193
171
|
}
|
|
194
172
|
verdict_path = mint_for_payload(payload)
|
|
195
173
|
try:
|
|
196
174
|
assert verdict_path is not None
|
|
197
175
|
verdict_record = json.loads(verdict_path.read_text(encoding="utf-8"))
|
|
198
176
|
assert verdict_record["all_pass"] is True
|
|
177
|
+
assert verdict_record["minted_from_agent_id"] == "a02b9583eedc74093"
|
|
199
178
|
finally:
|
|
200
179
|
if verdict_path is not None and verdict_path.exists():
|
|
201
180
|
verdict_path.unlink()
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import ast
|
|
16
16
|
import hashlib
|
|
17
17
|
import json
|
|
18
|
+
import re
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
20
21
|
import time
|
|
@@ -25,20 +26,35 @@ if blocking_directory not in sys.path:
|
|
|
25
26
|
sys.path.insert(0, blocking_directory)
|
|
26
27
|
|
|
27
28
|
from config.verified_commit_constants import (
|
|
29
|
+
AGENT_META_SIDECAR_SUFFIX,
|
|
30
|
+
AGENT_META_TYPE_KEY,
|
|
31
|
+
AGENT_TRANSCRIPT_GLOB,
|
|
28
32
|
CLAUDE_HOME_DIRECTORY_NAME,
|
|
29
33
|
CONFTEST_FILE_NAME,
|
|
30
34
|
DOCS_ONLY_EXTENSIONS,
|
|
31
35
|
ALL_FALLBACK_BASE_REFERENCES,
|
|
32
36
|
GIT_TIMEOUT_SECONDS,
|
|
37
|
+
MANIFEST_HASH_CLI_FLAG,
|
|
33
38
|
MINIMUM_STATUS_FIELD_COUNT,
|
|
39
|
+
MINTING_AGENT_TYPE,
|
|
34
40
|
PYTHON_EXTENSION,
|
|
35
41
|
ROOT_KEY_HEX_LENGTH,
|
|
42
|
+
SUBAGENTS_DIRECTORY_NAME,
|
|
36
43
|
TEST_FILE_PREFIX,
|
|
37
44
|
TEST_FILE_SUFFIX,
|
|
38
45
|
ALL_TOOLING_STATE_PREFIXES,
|
|
46
|
+
TRANSCRIPT_ASSISTANT_ENTRY_TYPE,
|
|
47
|
+
TRANSCRIPT_CONTENT_KEY,
|
|
48
|
+
TRANSCRIPT_CONTENT_TYPE_KEY,
|
|
49
|
+
TRANSCRIPT_ENTRY_TYPE_KEY,
|
|
50
|
+
TRANSCRIPT_MESSAGE_KEY,
|
|
51
|
+
TRANSCRIPT_TEXT_CONTENT_TYPE,
|
|
52
|
+
TRANSCRIPT_TEXT_KEY,
|
|
39
53
|
VERDICT_DIRECTORY_NAME,
|
|
54
|
+
VERDICT_FENCE_PATTERN,
|
|
40
55
|
VERDICT_JSON_INDENT,
|
|
41
56
|
VERDICT_KEY_ALL_PASS,
|
|
57
|
+
VERDICT_KEY_FINDINGS,
|
|
42
58
|
VERDICT_KEY_MANIFEST_SHA256,
|
|
43
59
|
)
|
|
44
60
|
|
|
@@ -273,6 +289,187 @@ def load_valid_verdict(repo_root: str, expected_manifest_sha256: str) -> dict |
|
|
|
273
289
|
return verdict_record
|
|
274
290
|
|
|
275
291
|
|
|
292
|
+
def _subagents_directory_for_transcript(transcript_path: str) -> Path | None:
|
|
293
|
+
"""Locate the live session's subagents directory from a transcript path.
|
|
294
|
+
|
|
295
|
+
Handles both transcript shapes the runtime produces: a transcript already
|
|
296
|
+
inside a ``.../subagents/...`` tree resolves to its nearest ancestor named
|
|
297
|
+
``subagents``; a session transcript ``<dir>/<session-id>.jsonl`` resolves
|
|
298
|
+
to ``<dir>/<session-id>/subagents``.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
transcript_path: The live session's transcript path from the payload.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The existing subagents directory, or None when neither shape yields
|
|
305
|
+
an existing directory.
|
|
306
|
+
"""
|
|
307
|
+
if not transcript_path:
|
|
308
|
+
return None
|
|
309
|
+
transcript_file = Path(transcript_path)
|
|
310
|
+
for each_ancestor in transcript_file.parents:
|
|
311
|
+
if each_ancestor.name == SUBAGENTS_DIRECTORY_NAME and each_ancestor.is_dir():
|
|
312
|
+
return each_ancestor
|
|
313
|
+
session_subagents_directory = (
|
|
314
|
+
transcript_file.with_suffix("") / SUBAGENTS_DIRECTORY_NAME
|
|
315
|
+
)
|
|
316
|
+
if session_subagents_directory.is_dir():
|
|
317
|
+
return session_subagents_directory
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _agent_type_for_transcript(transcript_file: Path) -> str | None:
|
|
322
|
+
"""Read an agent transcript's sidecar to learn the agent type it ran as.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
transcript_file: An ``agent-*.jsonl`` transcript path.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or
|
|
329
|
+
None when the sidecar is missing, unreadable, or carries no type.
|
|
330
|
+
"""
|
|
331
|
+
sidecar_file = transcript_file.with_suffix(AGENT_META_SIDECAR_SUFFIX)
|
|
332
|
+
try:
|
|
333
|
+
sidecar_record = json.loads(sidecar_file.read_text(encoding="utf-8"))
|
|
334
|
+
except (OSError, json.JSONDecodeError):
|
|
335
|
+
return None
|
|
336
|
+
if not isinstance(sidecar_record, dict):
|
|
337
|
+
return None
|
|
338
|
+
recorded_agent_type = sidecar_record.get(AGENT_META_TYPE_KEY)
|
|
339
|
+
return recorded_agent_type if isinstance(recorded_agent_type, str) else None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _assistant_text_blocks(transcript_file: Path) -> list[str]:
|
|
343
|
+
"""Collect every assistant text block from an agent transcript.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
transcript_file: An ``agent-*.jsonl`` transcript path.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The text of each assistant message content block, in order; empty
|
|
350
|
+
when the file is missing, unreadable, or holds no assistant text.
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
transcript_lines = transcript_file.read_text(encoding="utf-8").splitlines()
|
|
354
|
+
except OSError:
|
|
355
|
+
return []
|
|
356
|
+
all_text_blocks: list[str] = []
|
|
357
|
+
for each_line in transcript_lines:
|
|
358
|
+
if not each_line.strip():
|
|
359
|
+
continue
|
|
360
|
+
try:
|
|
361
|
+
transcript_entry = json.loads(each_line)
|
|
362
|
+
except json.JSONDecodeError:
|
|
363
|
+
continue
|
|
364
|
+
all_text_blocks.extend(_entry_text_blocks(transcript_entry))
|
|
365
|
+
return all_text_blocks
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _entry_text_blocks(transcript_entry: object) -> list[str]:
|
|
369
|
+
"""Extract assistant text from one parsed transcript entry.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
transcript_entry: One parsed JSONL transcript entry.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
The text of each text content block on an assistant entry, in order;
|
|
376
|
+
empty for any other entry shape.
|
|
377
|
+
"""
|
|
378
|
+
if not isinstance(transcript_entry, dict):
|
|
379
|
+
return []
|
|
380
|
+
if transcript_entry.get(TRANSCRIPT_ENTRY_TYPE_KEY) != TRANSCRIPT_ASSISTANT_ENTRY_TYPE:
|
|
381
|
+
return []
|
|
382
|
+
message_record = transcript_entry.get(TRANSCRIPT_MESSAGE_KEY)
|
|
383
|
+
if not isinstance(message_record, dict):
|
|
384
|
+
return []
|
|
385
|
+
content_blocks = message_record.get(TRANSCRIPT_CONTENT_KEY)
|
|
386
|
+
if not isinstance(content_blocks, list):
|
|
387
|
+
return []
|
|
388
|
+
all_text_blocks: list[str] = []
|
|
389
|
+
for each_block in content_blocks:
|
|
390
|
+
if not isinstance(each_block, dict):
|
|
391
|
+
continue
|
|
392
|
+
if each_block.get(TRANSCRIPT_CONTENT_TYPE_KEY) != TRANSCRIPT_TEXT_CONTENT_TYPE:
|
|
393
|
+
continue
|
|
394
|
+
block_text = each_block.get(TRANSCRIPT_TEXT_KEY)
|
|
395
|
+
if isinstance(block_text, str):
|
|
396
|
+
all_text_blocks.append(block_text)
|
|
397
|
+
return all_text_blocks
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _last_verdict_record(all_text_blocks: list[str]) -> dict | None:
|
|
401
|
+
"""Parse the last verdict fence across an agent's assistant text blocks.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
all_text_blocks: The assistant text blocks from one transcript.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
The parsed verdict mapping when the last verdict fence carries a bool
|
|
408
|
+
``all_pass``, a list ``findings``, and a string ``manifest_sha256``;
|
|
409
|
+
otherwise None.
|
|
410
|
+
"""
|
|
411
|
+
verdict_fence_pattern = re.compile(VERDICT_FENCE_PATTERN, re.DOTALL)
|
|
412
|
+
all_fence_bodies = [
|
|
413
|
+
each_match.group(1)
|
|
414
|
+
for each_block in all_text_blocks
|
|
415
|
+
for each_match in verdict_fence_pattern.finditer(each_block)
|
|
416
|
+
]
|
|
417
|
+
if not all_fence_bodies:
|
|
418
|
+
return None
|
|
419
|
+
try:
|
|
420
|
+
verdict_record = json.loads(all_fence_bodies[-1])
|
|
421
|
+
except json.JSONDecodeError:
|
|
422
|
+
return None
|
|
423
|
+
if not isinstance(verdict_record, dict):
|
|
424
|
+
return None
|
|
425
|
+
if not isinstance(verdict_record.get(VERDICT_KEY_ALL_PASS), bool):
|
|
426
|
+
return None
|
|
427
|
+
if not isinstance(verdict_record.get(VERDICT_KEY_FINDINGS), list):
|
|
428
|
+
return None
|
|
429
|
+
if not isinstance(verdict_record.get(VERDICT_KEY_MANIFEST_SHA256), str):
|
|
430
|
+
return None
|
|
431
|
+
return verdict_record
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def workflow_verdict_covers_surface(
|
|
435
|
+
transcript_path: str, expected_manifest_sha256: str
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""Decide whether a workflow code-verifier verdict covers the live surface.
|
|
438
|
+
|
|
439
|
+
A workflow-spawned ``code-verifier`` emits its verdict as assistant text in
|
|
440
|
+
its own transcript rather than through the SubagentStop minter, so this
|
|
441
|
+
walks the live session's subagent transcripts for a ``code-verifier`` whose
|
|
442
|
+
final verdict reports ``all_pass`` true and binds to the expected manifest
|
|
443
|
+
hash.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
transcript_path: The live session's transcript path from the payload.
|
|
447
|
+
expected_manifest_sha256: Hash of the live surface manifest the verdict
|
|
448
|
+
must match exactly.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
True as soon as one ``code-verifier`` transcript carries a passing
|
|
452
|
+
verdict bound to the expected hash; False when none match or the
|
|
453
|
+
subagents directory cannot be located.
|
|
454
|
+
"""
|
|
455
|
+
subagents_directory = _subagents_directory_for_transcript(transcript_path)
|
|
456
|
+
if subagents_directory is None:
|
|
457
|
+
return False
|
|
458
|
+
for each_transcript_file in subagents_directory.rglob(AGENT_TRANSCRIPT_GLOB):
|
|
459
|
+
if _agent_type_for_transcript(each_transcript_file) != MINTING_AGENT_TYPE:
|
|
460
|
+
continue
|
|
461
|
+
verdict_record = _last_verdict_record(
|
|
462
|
+
_assistant_text_blocks(each_transcript_file)
|
|
463
|
+
)
|
|
464
|
+
if verdict_record is None:
|
|
465
|
+
continue
|
|
466
|
+
if verdict_record[VERDICT_KEY_ALL_PASS] is not True:
|
|
467
|
+
continue
|
|
468
|
+
if verdict_record[VERDICT_KEY_MANIFEST_SHA256] == expected_manifest_sha256:
|
|
469
|
+
return True
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
|
|
276
473
|
def write_verdict(
|
|
277
474
|
repo_root: str,
|
|
278
475
|
bound_manifest_sha256: str,
|
|
@@ -444,3 +641,46 @@ def is_verification_exempt_diff(repo_root: str, merge_base_sha: str) -> bool:
|
|
|
444
641
|
if not _is_python_change_docstring_only(repo_root, merge_base_sha, changed_path):
|
|
445
642
|
return False
|
|
446
643
|
return True
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _print_live_manifest_hash(repo_directory: str) -> int:
|
|
647
|
+
"""Print the live surface manifest hash for a repo, for a workflow verifier.
|
|
648
|
+
|
|
649
|
+
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.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
repo_directory: A directory inside the work tree to bind the verdict to.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
0 after printing the hash; nonzero with no stdout when the repo root or
|
|
657
|
+
merge base cannot be resolved.
|
|
658
|
+
"""
|
|
659
|
+
repo_root = resolve_repo_root(repo_directory)
|
|
660
|
+
if repo_root is None:
|
|
661
|
+
return 1
|
|
662
|
+
merge_base_sha = resolve_merge_base(repo_root)
|
|
663
|
+
if merge_base_sha is None:
|
|
664
|
+
return 1
|
|
665
|
+
surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
|
|
666
|
+
if surface_manifest_text is None:
|
|
667
|
+
return 1
|
|
668
|
+
print(manifest_sha256(surface_manifest_text))
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def main() -> None:
|
|
673
|
+
"""Run the verdict-store CLI: compute the live surface-manifest hash.
|
|
674
|
+
|
|
675
|
+
Reads ``--manifest-hash <repo_root>`` from argv and prints the live
|
|
676
|
+
``manifest_sha256`` so a workflow code-verifier can bind its verdict to the
|
|
677
|
+
exact surface the gate checks. Exits nonzero with no stdout on any other
|
|
678
|
+
argument shape or when the surface cannot be resolved.
|
|
679
|
+
"""
|
|
680
|
+
if len(sys.argv) == 3 and sys.argv[1] == MANIFEST_HASH_CLI_FLAG:
|
|
681
|
+
sys.exit(_print_live_manifest_hash(sys.argv[2]))
|
|
682
|
+
sys.exit(1)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
if __name__ == "__main__":
|
|
686
|
+
main()
|
|
@@ -56,6 +56,7 @@ from verification_verdict_store import (
|
|
|
56
56
|
manifest_sha256,
|
|
57
57
|
resolve_merge_base,
|
|
58
58
|
resolve_repo_root,
|
|
59
|
+
workflow_verdict_covers_surface,
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
|
|
@@ -464,15 +465,23 @@ def gated_repo_directories(command_text: str, fallback_directory: str) -> list[s
|
|
|
464
465
|
return target_directories
|
|
465
466
|
|
|
466
467
|
|
|
467
|
-
def deny_reason_for_directory(target_directory: str) -> str | None:
|
|
468
|
+
def deny_reason_for_directory(target_directory: str, transcript_path: str) -> str | None:
|
|
468
469
|
"""Decide whether a commit/push in a directory must be blocked.
|
|
469
470
|
|
|
471
|
+
Accepts the command when a minted verdict binds to the live surface, or
|
|
472
|
+
when a workflow-spawned code-verifier emitted a passing verdict bound to
|
|
473
|
+
the same surface in its own transcript — the latter covers workflow runs,
|
|
474
|
+
where SubagentStop never fires to mint a verdict file.
|
|
475
|
+
|
|
470
476
|
Args:
|
|
471
477
|
target_directory: The directory the git command targets.
|
|
478
|
+
transcript_path: The live session's transcript path from the payload,
|
|
479
|
+
used to find a workflow code-verifier's verdict.
|
|
472
480
|
|
|
473
481
|
Returns:
|
|
474
|
-
The deny reason when the branch diff needs a verdict and
|
|
475
|
-
to it; None when the command may
|
|
482
|
+
The deny reason when the branch diff needs a verdict and neither a
|
|
483
|
+
minted nor a workflow verdict binds to it; None when the command may
|
|
484
|
+
proceed.
|
|
476
485
|
"""
|
|
477
486
|
repo_root = resolve_repo_root(target_directory)
|
|
478
487
|
if repo_root is None:
|
|
@@ -486,10 +495,12 @@ def deny_reason_for_directory(target_directory: str) -> str | None:
|
|
|
486
495
|
if surface_manifest_text is None:
|
|
487
496
|
return f"{CORRECTIVE_MESSAGE} (surface manifest failed in {repo_root})"
|
|
488
497
|
live_manifest_sha256 = manifest_sha256(surface_manifest_text)
|
|
489
|
-
if load_valid_verdict(repo_root, live_manifest_sha256) is None:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
498
|
+
if load_valid_verdict(repo_root, live_manifest_sha256) is not None:
|
|
499
|
+
return None
|
|
500
|
+
if workflow_verdict_covers_surface(transcript_path, live_manifest_sha256):
|
|
501
|
+
return None
|
|
502
|
+
hash_preview = live_manifest_sha256[:HASH_PREVIEW_LENGTH]
|
|
503
|
+
return f"{CORRECTIVE_MESSAGE} (repo: {repo_root}, surface sha256 {hash_preview}...)"
|
|
493
504
|
|
|
494
505
|
|
|
495
506
|
def main() -> None:
|
|
@@ -504,8 +515,9 @@ def main() -> None:
|
|
|
504
515
|
if not command_text:
|
|
505
516
|
return
|
|
506
517
|
session_directory = pretooluse_payload.get("cwd", ".")
|
|
518
|
+
transcript_path = pretooluse_payload.get("transcript_path", "")
|
|
507
519
|
for each_target_directory in gated_repo_directories(command_text, session_directory):
|
|
508
|
-
deny_reason = deny_reason_for_directory(each_target_directory)
|
|
520
|
+
deny_reason = deny_reason_for_directory(each_target_directory, transcript_path)
|
|
509
521
|
if deny_reason is None:
|
|
510
522
|
continue
|
|
511
523
|
deny_payload = {
|