0bc734c020
Applies `ruff format` to the accumulated formatting debt on this branch. Formatting-only — no behavioral changes. Required for CI/lint's format gate (`nox -s format -- --check`), which the branch was failing on 288 tracked files that drifted from ruff's canonical style. In-progress WIP files are intentionally excluded so this commit stays a clean formatting-only diff. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
514 lines
21 KiB
Python
Executable File
514 lines
21 KiB
Python
Executable File
#!/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 <N> --field <name>``
|
|
|
|
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())
|