Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ Surface:

**Stability contract.** Two version constants live in `internal/mcp/version.go`, advertised in the handshake under `capabilities.experimental.padCmdhelp` and `capabilities.experimental.padToolSurface`:
- `CmdhelpVersion` (currently `"0.1"`) — the cmdhelp CLI help-tree contract. Bump when CLI flag/arg schemas change incompatibly.
- `ToolSurfaceVersion` (currently `"0.7"`) — the MCP tool catalog contract. Bump when tool names, action enums, or parameter shapes change incompatibly. **v0.7** adds `export` + `import` actions to `pad_item`, mirroring the CLI `pad item export` / `pad item import` (covers playbooks AND conventions). `export` (read-only) takes `ref` and returns the portable artifact text — it forces the CLI's stdout sink (`-o -`) so the bytes come back as the result instead of a file. `import` (mutating, not destructive) takes a new `artifact` param (the full artifact text) and returns `{ref, slug, warnings}`; the ExecDispatcher can't pipe stdin, so it spills the artifact to a temp file and dispatches `item import <tmpfile>`. v0.6 added the `pad_item.backlinks` action; v0.5 added `pad_library`. v0.3 (PLAN-1377 / TASK-1380) introduced `pad_meta.action: bootstrap`, `pad_set_workspace`'s embedded-bootstrap response, and the `pad://workspace/{ws}/bootstrap` resource. **v0.4 (PLAN-1410)** is a comprehensive bootstrap-payload trim — same tool catalog, slimmer JSON shape inside bootstrap responses: `BootstrapCollection` projection drops `id`/`workspace_id`/timestamps/`settings` and emits `schema` as a nested object; `BootstrapRole` projection drops UUIDs/timestamps/`tools`; convention `slug` dropped; top-level `recent_activity` (a duplicate of `dashboard.recent_activity`) removed; new `BootstrapDashboard` wrapper caps five sub-arrays (`attention`, `recent_activity`, `active_items`, `active_plans`, `by_role`) at 5 entries each with parallel `*_overflow_count` fields; redundant schema labels omitted when `label == TitleCase(key)`. Cumulative size reduction: ~40% on a representative workspace, ~54% on the fixture (see PLAN-1410's Result section for per-section deltas). Compatibility: most changes are subtractive (dropped fields) or additive (overflow counts), but **one type change is breaking**: `collections[].schema` went from a JSON-encoded string to a nested JSON object — clients that JSON.parse()'d the string need to consume it directly as an object now. The dropped fields (UUIDs, timestamps, settings, duplicate `recent_activity`, convention `slug`) have canonical alternatives (slugs for addressing; `pad collection list` / `pad role list` for the full models when needed).
- `ToolSurfaceVersion` (currently `"0.8"`) — the MCP tool catalog contract. Bump when tool names, action enums, or parameter shapes change incompatibly. **v0.8** adds `restore` + `deleted` actions to `pad_workspace`, mirroring the CLI `pad workspace restore` / `pad workspace deleted` (TASK-1972): `deleted` (read-only) lists the caller's soft-deleted workspaces still inside the 30-day restore window; `restore` (mutating, not destructive, owner-only) un-soft-deletes a workspace by `slug` while it's still restorable. Both reuse the existing `slug` param — no new params; pure addition. **v0.7** adds `export` + `import` actions to `pad_item`, mirroring the CLI `pad item export` / `pad item import` (covers playbooks AND conventions). `export` (read-only) takes `ref` and returns the portable artifact text — it forces the CLI's stdout sink (`-o -`) so the bytes come back as the result instead of a file. `import` (mutating, not destructive) takes a new `artifact` param (the full artifact text) and returns `{ref, slug, warnings}`; the ExecDispatcher can't pipe stdin, so it spills the artifact to a temp file and dispatches `item import <tmpfile>`. v0.6 added the `pad_item.backlinks` action; v0.5 added `pad_library`. v0.3 (PLAN-1377 / TASK-1380) introduced `pad_meta.action: bootstrap`, `pad_set_workspace`'s embedded-bootstrap response, and the `pad://workspace/{ws}/bootstrap` resource. **v0.4 (PLAN-1410)** is a comprehensive bootstrap-payload trim — same tool catalog, slimmer JSON shape inside bootstrap responses: `BootstrapCollection` projection drops `id`/`workspace_id`/timestamps/`settings` and emits `schema` as a nested object; `BootstrapRole` projection drops UUIDs/timestamps/`tools`; convention `slug` dropped; top-level `recent_activity` (a duplicate of `dashboard.recent_activity`) removed; new `BootstrapDashboard` wrapper caps five sub-arrays (`attention`, `recent_activity`, `active_items`, `active_plans`, `by_role`) at 5 entries each with parallel `*_overflow_count` fields; redundant schema labels omitted when `label == TitleCase(key)`. Cumulative size reduction: ~40% on a representative workspace, ~54% on the fixture (see PLAN-1410's Result section for per-section deltas). Compatibility: most changes are subtractive (dropped fields) or additive (overflow counts), but **one type change is breaking**: `collections[].schema` went from a JSON-encoded string to a nested JSON object — clients that JSON.parse()'d the string need to consume it directly as an object now. The dropped fields (UUIDs, timestamps, settings, duplicate `recent_activity`, convention `slug`) have canonical alternatives (slugs for addressing; `pad collection list` / `pad role list` for the full models when needed).

