carlos / platform Public

docs: plan for runs started by a scoped service credential (PRO-6214)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev ff19c7c3671a3b2baf335a83e90b81138dd47216 parent bd3ab34
1 file changed, +442 −0
  • docs/superpowers/plans/2026-10-24-service-runs.md +442 −0
diff --git a/docs/superpowers/plans/2026-10-24-service-runs.md b/docs/superpowers/plans/2026-10-24-service-runs.md
new file mode 100644
index 000000000..3e3c08730
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-24-service-runs.md
@@ -0,0 +1,442 @@
+# Runs started by a scoped service credential: implementation plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** A service credential minted with the new `run` role and a command
+list can create, follow, cancel, expire and ack runs of those commands on its
+own app's instances, and can recover from a lost response without starting a
+second run.
+
+**Architecture:** There are three layers:
+- the role and the credential's stored scope (`svcrole`, `cli_tokens`,
+ `Principal`);
+- the run doors admitting that principal with its narrower checks
+ (`apiruns.go`, `apirunsfollow.go`);
+- an idempotency key, stored create-only beside the run objects, that answers
+ retries from its own state.
+
+**Tech Stack:** Go. The console's SQLite store (`internal/console/authstore.go`).
+The deployment bucket through `release.Store` (`Put(ctx, key, body, ifMatch)`:
+an empty `ifMatch` means create-only, and `release.ErrCASConflict` is returned
+on a 412).
+
+**Spec:** `docs/superpowers/specs/2026-10-24-service-runs-design.md`, which
+amends `docs/superpowers/specs/2026-10-08-carlos-run-design.md` § 2 ruling 1.
+
+## Global Constraints
+
+- **People's behaviour on every run door is unchanged.** Every existing run
+ test passes unmodified, apart from new fields in JSON bodies.
+- **Wire and storage changes are additive** (AGENTS.md):
+ - new `cli_tokens` columns go in the ALTER list (`authstore.go:217-224`), with
+ defaults equal to today's value;
+ - new `runs.Request` and `runs.Meta` fields are `omitempty`.
+- **`Run` is reachable only through `svcrole.Exactly(svcrole.Run)`.** No other
+ role reaches the run doors, and `Admin` does not.
+- **Command prefixes:**
+ - at most 32 per credential;
+ - each word 1–128 bytes, with no empty word;
+ - matched word for word, exactly: no globbing, no case folding, no substring.
+- **`Idempotency-Key`:** 1–128 bytes of `[A-Za-z0-9._:-]`. It is honoured for 7
+ days from the create.
+- **Timings:**
+ - create handler deadline: 2 minutes;
+ - pending-key abandon threshold: 5 minutes;
+ - key sweep age: 7 days.
+- **`on_behalf_of`:** an email of at most 254 bytes, set only by a `run`
+ service. A person who sends it gets a 400.
+- **Every refusal on a run door carries a stable `code`** in its JSON body,
+ alongside today's `error` message.
+- **A new `carlos` flag isn't done until `docs/site/cli.md` names it**
+ (AGENTS.md).
+- **Branch rules:** the branch lands through amadan (`amadan branch describe`,
+ then squash, then green CI, then `amadan branch merge … -expect <tip>`),
+ never GitHub.
+
+## Review Focus
+
+1. **A retry of a known key while the first run is still open.** Today the
+ "already has a run open" refusal (`apiruns.go:201`) fires before the body is
+ read. A keyed retry must get its run back, not that refusal. The pin is
+ `TestRunKeyedRetryWhileOpenReturnsTheRun` in Task 4.
+2. **A service listing or following a person's run by guessing its id.** It
+ must get a 404 identical to an unknown id. The pin is
+ `TestRunServiceCannotSeeAPersonsRun` in Task 3.
+3. **A revoked credential name minted again.** The new credential must not see
+ the old one's runs. The pin is `TestRunRemintedNameDoesNotInheritRuns` in
+ Task 3.
+4. **Argv that matches a prefix only as a substring of a word.** For example,
+ `importer` under the prefix `import`. It is refused. The pin is in Task 2's
+ `TestCommandGranted` table.
+5. **A keyed create whose meta declares a sha256 that doesn't match the
+ uploaded bytes.** It gets a 400 `file_hash_mismatch`, and nothing is
+ written, key included. The pin is `TestRunKeyedHashMismatchWritesNothing`
+ in Task 4.
+
+---
+
+### Task 1: The `run` role, which Admin does not reach
+
+**Files:**
+- Modify: `internal/svcrole/svcrole.go`. Add `Run`, add it to `catalogue`,
+ add `Exactly`, and update the Admin doc comment.
+- Test: `internal/svcrole/svcrole_test.go`
+
+**Interfaces:**
+- Produces:
+ - `const Run Role = "run"`
+ - `func Exactly(roles ...Role) Set`. Like `Allow`, it panics on an unknown
+ role, but it does NOT add Admin.
+- The doc on `Admin` changes from "the union of the five above" to say it is
+ the union of every role except `Run`, which is reachable only through
+ `Exactly` (spec § 1).
+
+- [ ] **Step 1: Write the failing tests**
+ - `TestRunIsInTheCatalogue`: `Parse("run")` returns `(Run, true)`, and
+ `All()` contains `Run`.
+ - `TestExactlyDoesNotAddAdmin`: for `s := Exactly(Run)`,
+ `s.Permits(Run)` is true and `s.Permits(Admin)` is false, as is every
+ other role.
+ - `TestExactlyPanicsOnAnUnknownRole`: `Exactly(Role("rnu"))` panics.
+ - Amend `TestAdminPermitsEveryNonEmptySet` so it still covers every `Allow`
+ set, then add: "an `Exactly` set admits only its listed roles."
+- [ ] **Step 2: Run them and watch them fail.**
+ `go test ./internal/svcrole/` fails: `Run` and `Exactly` are undefined.
+- [ ] **Step 3: Implement** in `svcrole.go`.
+- [ ] **Step 4: Watch them pass.** `go test ./internal/svcrole/` passes. Then
+ run `go test ./internal/console/ -run 'Role|Service'` and fix any role-list
+ expectations that enumerate the catalogue. That includes `roleNames()` users
+ in `services.go`. Every one of them must now include `run`.
+- [ ] **Step 5: Commit**: `svcrole: a run role that only an exact set reaches,
+ never Admin (PRO-6214)`
+
+---
+
+### Task 2: A `run` credential's id and command list
+
+**Files:**
+- Modify: `internal/console/authstore.go`. Add to the ALTER list:
+ `ALTER TABLE cli_tokens ADD COLUMN credential_id TEXT NOT NULL DEFAULT ''`
+ and `ALTER TABLE cli_tokens ADD COLUMN commands TEXT NOT NULL DEFAULT ''`.
+ Then a one-time backfill: every row with `kind` a service and an empty
+ `credential_id` gets a fresh random id. It is idempotent, and only empty
+ rows are written.
+- Modify: `internal/console/svcprincipal.go`. Covers `Principal`,
+ `CLIPrincipal`, minting, rotation and listing.
+- Modify: `internal/console/apiservices.go`. Create accepts `commands`, and
+ create and list echo it.
+- Modify: `services.go`. Adds the `--commands` flag.
+- Create: `internal/runs/commands.go`, holding the matcher. It stays a pure
+ function so the console and its tests share it.
+- Test: `internal/runs/commands_test.go`,
+ `internal/console/svcprincipal_run_test.go` and `services_test.go`
+ (extended).
+
+**Interfaces:**
+- Produces in `internal/runs`:
+ - `func CommandGranted(argv []string, prefixes [][]string) bool`
+ - `func CheckCommandPrefixes(prefixes [][]string) error`. It enforces the
+ limits in Global Constraints and returns `ErrNoCommands` for an empty
+ list.
+- Produces in `internal/console`:
+ - `Principal.CredentialID string`. It is set for every service principal,
+ and empty for a person.
+ - `Principal.Commands [][]string`. It is non-nil only for `Run`.
+ - `ErrRunNeedsCommands`, `ErrCommandsNeedRun`.
+ - `NewServicePrincipalScoped(accountID, name, app, role, rungs, commands [][]string, mintedBy, now)`.
+ The signature gains `commands`, and every caller is updated.
+- The stored form of `commands` is canonical JSON: an array of arrays of
+ strings, in the order given. Rotation keeps both `credential_id` and
+ `commands`. A new mint always gets a new `credential_id`.
+- `CLIPrincipal` refuses a `run` row whose `commands` can't be parsed or is
+ empty, the same way it refuses a deploy row without rungs
+ (`svcprincipal.go:136-147`).
+- `--commands` is a comma-separated list of prefixes, and each prefix's words
+ are separated by spaces: `--commands "import classic,import sync"`. It is
+ required with `--role run`, and refused with any other role. Create also
+ requires `--app` for `run`, using the same refusal path as `Service`.
+
+- [ ] **Step 1: Write the failing tests**
+ - `TestCommandGranted`, a table test:
+ - `{"import","classic","--slug","x"}` under `[["import","classic"]]` is
+ true;
+ - `{"import"}` under `[["import","classic"]]` is false;
+ - `{"importer","classic"}` under `[["import","classic"]]` is false;
+ - `{"Import","classic"}` is false;
+ - `{}` under anything is false;
+ - with two prefixes, matching the second is true.
+ - `TestCheckCommandPrefixes`: an empty list gives `ErrNoCommands`. An empty
+ word, a 129-byte word and 33 prefixes are each refused. 32 prefixes of
+ 128-byte words are accepted.
+ - `TestRunCredentialCarriesItsCommands`: mint a `Run` principal with
+ `[["import","classic"]]`, and `CLIPrincipal` returns those `Commands` and a
+ non-empty `CredentialID`.
+ - `TestRunCredentialNeedsCommands`: minting `Run` with nil commands gives
+ `ErrRunNeedsCommands`. Minting `Instance` with commands gives
+ `ErrCommandsNeedRun`. Minting `Run` with no app gives the account-scoped
+ refusal.
+ - `TestRotationKeepsCredentialIDAndCommands`, and
+ `TestRemintGetsANewCredentialID` (revoke, then mint the same name).
+ - `TestCredentialIDBackfillIsIdempotent`: reopening the store keeps existing
+ ids.
+ - A services CLI test: `--commands` without `--role run` fails, and
+ `--role run` without `--commands` fails.
+- [ ] **Step 2: Run them and watch them fail.**
+ `go test ./internal/runs/ ./internal/console/ -run 'Command|RunCredential|Rotation|Remint|CredentialID'`
+ fails to compile.
+- [ ] **Step 3: Implement.** Follow how rungs are minted, stored, parsed and
+ echoed, which is the pattern for every piece.
+- [ ] **Step 4: Watch them pass**, plus `go test ./internal/console/ -run Service`
+ and the services CLI tests.
+- [ ] **Step 5: Commit**: `console: a run credential carries its own id and the
+ commands it may run (PRO-6214)`
+
+---
+
+### Task 3: The run doors admit a `run` service, for its own runs only
+
+**Files:**
+- Modify: `internal/console/authcli.go:736-740`. The five run routes go from
+ `svcrole.Set{}` to `svcrole.Exactly(svcrole.Run)`.
+- Modify: `internal/console/apiruns.go`. On create:
+ - the principal check becomes "a person, or a `Run` service";
+ - the command check comes after the meta parse and before any write: 403,
+ code `command_not_granted`;
+ - `on_behalf_of` is validated;
+ - the request fields are set;
+ - the open-run refusal is redacted for a service.
+- Modify: `internal/console/apirunsfollow.go`. `runGate` admits a `Run`
+ service. `runCall` gains the principal. Every read and write of a run
+ checks `request.json`'s `ByCredential` against a service principal's
+ `CredentialID`, and a mismatch answers exactly what an unknown id does.
+- Modify: `internal/runs/runs.go`.
+ - `Request` gains `ByService string`, `ByCredential string` and
+ `OnBehalfOf string` (all `omitempty`).
+ - `Meta` gains `OnBehalfOf string` (`omitempty`).
+- Modify: `internal/console/authcli.go:1098`. Add
+ `func jsonErrorCode(w http.ResponseWriter, status int, code, msg string)`,
+ which writes `{"error": msg, "code": code}`. Every refusal on the five run
+ routes switches to it. Name the codes for what they refuse (for example
+ `not_found`, `host_claim`, `rate_limited`, `run_open`, `moving`,
+ `import_uncommitted`, `files_too_large`, `malformed`, `command_not_granted`
+ and `on_behalf_of_invalid`), and list them in one place:
+ `var runRefusalCodes = []string{...}`.
+- Modify: wherever a run's requester is shown. Find these by grepping for
+ `.By` on `runs.Request` and the run audit rows (CLI follow output, the
+ console run history and the audit log). A service's run reads `run by
+ service <name> for <email> (as stated by the service)`, or `run by service
+ <name> (scheduled)` when `OnBehalfOf` is empty.
+- Test: `internal/console/apiruns_service_test.go`
+
+**Interfaces:**
+- Consumes: `svcrole.Exactly`, `svcrole.Run` (Task 1); `Principal.CredentialID`,
+ `Principal.Commands` and `runs.CommandGranted` (Task 2).
+- Produces:
+ - `runs.Request{ByService, ByCredential, OnBehalfOf}`;
+ - `jsonErrorCode`;
+ - `runRefusalCodes`;
+ - `func (s *Server) runVisibleTo(p Principal, req runs.Request) bool`, which
+ is true for a person (membership was already checked) and, for a service,
+ true only when `req.ByCredential == p.CredentialID`.
+
+- [ ] **Step 1: Write the failing tests** (use `runFixture` and
+ `serviceToken`, and mint `Run` with `[["import","classic"]]`):
+ - `TestRunRoleMatrix`: on all five routes, a `Run` service passes the role
+ gate. `Publish`, `Deploy`, `Instance`, `Operate`, `Service` and `Admin`
+ get the "closed to service credentials" 403. Today's
+ `apiruns_test.go:189` expectation for `Instance` still holds.
+ - `TestRunServiceStartsAGrantedCommand`: argv `import classic --slug x`
+ gets a 201 or 200 (as today), and request.json has `ByService`,
+ `ByCredential` and `OnBehalfOf`.
+ - `TestRunServiceRefusedAnUngrantedCommand`: argv `db shell` gets 403
+ `command_not_granted`, and no run objects exist.
+ - `TestRunServiceWrongAppRefused`: a `Run` credential for app `other` on
+ `shop` is refused.
+ - `TestRunRevokedServiceRefused`.
+ - `TestRunServiceCannotSeeAPersonsRun`: the person starts a run, and the
+ service's GET, cancel, expire and ack on its id each give a body identical
+ to an unknown id's.
+ - `TestRunRemintedNameDoesNotInheritRuns`: the service starts a run, the
+ credential is revoked, the same name is minted again, and the new
+ credential's GET is the unknown-id answer.
+ - `TestRunOpenRefusalRedactedForServices`: a person's run is open, and the
+ service's create gets `run_open` with neither that run's id nor the
+ person's email anywhere in the body.
+ - `TestRunPersonMaySetNoOnBehalfOf`: a person's meta with `on_behalf_of`
+ gets 400 `on_behalf_of_invalid`.
+ - `TestRunOnBehalfOfShape`: not an email, or 255 bytes, gets 400.
+ - `TestRunRefusalsCarryCodes`: every refusal the run tests provoke has a
+ `code` that is in `runRefusalCodes`.
+ - `TestServiceRunRendersAsTheServices`: the follow and history rendering of
+ a service's run contains `run by service ci for paul@example.com (as
+ stated by the service)` and never presents the email as the actor.
+ - Repeat the existing incarnation, one-run and moving tests with a `Run`
+ service as the creator: delete and recreate, a move in progress, and a
+ person's run plus a service's run on one instance.
+- [ ] **Step 2: Run them and watch them fail.**
+ `go test ./internal/console/ -run 'TestRun'` fails.
+- [ ] **Step 3: Implement.**
+- [ ] **Step 4: Watch them pass**, and run all of
+ `go test ./internal/console/ -run 'Run|Service'`, with every pre-existing run
+ test unmodified.
+- [ ] **Step 5: Commit**: `console: a run service starts and follows its own
+ runs of the commands it was granted (PRO-6214)`
+
+---
+
+### Task 4: Idempotent create
+
+**Files:**
+- Create: `internal/runs/keys.go`
+- Modify: `internal/console/apiruns.go`
+- Modify: the run sweep (`internal/console/runsweep.go`), adding a key sweep
+- Test: `internal/runs/keys_test.go`, `internal/console/apiruns_key_test.go`
+
+**Interfaces:**
+- Produces in `internal/runs`:
+ - `func ValidKey(k string) bool`
+ - `func KeyObject(acct, app, scope, host, key string) string`. It returns
+ `RunsPrefix(acct, app) + "keys/" + hex(sha256(scope + "\x00" + host + "\x00" + key)) + ".json"`.
+ `scope` is `"svc:" + CredentialID` for a service and `"user:" + address`
+ for a person.
+ - `type KeyState string`, with `KeyPending`, `KeyAdmitted`, `KeyRefused`
+ and `KeyAbandoned`.
+ - `type Key struct { Schema int; State KeyState; RunID string; Digest string; Code string; Status int; CreatedAt, UpdatedAt time.Time }`
+ - `func MetaDigest(m Meta) string`: sha256 over a canonical encoding of argv,
+ timeout_s, on_behalf_of and every file's name, size and sha256.
+- `MetaFile` gains `SHA256 string` (`omitempty`). A keyed create requires it
+ on every file. The spooled bytes are verified against it, and a mismatch
+ gives 400 `file_hash_mismatch` before any write.
+- **Create's order changes, but only for a request with an `Idempotency-Key`.**
+ An unkeyed request keeps today's exact order, so Global Constraint 1 holds.
+ For a keyed create:
+ 1. principal, membership, host-claim and rate budget;
+ 2. read the record (a 404 stays a 404);
+ 3. parse the meta;
+ 4. check the key (below);
+ 5. today's class, move, open-run and import refusals, then the command check
+ (Task 3);
+ 6. spool and verify the files;
+ 7. mint the id;
+ 8. write the `pending` key create-only;
+ 9. write the objects;
+ 10. admit;
+ 11. CAS the key to `admitted` or `refused`, recording the code and status.
+- **The key check**, at step 4:
+ - **No key object:** continue.
+ - **Digest differs:** 409 `idempotency_key_reused`, and write nothing.
+ - **`admitted`:** 200 with the create response for `RunID` (its id and the
+ record's state if the record still holds it, else the terminal state from
+ result.json, else `done`). Stop reading the body.
+ - **`refused`:** repeat the stored `Status` and `Code`.
+ - **`pending`, younger than 5 minutes:** 409 `in_progress`, with
+ `Retry-After: 5`.
+ - **`pending`, 5 minutes or older:** re-read the record. If its `ActiveRun.ID`
+ equals `RunID`, CAS the key to `admitted` and answer as `admitted`.
+ Otherwise CAS it to `abandoned` and proceed as fresh.
+ - **`abandoned`:** proceed as fresh, overwriting the key by CAS on its etag
+ at step 8.
+- The create handler's context gets a 2-minute deadline that covers the whole
+ keyed path. The admission's own 30-second write context sits inside it.
+- `GET .../instances/{host}/runs?key=<k>` is a new route on the same
+ `Exactly(Run)` set plus people. It returns
+ `{"state": ..., "run_id": ...}` or a 404, scoped by the caller's `scope`.
+- **The key sweep** deletes key objects whose `CreatedAt` is older than 7
+ days. It runs in the same loop as the run sweep, and it logs a count and
+ never deletes anything younger.
+
+- [ ] **Step 1: Write the failing tests**
+ - `TestValidKey`: a table of good keys, plus an empty key, a 129-byte key and
+ one containing `/` or a space.
+ - `TestKeyObjectSeparatesCallersAndHosts`: the same key under two scopes, or
+ two hosts, gives two different objects.
+ - `TestMetaDigestCoversEveryField`: changing argv, timeout, on_behalf_of, a
+ file name, a size or a sha256 each changes the digest.
+ - `TestRunKeyedCreateThenRetryReturnsTheSameRun`: create with key K, retry
+ the identical request, get the same `id`, and the box saw one request
+ (one request.json).
+ - `TestRunKeyedRetryWhileOpenReturnsTheRun`: the run is still `queued` or
+ `picked`, and the retry gives a 200 with that id, not `run_open`.
+ - `TestRunKeyedRetryAfterAckStillReturnsTheRun`: ack, sweep the run's
+ objects, retry, and still get the same id with no new run.
+ - `TestRunKeyReusedWithADifferentRequest`: 409 `idempotency_key_reused`,
+ with nothing written.
+ - `TestRunConcurrentIdenticalKeyedCreatesMakeOneRun`: two goroutines create
+ with the same key. One run results, and the other answer is either 409
+ `in_progress` or the same id.
+ - `TestRunAbandonedPendingKeySettles`: a planted `pending` key, 6 minutes
+ old (the test clock), whose run the record holds, settles to `admitted`.
+ The same, with the record not holding it, settles to `abandoned` and a
+ fresh run is created.
+ - `TestRunRefusedKeyRepeatsItsRefusal`: a keyed create refused for
+ `command_not_granted` is refused the same way on retry.
+ - `TestRunKeyOfAnotherCallerIsInvisible`: the person's key K, used by the
+ service, behaves as a fresh key, and `?key=K` from the service is a 404.
+ - `TestRunKeyedHashMismatchWritesNothing`
+ - `TestRunKnownKeyAnsweredWithoutReadingFiles`: retry with a body whose file
+ part errors on read. The answer is still a 200 with the id.
+ - `TestRunKeyLookup`: `?key=` returns the state and run id.
+ - `TestRunKeySweepRemovesOnlyOldKeys`: an 8-day-old key is gone, and a 6-day
+ one stays.
+ - `TestRunUnkeyedOrderUnchanged`: an unkeyed create against an open run is
+ still refused before the body is read (today's order).
+- [ ] **Step 2: Run them and watch them fail.**
+ `go test ./internal/runs/ ./internal/console/ -run 'Key|Keyed|Digest'` fails.
+- [ ] **Step 3: Implement.**
+- [ ] **Step 4: Watch them pass**, plus every pre-existing test under
+ `go test ./internal/console/ -run Run`.
+- [ ] **Step 5: Commit**: `console: a keyed run create answers a retry from the
+ key's own state and never runs twice (PRO-6214)`
+
+---
+
+### Task 5: The CLI flag, the docs and the run-spec amendment
+
+**Files:**
+- Modify: `run.go`. `carlos run` gains `--idempotency-key <key>`, sends it as
+ the header, and sends each file's sha256 in the meta when a key is given.
+ It prints the run id before streaming, as it does today.
+- Modify: `docs/site/cli.md`, covering `carlos run --idempotency-key` and
+ `carlos services create --role run --commands`.
+- Modify: the services and run docs pages under `docs/site/`, wherever the
+ role catalogue and the run API are described. Add the `run` role, the
+ command-list rule, `on_behalf_of`, the refusal codes (from
+ `runRefusalCodes`) and the key lifetime.
+- Modify: `docs/superpowers/specs/2026-10-08-carlos-run-design.md` § 2 ruling 1.
+ Add one line: "Amended 2026-10-24: a `run` service credential may, for the
+ commands it was granted. See `2026-10-24-service-runs-design.md`."
+- Test: `run_test.go` (extended), plus the docs test that checks
+ `docs/site/cli.md` names every command or flag, if one exists. Find it with
+ `grep -rn "cli.md" *_test.go`.
+
+**Interfaces:**
+- Consumes: `runs.ValidKey` (Task 4).
+
+- [ ] **Step 1: Write the failing tests**
+ - `TestRunIdempotencyKeyFlag`: the request carries `Idempotency-Key`, and the
+ meta's files carry `sha256`.
+ - An invalid key is refused by the CLI before any network call.
+ - `TestRunRefusalCodesAreDocumented`: every entry in `runRefusalCodes`
+ appears in the run API doc page.
+- [ ] **Step 2: Run them and watch them fail.**
+- [ ] **Step 3: Implement.**
+- [ ] **Step 4: Watch them pass**, and run `make test` once, before the branch
+ is described.
+- [ ] **Step 5: Commit**: `cli, docs: carlos run takes an idempotency key, and
+ the run role is documented (PRO-6214)`
+
+---
+
+## Landing
+
+One branch, `pro-6214-service-runs`, landed through amadan as AGENTS.md
+describes. Describe it with `amadan branch describe carlos/platform <branch>`,
+then squash to one commit, wait for CI to go green on the tip, and merge with
+`-expect <tip>`. Before landing, an Opus or Fable review covers the whole
+branch, because it grants authority (AGENTS.md delegation policy). Astra
+reviews too, since the session is Opus.
+
+After it lands and the console is rolled, Tito mints `fleet-runs` with
+`--role run --app titogo --commands "<the import and sync commands>"`. The
+exact commands are settled with the titogo side of PRO-6214, outside this
+plan.