Adds a structured dispute mechanism when capability request routing is wrong:
- New `routing_disputed` status with four DB columns (dispute_reason, disputed_by,
dispute_suggested_domain, disputed_at) via Alembic migration m0h1i2j3k4l5
- POST /capability-requests/{id}/dispute — any party can flag misrouting with a reason
and optional suggested domain; notifies custodian + current fulfilling domain
- POST /capability-requests/{id}/reroute — custodian re-routes to correct domain via
catalog_entry_id or direct slug; appends audit trail to routing_note; resets to requested
- Two new MCP tools: dispute_capability_routing and reroute_capability_request
- Dashboard: amber disputed-banner at top of Summary, routing_disputed Kanban column,
dispute details (reason, suggested domain, raised-by) shown on disputed cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.6 KiB
id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_workstream_id
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| CUST-WP-0027 | workplan | Capability Request Dispute & Negotiation | custodian | the-custodian | done | custodian | custodian | 2026-03-21 | 2026-03-21 | 72d62c88-3745-48ad-9e18-44df51ab21a4 |
CUST-WP-0027 — Capability Request Dispute & Negotiation
Problem
The routing algorithm is keyword-based and makes mistakes. When a request is misrouted (e.g. "KeyCape container image published to GHCR" → railiance instead of netkingdom), there is no structured way for any party to:
- Flag the routing as wrong
- Suggest the correct domain
- Pause the workflow while the routing is negotiated
- Have the custodian re-route with an audit trail
Today the only recourse is a manual PATCH to catalog_entry_id —
discoverable only if you know the API. The lifecycle skips straight from
requested to accepted with no dispute window.
Goal
Any agent (requester, proposed fulfiller, custodian) can:
- Dispute a routing decision with a reason and an optional suggested domain
- See disputed requests prominently in the dashboard
- Re-route the request to the correct domain (custodian action)
- Resume the normal lifecycle once routing is settled
Design
New status: routing_disputed
Insert between requested and accepted:
requested
├── accepted (happy path — no dispute)
├── routing_disputed (any party flags misrouting)
│ ├── requested (re-routed — back to start with new domain)
│ └── withdrawn
├── rejected
└── withdrawn
New DB columns on capability_requests
| Column | Type | Purpose |
|---|---|---|
dispute_reason |
Text nullable | Why the routing is wrong |
disputed_by |
String(100) nullable | Agent who raised the dispute |
dispute_suggested_domain |
String(100) nullable | Where it should go instead |
disputed_at |
DateTime nullable | When the dispute was raised |
No new table needed — disputes are single-valued (one active dispute at a
time per request). History is in routing_note (appended on each action).
New endpoints
POST /capability-requests/{id}/dispute
- Body:
{reason: str, suggested_domain: str | null, disputed_by: str} - Allowed from:
requestedstatus only - Sets status →
routing_disputed, fills dispute columns - Notifies: custodian + current fulfilling domain agent
POST /capability-requests/{id}/reroute
- Body:
{domain: str | null, catalog_entry_id: UUID | null, note: str, rerouted_by: str} - Allowed from:
routing_disputedstatus only - Changes
fulfilling_domain_id(via catalog entry or direct domain lookup) - Clears dispute columns
- Appends to
routing_note - Sets status →
requested - Notifies: requester + new fulfilling domain agent
Updated _VALID_TRANSITIONS
_VALID_TRANSITIONS = {
"requested": {"accepted", "rejected", "withdrawn", "routing_disputed"},
"routing_disputed": {"requested", "withdrawn"},
"accepted": {"in_progress", "rejected", "withdrawn"},
...
}
Tasks
T01 — DB migration: dispute columns
id: CUST-WP-0027-T01
status: done
priority: high
state_hub_task_id: "41357812-9791-488d-883b-75cb07ae4c90"
Add Alembic migration with four new nullable columns on
capability_requests:
dispute_reason: Textdisputed_by: String(100)dispute_suggested_domain: String(100)disputed_at: DateTime(timezone=True)
Update CapabilityRequest model and CapabilityRequestRead schema.
Gate: make migrate runs cleanly; make test passes.
T02 — Dispute endpoint + routing_disputed status
id: CUST-WP-0027-T02
status: done
priority: high
state_hub_task_id: "39429f29-cf3a-440c-aca7-5b9e3b31b2d0"
- Add
routing_disputedto_VALID_TRANSITIONS - Add
POST /capability-requests/{id}/disputeendpoint:- Body schema:
CapabilityRequestDispute(reason, suggested_domain, disputed_by) - Guard: only from
requestedstatus - Fills dispute columns, sets status
- Notifies custodian and current fulfilling domain
- Body schema:
- Add
CapabilityRequestDisputePydantic schema
Gate: make test passes; dispute transitions correctly; custodian +
fulfilling domain receive notifications.
T03 — Reroute endpoint
id: CUST-WP-0027-T03
status: done
priority: high
state_hub_task_id: "94ede349-ddd1-4572-bb18-43cb2e67326b"
- Add
POST /capability-requests/{id}/rerouteendpoint:- Body schema:
CapabilityRequestReroute(domain, catalog_entry_id, note, rerouted_by) - Guard: only from
routing_disputedstatus - Accept
catalog_entry_id(preferred, re-derives domain) ORdomainslug (direct) - Clear dispute columns
- Append to
routing_note:"re-routed by {rerouted_by}: {note}" - Set status →
requested - Notify: requester + new fulfilling domain
- Body schema:
- Add
CapabilityRequestReroutePydantic schema
Gate: full dispute→reroute→accept round-trip passes in tests.
T04 — MCP tools
id: CUST-WP-0027-T04
status: done
priority: medium
state_hub_task_id: "e8d1e24e-8ec7-4c4a-b2b5-f6072ab7582c"
Add two MCP tools to the state-hub MCP server:
dispute_capability_routing(request_id, reason, suggested_domain, disputed_by)
- Calls
POST /capability-requests/{id}/dispute - Returns updated request
reroute_capability_request(request_id, domain, note, rerouted_by, catalog_entry_id)
- Calls
POST /capability-requests/{id}/reroute - Returns updated request
Gate: both tools callable from Claude Code; test against the misrouting pattern (route → dispute → reroute → accept).
T05 — Dashboard: highlight disputed requests
id: CUST-WP-0027-T05
status: done
priority: low
state_hub_task_id: "a40b6a57-2cfd-4e09-ac00-3d4d2920eb7a"
Update state-hub/dashboard/src/capability-requests.md:
- Add a
routing_disputedfilter/badge (amber, same pattern as interventions) - Show
dispute_reasonanddispute_suggested_domainin the detail view - Disputed requests should appear at the top of the list regardless of creation order
Gate: a routing_disputed request renders with amber badge and dispute
details visible.
Done Criteria
POST /capability-requests/{id}/disputetransitions torouting_disputedand notifies custodian + current fulfilling domainPOST /capability-requests/{id}/reroutere-routes and resets torequestedwith full audit trail inrouting_notedispute_capability_routingandreroute_capability_requestMCP tools work- Dashboard shows disputed requests with amber badge
make testpasses after all changes