Both are also returned by `pad://_meta/version` and `pad_meta.action: version`.

Expand Down
12 changes: 12 additions & 0 deletions internal/mcp/catalog_readonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ func TestReadOnlyCatalog_ActionsMatchCmdhelp(t *testing.T) {
// PLAN-1519 / TASK-1521 / IDEA-1517 §1 + §4: workspace lifecycle.
{"pad_workspace", "create"}: {"workspace", "create"},
{"pad_workspace", "claim"}: {"workspace", "claim"},
// TASK-1973: workspace soft-delete recovery.
{"pad_workspace", "deleted"}: {"workspace", "deleted"},
{"pad_workspace", "restore"}: {"workspace", "restore"},

{"pad_collection", "list"}: {"collection", "list"},
{"pad_collection", "create"}: {"collection", "create"},
Expand Down Expand Up @@ -229,6 +232,9 @@ func TestReadOnlyCatalog_ActionsDispatchExpectedCmdPath(t *testing.T) {
// PLAN-1519 / TASK-1521 / IDEA-1517 §1 + §4: workspace lifecycle.
{"pad_workspace", "create"}: {"workspace", "create"},
{"pad_workspace", "claim"}: {"workspace", "claim"},
// TASK-1973: workspace soft-delete recovery.
{"pad_workspace", "deleted"}: {"workspace", "deleted"},
{"pad_workspace", "restore"}: {"workspace", "restore"},

{"pad_collection", "list"}: {"collection", "list"},
{"pad_collection", "create"}: {"collection", "create"},
Expand Down Expand Up @@ -441,6 +447,12 @@ func liveCmdhelpDoc(t *testing.T) *cmdhelp.Document {
Args: mkArgs("code"),
Flags: mkFlags("workspace"),
},
// TASK-1973: workspace soft-delete recovery.
"workspace deleted": {Summary: "list soft-deleted workspaces"},
"workspace restore": {
Summary: "restore a soft-deleted workspace",
Args: mkArgs("slug"),
},
"workspace audit-log": {
Summary: "audit log",
Flags: map[string]cmdhelp.Flag{
Expand Down
21 changes: 17 additions & 4 deletions internal/mcp/catalog_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
)

// padWorkspaceTool exposes workspace-level operations: discovery
// (list), membership (members, invite), storage usage, and audit log.
// (list), membership (members, invite), storage usage, audit log,
// lifecycle (create, claim), and soft-delete recovery (deleted,
// restore).
//
// All actions dispatch through the existing CLI/HTTP route table.
// Workspace context: this tool operates ON workspaces, so the
Expand Down Expand Up @@ -66,7 +68,7 @@ var padWorkspaceTool = ToolDef{
{
Name: "slug",
Type: "string",
Description: "Workspace slug (kebab-case, globally unique). Optional for action=create; derived from name when omitted.",
Description: "Workspace slug (kebab-case, globally unique). Optional for action=create (derived from name when omitted); required for action=restore (names the soft-deleted workspace to restore).",
},
{
Name: "template",
Expand All @@ -89,6 +91,11 @@ var padWorkspaceTool = ToolDef{
// PLAN-1519 / TASK-1521 / IDEA-1517 §1 + §4.
"create": passThrough([]string{"workspace", "create"}),
"claim": passThrough([]string{"workspace", "claim"}),
// TASK-1973: workspace soft-delete recovery (mirrors the CLI
// `pad workspace deleted` / `pad workspace restore` from TASK-1972).
// Both non-interactive; restore is mutating but non-destructive.
"deleted": passThrough([]string{"workspace", "deleted"}),
"restore": passThrough([]string{"workspace", "restore"}),
},
}

Expand Down Expand Up @@ -154,7 +161,13 @@ Actions:
invalid_code, 404 not_found (workspace doesn't exist or you're
not a member), 412 connection_not_persisted (pre-Phase-C grant
— re-authorize).
deleted — List the caller's soft-deleted workspaces still inside the
30-day restore window, with days remaining before each is
permanently purged. No params. Read-only.
restore — Un-soft-delete a workspace by slug, provided it is still inside
the restore window. Owner-only. Mutating but non-destructive.
Required: slug.

Use pad_workspace when an agent needs to discover, manage membership, audit
workspace activity, or bring a new/additional workspace into this connection.
For item-level operations use pad_item.`
workspace activity, recover a soft-deleted workspace, or bring a new/additional
workspace into this connection. For item-level operations use pad_item.`
64 changes: 64 additions & 0 deletions internal/mcp/catalog_workspace_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,67 @@ func TestRouteTable_RegistersWorkspaceLifecycle(t *testing.T) {
}
}
}

// TASK-1973: workspace soft-delete recovery route wiring. The catalog
// advertises pad_workspace.deleted / .restore; without these route
// entries the HTTPHandlerDispatcher (cloud MCP) would return "not yet
// implemented over HTTP transport" for calls the tool surface promises.
func TestRouteTable_RegistersWorkspaceRecovery(t *testing.T) {
for _, key := range []string{"workspace deleted", "workspace restore"} {
if _, ok := routeTable[key]; !ok {
t.Errorf("routeTable missing entry for %q — cloud MCP dispatch would fail", key)
}
}
}

func TestWorkspaceDeletedRoute_HTTPMappingShape(t *testing.T) {
mapper := routeTable["workspace deleted"]
if mapper == nil {
t.Fatal("routeTable['workspace deleted'] is nil")
}
method, path, body, err := mapper(map[string]any{})
if err != nil {
t.Fatalf("mapper: %v", err)
}
if method != "GET" {
t.Errorf("method = %q, want GET", method)
}
if path != "/api/v1/workspaces/deleted" {
t.Errorf("path = %q, want /api/v1/workspaces/deleted", path)
}
if body != nil {
t.Errorf("body = %s, want nil", body)
}
}

func TestWorkspaceRestoreRoute_HTTPMappingShape(t *testing.T) {
mapper := routeTable["workspace restore"]
if mapper == nil {
t.Fatal("routeTable['workspace restore'] is nil")
}
// The deleted workspace is identified by the `slug` positional/input —
// it fills the {slug} path placeholder, not the session `workspace`.
method, path, body, err := mapper(map[string]any{"slug": "my-old-ws"})
if err != nil {
t.Fatalf("mapper: %v", err)
}
if method != "POST" {
t.Errorf("method = %q, want POST", method)
}
if path != "/api/v1/workspaces/my-old-ws/restore" {
t.Errorf("path = %q, want /api/v1/workspaces/my-old-ws/restore", path)
}
if body != nil {
t.Errorf("body = %s, want nil", body)
}
}

func TestWorkspaceRestoreRoute_RequiresSlug(t *testing.T) {
mapper := routeTable["workspace restore"]
if mapper == nil {
t.Fatal("routeTable['workspace restore'] is nil")
}
if _, _, _, err := mapper(map[string]any{}); err == nil {
t.Error("missing slug should error (path placeholder unfilled)")
}
}
15 changes: 15 additions & 0 deletions internal/mcp/dispatch_http_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,21 @@ func init() {
// /api/v1/oauth/claim with the same payload shape.
"workspace create": mapWorkspaceCreate,
"workspace claim": mapWorkspaceClaim,
// TASK-1973: workspace soft-delete recovery over MCP. `deleted`
// GETs the caller's soft-deleted workspaces still inside the
// restore window (no path params — the endpoint scopes to the
// requesting user). `restore` POSTs to the target workspace's
// {slug} (the deleted workspace, passed as the `slug` input —
// NOT the session `workspace` param) with an empty body. Mirrors
// the CLI client's /workspaces/deleted + /workspaces/{slug}/restore.
"workspace deleted": routeSpec{
method: http.MethodGet,
pathTemplate: "/api/v1/workspaces/deleted",
}.toRouteMapper(),
"workspace restore": routeSpec{
method: http.MethodPost,
pathTemplate: "/api/v1/workspaces/{slug}/restore",
}.toRouteMapper(),

// --- TASK-968 follow-up: project intelligence + admin extras ---
// `project next` returns the full dashboard JSON — same shape the
Expand Down
5 changes: 4 additions & 1 deletion internal/mcp/tool_surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ type toolSurfaceToolSummary struct {
// - pad_item: get/list/deps/starred/list-comments/backlinks/export read;
// create/update/delete/move/restore/link/unlink/star/unstar/
// comment/bulk-update/note/decide/import write.
// - pad_workspace: list/members/storage/audit-log read; invite/create/claim write.
// - pad_workspace: list/members/storage/audit-log/deleted read;
// invite/create/claim/restore write.
// - pad_collection: list read; create/update/delete write.
// - pad_project: all read (dashboard/next/standup/changelog/report).
// - pad_role: list read; create/update/delete write.
Expand All @@ -81,6 +82,8 @@ var readOnlyActions = map[string]map[string]bool{
"members": true,
"storage": true,
"audit-log": true,
// TASK-1973: restore is a write; deleted is read-only.
"deleted": true,
},
"pad_collection": {
"list": true,
Expand Down
15 changes: 13 additions & 2 deletions internal/mcp/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const CmdhelpVersion = "0.1"
// pagination. Pure addition; existing pad_item actions unchanged.
// Backwards-compatible for v0.5 consumers that don't enumerate
// the new action.
// - "0.7" — current. Artifact export/import (Phase 5): adds two
// - "0.7" — historical. Artifact export/import (Phase 5): adds two
// actions to `pad_item` mirroring the CLI `pad item export` /
// `pad item import`. `export` (read-only) takes `ref` and returns
// the portable artifact TEXT (YAML frontmatter + Markdown body) —
Expand All @@ -94,6 +94,17 @@ const CmdhelpVersion = "0.1"
// the `artifact` param to the vocabulary. Pure addition; existing
// pad_item actions unchanged. Backwards-compatible for v0.6
// consumers that don't enumerate the new actions.
// - "0.8" — current. TASK-1973: workspace soft-delete recovery.
// Adds two actions to `pad_workspace` mirroring the CLI
// `pad workspace deleted` / `pad workspace restore` (TASK-1972):
// `deleted` (read-only) lists the caller's soft-deleted workspaces
// still inside the 30-day restore window; `restore` (mutating, not
// destructive) un-soft-deletes a workspace by `slug` while it's
// still restorable (owner-only). Both non-interactive. Reuses the
// existing `slug` param (now also required for action=restore); no
// new params. Pure addition; existing pad_workspace actions
// unchanged. Backwards-compatible for v0.7 consumers that don't
// enumerate the new actions.
// - "0.4" — PLAN-1410: comprehensive bootstrap-payload
// trim, cutting ~40% of bytes off the AgentBootstrap response.
// Same tool catalog (still eight resource × action tools +
Expand Down Expand Up @@ -136,7 +147,7 @@ const CmdhelpVersion = "0.1"
// - result.capabilities.experimental.padToolSurface.version (handshake).
// - pad://_meta/version resource (queryable JSON document).
// - pad_meta.action: tool-surface (full catalog introspection).
const ToolSurfaceVersion = "0.7"
const ToolSurfaceVersion = "0.8"

// MetaVersionURI is the canonical URI of the queryable version document.
// Lives outside the pad://workspace/{ws}/... namespace because it's a
Expand Down