fix(issue-5): keep finalize inside delegation and refresh the CLI golden

Two failures the full suite caught, both mine.

finalize.py started in lib_py/loop, which made publish_event import a sibling
package. That is the rule phase 1 wrote down and phase 2a already made me move
the engine for: under a frozen loop, delegation is bound to one tree while the
parent may be another, so reaching sideways mixes them. The manifest is written
into the job's own report directory and the CLI that declares it is a delegation
entry point, so finalize belongs there. Second time on the same rule, and the
guard caught it both times.

The publish_event --help golden no longer matched, because phase 2b
intentionally added the finalization options. The diff is exactly those three
options and nothing else, so the capture is refreshed rather than the assertion
weakened, and the test now states when a refresh is legitimate: an intended
extension, never drift nobody meant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-06 21:47:54 +09:00
1 parent 8adbe2245c
commit 12cba25c18
7 files changed
+47 -14

No files matched your search

@@ -11,6 +11,13 @@ Consequence, and it is the useful one: an agent that declares nothing leaves no
manifest, a missing manifest already holds, and the explicit-event path is
therefore preserved for unsupported agents without any special case.
It lives in `lib_py.delegation`, not `lib_py.loop`, for the same reason the
transition engine does: the artifact it writes sits in the job's own directory,
and the CLI an agent calls to declare a finalization is a delegation entry point.
A frozen loop can bind delegation to a different tree than the parent package, so
delegation reaching sideways into `lib_py.loop` would mix trees -- the rule that
caught this, twice now.
writing report-final.md.tmp is being written
stable_candidate the file stopped changing (a filter, never an authority)
finalized atomically renamed, and this manifest exists
@@ -240,7 +240,7 @@ def main(argv=None) -> int:
# written would invite exactly the partial-report adoption the contract
# forbids. Failing here fails the publish, on purpose.
if args.finalize_report:
from lib_py.loop import finalize # local import: optional dependency of this CLI
from lib_py.delegation import finalize # same package: no sibling hop (see __init__)
try:
side_effects = finalize.parse_side_effects(args.side_effects)
+2 -1
View File
@@ -19,7 +19,8 @@ import logging
import os
from typing import Any, Dict, List, Optional
from lib_py.loop import finalize, policy, store
from lib_py.delegation import finalize
from lib_py.loop import policy, store
logger = logging.getLogger("loop.autocomplete")
@@ -77,6 +77,12 @@
`attempt_id` 승격은 열린 질문으로 남긴다. 승격하면 엔진에 2차 방어선이 생기지만 동결 계약을 바꿔야 하며, 현재는 필요하지 않다. `test_x03_the_record_carries_iteration_not_an_attempt_id`가 이 전제를 감시한다 — 레코드가 `attempt_id`를 갖게 되는 순간 실패하며, 그때가 재검토 시점이다.
## 9-1. 구현 중 정정 1건 — `finalize`의 패키지 위치
처음에는 `finalize.py`를 `lib_py/loop/`에 두었는데, 그러면 `publish_event`(=`lib_py.delegation`)가 **형제 패키지를 임포트**하게 되어 freeze 하에서 트리가 섞인다. Phase 1이 명문화하고 Phase 2a에서 엔진 배치를 정정하게 만들었던 바로 그 규칙이며, 기존 가드 테스트(`test_delegation_package_imports_no_sibling_lib_py_modules`)가 전체 스위트에서 이를 검출했다.
**매니페스트는 job의 리포트 디렉터리에 쓰이고 에이전트가 호출하는 CLI는 delegation 진입점이므로, `finalize`는 `lib_py/delegation/`에 속한다.** `lib_py.loop`가 `lib_py.delegation`을 임포트하는 방향은 허용된다. 같은 규칙에 두 번 걸린 셈이며, 가드가 두 번 다 잡았다.
## 10. Phase 3 인계
1. `preexec_fn`: 2b도 데몬을 스폰하지 않아 미노출 유지. 알림 flush는 기존 구독자 프로세스 안에서 수행한다.
+12 -1
View File
@@ -3,7 +3,9 @@ usage: publish_event.py [-h] (--job JOB | --pick-pending)
[--event {started,permission_required,progress,completed,error,cancelled}]
[--detail DETAIL] [--data DATA] [--retained]
[--registry-dir REGISTRY_DIR] [--attempts ATTEMPTS]
[-v]
[--finalize-report PATH]
[--side-effect KIND=REFERENCE]
[--attempt-id ATTEMPT_ID] [-v]
Publish a Job event to MQTT
@@ -19,4 +21,13 @@ optional arguments:
--retained force retain=True (auto for completed/error)
--registry-dir REGISTRY_DIR
--attempts ATTEMPTS
--finalize-report PATH
atomically finalize this report and declare its
completion manifest before publishing
--side-effect KIND=REFERENCE
evidence for an external side effect this job
performed
--attempt-id ATTEMPT_ID
attempt this finalization belongs to (default: the
job's current iteration)
-v, --verbose
+7 -1
View File
@@ -488,7 +488,13 @@ def test_importing_delegation_does_not_mutate_environment(mam_sandbox):
def test_shim_help_output_matches_pre_move_capture():
"""The shim must be indistinguishable from the pre-move script at the CLI."""
"""The shim must be indistinguishable from the implementation at the CLI.
The captures were taken before the phase 1 move to prove the move changed
nothing. They are refreshed only when the CLI is *intentionally* extended
(phase 2b added the finalization options to publish_event); the guard's job
is to catch drift nobody meant, which it still does.
"""
for mod in ("publish_event", "registry", "job_subscriber"):
golden = (FIXTURES / "help-{}.txt".format(mod)).read_text(encoding="utf-8")
res = subprocess.run(
+12 -10
View File
@@ -154,7 +154,7 @@ def _report(tmp_path, body="done\n", staged=False):
def test_finalize_promotes_a_staged_report_atomically(tmp_path):
"""A reader must never see a half-written report as finalized."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path, "final body\n", staged=True)
assert not path.exists() and path.with_name(path.name + ".tmp").exists()
@@ -165,7 +165,7 @@ def test_finalize_promotes_a_staged_report_atomically(tmp_path):
def test_finalize_verifies_against_the_file_not_the_declaration(tmp_path):
"""The hash is recomputed, so a forged manifest cannot certify itself."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1)
@@ -181,7 +181,7 @@ def test_finalize_verifies_against_the_file_not_the_declaration(tmp_path):
def test_finalize_is_invalidated_by_a_rewritten_report(tmp_path):
"""A new body needs a new finalization; the old hash is void immediately."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path, "first\n")
finalize.finalize_report(path, "job1", 1)
@@ -196,7 +196,7 @@ def test_finalize_is_invalidated_by_a_rewritten_report(tmp_path):
({"expected_attempt": 9}, "belongs to attempt"),
])
def test_finalize_rejects_a_manifest_for_a_different_job_or_attempt(tmp_path, kwargs, fragment):
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1)
args = {"job_id": "job1"}
@@ -207,7 +207,7 @@ def test_finalize_rejects_a_manifest_for_a_different_job_or_attempt(tmp_path, kw
def test_finalize_requires_evidence_for_every_declared_side_effect(tmp_path):
"""Partial evidence is no evidence: the work is not known to be complete."""
from lib_py.loop import finalize
from lib_py.delegation import finalize
path = _report(tmp_path)
finalize.finalize_report(path, "job1", 1,
@@ -220,7 +220,7 @@ def test_finalize_requires_evidence_for_every_declared_side_effect(tmp_path):
def test_finalize_rejects_a_malformed_side_effect_pair():
from lib_py.loop import finalize
from lib_py.delegation import finalize
with pytest.raises(ValueError):
finalize.parse_side_effects(["nokey"])
with pytest.raises(ValueError):
@@ -228,7 +228,7 @@ def test_finalize_rejects_a_malformed_side_effect_pair():
def test_finalize_missing_manifest_is_a_refusal_not_a_crash(tmp_path):
from lib_py.loop import finalize
from lib_py.delegation import finalize
d = tmp_path / "empty"
d.mkdir()
verdict = finalize.verify_manifest(d, "job1")
@@ -239,7 +239,7 @@ def test_publish_event_finalizes_before_it_publishes(mam_sandbox, tmp_path, monk
"""The CLI surface an agent actually calls."""
monkeypatch.chdir(mam_sandbox)
from lib_py.delegation import publish_event
from lib_py.loop import finalize
from lib_py.delegation import finalize
reg = _reg(tmp_path)
job_id = _make_job(reg)
@@ -280,7 +280,8 @@ def test_publish_event_reports_a_bad_side_effect_instead_of_publishing(mam_sandb
def _ready_job(tmp_path, mam_sandbox, *, backend="claude-code", opt_in=True,
artifacts=None, body="report\n"):
"""A job in exactly the state that should auto-complete, plus its evidence."""
from lib_py.loop import finalize, store
from lib_py.delegation import finalize
from lib_py.loop import store
reg = _reg(tmp_path)
job_id = _make_job(reg, agent=backend, expected_artifacts=artifacts or [])
@@ -398,7 +399,8 @@ def test_g5_a_rewritten_report_stops_the_gate(mam_sandbox, tmp_path, monkeypatch
def test_g5_missing_side_effect_evidence_stops_the_gate(mam_sandbox, tmp_path, monkeypatch):
monkeypatch.chdir(mam_sandbox)
monkeypatch.setenv("MAM_AUTO_COMPLETE", "1")
from lib_py.loop import autocomplete, finalize
from lib_py.loop import autocomplete
from lib_py.delegation import finalize
job_id, reg, scenario, report_dir = _ready_job(tmp_path, mam_sandbox,
artifacts=["git_push"])