Reserve slots and dispatch lane script

Task 5523 · Done · Coder · created · updated

Epic: Merge lane — CP-scheduled verification queue, plan-gated. Planner Task 5517; the full PRD is BA Task 5515 and the full architecture is Architect Task 5516. Read architecture L1/L4/L9/L10 and §4.5 in full with `task_read`. Builds on C3a (`apps/api/app/merge_lane.py`: `lane_state`, `reserved_slots`, `ensure_lane`, `release_lane_rows`; the `Persona.script` field and the `merge_lane` record). ## Requirements (verbatim) - Item 2: "Per-project setting: number of reserved verification slots (default 1 when the feature is on). The pool never leases a verification slot to a runner (`docs/policies/worktree-pool-policy.md`, lease/release)." - Item 5 (dispatch clause): "Lane: `tools/merge_lane.py`, a deterministic script (no model) dispatched to a verification slot through lease-drain, event-driven (a queued PR wakes it, like `task_wake`); at most k in flight." ## Approach (settled) - **Reservation wiring (L4):** - `lease_drain` adds `merge_lane.reserved_slots(...)` `(node, number)` pairs to `fleet_decide.select_slot`'s `exclude` set (~197-231) for every non-`merge_lane` task, and restricts a `merge_lane` task's roster to the reserved rows. - `slot_state.claim_for_user` (~172-219) gains `exclude_ids`, passed by its route, so a human console never claims a reserved slot. - `slot_state.lease` itself stays generic. - A reserved slot still held by an earlier runner is simply waited on. - **Script dispatch (§4.5):** - `Persona.dispatchable` = `charter_key is not None or script is not None`. - `lease_drain.build_lease_intent` emits `payload.launch = {"kind":"script","tool":"merge_lane.py"}` and NO prompt for a script persona. For model personas `launch` is absent and the intent is byte-identical to today's. - `dynamic_placement` (~96-111) launches a script persona the same way, skipping the account/API-key requirement. Concurrency is capped at k by `ensure_lane`. - jaahd `_enact_lease_slot` → `_launch_into_slot`: when `launch.kind == "script"`, `fleet_dispatch.launch_runner` uses a `ScriptAdapter`: - its exec line is `python3 -I <installed jaah tools dir>/<tool>`; - a tool outside the allowlist `{"merge_lane.py"}` is refused; - the tool is NEVER resolved against the slot checkout (L2); - it exports the usual `CONTROL_TASK_ID` / runner env and registers the pane in `runner_registry`, exactly like a model pane, so the credential broker resolves the forge login by peer pid → lane task → `started_by_id`; - no hooks, no account, no model, and `bind_account` is skipped. - Script liveness: alive = the registered pane process is alive (the existing `pid`/`pid_start_time` fence). - ENUMERATE every sweep that infers idleness or breakage from model-session activity (transcript/hook heartbeats, idle autoclose, broken-session classification) and exempt `persona.script` rows. - A script that exits without settling is `runner_lost`. - **Scheduling hooks (L9 b/c, L10):** - Call `ensure_lane` from the existing `task-pipeline` cron tick (the backstop for `recheck_after`). - In the SAME transaction as any transition of a `merge_lane` task out of `active` (settle, fail, `runner_lost`, `system_failed`, operator stop), call `release_lane_rows` and then `ensure_lane`. Hook the one transition table (`task_lifecycle.py` consumers in `routes/tasks.py` / `task_pipeline_cp.py`). - Coordination: C6 also edits `lease_drain` / `dynamic_placement` (it passes `lane_state` into the compose input) in the same wave. That is not a dependency; rebase onto whichever merges first. ## Acceptance (verbatim) - AC2.2 On a `static` project the pool never leases a reserved slot to a runner or to a human claim. The lease UPDATE and `select_slot` both exclude it, and no path assigns a task to it except a lane dispatch. Reserved slots are the k highest-numbered pool slots (D7). With the feature OFF, **zero** slots are reserved and the pool behaves exactly as today. - AC5.1 `tools/merge_lane.py` is a deterministic script with no model invocation. It is dispatched through lease-drain to a reserved verification slot of the project. It is **event-driven**: a newly `queued` batchable row wakes dispatch the way `task_wake` does, so it is not waiting on a polling interval. At most k lane runs per project are in flight at once. Credentials & reach: CP and jaahd unit/integration tests use none beyond the worktree's own `./dc` stack (verified; worktree-local). Out of scope: - Epic (verbatim): "GitHub merge queue / merge_group; any CP-side forge calls; changing the manual develop → master promotion; cgroup tuning of GHA runners; squash merges." - BA-derived (verbatim): "No change to the runner flow, guard, prompt, pool or CI when the flag is OFF (R0)." - Do NOT: - add a `reserved_for` column; - change `slot_state.lease`'s generic contract; - write `tools/merge_lane.py` itself (C4); - change runner prompts or guards (C6); - give the script hooks or a model account; - resolve the script from the worktree. Done means: - AC2.2 — a `slot_state`/`fleet_decide`/`lease_drain` unit test on a 4-slot project with k=1: - runner leases only ever land on slots 1–3, and lane dispatch only on slot 4; - `claim_for_user` never claims slot 4; - with the flag off, all 4 are leasable (AC0.1 OFF branch); - the drain rule (flag off but open rows) keeps slot 4 reserved. - AC5.1 dispatch: - a `merge_lane` task's lease intent carries `launch.kind=script` and no prompt; - a model persona's lease intent is byte-identical to develop's (snapshot); - a jaahd test shows `ScriptAdapter` launching `python3 -I <tools dir>/merge_lane.py`, registering the pane in `runner_registry`, skipping `bind_account`, and refusing a non-allowlisted tool; - dynamic placement launches a script persona with no account. - AC5.1 event-driven — filing a `ready` `merge_lane` task fires lease-drain through `task_wake` (test), and never more than k lane tasks are open. - Liveness — a lane task whose pid lives is never reaped by any enumerated sweep after 40 simulated minutes without transcript activity (test). A dead pid gives `runner_lost`, its `testing` rows become `queued`, and `ensure_lane` re-files. Required checks: gate, review-lane

Outcome

Merge lane C3b: reserved slots honoured in plan, allocation and console. merge_lane dispatches as an allowlisted script (no model/account/prompt; static + dynamic), with exit hook and tick. Full gate green; PR #4836 merged; Jaah Deploy ok. Deferred to C4: strip CONTROL_*/git creds from the gate.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)