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:
220
workplans/CUST-WP-0027-capability-request-dispute.md
Normal file
220
workplans/CUST-WP-0027-capability-request-dispute.md
Normal 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
|
||||
Reference in New Issue
Block a user