Problem
A worker can never create the pr-cure-change follow-up that
ADR-0061
requires for a substantive cure. The path is structurally unreachable, so the
only lawful outcome when a cure needs a patch change is block_work and a
hand-minted root from the operator.
Observed 2026-08-28 on a pr-cure item for frostyard/updex#403 (head
5ac9b99). The worker did the mechanical cure, established that the change
needed was a two-file documentation edit on the pull request's own branch, and
tried to propose the pr-cure-change child ADR-0061 prescribes.
complete_work refused the entire completion:
follow-up "pr-cure-change": an existing-pull-request item must bind its pull
request: a review or cure record, or a sourceRef naming <url>@<head SHA>
The three pieces that close the loop:
src/mcp/server.ts:64-76 — followUpSchema is a z.strictObject over
exactly eight fields (kind, objective, instructions,
acceptanceCriteria, allowedActions, delegableActions,
requiredArtifact, executionTarget). There is no sourceRef, cure, or
review, and strictObject rejects any additional key. A worker has no
way to send a binding.
src/queue/store.ts:1830 — the follow-up is validated as
contractProblem({ ...followUp, parentId: parent.id }). Only parentId is
added; the parent's own cure record and sourceRef are not carried onto
the child.
src/queue/store.ts:3002 — hasPullRequestBinding is satisfied only by a
defined cure/review or a sourceRef matching
<prefix>:https://github.com/<owner>/<repo>/pull/<n>@<40 hex>. With (1)
and (2), a proposed child can satisfy none of them, so
contractProblem returns existing-pull-request-without-binding
(store.ts:3082-3087) for every pr-cure-change proposal.
Nothing else mints the kind either: enqueueCureRoot (store.ts:1012) throws
unless kind === "pr-cure", and grep -rn "pr-cure-change" src/ finds only
instruction text, refusal messages, and read-side kind sets — no creation
path anywhere.
ADR-0073's
binding predicate is correct in itself; the defect is that it was applied to
proposed children without giving a cure child any way to inherit the binding
its parent already holds. The two ADRs are individually sound and jointly
unsatisfiable.
The failure is also silent in the worst way: the refusal rejects the whole
complete_work, so a worker that follows ADR-0061 faithfully cannot complete
at all, and the tempting workaround — declaring the child
executionTarget: "new-pull-request" — passes validation while instructing
the next worker to open a second pull request for a head that already has one,
which ADR-0061 forbids.
The change
Let the store derive a cure/review child's binding from its parent instead of
requiring the worker to supply one.
- In
src/queue/store.ts, where complete_work builds each follow-up
(around store.ts:1826-1845), when the proposed child's kind is a
pull-request-bound kind and the parent itself carries a binding, copy the
parent's sourceRef (and cure/review record as applicable) onto the
child before calling contractProblem, and persist it in
insertWork. A child must bind exactly the parent's pull request and head
— never a different one.
- Keep
followUpSchema in src/mcp/server.ts closed. Do not add
sourceRef, cure, or review to it: a worker-supplied binding would let
a proposal name an arbitrary pull request and head, which is precisely what
the predicate exists to prevent. The binding must come from the parent.
- When the parent has no binding and the child declares
executionTarget: "existing-pull-request", keep refusing exactly as today,
with the same code and message.
- Update
docs/specs/work-queue.md (the follow-up contract rules) and
docs/design/queue-execution-boundary.md to state that a
pull-request-bound child inherits its parent's binding and cannot name
another.
- If the resolution changes what ADR-0061 or ADR-0073 decided rather than
implementing them, write the new ADR first and link both — do not amend an
Accepted ADR.
Do not: bump SCHEMA_VERSION or add a migration rung; add or change an
MCP tool; widen followUpSchema; or let a child bind a pull request or head
different from its parent's.
Acceptance criteria
- On the pull request head's
check workflow run, the job log shows the new
test names running and passing, and the run concludes success.
- A new test proves the end-to-end path: a
pr-cure item with a cure
record completes with one pr-cure-change follow-up declaring
executionTarget: "existing-pull-request", the completion is accepted,
and the stored child's binding names the same pull request URL and the same
head SHA as the parent's.
- A new test proves the guard still holds: the same follow-up proposed under
a parent with no binding is still refused with
existing-pull-request-without-binding.
- A new test proves a child cannot bind a different pull request or head
than its parent.
grep -rn "pr-cure-change" src/ shows at least one creation path, not only
instruction text and refusal messages.
git diff origin/main -- src/queue/store.ts shows no change to
SCHEMA_VERSION and no edit or reorder of any existing migration rung;
git diff origin/main -- src/mcp/server.ts shows no new field on
followUpSchema.
npm run check passes.
Blast radius
This widens what a completion may create, so it is the authorization boundary
and deserves the higher risk tier. The mitigation is that the binding is
derived from the parent and never accepted from the worker, so a proposal
cannot reach a pull request its parent does not already own. If an
implementation would let a worker influence which pull request or head a child
binds, that is a defect — block rather than ship it.
Until this lands, every substantive cure dead-ends at block_work and needs
the operator to mint the bound root by hand.
Problem
A worker can never create the
pr-cure-changefollow-up thatADR-0061
requires for a substantive cure. The path is structurally unreachable, so the
only lawful outcome when a cure needs a patch change is
block_workand ahand-minted root from the operator.
Observed 2026-08-28 on a
pr-cureitem for frostyard/updex#403 (head5ac9b99). The worker did the mechanical cure, established that the changeneeded was a two-file documentation edit on the pull request's own branch, and
tried to propose the
pr-cure-changechild ADR-0061 prescribes.complete_workrefused the entire completion:The three pieces that close the loop:
src/mcp/server.ts:64-76—followUpSchemais az.strictObjectoverexactly eight fields (
kind,objective,instructions,acceptanceCriteria,allowedActions,delegableActions,requiredArtifact,executionTarget). There is nosourceRef,cure, orreview, andstrictObjectrejects any additional key. A worker has noway to send a binding.
src/queue/store.ts:1830— the follow-up is validated ascontractProblem({ ...followUp, parentId: parent.id }). OnlyparentIdisadded; the parent's own
curerecord andsourceRefare not carried ontothe child.
src/queue/store.ts:3002—hasPullRequestBindingis satisfied only by adefined
cure/reviewor asourceRefmatching<prefix>:https://github.com/<owner>/<repo>/pull/<n>@<40 hex>. With (1)and (2), a proposed child can satisfy none of them, so
contractProblemreturnsexisting-pull-request-without-binding(
store.ts:3082-3087) for everypr-cure-changeproposal.Nothing else mints the kind either:
enqueueCureRoot(store.ts:1012) throwsunless
kind === "pr-cure", andgrep -rn "pr-cure-change" src/finds onlyinstruction text, refusal messages, and read-side kind sets — no creation
path anywhere.
ADR-0073's
binding predicate is correct in itself; the defect is that it was applied to
proposed children without giving a cure child any way to inherit the binding
its parent already holds. The two ADRs are individually sound and jointly
unsatisfiable.
The failure is also silent in the worst way: the refusal rejects the whole
complete_work, so a worker that follows ADR-0061 faithfully cannot completeat all, and the tempting workaround — declaring the child
executionTarget: "new-pull-request"— passes validation while instructingthe next worker to open a second pull request for a head that already has one,
which ADR-0061 forbids.
The change
Let the store derive a cure/review child's binding from its parent instead of
requiring the worker to supply one.
src/queue/store.ts, wherecomplete_workbuilds each follow-up(around
store.ts:1826-1845), when the proposed child'skindis apull-request-bound kind and the parent itself carries a binding, copy the
parent's
sourceRef(andcure/reviewrecord as applicable) onto thechild before calling
contractProblem, and persist it ininsertWork. A child must bind exactly the parent's pull request and head— never a different one.
followUpSchemainsrc/mcp/server.tsclosed. Do not addsourceRef,cure, orreviewto it: a worker-supplied binding would leta proposal name an arbitrary pull request and head, which is precisely what
the predicate exists to prevent. The binding must come from the parent.
executionTarget: "existing-pull-request", keep refusing exactly as today,with the same code and message.
docs/specs/work-queue.md(the follow-up contract rules) anddocs/design/queue-execution-boundary.mdto state that apull-request-bound child inherits its parent's binding and cannot name
another.
implementing them, write the new ADR first and link both — do not amend an
Accepted ADR.
Do not: bump
SCHEMA_VERSIONor add a migration rung; add or change anMCP tool; widen
followUpSchema; or let a child bind a pull request or headdifferent from its parent's.
Acceptance criteria
checkworkflow run, the job log shows the newtest names running and passing, and the run concludes
success.pr-cureitem with acurerecord completes with one
pr-cure-changefollow-up declaringexecutionTarget: "existing-pull-request", the completion is accepted,and the stored child's binding names the same pull request URL and the same
head SHA as the parent's.
a parent with no binding is still refused with
existing-pull-request-without-binding.than its parent.
grep -rn "pr-cure-change" src/shows at least one creation path, not onlyinstruction text and refusal messages.
git diff origin/main -- src/queue/store.tsshows no change toSCHEMA_VERSIONand no edit or reorder of any existing migration rung;git diff origin/main -- src/mcp/server.tsshows no new field onfollowUpSchema.npm run checkpasses.Blast radius
This widens what a completion may create, so it is the authorization boundary
and deserves the higher risk tier. The mitigation is that the binding is
derived from the parent and never accepted from the worker, so a proposal
cannot reach a pull request its parent does not already own. If an
implementation would let a worker influence which pull request or head a child
binds, that is a defect — block rather than ship it.
Until this lands, every substantive cure dead-ends at
block_workand needsthe operator to mint the bound root by hand.