Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
21 KiB
State Hub retirement — cutover slice plan
Workplan task: STATE-WP-0079-T02
Inventory: SHR-INV-0001 (prj-state-hub-retirement/inventory/), 425 items, v0.1.1
Created: 2026-08-19
Groups every dispositioned inventory item into an ordered cutover slice. Each
slice names its adapter flag, the owner-side API that receives the traffic, and
its rollback. Slices are the execution unit for STATE-WP-0079-T04.
Method
Slices are cut on capability, not on route or table, because a capability is
the smallest unit with a single owner and a coherent contract. The inventory's
capability field yields 51 groups; those are bundled into waves by owner, and
ordered within a wave by dependency and blast radius.
Two rules constrain the ordering:
- A slice cannot move before its owner can receive it. Owner readiness is the owner repo's own workplan status, not an assertion here.
- Read-only projections move before write paths in the same capability, so a failed cutover degrades to stale reads rather than lost writes.
Owner readiness (verified 2026-08-19)
| Owner | Items | Gating workplan | Status | Can receive? |
|---|---|---|---|---|
repo-manager |
167 | RMGR-WP-0001/0002/0003 | finished |
Partial — see correction below |
hub-core |
128 | HUB-WP-0004 runtime + extension contract | proposed |
No |
state-hub-until-cutover |
55 | — (keep/retire in place) | — | n/a |
activity-core |
18 | ACTIVITY-WP-0029 hub port alignment | active |
Partial |
ops-hub |
13 | OPS-WP-0003 extension contract alignment | proposed |
No |
fin-hub |
10 | FIN-WP-0003 fabric authority boundary | proposed |
No |
archive |
30 | legacy-meter evidence | see Wave E | Conditional |
ops-bridge |
2 | — (keep) |
— | n/a |
the-custodian |
2 | — | — | Yes |
Only the repo-manager lane is unblocked today, and only partly — see the
correction immediately below. Everything else is waiting on an owner-side
contract, not on State Hub.
Correction (2026-08-20): repo-manager readiness is per-capability
The table above reads owner readiness off workplan status, which is too coarse.
RMGR-WP-0001/0002/0003 are finished, but what they delivered is work-record
dual-run, repository observation, and scaffolding — not a general register
surface. Verified against repo-manager/src/repo_manager/ on 2026-08-20:
rmgr surface |
Covers | Slice |
|---|---|---|
observe, load_classification |
repository representation, classification | A1 (partial) |
reconcile, index_store, parse/workplan.py, task-status, dual_run |
work records | A2 |
conform, standards.py, scaffold |
standards, onboarding | A1 repo-onboarding (already cut over) |
prefix |
workplan prefix registry | A1 (partial) |
parse/ contains only workplan.py. A grep for sbom, repo_goal,
contribution, technical_debt, and extension_point across the whole source
tree returns nothing.
Consequence: A3, A4, and A5 have no owner-side surface to receive them.
| Slice | Items | Receivable? |
|---|---|---|
| A1 repository registry | 27 | Partial — observe/conform cover representation; registry CRUD does not exist |
| A2 work records | 59 | Yes — this is exactly what RMGR-WP-0002/0003 proved |
| A3 registers | 49 | No — no register surface of any kind in repo-manager |
| A4 work + repo UI | 22 | No — rmgr is a CLI; there is no owner-side dashboard |
| A5 topic spine | 8 | No — no topic/classification contract beyond observe |
The near-term executable surface is therefore A1 + A2, not 165 items, and A2 is the only slice with a proven adapter.
A3 is not the low-blast-radius first cut this plan originally recommended — it is the least ready slice in Wave A. The recommendation is withdrawn.
Do not build the register surface in State Hub. New register capability here
would be new permanent ownership in the component being retired, which
policies/retirement-freeze.md makes inadmissible. A3/A4/A5 need a
repo-manager workplan; RMGR-WP-0004 (standards conformance) and RMGR-WP-0005
(registrar consolidation) do not cover them.
Adapter mechanism
Wave A reuses the dual-run strangler proven in RMGR-WP-0002/0003 rather than introducing a new mechanism:
- Adapter:
api/services/repo_manager_dual_run.py - Existing flags:
RM_WRITEBACK,RM_WRITEBACK_PUSH,RM_RECONCILE,RM_PILOT_REPOS,RM_METER_PATH,RM_DUAL_RUN_CONFIG - Reference:
repo-manager/docs/dual-run.md, evidencerepo-manager/docs/evidence/wp0002-completion.md
New slices add a slice-scoped flag of the form RM_SLICE_<NAME> with three
states — off (hub only), dual (both, hub authoritative, divergence metered),
owner (owner authoritative, hub proxies). Rollback for every Wave A slice is
therefore the same operation: set the slice flag back one state. No slice
advances to owner until it has run a full consistency-sweep cycle at dual
with zero divergence.
Waves B–D must not invent hub-side adapters ahead of their owner contract. Per the T01 freeze rule, building an adapter for an owner that cannot yet receive establishes new permanent ownership here — the exact thing retirement is removing.
Wave A — repo-manager (167 items, executable now)
A1 · Repository registry — 27 items
repository-registry (24: 16 route, 7 mcp_tool, 1 dataset) · repo-reconciliation (1)
· repo-doi (1) · repo-onboarding (1)
| Flag | RM_SLICE_REGISTRY |
| Owner API | rmgr repo registry + classification spine |
| Rollback | flag → dual, then off; hub tables retain rows throughout |
First because every other repo-manager slice keys off repo identity, and because
repo-onboarding is already delegated (STATE-WP-0080-T02, rmgr scaffold),
so the slice starts partly cut over.
A2 · Work records — 59 items
work-records (20) · work-records-workplan (12) · work-records-intake (9) ·
work-records-decision (8, move+replace) · work-records-deps (2) ·
work-records-task (8, repo-manager portion)
| Flag | RM_SLICE_WORKRECORDS |
| Owner API | rmgr file-backed work-record index + PATCH /tasks writeback |
| Rollback | flag → dual; C-15 writeback already reconciles file↔DB drift |
Highest blast radius in the program — this is the surface every agent session touches. Split the flag by record kind if divergence appears; do not cut the whole slice at once.
work-records-task is owner-split: the route/dataset portion is repo-manager,
the job portion is activity-core (Wave C1). Cut the repo-manager portion first
and leave the job dual-running.
A2 readiness detail (2026-08-20)
Same check as for A3, applied one level down. The State Hub adapter
(api/services/repo_manager_dual_run.py) exposes exactly three operations:
| Adapter function | Routes to | Covers |
|---|---|---|
rm_update_task_status |
rmgr task-status |
task status writes |
rm_reconcile |
rmgr reconcile |
work-record index rebuild |
rm_scaffold |
rmgr scaffold |
repo scaffolding (A1 repo-onboarding, already cut over) |
There is no adapter for workplan create/update/delete, intake, decision,
dependency, or human-flag records — and no parser for them on the repo-manager
side either (parse/ is workplan.py only). So A2 splits:
| Sub-slice | Items | Receivable |
|---|---|---|
| A2a task status + reconcile | ~6 (update_task_status, bulk_update_task_statuses, PATCH /tasks/{id}, POST /tasks/bulk-status-sync, index refresh job, table:tasks) |
Yes — adapter exists and was proven by RMGR-WP-0002/0003 |
| A2b workplan CRUD | ~9 routes + table:workplans |
No adapter; repo-manager parses workplans but exposes no write path |
| A2c intake | 9 | No parser, no adapter |
| A2d decision | 8 | No parser, no adapter |
| A2e dependencies + human-flag | ~6 | No parser, no adapter |
A2a is the only slice in the entire plan that is executable today, and it is roughly 6 items, not 59.
Dual-run is currently off
rmgr dual-run-status on 2026-08-20:
config_exists: false writeback: false reconcile: false pilot_repos: null
meter: {state-hub: 15, repo-manager: 8, total: 23}
The 23 metered mutations are the RMGR-WP-0002/0003 pilot runs. Nothing is
dual-running now — the adapter is proven but switched off, and there is no
~/.repo-manager/dual-run.yaml.
So the concrete next executable step is operational, not code: create the
dual-run config and enable RM_WRITEBACK + RM_RECONCILE, then watch the
mutation meter for divergence. That is A2a moving from off to dual.
The pilot repo is already established — repo-manager, per RMGR-WP-0003 and
the shipped repo-manager/config/dual-run.pilot.example.yaml. No new decision
is needed about which repo; the procedure is documented in
repo-manager/docs/dual-run.md:
mkdir -p ~/.repo-manager
cp ~/repo-manager/config/dual-run.pilot.example.yaml ~/.repo-manager/dual-run.yaml
# then restart the State Hub API — settings are cached per process
Rollback is rm ~/.repo-manager/dual-run.yaml plus a restart, or setting
writeback/reconcile false.
Requires an operator, for one reason only: it changes how live task writes
flow and needs a State Hub API restart to take effect. Piloting on
repo-manager keeps the blast radius to a single repo.
A3 · Registers — 49 items
sbom-inventory (10) · repo-goals (9) · upstream-contributions (9) ·
register-technical-debt (9) · register-extension-points (6) · register-entries (6)
| Flag | RM_SLICE_REGISTERS |
| Owner API | rmgr register ...; `rmgr sbom scan |
| Rollback | flag → off; retain current State Hub projection |
Owner ready (2026-08-21). Finished RMGR-WP-0008 supplies the shared six-kind register spine, governed compatibility adapter, lockfile-derived SBOM and licence/copyleft report. Historical SBOM rows are cache provenance; rebuild from current repository sources rather than copying them into authority.
A4 · Work + repo UI — 22 items
work-coordination-ui (14, replace) · repo-ui (8)
| Flag | RM_SLICE_UI |
| Owner API | hub-core projection/query plus hub/ops UI clients |
| Rollback | remove redirect; hub pages are unchanged behind it |
replace, not move: the contract changes. Must follow A1–A3, since the pages
render what those slices serve. RMGR-ADR-003 corrects the earlier inventory
assignment: Repo Manager owns the repository contracts, not a dashboard. A4's
UI implementation therefore rides with B5 even though these 22 inventory items
remain grouped here for traceability.
A5 · Topic spine — 8 items
topic-spine (8, replace)
| Flag | RM_SLICE_TOPICSPINE |
| Owner API | rmgr topic/classification contract |
| Rollback | flag → off |
The versioned Repo Manager classification contract is published and accepted by finished HUB-WP-0004. Consumer route switching remains a State Hub cutover action.
Wave B — hub-core (128 items) · HUB-WP-0004 owner gate satisfied
HUB-WP-0004 finished 2026-08-21 and accepted the project architecture, runtime, ports, conformance scaffold, and absorption plan. Individual production cutovers still require their slice evidence and operator gates.
| Slice | Capabilities | Items |
|---|---|---|
| B1 catalogs | capability-catalog (18), cross-domain-registry (16) |
34 |
| B2 messaging + interface log | agent-messaging (11), interface-change-log (11) |
22 |
| B3 telemetry | token-telemetry (14), third-party-software-catalog (10), progress-telemetry (8) |
32 |
| B4 projections + policy | state-projections (9), domain-goals (6), policy-integration (2), write-idempotency (1), event-bus (1), recently-on-scope (1) |
20 |
| B5 hub UI | hub-projection-ui (14, replace), agent-integration (2), dashboard (1) |
17 |
Order B1 → B4 → B2 → B3 → B5: catalogs and projections are read-mostly and carry the contract; messaging and telemetry are write paths; UI last.
Wave C — activity-core (18 items) · ACTIVITY-WP-0029 active
| Slice | Capabilities | Items |
|---|---|---|
| C1 execution | workplan-execution-queue (7, replace), task-flow-engine (6, replace) |
13 |
| C2 jobs | ops-run-projection (1), repo-consistency (3), work-records-task activity-core portion (1) |
5 |
Both are replace — schedulers move to activity-core under a different
contract. C2 must follow A2, since the consistency job writes work records.
Wave D — remaining owners (25 items) · all gated
| Slice | Owner | Capabilities | Items | Gate |
|---|---|---|---|---|
| D1 | ops-hub |
service-catalog (9), service-catalog-ui (2) |
11 | OPS-WP-0003 proposed |
| D2 | fin-hub |
fabric-graph (10) |
10 | FIN-WP-0003 proposed |
| D3 | the-custodian |
kaizen-agents (2) |
2 | none — can go anytime |
D3 is two MCP tools with no dependants; take it opportunistically.
Wave E — retire (42 items)
| Slice | Capability | Items | Gate |
|---|---|---|---|
| E1 | suggestion-backlog → archive |
17 | history archived 2026-08-20; read surface + dashboard remain |
| E2 | work-records-workplan-legacy → archive |
13 | legacy-meter — candidate rule, see below |
| E3 | dashboard-meta (/ui-feedback) |
1 | T06 freeze window — owner is state-hub-until-cutover |
| E4 | legacy-meter itself |
9 | last — it is the instrument for E2 |
E4 retires only after every other legacy surface is closed; retiring the meter first destroys the evidence for the retirements it gates.
E1 status (2026-08-20)
Already half-retired before this plan: mutations have returned HTTP 410 since
2026-07-21 (CUST-WP-0061-T06), pointing at POST /intakes/. Reads were kept
live so the historical record stayed reachable.
History is now archived at the-custodian/docs/archived-suggestion-backlog.md
— all 10 suggestions, 10 notes, and 5 relevance bumps, with each entry's
migration target. Every entry was declined in the intake migration and none
was promoted, so nothing forward-looking is lost by removing the surface.
Executed 2026-08-20. The 8 read routes now 410 with a pointer to intake and
to the archive, the 6 MCP tools are removed (not stubbed — a retired tool that
still appears in the tool list costs every agent session context on every call),
the 5 /suggestions write-idempotency rules are gone, and the dashboard page is
deleted with its nav entry, reference.md row, and wsjf-triage.md link
updated. dashboard/src/docs/suggestions.md is rewritten as the archive
pointer. Tables stay for the T06 dump.
One naming trap worth recording: ui-feedback.md and todo.md "suggestions"
are a different entity — Shift+click dashboard feedback backed by
technical_debt, not the suggestion backlog. They are untouched. Anything
grepping for suggestion during cutover will hit them.
E3 status (2026-08-20)
dashboard-meta is the /ui-feedback page — the feedback scratch surface just
mentioned. Its owner is state-hub-until-cutover and it is still in active use,
so it retires at the T06 freeze window, not before. Removing it now would
delete a working feedback loop during the retirement it serves. No action.
E2 evidence — defect found and partly fixed (2026-08-20)
The window discrepancy flagged on 2026-08-19 was a real defect in
scripts/capture_legacy_meter_evidence.py, not a labelling nit.
_review_query fell back to hours=8 whenever --days was omitted, and
--hours defaulted to 8. Every unattended capture therefore sampled an
8-hour band (06:00Z–14:00Z) while writing a file named weekly-review with
cadence: weekly in the payload. 39 of 40 captures ran this way — only the
first, 2026-07-08, used a true 7-day window.
Calls in the other 16 hours of each day were never sampled, so quiet-in-band interfaces were reported as retirement candidates while still having live callers. On the 2026-08-19 capture three candidates had recent traffic:
| Interface | All-time calls | Last seen |
|---|---|---|
GET /tasks/?workstream_id |
594 | 2026-08-18 |
GET /progress/?workstream_id |
2 | 2026-08-12 |
GET /workstreams/ |
144 086 | 2026-08-10 |
Fixed: the script now defaults to days=7, matching the endpoint name and
the cadence label. --hours is retained for spot checks and documented as not
retirement evidence. Corrected capture:
docs/evidence/legacy-meter-weekly-review-20260820.json — 18 candidates of 20
interfaces, GET /tasks/?workstream_id correctly excluded.
Candidate rule tightened (2026-08-20). _retirement_state now requires a
quiet period scaled to all-time call volume, because a clean window only proves
silence for that window — for a heavily used interface an infrequent caller can
fall outside it entirely. RETIREMENT_QUIET_LADDER in
api/services/legacy_meter.py:
| All-time calls | Days since last_seen_at required |
|---|---|
| 0 | none — never used |
| 1 – 99 | 7 |
| 100 – 9 999 | 30 |
| 10 000+ | 60 |
Applied to the 2026-08-20 capture this splits the 19 legacy interfaces cleanly:
15 retirable — all four workstream MCP tools, the state:// resource, and
the low-volume REST aliases, plus PATCH /workstreams/{id} (571 calls, quiet
42d), POST /workstreams/ (824, quiet 42d) and
POST /workstreams/{id}/dependencies/ (4 971, quiet 49d).
4 held:
| Interface | All-time calls | Reason |
|---|---|---|
GET /tasks/?workstream_id |
595 | traffic inside the window |
GET /workstreams/ |
144 086 | quiet 9d of 60d |
GET /workstreams/{id} |
511 406 | quiet 19d of 60d |
GET /workstreams/{id}/dependencies/ |
255 865 | quiet 42d of 60d |
Executed 2026-08-20: the 15 are retired; the meter reads 16 retired / 4
legacy with 0 outstanding candidates. Journalled at
the-custodian/docs/retired-legacy-interfaces.md. The four
held are the three six-figure read paths and the one with live traffic — exactly
the set where a false retirement would hurt most. They clear on their own as the
quiet counter runs, provided no new traffic arrives; the earliest,
GET /workstreams/{id}/dependencies/, needs 18 more silent days.
Re-check before executing E2: the ladder is evaluated at review time, so a capture older than the retirement decision is not evidence for it.
Keep — 43 items
schema-history (43 migrations, keep until cutover) plus ops-automation,
connectivity (ops-bridge). Not slices: they persist until the T06 freeze
window and are dispositioned by the final dump and archive.
Coverage
Computed against state-hub-disposition.yaml, not hand-counted. Every item is
assigned by capability; work-records-task splits on owner.
| Wave | Slices | Items |
|---|---|---|
| A repo-manager | A1 27 · A2 59 · A3 49 · A4 22 · A5 8 | 165 |
| B hub-core | B1 34 · B2 22 · B3 32 · B4 20 · B5 17 | 125 |
| C activity-core | C1 13 · C2 5 | 18 |
| D ops-hub / fin-hub / the-custodian | D1 11 · D2 10 · D3 2 | 23 |
| E retire | E1 17 · E2 13 · E3 1 · E4 9 | 40 |
| Keep | schema-history 43, ops-automation 1, connectivity 1 |
45 |
| Ops dependencies | see below | 9 |
| Total | 425 |
Wave A totals 165 rather than the owner table's 167, and Wave B 125 rather than
128: the difference is ops_dependency items, which carry no capability and
so attach to no capability slice.
Ops dependencies (9 items, no capability)
| Owner | Disposition | Items | Handling |
|---|---|---|---|
hub-core |
move / replace / keep | 3 | ride with Wave B; confirm at B4 |
ops-hub |
replace | 2 | ride with D1 |
state-hub-until-cutover |
retire | 2 | T06 freeze window |
activity-core |
keep | 1 | keep to cutover |
ops-bridge |
keep | 1 | keep — external dependency, survives retirement |
These are infrastructure couplings (DB, scheduler, tunnel), not API surface. They cannot be strangled behind a flag and are dispositioned at the freeze window instead. Worth an explicit owner confirmation during T04 so none is discovered live at cutover.
Next actions
Resolve the legacy-meter window discrepancy— done 2026-08-20; E1 and the evidenced part of E2 are executed.Build the Repo Manager receiving surface for A3/A5 and settle A4 ownership— finished in RMGR-WP-0008. A4 belongs to hub-core projection UI, not Repo Manager.- Cut A2a (task status + reconcile, ~6 items) — the only slice in the plan
executable today. First step is operational: create
~/.repo-manager/dual-run.yamland enable writeback/reconcile for one pilot repo, then watch the mutation meter. A2b–A2e need repo-manager parsers and adapters that do not exist. - HUB-WP-0004's owner-readiness gate is satisfied; advance Wave B only through its per-slice dual-run and operator gates.
- Do not build register, UI, or topic capability in State Hub to unblock
items 2–3 —
policies/retirement-freeze.mdmakes that inadmissible.