--- id: STATE-WP-0079 type: workplan title: "State Hub retirement strangler and disposition execution" domain: infotech repo: state-hub status: active owner: codex topic_slug: infotech created: "2026-08-09" updated: "2026-08-19" parent_project: prj-state-hub-retirement parent_workplan: SHR-WP-0001 related: - STATE-WP-0074 - SHR-INV-0001 - RMGR-WP-0001 - HUB-WP-0004 state_hub_workstream_id: "749beac6-3c62-4284-aab5-9ed7fce900c2" --- # State Hub retirement strangler and disposition execution ## Goal Execute the **keep/move/replace/retire** dispositions from `prj-state-hub-retirement/inventory` via strangler adapters and owner handoffs, without adding new permanent authorities here. End state: freeze window with no normal traffic, then archive. ## Dual-run handoff (RMGR-WP-0002) Repo Manager Stage B dual-run is available: - Flags: `RM_WRITEBACK`, `RM_RECONCILE`, `RM_PILOT_REPOS` (see `repo-manager/docs/dual-run.md`) - SH adapter: `api/services/repo_manager_dual_run.py` (PATCH `/tasks` + C-15 writeback + reconcile proxy) - Evidence: `repo-manager/docs/evidence/wp0002-completion.md` Expand pilot list here as cutover progresses; do not add new checkout mutators in State Hub. ## Freeze policy for new scope ```task id: STATE-WP-0079-T01 status: done priority: high state_hub_task_id: "375c0b66-98b0-4d5b-98dd-4ad2883a4051" ``` Document and enforce: changes during retirement must preserve compatibility, enable extraction, reduce scope, or fix operational risk — not establish new permanent ownership (INTENT retirement status). **Result (2026-08-20):** `policies/retirement-freeze.md`, following the existing `policies/` convention. States the four admissible reasons, the inadmissible case (new permanent ownership here), and the operative test — *where does this live after cutover?* — since most inadmissible changes are otherwise good changes. Uses `STATE-WP-0080`'s retarget as the worked example. **Enforced by review, not by new automation.** A freeze-checker built inside State Hub would be new permanent automation in the component being retired, so the policy would violate itself. Gates attach to points where changes are already reviewed: new workplan goal statements, non-trivial task starts, and slice execution. New tables, routes, or MCP tools carry a presumption against and are the one hard stop. ## Capability strangler plan ```task id: STATE-WP-0079-T02 status: done priority: high state_hub_task_id: "595c3936-20b6-453f-92b5-84f86798054a" ``` Group SHR-INV-0001 items into cutover slices (repo/work → repo-manager; messages/progress/projections → hub-core; catalogs → owners; retire aliases). Per slice: adapter flag, owner API, rollback. **Result (2026-08-19):** `docs/retirement-cutover-slice-plan.md`. All 425 inventory items assigned to 19 slices in five waves plus Keep, computed against `state-hub-disposition.yaml` rather than hand-counted. Each slice carries an `RM_SLICE_*` flag (off/dual/owner), owner API, and rollback; Wave A reuses the RMGR-WP-0002/0003 dual-run adapter rather than adding a mechanism. Owner readiness verified: only **Wave A (repo-manager, 165 items)** is executable — RMGR-WP-0001/0002/0003 are `finished`. Wave B is gated on HUB-WP-0004 (`proposed`), D1 on OPS-WP-0003 (`proposed`), D2 on FIN-WP-0003 (`proposed`). Recommended first cut is A3 (registers), lowest blast radius. Two findings for T04/T05: the 9 `ops_dependency` items carry no capability and cannot be strangled behind a flag — they resolve at the T06 freeze window; and the legacy-meter review window is 8 hours despite `cadence: weekly`, which is not sufficient evidence to retire high-volume `/workstreams/` interfaces (E2). ## Register CLI and GOAL.md support ```task id: STATE-WP-0079-T03 status: done priority: medium state_hub_task_id: "55716f12-fb12-4a1e-a770-076c490db111" ``` Adapt `statehub register` / scaffolding so `prj-` repos with `GOAL.md` and `repo_flavor: project` are valid without inventing durable-product `INTENT.md` (residual from SHR-WP-0001-T01). **Result (2026-08-19):** Implemented by `STATE-WP-0080-T01/T02`. Project flavor is detected from classification, `GOAL.md`, or the `prj-` slug; a repo with an existing `GOAL.md` is registered without durable scaffolding, while a fresh project delegates scaffolding to `rmgr scaffold`. The focused State Hub registration suite passes (`20 passed`). ## Execute move/replace slices ```task id: STATE-WP-0079-T04 status: todo priority: high state_hub_task_id: "db5291f2-3801-40fa-abf2-ceb0e77687c9" ``` Implement strangler handoffs coordinated with RMGR-WP-0001 and HUB-WP-0004. Compatibility tests for each dispositioned route family. **Owner-readiness correction (2026-08-20):** T02's 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 has **no** reference to `sbom`, `repo_goal`, `contribution`, `technical_debt`, or `extension_point`. So only **A1 (partial) and A2** are receivable — 86 items, not 165. A3 (registers, 49), A4 (UI, 22) and A5 (topic spine, 8) have no owner-side surface at all, and RMGR-WP-0004/0005 do not cover them. **Critical path is now repo-manager-side**, not here: a repo-manager workplan must build the register, UI, and topic surfaces before 79 Wave A items can move. Building them in State Hub is inadmissible under `policies/retirement-freeze.md` — it would be new permanent ownership in the component being retired. **A2 checked the same way (2026-08-20):** the State Hub adapter exposes exactly three operations — `rm_update_task_status`, `rm_reconcile`, `rm_scaffold`. There is no adapter for workplan CRUD, intake, decision, dependency, or human-flag records, and no repo-manager parser for them either. A2 therefore splits into **A2a** (task status + reconcile, ~6 items, receivable) and A2b–A2e (~32 items, not receivable). **A2a is the only slice in the whole plan executable today.** Its next step is operational rather than code: `rmgr dual-run-status` shows dual-run entirely off (`config_exists: false`, writeback/reconcile false, no pilot repos; the 23 metered mutations are RMGR-WP-0002/0003 pilot residue). Moving A2a from `off` to `dual` means creating `~/.repo-manager/dual-run.yaml`, enabling `RM_WRITEBACK` and `RM_RECONCILE` for one pilot repo, and watching the mutation meter for divergence. Reversible by clearing the flags; needs an operator decision because it changes how live task writes flow. Net: of 425 inventory items, roughly **6 can move today**. The retirement is gated almost entirely on owner-side capability in `repo-manager` and `hub-core`. **A2a executed (2026-08-20) — first live cutover slice.** Dual-run is on for the pilot repo: - `~/.repo-manager/dual-run.yaml` installed from `repo-manager/config/dual-run.pilot.example.yaml` — `writeback: true`, `reconcile: true`, `writeback_push: false`, `pilot_repos: [repo-manager]` - State Hub API reloaded via its own `--reload` watcher (no service restart, the operator's foreground `make api` was left alone) - `statehub fix-consistency --repo repo-manager` reported `RM dual-run reconcile ok: index=/home/worsch/repo-manager/.repo-manager/index.json` - mutation meter grew 23 → 24 with `{"kind": "reconcile", "repo_slug": "repo-manager", "workplan_count": 8, "task_count": 53, "record_count": 61}` **Divergence check:** rmgr's index holds 8 workplans; the hub has `rmgr-wp-0001`–`0007` registered. The single-item delta is `RMGR-WP-0008`, whose C-06 registration was skipped because this instance is not the identifier registrar (ADR-007 interim). That is a known registrar limitation, **not** dual-run divergence — rmgr correctly indexed the file that exists on disk. Slice state: **A2a is `dual`, not `owner`** — the hub remains authoritative and rmgr executes checkout mutation alongside it. Promotion to `owner` needs a full consistency-sweep cycle at `dual` with zero divergence, per this plan's adapter rules. Rollback is `rm ~/.repo-manager/dual-run.yaml`. ### Registrar bottleneck — status check 2026-08-21 **The registrar is reachable, and was all along.** `bridge status` shows all seven tunnels `connected` and LIVE, including `state-hub-primary`. It binds `[::1]:8000` — IPv6 localhost — while the workstation API listens on `127.0.0.1:8000` (IPv4). Same port number, different stacks, so a `127.0.0.1` probe silently hits the local hub and looks like the tunnel is down. Anything checking registrar reachability must use the bracketed IPv6 form. **But the two databases have diverged badly:** | | workstation (`127.0.0.1:8000`) | registrar (`[::1]:8000`) | | --- | --- | --- | | workplans | 987 | 649 | | tasks | 5 710 | 4 037 | | decisions | 119 | 80 | The registrar is **338 workplans and 1 673 tasks behind**. It also runs older code — its `/state/summary` still returns `open_workstreams` where the workstation returns `open_workplans`, so it predates the terminology rename. This is exactly the `ADR-007` defect `RMGR-WP-0005` describes, at fleet scale: two instances over two databases, each minting database-local primary keys into shared git artifacts. **A queue has formed.** Ten unread messages to `state-hub`, nine of them registrar sync requests from three agents (`agentic-resources`, `glas-harness`, `kaizen-agentic`), all dated 2026-08-20 — plus this repo's own `RMGR-WP-0008`. Agents fleet-wide are now blocked on identifier registration. **The dependency chain, re-checked:** - `RMGR-WP-0004-T08` (prefix uniqueness enforcement) — **`done`** 2026-08-18 - `RMGR-WP-0004-T09` (assign shared prefixes) — executed 2026-08-17 as an authorized exception, still `wait`, with one stated residual: *"Not yet reconciled — the workstation is not the registrar"* - `RMGR-WP-0005-T03` (deterministic UUIDv5 derivation) — still `wait`, but its stated blocker `RMGR-WP-0004-T08` **has closed** T03 is the permanent fix: derived identifiers make writeback idempotent, so any number of hubs write identical bytes and the queue disappears. Its formal gate is clear. Its *substantive* gate is not — `ADR-007` § Migration option 2 lets historical files keep colliding identifiers (`CUST-WP`, `RAILIANCE-WP`, and `PRJ-WP` in `prj-forgejo-org-refactor` all still present in the 2026-08-18 scan), and deriving UUIDv5 from a non-unique identifier manufactures collisions rather than removing them. That tension needs a ruling before T03 can start. **Inherited (2026-08-19) from `STATE-WP-0080-T04`:** this slice now also owns deletion of the templating path in `statehub_register.py` — `INTENT.md` generation, the `AGENTS.md` / `CLAUDE.md` / `.custodian-brief.md` templates, `project_rules/`, and bootstrap-workplan generation. The code is dead-but-present: project repos are already routed to `rmgr scaffold`. Gate the deletion on `RMGR-WP-0004-T06` proving byte-identical durable output and idempotent re-scaffolding. ## Retire legacy surfaces ```task id: STATE-WP-0079-T05 status: todo priority: medium state_hub_task_id: "02e508ed-3cde-4487-907e-d324a8a877d6" ``` Complete retirement of suggestions, workstream aliases, and other `retire` inventory items once meters/callers allow. Keep historical rows archive-readable. **Blocker identified (2026-08-20):** the legacy-meter evidence this task gates on was being captured over an 8-hour band, not a week — `capture_legacy_meter_evidence.py` fell back to `hours=8` whenever `--days` was omitted, so 39 of 40 captures sampled only 06:00Z–14:00Z. Interfaces called outside that band read as unused. Three candidates on the 2026-08-19 capture had live callers, including `GET /tasks/?workstream_id` (seen 2026-08-18). Script default corrected to `days=7`; corrected capture `docs/evidence/legacy-meter-weekly-review-20260820.json`. **Candidate rule tightened (2026-08-20):** `_retirement_state` now requires a quiet period scaled to all-time call volume (`RETIREMENT_QUIET_LADDER`: 7d under 100 calls, 30d under 10 000, 60d above). On the 2026-08-20 capture this makes 15 of 19 legacy interfaces retirable — all `workstream` MCP tools, the `state://` resource, and the low-volume REST aliases — while holding the three six-figure read paths (`GET /workstreams/`, `GET /workstreams/{id}`, `GET /workstreams/{id}/dependencies/`) and `GET /tasks/?workstream_id`, which still has live traffic. Covered by 7 new tests in `tests/test_legacy_meter.py`. **Slice E2 executed (2026-08-20):** the 15 evidenced interfaces are retired in the legacy meter — all four `workstream` MCP tools, the `state://` resource, and the low-volume REST aliases including `PATCH /workstreams/{id}` (571 calls), `POST /workstreams/` (824) and `POST /workstreams/{id}/dependencies/` (4 971). Meter now reads 16 retired / 4 legacy, 0 outstanding candidates. Evidence: `docs/evidence/legacy-meter-weekly-review-20260820.json`. Retirements are journalled fleet-side at `the-custodian/docs/retired-legacy-interfaces.md` — deliberately outside this repo, since State Hub is being archived and the record must outlive it. **Remaining work for this task:** the 4 held interfaces (`GET /workstreams/`, `GET /workstreams/{id}`, `GET /workstreams/{id}/dependencies/`, `GET /tasks/?workstream_id`) clear as their quiet counters run; earliest is 18 more silent days. Re-capture before deciding — the ladder evaluates at review time. **Slice E1 part-executed (2026-08-20):** `suggestion-backlog` history archived to `the-custodian/docs/archived-suggestion-backlog.md` — 10 suggestions, 10 notes, 5 bumps, all `declined` in the 2026-07-21 intake migration, none promoted. Mutations already 410 since `CUST-WP-0061-T06`; reads were open only to keep that history reachable, which the archive now replaces. **E1 removal executed the same day:** the 8 read routes 410 with a pointer to intake and the archive, the 6 MCP tools are removed outright, the 5 `/suggestions` write-idempotency rules are dropped, and the dashboard page is deleted with nav, `reference.md` and `wsjf-triage.md` updated; `dashboard/src/docs/suggestions.md` is now the archive pointer. Tables remain for the T06 dump. Two tests that pinned the old read-live behaviour were rewritten to pin the retirement contract. Naming trap for cutover: `ui-feedback.md` / `todo.md` "suggestions" are Shift+click dashboard feedback backed by `technical_debt`, **not** this capability. Untouched. **E3 (`dashboard-meta`) is `/ui-feedback`** — that same feedback surface. Owner is `state-hub-until-cutover` and it is still in use, so it retires at the T06 freeze window, not now. Then `dashboard-meta` (E3, 1); `legacy-meter` itself (E4, 9) retires last, being the instrument. ## Stabilization window and archive prep ```task id: STATE-WP-0079-T06 status: todo priority: high state_hub_task_id: "d52c95c6-af3f-4b2c-804e-e07763c9a8ab" ``` With T06 gates: zero normal read/write window, final dump, backup/restore evidence, stop service, archive repo read-only. Residuals → live owners. ## Acceptance - [x] Freeze policy documented - [x] Strangler plan covers all inventory capabilities - [x] prj-/GOAL.md register path works - [ ] Move/replace slices evidenced with tests/counts - [ ] Retire list closed or residual-owned - [ ] Stabilization + archive prep ready for project retirement