The operator migration — kickoff

Free the word "operator" from its person-meaning in shipped code so it can mean the company whose rule book governs a building, and build the two concepts that do not exist yet.

Status is DERIVED from each row’s own PR, decision and prod-evidence links by bin/refresh-tracker operator-migration-kickoff — never typed. Last refreshed 2026-09-11 00:37Z.

How this gets driven

Written down because the session that starts this will not be the session that finishes it. Everything an agent needs to pick this up cold is on this page — not in a conversation that has been compacted away.

The one hard invariant

Stop conditions — there is exactly ONE

Condition 1 above. Nothing else stops the loop. This list was four items on 2026-09-10 and Gera cut it to one the same evening: “I think we’re too sensitive when we ask humans for everything … we have Fable 5, we could always ask, or Astra, which are smart models that could unblock us. They should already have my intuition.” Three former stop conditions are now handled without stopping:

A decision that looks like a human’s — escalate to a MODEL, do not park the phase

The founder’s intuition is already written down, and a model can read it: notes/decided-2026-09-08.md, -09-09.md, -09-10.md, the eleven rulings annotated across the record, and this page. The ladder is:

  1. Astra decides from the recorded rulings, and writes down which ruling it reasoned from.
  2. If Astra genuinely cannot, Fable 5.1 — it is nearly exhausted, so this is the expensive rung, not the first one.
  3. Only if both decline does it reach Gera, and even then the loop carries on with every phase that does not depend on it. Parking the whole migration on one open question is the failure this replaced.

This explicitly covers the two that used to be called unguessable — the word for the fleet’s own operator, and Clara’s public brand title. Naming is something a model does well. Let it name them, record the reasoning, move.

Platform defaults — a SHIP gate on one row, not a stop for the loop

Build the defaults layer freely. Gera, 2026-09-10: “we’re not sending anything out right now, so there’s no need to worry about the defaults.” That is correct while the layer does not exist and nothing is wired to it.

The narrow thing that survives, and it binds at one moment only: before the defaults layer is connected to live outbound — anything Clara can say to a real resident — the business-versus-product question must have an answer. Product behaviour may default. A fee, a deposit or a lease rule may not, because a defaulted business term is unattributed and indistinguishable on screen from the operator’s own answer. This is a checklist item on the build the defaults layer row, not a reason to pause the migration.

Eval baselines — measure and record, do not block

Gera, 2026-09-10: “evals can move … admin merge is okay because we’re making a lot of changes, and we could always kind of fix that later. For now, this is more important.” Admin merge is authorised for this migration.

The one thing to keep doing, because it costs nothing and is what makes “fix it later” possible: record the before and after numbers in the row when a baseline moves. A baseline that moved and was written down is a half-hour fix later. A baseline that moved and was not recorded is indistinguishable from a model regression, and the only way back is a bisect over the whole migration.

In flight as of 2026-09-10 13:20Z — check these FIRST

Re-derive rather than trust this list; it is a snapshot, and the whole point of the page is that statuses go stale. Three consecutive re-derivations have now found the previous snapshot wrong in every open row. The third one found worse than staleness: the tracker below this table was rendering zero rows, and its statuses had never been derived at all — see the-tracker-rendered-zero-rows. Fetching a page is not checking a row. The contract lists them as separate steps for a reason, and I had been treating the first as evidence for the second.

