system/exclusive-lanes.md
Most backend tasks run in parallel. Some touch shared state and must be serialized into a single-flight queue called a lane. This doc defines the pattern and the lanes that exist today.
A task requires a lane when it modifies state that other concurrent tasks may depend on, where concurrent modification would cause: schema conflicts, dependency conflicts, deploy collisions, or non-deterministic test failures.
Lanes are about correctness, not resource limits.
| Lane | Flag | What triggers it | Concurrency |
|---|---|---|---|
backend_migration |
tasks.requires_migration = true |
Any task that adds/modifies/removes a Rails migration, or modifies db/schema.rb |
One Dev at a time |
release_train |
tasks.requires_release_conductor = true |
Gem publish, consumer lockfile adoption, production deploy, provider config, or env-var rollout | One conductor at a time per affected repo/app set |
Don't add lanes pre-emptively. Add a lane only after a class of conflict has
bitten twice, or when an action has irreversible production/provider effects.
Candidates that may become lanes later: shared seed file changes, asset
pipeline config, cross-app fixture/data contracts. Wait for them to actually
hurt before formalizing.
backend_migration worksThere are two paths into the lane.
Avi sets requires_migration: true during refinement if the spec obviously needs a schema change. The Dev acquires the lane at task start.
Avi often can't know up front. Carl (and any backend Dev) is responsible for self-recognizing the moment.
Stop rule: before running
bin/rails g migration, before creating a file indb/migrate/, before modifyingdb/schema.rb— stop. Check the lane. Acquire or queue.
If you started a task tagged requires_migration: false and discover you need one: update the flag, acquire the lane, then write the migration file. Never smuggle a migration in unflagged.
Self-flagging is not a failure — it's the system working. Avi can't see every implementation detail. The Dev who's closest to the code catches what refinement missed.
ApplicationRecord.connection.execute(
"SELECT pg_try_advisory_lock(hashtext('backend_migration'))"
)
If the query returns true: proceed.
If false: the lane is held. Your task transitions to lane_queued status. Chat the current holder asking ETA. Pick up a non-migration task in the meantime.
On task done/failed, advisory locks release automatically when the session closes. Belt-and-suspenders: also call pg_advisory_unlock(hashtext('backend_migration')) explicitly on the transition out.
Carl (the role, not any specific instance) is the coordination authority for the backend_migration lane:
Any Carl instance can hold the lane at a given time. Captaincy is about authority, not exclusivity.
When several upcoming tickets each need small schema changes, Avi (with Carl's input) batches them into one migration task. Three small column adds in one migration is better than three sequential migration tickets each fighting the lane.
The release_train lane exists because shared releases can otherwise overwrite
or strand other agents' work. Typical examples:
studio-engine version bump and RubyGems publishGemfile.lock updates after a gem releaseThe conductor must:
main in every affected repo.Feature agents can recommend entering the lane, but they do not run release
actions unless Mr. McRitchie assigns that lane to the session.
A new lane is a meaningful addition to system contention. Don't add one without:
We emailed a one-tap sign-in link to . It expires shortly and can only be used once.
No email? Check spam, or close this and try again.