The definitive guide for working with gastown's convoy system -- batch work tracking, event-driven feeding, stage-launch workflow, and dispatch safety guards. Use when writing convoy code, debugging convoy behavior, adding convoy features, testing convoy changes, or answering questions about how convoys work. Triggers on convoy, convoy manager, convoy feeding, dispatch, stranded convoy, feedFirstReady, feedNextReadyIssue, IsSlingableType, isIssueBlocked, CheckConvoysForIssue, gt convoy, gt sling, stage, launch, staged, wave.
The convoy system tracks batches of work across rigs. A convoy is a bead that tracks other beads via dependencies. The daemon monitors close events and feeds the next ready issue when one completes.
+================================ CREATION =================================+
| |
| gt sling <beads> gt convoy create ... gt convoy stage <epic> |
| | (auto-convoy) | (explicit) | (validated) |
| v v v |
| +-----------+ +-----------+ +----------------+ |
| | status: | | status: | | status: | |
| | open | | open | | staged:ready | |
| +-----------+ +-----------+ | staged:warnings| |
| +----------------+ |
| | |
| gt convoy launch |
| | |
| v |
| +----------------+ |
| | status: | |
| | open | |
| | (Wave 1 slung) | |
| +----------------+ |
| |
| All paths produce: CONVOY (hq-cv-*) |
| tracks: issue1, issue2, ... |
+============================================================================+
| |
v v
+= EVENT-DRIVEN FEEDER (5s) =+ +=== STRANDED SCAN (30s) ===+
| | | |
| GetAllEventsSince (SDK) | | findStranded |
| | | | | |
| v | | v |
| close event detected | | convoy has ready issues |
| | | | but no active workers |
| v | | | |
| CheckConvoysForIssue | | v |
| | | | feedFirstReady |
| v | | (iterates all ready) |
| feedNextReadyIssue | | | |
| (iterates all ready) | | v |
| | | | gt sling <next-bead> |
| v | | or closeEmptyConvoy |
| gt sling <next-bead> | | |
| | +============================+
+==============================+
Three creation paths (sling, create, stage), two feed paths, same safety guards:
operations.go): Polls beads stores every ~5s for close events. Calls feedNextReadyIssue which checks IsSlingableType + isIssueBlocked before dispatch. Skips staged convoys (isConvoyStaged check).convoy_manager.go): Runs every 30s. feedFirstReady iterates all ready issues. The ready list is pre-filtered by IsSlingableType in findStrandedConvoys (cmd/convoy.go). Only sees open convoys — staged convoys never appear.These prevent the event-driven feeder from dispatching work it shouldn't:
IsSlingableType)Only leaf work items dispatch. Defined in operations.go:
var slingableTypes = map[string]bool{
"task": true, "bug": true, "feature": true, "chore": true,
"": true, // empty defaults to task
}
Epics, sub-epics, convoys, decisions -- all skip. Applied in both feedNextReadyIssue (event path) and findStrandedConvoys (stranded path).
isIssueBlocked)Issues with unclosed blocks, conditional-blocks, or waits-for dependencies skip. parent-child is not blocking -- a child task dispatches even if its parent epic is open. This is consistent with bd ready and molecule step behavior.
Fail-open on store errors (assumes not blocked) to avoid stalling convoys on transient Dolt issues.
Both feed paths iterate past failures instead of giving up:
feedNextReadyIssue: continue on dispatch failure, try next ready issuefeedFirstReady: for range ReadyIssues with continue on skip/failure, return on first successgt convoy stage <epic-id> # analyze deps, build DAG, compute waves, create staged convoy
gt convoy stage gt-task1 gt-task2 # stage from explicit task list
gt convoy stage hq-cv-abc # re-stage existing staged convoy
gt convoy stage <epic-id> --json # machine-readable output
gt convoy stage <epic-id> --launch # stage + immediately launch if no errors
gt convoy launch hq-cv-abc # transition staged → open, dispatch Wave 1
gt convoy launch <epic-id> # stage + launch in one step (delegates to stage --launch)
gt convoy create "Auth overhaul" gt-task1 gt-task2 gt-task3
gt convoy add hq-cv-abc gt-task4
gt convoy check hq-cv-abc # auto-closes if all tracked issues done
gt convoy check # check all open convoys
gt convoy status hq-cv-abc # single convoy detail
gt convoy list # all convoys
gt convoy list --all # include closed
gt convoy stranded # ready work with no active workers
gt convoy stranded --json # machine-readable
gt convoy close hq-cv-abc --reason "done"
gt convoy land hq-cv-abc # cleanup worktrees + close
gt convoy -i # opens interactive convoy browser
gt convoy --interactive # long form
gt sling <bead1> <bead2> <bead3> creates one convoy tracking all beads. The rig is auto-resolved from the beads' prefixes (via routes.jsonl). The convoy title is "Batch: N beads to <rig>". Each bead gets its own polecat, but they share a single convoy for tracking.
The convoy ID and merge strategy are stored on each bead, so gt done can find the convoy via the fast path (getConvoyInfoFromIssue).
gt sling gt-task1 gt-task2 gt-task3 -- resolves rig from the gt- prefix. All beads must resolve to the same rig.gt sling gt-task1 gt-task2 gt-task3 myrig -- still works, prints a deprecation warning. If any bead's prefix doesn't match the explicit rig, errors with suggested actions.--force).cat .beads/routes.jsonl | grep <prefix>).If any bead is already tracked by another convoy, batch sling errors with detailed conflict info (which convoy, all beads in it with statuses, and 4 recommended actions). This prevents accidental double-tracking.
# Auto-resolve: one convoy, three polecats (preferred)
gt sling gt-task1 gt-task2 gt-task3
# -> Created convoy hq-cv-xxxxx tracking 3 beads
# Explicit rig still works but prints deprecation warning
gt sling gt-task1 gt-task2 gt-task3 gastown
# -> Deprecation: gt sling now auto-resolves the rig from bead prefixes.
# -> Created convoy hq-cv-xxxxx tracking 3 beads
Implemented in PR #1820. Depends on the feeder safety guards from PR #1759. Design docs:
docs/design/convoy/stage-launch/prd.md,docs/design/convoy/stage-launch/testing.md.
The stage-launch workflow is a two-phase convoy creation path that validates dependencies and computes wave dispatch order before any work is dispatched. This is the preferred path for epic delivery.
gt convoy stage accepts three mutually exclusive input types:
| Input | Example | Behavior |
|---|---|---|
| Epic ID | gt convoy stage bcc-nxk2o | BFS walks entire parent-child tree, collects all descendants |
| Task list | gt convoy stage gt-t1 gt-t2 gt-t3 | Analyzes exactly those tasks |
| Convoy ID | gt convoy stage hq-cv-abc | Re-reads tracked beads from existing staged convoy (re-stage) |
Mixed types (e.g., epic + task together) error. Multiple epics or multiple convoys error.
1. validateStageArgs — reject empty/flag-like args
2. bdShow each arg — resolve bead types
3. resolveInputKind — classify Epic / Tasks / Convoy
4. collectBeads — gather BeadInfo + DepInfo (BFS for epic, direct for tasks)
5. buildConvoyDAG — construct in-memory DAG (nodes + edges)
6. detectErrors — cycle detection + missing rig checks
7. detectWarnings — orphans, parked rigs, cross-rig, capacity, missing branches
8. categorizeFindings — split into errors / warnings
9. chooseStatus — staged:ready, staged:warnings, or abort on errors
10. computeWaves — Kahn's algorithm (only when no errors)
11. renderDAGTree — print ASCII dependency tree
12. renderWaveTable — print wave dispatch plan
13. createStagedConvoy — bd create --type=convoy --status=<staged-status>
Only slingable types participate in waves: task, bug, feature, chore. Epics are excluded.
Execution edges (create wave ordering):
blocksconditional-blockswaits-forNon-execution edges (ignored for wave ordering):
parent-child — hierarchy onlyrelated, tracks, discovered-fromAlgorithm:
Output example:
Wave ID Title Rig Blocked By
──────────────────────────────────────────────────────────────────────
1 bcc-nxk2o.1.1 Init scaffolding bcc —
2 bcc-nxk2o.1.2 Shared types bcc bcc-nxk2o.1.1
3 bcc-nxk2o.1.3 CLI wrapper bcc bcc-nxk2o.1.2
3 tasks across 3 waves (max parallelism: 1 in wave 1)
Four statuses with defined transitions:
| Status | Meaning |
|---|---|
staged:ready | Validated, no errors or warnings, ready to launch |
staged:warnings | Validated, no errors but has warnings. Fix and re-stage, or launch anyway. |
open | Active — daemon feeds work as beads close |
closed | Complete or cancelled |
Valid transitions:
| From → To | Allowed? |
|---|---|
staged:ready → open | Yes (launch) |
staged:warnings → open | Yes (launch) |
staged:* → closed | Yes (cancel) |
staged:ready ↔ staged:warnings | Yes (re-stage) |
open → closed | Yes |
closed → open | Yes (reopen) |
open → staged:* | No |
closed → staged:* | No |
Errors (fatal — prevent convoy creation):
| Category | Trigger | Fix |
|---|---|---|
cycle | Cycle detected in execution edges | Remove one blocking dep in the cycle |
no-rig | Slingable bead has no rig (prefix not in routes.jsonl) | Add routes.jsonl entry |
Warnings (non-fatal — convoy created as staged:warnings):
| Category | Trigger |
|---|---|
orphan | Slingable task with no blocking deps in either direction (epic input only) |
blocked-rig | Bead targets a parked or docked rig |
cross-rig | Bead on a different rig than the majority |
capacity | A wave has more than 5 tasks |
missing-branch | Sub-epic with children but no integration branch |
gt convoy launch <convoy-id> transitions a staged convoy to open and dispatches Wave 1:
opengt sling <beadID> <rig>If gt convoy launch receives an epic or task list (not a staged convoy), it delegates to gt convoy stage --launch to stage-then-launch in one step.
Staged convoys are completely inert to the daemon. Neither feed path processes them:
isConvoyStaged check in CheckConvoysForIssue skips any convoy with staged:* status. Fail-open on read errors (assumes not staged → processes, which is safe since a read error on a non-existent convoy does nothing).gt convoy stranded only returns open convoys. Staged convoys never appear.This means you can stage a convoy, review the wave plan, and launch when ready — no risk of premature dispatch.
Running gt convoy stage <convoy-id> on an existing staged convoy re-analyzes and updates:
tracks depsbd update (e.g., staged:warnings → staged:ready if warnings resolved)# Full convoy suite (all packages)
go test ./internal/convoy/... ./internal/daemon/... ./internal/cmd/... -count=1
# By area:
go test ./internal/convoy/... -v -count=1 # feeding logic
go test ./internal/daemon/... -v -count=1 -run TestConvoy # ConvoyManager
go test ./internal/daemon/... -v -count=1 -run TestFeedFirstReady
go test ./internal/cmd/... -v -count=1 -run TestCreateBatchConvoy # batch sling
go test ./internal/cmd/... -v -count=1 -run TestBatchSling
go test ./internal/cmd/... -v -count=1 -run TestResolveRig # rig resolution
go test ./internal/daemon/... -v -count=1 -run Integration # real beads stores
# Stage-launch:
go test ./internal/cmd/... -v -count=1 -run TestConvoyStage # staging logic
go test ./internal/cmd/... -v -count=1 -run TestConvoyLaunch # launch + Wave 1 dispatch
go test ./internal/cmd/... -v -count=1 -run TestDetectCycles # cycle detection
go test ./internal/cmd/... -v -count=1 -run TestComputeWaves # wave computation
go test ./internal/cmd/... -v -count=1 -run TestBuildConvoyDAG # DAG construction
feedFirstReady dispatches exactly 1 issue per call (first success wins)feedFirstReady iterates past failures (sling exit 1 -> try next)isRigParked returns true for everythingIsSlingableType("epic") == false, IsSlingableType("task") == true, IsSlingableType("") == trueisIssueBlocked is fail-open (store error -> not blocked)parent-child deps are NOT blockingresolveRigFromBeadIDs errors on mixed prefixes, unmapped prefixes, town-level prefixesstaged:* convoys (both feed paths skip)staged:warnings convoys can still be launched (warnings are informational)See docs/design/convoy/stage-launch/testing.md for the full stage-launch test plan (105 tests across unit, integration, snapshot, and property tiers).
See docs/design/convoy/testing.md for the general convoy test plan covering failure modes, coverage gaps, harness scorecard, test matrix, and recommended test strategy.
parent-child is never blocking. This is a deliberate design choice, not a bug. Consistent with bd ready, beads SDK, and molecule step behavior.isReadyIssue in cmd/convoy.go reads t.Blocked from issue details. isIssueBlocked in operations.go covers the event-driven path. Don't consolidate them without understanding both paths.isIssueBlocked is fail-open. Store errors assume not blocked. A transient Dolt error should not permanently stall a convoy -- the next feed cycle retries with fresh state.gt sling beads... rig still works but prints a warning. Prefer gt sling beads... with auto-resolution.gt convoy launch.staged:warnings before launching. Warnings are informational — fix and re-stage if possible, or launch anyway if they're acceptable.gt convoy launch on a non-staged input delegates to stage. If you pass an epic or task list to launch, it runs stage --launch internally. Only an already-staged convoy gets the fast path.isIssueBlocked checks, which are more dynamic.open → staged:* transition is rejected.| File | What it does |
|---|---|
internal/convoy/operations.go | Core feeding: CheckConvoysForIssue, feedNextReadyIssue, IsSlingableType, isIssueBlocked |
internal/daemon/convoy_manager.go | ConvoyManager goroutines: runEventPoll (5s), runStrandedScan (30s), feedFirstReady |
internal/cmd/convoy.go | All gt convoy subcommands + findStrandedConvoys type filter |
internal/cmd/sling.go | Batch detection at ~242, auto-rig-resolution, deprecation warning |
internal/cmd/sling_batch.go | runBatchSling, resolveRigFromBeadIDs, allBeadIDs, cross-rig guard |
internal/cmd/sling_convoy.go | createAutoConvoy, createBatchConvoy, printConvoyConflict |
internal/cmd/convoy_stage.go | gt convoy stage: DAG walking, wave computation, error/warning detection, staged convoy creation |
internal/cmd/convoy_launch.go | gt convoy launch: status transition, Wave 1 dispatch via dispatchWave1 |
internal/daemon/daemon.go | Daemon startup -- creates ConvoyManager at ~237 |