fix(issue-5): close the phase 2b gate holes found by all three reviewers
Seven blocking items across three reviews, every one reproduced here before being accepted. Two themes run through them: a gate whose strictness depended on what the caller remembered to pass, and a boundary that trusted its only current caller instead of enforcing its own rule. Independent verification is no longer optional. Omitting the report directory skipped the disk check entirely and handed the decision back to the collector's own flags -- the self-certification this design exists to prevent. It is now a refusal, as is an empty directory or a job with no recorded dispatch. Evidence is now bound to its target. The policy compares a scenario against the dispatch context carried inside that same scenario, which is self-consistent by construction, so one job's evidence could complete another. The gate now checks the job, attempt, session and pane generation against the recorded dispatch. A reviewer with no verdict token is no longer completed. Phase 0 made this a safety contract and run_loop reads a missing token as NOT PASS, so inferring completion first would start a correction round against a review that was never finished. The requirement applies to reviewers only. Bookkeeping may write one field. A status-free request was an unrestricted merge on a terminal record: it could rewrite the prompt and the session. Settling is also conditional now on the exact intent that was sent, because publishing happens outside the lock and a newer intent can legitimately land mid-flight; clearing unconditionally dropped a notification nobody had sent. The intent's own identity travels in the payload, since the transport sequence cannot identify a retry of the same intent. Side-effect evidence must be evidence. A kind with no reference names something without evidencing it, and the CLI parser could not be the only check because the API and hand-written manifests bypass it. Also honours MAM_ENGINE_CAS=0, which the operations note already promised disables auto-completion, matches backend keys exactly rather than by prefix so a future name cannot inherit evidence never collected for it, records an absolute report path, and imports the notifier at module scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
12cba25c18
commit
cb7ed1aa38
5 files changed
+444
-26
No files matched your search
@@ -68,6 +68,25 @@ def _atomic_write(path: Path, text: str) -> None:
|
||||
raise
|
||||
|
||||
|
||||
def validate_side_effect(entry) -> Optional[str]:
|
||||
"""None when the entry is usable evidence, else why it is not.
|
||||
|
||||
A kind with no reference names something without evidencing it, which is the
|
||||
shape "declared but unproven" takes. The CLI parser cannot be the only place
|
||||
this is checked: finalize_report is a public API and a manifest can also be
|
||||
written by hand.
|
||||
"""
|
||||
if not isinstance(entry, dict):
|
||||
return "side effect entry is not an object"
|
||||
kind = str(entry.get("kind") or "").strip()
|
||||
reference = str(entry.get("reference") or "").strip()
|
||||
if not kind:
|
||||
return "side effect entry has no kind"
|
||||
if not reference:
|
||||
return "side effect {!r} has no reference".format(kind)
|
||||
return None
|
||||
|
||||
|
||||
def parse_side_effects(raw: Optional[List[str]]) -> List[Dict[str, str]]:
|
||||
"""`kind=reference` pairs from the command line into evidence entries."""
|
||||
out = []
|
||||
@@ -89,7 +108,14 @@ def finalize_report(report_path, job_id, attempt_id, side_effects=None,
|
||||
never observe a half-written report as finalized. The rename is what makes
|
||||
the transition atomic; the manifest is what makes it attributable.
|
||||
"""
|
||||
report_path = Path(report_path)
|
||||
for entry in side_effects or []:
|
||||
problem = validate_side_effect(entry)
|
||||
if problem:
|
||||
raise ValueError(problem)
|
||||
|
||||
# Absolute, so a later reader in a different working directory resolves the
|
||||
# same file rather than a same-named one beside itself.
|
||||
report_path = Path(report_path).resolve()
|
||||
staged = report_path.with_name(report_path.name + ".tmp")
|
||||
if staged.exists():
|
||||
os.replace(str(staged), str(report_path))
|
||||
@@ -157,8 +183,13 @@ def verify_manifest(report_dir, job_id, expected_attempt=None,
|
||||
|
||||
# A job that declared side effects must show evidence for every one of them.
|
||||
# Partial evidence is no evidence: the work is not known to be complete.
|
||||
declared = {e.get("kind") for e in manifest.get("side_effects") or []}
|
||||
missing = [a for a in (expected_artifacts or []) if a not in declared]
|
||||
evidenced = set()
|
||||
for entry in manifest.get("side_effects") or []:
|
||||
problem = validate_side_effect(entry)
|
||||
if problem:
|
||||
return ManifestVerdict(False, problem)
|
||||
evidenced.add(str(entry["kind"]).strip())
|
||||
missing = [a for a in (expected_artifacts or []) if a not in evidenced]
|
||||
if missing:
|
||||
return ManifestVerdict(False, "no side-effect evidence for {}".format(missing))
|
||||
|
||||
|
||||
Reference in new issue
Block a user