Skip to content

Custody Transfer execution guide

The plan is 23 releases and roughly 110 slices. Nobody holds that in their head, and an agent should not try. This document says where the truth lives and what the protocol is for one slice.

1. Where state lives

Three places, and none of them is a conversation.

What Where Who writes it
Why the plan is shaped this way Proposal artifact 3ab34375 Rarely changes
The slice list, budgets, critical path, live status Release plan artifact a0fcca2f (declares the db capability) Claude writes status; the operator ticks gates
Resume state and the do-nots memory/project_custody_transfer_v1.md Claude, at the end of every session
Work item state GitHub issues and pull requests, titled [SLICE-ID] ... Updates itself

Anything that must survive goes in one of those. Nothing load-bearing lives only in a context window, because a window does not survive a compaction and its contents go stale silently.

2. The protocol for one slice

  1. Read the slice contract from the release plan artifact. It states inputs, outputs and error cases deliberately, so that exploring the codebase is mostly unnecessary. That is why the slices fit their budget.
  2. Check the tracker for its state and for anything it depends on. A slice with no status document is untouched.
  3. Confirm the issue is open. gh issue view N --json state. An issue is single-use: once its pull request merges it closes, and a later pull request pointing at a closed issue is silently skipped by review.
  4. Branch from the right base. rig-cockpit-ios defaults to feature/fork-cutie-admin-cockpit, not main. Getting this wrong produces a diff against an unrelated old fork.
  5. Tests first, then the implementation, then the adapter. Policy in a pure unit with its own tests; the wiring separately.
  6. Docs in the same pull request as the behaviour. A behaviour change without its doc is incomplete.
  7. Closes #N in the body. Not Part of. This is the single most common reason a pull request stalls.
  8. Write the tracker before doing anything else, including before compacting.

3. Context discipline

The plan was sized on the assumption that a slice starts cold. Three rules follow.

  • Re-read before you rely. Never act on a file snapshot from earlier in the window. A file read an hour ago may have been rewritten since, including by your own tooling.
  • Compact on repo and lane switches, not between slices. Consecutive slices on the same files are where warm context pays for itself. Switching from the Worker to the iOS app carries nothing over but the plan.
  • Write status first, compact second. Reversing that loses the detail you meant to record, and the record then drifts from reality.

4. Rules that are load-bearing

  • Claude never runs a command that touches a secret. No sops, no bw, no kubectl exec, no token minting. Every tool in the custody lane is written by Claude and run by the operator. This constrains tool design: one command, no prompts, re-runnable, output reviewable before it is committed.
  • The rotation tool has zero third-party dependencies. Not a preference. It is what makes the review surface small enough for one person to read in a sitting, which is the entire reason the tool is one-shot rather than a service.
  • The rotation tool is deleted at the end (DEL-1). If it survives, the one-shot design has silently become a permanent privileged service.
  • Infrastructure changes go through OpenTofu. No gh api -X PATCH on repository settings, no dashboards, no CLI mutation of managed resources.
  • The 500-line size gate counts tests. When it misfires, the documented escape hatch is the large-pr-ok label plus a one-line justification in the body, not a dismissal. A body-only edit fires no synchronize event, so an empty commit is needed to re-dispatch review.

5. The do-nots

  • Do not start any slice before INV-0 and INV-2a are settled. INV-0: coupling group identities were keyed per collection run, so every group re-keys when the cutover re-collects and the classification gate goes red across the whole estate on the day it must be trusted. INV-2a: no slice says how the cluster is read, and it decides whether plaintext crosses an SSH session into the operator's shell history.
  • Do not claim the lockout is clean. The stated residual is that code Claude authors runs in pods with secrets mounted, so a pod can read what is mounted into it. Revoking credentials does not touch this. It is held down by operator review on the few secret-mounting repositories, egress policy on those namespaces, and moving credentials to short-lived federation.
  • Do not write half of a coupled pair. The Rig-E chat token exists in the cluster and in a Cloudflare Worker secret and the two must stay byte-identical. A half rotation returns 401, which the cockpit renders as a logout, so it presents as a login bug rather than a rotation fault.
  • Do not revoke before verifying. Rotation order is issue, verify against a real consumer, then revoke. Revoke-first has no rollback, because at the moment verification fails the working value is already gone.
  • Do not run two slices at once on a serialised file. src/web/dashboard.html and its service worker are touched by four slices that each bump a cache version; they must land one at a time under one owner. The conductor's deployment manifest is touched by three, and a merge race on that exact file has already rolled production back one image pin.
  • Do not treat a self-report as evidence. The GPU model reports itself as something it is not. Check what the service publishes about itself, and read the code that produced the string.

