diff --git a/workplans/CUST-WP-0027-capability-request-dispute.md b/workplans/CUST-WP-0027-capability-request-dispute.md new file mode 100644 index 0000000..463f635 --- /dev/null +++ b/workplans/CUST-WP-0027-capability-request-dispute.md @@ -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