Execution Lanes
Spec Kitty uses a lane-based execution model.
finalize_taskscomputeslanes.jsonfrom dependencies, ownership, and predicted surfaces.- Each lane gets exactly one git worktree and one lane branch.
- Sequential work packages in the same lane reuse that same worktree.
- Independent lanes can run in parallel in separate worktrees.
Core Rules
- Planning happens in the primary repository checkout.
spec-kitty agent action implement WP## --agent <name>requires a validlanes.json.- The runtime chooses the lane worktree. Agents do not pick a base branch manually.
- If a feature computes one lane, the feature uses one worktree.
- Merge always follows
lane branches -> mission branch -> target branch.
Workspace Resolution Contract
spec-kitty implement WP## creates/reuses the execution workspace through
resolve_workspace_for_wp (src/specify_cli/workspace/context.py). For a
code_change WP, resolution ends at one of two places:
- An existing lane workspace context (
find_context_for_wp) — the lane this WP's mission already allocated a worktree for. - Otherwise, the mapping in
lanes.json, read throughrequire_lanes_json(src/specify_cli/lanes/persistence.py).
require_lanes_json is fail-closed: when lanes.json is absent it raises
MissingLanesError rather than degrading to a name-guessed path. There is
no -WP## legacy worktree fallback — flat, SINGLE_BRANCH, and LANES
missions all still require a computed lanes.json; a mission that hasn't run
finalize-tasks cannot resolve a workspace at all. (An earlier revision of
this repo's AGENTS.md claimed such a fallback existed; that claim was
false and has been corrected — see the MissingLanesError contract above
for the real behavior.)
Naming
As of mission 083-mission-id-canonical-identity-migration, every mission carries a ULID
identity (mission_id), and branch and worktree names embed the first 8 characters of
that ULID (mid8) to guarantee collision-free naming even when two missions share the
same human slug.
- Mission branch:
kitty/mission-<human-slug>-<mid8> - Lane branch:
kitty/mission-<human-slug>-<mid8>-lane-a - Lane worktree:
.worktrees/<human-slug>-<mid8>-lane-a/
Example, for a mission with mission_slug=my-feature and
mission_id=01J6XW9KQT7M0YB3N4R5CQZ2EX (so mid8=01J6XW9K):
- Mission branch:
kitty/mission-my-feature-01J6XW9K - Lane branch:
kitty/mission-my-feature-01J6XW9K-lane-a - Lane worktree:
.worktrees/my-feature-01J6XW9K-lane-a/
Legacy (pre-083) forms such as kitty/mission-001-my-feature-lane-a and
.worktrees/001-my-feature-lane-a/ remain readable by current tooling but
are no longer the form produced by implement. Upgrade via the
mission identity migration runbook.
Why This Replaced Per-WP Worktrees
Per-work-package worktrees allowed overlapping work packages to run in parallel and collide at merge time. Execution lanes eliminate that by forcing dependent or overlapping work packages into the same lane, branch, and worktree.
Parallelism Preservation
finalize-tasks assigns WPs to lanes based on two criteria:
- File ownership overlap — WPs that declare no files in common are placed in separate lanes and run in parallel.
- Explicit dependencies — If WP B lists WP A in its
dependenciesfield, they are assigned to the same lane and run sequentially (A then B).
When neither criterion forces a merge, the pipeline keeps WPs in separate lanes to maximise parallelism. When a merge is forced, it is recorded in lanes.json under the collapse_report field:
{
"collapse_report": [
{
"merged_wps": ["WP02", "WP03"],
"reason": "overlapping owned files: src/foo.py"
}
]
}
Each entry in collapse_report lists the WPs that were merged into a single lane and the reason (file overlap or explicit dependency). Inspect this field after finalize-tasks to understand why two WPs share a lane.
Disjoint Ownership vs. the Surface Heuristic (bulk-edit missions)
compute_lanes (src/specify_cli/lanes/compute.py) has a second collapse
rule beyond file-overlap: two WPs that share an inferred surface keyword
(e.g. both bodies mention "legacy" or "cleanup", matching the
legacy-cleanup tag in SURFACE_TAXONOMY) are also candidates for merging
into one lane — unless their owned_files are provably disjoint
(_are_disjoint), in which case the merge is skipped.
This matters most for bulk-edit missions (see the bulk-edit occurrence classification guardrail ADR): a rename/replace mission routinely produces many WPs whose bodies all describe the same edit applied to different files, so they trip the same surface keyword nearly every time. Without the disjoint-ownership check, the surface heuristic alone would collapse every one of those WPs into a single giant lane, discarding the parallel partition the occurrence-map classification was built to produce. The disjoint check is what lets bulk-edit WPs with non-overlapping file scopes stay in separate lanes and run in parallel.
It also keeps the lane dependency graph (lane_deps) well-formed. Lane
depth is computed by _compute_lane_depths, which treats a self-loop or
cycle as a best-effort depth-0 anchor rather than crashing — see
finalize-tasks internals
for why that fallback exists and why it is not a substitute for a clean
input graph. An over-aggressive surface-only merge across many
similarly-worded bulk-edit WPs is exactly the kind of input that could
otherwise produce a lane graph the depth function has to paper over instead
of compute correctly; skipping the merge when ownership is disjoint avoids
manufacturing that situation in the first place.
See Also
- Branch-Target Routing — where each diff type lands, decided
per artifact kind (
src/mission_runtime/artifacts.py): planning + identity artifacts (spec, plan, tasks, work-package files,data-model.md,lanes.json,meta.json) go to the primary target branch for every topology; coordination-owned artifacts (status events,acceptance-matrix.json,issue-matrix.md,analysis-report.md) go to the coordination branch; code changes go to the lane branch; shared documentation and the merge target go to the base branch. Also explains the simple-case flat-topology collapse when no coordination branch or lane worktrees are configured.
Lane-Specific Test Database Isolation (FR-006)
Two parallel SaaS / Django lanes used to share a single test database when their per-lane test runners booted concurrently, which produced flaky failures. Each lane workspace now exposes a lane-suffixed identifier via LaneWorkspaceResult.lane_test_env, which sets SPEC_KITTY_TEST_DB_NAME=test_<safe-mission>_<safe-lane>. Test settings modules (Django and otherwise) should read that env var when constructing their per-lane test database name; the helpers lane_test_db_name() and lane_test_env() in specify_cli.lanes.lane_env are the canonical entry points and guarantee distinct DB names for distinct (mission_slug, lane_id) pairs.