Files
the-custodian/workplans/CUST-WP-0005-dynamic-domains.md
tegwick d96ed44c57 feat(maintenance): add stale-task cleanup scheme
- scripts/cleanup_stale_tasks.py: daily script that cancels open tasks
  in completed/archived workstreams; handles 307 redirects; emits a
  cleanup progress event summarising results
- Makefile: add cleanup-stale target (also suitable for cron)
- ADR-001: append Workstream Closure Protocol section — mandatory closure
  review before marking workstream completed, with task classification
  table (done/cancelled/carry-forward) and Closure Review file format
- WP-0002 + WP-0005: append Closure Review sections documenting the
  2026-03-02 cleanup run (26 stale DB rows cancelled — all were legacy
  pre-ADR-001 DB-first records; file status was already done)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:32:35 +01:00

12 KiB
Raw Permalink Blame History

id, type, title, domain, status, owner, topic_slug, state_hub_workstream_id, created, updated, completed
id type title domain status owner topic_slug state_hub_workstream_id created updated completed
CUST-WP-0005 workplan State Hub v0.5 — Dynamic Domains & Multi-Repo custodian completed custodian custodian 2271eb55-62ca-4bcc-8d9f-f9aacd6922d6 2026-02-28 2026-02-28 2026-02-28

State Hub v0.5 — Dynamic Domains & Multi-Repo

Summary

Replace the hardcoded 6-domain Python enum with a first-class domains DB table, enabling new domains to be added and existing ones to be renamed without schema migrations. Alongside: introduce a managed_repos table so multiple git repositories can be registered per domain, with a corresponding registration workflow update.

Context

Domains are currently baked into a PostgreSQL ENUM type (domain with 6 fixed values). Adding a domain requires a schema migration; renaming is impossible at runtime. The registration workflow (make register-project) assumes one repository per domain. Neither constraint fits the actual project structure, where a domain may span multiple repos and the domain set itself should be open.

Dependencies

  • Blocks: CUST-WP-0003 (v0.3 Contribution Tracking) — managed_repos table, /repos/ router, register_repo MCP tool, and base registration workflow in v0.3 are superseded by P2.1P2.3 here.
  • state_hub_dependency_id: 2033172d-2462-4253-acb7-cb64c7432480

Phase 1 — Domain as a DB Entity

P1.1 — Create domains table + Alembic migration

id: CUST-WP-0005-T01
state_hub_task_id: 456a0252-9a34-43a6-8244-c3a2caf95d51
status: done
priority: high

New table: domains (id UUID PK, slug VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, status VARCHAR(20) DEFAULT 'active' CHECK IN ('active','archived'), created_at, updated_at).

Migration (down_revision: a3f1c2d4e5b6):

  1. Create domains table.
  2. INSERT the 6 canonical rows from the existing enum values.
  3. ADD COLUMN domain_id UUID REFERENCES domains(id) to topics.
  4. UPDATE topics SET domain_id = (SELECT id FROM domains WHERE slug = domain::text).
  5. ALTER TABLE topics ALTER COLUMN domain_id SET NOT NULL.
  6. DROP COLUMN domain (old enum column) from topics.
  7. DROP TYPE domain (the PostgreSQL enum type).
  8. Leave EP and TD domain columns as String(50) — validated at API layer against live domains list, not via FK. Document as acceptable loose coupling (avoids cascading complexity).

Downgrade: reverse all steps (recreate enum, repopulate, drop domain_id).

P1.2 — Domain ORM model + Pydantic schemas

id: CUST-WP-0005-T02
state_hub_task_id: eddff1ae-b263-4d3e-af95-91b4c3c2ecea
status: done
priority: high

New file: state-hub/api/models/domain.py

  • Domain(Base): id, slug, name, description, status, created_at, updated_at
  • Relationship: topics: list[Topic]

Update state-hub/api/models/topic.py:

  • Remove Domain(str, enum.Enum) class
  • Change domain column → domain_id: Mapped[uuid.UUID] (FK to domains.id)
  • Add domain: relationship(Domain) backref

New file: state-hub/api/schemas/domain.py

  • DomainCreate: slug, name, description?
  • DomainRead: id, slug, name, description, status, created_at, updated_at
  • DomainUpdate: name?, description?, status?
  • DomainRename: new_slug, new_name (cascades EP/TD string columns)

P1.3 — Domain API router: list, get, create, rename, archive

id: CUST-WP-0005-T03
state_hub_task_id: b91cd0b5-c647-41e5-abf5-98f6f5eb3333
status: done
priority: high

