Repository navigation
Description
What to build
Deepen the existing child Run module so a Spec run starts concurrent Ticket Runs and receives their endings through one owned collection. The collection owns launch registration, polling, completion retirement and shutdown. A Spec run keeps its Ticket graph and Spec PR accounting, while every active child Run has time to finish its Failed run salvage before the Spec branch worktree is removed.
Ordinary errors and Command interruption already drain started Tickets. The uncovered teardown case is dropping the owner, including unwinding: today its worktree is dropped before the child handle map, and that map stops and waits for children one at a time. Sibling cleanup can require both children to receive SIGTERM before either finishes.
Acceptance criteria
- The Spec run adapter uses one owned collection from the child Run module to start a Ticket Run and receive the next numbered ending. It no longer owns a handle map, ending queue, child polling loop, retirement rules or supervision sleep.
- Receiving the next ending polls every active child before returning any queued result; completed handles are retired after their process is reaped and readers joined. Each ending is delivered once, empty collections return immediately, and pending children keep the existing 100 ms supervision cadence during interruption.
- Starting a duplicate outstanding issue number fails before spawning a child or replacing an existing handle. Launch failure preserves ownership of existing children; the Spec run still stops scheduling and drains them through its established error path.
- Dropping the collection, including during unwinding, sends the existing nonblocking, once-only SIGTERM to every live owned child before waiting for any child. It then reaps every child and joins every reader, leaving the Command's interruption flag unchanged and granting graceful salvage unbounded time.
- The Spec run adapter destroys the owned child collection before its Worktree on teardown. Normal draining, opening the Spec PR, Spec review and Delivery continue to follow the existing lifecycle.
- Ticket graph readiness, parallelism, result order, failure causes, Base fix accounts, progress wording, Tickets checklist updates and final Ticket outcome lines retain their current behavior. Base fix continues using the existing standalone child Run wait.
- Tests through the collection's interface with finite local child substitutes cover empty and pending collections, distinct children completing out of order, simultaneous completions delivered exactly once, launch and duplicate refusal, interruption reaching all active children even with an ending queued, and ordinary drop and unwinding with sibling cleanup rendezvous. Assert saved work, one stop signal per live child and no surviving owned children after cleanup.
- Keep the existing single-child execution tests and Spec run lifecycle tests. The end-to-end sibling interruption/salvage regression and final drained-Ticket accounting regression pass, together with repository formatting, Clippy and test checks.
Blocked by
None (can start immediately).
Decisions
The design tree is settled in three rounds. Round 1 fixes scope and plan size. Round 2 settles the interface, launch/identity rules, ending delivery and teardown; teardown determines Worktree lifetime. Round 3 settles implementation locality, the test seam, coverage and documentation. No decision needs a human or remains open.
- Scope — existing child Run module. Extend its depth to concurrent ownership. Keep Ticket scheduling and Spec PR accounting in Spec run, and preserve ADR-0006's separate child thirdshift processes. This removes execution knowledge from a domain caller without changing who decides which Ticket runs.
- Plan size — one standalone Ticket. The collection, its sole production caller and focused execution regressions fit one fresh context window. There is no prefactor or independently valuable second slice, so there is no Spec or parent issue.
- Interface — an opaque concrete collection. Provide construction, start with the existing child Run launch facts, and next-ending delivery as an optional pair of issue number and the existing child Run result. Empty returns no pair immediately; with outstanding children it waits for the next ending. This hides ownership bookkeeping and avoids a caller precondition that would otherwise permit an empty collection to wait forever.
- Launch registration — inside the owner. The collection calls the existing owned launch path and retains each successfully started Handle before returning. A failed launch leaves earlier children owned and running. This concentrates spawn-to-retention knowledge and preserves the Spec run's existing stop-scheduling-and-drain behavior rather than cancelling healthy siblings on an ordinary error.
- Identity — one outstanding child per issue number. Reject an identity already active or queued before starting its replacement. The collection belongs to one Spec run, whose Tickets share one repository. Once an ending is delivered, that identity may be used again; the Ticket graph remains responsible for never restarting a finished Ticket. This prevents ownership loss without duplicating scheduling history.
- Ending delivery — sweep all, then deliver one. Each call first polls every active Handle, collects terminal results, removes completed Handles and then returns the oldest queued ending. Preserve the existing ascending-number traversal for endings discovered in one sweep and preserve previously queued order. Waiting uses the existing 100 ms child supervision cadence, including after interruption, because GitHub polling's interruption behavior cannot supervise graceful child cleanup.
- Teardown — stop all before draining any. Use the existing private nonblocking Handle stop operation on every active child, then drive existing completion until all processes are reaped and readers joined. Discard undelivered results only when the collection itself is dropped. Do not add deadlines, force killing or synthetic global interruption: child Runs own their Failed run salvage, and one child's cleanup can depend on another's.
- Worktree lifetime — child ownership drops first. Declare the collection before the Worktree in the Spec run adapter and document this lifetime requirement. Rust drops fields in declaration order; the collection's Drop therefore finishes collective shutdown before Worktree cleanup begins, including during unwinding. This avoids an additional explicit shutdown operation at the caller's interface.
- Domain effects — retain current owners. Keep the
#<n> landed/failed/interruptedprogress line in the Spec run adapter after it receives an ending, and keep graph refresh, checklist publication and final accounting in the existing loop. These are Ticket outcomes, while the collection owns execution; preserving that seam prevents it from learning Spec PR policy. - Implementation locality — one private collection implementation beside Handle. Move the production map, queue, sweep and wait logic behind the child Run interface. Hide Handle polling from external production callers once Spec run migrates; keep standalone start and blocking wait for Base fix. Do not merge child Run supervision with the ordinary CLI process module: their graceful salvage and forced process-tree stop rules intentionally differ.
- Test seam — the interface is the test surface. Use real finite local executables behind the existing private launch seam, with distinct issue numbers. Production thirdshift and finite test executables are the concrete adapters at this local-substitutable seam. Keep fixture substitution internal; add no public process trait, mock-only interface or configurable signal behavior.
- Completion tests — observe outcomes. Check numbers and returned endings, saved files, refusal before launch, and absence of surviving owned children. Cover queued endings while another child must still receive interruption. These tests verify ownership and delivery contracts without inspecting maps, queue lengths or implementation call sequences.
- Teardown tests — sibling rendezvous on drop and unwind. Two finite child substitutes each record SIGTERM and await the other's stopping marker before saving and exiting; run both ordinary owner drop and caught unwinding. Assert each receives one TERM and both finish saving and are reaped. Exercise global signals only in isolated test processes, following the existing signal-test support, so recorded interruption cannot leak between tests.
- Existing tests — retain different guarantees. Single-child fault, transport, signal and reaping tests continue to verify Handle. Spec run graph and accounting tests continue to verify the domain loop. Keep the end-to-end parent-only interruption test whose sibling Failed-run pushes rendezvous. Replace any tests that only restate the removed caller supervision bookkeeping with collection-interface tests; do not layer duplicate assertions over that bookkeeping.
- Domain documentation — no glossary or ADR change required. The design deepens the existing child Run module and changes reversible ownership implementation. It introduces no domain concept, changes no Command interruption policy and contradicts no ADR. No CONTEXT or ADR edit is part of this Ticket.
- Selection — highest uncovered Strong opportunity. Recent child Run ownership and Command interruption changes still leave collective supervision rules in their caller, and a current regression already proves those rules matter. All open issue titles and bodies were reviewed; none proposes this deepening. This gives immediate locality and test leverage without speculative future variation.
Baseline validation on main at dcf2ed85cbd2b46498a1948cb76c00525f1e762b: all 11 child Run execution tests passed, and the end-to-end parent-only interruption/sibling-salvage regression passed. These establish current behavior; the new collection and teardown tests are this Ticket's work.
Architecture review
Strength: Strong · Dependencies: local-substitutable
Files: src/child_run.rs, src/child_run/runs.rs (new), src/child_run/execution_tests.rs, src/spec_run.rs, tests/spec_run.rs
Before
flowchart LR
subgraph Spec[Spec run module]
Graph[Ticket graph and Spec PR accounting]
Supervision[Child map, queue, sweep and wait]
Owner[Spec run owner teardown]
end
Graph --> Supervision
Supervision -. supervision rules leak .-> Handle[child Run module: one Handle]
Owner --> Worktree[Worktree cleanup]
Worktree -. then drops children one at a time .-> Handle
Handle --> Processes[child thirdshift processes]
classDef leak stroke:#dc2626,stroke-width:2px;
class Supervision,Worktree leak
After
flowchart LR
subgraph Spec[Spec run module]
Graph[Ticket graph and Spec PR accounting]
Owner[Spec run owner teardown]
end
subgraph Children[child Run module]
Supervision[Owned concurrent child Runs]
Handle[one Handle]
Supervision --> Handle
end
Graph --> Supervision
Owner -->|stop all and drain| Supervision
Owner -->|after child cleanup| Worktree[Worktree cleanup]
Handle --> Processes[child thirdshift processes]
classDef deep stroke-width:3px;
class Children deep
Problem: Spec run carries concurrent supervision rules across the child Run seam, and owner unwinding cleans its Worktree before stopping children one at a time.
Solution: Deepen the child Run module behind one collection interface, owning completion retirement and collective shutdown before Worktree teardown.
Deletion test: Deleting the owner restores maps, queues, polling, retirement and shutdown ordering to callers; its depth concentrates behavior rather than adding a wrapper.
Wins
- Locality for collective supervision
- Leverage for child Run callers
- Tests cross the ownership interface
- Shutdown preserves sibling salvage
- Adapter substitution stays internal
Activity