docs: say that only the worker reports whether a Run delivered #210

Merged
ryangr0 merged 2 commits from ryangr0/replace/199-delivery-from-worker into development 2026-10-04 08:39:33 +00:00 AGit
Owner

Replaces #199 (fix(ploeg): only the worker says whether a Run delivered a pull request) after the Ploeg separation in #206. Its Ploeg part is ploeg-hq/ploeg#49; this PR moves Unfold's pin to that branch and carries the rest.

Commits

  • 81bb7c55 build(ploeg): pin ploeg-hq/ploeg#49 to let only the worker report delivery
  • 083169f8 docs: say that only the worker reports whether a Run delivered (from abb2e2ea2d)

Merge order

  1. #206, the cutover. Until it merges, this PR's diff also shows the cutover commits it is stacked on.
  2. ploeg-hq/ploeg#49.
  3. Move the pin in this PR's build(ploeg) commit to the merge commit on Ploeg's main, then merge. Until then the ploeg-pin check is red by design: it requires the pinned commit on Ploeg's main.

Verification (local, at 083169f8 with Ploeg 5b9cccfc)

  • mise run verify with the result cache, as a pull request runs it: all gates passed. That includes the Ploeg group (its own scripts/verify.sh at the pin), Vloer, the extension, the demo replay check, integration (managed qualification) and docs.
  • Ploeg side: GitHub CI passed on ploeg-hq/ploeg#49.

What happened to each commit of #199

Commit Subject Here
9411473cef fix(ploeg): read a writer's branch on the forge before it counts as no change or updated already on development through #188 and in Ploeg v0.1.0; nothing to apply
abb2e2ea2d fix(ploeg): only the worker says whether a Run delivered a pull request Ploeg part ported to ploeg-hq/ploeg#49; Unfold part re-applied as 083169f8 ("docs: say that only the worker reports whether a Run delivered")

The original had no reviews or comments. Its checks were red because development itself failed at mise install --locked; #206 fixes that lock. No earlier check result carries over.

Original description of #199

Stacked on #188. The first commit here is #188's commit, and this PR's own change is the second commit (abb2e2ea). Merge #188 first; after that, this PR shows only its own commit. First slice of VIK-1732 (handoff EXEC-02). Implements the worker side of ADR-0059, which is still proposed; see "Owner decisions" below.

Problem

An agent could decide whether its Run delivered, and where:

  • Drop box claims were kept. Claude Code and ACP merged the drop box's outcome, links, checkpoint and failureReason whenever the adapter concluded nothing. OpenHands and the exec adapter returned the file as the whole report. resolveOutcome then kept any valid structured outcome together with its own links. So a drop box with {"outcome":"pr_opened","links":[".../other/repo/pulls/123"]} reached ploegd as this Run's pull request; TestAWriterThatClaimsAPullRequestItNeverOpenedDeliversNothing reproduces this.
  • A failed forge read meant "no pull request". The error was logged and the Run carried on as if the forge had answered with nothing.
  • A fork's pull request counted as the Run's. A pull request from a fork whose branch had the same name was treated as the Run's own.
  • A replaced pull request read as an update. If the pull request was replaced during the Run, the new one counted as pr_updated.

Invariant

  • Delivery comes only from the worker. No adapter or agent report can set pr_opened, pr_updated, links, checkpoint, failureReason, verification or delivery. The worker derives them from its own forge reads before and after the harness.
  • Those reads are bound to this Run. The pull request's head must be the Run's branch, in the Work Item's own repository (not a fork), against the target's base branch.
  • A failed read is "unknown", never "no pull request".

Change

  • Drop box. harness.MergeDropBox keeps only what the agent may report: findings, verdict, problem, solution, proposed Work Items, and a non-delivery outcome with its summary and stuck reason. OpenHands, the exec adapter and Claude Code now all read the drop box through it; ACP already did. A new conformance property, DeliveryClaimsNeverSurviveTheAdapter, checks all four adapters.
  • resolveOutcome. Strips the same claims from whatever an adapter returns. A pr_* claim becomes "no structured outcome", so the forge decides.
  • Forge lookup.
    • It returns the pull request's number, head commit and base branch.
    • It skips pull requests whose head repository differs: Forgejo head.repo.full_name, GitLab source_project_id / target_project_id.
    • Each read is tried 3 times (0 s, 2 s, 5 s).
  • Failed reads.
    • Before the harness: a writer does not start. It ends failed/infra_node, before any key is minted or money spent. A reader keeps today's behaviour of warning and continuing.
    • After the harness: the delivery is unknown, and a writer ends stuck (through #188's guard).
  • Delivery record. A new optional delivery field on the outcome carries forge, repository, branch, number, URL, base, head, headBefore, and observed (opened | updated | none | unknown) with a reason. outcomereport.v1 gains it additively: ploegd's decoder is lenient, so older ploegd ignores it.
  • Docs. docs/concepts/inside-a-run.md and a dated note in ADR-0059 describe what is implemented.

Owner decisions this PR takes provisionally (ADR-0059 is proposed)

  • Failure reason before the harness. A failed read before the harness uses the existing infra_node, as the reviewer's fetch failure already does, not a new infra_forge. A failed read after the harness is stuck with no failure reason.
  • Agent links are always dropped. An agent cites URLs in its prose instead. This is the ADR's proposal.
  • Retry policy. 3 tries, 7 s in total.

Not in this slice (next slice of VIK-1732)

  • ploegd does not use delivery yet. It does not check delivery against the claimed Work Item, does not store it, and does not read it in pullRequest (publication), the review watch or readyForReview. A direct outcome API call with crafted links is therefore still trusted by ploegd.
  • pr_updated without a push. A clean exit on an already-open pull request is still pr_updated even when its head did not move. Fixing that needs ploegd to settle with delivery first, otherwise a uniform-plan item would settle done with its pull request still open.
  • Legacy link-only rows. Marking them, and the older-worker window.

Tests

  • Shown failing with the old claim handling. I restored the old MergeDropBox and resolveOutcome handling, ran these, then removed it again:
    • TestAWriterThatClaimsAPullRequestItNeverOpenedDeliversNothing, a real ACP agent writing the exploit drop box. The old handling reported pr_opened with .../other/repo/pulls/123 and failureReason: budget.
    • TestAnAgentCannotClaimDelivery, with 3 cases.
    • TestMergeDropBox_DeliveryClaimsNeverSurvive.
    • The conformance property, which failed for claudecode, openhands and execbin.
  • Also new:
    • fork pull and merge requests are excluded, for Forgejo and GitLab
    • the 3-try read
    • observedDelivery in all 6 states, including a pull request replaced during the Run
    • real pr_opened and pr_updated against a git-http-backend forge, with number, head and headBefore
    • a pre-harness read failure stops the writer, and the harness never runs
    • a post-harness read failure is unknown
    • schema accept and reject cases for delivery
  • Updated tests:
    • The verify tests no longer rely on the agent's pr_opened claim. Their fake agent now really pushes, and the fake forge lists the pull request.
    • "a structured report with no PR keeps its own links" now expects the links to be dropped.

Commands (at abb2e2ea, Go 1.27.1, Node 24.21.0)

  • mise exec -- go test ./pkg/worker/... ./pkg/harness/... -count=1 -v: 281 passed, 0 failed, 2 skipped (TestLiveCanaries, TestLiveClaudeCodeIgnoresTargetHooksAndMCPServers, both opt-in live tests)
  • mise exec -- go test ./... -count=1 in apps/ploeg: 34 packages ok
  • mise run verify: all gates passed (vloer 7, demo-replay 1, vloer-extension 4, ploeg 7, brand 1, site 5, site-demo 1, helm 15, release 3, integration 1, docs 1)

Refs VIK-1732

🤖 Generated with Claude Code

🤖 Generated with Claude Code

Replaces #199 (fix(ploeg): only the worker says whether a Run delivered a pull request) after the Ploeg separation in #206. Its Ploeg part is [ploeg-hq/ploeg#49](https://github.com/ploeg-hq/ploeg/pull/49); this PR moves Unfold's pin to that branch and carries the rest. ## Commits - `81bb7c55` build(ploeg): pin ploeg-hq/ploeg#49 to let only the worker report delivery - `083169f8` docs: say that only the worker reports whether a Run delivered (from `abb2e2ea2d`) ## Merge order 1. #206, the cutover. Until it merges, this PR's diff also shows the cutover commits it is stacked on. 2. [ploeg-hq/ploeg#49](https://github.com/ploeg-hq/ploeg/pull/49). 3. Move the pin in this PR's `build(ploeg)` commit to the merge commit on Ploeg's `main`, then merge. Until then the `ploeg-pin` check is red by design: it requires the pinned commit on Ploeg's `main`. ## Verification (local, at `083169f8` with Ploeg `5b9cccfc`) - `mise run verify` with the result cache, as a pull request runs it: all gates passed. That includes the Ploeg group (its own `scripts/verify.sh` at the pin), Vloer, the extension, the demo replay check, integration (managed qualification) and docs. - Ploeg side: GitHub CI passed on [ploeg-hq/ploeg#49](https://github.com/ploeg-hq/ploeg/pull/49). ## What happened to each commit of #199 | Commit | Subject | Here | | --- | --- | --- | | `9411473cef` | fix(ploeg): read a writer's branch on the forge before it counts as no change or updated | already on `development` through #188 and in Ploeg `v0.1.0`; nothing to apply | | `abb2e2ea2d` | fix(ploeg): only the worker says whether a Run delivered a pull request | Ploeg part ported to ploeg-hq/ploeg#49; Unfold part re-applied as `083169f8` ("docs: say that only the worker reports whether a Run delivered") | The original had no reviews or comments. Its checks were red because `development` itself failed at `mise install --locked`; #206 fixes that lock. No earlier check result carries over. <details><summary>Original description of #199</summary> **Stacked on #188.** The first commit here is #188's commit, and this PR's own change is the second commit (`abb2e2ea`). Merge #188 first; after that, this PR shows only its own commit. First slice of VIK-1732 (handoff EXEC-02). Implements the worker side of [ADR-0059](apps/ploeg/docs/adrs/0059-delivery-facts-come-from-the-forge-never-from-the-agents-outcome.md), which is still **proposed**; see "Owner decisions" below. ## Problem An agent could decide whether its Run delivered, and where: - **Drop box claims were kept.** Claude Code and ACP merged the drop box's `outcome`, `links`, `checkpoint` and `failureReason` whenever the adapter concluded nothing. OpenHands and the exec adapter returned the file as the whole report. `resolveOutcome` then kept any valid structured outcome together with its own links. So a drop box with `{"outcome":"pr_opened","links":[".../other/repo/pulls/123"]}` reached ploegd as this Run's pull request; `TestAWriterThatClaimsAPullRequestItNeverOpenedDeliversNothing` reproduces this. - **A failed forge read meant "no pull request".** The error was logged and the Run carried on as if the forge had answered with nothing. - **A fork's pull request counted as the Run's.** A pull request from a fork whose branch had the same name was treated as the Run's own. - **A replaced pull request read as an update.** If the pull request was replaced during the Run, the new one counted as `pr_updated`. ## Invariant - **Delivery comes only from the worker.** No adapter or agent report can set `pr_opened`, `pr_updated`, `links`, `checkpoint`, `failureReason`, `verification` or `delivery`. The worker derives them from its own forge reads before and after the harness. - **Those reads are bound to this Run.** The pull request's head must be the Run's branch, in the Work Item's own repository (not a fork), against the target's base branch. - **A failed read is "unknown", never "no pull request".** ## Change - **Drop box.** `harness.MergeDropBox` keeps only what the agent may report: findings, verdict, problem, solution, proposed Work Items, and a non-delivery outcome with its summary and stuck reason. OpenHands, the exec adapter and Claude Code now all read the drop box through it; ACP already did. A new conformance property, `DeliveryClaimsNeverSurviveTheAdapter`, checks all four adapters. - **resolveOutcome.** Strips the same claims from whatever an adapter returns. A `pr_*` claim becomes "no structured outcome", so the forge decides. - **Forge lookup.** - It returns the pull request's number, head commit and base branch. - It skips pull requests whose head repository differs: Forgejo `head.repo.full_name`, GitLab `source_project_id` / `target_project_id`. - Each read is tried 3 times (0 s, 2 s, 5 s). - **Failed reads.** - Before the harness: a writer does not start. It ends `failed`/`infra_node`, before any key is minted or money spent. A reader keeps today's behaviour of warning and continuing. - After the harness: the delivery is `unknown`, and a writer ends `stuck` (through #188's guard). - **Delivery record.** A new optional `delivery` field on the outcome carries forge, repository, branch, number, URL, base, `head`, `headBefore`, and `observed` (`opened` | `updated` | `none` | `unknown`) with a reason. `outcomereport.v1` gains it additively: ploegd's decoder is lenient, so older ploegd ignores it. - **Docs.** `docs/concepts/inside-a-run.md` and a dated note in ADR-0059 describe what is implemented. ## Owner decisions this PR takes provisionally (ADR-0059 is proposed) - **Failure reason before the harness.** A failed read before the harness uses the existing `infra_node`, as the reviewer's fetch failure already does, not a new `infra_forge`. A failed read after the harness is `stuck` with no failure reason. - **Agent links are always dropped.** An agent cites URLs in its prose instead. This is the ADR's proposal. - **Retry policy.** 3 tries, 7 s in total. ## Not in this slice (next slice of VIK-1732) - **ploegd does not use `delivery` yet.** It does not check `delivery` against the claimed Work Item, does not store it, and does not read it in `pullRequest` (publication), the review watch or `readyForReview`. A direct outcome API call with crafted links is therefore still trusted by ploegd. - **`pr_updated` without a push.** A clean exit on an already-open pull request is still `pr_updated` even when its head did not move. Fixing that needs ploegd to settle with `delivery` first, otherwise a uniform-plan item would settle `done` with its pull request still open. - **Legacy link-only rows.** Marking them, and the older-worker window. ## Tests - **Shown failing with the old claim handling.** I restored the old `MergeDropBox` and `resolveOutcome` handling, ran these, then removed it again: - `TestAWriterThatClaimsAPullRequestItNeverOpenedDeliversNothing`, a real ACP agent writing the exploit drop box. The old handling reported `pr_opened` with `.../other/repo/pulls/123` and `failureReason: budget`. - `TestAnAgentCannotClaimDelivery`, with 3 cases. - `TestMergeDropBox_DeliveryClaimsNeverSurvive`. - The conformance property, which failed for claudecode, openhands and execbin. - **Also new:** - fork pull and merge requests are excluded, for Forgejo and GitLab - the 3-try read - `observedDelivery` in all 6 states, including a pull request replaced during the Run - real pr_opened and pr_updated against a git-http-backend forge, with number, head and headBefore - a pre-harness read failure stops the writer, and the harness never runs - a post-harness read failure is unknown - schema accept and reject cases for `delivery` - **Updated tests:** - The verify tests no longer rely on the agent's `pr_opened` claim. Their fake agent now really pushes, and the fake forge lists the pull request. - "a structured report with no PR keeps its own links" now expects the links to be dropped. ## Commands (at abb2e2ea, Go 1.27.1, Node 24.21.0) - `mise exec -- go test ./pkg/worker/... ./pkg/harness/... -count=1 -v`: 281 passed, 0 failed, 2 skipped (`TestLiveCanaries`, `TestLiveClaudeCodeIgnoresTargetHooksAndMCPServers`, both opt-in live tests) - `mise exec -- go test ./... -count=1` in apps/ploeg: 34 packages ok - `mise run verify`: all gates passed (vloer 7, demo-replay 1, vloer-extension 4, ploeg 7, brand 1, site 5, site-demo 1, helm 15, release 3, integration 1, docs 1) Refs VIK-1732 🤖 Generated with [Claude Code](https://claude.com/claude-code) </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
711a4814 moved uv to 0.12.22 in mise.toml but left the lockfile at
0.12.21. mise-action runs `mise install --locked`, which refuses a
version the lockfile does not hold, so the checks job has failed before
any gate since that commit, on development and on every pull request.

`mise lock uv` (mise 2026.9.18) records 0.12.22 for all seven platforms.
The openspec lock files it deletes under .mise/locks are kept.

Refs: https://github.com/webgrip/unfold/issues/2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the vendored apps/ploeg tree with a gitlink to
https://github.com/ploeg-hq/ploeg.git at v0.1.0
(87f8dc45a0ea768c6ab95196d8b99d481df10c65), which was extracted from
this repository at 9c1d53f.

- mise run setup, the verify, docs and demo checkouts and the TechDocs
  prepare commands initialise the submodule.
- scripts/ploeg-pin.mjs refuses vendored source, another repository and
  an uninitialised or modified checkout. The ploeg-pin job also requires
  the pinned commit on Ploeg's main, and the release waits for it.
- verify runs Ploeg's own scripts/verify.sh at the pin and compiles the
  unified demo helper, which now imports github.com/ploeg-hq/ploeg.
- The docs build still renders Ploeg's pinned pages, but no longer
  regenerates or validates Ploeg's configuration reference, domain pages
  or decision ledger, and it links Ploeg's source files on GitHub at the
  pinned commit. The combined glossary keeps a decision that a pinned
  model cites by URL instead of mangling it into a relative path.
- Renovate ignores apps/ploeg, drops the Go overlay and leaves the pin
  to people. CI no longer builds the ploegd image context.

Refs: https://github.com/webgrip/unfold/issues/2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unfold's train now versions Vloer only; github.com/ploeg-hq/ploeg
versions and publishes Ploeg with GitHub Actions.

- on_release_published.yml drops the Ploeg chart, image, signing and
  distribution jobs. The Vloer publisher is the only one, so it takes
  the GitHub release out of draft itself.
- publish_release.py and publish_chart.py refuse ploeg before any Git,
  network or file access. The Go module export to github.com/webgrip/ploeg
  is gone, including the call that disabled GitHub Actions there.
- release-prepare.mjs and apps/.releaserc.cjs touch only Vloer's chart,
  and a commit scoped ploeg never releases Unfold.
- release-floors.json keeps Ploeg's floor and withdrawn 1.0.0-rc.1, marks
  the component retired after 0.4.0-rc.35, and both loaders refuse a
  train that versions a retired component.
- The release preflight no longer checks registry access for ploegd or
  charts/ploeg.

Refs: https://github.com/webgrip/unfold/issues/2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: record that Unfold pins Ploeg and releases only Vloer
All checks were successful
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 42s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 16s
[Workflow] On Pull Request / checks (pull_request) Successful in 7m8s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
1bca2ac69b
ADR-0019 records the consumer side of the separation approved in
webgrip/unfold#1: Ploeg lives in github.com/ploeg-hq/ploeg, Unfold pins
it as a submodule, and the unfold-v train versions Vloer only. It
supersedes ADR-0004; ADR-0001 and ADR-0018 get dated notes.

README, AGENTS.md, NOTICE, the team-silver skill and the current pages
now say where Ploeg lives, how the pin moves, what Unfold releases and
where Ploeg's artifacts come from.

Refs: https://github.com/webgrip/unfold/issues/2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build(docs): treat Ploeg's archived history pages as records
All checks were successful
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 24s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 29s
[Workflow] On Pull Request / checks (pull_request) Successful in 2m22s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
3870a8b560
Ploeg's main keeps its pre-separation release history in
docs/history/legacy-changelog.md, a record no current page links. Unfold
renders Ploeg's docs from the pinned commit, so any pin past v0.1.0 failed
the docs build with that page as an orphan. ploeg/history now joins
ploeg/backlog as a record path: kept, marked "not current guidance" and
left out of the nav and search.

Refs: https://github.com/webgrip/unfold/issues/2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves apps/ploeg to 5b9cccfc, the head of ploeg-hq/ploeg#49, which is
the Ploeg side of Unfold PR 199. Once that pull request merges, move
the pin to its merge commit on Ploeg's main. Until then the ploeg-pin check
stays red, as it should.

Refs: https://github.com/ploeg-hq/ploeg/pull/49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: say that only the worker reports whether a Run delivered
Some checks failed
[Workflow] On Pull Request / ploeg-pin (pull_request) Failing after 29s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 18s
[Workflow] On Pull Request / checks (pull_request) Successful in 7m47s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
083169f814
The Unfold side of "fix(ploeg): only the worker says whether a Run
delivered a pull request" (ploeg-hq/ploeg#49): inside-a-run says the
worker reads delivery on the forge and drops delivery claims from the
agent's drop box. ADR-0059 is still proposed.

Refs VIK-1732

Replaces-commit: abb2e2ea2d (#199)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ryangr0 force-pushed ryangr0/replace/199-delivery-from-worker from 083169f814
Some checks failed
[Workflow] On Pull Request / ploeg-pin (pull_request) Failing after 29s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 18s
[Workflow] On Pull Request / checks (pull_request) Successful in 7m47s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
to 99771e7b7d
Some checks failed
[Workflow] On Pull Request / checks (pull_request) Has been cancelled
[Workflow] On Pull Request / warnings (pull_request) Has been cancelled
[Workflow] On Pull Request / ploeg-pin (pull_request) Has been cancelled
[Workflow] On Pull Request / release-policy (pull_request) Has been cancelled
2026-10-04 07:08:56 +00:00
Compare
ryangr0 force-pushed ryangr0/replace/199-delivery-from-worker from 99771e7b7d
Some checks failed
[Workflow] On Pull Request / checks (pull_request) Has been cancelled
[Workflow] On Pull Request / warnings (pull_request) Has been cancelled
[Workflow] On Pull Request / ploeg-pin (pull_request) Has been cancelled
[Workflow] On Pull Request / release-policy (pull_request) Has been cancelled
to 59b9591fa6
All checks were successful
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 44s
[Workflow] On Pull Request / release-policy (pull_request) Successful in 50s
[Workflow] On Pull Request / checks (pull_request) Successful in 6m14s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
2026-10-04 07:12:37 +00:00
Compare
ryangr0 merged commit db7e50e63b into development 2026-10-04 08:39:33 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/unfold!210
No description provided.