#!/usr/bin/env python3 """Worker-side reader for the dispatcher's PR-context handoff sentinel. What this is ------------ The dispatcher pre-fetches everything the implementer worker needs to make a code change — PR description, full diff, CI status + per-check detail, PR comments, active REQUEST_CHANGES reviews, linked issues, parent Epic body — and writes the lot to a sentinel at ``/tmp/cleveragents-implementer-handoff/pr-{N}.json``. The dispatcher ALSO embeds those sections in the wrapper session's user prompt, but each ``task`` tool call inside OpenCode re-summarises the prompt for the child agent — by the time the chain reaches ``task-implementor`` at depth 3, only the diff section reliably survives. This script is what the implementer worker calls (via the ``implementer-pr-context`` skill) to re-read any prefetched section on demand, regardless of how the prompt got mangled upstream. It is the read half of :mod:`tools._pr_context_sentinel`. Contract -------- Subcommand ``read``: ``python3 tools/implementer_pr_context.py read --pr --field `` Reads the sentinel at ``/tmp/cleveragents-implementer-handoff/pr-{N}.json``, pulls the requested field, prints it on stdout, and exits 0. The ``--field`` value selects which slice of the prefetched data: - ``description`` — the PR body text (plain string) - ``title`` — the PR title (plain string) - ``metadata`` — JSON with work_type / work_group / head_sha / head_ref / base_ref / pr_number / data_complete / error_kinds - ``ci`` — JSON with the latest CI ``state`` plus ``checks[]`` (per-status detail, including ``target_url`` and any prefetched failure-log excerpts) - ``comments`` — JSON list of PR / issue comments (chronological). For PRs this is the BOUNDED view: the most-recent N comments plus every human/reviewer comment — the older bot ``**Implementation Attempt**`` comments are rolled up into ``comments_digest`` instead. - ``comments_digest`` — JSON dict: the deterministic attempt-history digest (counts by tier / outcome, gates failing most, last success) computed from the FULL PR comment list. ``{}`` when no attempt comments were parsed. - ``reviews`` — JSON list of active REQUEST_CHANGES reviews + their inline comments (for ``request_changes_pr`` work only) - ``issues`` — JSON list of linked issues (the ``Closes #N`` targets, with their bodies inlined) - ``epic`` — JSON dict with the parent Epic body, or empty if no Epic reference was found - ``diff`` — the unified diff as plain text (between ``BEGIN_PR_DIFF`` / ``END_PR_DIFF`` markers in the original prompt) - ``compliance_gaps`` — JSON dict of the dispatcher's deterministic PR Compliance Checklist scan (worktree_clean, changelog, contributors, issues-closed footer). Empty stdout when the dispatcher did not run the scan this cycle. - ``gate_preflight`` — JSON dict of the heavy flaky-test gate pre-flight (two ``local_ci_gate.sh --fast`` runs, classification of persistent vs. flaky failures). Empty stdout when the dispatcher did not run the pre-flight this cycle. - ``all`` — the entire sentinel payload as JSON (useful for debugging; ``task-implementor`` should pull narrowly) The script emits one of three signals on stdout, all with exit code 0: - **Empty stdout** — the sentinel is missing, malformed, or the dispatcher's fetch for this field did not complete (per the matching ``*_completed`` flag). The worker should fall through to its legacy curl-based GET against the live Forgejo API. - ``null\\n`` — the dispatcher fetched successfully and authoritatively confirmed absence (e.g. the PR has no Epic reference). The worker should SKIP its legacy GET; there is nothing to fetch. - Any other content — the field's value (plain text for description/title/diff/issue_body, pretty-printed JSON for everything else). The worker should use it verbatim. This three-case contract is how the worker distinguishes "the dispatcher didn't try" (fall through) from "the dispatcher tried and confirmed there is nothing" (don't re-curl). Conflating the two — as the original ``or None`` projection did — caused the worker to redundantly hit Forgejo for sections the dispatcher had already verified empty. Exit codes ---------- - 0: success (or empty stdout for "no data available" — see above). - 2: usage error (bad / missing CLI arg, unknown ``--field``). - 3: schema-version mismatch (a config issue the operator must notice; the worker should treat this as "no data available" but the non-zero code makes the situation visible in logs). Allow-rule ---------- This script's path is whitelisted under ``python3 tools/implementer_pr_context.py *`` in the agent's permission allowlist (see :file:`.opencode/agents/task-implementor.md`). """ from __future__ import annotations import argparse import json import os import sys from pathlib import Path from typing import Any # Must match :data:`tools._pr_context_sentinel.SCHEMA_VERSION`. EXPECTED_SCHEMA_VERSION = 1 _DEFAULT_HANDOFF_DIR = Path("/tmp/cleveragents-implementer-handoff") def _handoff_dir() -> Path: """Resolve the on-disk directory. Mirrors :func:`tools._pr_context_sentinel.handoff_dir`.""" return Path( os.environ.get("IMPLEMENTER_DISPATCHER_PR_CONTEXT_DIR") or str(_DEFAULT_HANDOFF_DIR) ) def _handoff_path(pr_number: int) -> Path: return _handoff_dir() / f"pr-{int(pr_number)}.json" def _load(pr_number: int) -> tuple[dict[str, Any] | None, str | None]: """Read + parse + validate the sentinel. Returns ``(payload, error)``: - ``(payload, None)`` — sentinel exists, parses, schema matches. - ``(None, None)`` — sentinel does not exist (normal: dispatcher didn't prefetch, or already cleaned up). - ``(None, error)`` — sentinel exists but is unusable (malformed, schema mismatch). The caller emits the error to stderr. """ target = _handoff_path(pr_number) if not target.exists(): return None, None try: raw = target.read_text(encoding="utf-8") payload = json.loads(raw) except (OSError, ValueError) as e: return None, f"handoff read/parse failed at {target}: {e}" if not isinstance(payload, dict): return None, f"handoff at {target} is not a JSON object" schema = payload.get("schema_version") if schema != EXPECTED_SCHEMA_VERSION: return None, ( f"handoff at {target} has schema_version={schema!r}, " f"expected {EXPECTED_SCHEMA_VERSION}" ) return payload, None # Sentinel for "field is not present in this handoff" / "fetch failed # upstream". Distinct from ``None`` so the worker contract can # distinguish three cases: # # - ``_MISSING`` → emit NOTHING (empty stdout); worker falls through # to its legacy GET. Used when the dispatcher didn't attempt this # section (legacy prompt path) OR when the dispatcher attempted # and failed (transient error reflected in the matching # ``*_completed=False`` flag). # - ``None`` → emit the JSON literal ``null`` (stdout = ``null\n``). # Used when the dispatcher fetched successfully and confirmed # absence (e.g. the PR has no Epic reference). The worker sees # non-empty stdout and skips its legacy GET, treating "null" as # the authoritative "no data" answer. # - any other value → emit it; worker uses it verbatim. # # Without this distinction the worker re-curls the live Forgejo API # every time the dispatcher confirmed an absence, defeating the # whole point of the handoff. _MISSING: Any = object() def _gated_list( payload: dict[str, Any], value_key: str, completed_key: str ) -> Any: """Three-case helper for list-valued fields. ``_MISSING`` when ``completed_key`` is False; the value (or ``[]`` if absent / falsy) otherwise. Empty list serialises as ``[]\\n``, which the worker reads as authoritative-empty (skip legacy GET).""" if not payload.get(completed_key, True): return _MISSING return payload.get(value_key) or [] def _gated_text( payload: dict[str, Any], value_key: str, completed_key: str ) -> Any: """Three-case helper for plain-text fields. - completion flag False → ``_MISSING`` (worker falls through to legacy GET). - completion flag True + empty body → ``None`` (rendered as ``null\\n``; worker reads as authoritative-empty and skips the legacy GET). - completion flag True + non-empty body → the body verbatim. The completion-flag-aware path is what lets a legitimately-empty PR description (``description_completed=True``, ``description=""``) cleanly signal "no description, don't re-fetch" rather than forcing a redundant GET. Before the flag existed, every empty description / title / diff / issue_body cost the worker a redundant Forgejo round-trip. """ if not payload.get(completed_key, True): return _MISSING body = payload.get(value_key) if not body: return None return body def _project_field(payload: dict[str, Any], field: str) -> Any: """Return the slice of ``payload`` selected by ``--field``. Implements the **three-case contract** the worker relies on: - :data:`_MISSING` — the field is genuinely absent OR the dispatcher's fetch for it did not complete successfully (per the field's ``*_completed`` flag). :func:`_emit` renders this as empty stdout; the worker falls through to its legacy GET. - ``None`` — the dispatcher's fetch succeeded AND confirmed the field is empty in a way the JSON schema can encode (e.g. ``epic=None`` means "PR has no parent Epic"). :func:`_emit` renders this as ``null\\n``; the worker skips the legacy GET. - any other value — the value itself, rendered verbatim (plain text for ``description``/``title``/``diff``/``issue_body``, pretty-printed JSON for the rest). Empty lists / empty dicts ALSO fall through this case and render as ``[]\\n`` / ``{}\\n`` — they carry the "fetched, confirmed empty" semantics for list/dict-valued fields. The :data:`_MISSING` vs ``None`` vs value distinction is what stops the worker from re-curling Forgejo for sections the dispatcher has already confirmed are empty. Mapping all three cases to ``None`` (the pre-2026-05-10 behavior) collapsed the "didn't try" and "tried, confirmed empty" cases together and every confirmed-empty section cost the worker a redundant GET. """ if field == "all": return payload if field == "description": return _gated_text(payload, "description", "description_completed") if field == "title": return _gated_text(payload, "title", "title_completed") if field == "diff": return _gated_text(payload, "diff", "diff_completed") if field == "issue_body": return _gated_text(payload, "issue_body", "issue_body_completed") if field == "metadata": # The ``completion`` sub-object surfaces the per-section # ``*_completed`` flags so an operator inspecting via # ``--field metadata`` can see ALL fetcher state in one # query without having to iterate every ``--field``. The # worker's per-field reads still rely on the individual # field projections — this is for human/operator triage. # # Enumerates payload keys (rather than hardcoding the # name list) so a new ``*_completed`` flag added to the # writer's ``COMPLETION_FLAG_NAMES`` tuple appears here # automatically — keeps this reader script standalone # (no dispatcher / prefetch imports) while still tracking # writer-side additions without code changes. Stable # sort order so two consecutive ``--field metadata`` # reads produce byte-identical output (worker / operator # diff tooling depends on this). completion = { k.removesuffix("_completed"): payload.get(k, True) for k in sorted(payload) if k.endswith("_completed") } return { "schema_version": payload.get("schema_version"), "pr_number": payload.get("pr_number"), "work_type": payload.get("work_type"), "work_group": payload.get("work_group"), "head_sha": payload.get("head_sha"), "head_ref": payload.get("head_ref"), "base_ref": payload.get("base_ref"), "listing_title": payload.get("listing_title"), "data_complete": payload.get("data_complete"), "completion": completion, "error_kinds": payload.get("error_kinds") or [], "created_at": payload.get("created_at"), "dispatcher_pid": payload.get("dispatcher_pid"), } if field == "ci": # ─── Contract divergence (intentional) ──────────────── # Every OTHER field gated on a completion flag uses # _gated_list / _gated_text and returns ``_MISSING`` # when the flag is False (empty stdout → worker falls # through to legacy GET). ``ci`` is the exception: it # ALWAYS emits the bundle when the sentinel exists at # all, and the worker is expected to inspect the inner # ``status_completed`` / ``detail_completed`` booleans # to decide what to do. # # Why diverge: # 1. CI carries TWO independent completion signals # (status + detail). Collapsing them into one # ``_MISSING`` would lose the case where the status # summary fetch succeeded but the per-check detail # fetch failed — the worker still has useful # aggregate state ("PR is red"), it just lacks # per-check evidence. # 2. A green PR legitimately has ``ci_detail=[]`` (no # failing checks to enumerate) with # ``ci_detail_completed=True``. Emitting that as a # bundle is the right answer. # # Workers reading ``--field ci`` MUST check both # completion booleans before treating the bundle as # authoritative. See SKILL.md "ci field exception" for # the worker-side procedure. return { "status": payload.get("ci_status"), "checks": payload.get("ci_detail") or [], "status_completed": payload.get("ci_status_completed", True), "detail_completed": payload.get("ci_detail_completed", True), } if field == "comments": # Dispatch on ``work_type``: pr_fix / request_changes_pr # cycles populate ``pr_comments``; new_issue cycles populate # ``issue_comments``. Mixing them was a latent bug — if a # future change ever populated both (e.g. an issue_impl # cycle that also happened to have a stale PR), the previous # ``or`` chain would return the wrong one. if payload.get("work_type") == "issue_impl": return _gated_list( payload, "issue_comments", "issue_comments_completed" ) return _gated_list(payload, "pr_comments", "pr_comments_completed") if field == "comments_digest": # The deterministic attempt-history digest the dispatcher # computed from the FULL comment list at prefetch time (see # _attempt_history.summarize_attempt_history). The verbatim # ``comments`` field only carries the bounded view; this is the # rollup of everything dropped from that window. Always emit the # dict when the sentinel exists — an empty ``{}`` means # "computed, zero attempts parsed", still authoritative. There # is no legacy GET to fall through to. return payload.get("pr_comments_digest") or {} if field == "reviews": return _gated_list( payload, "request_changes_reviews", "request_changes_reviews_completed", ) if field == "comment_reviews": # R3.4 (2026-05-17): non-RC reviews (COMMENT/APPROVE) — the # reviewer's advisory feedback. Separate field from RC so # callers can keep the blocking-vs-advisory distinction. return _gated_list( payload, "comment_reviews", "comment_reviews_completed", ) if field == "issues": return _gated_list(payload, "linked_issues", "linked_issues_completed") if field == "epic": # ``epic_completed=False`` means the fetch failed; fall # through. ``epic_completed=True`` with ``epic=None`` means # the dispatcher confirmed the PR has no Epic reference — # emit ``null`` so the worker skips its legacy GET. if not payload.get("epic_completed", True): return _MISSING return payload.get("epic") if field in ( "compliance_gaps", "gate_preflight", "recent_implementer_push", ): # Deterministic / cross-cycle sections — BINARY contract # (not the generic three-case prefetch contract). The # dispatcher either computed this cycle (``*_completed=True`` # AND a populated dict, emitted as JSON) or it did not # (both keys absent, emitted as empty stdout / worker falls # through). The detectors always produce a structured dict # when they run, so there is no "tried-but-empty" middle # state to surface. completed_key = f"{field}_completed" if not payload.get(completed_key): return _MISSING return payload.get(field) raise ValueError(f"unknown --field: {field}") _ALLOWED_FIELDS = ( "all", "description", "title", "diff", "metadata", "ci", "comments", "comments_digest", "reviews", "issues", "epic", "issue_body", "compliance_gaps", "gate_preflight", "recent_implementer_push", ) def _emit(value: Any, field: str) -> None: """Render ``value`` to stdout in the format the worker expects. Three cases (see :data:`_MISSING` docstring for full rationale): - ``value is _MISSING`` → emit nothing. The worker reads empty stdout and falls through to its legacy GET. - ``value is None`` → emit ``null\\n``. The worker reads non-empty stdout and skips its legacy GET (the dispatcher authoritatively confirmed absence). - any other value → emit verbatim. Strings print as plain text with a trailing newline; everything else prints as pretty-printed JSON. """ if value is _MISSING: return if value is None: # Authoritative "no data" answer (e.g. dispatcher confirmed # no Epic). The worker contract reads this as "skip the # legacy GET; there's nothing to fetch." JSON ``null`` is # parseable in any language the worker might use. sys.stdout.write("null\n") return if isinstance(value, str): # Plain-text contract for these fields so the worker can # pipe them straight into a file or display them inline # without an extra json parse. sys.stdout.write(value) # Ensure a trailing newline so terminal usage looks right. if not value.endswith("\n"): sys.stdout.write("\n") return # Structured fields: pretty-print JSON so the worker (or an # operator inspecting from the shell) gets a readable blob. json.dump(value, sys.stdout, indent=2, default=str) sys.stdout.write("\n") def _cmd_read(args: argparse.Namespace) -> int: """Implement ``read``. Always exits 0 on the "no data" path — that's a normal condition (e.g. epic-field on a PR with no Epic reference). Exit 3 only on schema mismatch, which is a config issue the operator must notice.""" payload, error = _load(int(args.pr)) if error: print(f"warning: {error}", file=sys.stderr) if "schema_version" in error: return 3 return 0 if payload is None: # No sentinel — the dispatcher didn't prefetch (or cleaned # up already). Empty stdout is the "no data" signal. return 0 try: value = _project_field(payload, args.field) except ValueError as e: print(f"error: {e}", file=sys.stderr) return 2 _emit(value, args.field) return 0 def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="implementer_pr_context", description=( "Worker-side reader for the dispatcher's PR-context " "handoff sentinel. See module docstring for the full " "contract." ), ) sub = parser.add_subparsers(dest="cmd", required=True) p_read = sub.add_parser( "read", help=( "Read one field from the PR's context handoff. Prints " "the field on stdout (plain text for description/title/" "diff/issue_body, JSON for everything else). Empty " "stdout means 'no data available — fall back to curl'." ), ) p_read.add_argument( "--pr", required=True, type=int, help="PR number" ) p_read.add_argument( "--field", required=True, choices=_ALLOWED_FIELDS, help="Which slice of the prefetched data to print", ) args = parser.parse_args(argv) if args.cmd == "read": return _cmd_read(args) parser.error(f"unknown subcommand {args.cmd!r}") return 2 # pragma: no cover — argparse exits before reaching here if __name__ == "__main__": sys.exit(main())