mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
a7b62ed886
* feat(mcp): wire item link/lifecycle commands + slug-based --role resolution (TASK-968 partial)
Expands HTTPHandlerDispatcher beyond TASK-967's per-command set with
twelve new commands across the link/lifecycle surface, plus the
prefetch-based --role slug → agent_role_id resolution that mirrors the
--assign path TASK-967 introduced.
New commands:
- item block / blocked-by / unblock (blocks links)
- item implements / unimplements (implements links)
- item supersedes / unsupersede (supersedes links)
- item split-from / unsplit (split_from links)
- item deps / related / implemented-by (read-only link queries)
The link create/delete commands have a URL/body asymmetry the simple
routeSpec framework can't express: block uses (source.Slug, target.ID)
while blocked-by inverts to (blocker.Slug, source.ID). They also need a
ref→{slug,id} prefetch pair before building the request, which means
they need a Handler reference. Those land as method-bound dispatchers
in dispatch_http_links.go alongside an itemLinkSpec table that captures
the per-command (urlRefKey, bodyTargetRefKey, linkType) tuple. Read-only
link queries (deps/related/implemented-by) all GET /items/{ref}/links
and let the agent group however it wants — same payload, different
rendering on top.
Slug-based --role resolution mirrors TASK-967's --assign path:
resolveRoleSlug + lookupRoleID hit /api/v1/workspaces/{ws}/agent-roles/{slug}
and rewrite role: <slug> → agent_role_id: <uuid> before the mapper
runs. This replaces the older --role rejection in mapItemCreate /
dispatchItemUpdate that pointed agents at the agent_role_id workaround;
the workaround still works (lifted via liftFieldsToColumns) but slugs
now flow through cleanly. commandsAcceptingRoleBySlug is the allowlist;
item.list intentionally stays out because its handler accepts both UUID
and slug at the query-param level.
Adds noRemoteEquivalent rejection set for genuinely-CLI-only commands
(agent status, mcp status/uninstall, server info/open, workspace
link/switch/context). The error message says "no remote equivalent —
CLI-only command" to give agents a stable signal distinct from
"not yet implemented over HTTP transport" — the former never lands
remotely, the latter just hasn't been wired yet.
Tests:
- Per-command happy path + missing-input rejections + prefetch
failure rejections.
- Asymmetry test (blocked-by URL goes through blocker, body
target_id is source).
- Canonical link-type wire form (split_from not split-from).
- Slug → agent_role_id resolution end-to-end through Dispatch.
- noRemoteEquivalent stable error format.
- Integration smoke against *server.Server: create A + B → block →
deps surfaces → unblock → deps empty → supersedes works (catches
canonical-wire-form regressions in the store).
Parent: PLAN-943.
* fix(mcp): related/implemented-by emit CLI's grouped JSON shape; wrap DELETE 204 as {status:removed} per Codex review (round 1)
Two parity gaps Codex caught on PR #346:
1. `item related` and `item implemented-by` were returning the raw
/links array. The CLI's `--format json` output wraps and post-
processes — `related` returns `{item_ref, item_title, collection,
group_count, groups[]}` (links grouped by canonical type +
direction); `implemented-by` returns `{item_ref, item_title, count,
results[]}` (filtered to incoming `implements` only).
Fix: the read-only link queries now have command-specific
dispatchers — `dispatchItemDeps` keeps the raw-array shape (matches
`deps --format json`), while `dispatchItemRelated` and
`dispatchItemImplementedBy` reproduce the CLI's wrapping and
filtering. The grouping helpers (buildRelatedGroups,
incomingImplementedBy, relatedEntryFromLink) are reproduced from
cmd/pad/query.go since the CLI versions are in package main.
2. Successful unblock/unimplements/unsupersede/unsplit dispatches
were returning empty TextContent because the handler emits 204
No Content. The CLI's `--format json` for these commands prints
`{"status":"removed"}`, so MCP clients lose the structured
success signal.
Fix: dispatchDeleteItemLink now wraps the 2xx response with
`{"status":"removed"}` via the new packageStructuredResponse
helper.
Also adds packageStructuredResponse: a small helper that marshal-
decodes synthesized payloads back to `any` before stuffing them into
NewToolResultStructured, so the StructuredContent surface matches
what packageHTTPResponse emits for normal route paths (`map[string]any`
/ `[]any` / JSON-decoded primitives, not Go-typed slice/struct
instances).
Tests:
- Per-command tests for `related`'s grouped shape (blocks +
implemented_by groups appear)
- `implemented-by`'s incoming-only filter (outgoing implements
+ non-implements links excluded from `count`/`results`)
- DELETE 204 wrapping for unblock
- `deps` keeps raw-array shape (parity preserved)
Parent: PLAN-943.