New file: state-hub/api/routers/domains.py

Endpoints:

  • GET /domains/ — list all (filter: ?status=active|archived|all)
  • GET /domains/{slug}/ — get by slug with counts (topics, workstreams, EPs, TDs, repos)
  • POST /domains/ — create new (slug: unique, lowercase, underscored)
  • PATCH /domains/{slug}/rename — update slug + name; cascade EP/TD string columns
  • PATCH /domains/{slug}/archive — soft-delete; 409 if active topics exist

Register in state-hub/api/main.py.

P1.4 — Update seed.py + TopicCreate schema for new domain model

id: CUST-WP-0005-T04
state_hub_task_id: 0825fa45-ec3a-43b5-8482-4a819db75c6a
status: done
priority: medium

Update state-hub/scripts/seed.py:

  • Insert 6 domains before topics (idempotent: ON CONFLICT DO NOTHING).
  • Topic insertion uses domain_id FK (lookup by slug) instead of enum.
  • Remove Domain enum import.

Update state-hub/api/schemas/topic.py:

  • TopicCreate.domain: Domain enum → str (validated at router via DB lookup).
  • TopicRead: expose domain_slug: str from FK relationship.

Update state router: any Domain.custodian etc. references → slug string.


Phase 2 — Multi-Repo Support

P2.1 — Create managed_repos table + migration

id: CUST-WP-0005-T05
state_hub_task_id: 50dc7eec-2b45-4a0d-b99e-27aca8a55a67
status: done
priority: high

New table: managed_repos Columns: id UUID PK, domain_id UUID FK domains(id) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, local_path TEXT, remote_url TEXT, description TEXT, status VARCHAR(20) DEFAULT 'active', topic_id UUID FK topics(id) NULLABLE, created_at, updated_at.

Design: domain_id is the primary association (not topic_id) — a domain can have multiple repos without a topic for each. topic_id is optional, for workstream-level linkage.

Coordinate with v0.3 P2.2: if v0.3 runs first, extend rather than replace. Add to models/__init__.py and register router.

P2.2 — Repo API router: register, list, update, archive

id: CUST-WP-0005-T06
state_hub_task_id: daca1636-0187-4fb7-a515-8a2cf84f89d9
status: done
priority: medium

New file: state-hub/api/routers/repos.py

Endpoints:

  • GET /repos/ — list all (filter: ?domain=slug)
  • GET /repos/{slug}/ — get detail
  • POST /repos/ — register (domain_slug or domain_id required)
  • PATCH /repos/{slug}/ — update (name, local_path, remote_url, description, topic_id)
  • PATCH /repos/{slug}/archive — soft-delete

Include repos list in GET /domains/{slug}/ response.

Schemas: RepoCreate, RepoRead, RepoUpdate.

P2.3 — Update registration workflow: multi-repo + dynamic domains

id: CUST-WP-0005-T07
state_hub_task_id: 930fd3fa-2e1d-401c-9e33-0378f33cb14d
status: done
priority: medium

Update state-hub/scripts/register_project.sh:

  • Verify domain via GET /domains/{slug}/ (not just topic lookup).
  • POST to /repos/ after writing CLAUDE.md.
  • Support second repo for existing domain without overwriting CLAUDE.md (--force flag or existence check).

Update state-hub/Makefile:

  • make add-domain DOMAIN=<slug> NAME=<name> — POST /domains/
  • make rename-domain DOMAIN=<old> NEW_SLUG=<s> NEW_NAME=<n> — PATCH rename
  • make add-repo DOMAIN=<slug> REPO_PATH=<path> — POST /repos/ + CLAUDE.md
  • make list-repos DOMAIN=<slug> — GET /repos/?domain=slug

Update state-hub/scripts/project_claude_md.template:

  • Add section listing other known repos in the same domain.

Phase 3 — MCP & Validation

P3.1 — MCP tools: domain lifecycle + repo registration

id: CUST-WP-0005-T08
state_hub_task_id: e99e6bd7-f6f2-496f-acbf-d2b013f37c73
status: done
priority: medium

Add to state-hub/mcp_server/server.py:

  1. list_domains(status="active") — GET /domains/?status=…
  2. create_domain(slug, name, description?) — POST /domains/
  3. rename_domain(slug, new_slug, new_name) — PATCH /domains/{slug}/rename
  4. archive_domain(slug) — PATCH /domains/{slug}/archive
  5. list_domain_repos(domain_slug) — GET /repos/?domain=…
  6. register_repo(domain_slug, name, local_path?, remote_url?, description?) — POST /repos/

