Add merge lane billing plan flag
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 both with `task_read` for any detail this body clamps.
Operator ask (verbatim): "Reduce CI/CD test gate flooding. Currently when we have multiple runners, each of them runs quick gates many times, then full gate in runner, then they push and merge and cicd runs the same groups of tests and more on push, pr and merge. That creates load on the nodes and some tests fail, which causes the runners to analyze tests fix something, rerun the full gates and push and pr again. Everything fails even more and this flood significantly delays the work. [...] Let's do this as a feature in billing plan. Initial value: off."
## Requirement (work item 1, verbatim)
"Plan feature `merge_lane_enabled` on `billing_plans`, per-org override on `orgs` (`merge_lane_enabled_override`), read by ONE policy module, following the `hosting_allowed` / `compliance_enabled` / `nodevault_allowed` pattern in `apps/api/app/plan_terms.py`. Initial value OFF on every plan. Console: plan editor + org override, same surface as the existing three."
## Approach (architecture §4.1, settled)
- Add one ADDITIVE migration:
- `billing_plans.merge_lane_enabled BOOLEAN NOT NULL DEFAULT false`;
- `orgs.merge_lane_enabled_override BOOLEAN NULL`.
- Seeded plans (trial/team/enterprise, `0401_baseline.py:1446`) take the column default.
- Add `plan_terms.PlanTerms.merge_lane_enabled: Term[bool]`, plus the scalar `plan_terms.merge_lane_enabled(session, org) -> bool`.
- Resolution order: override, then plan, then `False` when there is no plan row.
- Mirror `nodelink_allowed` (`plan_terms.py:324-329`). Four siblings exist today, nodelink included.
- THIS SCALAR IS THE CONTRACT the lane core child (C3a) consumes. Keep its name and signature exactly.
- Plan payloads in `routes/billing_plans.py` (~183-434) and `routes/orgs.py` (~1414-1581) gain `merge_lane_enabled` / `merge_lane_enabled_override`, in the same shape as `nodevault_allowed` / `nodevault_allowed_override`.
- Audit writes the way the NodeVault override is audited.
- `org_plan_admin.clear_overrides` (~138-145) clears it.
- Web:
- a `BillingPlanDialog.tsx` checkbox (~400-411);
- a `PlanTab.tsx` row (~355-370) with value, an "overridden" marker, the plan value and Reset.
- Copy: label "Merge lane"; help "Batch-test and merge runner PRs on the control plane".
## Acceptance (AC1.1–AC1.4, verbatim)
- AC1.1 `billing_plans.merge_lane_enabled` is a bool, NOT NULL, server default false. `orgs.merge_lane_enabled_override` is a nullable bool, where NULL means "use the plan". After the migration, every existing and every seeded plan (trial/team/enterprise) reads false and every org override reads NULL. The migration is additive (coding standard: it must stay additive while two releases overlap).
- AC1.2 `plan_terms` is the **only** reader. `effective()` carries `merge_lane_enabled: Term[bool]`, and a scalar `merge_lane_enabled(session, org)` resolves the override, then the plan, then **false when there is no plan row**. No other module reads the two columns directly.
- AC1.3 Plan CRUD (`GET/POST/PATCH /api/billing-plans`) exposes and accepts the field. Org plan (`GET/PUT /api/orgs/{id}/plan`) exposes value, source and plan value, and accepts set/reset (`null`). Writes are audited the way the NodeVault override is. A plan change clears the override exactly as `clear_overrides` does for the existing three.
- AC1.4 Console, same surface as the existing three:
- the plan editor dialog shows a "Merge lane" checkbox, unchecked on every seeded plan;
- the org Plan tab shows a "Merge lane" row with its value, an "overridden" marker, the plan value, and Reset;
- the label and its one-line help follow the UI copy standard: one noun label and one help line, no sentence labels;
- toggling saves and survives a reload;
- the new row is visually consistent with its NodeVault sibling: same control type, alignment and spacing, and nothing truncated or wrapped awkwardly at the supported viewport widths.
Credentials & reach:
- Unit, API and migration tests (AC1.1–AC1.3, the component tests): none beyond the worktree's own `./dc` stack. Status: verified (worktree-local; no external reach).
- AC1.4 browser availability on the dev CP:
- Login `gmind/skynet/dev`: credential verified. Its stored `base_url` names an older host; use `dev.jaah.ai` per `docs/architecture.md`.
- Environment `https://dev.jaah.ai`: **deferred**. `dev.jaah.ai/api/health` returned no answer from h2 (HTTP 000).
- Run it if your node reaches dev; otherwise report it deferred with this reason.
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 edits to `infra/github/rulesets/*.json`, `.github/workflows/validate.yml`, `tenancy-gate.yml`, `jaah-deploy.yml` or `supply-chain.yml`, or to the `tenancy-oracles` and `promotion-provenance` jobs."
- "No change to the runner flow, guard, prompt, pool or CI when the flag is OFF (R0)."
- "No new UI beyond the three surfaces (plan editor, org Plan tab, project settings field) plus a read-only queue/certificate view (D11)."
- This child does NOT touch:
- lane tables, `projects.verification_slots`, the project settings page, runner prompts, guards or CI — other children own those;
- any migration seed flipping a plan ON.
Done means:
- AC1.1 — run the migration on a fresh `./dc` DB. Then:
- `SELECT key, merge_lane_enabled FROM billing_plans` returns all false;
- `SELECT count(*) FROM orgs WHERE merge_lane_enabled_override IS NOT NULL` returns 0.
- AC1.2 — `apps/api/tests/test_plan_terms.py` covers override true/false/NULL × plan true/false × no plan, and the cases pass. A grep for `merge_lane_enabled` outside `plan_terms.py`, `models`, migrations, the routes/admin writer and tests returns no direct column read.
- AC1.3 — extend `tests/billing/test_plan_routes.py` and `tests/test_org_plan_admin.py` with the same cases the NodeVault field has. They pass.
- AC1.4 — `PlanTab.test.tsx` and the billing dialog tests are updated and pass. Availability (R29), (deferred — dev.jaah.ai unreachable from h2; run only if your node reaches it):
- On the dev CP as superadmin, open Settings › Billing › a plan and see "Merge lane" unchecked.
- Open Orgs › an org › Plan, toggle the override on, reload, and see it ON with source "override".
- Reset, reload, and see it back to the plan value.
- AC0.1 OFF branch for this surface: every seeded plan reads false and the existing suites stay green.
Required checks: gate, review-lane
Outcome
Merge lane plan term + per-org override (migration 0407, plan_terms.merge_lane_enabled scalar, routes, audit, web switch/row). Full gate green; PR #4817 merged, Jaah Deploy succeeded. Dev-CP availability check deferred (dev.jaah.ai unreachable).
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)