Files
the-custodian/workplans/CUST-WP-0027-capability-request-dispute.md
tegwick efbbef76b0 feat(capability-requests): add routing dispute & reroute workflow (CUST-WP-0027)
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>
2026-03-21 23:58:52 +01:00

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:

  1. Flag the routing as wrong
  2. Suggest the correct domain
  3. Pause the workflow while the routing is negotiated
  4. 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: requested status 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_disputed status 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: Text
  • disputed_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_disputed to _VALID_TRANSITIONS
  • Add POST /capability-requests/{id}/dispute endpoint:
    • Body schema: CapabilityRequestDispute(reason, suggested_domain, disputed_by)
    • Guard: only from requested status
    • Fills dispute columns, sets status
    • Notifies custodian and current fulfilling domain
  • Add CapabilityRequestDispute Pydantic 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}/reroute endpoint:
    • Body schema: CapabilityRequestReroute(domain, catalog_entry_id, note, rerouted_by)
    • Guard: only from routing_disputed status
    • Accept catalog_entry_id (preferred, re-derives domain) OR domain slug (direct)
    • Clear dispute columns
    • Append to routing_note: "re-routed by {rerouted_by}: {note}"
    • Set status → requested
    • Notify: requester + new fulfilling domain
  • Add CapabilityRequestReroute Pydantic 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_disputed filter/badge (amber, same pattern as interventions)
  • Show dispute_reason and dispute_suggested_domain in 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}/dispute transitions to routing_disputed and notifies custodian + current fulfilling domain
  • POST /capability-requests/{id}/reroute re-routes and resets to requested with full audit trail in routing_note
  • dispute_capability_routing and reroute_capability_request MCP tools work
  • Dashboard shows disputed requests with amber badge
  • make test passes after all changes