"""The completion decision. A pure function, deliberately. No I/O, no clock, no globals: that is what lets the phase-0 scenario fixtures be fed to it verbatim as test vectors. Those seven scenarios and their `expected_policy.decision` survived three-reviewer scrutiny, so they are the contract this implements rather than a fresh set of rules. Fail-closed throughout: every gate must pass for `candidate_approved`. Anything unproven holds. """ from __future__ import annotations from lib_py.loop import attribution from lib_py.loop.evidence import Decision # Native states that mean "not doing anything right now". Idle alone proves # nothing about completion; it only makes completion *possible* to consider. QUIESCENT = ("idle", "done") # The four identifiers an external collector must supply before a completion can # be attributed to this attempt. ATTRIBUTION_FIELDS = ("job_id", "attempt_id", "agent_session_uuid", "pane_process_generation") def _timeline_by_kind(scenario): out = {} for item in scenario.get("timeline") or []: out.setdefault(item.get("source_kind"), []).append(item) return out def collect_observed_identity(scenario) -> dict: """Gather identifiers the *evidence* carries, for comparison with dispatch. Two sources, and the distinction matters. Association of a job or attempt to an observation is the external collector's business, so it lives in a ``collector_attribution`` envelope beside the payload -- never inside it, because a native Herdr payload must stay native (phase 0 forbids MAM fields there, and a fabricated one would be indistinguishable from a real reading). The report manifest is a filesystem fact and stays in that payload. Every occurrence is kept. Taking only the first would drop a later, contradicting observation, which is precisely what this exists to catch. """ observed = {} for item in scenario.get("timeline") or []: envelope = item.get("collector_attribution") or {} for field in ATTRIBUTION_FIELDS: if envelope.get(field) is not None: observed.setdefault(field, []).append(envelope[field]) payload = item.get("payload") or {} if item.get("source_kind") != "herdr": # A MAM-side source (an event, a filesystem fact) legitimately names # the job or attempt in its own payload. A native Herdr payload never # may: phase 0 forbids MAM fields there, so an identifier appearing # inside one is either fabricated or a contract violation, and either # way it is not evidence. for field in ATTRIBUTION_FIELDS: if payload.get(field) is not None: observed.setdefault(field, []).append(payload[field]) manifest = payload.get("report_manifest") if isinstance(manifest, dict) and manifest.get("attempt_id") is not None: observed.setdefault("manifest_attempt_id", []).append(manifest["attempt_id"]) return observed def decide(scenario) -> Decision: """Map one composite scenario to a decision. Order matters: transport doubt and staleness invalidate the evidence itself, so they are answered before anything is inferred from it. """ dispatch = scenario.get("dispatch_context") or {} by_kind = _timeline_by_kind(scenario) herdr_items = by_kind.get("herdr") or [] latest = herdr_items[-1]["payload"] if herdr_items else {} filesystem = scenario.get("filesystem") or {} # 1. Transport doubt: evidence gathered before a gap cannot be trusted. for item in herdr_items: payload = item.get("payload") or {} if payload.get("transport_healthy") is False or payload.get("error"): return Decision("hold_reconnect", "herdr transport failed; cached observations are void") # 2. A signal from a previous attempt or process generation decides nothing. for item in herdr_items: payload = item.get("payload") or {} pane_generation = payload.get("pane_process_generation") if pane_generation and dispatch.get("pane_process_generation") and \ pane_generation != dispatch["pane_process_generation"]: return Decision("reject_stale", "signal belongs to a different process generation") pane_id = payload.get("pane_id") if pane_id and dispatch.get("pane_process_generation") and \ not attribution.pane_matches_generation( pane_id, dispatch["pane_process_generation"]): return Decision("reject_stale", "signal belongs to a different pane") # 3. A blocked agent is waiting for a human. Completion is forbidden. if latest.get("agent_status") == "blocked" or latest.get("matched_rule"): return Decision("hold_blocked", "agent is blocked on an interactive prompt") # 4. Attribution: the dispatch record must be well formed AND agree with the # identifiers the evidence itself carries. Flags on the dispatch record # alone are self-assertion, not correspondence. observed = collect_observed_identity(scenario) manifest_attempts = observed.pop("manifest_attempt_id", []) attributed = attribution.check(dispatch) if not attributed.ok: return Decision("hold", attributed.reason) # Absence of proof is not proof. Comparing only the identifiers that happen # to be present would let evidence carrying none at all be approved on the # dispatch record's own say-so, which is the self-assertion this check exists # to replace. Every identifier must be supplied, then must agree. for field in ATTRIBUTION_FIELDS: if not observed.get(field): return Decision("hold", "no observed {} to attribute this completion to".format(field)) for value in observed[field]: if value != dispatch.get(field): return Decision("hold", "attribution mismatch on {}: observed {!r}, " "dispatched {!r}".format(field, value, dispatch.get(field))) # The finalized report must belong to the attempt being judged. A missing # manifest attempt holds for the same reason: it proves nothing. if not manifest_attempts: return Decision("hold", "no report manifest attempt to associate with this attempt") for manifest_attempt in manifest_attempts: if manifest_attempt != dispatch.get("attempt_id"): return Decision("hold", "report manifest belongs to attempt {!r}, not {!r}".format( manifest_attempt, dispatch.get("attempt_id"))) # 5. Still working, or a tool is running underneath a ready prompt. if latest.get("agent_status") not in QUIESCENT: return Decision("hold", "agent is not quiescent (status={!r})".format( latest.get("agent_status")), attribution_ok=True) for item in by_kind.get("harness") or []: if (item.get("payload") or {}).get("active"): return Decision("hold", "a tool is still executing", attribution_ok=True) # 6. The report must be finalized, not merely paused. A debounce alone is a # candidate filter, never an authority to complete. if not filesystem.get("exists"): return Decision("hold", "no report present", attribution_ok=True) if not filesystem.get("manifest_committed"): return Decision("hold", "report is not finalized (no commit manifest)", attribution_ok=True) if not filesystem.get("hash_verified"): return Decision("hold", "report manifest hash unverified", attribution_ok=True) # Side-effect evidence is enforced authoritatively in finalize.verify_manifest, # which has the job record and therefore knows what was declared. The policy # honours an explicit negative from the collector so the case is expressible # as a scenario vector; absence here is not read as proof, because the # manifest check is the layer that fails closed on missing evidence. if filesystem.get("side_effects_verified") is False: return Decision("hold", "declared side effect has no evidence in the commit manifest", attribution_ok=True) return Decision("candidate_approved", "attribution, quiescence and a verified manifest all hold", attribution_ok=True)