oll.am · Release Engineering · Merge-to-Main Runbook

Converging the sprawl onto main.

Fourteen open feat/* branches, four of them duplicated work. This is the ordered plan to fold them into mainMemory first (its code is deployed but lives off-trunk), then rebase the rest onto the new main. Merge order is decided here; deploy is a separate deliberate act.
Jump: Do this first Branch inventory Merge sequence Rebase recipe Clean deploy

Surveyed read-only against origin/main @ 44a8ffe on 2026-07-07 (git fetch + gh pr list + git merge-tree). Nothing was merged, rebased, or pushed — merging is Sam's per-PR call. This page is the map for making those calls in the right order.

Do this first · do not skip

1 — Memory merges first. 2 — Frozen/money-path branches (Core · Model · Memory) need adversarial verify + Sam's explicit per-PR go. 3 — Docs go straight to main. 4 — Close the superseded PRs so the sprawl actually shrinks.

The sprawl is only partly real work. Four PRs are duplicated / retired designs (#75 · #76 · #77 → folded into #80; #79 → replaced by the thin seam). Retiring them takes the count from 10 open PRs to 6 that actually need a decision. Do that on the same pass you merge Memory — otherwise the noise regrows.

Why Memory is the foundation

memory.oll.am is already live and deployed, but its code lives on feat/oll-memorynot on main. Until it merges, main is not the source of truth for a running money-path spine, and every branch that builds on Memory (e.g. the Bible verse-reingest work, which is literally stacked on it) has nothing to rebase onto. Landing Memory makes main honest, then everything else rebases onto a main that finally contains the whole platform.

1 · The branch inventory

Every open PR and every origin/feat|docs/* branch, measured against origin/main @ 44a8ffe. "Diverged" = commits ahead of main. "Clean?" = does git merge-tree apply without conflict today. Verdict legend below the table.

Branch / PRWhat it doesKindFrozen /
money?
AheadClean
vs main?
Verdict
feat/oll-memory
PR #80
Consolidated Memory service (ingest + RAG + extract in ONE service on LlamaIndex; pgvector + RRF; gateway-routed extraction). The code behind the live memory.oll.am. Code YES — Memory spine 1 clean Merge · 1st
feat/bible-verse-reingest
PR #81
nomic task-prefixes + verse-level multi-granularity retrieval + real PDF text extraction, on top of Memory. Stacked on feat/oll-memory (contains its base commit daf6d7a). Code YES — Memory 9 clean* Rebase→Merge
feat/oll-extract
PR #75
Standalone structured-extraction service. Folded into #80 as extract.py. Code new svc 1 clean Superseded · close
feat/oll-ingest
PR #76
Standalone document-ingestion service. Folded into #80. Code new svc 1 clean Superseded · close
feat/oll-rag
PR #77
Standalone hybrid-retrieval service (pgvector + RRF). RRF fusion ported verbatim into #80; then superseded. Code new svc 1 clean Superseded · close
feat/oll-mcp
PR #79
Heavy deployable MCP gateway (auth + entitlement-gated platform tools, own deploy). The thin seam is the chosen design (proven live this session over stdio + HTTP). Code new svc 3 clean Superseded · close
feat/oll-mcp-thin
(no PR yet)
Thin FastMCP adapter over live write/account endpoints + retrieval-first Memory tools — the Agent Seam v1. Both branches edit services/oll-mcp; keep this one. Code seam 3 clean Open PR→Rebase→Merge
feat/oll-model-sovereign
PR #78
Additive EU / on-device privacy-routing tier for the Model gateway (backward-compatible). Parked per #80's note. Code YES — Model 1 clean Rebase→Merge
fix/billing-status-selfheal
PR #30
Core billing /status self-heals a stale Stripe customer (test→live cutover). Oldest open PR (2026-06-29). Code YES — Core 1 CONFLICT† Rebase→Merge
feat/core-guest-checkout
(remote)
Core guest (no-JWT) checkout — the next-dollar unblock. Being re-landed locally on feat/core-guest-checkout-reland (not yet pushed). Code YES — Core 2 clean In-flight · verify
feat/dev-compose
PR #82
One keyless dev docker-compose (Core→SQLite, model→mock) — "docker compose up just works." Green + adversarially cleared. Code (dev/infra) no 1 clean Merge
feat/observability-sentry
(no PR)
Sentry wiring across services + Core. Touches many service dirs — best landed after Memory so it can cover it too. Code touches frozen 1 clean Rebase→Merge (late)
feat/ci-hardening
(no PR)
CI workflow hardening (.github/** + integration compose). Foundation for everything else's green gate. Code (CI) no 1 clean Merge (early)
feat/landing-pages
(no PR)
Landing/marketing pages under landing/. Product-frontend surface, no backend risk. Code (landing) no 2 clean Rebase→Merge
feat/postiz-distribution-layer
PR #83
Postiz distribution layer (deploy config under deploy/ + a site page). Deploy-ready, not deployed. Code (deploy) + docs no 1 CONFLICT† Rebase→Merge

* feat/bible-verse-reingest merge-tree's clean against today's main because it carries the whole Memory branch inside it — but it must be rebased onto main after Memory lands, or it will re-introduce Memory's commit as a duplicate. Treat it as stacked, not independent.
The only two "conflicts" are in site/build-log.html and state/state.json — the hand-written timeline + generated state, not code. They vanish on rebase (regenerate state, re-add the timeline card). No source-code conflict exists in any open branch.

Net read

Of 10 open PRs, 4 are retire-on-sight (#75/#76/#77/#79). The remaining 6 + three PR-less branches are real. Zero of them conflict in code — the platform is far less tangled than the branch count suggests. The whole job is: land Memory, retire the duplicates, rebase the rest onto the new main in dependency order.

2 · The dependency-ordered merge sequence

Each rung: rebase on latest main → CI green → adversarial verify (if frozen/money) → Sam per-PR go → merge. Docs-only changes skip the PR and go straight to main behind the design gate. Retire the superseded PRs on the Memory pass.

0Retire the duplicates + land the CI floorclears the noise · no product risk

Close, don't merge: PR #75 (oll-extract), #76 (oll-ingest), #77 (oll-rag) — folded into #80. Close #79 (heavy oll-mcp) — the thin seam is chosen. Leave a one-line "superseded by #80 / thin-mcp" comment on each so the decision is auditable, then delete the branches.

Merge first, if you want a hard gate under the rest: feat/ci-hardening (CI-only). It has no product surface, so it's a clean, low-risk first merge that strengthens every subsequent branch's green check.

Result: 10 open PRs → 6 real ones. Sprawl actually shrinks before any product code moves.

1Memory — the foundational mergePR #80 · FROZEN / money-path

PR #80 feat/oll-memory. Adds services/oll-memory/ (new dir — no code conflict with main). This is a frozen money-path spine, so:

  1. Rebase on origin/main (only state/timeline may conflict — regenerate).
  2. CI green: services.yml + the keyless integration.yml boot (mock providers, no keys).
  3. Adversarial verify — fan out reviewers on the ACL/IDOR path (public-or-intersect metadata filters), the document_iddocument remap, RRF fusion parity vs the retired oll-rag, and the extraction retry/422 discipline. Verify by running a real ingest→query→extract loop against a Neon oll_memory branch, not by reading.
  4. Sam per-PR go.
  5. Merge → main now contains the deployed Memory spine.

On merge: add oll-memory to deploy-all.yml as a T0 foundation service (it's a spine, same tier as Core + Model) and to the plan job's path-map — currently the workflow only knows core/model/write/foto/site. Without this, the decoupled deploy can't target Memory. (Small workflow edit; can ride in the same PR or a fast follow.)

2Bible verse-reingest — rebase off the new mainPR #81 · stacked on Memory

PR #81 feat/bible-verse-reingest. It is stacked on Memory (carries daf6d7a). The instant #80 lands, rebase #81 onto the new main so its 8 own commits replay on top of the merged Memory — dropping the now-duplicated base. Then CI green → adversarial (Memory retrieval quality: nomic task-prefixes, verse-level granularity, the CiteBible locators) → Sam go → merge.

3The independent, low-risk mergesrebase → green → merge · no frozen surface

These touch no frozen money-path service — merge them in any order once rebased + green (no adversarial gate, just Sam's go where a PR exists):

  • feat/dev-compose (PR #82) — already green + cleared; merge.
  • feat/landing-pages — rebase, green, merge.
  • feat/postiz-distribution-layer (PR #83) — rebase (resolves the build-log conflict), green, merge.
  • feat/oll-mcp-thin — open its PR, rebase, green; it's the seam, not a money path, but verify the tool wiring loop still passes both transports before merge.
4The frozen-service fixes + cross-cuttingadversarial + Sam-go · land last

Money-path Core/Model changes — each needs adversarial verify + Sam's explicit go. Land last so they rebase onto a fully-converged main:

  • fix/billing-status-selfheal (PR #30) — Core billing; rebase (resolves state/build-log conflict), adversarial on the Stripe test→live self-heal path, Sam go, merge.
  • feat/core-guest-checkout / -reland — the next-dollar unblock; reconcile the remote branch with the local reland first (pick one), then treat as a Core money-path merge.
  • feat/oll-model-sovereign (PR #78) — additive Model tier; rebase, adversarial (backward-compat of the routing tier), Sam go, merge.
  • feat/observability-sentry — cross-cutting; land after Memory so Sentry covers it too; rebase, green, merge.

3 · The per-branch rebase recipe

After Memory (or any earlier rung) lands, every remaining feat/* must be replayed onto the new main before its own merge. The concrete moves:

# 0. always start from a fresh main
git fetch origin

# 1. rebase the feature branch onto the new main
git switch feat/<branch>
git rebase origin/main

# 2a. clean replay → straight to force-push
# 2b. conflict? the ONLY conflicts here are the hand-written narrative files:
#     site/build-log.html   (timeline rail)
#     state/state.json      (generated live-state)
#   resolve by taking main's version, then regenerate/re-add your card:
git checkout --theirs state/state.json && python3 scripts/state.py   # regen, don't hand-merge
git checkout --theirs site/build-log.html                            # keep main's rail, re-add your entry after
git add state/state.json site/build-log.html
git rebase --continue

# 3. re-run the gate + CI locally before pushing
sh scripts/design-audit.sh          # docs/nav/token/naming gate
sh scripts/oll-am-local.sh mock     # keyless full-stack boot, if the branch touches a service

# 4. publish the rebased branch (force-with-lease is safe; plain --force is not)
git push --force-with-lease origin feat/<branch>

# 5. CI re-runs on the pushed branch → wait green → request Sam's per-PR go → merge

Conflict triage — the one rule

Across all 14 branches, no two touch the same source file. Every conflict you will see is in state/state.json or site/build-log.html — machine-generated state and the hand-written timeline. Never hand-merge those: take main's copy, then re-run scripts/state.py / re-add your one timeline card. If a rebase ever surfaces a conflict in a .py / Dockerfile / openapi.yaml, stop — that means two branches genuinely overlap and it's a design decision, not a mechanical resolve.

4 · The clean deploy — decoupled from merge

Merge ≠ deploy

A merge to main NEVER deploys. Deployment is a deliberate, separate act: tag a CI-green commit, or dispatch deploy-all.yml. Converge all the merges first; deploy once, cleanly, at the end.

Coolify's own git-auto-deploy is OFF on every backend app (ON only for site), so the tag / workflow dispatch is the only path to prod. Once the merge sequence above is done and main is green:

# Option A — tag a green commit (canonical release)
git switch main && git pull
git tag v0.4.0            # that commit must be CI-green
git push origin v0.4.0   # → deploy-all.yml fires

# Option B — deliberate partial deploy
#   Actions → deploy-all.yml → Run workflow
#   services = all | changed | "core,model,memory"

deploy-all.yml gates and orders it for you:

Tier 0 · foundation
Core · Model · Memory
Identity/billing/email, the model seam, and the corpus/retrieval spine. Everything depends on these. Memory joins T0 once #80's workflow entry lands.
Tier 1 · products
foto · write
Product services — thin Core + Model clients. Start only after T0 is healthy.
Tier 2 · edge
site
The landing + these live views. Deploys last.

Memory's "deploy" is a re-point, not a first-launch

memory.oll.am is already running. After #80 merges, its deploy is simply pointing the deployed Coolify app at the main-built image instead of the feat-branch build — a re-point of a live service to the now-authoritative trunk, health-gated like any T0 service. Confirm the running image's digest matches the main-tagged build after the deploy so the "source of truth = main" claim is actually true, not just asserted.

Full CD model + env matrix: CI/CD & Environments. Service topology: The Platform. Live PRs + running services: Control Room.