6. Operator gates

Fourteen points where work stops until a person acts, listed in the release plan and tickable in the tracker. Two rules for handling them.

Surface a gate as soon as it becomes reachable, not when it blocks. Several of them are slow (a build attach, a destination capture, two soak periods) and can run in parallel with other work if raised early.

Never perform one on the operator's behalf, and never record one as done because it probably was. A gate marked done without evidence is worse than an open gate, because it stops being visible.

7. Who implements what

Three streams, split by which repositories each actor is permitted to touch. That constraint does the scheduling.

Stream Repos Implemented by
Lane A — cockpit product rig-cockpit-worker, rig-cockpit-ios Dev-E, dispatched by labelling the issue agent-ready plus a tier
Lane B — inventory, rotation, cutover claude-3, and the rotation tool tree Orchestrator only
Lane C — platform boundary rig-conductor, rig-gitops, infra Orchestrator only

Lanes B and C are orchestrator-only by standing rule: rig agents do not work on the rig. Lane A is different because the cockpit repos are ordinary product repos that the agents already work in.

One slice per agent at a time. Never batch several onto one agent — batching has already dropped completion to a fraction of the single-task rate. And do not queue a second slice ahead of the first one's merge: stacked branches fare badly here, because the merge gate kills stacked children regardless of how the parent is merged.

A slice is not a workflow. Each was sized for one agent under one budget, so fanning out subagents per slice is waste. Reserve orchestration for work that genuinely fans out: sizing a delta across several subsystems, or an adversarial review pass where a second independent opinion earns its tokens.

8. The merge path, and what currently blocks it

Repo Review Merge
claude-3 None. No Review-E, no Copilot approvals Manual squash, delete branch
dashecorp rig repos Review-E, dispatched push-based Server-side merge gate
Orchestrator PRs outside the rig-internal repos None unless labelled needs-review

The size gate will stop almost every slice, and a label does not clear it

Settled by experiment on 2026-09-12 across worker PRs 166, 170 and 173. This is the single ritual that costs the most time if you do not know it.

Review-E's size gate fires within about four seconds of PR creation and leaves a standing CHANGES_REQUESTED. Its own escape hatch is the large-pr-ok label plus a one-line justification in the body. Applying both does nothing on its own — the gate never re-evaluates, and the standing review blocks the merge indefinitely. Worker PR 166 sat blocked for twenty-three hours with the label already applied.

The fix is a push. An empty commit re-dispatches Review-E, which then passes the gate and does a real review. PR 166 went from stuck to approved about six minutes later, and the conductor's merge gate merged it server-side about a minute after that. A reviewer's newest review supersedes their earlier one, so the approval clears the stale changes-requested with no dismissal needed.

Never dismiss the review instead. On worker main, .github/CODEOWNERS is * @review-e-bot with code-owner review required, so only Review-E's approval satisfies the branch protection. An operator approval does not count. Since nothing re-dispatches a review after a dismissal (rig-conductor#2079), dismissing leaves the pull request permanently un-approvable.

Expect this on most Lane A pull requests. The rig requires tests in the same pull request, and a typical slice is two to three hundred lines of production code against three to six hundred lines of tests, so nearly every slice trips a gate measuring the wrong number. Budget the ritual rather than being surprised by it: label, justification in the body, empty commit, wait.

9. Ship order

Lane A first and compressed, for a reason unrelated to the custody work: TestFlight builds 1-8 expire 2026-09-18/19 and the phone has no chat surface. Lanes A, B and C share no files and run concurrently. Lane B is the critical path at 27 sequential slices with five operator events inside it, so progress elsewhere is real but does not move the end date.