feat(workplan): CUST-WP-0027 — capability request dispute & negotiation

Adds a routing_disputed status, dispute/reroute endpoints, MCP tools,
and dashboard highlighting so any agent can flag a misrouting and
negotiate before a request is accepted.

Also rerouted 0e0aefd7 (KeyCape GHCR image) from railiance → netkingdom
and created netkingdom catalog entry for container image publishing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 23:42:25 +01:00
parent e423ff7126
commit e31693ad67

View File

@@ -0,0 +1,220 @@
---
id: CUST-WP-0027
type: workplan
title: "Capability Request Dispute & Negotiation"
domain: custodian
repo: the-custodian
status: active
owner: custodian
topic_slug: custodian
created: "2026-03-21"
updated: "2026-03-21"
state_hub_workstream_id: "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`
```python
_VALID_TRANSITIONS = {
"requested": {"accepted", "rejected", "withdrawn", "routing_disputed"},
"routing_disputed": {"requested", "withdrawn"},
"accepted": {"in_progress", "rejected", "withdrawn"},
...
}
```
---
## Tasks
### T01 — DB migration: dispute columns
```task
id: CUST-WP-0027-T01
status: todo
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
```task
id: CUST-WP-0027-T02
status: todo
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
```task
id: CUST-WP-0027-T03
status: todo
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
```task
id: CUST-WP-0027-T04
status: todo
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
```task
id: CUST-WP-0027-T05
status: todo
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