PRD: Convoy Stage & Launch (gt convoy stage, gt convoy launch)
Problem
1. No pre-flight validation before dispatching work. gt sling dispatches tasks immediately with no structural analysis. A user who runs gt sling task1 task2 task3 has no way to know beforehand that task2 has a circular dependency, task3's rig doesn't exist, or that all three tasks will try to run in parallel when they should be serialized. Problems surface only after polecats are spawned and work is underway — at which point cleanup is manual and error-prone.
2. No visibility into execution order. The daemon's feeder respects blocks dependencies at runtime, but the user has no way to preview the dispatch plan. When /design-to-beads creates an epic with 15 tasks across 3 sub-epics, the user cannot see which tasks will run in Wave 1, which are blocked until Wave 2, or whether the dependency graph even makes sense. The execution order is a black box until tasks start (or fail to start).
3. Batch sling dispatches without dependency-aware ordering. gt sling task1 task2 task3 iterates through tasks sequentially and spawns a polecat for each, regardless of dependency ordering. The daemon's isIssueBlocked check prevents re-feeding blocked tasks after a close event, but the initial batch dispatch does not check blocking deps before spawning. This means tasks that should wait for blockers to complete get polecats spawned and may execute prematurely.
4. No staged state for convoys. Convoys go directly from creation to open, which immediately makes them eligible for daemon feeding. There is no way to create a convoy, inspect it, validate it, and then activate it. Users who want a "dry run" have no mechanism — the act of creating a convoy is the act of launching it.
5. No single command bridges design-to-beads output to dispatch. The workflow from /design-to-beads (which creates epics, tasks, and dependencies) to actual polecat dispatch requires the user to manually identify leaf tasks, determine the correct rig, and run gt sling. For a complex epic with sub-epics and cross-task dependencies, this manual step is tedious and error-prone — exactly the kind of work the system should automate.
Overview
Add gt convoy stage and gt convoy launch commands that enable a structured workflow for dispatching work: analyze bead dependencies, compute execution waves, surface problems, create a staged convoy, and dispatch Wave 1 tasks. This bridges the gap between /design-to-beads output (or manually created beads) and reliable multi-task convoy execution.
The core insight: staging is a pre-flight check that catches dependency cycles, routing problems, orphaned tasks, and capacity issues before any polecats are spawned. Launching is the act of activating a validated convoy.
Goals
- Enable
gt convoy stage <epic-id | task1 task2 ... | convoy-id>as a pre-flight analysis and staging command - Enable
gt convoy launch <convoy-id>as an alias forgt convoy stage <convoy-id> --launch - Compute execution waves from blocks deps and display them alongside the DAG tree
- Surface errors (cycles, invalid rigs) and warnings (parked rigs, missing branches, capacity) with clear categorization
- Create staged convoys with
staged_readyorstaged_warningsstatus based on analysis results - On launch, dispatch only Wave 1 (unblocked) tasks; daemon feeds subsequent waves
- Support
--jsonfor programmatic consumption (design-to-beads pipeline)
Quality Gates
These commands must pass for every user story:
go vet ./... && go build ./... && go test ./internal/cmd/... ./internal/convoy/... ./internal/daemon/... -count=1
User Stories
US-001: Bead validation and DAG construction
Description: As a user, I want gt convoy stage to validate that all specified beads exist and construct the dependency graph, so that I catch typos and missing beads before any work is dispatched.
Acceptance Criteria:
-
gt convoy stage <bead-id>runsbd showon each bead and errors if any don't exist - For epic input: walks the full parent-child tree (sub-epics, their children, recursively) to collect all descendant beads
- For task list input: analyzes exactly the given tasks (no auto-expansion to parent epic)
- For convoy input: reads the convoy's tracked beads via
bd dep list --type=tracks - Constructs an in-memory DAG from
blocks,conditional-blocks, andwaits-fordependencies between the collected beads -
parent-childdeps are recorded for hierarchy display but do NOT create execution edges
US-002: Error detection (blocks staging)
Description: As a user, I want staging to detect fatal structural problems and refuse to create a convoy, so that I never launch a fundamentally broken work plan.
Acceptance Criteria:
- Dependency cycles (circular
blocks/conditional-blocks/waits-forchains) are detected and reported with the cycle path - Beads with no valid rig (prefix not mapped in
routes.jsonl, or resolves to empty) are detected and reported - When errors are found: no convoy is created, no bead statuses are modified
- Error output lists each error with the affected bead ID(s) and a suggested fix
- Exit code is non-zero when errors are found
US-003: Warning detection (allows staging with acknowledgment)
Description: As a user, I want staging to surface non-fatal issues as warnings so that I can decide whether to fix them or proceed to launch with --force. (Note: --force is a launch-time flag, not a stage flag. Staging with warnings always creates a staged_warnings convoy; the decision point is at launch.)
Acceptance Criteria:
- Orphan detection (epic input only): tasks not reachable from the epic's descendant tree, or tasks with no blocking deps from any other staged task (isolated in the wave graph). For task-list input, isolation is expected (all tasks are explicitly selected) — no orphan warning.
- Missing integration branches on sub-epics (warn, don't block)
- Parked or unavailable target rigs
- Cross-rig routing: beads that resolve to different rigs than expected
- Capacity estimation: number of polecats needed per wave vs available capacity (informational)
- Warnings are clearly distinguished from errors in output
- When only warnings (no errors): convoy is created with
staged_warningsstatus
US-004: Wave computation
Description: As a user, I want to see which tasks can run in parallel (waves) based on their dependency ordering, so that I understand the execution plan before launching.
Acceptance Criteria:
- Wave 1 = all tasks with no unsatisfied
blocks/conditional-blocks/waits-fordeps within the staged set - Wave N+1 = tasks whose blockers are all in Wave N or earlier
- Tasks not reachable by any blocking dep chain are placed in Wave 1 (maximum parallelism)
- Epics and non-slingable types are excluded from waves (they're containers, not dispatchable work)
- Wave computation handles the full descendant set (for epic input) or just the given tasks (for task list input)
US-005: DAG tree display
Description: As a user, I want to see the full epic hierarchy as an ASCII tree, so that I understand the structural organization of the work.
Acceptance Criteria:
- For epic input: display the full parent-child tree with indentation showing nesting
- Each node shows: bead ID, title, type, status, and target rig
- Sub-epics are visually distinct from leaf tasks
- Blocked tasks show their blocker(s) inline
- For task list input: show a flat list (no tree, since there's no epic hierarchy)
- Tree is displayed before the wave table
US-006: Wave dispatch plan display
Description: As a user, I want to see a wave-by-wave dispatch plan table showing what will be dispatched and when, so that I can validate the execution order.
Acceptance Criteria:
- Table shows Wave number, task IDs, task titles, target rig, and blockers (if any)
- Displayed after the DAG tree
- Summary line: total waves, total tasks, estimated parallelism per wave
- For
staged_warningsconvoys: warnings are printed after the wave table
US-007: Convoy creation with staged status
Description: As a user, I want gt convoy stage to create a convoy with staged_ready or staged_warnings status, so that the convoy exists in beads and can be launched later.
Acceptance Criteria:
-
staged_ready: convoy created when analysis finds no errors and no warnings -
staged_warnings: convoy created when analysis finds warnings but no errors - No convoy created when analysis finds errors
- Convoy tracks all slingable beads in the analyzed set via
tracksdeps - Convoy description includes wave count, task count, and staging timestamp
- Convoy ID is printed to console for use with
gt convoy launch - Re-staging an existing convoy-id re-analyzes and updates the status (may change from ready to warnings or vice versa)
US-008: Launch — dispatch Wave 1
Description: As a user, I want gt convoy stage --launch (or gt convoy launch) to activate the convoy and dispatch all Wave 1 tasks, so that work begins immediately after validation.
Acceptance Criteria:
- Transitions convoy status from
staged_readytoopen - For
staged_warningsconvoys: requires--forceflag to launch (otherwise errors with warning summary) - Dispatches all Wave 1 tasks via internal Go dispatch functions (one polecat per task, no auto-convoy creation — the staged convoy already tracks these tasks)
- Subsequent waves are NOT dispatched — the daemon feeds them as Wave 1 tasks close
- If a Wave 1 dispatch fails, continues to next task (same as batch sling iteration behavior)
US-009: Launch console output
Description: As a user, I want rich console output after launching, so that I know how to monitor progress.
Acceptance Criteria:
- Prints convoy ID and
gt convoy status <convoy-id>command - Prints wave summary (how many waves, how many tasks per wave)
- Lists each dispatched Wave 1 task with its assigned polecat
- Prints hint:
gt convoy -ifor interactive TUI monitoring - Explains that subsequent waves are fed automatically by the daemon as tasks complete
US-010: gt convoy launch as alias
Description: As a user, I want gt convoy launch <bead-id> to work as an alias for gt convoy stage <bead-id> --launch, so that I have a clean two-step workflow (stage then launch) or a one-step workflow (launch directly).
Acceptance Criteria:
-
gt convoy launch <epic-id>=gt convoy stage <epic-id> --launch -
gt convoy launch <task1> <task2>=gt convoy stage <task1> <task2> --launch -
gt convoy launch <convoy-id>activates an already-staged convoy (no re-analysis needed if status isstaged_ready) -
gt convoy launch <convoy-id>on astaged_warningsconvoy requires--force -
gt convoy launch <convoy-id>on an already-openconvoy errors: "convoy is already launched"
US-011: JSON output mode
Description: As a user (or automation tool like design-to-beads), I want gt convoy stage --json to output machine-readable analysis results, so that I can programmatically consume the staging output.
Acceptance Criteria:
-
--jsonflag outputs structured JSON to stdout - JSON includes:
errorsarray,warningsarray,wavesarray (each with task list),tree(nested structure),convoy_id(if created),status(staged_ready | staged_warnings | error) - Human-readable output is suppressed when
--jsonis used - Non-zero exit code on errors (same as non-JSON mode)
Functional Requirements
- FR-1:
gt convoy stagemust accept three input forms: epic ID, space-separated task IDs, or convoy ID - FR-2: Epic DAG walking must use the Go SDK (
beads.List(ListOptions{Parent: rootID})) recursively, consistent withmolecule_dag.go:buildDAG. The SDK approach avoids subprocess overhead per tree level and is faster for deep hierarchies. Integration tests stub the beads store, not thebdCLI. - FR-3: Wave computation must use topological sort on the blocks/conditional-blocks/waits-for subgraph
- FR-4: Cycle detection must use standard graph cycle detection (DFS with back-edge detection or Kahn's algorithm failure)
- FR-5: Rig resolution must use existing
beads.ExtractPrefix+beads.GetRigNameForPrefixinfrastructure - FR-6: Staged convoy must use
bd create --type=convoy --status=staged_ready(or staged_warnings) - FR-7: Launch must dispatch Wave 1 tasks by calling internal Go dispatch functions directly (not
gt slingCLI). Usinggt slingwould create a separate auto-convoy per task viacreateAutoConvoy, duplicating the staged convoy. The internal dispatch path reuses the sling command's core logic (rig resolution, polecat spawning) without convoy creation overhead. - FR-8: Re-staging an existing convoy must update its status and re-compute waves without creating a duplicate
- FR-9:
gt convoy launchmust be registered as a subcommand ofgt convoyininternal/cmd/convoy.go - FR-10: Mixed input types (e.g., epic ID + task IDs in the same invocation) must be detected and rejected with a clear error message suggesting separate invocations
Non-Goals (Out of Scope)
- Sub-epic status management (open → in_progress → closed) — deferred to later milestone
- Integration branch creation — manual or design-to-beads responsibility
- Auto-formula detection for epic slinging — Milestone 4
- Coordinator polecat — Milestone 4
--infer-blocksflag to auto-generate deps from hierarchy — Milestone 4- Capacity plumbing (
isRigAtCapacitycallback) — not yet wired into feeder - The design-to-beads plugin itself — reference only, not part of this PR
Technical Considerations
- Wave computation is informational (display only at stage time). Runtime dispatch uses the daemon's per-cycle
isIssueBlockedchecks, which is more dynamic and handles external status changes. - The
staged_readyandstaged_warningsstatuses are new beads statuses that need to be recognized by the convoy system. The daemon should NOT feed issues from staged convoys (onlyopenconvoys get fed). - Epic DAG walking may involve cross-rig beads. Use the existing
routes.jsonlinfrastructure for rig resolution but be prepared for external references. - The
--jsonoutput should be stable enough for design-to-beads to depend on, but mark it as experimental in v1.
Staged status: codebase impact (blocked on Q1)
The current codebase assumes convoys are always open or closed. Adding staged statuses requires changes in multiple places:
ensureKnownConvoyStatus(convoy.go:96-108) — Currently only accepts"open"and"closed". Must be extended to recognize staged statuses. Called from 14+ callsites (add, close, land, check, isSlingableBead).validateConvoyStatusTransition(convoy.go:110-131) — Must define valid transitions:staged_ready→open,staged_warnings→open(with--force). Currently only knowsopen↔closed.- Daemon feeding has TWO paths that need guards:
- Event-driven path (
operations.go:57,70):isConvoyClosedchecks onlystatus == "closed". A staged convoy passes this check (not closed) and would be fed. This path needs an explicitisStagedConvoy(status)guard inCheckConvoysForIssue. - Stranded scan path (
convoy.go:1231): usesbd list --status=openwhich excludes non-open statuses. This path is safe by accident — staged convoys won't appear in the query results.
- Event-driven path (
bd doctorregex (^[a-z][a-z0-9_]*$) rejects colons in custom statuses. The status format decision (Q1) must account for this.
Key files expected to be modified:
internal/cmd/convoy.go— newstageandlaunchsubcommands; updateensureKnownConvoyStatusandvalidateConvoyStatusTransitionfor staged statusesinternal/cmd/convoy_stage.go— new file: staging logic, DAG walking, wave computation, displayinternal/cmd/convoy_launch.go— new file: launch logic, Wave 1 dispatch via internal Go functions (notgt slingCLI, to avoid auto-convoy creation)internal/convoy/operations.go— add staged-convoy guard inCheckConvoysForIssue(event-driven path);isConvoyClosedis insufficientinternal/daemon/convoy_manager.go— ensure staged convoys are not fed (stranded scan path is already safe via--status=openquery)
Existing code to reuse:
beads.ExtractPrefix,beads.GetRigNameForPrefix(internal/beads/routes.go) — rig resolution from bead prefixIsSlingableType(internal/convoy/operations.go) — filter non-dispatchable types from wavesisIssueBlocked(internal/convoy/operations.go) — already handles blocks dep checking (fail-open on store errors)createBatchConvoypattern (internal/cmd/sling_convoy.go:309) — the pattern of creating a convoy + addingtracksdeps is reusable, but the function itself requires arigNameparameter (used in title "Batch: N beads to <rig>"). Stage/launch convoys may span multiple rigs, so a new convoy creation function is needed with a different title format and staged status.workspace.FindFromCwdOrError(internal/workspace/find.go:98) — workspace resolution (used by existing convoy commands viagetTownBeadsDir)molecule_dag.go:computeTiers(internal/cmd/molecule_dag.go:213-262) — prior art for Kahn's algorithm (topological sort). Note: this function silentlybreaks on cycle detection (line 237) rather than returning an error with the cycle path. NewdetectCyclesmust be a separate function that returns the cycle path, as required by US-002 AC-1.
Success Metrics
gt convoy stage <epic-id>correctly identifies all descendant tasks and computes waves- Cycle detection catches all circular dependency chains
- Wave 1 dispatch only sends unblocked tasks
- Daemon correctly ignores staged convoys (only feeds open ones)
--jsonoutput is parseable and includes all analysis data- Round-trip works:
/design-to-beads→gt convoy stage→gt convoy launch→ tasks dispatched in correct order
Open Questions
-
OPEN: Should
staged_readyandstaged_warningsbe proper beads statuses (requiring SDK changes), or simulated via labels/description fields? The beads SDK'sbd doctorregex (^[a-z][a-z0-9_]*$) rejects colons, sostaged_readytriggers warnings. Options under consideration: colons (canonical but triggers doctor warnings), underscores (staged_ready), or hyphens (staged-ready). This is architecturally blocking — the daemon guard (I-7), FR-6, and most integration tests depend on the answer. -
RESOLVED: Informational only. Wave computation does not consider rig capacity when splitting waves. Capacity is surfaced as an informational warning (US-003 AC-5) but does not affect wave assignment. Capacity plumbing (
isRigAtCapacity) is deferred (see Non-Goals). -
RESOLVED: Preserve and update in place. Re-staging an existing convoy preserves the original convoy ID and updates its status and wave data. No new convoy is created. This is consistent with FR-8 and I-9 (no duplicates).