Update state-hub/mcp_server/TOOLS.md to document all 6 new tools.

P3.2 — Live domain validation for EP/TD + domain stats in state summary

id: CUST-WP-0005-T09
state_hub_task_id: f2b352b6-4b9e-47b3-b629-48485245390e
status: done
priority: low

Replace hardcoded VALID_DOMAINS set in EP/TD routers with a per-request DB lookup: get_valid_domain_slugs(db: AsyncSession) -> set[str]. Return 422 with a helpful message for unknown domains.

Add to /state/summary response:

  • domains: list[DomainSummary] — slug, name, repo_count, active_workstream_count, ep_count, td_count.

Update global CLAUDE.md and project CLAUDE.md template to note list_domains() is now available.


Phase 4 — Dashboard

P4.1 — Dashboard: domains.md page

id: CUST-WP-0005-T10
state_hub_task_id: ddb91669-5297-45d9-8b5e-10213135a96d
status: done
priority: low

New file: state-hub/dashboard/src/domains.md

Layout:

  • KPI row: total domains, total repos, newest domain.
  • Domain cards: name, slug, status badge, repo count, workstream count, EP count, TD count.
  • Repos sub-list per card: name, local_path, remote_url (linked), date.
  • Click → entity modal (extend entity-modal.js for domain type).

Data loaders:

  • src/data/domains.json.py — GET /domains/?status=all
  • src/data/repos.json.py — GET /repos/

Add to observablehq.config.js nav.

P4.2 — Dashboard: domain filter on workstreams, EP, TD pages

id: CUST-WP-0005-T11
state_hub_task_id: 3fec152c-00e1-4cdb-b6e5-26136c8cb6c1
status: done
priority: low

Add Inputs.select domain filter to:

  • workstreams.md — filter by domain via topic.domain_slug
  • extensions.md — wire existing API domain filter to UI
  • techdept.md — wire existing API domain filter to UI

Each filter: load domain list from /domains/?status=active, default "All", client-side reactive re-filter, URL hash persistence (?domain=railiance).

Check if WorkstreamRead already exposes domain_slug; add if missing.


Notes

  • v0.5 P2.1 supersedes the managed_repos design in v0.3 P2.2. v0.3 is annotated accordingly and depends on this workplan.
  • EP/TD domain columns remain String(50) (not FK) for simplicity. The rename endpoint cascades updates to these columns.
  • sync_workplans.py (future, v0.3 Phase 4) will be able to ingest this file and reconcile it with the DB rows created during the planning session.

Closure Review — 2026-03-02

Outcome: All 11 tasks completed. No carry-forwards. No dropped tasks.

Context: This workplan was created DB-first on 2026-02-28, before ADR-001 was formalised. The workplan file correctly recorded all tasks as status: done, but the DB rows were never synced from the file — they remained in their initial todo state in the database. The daily stale-task cleanup script (scripts/cleanup_stale_tasks.py) detected these 11 stale DB rows and cancelled them on 2026-03-02. No actual work was lost: all deliverables in Phase 14 were shipped as part of State Hub v0.5.

Completed (DB updated at delivery time; file status = done)

  • CUST-WP-0005-T01 — Create domains table + Alembic migration
  • CUST-WP-0005-T02 — Domain ORM model + Pydantic schemas
  • CUST-WP-0005-T03 — Domain API router: list, get, create, rename, archive
  • CUST-WP-0005-T04 — Update seed.py + TopicCreate schema for new domain model
  • CUST-WP-0005-T05 — Create managed_repos table + migration
  • CUST-WP-0005-T06 — Repo API router: register, list, update, archive
  • CUST-WP-0005-T07 — Update registration workflow: multi-repo + dynamic domains
  • CUST-WP-0005-T08 — MCP tools: domain lifecycle + repo registration
  • CUST-WP-0005-T09 — Live domain validation for EP/TD + domain stats in state summary
  • CUST-WP-0005-T10 — Dashboard: domains.md page
  • CUST-WP-0005-T11 — Dashboard: domain filter on workstreams, EP, TD pages

Cancelled (DB records only — legacy stale rows, not real cancellations)

All 11 DB task rows were cancelled by the cleanup script. The workplan file was authoritative; the DB rows were artefacts of the pre-ADR-001 DB-first creation pattern. This does not reflect a change in work outcome.