Files
pad/internal
xarmian a7b62ed886 feat(mcp): wire item link/lifecycle commands + slug-based --role resolution (TASK-968 partial) (#346)
* 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.
2026-05-01 13:29:45 -04:00
..
2026-03-26 01:52:36 +00:00