Skip to content

The Spec run owns its lifecycle to its Delivery, tested in-process behind its seam #419

Closed

Description

@JacobStephens2

What to build

The Spec run owns its whole lifecycle behind its seam, from the Ticket loop through the Spec review's Delivery, the same way the Run, the Architect run and the Pickup run now do. Today only the Ticket loop sits behind the Spec run's seam. The steps after it are wired straight to the real Spec PR and the real Delivery, so their rules can only be checked by the slow end-to-end Spec run tests. Those steps are: opening or showing the Spec PR once every Ticket has landed, the Spec review's opening, putting the Tickets checklist back before ready, showing it again after a failed Delivery, the Spec PR's URL on a Failed spec run, and the Ticket lines on every ending.

After this change, one function takes a Spec from its Tickets to a Reached or a Failed spec run through the seam. In-process tests drive that function with the existing scripted fake, extended to cover the new steps. The public entry point only resumes the Spec PR, builds the real adapter and calls that function. Nothing changes in behaviour: stderr, the Spec PR, the notification and exit codes stay exactly as they are.

Acceptance criteria

  • The Spec run's seam also covers what comes after the Ticket loop: telling the Spec PR the last Ticket landed (returning its URL), the Spec PR's URL if it is open, putting the Tickets checklist back, and the Delivery of the Spec PR given an Opening and a step to run before ready.
  • A single lifecycle function, generic over that seam, holds every rule from the Ticket loop to the ending. This includes the Spec review's Opening (its kind, its Session prompt naming the Spec PR, and catching up from origin), the checklist put back before ready, the checklist shown again after a failed Delivery, the Spec PR's URL on a failed landing, and the Ticket lines on both Reached and Failed spec run.
  • A failed landing decides whether it was interrupted by asking the seam, not the process-wide interrupt flag. The lifecycle builds that Failed spec run explicitly, the way the Delivery's Failed run path does, not through the conversion from a bare error.
  • The real adapter owns the worktree, the Delivery and the Spec PR, and hands the worktree and Delivery over once, when the Delivery starts. The public entry point only resumes the Spec PR (a failed resume still fails the Spec run with no lines), builds the adapter and calls the lifecycle function.
  • In-process tests, through the scripted fake, cover:
    • every Ticket lands, then the Spec PR is told so, then the Delivery runs with the Spec review's Opening, with the put-back inside it before ready, and the Reached carries the Ticket lines;
    • a failed landing means no Delivery, with the Spec PR's URL (when open) and the Ticket lines on the Failed spec run;
    • a failed landing with an interrupt requested ends as interrupted;
    • the Spec PR that can't be opened after the last landing means no Delivery;
    • a failed Delivery shows the checklist again and keeps the Delivery's own PR URL, log and the Ticket lines;
    • a put-back that fails fails the Delivery.
  • Replace, don't layer: delete each end-to-end Spec run test whose assertions are now all covered in-process. Likely candidates are the ones on the checklist being put back after the Spec review and after a failed Spec review. Keep those that check the real Delivery, the Spec PR on GitHub, child Runs, notifications or the Session prompt's text.
  • cargo test and cargo clippy pass.

Blocked by

None (can start immediately).

Decisions

  • Extend the existing Ticket-loop seam rather than add a second trait. The Spec run gets one seam for everything it does outside itself, as the Run's own start has one seam that includes deliver and spec_run. Two traits would split one lifecycle across two fakes.
  • The Delivery goes behind the seam as deliver(opening, before_ready), with before_ready a step that takes the seam itself. This keeps the rule "put the checklist back before ready" in the lifecycle, where tests see it, instead of hiding it in the adapter. The Delivery's own steps already take their before-ready step this way. A throwaway prototype confirmed the borrow works: the adapter takes the worktree and Delivery out of itself (each held in an Option), then calls the Delivery with a closure that borrows the adapter for the put-back.
  • The Spec PR's resume stays outside the lifecycle, in the public entry point. Its rule (an open Spec PR goes back to a draft) is already tested in-process in the Spec PR module. A failed resume needs no lines and no PR URL, so it is wiring, like opening the Launch directory before the Run's start.
  • The adapter owns the Spec PR by value instead of borrowing it. The lifecycle reaches it only through the seam, and the Spec PR outlives the hand-over of the worktree to the Delivery inside the adapter.
  • landed returns the Spec PR's URL. The lifecycle needs it for the Spec review's Session prompt. The Ticket loop ignores it.
  • The Spec review's Session prompt is built in the lifecycle, not the adapter, so tests can assert its Opening. The lifecycle takes the Spec, the Base branch and the Spec branch as plain arguments, because the adapter has these as well.
  • The interrupt on a failed landing is read through the seam. That conversion currently reads the process-wide flag, which an in-process test that raises a real signal can set at the same moment. Reading it through the seam is in scope because the lifecycle is what builds that Failed spec run. Other uses of that conversion elsewhere are out of scope.
  • No new domain terms, no ADR. Spec run, Spec PR, Tickets checklist, Spec review, Failed spec run and Delivery are all in CONTEXT.md already. The change is reversible and follows the established seam pattern, so it does not qualify for an ADR.

Architecture review

Strength: Strong · Dependencies: in-process (outside effects behind the Spec run's seam, an in-memory scripted fake in tests)

Files: src/spec_run.rs, src/spec_run/spec_pr.rs, src/delivery.rs, src/run.rs, tests/spec_run.rs

Before

flowchart LR
  Run[Run start] --> Entry[Spec run entry]
  Entry --> Loop[Ticket loop]
  Loop --> Seam1[Spec run seam]
  Entry -.untested.-> SpecPR[Spec PR]
  Entry -.untested.-> Delivery[Delivery]
  Entry -.untested.-> Flag[interrupt flag]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class SpecPR,Delivery,Flag leak
Loading

After

flowchart LR
  Run[Run start] --> Entry[Spec run entry]
  Entry --> Life
  subgraph Life[Spec run lifecycle]
    direction LR
    Loop[Ticket loop] --> Review[Spec review opening] --> Deliver[deliver, put back before ready]
  end
  Life --> Seam[Spec run seam]
  Seam --> Real[Child Runs, Spec PR, Delivery]
  Seam --> Fake[Scripted fake]
  classDef deep stroke-width:3px;
  class Life deep
Loading

Problem: the Spec run's steps after the Ticket loop call the real Spec PR, Delivery and interrupt flag directly, so their rules are only testable through slow end-to-end Spec runs.

Solution: widen the Spec run's seam to the Spec PR's last landing, the put-back and the Delivery, and move the whole lifecycle into one function tested in-process.

Wins

  • Tests hit one interface
  • Locality: Spec run lifecycle in one module
  • Drop redundant end-to-end Spec run tests
  • No process-wide interrupt read
  • Matches Run, Architect and Pickup seams

Activity

  1. added
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    architect-planAn Architect plan: the Spec or Ticket an Architecture review published
    in-progressA Claim: thirdshift has taken this issue
    and removed
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    architect-planAn Architect plan: the Spec or Ticket an Architecture review published

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions