Stop runner self-merge when lane on
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 §3 (the quality traps) and §4.3 with `task_read`.
Builds on C3a: `merge_lane.lane_state`, the `task_pr_queue` MCP tool, and the projection field `merge_lane` on `_project_out`.
Principle (verbatim): "a runner never runs the full suite and never sees a failure it did not cause".
## Requirements (verbatim)
- Item 4: "Queue: a runner reports its PR at OPEN (not only at merge); CP keeps queue rows per project. The runner stops calling `jaah-forge pr auto-merge`/`pr merge` when the feature is on (node guard denies both in runner panes) and keeps its current poll-until-merged loop."
- Item 7: "Runner playbook: with the feature on, drop the local `./dc check full` before PR (`playbook_prompt.py`, `repo-access-levels.md`); keep quick + changed-area tests."
- Decided by the BA (D15): the real instruction homes are `tools/project_config.py::gate_directive` (~275-282) and `apps/api/app/dispatch_compose.py` (~415-519, 806, 822-835, 941, and the `_start_here_block` gate line). `playbook_prompt.py:38` is a fixture; change it only if a test needs it. The `repo-access-levels.md` doc line is C9's.
## Approach (settled)
- **Guard.** In `tools/check_gh_pr_guard.py`, deny `jaah-forge pr merge` and `jaah-forge pr auto-merge` ONLY when `CONTROL_TASK_ID` is set AND the governing project's `merge_lane` is true.
- The deny message names the replacement: "merge lane merges this project — report with task_pr_queue and poll `jaah-forge pr view`".
- The lane script is not a Claude session, so the hook never sees it.
- **Projection.** `tools/project_config.ProjectConfig.merge_lane: bool` is read LENIENTLY: absent → False. It is NOT in `GOVERNANCE_FIELDS`/`BOOL_FIELDS`, because a strict read would make an older CP's projection UNRESOLVED (fail-closed).
- **Prompt.** In `dispatch_compose`, add an ON ALTERNATE block, selected before substitution, that:
- drops the full gate before PR;
- calls `task_pr_queue` right after `jaah-forge pr create` and after every later push;
- does not arm auto-merge;
- keeps the `jaah-forge pr view` poll-until-merged loop;
- settles `merged_pr` only after the lane merged.
- **Gate directive.** The `gate_directive` ON variant reads `gate: <quick> (loop) · merge lane runs <full>`.
- **Compose input.** The input is `lane_state` passed from `lease_drain` and `dynamic_placement` (~553).
- **OFF must be BYTE-IDENTICAL** to develop's render. Never re-render a shared template with a branch that moves whitespace.
- Coordination: C3b also edits `lease_drain` / `dynamic_placement` in this wave. That is not a dependency; rebase onto whichever merges first.
## Acceptance (verbatim)
- AC4.3 With the feature ON, a runner pane (`CONTROL_TASK_ID` set) **cannot** run `jaah-forge pr merge` or `jaah-forge pr auto-merge`. The node guard denies both, with a message that the merge lane merges this project. Human (non-runner) sessions are unaffected. With the feature OFF, both are allowed exactly as today.
- AC4.4 The runner keeps its current poll-until-merged loop and settles `merged_pr` only once the lane has merged it. The runner instruction text says so when ON. With the feature OFF, the instruction text is byte-identical to today's.
- AC7.1 With the feature ON for the task's project, the runner prompt and the gate directive no longer tell the runner to run `gate_full_cmd` before PR. They keep the quick gate and any changed-area tests the project already prescribes. They tell the runner to report the PR at open and to poll until the lane merges it. With the feature OFF, the text is byte-identical to today's.
Credentials & reach: none. Every oracle is a worktree-local unit test (verified).
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)."
- "No change to how a runner settles: it still settles `merged_pr` after the merge."
- Do NOT:
- deny `pr merge` in human sessions or for OFF projects;
- add `merge_lane` to the strict governance field sets;
- drop the quick gate;
- edit docs (C9), CP lane routes (C3a) or CI.
Done means:
- AC4.3 — guard tests:
- flag ON + runner env → both `pr merge` and `pr auto-merge` are denied with the replacement message;
- flag OFF + runner env → both allowed;
- no `CONTROL_TASK_ID` → both allowed;
- a projection missing `merge_lane` → allowed, and not UNRESOLVED.
- AC4.4 / AC7.1 — a `dispatch_compose` prompt-render test in both modes:
- the OFF render is byte-identical to a snapshot captured from develop (AC0.1 OFF branch);
- the ON render contains `task_pr_queue` and the `pr view` poll, and contains no `pr auto-merge` instruction;
- a grep of the ON render shows no "before PR" plus full-gate text;
- the `gate_directive` ON/OFF test shows OFF unchanged and ON reading `merge lane runs <full>`.
Required checks: gate, review-lane
Outcome
Merge lane runner side: guard denies runner pr merge/auto-merge on lane projects, lenient ProjectConfig.merge_lane, lane prompt + gate directive; OFF byte-identical. Full gate green; PR #4831 merged; Jaah Deploy ok. Deferred: task_pr_queue refusal when lane turns off.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)