# 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: 1. **A slice cannot move before its owner can receive it.** Owner readiness is the owner repo's own workplan status, not an assertion here. 2. **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`, evidence `repo-manager/docs/evidence/wp0002-completion.md` New slices add a **slice-scoped flag** of the form `RM_SLICE_` 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`: ```bash 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 | **does not exist** — see the readiness correction above | | Rollback | n/a until there is something to roll back from | **Blocked.** repo-manager has no register surface: no SBOM ingest, no repo goals, no contributions, no technical-debt or extension-point registry. This slice cannot start until a repo-manager workplan builds one. Low coupling and low write volume still make this the right slice to prove the `RM_SLICE_*` pattern **once the owner side exists** — but that is a repo-manager-side prerequisite, not a State Hub task. ### A4 · Work + repo UI — 22 items `work-coordination-ui` (14, `replace`) · `repo-ui` (8) | | | | --- | --- | | Flag | `RM_SLICE_UI` | | Owner API | owner-side dashboard; hub pages redirect | | 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. ### A5 · Topic spine — 8 items `topic-spine` (8, `replace`) | | | | --- | --- | | Flag | `RM_SLICE_TOPICSPINE` | | Owner API | `rmgr` topic/classification contract | | Rollback | flag → `off` | Last in Wave A: it is a `replace` under a changed contract, and A1–A4 read it. --- ## Wave B — hub-core (128 items) · gated on HUB-WP-0004 HUB-WP-0004 is `proposed`. **No hub-side adapter work until it is at least `ready`.** Slices are listed to fix scope and order, not to start. | 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 1. ~~Resolve the legacy-meter window discrepancy~~ — done 2026-08-20; E1 and the evidenced part of E2 are executed. 2. **Raise a `repo-manager` workplan for the register surface** (A3), the UI surface (A4), and the topic-spine contract (A5). Until that exists, 79 of Wave A's 165 items have nowhere to go. This is now the critical path for the whole retirement, ahead of anything State Hub can do to itself. 3. Cut **A2a** (task status + reconcile, ~6 items) — the only slice in the plan executable today. First step is operational: create `~/.repo-manager/dual-run.yaml` and enable writeback/reconcile for one pilot repo, then watch the mutation meter. A2b–A2e need repo-manager parsers and adapters that do not exist. 4. Do not open Wave B until HUB-WP-0004 reaches `ready`; 125 items sit behind it. 5. Do not build register, UI, or topic capability in State Hub to unblock items 2–3 — `policies/retirement-freeze.md` makes that inadmissible.