docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
# Orchestrator instructions — trunk-based flow
|
|
|
|
|
|
|
|
|
|
`feat/*` → `dev` → `main` → `release`. One PR per hop, merges serialised by the
|
|
|
|
|
orchestrator. Forgejo has no native merge queue, so you are the merge queue.
|
|
|
|
|
|
|
|
|
|
Use the Centralis `forge_*` tools for every remote operation — PRs, protection,
|
|
|
|
|
merges, tags. Use local git only for rebases you have to resolve by hand.
|
|
|
|
|
Never call the Forgejo API with curl and a pasted token; the server holds it.
|
|
|
|
|
|
|
|
|
|
The same file exists in `centralis` with the same rules. Where the two repos
|
|
|
|
|
differ it is noted inline.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## The branches
|
|
|
|
|
|
|
|
|
|
Each branch has its own environment here, and `infra/deploy/env-topology.sh` is
|
|
|
|
|
the single source of truth for where each one lives:
|
|
|
|
|
|
|
|
|
|
| Branch | Deploys to | Receives |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `feat/*`, `fix/*` | nothing | your work |
|
|
|
|
|
| `dev` | dev — vps1, mesh-only, own Postgres | feature PRs |
|
|
|
|
|
| `main` | beta — beta.thermograph.org, vps2 | promotions from `dev` |
|
|
|
|
|
| `release` | prod — thermograph.org, vps2 | promotions from `main`, and hotfixes |
|
|
|
|
|
|
|
|
|
|
`deploy.yml` handles all three: the branch selects the environment, a matrix
|
|
|
|
|
covers backend and frontend, and each leg checks whether the push actually
|
|
|
|
|
touched its domain before rolling. So **FE and BE ship out of lockstep** — a
|
|
|
|
|
backend change lands without a frontend deploy. That is the point of the split,
|
|
|
|
|
and it is why `/api/version` and `PAYLOAD_VER` discipline are load-bearing:
|
|
|
|
|
whatever you promote, the other half may already be running something older.
|
|
|
|
|
|
|
|
|
|
`dev` is the default branch, so PRs target it without anyone remembering to
|
|
|
|
|
retarget. All three are protected: **everything is a PR**, for humans and agents
|
|
|
|
|
alike.
|
|
|
|
|
|
|
|
|
|
## Who may merge what
|
|
|
|
|
|
|
|
|
|
This is not the same question as how a change travels, and it is deliberately
|
|
|
|
|
asymmetric. `MERGE_AUTHORITY` in centralis'
|
|
|
|
|
`src/lib/promotion.ts` is the whole policy; `forge_pr_merge` enforces it.
|
|
|
|
|
|
|
|
|
|
- **Into `dev` — free.** Merge feature PRs yourself, and do not leave them open.
|
|
|
|
|
A PR opened against `dev` and abandoned is an unfinished task wearing the
|
|
|
|
|
costume of a delivered one.
|
|
|
|
|
- **`dev` → `main` — yours, batched.** Beta is the test environment, so
|
|
|
|
|
withholding this merge withholds the testing. The judgement being asked of you
|
|
|
|
|
is *when a batch is coherent enough to be worth testing*, not whether you are
|
|
|
|
|
allowed to test it.
|
|
|
|
|
- **`main` → `release` — the owner's.** That merge deploys to prod. Call
|
|
|
|
|
`promote(from="main", to="release")`, which prepares the PR and returns what
|
|
|
|
|
would ship, the CI evidence and a recommendation. Then put it to the owner —
|
|
|
|
|
including "I would wait", if that is what you think. No flag overrides this,
|
|
|
|
|
and routing around it is not an option.
|
|
|
|
|
|
|
|
|
|
`confirm_protected_base` is unrelated to the above: it is the hotfix path for a
|
|
|
|
|
*non-promotion* PR aimed straight at `main` or `release`, and it is audited
|
|
|
|
|
loudly.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 1 — repo setup
|
|
|
|
|
|
|
|
|
|
Idempotent. Re-run it whenever you are unsure; every step reports "already
|
|
|
|
|
correct" rather than erroring.
|
|
|
|
|
|
|
|
|
|
1. **The three branches exist.**
|
|
|
|
|
`forge_branches(create={branch:"main", from:"dev"})` and the same for
|
|
|
|
|
`release`. If a branch exists but points elsewhere, the tool says so — that
|
|
|
|
|
is a divergence to reconcile deliberately, not to overwrite.
|
|
|
|
|
2. **`dev` is the default branch.**
|
|
|
|
|
`forge_repo_settings(apply_flow_defaults=true)` — sets the default branch and
|
2026-08-02 00:15:42 +00:00
|
|
|
permits the merge styles the chain uses, plus rebase-updates (without which
|
|
|
|
|
the merge queue's core primitive 403s).
|
|
|
|
|
|
|
|
|
|
The chain uses exactly two: **squash** for `feat/*` → `dev`, and
|
|
|
|
|
**fast-forward-only** for both promotions. Merge commits and rebase merges
|
|
|
|
|
are disabled deliberately — see the merge-style table in the root
|
|
|
|
|
`CLAUDE.md`. If `apply_flow_defaults` re-enables them, that is the tool
|
|
|
|
|
disagreeing with the policy, not permission to use them.
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
3. **Protection.** `forge_branch_protection(branch="dev", apply=true)`, then
|
|
|
|
|
`main`, then `release`.
|
|
|
|
|
|
2026-08-02 00:19:59 +00:00
|
|
|
**All three** branches get the required check. `release` was the exception
|
|
|
|
|
until 2026-08-01: `pr-build.yml` fired only for PRs into `dev` and `main`, so
|
|
|
|
|
a PR into `release` reported no status at all, and requiring a context
|
|
|
|
|
nothing produces makes every production promotion permanently unmergeable —
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
a 405 that names no cause. This estate has already lost an afternoon to
|
2026-08-02 00:19:59 +00:00
|
|
|
exactly that, from a single mistyped check name.
|
|
|
|
|
|
|
|
|
|
It was closed in the only safe order, and that order still governs any change
|
|
|
|
|
to the context name, or a new protected branch: add the branch to
|
|
|
|
|
`pr-build.yml`'s `on.pull_request.branches`, confirm **on a real PR** that
|
|
|
|
|
the context appears, *then* require it. Confirmed on #167 — the gate reported
|
|
|
|
|
`PR build (required check) / gate (pull_request)` — before the check was
|
|
|
|
|
enabled. The other order blocks prod.
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
4. **Stale branches.** `forge_branches()` lists every branch with its age, how
|
|
|
|
|
much unmerged work it carries, and a verdict:
|
|
|
|
|
- `merged` — nothing on it the trunk lacks. Delete freely.
|
|
|
|
|
- `stale` — unmerged work, untouched 14+ days. **File a `stale-branch: <name>`
|
|
|
|
|
issue summarising its diff first**, then delete. Never discard work
|
|
|
|
|
silently.
|
|
|
|
|
- `ageing` — 2+ days with work still on it. Report it; do not delete.
|
|
|
|
|
5. **Module map.** `.claude/ownership.md` — read it before partitioning work.
|
|
|
|
|
|
|
|
|
|
## Phase 2 — the ongoing loop
|
|
|
|
|
|
|
|
|
|
### Dispatching subagents
|
|
|
|
|
|
|
|
|
|
Partition by module using `.claude/ownership.md`. Never give two concurrent
|
|
|
|
|
agents tasks touching the same files; if that is unavoidable, sequence them.
|
|
|
|
|
|
|
|
|
|
Inject this into every subagent task:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Branch off latest `dev` as feat/<slug>. Touch only these files/modules: <scope>.
|
|
|
|
|
One logical change, small diff. Rebase onto dev before opening your PR.
|
|
|
|
|
PR targets `dev`. The description must list: what changed, how it was tested,
|
|
|
|
|
files touched. Do not merge your own PR — the orchestrator handles all merges.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Keep a branch alive **less than a day**. A task bigger than that is several
|
|
|
|
|
stacked tasks.
|
|
|
|
|
|
|
|
|
|
### The merge queue
|
|
|
|
|
|
|
|
|
|
You merge **one PR into `dev` at a time**. For each:
|
|
|
|
|
|
|
|
|
|
1. `forge_prs(base="dev")` — the queue, with each PR's head sha and triage flag.
|
|
|
|
|
2. `forge_pr_update(pr=N)` — replay it onto the current tip. If the head sha
|
|
|
|
|
does not move, it was already current and no CI will re-fire; do not sit
|
|
|
|
|
waiting for a run that was never scheduled.
|
|
|
|
|
3. `forge_pr_await(pr=N, until="checks_complete")` — checks on the **rebased**
|
|
|
|
|
head. The run you saw before the update was for a merge base that no longer
|
|
|
|
|
exists.
|
2026-08-02 01:06:41 +00:00
|
|
|
4. `forge_pr_merge(pr=N, method="squash", delete_branch=true)`.
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
5. Only then move to the next PR.
|
|
|
|
|
|
|
|
|
|
`block_on_outdated_branch` is set on `dev`, so Forgejo enforces step 2 rather
|
|
|
|
|
than trusting you to remember it: after one merge, the next PR is refused until
|
|
|
|
|
it has been replayed. Treat that 405 as the queue working, not as a fault.
|
|
|
|
|
|
|
|
|
|
If a rebase conflicts: resolve it yourself if it is trivial and mechanical
|
|
|
|
|
(under ~20 lines), otherwise hand it back to the owning agent with the conflict
|
|
|
|
|
context. Do not resolve a large conflict on someone else's behalf.
|
|
|
|
|
|
|
|
|
|
### Promotion: `dev` → `main`
|
|
|
|
|
|
|
|
|
|
Promote when `dev` is green and a coherent batch is complete — not on a timer,
|
|
|
|
|
not mid-feature. `promote(from="dev", to="main")` opens the PR with the
|
|
|
|
|
divergence, the conflict prediction and the changelog written into its body.
|
2026-08-02 01:06:41 +00:00
|
|
|
Merge it with `method="fast-forward-only"`.
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
|
|
|
|
|
Unready work belongs behind a feature flag, or not on `dev` yet. **Never promote
|
|
|
|
|
around it with cherry-picks.**
|
|
|
|
|
|
|
|
|
|
`promote` refuses a hop whose two tips have identical trees — the commit counts
|
|
|
|
|
look like real work while the content is already on the target under different
|
|
|
|
|
SHAs, and merging it would fire the target's deploys for nothing.
|
|
|
|
|
|
|
|
|
|
### Promotion: `main` → `release`
|
|
|
|
|
|
2026-08-02 01:06:41 +00:00
|
|
|
Prepare with `promote`, then ask. On the owner's yes, merge with
|
|
|
|
|
`method="fast-forward-only"` and tag it: `forge_releases(create={tag:"v<semver>", target:"release", ...})`.
|
|
|
|
|
|
|
|
|
|
**On fast-forward.** Both promotions move by fast-forward, and as of
|
|
|
|
|
2026-08-01 that is in force rather than aspirational. It was blocked until then
|
|
|
|
|
for a structural reason worth remembering: every promotion had been a merge
|
|
|
|
|
commit *into* the target, which the source never receives, so the branches were
|
|
|
|
|
mutually divergent by construction and Forgejo refused `fast-forward-only` —
|
|
|
|
|
correctly. The one-time reconciliation the old policy was waiting for has
|
|
|
|
|
happened; the trees were already identical, so it carried no content.
|
|
|
|
|
|
|
|
|
|
`release ⊆ main ⊆ dev` now holds, and a promotion lands the branches on the
|
|
|
|
|
same SHA rather than merely the same tree. Merge commits and rebase merges are
|
|
|
|
|
disabled repo-wide, so a slip fails loudly instead of quietly re-diverging the
|
|
|
|
|
chain. If Forgejo refuses with "diverging branches", the target has picked up a
|
|
|
|
|
commit the source lacks — reconcile it once, target into source, fast-forward.
|
|
|
|
|
Do not attempt to "fix" it with a rebase or a force-push.
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
|
|
|
|
|
### Hotfix
|
|
|
|
|
|
|
|
|
|
1. `forge_branches(create={branch:"hotfix/<slug>", from:"release"})`.
|
|
|
|
|
2. Fix, PR into `release`, checks green, merge with
|
|
|
|
|
`confirm_protected_base: true`, tag `v<semver-patch>`.
|
|
|
|
|
3. **Immediately down-merge** `release` → `main` and `main` → `dev`, as merges,
|
|
|
|
|
not cherry-picks, in the same session. A hotfix that exists only on `release`
|
|
|
|
|
is a regression scheduled for the next promotion.
|
|
|
|
|
|
|
|
|
|
Note that CI does not gate PRs into `release` — `pr-build.yml` fires only for
|
|
|
|
|
`dev` and `main`. A hotfix's green evidence therefore has to come from somewhere
|
|
|
|
|
else; say plainly where it came from rather than implying a check passed.
|
|
|
|
|
|
|
|
|
|
A hotfix that touches only one domain deploys only that domain. Check the other
|
|
|
|
|
half's `/api/version` before assuming the fix is live end to end.
|
|
|
|
|
|
2026-07-26 22:05:26 +00:00
|
|
|
### When you genuinely need to push directly
|
|
|
|
|
|
|
|
|
|
`apply_to_admins` is on, which means these rules bind the owner too. That is
|
|
|
|
|
deliberate — protection an admin walks straight through is documentation, not a
|
|
|
|
|
control, and every account here is an admin — but it does remove the escape
|
|
|
|
|
hatch, so the way back in is worth writing down before it is needed at 3am.
|
|
|
|
|
|
|
|
|
|
There is no force flag. The supported route is to lift the rule, do the thing,
|
|
|
|
|
and put it back:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
forge_branch_protection(branch="dev", delete=true) # rule gone; direct push allowed
|
|
|
|
|
git push origin dev # the emergency change
|
|
|
|
|
forge_branch_protection(branch="dev", apply=true) # restored to policy
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Both the removal and the restore are audited, which is the point: the hatch is
|
|
|
|
|
open on the record rather than by a quiet exception. Restore it in the same
|
|
|
|
|
session — a protection rule "temporarily" removed is how a repo ends up
|
|
|
|
|
unprotected for a month.
|
|
|
|
|
|
|
|
|
|
The failure mode that actually justifies this: if the required status context
|
|
|
|
|
is ever renamed and no longer matches, **nothing can merge into `dev` or `main`
|
|
|
|
|
at all**, including by you, because `apply_to_admins` no longer exempts anyone.
|
|
|
|
|
`forge_pr_await` and `forge_pr_merge` both name that case explicitly when they
|
|
|
|
|
see it. The fix is to correct `pr-build.yml`'s workflow/job names and
|
|
|
|
|
`CENTRALIS_REQUIRED_CHECK` together — but if you need to land the fix itself,
|
|
|
|
|
lift the rule as above.
|
|
|
|
|
|
docs: the orchestrator runbook and the module map
.claude/BRANCHING.md — how the trunk-based flow is actually run: the serialised
merge queue (Forgejo has no native one, so whoever orchestrates is it), the two
promotions and who decides each, and the hotfix down-merge that has to happen in
the same session or it becomes a regression scheduled for the next promotion.
Two things it states because getting them wrong is expensive here. CI does not
gate PRs into `release` — pr-build.yml fires only for dev and main — so a
required check on that branch would make every production promotion unmergeable
with a 405 naming no cause; closing that gap means changing the workflow first
and turning the requirement on second. And the chain cannot fast-forward: every
promotion leaves a merge commit on the target the source never receives, so
promotions are merge commits until someone deliberately reconciles the branches.
.claude/ownership.md — which tasks may run concurrently, seamed along the four
domains, plus the shared spine that has to be serialised: the workflows (CI
lives only in the root .forgejo/), env-topology.sh, the deploy entry points and
the CLAUDE.md files. Names the recurring collisions too — a schema change spans
alembic and whatever reads the column, and if the payload shape moves it is
PAYLOAD_VER and the frontend as well, which is one task and not two.
2026-07-26 19:53:33 +00:00
|
|
|
## Guardrails
|
|
|
|
|
|
|
|
|
|
- Never force-push `dev`, `main` or `release`.
|
|
|
|
|
- Never merge a red PR, hotfix included. Fix CI, or fix the tests in the same PR.
|
|
|
|
|
- Never merge two green PRs back to back without re-checking the second against
|
|
|
|
|
the new tip.
|
|
|
|
|
- If two down-merges or promotions conflict beyond the trivial, stop and report
|
|
|
|
|
rather than resolving autonomously.
|
|
|
|
|
- Weekly: `forge_branches()` and report anything `ageing` or `stale`.
|