Skip to content

Latest commit

 

History

History
198 lines (163 loc) · 11.1 KB

File metadata and controls

198 lines (163 loc) · 11.1 KB

Parallel dev sessions

Running several AI-agent sessions at once is the fastest way to burn a day of work — or to corrupt three branches and dilute every review. This kit's answer is cockpit + isolated lanes (Principle #3): one coordinating session owns the shared narrative and the merges, while each unit of parallel work runs in its own isolated lane. This page explains what a lane is, when parallelism is safe, and the end-to-end workflow.

Engine: scripts/dev_session.sh (launch/list/remove lanes) and scripts/reconcile_sessions.sh (drive every lane to a terminal state). The in-session front-end is the shared parallel workflow. These links use the template default paths.engines: scripts; a namespaced adopter substitutes its configured directory, such as scripts/devkit.

What a lane is

A lane is one unit of parallel work, fully isolated:

  • its own git worktree on a fresh branch off origin/<base> — so two lanes never share a working tree or step on each other's checkout;
  • its own state sandbox — a DEVKIT_STATE_ROOT pointed at a per-lane directory (with a .devkit_state_root marker file for tool calls that don't inherit the shell), so concurrent writes to state/cache/ and other scratch state can't clobber each other. See scripts/lib/state_paths/.

The cockpit is your main session. It owns the two narrative files (paths.handoff and paths.friction_log in config/dev-model.yamldocs/handoff.md and docs/friction-log.md by default), the review pass, and the terminal merge decision. Lanes never touch the narrative files — they carry their handoff in their pull request description, the one channel that's reviewed and visible across every lane.

When parallelism is safe — disjoint file footprints

This is the one rule that matters: two lanes are safe together only when no source file is edited by both.

The state sandbox makes concurrent scratch-state writes safe. It does nothing for two branches editing the same source file — that's a merge conflict plus a diluted review at PR time, and no amount of sandboxing prevents it. So before launching a batch, map each candidate's file footprint and only run truly disjoint work concurrently. Footprint-mapping is a separate, deliberate step from isolation — the sandbox prevents state collisions, not source merge conflicts.

Rule of thumb: parallelize independent features/areas; keep anything that touches a shared module, schema, or config in the same lane and run it sequentially.

The workflow

flowchart TD
    CP["cockpit<br/>owns handoff · friction-log · merges"] --> Plan["parallel plan<br/>cluster candidates by file footprint<br/>· at most one per disjoint cluster"]
    Plan --> L1["lane A<br/>worktree + branch + sandbox"]
    Plan --> L2["lane B<br/>worktree + branch + sandbox"]
    Plan --> L3["lane C<br/>worktree + branch + sandbox"]
    L1 --> D1["green · marked ready"]
    L2 --> D2["green · marked ready"]
    L3 --> D3["green · marked ready"]
    D1 --> R["cockpit reconciles<br/>list --watch + reconcile_sessions.sh"]
    D2 --> R
    D3 --> R
    R --> M["cockpit merges<br/>via dev_session.sh merge (self class)<br/>or operator sign-off (operator class)"]
Loading

1 · Plan the batch — parallel plan

Don't spin up lanes ticket-by-ticket. Compose the batch deliberately:

  1. Orientscripts/dev_session.sh list + git worktree list show the file territory already claimed by in-flight lanes; exclude those footprints.
  2. Cluster by footprint — group candidate tickets by the files each touches (read the ticket and grep the code; don't infer from the title). Pick at most one per cluster; the rest go sequential.
  3. Stale-premise pre-flight — drop any candidate whose fix may already be shipped (a checklist item matching a recently merged PR, a "Done" state that was a bot auto-complete). Verify against live code before recommending it.
  4. Scope outward-safe — a lane that would push to an external system or send a notification is scoped to its in-repo half; the outward step stays an operator action after merge.
  5. Assign an effort tier and a merge class per lane — decide up front how much reasoning each lane gets (cheap → top) and whether it may self-merge once green or must hand back for operator sign-off. Deciding the merge boundary at plan time stops a batch stalling on ad-hoc "can I merge this?" calls.

The exact per-step commands, plus the effort-tier and merge-class tables, are in workflows/parallel.md — this doc keeps only the reasoning behind each step.

2 · Launch each lane — dev_session.sh new

Each new creates the worktree + branch + sandbox, persists the merge class, and hands off to the lane: a copy-paste line for an interactive operator, or (--headless) a sticky on-disk marker plus a canonical one-shot JSON descriptor for an unattended launcher. Codex headless lanes use the kit-owned wrapper, whose child independently observes Git, filesystem, state, and process identity before exec and whose receipt binds that observation to the launch request. Native agent dispatch is not a substitute for the descriptor environment/receipt chain. Omitting --merge-class fails safe to operator. The exact command, every flag, and the headless JSON descriptor are in workflows/parallel.md (interactive) and workflows/parallel-headless.md (unattended); for task-oriented recipes, see parallel-howto.md.

3 · Each lane works to a green, ready-for-review PR

A lane's job ends at green-and-ready, not at merge — bound by the same lane contract every launch mechanism injects verbatim into the lane's prompt. Completed work opens ready by default, and the lane immediately runs --assert-ready before review polling. Draft is reserved for material unfinished work that must already exist remotely because required forge-hosted validation cannot run before the PR exists; that lane owns the draft assertion, completion, ready transition, and ready assertion. Active CI polling, narrative ownership, branch hygiene, and the no-merge boundary remain in the same contract. Marking ready is the lane's; landing it is the cockpit's. Fetch the contract yourself with dev_session.sh print-contract, or read it in workflows/parallel-headless.md — this guide summarizes the lifecycle without copying the verbatim contract.

4 · Watch the board — list --watch

scripts/dev_session.sh list --watch        # re-render every 30s

The live board marks every row that changed since the last frame with a leading * — a CI flip, a new commit, the DIRTY count moving, a PR-state change — and surfaces a silently-dead lane that stops moving. Use it as the cockpit's ambient board while a batch runs; full behavior (piped output, --max-iters, per-call timeouts) is in workflows/parallel.md.

5 · Reconcile, then merge — reconcile_sessions.sh

Before the cockpit writes anything to the shared handoff, drive every launched lane to a terminal state — merged, held, or parked-with-reason — with scripts/reconcile_sessions.sh. (still-open is the fourth result and the one that is not terminal: it means reconciliation is unfinished, and it alone keeps the batch un-closeable.) An aggregate "everything's done" is not evidence a specific lane actually shipped; reconcile per lane. Then review and merge each per its pre-assigned merge class: a self-merge lane goes through the deterministic wrapper (re-polls CI/review/merge readiness at act time and refuses missing or operator metadata); an operator lane is merged only after explicit cockpit sign-off. held is that second kind, seen from the reconciler: an operator lane whose PR is already merge-ready, so the batch has nothing left to do and the sign-off is the only thing outstanding. Exit 4 is reached only when no lane is open or parked and at least one is held; an all-merged batch still exits 0, because a held lane has not shipped. The exact pr-watch / merge / rm command sequence lives in workflows/parallel.md and parallel-howto.md — this doc keeps only the reconcile-before-merge reasoning.

Model / effort tiering per lane

The risk read from planning also sets each lane's reasoning budget (Principle #7): a mechanical sweep gets a cheap tier; the one lane whose decision is expensive to get wrong gets the top tier. The tier travels with the lane as an explicit field, not an assumption the lane has to infer — a cheap-tier agent handed a subtle-invariant task will confidently ship the wrong thing.

A worked example

You have four open tickets. Planning clusters them by footprint:

Ticket Touches Cluster
Add rate-limit to the auth endpoint auth/ A
Rename the metrics module metrics/ (repo-wide import sweep) B
Fix a typo in the CLI help cli/help.py C
Tighten the auth token TTL auth/ A

Two of them share auth/ (cluster A) — so you run one of them now and defer the other. You launch three disjoint lanes:

scripts/dev_session.sh new auth-ratelimit --headless --merge-class operator --runtime codex  # cluster A · top tier
scripts/dev_session.sh new metrics-rename --headless --merge-class self --runtime codex      # cluster B · cheap tier
scripts/dev_session.sh new cli-help-typo --headless --merge-class self --runtime codex       # cluster C · cheap tier

Each lane works to a green, ready-for-review PR while you watch list --watch from the cockpit — each flipping its own PR ready as it finishes, so the review bots pick them up staggered rather than all at once. The two cheap self-merge lanes land through dev_session.sh merge; the auth rate-limit lane (security-adjacent) hands back for operator review. You reconcile all three, merge, and only then update the handoff (paths.handoff) with what shipped — from the cockpit, once.

See also