# Canonical PR rework completion readback ## Scope and source Canonical entry point: `/harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py`. The adjacent `rework_readback.py` derives readback inputs; `rework_readback_cli.py` provides the role-local GET-only transport and private artifact writer. Update these canonical files and this reference in place; do not copy them into another profile or use a review-role credential for impl delivery. `snapshot` and `verify-live` never create, edit, retry, or submit a Gitea comment. The `create` action verifies an already observed single comment addition; `reuse` verifies an unchanged collection with exactly one matching existing comment. An independently authorized publication workflow remains responsible for its single external write, uncertain-write reconciliation, Issue lease, and fresh pre-write delegation admission. ## Prerequisites and independent expectations - The canonical access contract is `harness/operations/gitea-access.md`. - Run in the impl role's own `HERMES_HOME`. Only its owned, non-symlink, mode-0600 `.env` file supplies `GITEA_TOKEN`. No ambient token or other-profile fallback. - Independently supply the expected stable impl account ID. The first API read must authenticate that ID and the `gitea-impl-agent` login. - Generate `expected.json` with the existing canonical renderer. Its exact target, final head, reviewed head, review ID, body, marker and digest bind the workflow. - Supply a reviewed provider contract and its independently expected SHA-256. The exact provider version is read from the server and must match the contract. A digest calculated from an untrusted delegate file is not independent evidence. - Own a private mode-0700 runtime directory. Inputs must be owned regular files, not group/world writable or symlinked. Output files use mode 0600; a failed verification does not replace an existing receipt. ## Provider contract Schema: `gitea-agent.rework-provider/v1`. The contract explicitly declares the aggregate semantics for PR and underlying PR-Issue `comments` fields. Each `components` array contains unique values from `top_level`, `reviews`, `inline` and must include `top_level`. An optional `cache_mode: "gitea-conversation-v1"` on each aggregate scope is accepted only for provider version `1.27.3`, field `comments`, and components consisting of exactly `top_level` and `reviews`. Omission retains strict legacy behavior. Choose the reviewed deployment semantics, not the combination that happens to fit a current response. There is no guessed version or automatic capability downgrade. Shape example only: upstream v1.27.3 source supports this shape; it does not attest that a custom build matches upstream: ```json { "schema": "gitea-agent.rework-provider/v1", "version": "1.27.3", "aggregate": { "pr": {"field": "comments", "components": ["top_level", "reviews"], "cache_mode": "gitea-conversation-v1"}, "pr_issue": {"field": "comments", "components": ["top_level", "reviews"], "cache_mode": "gitea-conversation-v1"} }, "inline": { "mode": "per_review", "field": "review_comments", "components": ["reviews"] } } ``` ### Cached conversation admission For the explicit cache mode, snapshot counters must be present non-boolean, nonnegative integers no greater than the complete collection-derived conversation count. This admits bounded stale lag, not arbitrary mismatch; counters ahead of the collected count, missing values and malformed scalars remain BLOCK. Upstream `v1.27.3` `services/convert/issue.go` reads stored `Issue.NumComments`. `models/issues/comment.go` counts plain comments and review-summary comments; review creation bypasses the recomputation that plain-comment creation performs. This source pin explains the opt-in policy, not a deployed custom-build attestation. For `create`, fresh readback must prove exactly one canonical top-level addition and the recomputed aggregate must equal the complete post-write conversation count. A stale baseline counter `0` with one review can become `2` after the new plain comment; `before counter + 1` is not the cache-mode rule. All old rows, reviews, inline evidence, Source Issue metadata/comments, identity, body, head and unrelated native fields retain their existing protections. For `reuse`, no addition or counter/metadata refresh is admitted: even a legitimate-looking cache change without the authorized create action is BLOCK. Strict contracts without cache opt-in retain exact snapshot projections and the original delta. For `inline.components`, the explicit projection modes are `["inline"]`, `["reviews"]`, and `["reviews", "inline"]`. The review-only mode projects only the number of reviews into `review_comments`; it does not add per-review inline comments. The inline-only mode projects collected per-review inline comments; the combined mode projects both. These are caller-supplied policies: select one only from independently verified deployed-provider semantics, never automatically detect a mode or choose one to fit observed counters. Both `per_review` and `per_review_or_pr_count` retain the same explicit fallback policy; per-review counters, complete collection pagination, duplicate IDs and record cardinality remain checked. `per_review` requires every exact-review comment endpoint. Only publication `snapshot`/`verify-live` with an explicit `per_review_or_pr_count` capability permits endpoint-unavailable 404/405 fallback. Initial `input-gate` instead requires real inline records and rejects this fallback; its PR-wide counter projections are recorded as non-blocking `counter_diagnostics`, not used to establish collection completeness. See [input-gate.md](input-gate.md) for the phase boundary. Publication counter crosschecks, Source Issue protection and review decision rules are unchanged. Permission errors, redirects, malformed JSON and conflicting metadata never trigger fallback. Fallback uses a fresh PR count, the declared review/inline components and complete review `comments_count` records; disagreement is BLOCK. Counts-only fallback is recorded as such and is not proof of inline comment bodies or a complete inline-record inventory. Native wire-shape reference: the pinned Gitea `v1.24.6` serializers in `services/convert/pull.go` and `modules/structs/{issue,pull}.go` expose PR `url` and `html_url` as HTML URLs, and PR-Issue `pull_request.html_url` without a native `pull_request.url`. Require the actual native relation URL; do not invent a missing field. This source pin is fixture/protocol evidence, not a claim about the deployment version. The caller's independent provider contract still binds that version. In Gitea `v1.27.3`, `services/convert/pull.go` delegates the PR review-comment count to `models/issues/pull.go`'s `GetReviewCommentsCount`, which filters `CommentTypeReview`; this is source evidence for that version's review-only projection, not a blanket claim for all versions or a live-gate success. In `per_review` mode only, an omitted `review_comments` is accepted as zero only when complete per-review records and their counters prove a zero projection. Explicit null, nonzero projection or counts-only fallback with a missing scalar is rejected. Raw pagination Link evidence is preserved and `first`, `prev`, `next`, `last` are checked against the declared total, including the empty sentinel page after an exact multiple of the page size. ## Commands and canonical artifact paths The caller's admitted Issue runtime is `/repositories///worktrees/issue-/runtime/`. Place the canonical `rework-baseline.json` and `rework-receipt.json` there, outside the product checkout. Preserve the baseline SHA-256 returned by its producer independently; never replace the baseline after the external write. Through `terminal`, before publication: ```sh python3 -B /harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py snapshot \ --expected /expected.json \ --provider-contract \ --provider-contract-sha256 \ --actor-id \ --output /rework-baseline.json ``` After the authorized write or existing-comment reuse, through `terminal`: ```sh python3 -B /harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py verify-live \ --expected /expected.json \ --provider-contract \ --provider-contract-sha256 \ --actor-id \ --baseline /rework-baseline.json \ --baseline-sha256 \ --action create \ --output /rework-receipt.json ``` Use `--action reuse` only for an unchanged scoped collection. Do not recapture a baseline or retry a write merely because readback fails. Exit 0 and a receipt are required; stdout reports only the artifact path, fingerprint and read-only flag. The existing offline `verify` command remains compatible, but manually supplied JSON/true flags or archived artifacts alone are not fresh provider readback. ## Verified invariants - Source Issue, PR, underlying PR-Issue, repository, exact source review and heads are independently bound to authenticated endpoint responses. - Pagination is bounded; duplicate IDs, contradictory totals, truncated pages, malformed metadata and off-target continuation links are rejected. - Genuine empty arrays without a count header are supported; null is not silently accepted as an empty collection. - Existing top-level comments, Source Issue fields/comments, native reviews and admitted inline evidence remain unchanged. Create admits exactly one matching impl-authored comment. Reuse admits no addition. - Only provider-declared count projections and the bounded PR/underlying-PR-Issue state-delta/exact-timestamp admission for that verified new comment are admitted; Source Issue metadata, including `updated_at`, is fully immutable. This admission is not proof of causal event attestation; the receipt records that limitation. - The adapter computes normalized inputs and passes them to the same verifier used by `rework_comment.py verify`, rather than accepting caller success flags. ## Approved bounded correction of an existing publication This is a narrow, separately approved exception to normal rework publication—not a new create/reuse policy and not an implementation/review eligibility decision. It applies only when explicit correction approval identifies the same exact, verified impl-authored top-level PR comment ID, and the active Issue lease, exact PR endpoint/metadata binding, complete pagination, unique correlation and absence of duplicate correlation rows are all proven. Approval to correct does not make a stale review admissible, satisfy the normal rework input gate, authorize product edits, or claim normal workflow completion. Before any external write, preserve the original baseline bytes and digest, failed verification output/receipt, and source timeline event as immutable historical evidence. Never replace, hide, repair in place, recapture over, or reinterpret the original baseline or failed verify. A fresh readback snapshot is a separate artifact explicitly labeled as a correction snapshot and bound to the historical-baseline digest and the independently recorded correction authority. It detects present state; it does not erase source drift, retroactively validate the old workflow, or yield its original verified handoff. The only candidate correction is a pure canonical `prepare-correction` plan. The executable command prepares private JSON only; it does not PATCH and it cannot emit a verified handoff/correction receipt: ```sh python3 -B /harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py prepare-correction \ --previous-expected \ --expected \ --comment-id --actor-id \ --output ``` The output is `gitea-rework-correction-plan/v1`, with `approval_required=true`, `prior_handoff_verified=false` and no native write/readback proof. The old and new inputs and any existing plan are never overwritten. The plan proves the legacy body has the exact three label-line format for Source Issue, Pull Request and Source Review, changes those human-readable values to plain decimal numbers, preserves all other human-readable semantics and every hidden marker identifier/correlation input unchanged, then recomputes the marker-free body `result-sha256` and canonical marker. It must prove there are no changed findings, result/status, reviewed/final head, contract digest, PR body semantics, marker identity or other semantic differences. The plan helper is a pure preparation/validation function: it performs no native write and provides no approval assurance. Any non-exact legacy format or additional body/marker change is unsupported and must stop before mutation. Existing same-correlation, different-body cases remain fail-closed under ordinary create/reuse; this correction exception is not permission to create another comment or broaden reuse. Default correction is blocked in this revision because there is no supported client-snapshot CAS execution/verification pair; a lease or capability flag is not provider CAS. Fail closed unless the user separately and explicitly approves the supported ordinary single PATCH exception with its residual read/write race disclosed. Future CAS support requires both execution and matching verifier proof before admission. For that approved path, the main impl workflow under the active Issue lease performs fresh preflight immediately before the one PATCH: exact target, comment ID, unique correlation/no duplicates, author identity, current body, Source Issue, PR, underlying PR-Issue, reviews, inline evidence, complete collections and provider contract must match. No POST, retry, delete, or source-state restoration is allowed. Report `atomic_client_precondition=false` and the residual race for ordinary PATCH. After PATCH, read back the exact same comment ID and verify actor stable account ID, immutable `created_at`, URL, full scoped collection count, exact corrected body, recomputed result digest and unchanged marker identity/correlation. Prove all old rows, reviews, inline evidence, Source Issue fields and entire metadata (including `updated_at`), timeline, and cached counter are unchanged by default. Strict no-timeline-change is the default. A separately approved observed reference-neutralization profile may admit only a pre-existing `comment_ref` pointing to this exact comment/PR transitioning `none` to `neutered`; ID, actor, `created_at`, and target remain immutable, and only `updated_at` plus the exact embedded corrected-comment body/updated_at projection may change. This is observed behavior, not proof of every deployed build; unchanged events remain structurally equal and any other timeline delta fails. Only this comment's body/result hash/marker and provider-verified comment `updated_at` or revision may change. A PR or underlying PR-Issue `updated_at` may change only when exact timestamp is explained by verified correction semantics. Any unexplained delta, missing field, incomplete pagination or unsupported semantic is partial and stops further action. Never delete comments, duplicate-POST, restore or rewrite the Source Issue, perform a native review, or clean up source timeline history. Archive the original failed baseline and failed verification as still incomplete. Issue a distinct correction receipt, explicitly linked to the historical baseline digest, approval authority, fresh correction snapshot, same comment ID and verified post-PATCH readback. This receipt is not the original `gitea-rework-handoff` receipt and cannot turn the original workflow into a verified completion. The source-repair record reports only the bounded correction and its limitations; no product edits or claim that an old/stale review is admissible. ## Limits and verification The CLI enforces verified HTTPS, no redirects, GET-only requests, a one-megabyte response cap, ten-second socket timeouts and a 120-second operation budget. Body budget checks occur between socket reads, so an already-blocking read may add its remaining socket-timeout allowance. There are no automatic retries. The authenticated transport and independently supplied actor/provider/baseline expectations are trust inputs, not a sandbox or proof that arbitrary caller code is honest. The adapter cannot prove missing inline bodies from scalar counts, and state deltas plus exact timestamps are not independent causal-event attestation. Run focused tests with `terminal`: ```sh python3 -B -m unittest harness.tests.impl.test_rework_readback harness.tests.impl.test_rework_readback_transport harness.tests.impl.test_rework_comment -v python3 -B harness/tests/run.py ``` Local synthetic transport and HTTP fixtures validate the executable paths; they do not constitute a live Gitea readback or an external-write completion record.