From afeebea2dad2f60e6998a31b9030c5394526253d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:17:38 +0200 Subject: [PATCH] fix(mcp): expose accepted workspace mode values (#3667) --- .../backendapp/mcp_workspace_mode_test.go | 25 +++ .../mcp/handlers/workspace_policy_test.go | 41 +++++ .../server/create_task_workspace_mode_test.go | 99 +++++++++++ apps/backend/internal/mcp/server/server.go | 2 +- docs/plans/mcp-workspace-mode-enum/plan.md | 108 ++++++++++++ .../task-01-expose-workspace-mode-enum.md | 156 ++++++++++++++++++ docs/public/automation-and-mcp.md | 4 + docs/public/coordination.md | 2 +- .../tasks/requirements/mcp-workspace-mode.md | 30 +++- .../tasks/system-design/mcp-workspace-mode.md | 61 ++++++- 10 files changed, 523 insertions(+), 5 deletions(-) create mode 100644 apps/backend/internal/mcp/handlers/workspace_policy_test.go create mode 100644 apps/backend/internal/mcp/server/create_task_workspace_mode_test.go create mode 100644 docs/plans/mcp-workspace-mode-enum/plan.md create mode 100644 docs/plans/mcp-workspace-mode-enum/task-01-expose-workspace-mode-enum.md diff --git a/apps/backend/internal/backendapp/mcp_workspace_mode_test.go b/apps/backend/internal/backendapp/mcp_workspace_mode_test.go index 2a7946af27..5e060a85fc 100644 --- a/apps/backend/internal/backendapp/mcp_workspace_mode_test.go +++ b/apps/backend/internal/backendapp/mcp_workspace_mode_test.go @@ -87,6 +87,31 @@ func TestExternalMCPTaskModesReachPersistenceAndManagement(t *testing.T) { httpServer := httptest.NewServer(router) t.Cleanup(httpServer.Close) + // @covers AC-TASKS-MCP-WORKSPACE-MODE-004.3 + t.Run("inheritance requires parent through MCP", func(t *testing.T) { + before, err := harness.taskSvc.ListTasks(ctx, kanbanWorkflows[0].ID) + require.NoError(t, err) + response := postExternalWorkspaceModeMCPRequest(t, httpServer.URL+"/mcp", 50, "create_task_kandev", map[string]any{ + "title": "Invalid root inheritance", + "workspace_id": kanbanWorkspace.ID, + "workflow_id": kanbanWorkflows[0].ID, + "workspace_mode": "inherit_parent", + "agent_profile_id": "profile-1", + "start_agent": false, + }) + require.Equal(t, http.StatusOK, response.StatusCode) + message := decodeExternalMCPResponse(t, response.Body) + require.NotContains(t, message, "error") + result, ok := message["result"].(map[string]any) + require.True(t, ok) + require.Equal(t, true, result["isError"]) + require.Contains(t, response.Body, "workspace_mode=inherit_parent requires parent_id") + require.NotContains(t, response.Body, "keyword: enum") + after, err := harness.taskSvc.ListTasks(ctx, kanbanWorkflows[0].ID) + require.NoError(t, err) + require.Equal(t, before, after) + }) + created := make(map[string]string, 2) for _, tc := range []struct { name, workspaceID, workflowID, title, externalID string diff --git a/apps/backend/internal/mcp/handlers/workspace_policy_test.go b/apps/backend/internal/mcp/handlers/workspace_policy_test.go new file mode 100644 index 0000000000..5adeef4275 --- /dev/null +++ b/apps/backend/internal/mcp/handlers/workspace_policy_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// @covers AC-TASKS-MCP-WORKSPACE-MODE-004.3 +func TestResolveMCPWorkspacePolicyCompatibility(t *testing.T) { + h := &Handlers{} + for _, tc := range []struct { + name, parent, mode, want, wantError string + }{ + {name: "root omission"}, + {name: "parent omission", parent: "parent", want: "inherit_parent"}, + {name: "root blank", mode: " \t"}, + {name: "parent blank", parent: "parent", mode: " \t", want: "inherit_parent"}, + {name: "inherit", parent: "parent", mode: "inherit_parent", want: "inherit_parent"}, + {name: "inherit needs parent", mode: "inherit_parent", wantError: "requires parent_id"}, + {name: "new root", mode: "new_workspace", want: "new_workspace"}, + {name: "new child", parent: "parent", mode: "new_workspace", want: "new_workspace"}, + // Direct backend policy handling retains trimming; MCP schema validation + // rejects padded input before it reaches this resolver. + {name: "trim inheritance", parent: "parent", mode: " inherit_parent ", want: "inherit_parent"}, + {name: "trim new", mode: " new_workspace ", want: "new_workspace"}, + {name: "shared", mode: "shared", wantError: "invalid workspace_mode"}, + {name: "shared group", mode: "shared_group", wantError: "invalid workspace_mode"}, + } { + t.Run(tc.name, func(t *testing.T) { + policy, err := h.resolveMCPWorkspacePolicy(tc.parent, tc.mode) + if tc.wantError != "" { + require.ErrorContains(t, err, tc.wantError) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, policy.Mode) + }) + } +} diff --git a/apps/backend/internal/mcp/server/create_task_workspace_mode_test.go b/apps/backend/internal/mcp/server/create_task_workspace_mode_test.go new file mode 100644 index 0000000000..cbd208e64f --- /dev/null +++ b/apps/backend/internal/mcp/server/create_task_workspace_mode_test.go @@ -0,0 +1,99 @@ +package mcp + +import ( + "encoding/json" + "testing" + + ws "github.com/kandev/kandev/pkg/websocket" + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// @covers AC-TASKS-MCP-WORKSPACE-MODE-004.1, AC-TASKS-MCP-WORKSPACE-MODE-004.2 +func TestCreateTaskWorkspaceModeSchema(t *testing.T) { + for _, mode := range []string{ModeTask, ModeExternal} { + t.Run(mode, func(t *testing.T) { + s := New(&testBackend{}, "", "", 10005, newTestLogger(t), "", false, mode) + tool, ok := s.mcpServer.ListTools()["create_task_kandev"] + require.True(t, ok) + catalog, err := json.Marshal(tool.Tool.InputSchema) + require.NoError(t, err) + for name, raw := range map[string][]byte{ + "catalog": catalog, "attachment": mcpToolInputSchema(tool.Tool), + } { + t.Run(name, func(t *testing.T) { + var schema struct { + Properties map[string]map[string]any + Required []string + } + require.NoError(t, json.Unmarshal(raw, &schema)) + property := schema.Properties["workspace_mode"] + require.NotNil(t, property) + assert.Equal(t, "string", property["type"]) + assert.Equal(t, []any{"inherit_parent", "new_workspace"}, property["enum"]) + assert.NotContains(t, schema.Required, "workspace_mode") + assert.NotContains(t, property, "default") + description, ok := property["description"].(string) + require.True(t, ok) + for _, phrase := range []string{ + "Omit for subtasks", "inherit_parent requires parent_id", + "reuses the parent's materialized workspace/worktree", + "new_workspace requests a separate workspace/worktree", + } { + assert.Contains(t, description, phrase) + } + }) + } + }) + } +} + +// @covers AC-TASKS-MCP-WORKSPACE-MODE-004.3, AC-TASKS-MCP-WORKSPACE-MODE-004.4 +func TestCreateTaskWorkspaceModeArguments(t *testing.T) { + for _, mode := range []string{ModeTask, ModeExternal} { + t.Run(mode, func(t *testing.T) { + for _, tc := range []struct { + name string + value string + omit bool + valid bool + }{ + {name: "omitted", omit: true, valid: true}, + {name: "inherit", value: "inherit_parent", valid: true}, + {name: "new", value: "new_workspace", valid: true}, + {name: "empty"}, + {name: "whitespace", value: " \t"}, + {name: "padded", value: " new_workspace "}, + {name: "shared", value: "shared"}, + {name: "shared group", value: "shared_group"}, + {name: "unknown", value: "unknown"}, + } { + t.Run(tc.name, func(t *testing.T) { + backend := &testBackend{response: map[string]any{"id": "child"}} + s := New(backend, "", "", 10005, newTestLogger(t), "", false, mode) + args := map[string]any{"title": "Child task", "parent_id": "parent", "start_agent": false} + if !tc.omit { + args["workspace_mode"] = tc.value + } + result := callTool(t, s, "create_task_kandev", args) + if !tc.valid { + require.True(t, result.IsError) + assert.Empty(t, backend.lastAction) + require.NotEmpty(t, result.Content) + content, ok := result.Content[0].(mcp.TextContent) + require.True(t, ok) + assert.Contains(t, content.Text, "/workspace_mode") + assert.Contains(t, content.Text, "keyword: enum") + return + } + require.False(t, result.IsError) + assert.Equal(t, ws.ActionMCPCreateTask, backend.lastAction) + payload, ok := backend.lastPayload.(map[string]any) + require.True(t, ok) + assert.Equal(t, tc.value, payload["workspace_mode"]) + }) + } + }) + } +} diff --git a/apps/backend/internal/mcp/server/server.go b/apps/backend/internal/mcp/server/server.go index 3bd6814034..dcd15f831e 100644 --- a/apps/backend/internal/mcp/server/server.go +++ b/apps/backend/internal/mcp/server/server.go @@ -1477,7 +1477,7 @@ func (s *Server) registerCreateTaskTool() { mcp.WithString("workspace_id", mcp.Description("The workspace ID. Auto-resolved if only one workspace exists. Defaulted from parent for subtasks when omitted.")), mcp.WithString("workflow_id", mcp.Description("The workflow ID. Auto-resolved if the workspace has only one workflow. Defaulted from parent for subtasks when workspace_id is also omitted; if supplied, it must belong to the effective workspace_id.")), mcp.WithString("workflow_step_id", mcp.Description("The workflow step ID (optional, auto-resolved if omitted; for subtasks, pass only with an explicit workflow_id)")), - mcp.WithString("workspace_mode", mcp.Description("Subtask materialized-workspace mode: inherit_parent reuses the parent's worktree/materialized workspace (default for subtasks); new_workspace launches the subtask in its own workspace/worktree.")), + mcp.WithString("workspace_mode", mcp.Enum("inherit_parent", "new_workspace"), mcp.Description("Optional materialized-workspace mode. Omit for subtasks to inherit the parent's workspace/worktree. inherit_parent requires parent_id and reuses the parent's materialized workspace/worktree; new_workspace requests a separate workspace/worktree.")), mcp.WithString("title", mcp.Required(), mcp.MaxLength(service.TaskTitleMaxLength), mcp.Description("A concise, few-word task title (maximum 60 characters).")), mcp.WithString("prompt", mcp.Description("The initial prompt for the task agent. This is the ONLY context the agent receives when it starts — treat it as the agent's first user message. For auto-started subtasks, provide a specific and detailed prompt; omitting it starts the task agent without task-specific context.")), mcp.WithBoolean("autopilot", mcp.Description("Start this task in autopilot mode. Default: false. The value is fixed at creation and is not inherited by subtasks. The agent does not ask the user directly; it asks its direct parent only for critical decisions.")), diff --git a/docs/plans/mcp-workspace-mode-enum/plan.md b/docs/plans/mcp-workspace-mode-enum/plan.md new file mode 100644 index 0000000000..48ccad0ad3 --- /dev/null +++ b/docs/plans/mcp-workspace-mode-enum/plan.md @@ -0,0 +1,108 @@ +--- +created: 2026-09-13 +status: implemented +requirements: + - REQ-TASKS-MCP-WORKSPACE-MODE-004 +system_design: + - ../../specs/tasks/system-design/mcp-workspace-mode.md +legacy_specs: [] +--- + +# Implementation plan: MCP workspace mode enum + +## Overview + +Expose the accepted materialization choices in `create_task_kandev` discovery. +One sequential work order delivers schema metadata, regression evidence, and +public caller guidance. This is a follow-up to the completed +[creation-admission package](../mcp-workspace-mode/plan.md); its earlier work +orders and verification results remain historical and are not reopened. + +## Scope + +In scope: the optional enum, conditional-default description, task/external +registration and propagation checks, input compatibility evidence, and docs. + +Out of scope: new workspace modes, handler normalization changes, execution +behavior, Office admission, custom prompts, UI, dependency changes, running +instance changes, delegation, and PR merge. + +## Technical approach + +Use `mcp.Enum` in `registerCreateTaskTool` alongside `mcp.WithString` and +`mcp.Description`. Leave required/default metadata absent. Preserve +`resolveMCPWorkspacePolicy`. See the +[requirements](../../specs/tasks/requirements/mcp-workspace-mode.md) and +[design's materialization section](../../specs/tasks/system-design/mcp-workspace-mode.md#materialization-schema-discoverability). + +The task system owns the task-creation input contract. The requested values and +optional/default semantics are confirmed by the task brief and local source. +No material design question remains. A new ADR is unnecessary: the existing +schema-validation ADR already defines enforcement, and the paired design +records this small field-level compatibility consequence. + +## Tests + +- AC 004.1/004.2: `TestCreateTaskWorkspaceModeSchema` in + `server/create_task_workspace_mode_test.go`, table-driven over task/external + modes, checks serialized catalog schema and `mcpToolInputSchema` output. +- AC 004.3/004.4: `TestCreateTaskWorkspaceModeArguments` drives the + existing `callTool` harness through schema validation into a recording + backend; failures must leave dispatch untouched. +- AC 004.3: existing handler tests + `TestHandleCreateTask_SubtaskDefaultsToParentWorkspaceAndWorkflow` and + `TestHandleCreateTask_SubtaskCanRequestNewWorkspaceMode`; proposed + `TestResolveMCPWorkspacePolicyCompatibility` in a dedicated handler test file + covers unchanged direct policy semantics. +- AC 004.3: `backendapp.TestExternalMCPTaskModesReachPersistenceAndManagement` + sends inheritance without a parent over real MCP HTTP, checks the backend + error rather than an enum failure, and verifies no task was created. + +AC suffixes refer to `AC-TASKS-MCP-WORKSPACE-MODE-`. +Go protocol/dispatch coverage is the end-to-end evidence for this MCP-only +change. No browser flow changes. + +## Documentation + +Updated `docs/public/automation-and-mcp.md` (reference) and +`docs/public/coordination.md` (coordination guide) to name exact choices, the +parent requirement, materialization semantics, and omission instead of blanks. +Searches found no +corresponding field guidance in the root README or screenshot catalog. + +## Work orders + +- [x] [01: Expose workspace mode enum](task-01-expose-workspace-mode-enum.md) + +No dependencies on parent checkout changes. Completed sequentially after the +user's explicit implementation request. + +## Verification results + +Completed on 2026-09-13. The schema and dispatch regressions failed before +the schema edit, then passed afterward. The direct handler compatibility +matrix and existing subtask default/new-workspace tests passed. Config/Office +exclusion and fixed automation catalog tests passed. Public-doc tests and +validation (46 pages), catalog validation (267 decisions/868 specs), +specification tests (36), specification lint, gofmt, and diff checks passed. + +All exact commands and results are recorded in the +[completed work order](task-01-expose-workspace-mode-enum.md#results). +Go verification used `GOCACHE=/tmp/kandev-workspace-enum-go-cache` because the +default cache is read-only in this workspace. Production changes are limited +to the optional enum and its description; backend validation/defaulting remain +unchanged. Requirements are active and the paired design is current. + +Review remediation on 2026-09-14 removed transient checkout/session status +from the delivery records. Documentation catalog validation, specification +lint, and `git diff --check` passed after that documentation-only correction. + +## Risks + +- Empty strings, whitespace-only strings, and padded values previously reached + the trimming handler but now fail MCP enum validation. Callers must omit + the key for defaulting; this narrowing must be explicit in delivery notes. +- A schema default would incorrectly imply inheritance for top-level tasks. +- Testing only description text misses missing enum metadata and optionality. +- Attachment size limits may omit whole schemas; no inspected transform strips + enum selectively. External client behavior is outside the local guarantee. diff --git a/docs/plans/mcp-workspace-mode-enum/task-01-expose-workspace-mode-enum.md b/docs/plans/mcp-workspace-mode-enum/task-01-expose-workspace-mode-enum.md new file mode 100644 index 0000000000..9dd0ec0dbe --- /dev/null +++ b/docs/plans/mcp-workspace-mode-enum/task-01-expose-workspace-mode-enum.md @@ -0,0 +1,156 @@ +--- +id: "01-expose-workspace-mode-enum" +title: "Expose workspace mode enum" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-TASKS-MCP-WORKSPACE-MODE-004 +acceptance_criteria: + - AC-TASKS-MCP-WORKSPACE-MODE-004.1 + - AC-TASKS-MCP-WORKSPACE-MODE-004.2 + - AC-TASKS-MCP-WORKSPACE-MODE-004.3 + - AC-TASKS-MCP-WORKSPACE-MODE-004.4 +system_design: + - ../../specs/tasks/system-design/mcp-workspace-mode.md +--- + +# Task 01: Expose workspace mode enum + +## Summary + +Advertise supported materialization choices and prove the enum survives the +shared task/external registration. Document the stricter MCP input boundary +without changing backend policy handling. + +## In scope + +- Follow `/tdd`: write the schema and wrapped-call regression first, confirm + missing-enum failures, then add the enum/description and rerun. +- Serialize the registered tool and attachment schema in task/external modes; + assert string type, exact ordered enum, optionality, no default, and the + required description semantics. Reuse `newTaskModeServer`, `New`, and + `mcpToolInputSchema`; do not introduce a second schema implementation. +- Drive accepted values and omission through `callTool` with a recording + backend. Reject `shared`, `shared_group`, empty/whitespace strings, padded + values, and an unknown value without backend dispatch. Use fresh backends + per case. Do not mistake a recording backend for proof of parent validation. +- Add a direct `resolveMCPWorkspacePolicy` matrix for omission/blank with and + without parent, both modes, explicit inheritance without parent, whitespace + trimming, and unsupported values. Keep production handler code untouched. +- Update the two public pages and record compatibility and validation results. + +## Out of scope + +New modes, normalization exceptions, execution changes, Office tools, UI, +custom prompts, running instances, parent changes, subagents, and PR merge. + +## Acceptance + +1. Both catalogs and attachment schema expose the exact optional enum without + a default, with materialization and parent-dependent omission guidance. +2. Wrapped-call tests prove accepted and rejected schema input; direct handler + tests preserve defaulting, trimming, and the parent requirement. +3. Public docs and delivery results explicitly describe blank-input narrowing; + all focused tests and document checks pass. + +## Verification + +Run from repository root; each Go command has its own working directory: + +```bash +(cd apps/backend && GOCACHE=/tmp/kandev-workspace-enum-go-cache go test ./internal/mcp/server -run 'TestCreateTaskWorkspaceMode|TestCreateTask_ToolSchema_HasParentID|TestToolArgumentValidation|TestMCPAttachmentObserverPublishesStructuredInputSchema' -count=1) +(cd apps/backend && GOCACHE=/tmp/kandev-workspace-enum-go-cache go test ./internal/mcp/handlers -run 'TestResolveMCPWorkspacePolicyCompatibility|TestHandleCreateTask_Subtask(DefaultsToParentWorkspaceAndWorkflow|CanRequestNewWorkspaceMode)$' -count=1) +(cd apps/backend && GOCACHE=/tmp/kandev-workspace-enum-go-cache go test ./internal/mcp/server -run 'TestServerMode(Config|Office)_RegistersCorrectTools|TestServerSurfaceAutomationHasFixedCoordinatorCatalog' -count=1) +(cd apps/backend && GOCACHE=/tmp/kandev-workspace-enum-go-cache go test ./internal/backendapp -run TestExternalMCPTaskModesReachPersistenceAndManagement -count=1) +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +python3 scripts/list-docs.py validate +python3 scripts/lint-spec-files.test.py +python3 scripts/lint-spec-files.py --all +git diff --check +``` + +Run the new schema test before production edits to establish red, then all +commands above after implementation. If Go caches need a writable location, +set `GOCACHE` to an isolated directory under `/tmp`. + +## Files likely touched + +- `apps/backend/internal/mcp/server/server.go` +- `apps/backend/internal/mcp/server/create_task_workspace_mode_test.go` (new) +- `apps/backend/internal/mcp/handlers/workspace_policy_test.go` (new) +- `docs/public/automation-and-mcp.md`, `docs/public/coordination.md` +- This plan/work order and paired requirements/design status/results. + +## Dependencies + +None. Existing admission package is implemented in this checkout. + +## Risks + +Enum validation precedes handler trimming and rejects formerly accepted blank +and padded values. Preserve this documented consequence; do not silently +add normalization. Existing large `handlers_test.go` files should not grow. + +## Parallelism + +`sequential`; no delegation authorized. + +## Inputs + +- [Requirements](../../specs/tasks/requirements/mcp-workspace-mode.md), REQ 004. +- [System design](../../specs/tasks/system-design/mcp-workspace-mode.md), + materialization schema discoverability and compatibility sections. +- `server/tool_argument_validation.go`, `server/tool_argument_validation_test.go`, + `server/handlers_test.go`, and `handlers/handlers.go`. + +## Results + +Completed 2026-09-13 after the explicit implementation request. + +- Red: `go test ./internal/mcp/server -run TestCreateTaskWorkspaceMode -count=1` + with the isolated cache failed on missing enum/description metadata and + out-of-enum values reaching the recording backend. +- Green: both focused Go commands in Verification passed after the single + production schema edit. Both enum values and omission dispatch correctly; + blank/unsupported values fail at the enum boundary. Catalog and attachment + serialization retain the enum and optionality. The direct handler matrix + and existing parent-default/new-workspace tests passed unchanged. +- The additional catalog command passed, preserving config/Office exclusions + and the fixed automation catalog. +- `node --test scripts/validate-public-docs.test.mjs`: passed. +- `node scripts/validate-public-docs.mjs`: passed, 46 pages. +- `python3 scripts/list-docs.py validate`: passed, 267 decisions and 868 specs. +- `python3 scripts/lint-spec-files.test.py`: passed, 36 tests. +- `python3 scripts/lint-spec-files.py --all`: passed. +- `gofmt -l` on the three changed/new Go files: no output. +- `git diff --check`: passed. + +Both public pages now explain exact values and omission instead of blank +strings. The handler production code is unchanged. Empty, whitespace-only, +and padded inputs now fail MCP validation as documented in the design. +No observed registration, validator, or attachment transform strips enum +metadata. + +Review remediation on 2026-09-14 removed stale uncommitted-state claims and +transient session status from this work order and its plan. Revalidated with +`python3 scripts/list-docs.py validate`, +`python3 scripts/lint-spec-files.py --all`, and `git diff --check`; all passed. +This correction changes documentation only; the implementation and test +commands above remain unchanged. + +Claude review follow-up adds a real external MCP HTTP composition case in +`backendapp/mcp_workspace_mode_test.go`: an exact `inherit_parent` value +without `parent_id` passes schema validation, returns the backend parent +requirement error, and leaves persisted tasks unchanged. Direct policy trim +cases now explain the backend-versus-MCP boundary without implying that raw +WebSocket clients can invoke MCP actions. This is test-only coverage of +existing behavior, so no production implementation changed. + +Follow-up validation: the external MCP composition test and direct resolver +test passed, as did documentation catalog validation, specification lint, and +`git diff --check`. The HTTP test required local socket permission; its first +sandboxed attempt failed to bind a listener, and the socket-enabled retry +passed. The new case covers existing behavior and required no production fix. diff --git a/docs/public/automation-and-mcp.md b/docs/public/automation-and-mcp.md index 1a0ff0eea0..5e9aef986c 100644 --- a/docs/public/automation-and-mcp.md +++ b/docs/public/automation-and-mcp.md @@ -32,6 +32,10 @@ Task creation depends on the caller surface and the destination workspace: Office sessions do not receive an MCP task-creation tool. A direct backend call from an Office session is also denied. External MCP uses the same `create_task_kandev` contract for both workspace modes, subject to the client's authorization. The `workspace_mode` argument controls materialized workspace behavior, not Kanban or Office mode. `agent_profile_id` selects a launch profile and is not an Office assignee. +The optional `workspace_mode` field advertises exactly two values: `inherit_parent` reuses the parent's materialized workspace/worktree and requires `parent_id`; `new_workspace` requests a separate workspace/worktree. Omitting the field for a subtask selects `inherit_parent`. There is no unconditional schema default for top-level tasks. + +Omit `workspace_mode` to use defaulting instead of sending an empty string. MCP schema validation rejects empty, whitespace-only, and padded values before backend dispatch, even though the backend policy resolver still trims strings. `shared` and `shared_group` are not supported by this MCP endpoint. + ## Quick path - Use a **workflow event** for predictable transitions on existing tasks. diff --git a/docs/public/coordination.md b/docs/public/coordination.md index fa34163a9c..78ef9f56ee 100644 --- a/docs/public/coordination.md +++ b/docs/public/coordination.md @@ -115,7 +115,7 @@ Detaching changes task hierarchy only. An inherited workspace remains shared wit ### Create a subtask from an agent -Call `create_task_kandev` with `parent_id: "self"`. `workspace_mode` defaults to `inherit_parent`; set it to `new_workspace` for isolated materialization. +Call `create_task_kandev` with `parent_id: "self"`. Omit `workspace_mode` to inherit the parent's materialized workspace/worktree, or set it to `new_workspace` for a separate workspace/worktree. The only explicit values are `inherit_parent` and `new_workspace`; `inherit_parent` requires `parent_id`. Send an exact value or omit the field: empty, whitespace-only, and padded strings fail MCP schema validation. - `start_agent` defaults to `true`. Supply a detailed `prompt` when it is true; if omitted, Kandev still starts the agent without task-specific instructions. Set it to `false` for a placeholder task. - `autopilot` defaults to `false`. Set it to `true` to start an autonomous task. This choice is immutable and is not inherited by subtasks. An autopilot child receives `ask_parent_question_kandev` instead of `ask_user_question_kandev`; an autopilot root receives neither question tool. See [Agent Communication](agent-communication.md#autopilot-parent-questions) for the answer protocol. diff --git a/docs/specs/tasks/requirements/mcp-workspace-mode.md b/docs/specs/tasks/requirements/mcp-workspace-mode.md index c5c5d84055..c11b12808d 100644 --- a/docs/specs/tasks/requirements/mcp-workspace-mode.md +++ b/docs/specs/tasks/requirements/mcp-workspace-mode.md @@ -1,5 +1,5 @@ --- -status: draft +status: active system: tasks created: 2026-09-11 owners: @@ -98,11 +98,36 @@ skills and CLI, with no Office task-creation MCP tool. shall remain indistinguishable. Documentation shall distinguish session MCP, Office skills/CLI, external MCP, and materialized-workspace policy. +### REQ-TASKS-MCP-WORKSPACE-MODE-004: Discoverable materialization choices + +**Intent:** Let MCP callers select a supported materialized-workspace policy +from the advertised contract. + +#### Acceptance criteria + +- **AC-TASKS-MCP-WORKSPACE-MODE-004.1:** Task and external MCP catalogs shall + advertise optional string `workspace_mode` with exactly the ordered enum + `["inherit_parent", "new_workspace"]` and no schema default. +- **AC-TASKS-MCP-WORKSPACE-MODE-004.2:** The field description shall explain + that omission for a subtask inherits its parent's materialized workspace, + explicit `inherit_parent` requires `parent_id` and reuses that workspace or + worktree, and `new_workspace` requests a separate workspace or worktree. +- **AC-TASKS-MCP-WORKSPACE-MODE-004.3:** Omitted input shall retain existing + parent-dependent defaulting. Both explicit enum values shall reach existing + backend validation, including the parent requirement for `inherit_parent`. + No workspace modes or execution behavior shall be added. +- **AC-TASKS-MCP-WORKSPACE-MODE-004.4:** MCP schema validation shall reject + out-of-enum input before backend dispatch, including `shared`, `shared_group`, + empty strings, whitespace-only strings, and padded mode names. Public caller + guidance shall explain using omission instead of blank input for defaulting. + Backend handling of blank values shall remain unchanged. + ## Exclusions - New Office MCP tools or external assignment fields. - Office CLI, permission, scheduler, or lifecycle redesign. -- Changes to external creation schemas, launch behavior, or idempotency contracts. +- External creation schema changes beyond materialization enum metadata and its + description; changes to launch behavior or idempotency contracts. - Workspace conversion, historical migration, rendered UI, or new endpoints. - Changes to automation/configuration catalogs or their established authority. @@ -110,3 +135,4 @@ skills and CLI, with no Office task-creation MCP tool. - [System design](../system-design/mcp-workspace-mode.md) - [Implementation plan](../../../plans/mcp-workspace-mode/plan.md) +- [Enum discoverability follow-up](../../../plans/mcp-workspace-mode-enum/plan.md) diff --git a/docs/specs/tasks/system-design/mcp-workspace-mode.md b/docs/specs/tasks/system-design/mcp-workspace-mode.md index 59ecdd99eb..82fc30bfa6 100644 --- a/docs/specs/tasks/system-design/mcp-workspace-mode.md +++ b/docs/specs/tasks/system-design/mcp-workspace-mode.md @@ -1,10 +1,11 @@ --- -status: draft +status: current system: tasks requirements: - REQ-TASKS-MCP-WORKSPACE-MODE-001 - REQ-TASKS-MCP-WORKSPACE-MODE-002 - REQ-TASKS-MCP-WORKSPACE-MODE-003 + - REQ-TASKS-MCP-WORKSPACE-MODE-004 --- # MCP task workspace mode design @@ -35,6 +36,7 @@ them. | REQ-TASKS-MCP-WORKSPACE-MODE-001 | Trusted admission; Destination resolution | | REQ-TASKS-MCP-WORKSPACE-MODE-002 | Surface contract | | REQ-TASKS-MCP-WORKSPACE-MODE-003 | External compatibility; Errors | +| REQ-TASKS-MCP-WORKSPACE-MODE-004 | Materialization schema discoverability | ## Surface contract @@ -140,6 +142,63 @@ No migration, mode column, historical rewrite, new endpoint, or UI is planned. Update public reference documentation when the gate is implemented. The earlier proposal for a separate Office MCP creation tool is withdrawn. +## Materialization schema discoverability + +The task system owns this addition because the field is a task-creation +contract. Workspace lifecycle, transport discovery guidance, and Office +admission remain with their existing owners. + +In `internal/mcp/server/server.go`, `registerCreateTaskTool` adds +`mcp.Enum("inherit_parent", "new_workspace")` to the existing +`mcp.WithString("workspace_mode", ...)` in `registerCreateTaskTool`. +The pinned `mcp-go` v1.0.0-beta.1 supports this property option; existing +`delivery_mode` and profile-session-policy fields use it. Do not add +`mcp.Required()` or a default. Both `ModeTask` (through `registerKanbanTools`) +and `ModeExternal` use this registration. + +Field description: "Optional materialized-workspace mode. Omit for +subtasks to inherit the parent's workspace/worktree. inherit_parent requires +parent_id and reuses the parent's materialized workspace/worktree; +new_workspace requests a separate workspace/worktree." + +Keep `resolveMCPWorkspacePolicy` in `internal/mcp/handlers/handlers.go` +unchanged. It trims input, defaults blank with a parent to `inherit_parent`, +leaves blank root policy unspecified, rejects explicit inheritance without a +parent, and accepts only the two explicit modes. Do not advertise the broader +service's `shared_group` or an invented `shared` alias. + +### Compatibility and schema propagation + +`compileToolArgumentSchema` marshals the registered schema and only adds root +`additionalProperties: false`; it preserves enum metadata. Validation runs +before dispatch. `normalizeToolArguments` only handles the legacy prompt alias, +so the enum rejects empty, whitespace-only, and padded strings that +previously reached the trimming handler. This is a real MCP input narrowing, +not merely a description improvement. Callers should omit the key to request +defaulting, or send an exact enum value. Do not introduce normalization or alter +the backend to conceal this effect. This follows the existing +[schema validation decision](../../../decisions/2026-08-01-validate-mcp-tool-arguments.md). + +`mcpToolInputSchema` preserves raw schemas or marshals structured schemas for +attachment evidence. `cloneValidMCPInputSchema` in +`internal/agentctl/types/streams/mcp_attachment.go` copies valid JSON unchanged; +size limits can drop a whole schema with a truncation marker, not individual +enum keywords. No enum-stripping transform was found in the inspected MCP +registration, validator, or attachment paths. Regression tests verify serialized +catalog and attachment schemas; this does not prove behavior of arbitrary +external clients. + +Schema and wrapped-call tests in `server/create_task_workspace_mode_test.go` +cover task/external registration, exact enum order, optionality, absent default, +description semantics, both accepted values, omission, and rejected +blank/unsupported inputs without dispatch. The direct policy matrix in +`handlers/workspace_policy_test.go` and existing handler tests preserve root, +parent, trimming, and rejection behavior. The public MCP reference and +coordination guide document omission and blank-input compatibility. +No rendered UI changes or browser test are needed. + +- [Enum implementation plan](../../../plans/mcp-workspace-mode-enum/plan.md) + ## Related contracts - [Workspace-owned mode](../../../decisions/2026-08-15-office-mode-follows-active-workspace.md)