The T02 slice plan marked all 167 repo-manager items receivable because RMGR-WP-0001/0002/0003 are finished. That reads readiness off workplan status, which is too coarse. Verified against repo-manager/src/repo_manager/: those workplans delivered work-record dual-run, repository observation, and scaffolding. parse/ holds only workplan.py, and the source tree contains no reference to sbom, repo_goal, contribution, technical_debt, or extension_point. Only A1 (partial) and A2 are receivable -- 86 items, not 165. A3, A4 and A5 have no owner-side surface. A3 was recommended twice as the low-blast-radius first cut; it is in fact the least ready slice in Wave A. Recommendation withdrawn. Critical path moves to repo-manager: a workplan there must build the register, UI, and topic surfaces. Building them here is inadmissible under policies/retirement-freeze.md. First executable slice is A2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
18 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.
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
Resolve the legacy-meter window discrepancy— done 2026-08-20; E1 and the evidenced part of E2 are executed.- Raise a
repo-managerworkplan 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. - Cut A2 (work records, 59 items) as the first real slice — it is the only
Wave A slice with a proven adapter. Split
RM_SLICE_WORKRECORDSby record kind to manage the blast radius rather than cutting it whole. - Do not open Wave B until HUB-WP-0004 reaches
ready; 125 items sit behind it. - Do not build register, UI, or topic capability in State Hub to unblock
items 2–3 —
policies/retirement-freeze.mdmakes that inadmissible.