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.
Files changed (32) hide show
  1. package/CLAUDE.md +4 -0
  2. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  3. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  4. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  5. package/docs/CODE_RULES.md +2 -2
  6. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  7. package/hooks/blocking/code_rules_enforcer.py +8 -0
  8. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  9. package/hooks/blocking/config/verified_commit_constants.py +14 -2
  10. package/hooks/blocking/destructive_command_blocker.py +483 -61
  11. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  12. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  13. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  14. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  15. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  16. package/hooks/blocking/test_verification_verdict_store.py +212 -0
  17. package/hooks/blocking/test_verified_commit_gate.py +127 -0
  18. package/hooks/blocking/test_verifier_verdict_minter.py +74 -95
  19. package/hooks/blocking/verification_verdict_store.py +240 -0
  20. package/hooks/blocking/verified_commit_gate.py +20 -8
  21. package/hooks/blocking/verifier_verdict_minter.py +46 -124
  22. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  23. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  24. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  25. package/hooks/validation/mypy_validator.py +59 -7
  26. package/hooks/validation/test_mypy_validator.py +94 -0
  27. package/package.json +1 -1
  28. package/rules/orphan-css-class.md +23 -0
  29. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -1
  30. package/skills/autoconverge/workflow/converge.contract.test.mjs +202 -13
  31. package/skills/autoconverge/workflow/converge.mjs +392 -51
  32. 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 live
4
- SubagentStop payload names the stopping subagent by ``agent_id`` and carries
5
- no flat agent-type key, so the minter recovers the spawning agent type from
6
- the parent transcript: it walks the parent transcript for the completion
7
- record whose ``agentId`` matches the payload and reads that record's sibling
8
- ``agentType``. These tests build a faithful parent transcript and assert the
9
- minter gates on the resolved type and on the shared MINTING_AGENT_TYPE
10
- constant, so a rename in config propagates to the minter without a second
11
- edit. One test proves that only a structured ``agentType`` key resolves: a
12
- text block that merely quotes the identity keys mints nothing. A further test
13
- holds the shipped settings.json to the minter docstring's anti-forgery claim:
14
- the main session is denied writes to the verdict directory, so only this hook
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 _write_parent_transcript(transcript_file: pathlib.Path, agent_id: str, agent_type: str) -> None:
55
- spawn_record = {
56
- "type": "assistant",
57
- "message": {
58
- "content": [
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 test_resolves_subagent_type_from_parent_transcript(tmp_path: pathlib.Path) -> None:
74
- transcript_file = tmp_path / "parent.jsonl"
75
- _write_parent_transcript(transcript_file, "agent-7", MINTING_AGENT_TYPE)
76
- payload = {"agent_id": "agent-7", "transcript_path": str(transcript_file)}
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 test_resolves_none_when_agent_id_absent_from_transcript(
81
- tmp_path: pathlib.Path,
82
- ) -> None:
83
- transcript_file = tmp_path / "parent.jsonl"
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 test_resolves_type_when_record_arrives_after_first_read(
90
- tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
91
- ) -> None:
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
- monkeypatch.setattr(minter_module.time, "sleep", write_record_on_first_sleep)
101
- payload = {"agent_id": "agent-7", "transcript_path": str(transcript_file)}
102
- assert resolved_subagent_type(payload) == MINTING_AGENT_TYPE
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 test_quoted_agent_type_in_text_block_does_not_resolve(
106
- tmp_path: pathlib.Path,
107
- ) -> None:
108
- transcript_file = tmp_path / "parent.jsonl"
109
- forged_entry = {
110
- "type": "assistant",
111
- "message": {
112
- "content": [
113
- {
114
- "type": "text",
115
- "text": json.dumps({"agentId": "agent-7", "agentType": MINTING_AGENT_TYPE}),
116
- }
117
- ]
118
- },
119
- }
120
- transcript_file.write_text(json.dumps(forged_entry) + "\n", encoding="utf-8")
121
- payload = {"agent_id": "agent-7", "transcript_path": str(transcript_file)}
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
- transcript_file = tmp_path / "parent.jsonl"
127
- _write_parent_transcript(transcript_file, "agent-7", "general-purpose")
128
- payload = {
129
- "agent_id": "agent-7",
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 test_minting_agent_type_passes_the_agent_type_gate(
138
- tmp_path: pathlib.Path,
139
- ) -> None:
140
- transcript_file = tmp_path / "parent.jsonl"
141
- _write_parent_transcript(transcript_file, "agent-7", MINTING_AGENT_TYPE)
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
- transcript_file = tmp_path / "parent.jsonl"
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 none binds
475
- to it; None when the command may proceed.
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
- hash_preview = live_manifest_sha256[:HASH_PREVIEW_LENGTH]
491
- return f"{CORRECTIVE_MESSAGE} (repo: {repo_root}, surface sha256 {hash_preview}...)"
492
- return None
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 = {