ThingState when writtenWhat to do
Astra jobsNone running (pgrep -f "codex exec" → empty). Last logs /tmp/astra/{D,deck,how}.log, all finished 2026-09-10 01:06–03:07Z.If any are running, wait and verify the ARTIFACT before believing the summary — an exit code is not evidence. Always launch with </dev/null.
Phase 00 — PRs #7538, #7539, #7545ALL MERGED. The previous snapshot had #7539 and #7545 open with active review rounds; both landed. All four Decide before touching code rows are shipped: fleet engineer / fleet agent, Clara’s title AI Teammate, ADMIN_HANDOFF, and the per-key defaults split.Nothing owed. Before running any TRANSFER replacement, still read the handover-sweep-boundary row — the boundary survives the decision.
Phase 0 guards — #7548, #7550, #7587ALL MERGED, and #7587 is the first row on this page verified at the outcome layer rather than at the merge. propflowai.co/security served the new copy at 12:54Z: Per-sender and per-IP rate limits, No reads outside the properties in your role, Work orders need staff approval, and zero occurrences of any old spelling.Round 1 caught a line where the sweep reworded a security CLAIM, not a noun, and it had a sibling the reviewer had demoted to a question. Both settled from the implementation: twilio/webhook/route.ts:108 keys the limiter on from, the sender’s phone. The verification has a boundary and the row states it — the deep-dive strings render client-side inside LockedExpandable, so a fetch cannot see them, and their OLD spellings are equally absent. That is consistent with “not rendered” and is not evidence of “changed”.
Rename slices — #7573, #7575, #7578, #7579, #7580ALL MERGED 2026-09-10. #7579 is the load-bearing one: the write path could not express triggerActor: 'staff' that the read path already accepted, so the writer half of the cutover was structurally impossible.Two rows sit merged · untested on purpose — the prod evidence is the alert firing on a staff-attributed send, and nothing writes staff until the first writer flips. Re-check then.
PR #7577 — the ratchetOPEN at e67a70e4, awaiting round 8. Round 7 found that round 6’s fix caused round 7’s bug — fourth consecutive round on one mechanism — and named the redesign. Ceiling re-taken 4957 → 4917 after the two merges.Do NOT dismiss this review; every round has found a real defect. The reviewer’s hypothetical became real on the next rebase: the ceiling was 40 stale, which is precisely the case the old self-test went red on. Simulated ceilings now derive from the count the guard itself prints, and per-directory rises are produced by construction rather than by arithmetic on recorded numbers. Proved by the failing case, not by reading: on a hand-staled ceiling the new file passes and the file at d6f4484d goes red on exactly one assertion. Merge LAST.
PR #7581 — value supersessionsMERGED 12:50:58Z, after a rebase onto current main. Its CI had been red twice on “The operation was canceled” two minutes into tsgo — not a type error: npx tsgo --noEmit is clean in 8s locally, and the rebased head passed first time.Three of the four owed values are survivors, not renames. The two alert labels leave the repo, so renaming them detaches Sentry rules nothing here can prove are absent. A red check that says failure is not the same claim as a failing test — read the log before believing the colour, and prefer a rebase to a third rerun.
PR #7590 — the prod-evidence standardOPEN. Astra’s decision note, routed under the “a decision that looks like mine goes to Astra first” rule because it changes this page’s headline by 12x.Already implemented in 879cdeb5. 22 of 24 shipped rows were proved by a GitHub link; prod_evidence() accepts any URL while merged_status() says a merged PR is not evidence. The page now reads 2 shipped, 41 merged · untested. Every one of the 22 keeps its old link inside its own context.
propflow-docs mainCurrent. The tracker on this page rendered ZERO rows until 13:00Z — the data block was a bare [ ... ] where {"items": [ ... ]} belongs, and the template reader is (data.items || []). Every PR ref was a bare int, which derive_item’s if "#" in str(p) filter dropped, so no status here had ever been derived.Both failures were silent AND reported success. They are loud now, and pinned in tests/refresh-tracker-controls.py as real regression cases: run against the pre-fix script they FAIL, both with an empty stderr, which is the silence they exist to catch.
The shared checkout~/code/PropFlow/propflow-docs is shared, and a launchd agent refreshes trackers every 15 minutes and commits when a status really moves.Expect push races. Rebase and retry; the conflict is almost always one derived timestamp line — keep the newer value. Never bulk-stage; stage explicit paths.

Who runs what

LaneRunnerShape
Design authoring — the record, the deck, the HOW page, proseAstra (codex exec -m gpt-6-astra --approve-for-me)Sequential. Astra cannot spawn subagents and has no dynamic workflows. Two concurrent sessions on different files is the ceiling; poll until both exit, then verify. Always </dev/null — without it a run blocks on stdin for half an hour and exits 0 having done nothing.
Code, tests, parallel sweeps, per-line passesOpus 5 + subagents + dynamic workflowsParallel wherever the work is independent. The 100 undecidable agentflow lines are a per-line pass no pattern can do — that is a fan-out, not a regex.
Independent critiqueFable 5.1Nearly exhausted. Weekly allowance. Spend it on adversarial review of a phase boundary, never on authoring.

The loop, per phase

  1. Re-derive, do not trust. Counts, statuses and “already done” claims go stale. policy_authority_matrix was reported stale-open in 24 places and was already fixed in all 25; the report was the stale thing. Measure first.
  2. Split. Independent work fans out to subagents. Dependent work goes sequential. Astra work is always sequential.
  3. Ship the whole thing. Invoke /boil. Defer-language — “follow-up PR”, “for now”, “out of scope” — is a tell that the agent is pattern-matching a tired human. The adjacent rough edge is in scope.
  4. PR with --label run-tests, drive to green, merge.
  5. Verify on prod, at the outcome layer. An exit code is not evidence. Read the diff, fetch the page, check the row. Two silent failures in one night were both exit-code-zero.
  6. Append what you learned to this page as a row, so the next session inherits it.

Phase order, and the one that is not optional

Phase 00 is this page’s own four blocking rows. They gate everything: two need a human, and the vocabulary the whole migration renames toward is what they settle. Phase 0 is the hard cutover — one maintenance window, chosen deliberately over a long transition, because a platform where half the developers use the old meaning of operator and half the new one is worse than an hour of downtime. Phases 1–9 follow the plan in the record.

The framing that keeps being lost: this is not “add acting operator”. Today operator means a human, as opposed to Clara in shipped code — 4,386 occurrences across 1,811 files. Until that meaning moves to staff, the word is not available for the company. That rename is the migration. acting operator has zero code footprint and is the small build at the end.

Overall
 
Shipped
Merged · untested
In flight
Open
Waiting on you
Groups — click to filter
Status

Every row opens: when it was added, why it exists, and the links that prove it. Rows waiting on a human carry a Decide → link straight to the answerable question on the decisions page. Statuses are re-derived from live PR, decision and prod-evidence state by bin/refresh-trackers; nothing here is typed by hand, and merged is not shipped until a prod_evidence permalink says the fix was exercised in production.

PropFlow Docs