fix: report effective spawn session profile (#2561) (#2566)

This commit is contained in:
Carlos Florêncio
2026-08-12 16:52:19 +01:00
committed by GitHub
parent e014f8072f
commit ff9f0f48bc
13 changed files with 455 additions and 18 deletions
@@ -31,16 +31,17 @@ type fakeOrchestrator struct {
queue *messagequeue.Service
promptCalls []promptCall
startCreatedCalls []startCreatedCall
resumeCalls int
turnStartCalls []turnStartCall
onTurnStart func(context.Context, string, string) error
interruptCalls []interruptCall
launchCalls []*orchestrator.LaunchSessionRequest
launchErr error
renameCalls []renameCall
renameErr error
promptCalls []promptCall
startCreatedCalls []startCreatedCall
resumeCalls int
turnStartCalls []turnStartCall
onTurnStart func(context.Context, string, string) error
interruptCalls []interruptCall
launchCalls []*orchestrator.LaunchSessionRequest
launchErr error
launchResponseProfileID string
renameCalls []renameCall
renameErr error
// Configurable: error returned by PromptTask. Cleared after first call so
// the retry-after-resume path can succeed on the second call.
@@ -99,12 +100,18 @@ func (f *fakeOrchestrator) LaunchSession(_ context.Context, req *orchestrator.La
if f.launchErr != nil {
return nil, f.launchErr
}
return &orchestrator.LaunchSessionResponse{
response := &orchestrator.LaunchSessionResponse{
Success: true,
TaskID: req.TaskID,
SessionID: "spawned-sess-1",
State: "STARTING",
}, nil
}
profileID := req.AgentProfileID
if f.launchResponseProfileID != "" {
profileID = f.launchResponseProfileID
}
response.AgentProfileID = profileID
return response, nil
}
func (f *fakeOrchestrator) PromptTask(_ context.Context, taskID, sessionID, prompt, _ string, _ bool, _ []v1.MessageAttachment, dispatchOnly bool) (*orchestrator.PromptResult, error) {
@@ -67,7 +67,7 @@ func (h *Handlers) handleSpawnSession(ctx context.Context, msg *ws.Message) (*ws
"task_id": req.TaskID,
"session_id": resp.SessionID,
"state": resp.State,
"agent_profile_id": profileID,
"agent_profile_id": resp.AgentProfileID,
})
}
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -12,6 +13,8 @@ import (
"github.com/kandev/kandev/internal/orchestrator"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/service"
workflowmodels "github.com/kandev/kandev/internal/workflow/models"
v1 "github.com/kandev/kandev/pkg/api/v1"
ws "github.com/kandev/kandev/pkg/websocket"
)
@@ -106,6 +109,89 @@ func TestHandleSpawnSession_ExplicitProfile(t *testing.T) {
assert.Empty(t, orch.renameCalls)
}
func TestHandleSpawnSessionReportsEffectiveAgentProfile(t *testing.T) {
svc, repo := newTestTaskService(t)
_, target, sess := seedTaskWithSession(t, svc, repo, models.TaskSessionStateRunning)
h, orch := newMessageTaskHandler(t, svc)
orch.launchResponseProfileID = "workflow-default-profile"
payload := spawnPayload(target.ID, "work with the workflow profile", target.ID, sess.ID)
payload["agent_profile_id"] = "requested-profile"
msg := makeWSMessage(t, ws.ActionMCPSpawnSession, payload)
resp, err := h.handleSpawnSession(context.Background(), msg)
require.NoError(t, err)
require.Equal(t, ws.MessageTypeResponse, resp.Type)
var out map[string]interface{}
require.NoError(t, json.Unmarshal(resp.Payload, &out))
assert.Equal(t, "workflow-default-profile", out["agent_profile_id"])
assert.Equal(t, "requested-profile", orch.launchCalls[0].AgentProfileID)
}
func TestHandleSpawnSessionReportsStepPinnedAgentProfile(t *testing.T) {
svc, repo, _, workflowRepo := newTestTaskServiceWithWorkflow(t)
ctx := context.Background()
workspace, workflow := defaultWorkspaceAndWorkflow(t, ctx, svc)
workflowProfileID := "workflow-default-profile"
_, err := svc.UpdateWorkflow(ctx, workflow.ID, &service.UpdateWorkflowRequest{
AgentProfileID: &workflowProfileID,
})
require.NoError(t, err)
step := seedWorkflowStep(t, ctx, workflowRepo, &workflowmodels.WorkflowStep{
WorkflowID: workflow.ID,
Name: "Pinned",
Position: 0,
IsStartStep: true,
AgentProfileID: "step-pinned-profile",
AllowManualMove: true,
})
now := time.Now().UTC()
target := &models.Task{
ID: "task-pinned-target",
WorkspaceID: workspace.ID,
WorkflowID: workflow.ID,
WorkflowStepID: step.ID,
Title: "Target task",
State: v1.TaskStateInProgress,
CreatedAt: now,
UpdatedAt: now,
}
sender := &models.Task{
ID: "task-pinned-sender",
WorkspaceID: workspace.ID,
WorkflowID: workflow.ID,
Title: "Sender task",
State: v1.TaskStateInProgress,
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, repo.CreateTask(ctx, target))
require.NoError(t, repo.CreateTask(ctx, sender))
session := &models.TaskSession{
ID: "sess-pinned",
TaskID: target.ID,
AgentProfileID: "sender-profile",
IsPrimary: true,
State: models.TaskSessionStateRunning,
}
require.NoError(t, repo.CreateTaskSession(ctx, session))
h, orch := newMessageTaskHandler(t, svc, repo)
orch.launchResponseProfileID = step.AgentProfileID
payload := spawnPayload(target.ID, "use the pinned profile", sender.ID, session.ID)
payload["agent_profile_id"] = "explicit-profile"
msg := makeWSMessage(t, ws.ActionMCPSpawnSession, payload)
resp, err := h.handleSpawnSession(ctx, msg)
require.NoError(t, err)
require.Equal(t, ws.MessageTypeResponse, resp.Type)
var out map[string]interface{}
require.NoError(t, json.Unmarshal(resp.Payload, &out))
assert.Equal(t, "step-pinned-profile", out["agent_profile_id"])
assert.Equal(t, "explicit-profile", orch.launchCalls[0].AgentProfileID)
}
// Cross-task spawns (sender session belongs to another task) fall back to the
// target task's primary-session profile.
func TestHandleSpawnSession_CrossTask_DefaultsToTargetPrimaryProfile(t *testing.T) {
+4 -2
View File
@@ -974,9 +974,11 @@ Unlike create_task_kandev this does NOT create a new task — the new session ru
The spawned session knows it was spawned by you and can reply via message_task_kandev using your task_id + session_id. You can message it the same way using the session_id returned by this tool.
Returns {task_id, session_id, state}.`),
The returned agent_profile_id is the effective agent profile used by the new session. On a workflow step, the step's launch profile wins: a pinned step profile outranks the requested agent_profile_id, and an unpinned step uses the workflow default. Without a workflow launch profile, an explicit agent_profile_id wins; when omitted, the existing inheritance rules apply.
Returns {task_id, session_id, state, agent_profile_id}.`),
mcp.WithString("prompt", mcp.Required(), mcp.Description("The spawned session's initial prompt. This is the ONLY context the new agent receives — be specific and detailed.")),
mcp.WithString("agent_profile_id", mcp.Description("Agent profile for the new session. Omit to reuse your own session's agent profile.")),
mcp.WithString("agent_profile_id", mcp.Description("Requested agent profile for the new session. Omit to inherit your session's profile; a workflow launch profile may override it.")),
mcp.WithString("name", mcp.Description("Optional session name shown on the session tab (e.g. 'reviewer'). Helps the user tell concurrent sessions apart.")),
mcp.WithString("task_id", mcp.Description("Task to spawn the session on. Omit to use your current task.")),
),
@@ -0,0 +1,24 @@
package mcp
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSpawnSessionToolDescribesEffectiveAgentProfile(t *testing.T) {
s := newTaskModeServer(t, &testBackend{}, "task-current")
tool, ok := s.mcpServer.ListTools()["spawn_session_kandev"]
require.True(t, ok, "spawn_session_kandev must be registered in task mode")
description := tool.Tool.Description
assert.Contains(t, description, "agent_profile_id")
assert.Contains(t, description, "effective agent profile")
assert.Contains(t, description, "Returns {task_id, session_id, state, agent_profile_id}")
profileProperty, ok := toolInputProperties(t, s, "spawn_session_kandev")["agent_profile_id"].(map[string]interface{})
require.True(t, ok, "agent_profile_id input must be described")
assert.Contains(t, profileProperty["description"], "workflow launch profile may override it")
}
@@ -91,6 +91,7 @@ type LaunchSessionResponse struct {
TaskID string `json:"task_id"`
SessionID string `json:"session_id,omitempty"`
AgentExecutionID string `json:"agent_execution_id,omitempty"`
AgentProfileID string `json:"agent_profile_id,omitempty"`
State string `json:"state"`
WorktreePath *string `json:"worktree_path,omitempty"`
WorktreeBranch *string `json:"worktree_branch,omitempty"`
@@ -415,6 +416,7 @@ func executionToLaunchResponse(taskID string, exec *executor.TaskExecution) *Lau
TaskID: taskID,
SessionID: exec.SessionID,
AgentExecutionID: exec.AgentExecutionID,
AgentProfileID: exec.AgentProfileID,
State: string(exec.SessionState),
}
if exec.WorktreePath != "" {
@@ -8,12 +8,25 @@ import (
"time"
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
"github.com/kandev/kandev/internal/orchestrator/executor"
"github.com/kandev/kandev/internal/task/models"
sqliterepo "github.com/kandev/kandev/internal/task/repository/sqlite"
wfmodels "github.com/kandev/kandev/internal/workflow/models"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
func TestExecutionToLaunchResponseReportsAgentProfile(t *testing.T) {
response := executionToLaunchResponse("task-1", &executor.TaskExecution{
SessionID: "session-1",
AgentExecutionID: "execution-1",
AgentProfileID: "effective-profile",
SessionState: v1.TaskSessionStateRunning,
})
if got, want := response.AgentProfileID, "effective-profile"; got != want {
t.Fatalf("agent_profile_id = %v, want %v", got, want)
}
}
func TestResolveIntent(t *testing.T) {
tests := []struct {
name string
@@ -0,0 +1,93 @@
---
spec: docs/specs/spawn-session-effective-profile/spec.md
created: 2026-08-12
status: complete
---
# Implementation Plan: Spawn Session Effective Agent Profile
## Overview
`spawn_session_kandev` sends a requested profile to `LaunchSession`, then reports that request value. The orchestrator can replace it with the workflow launch profile.
The launch path already retains the effective profile in `executor.TaskExecution`. The fix adds it to `LaunchSessionResponse` and uses that value in the MCP response.
## Confirmed root cause
`resolveSpawnAgentProfile` selects the pre-launch profile. `handleSpawnSession` reports that value after `LaunchSession` returns.
`startTask` can replace that value through `resolveEffectiveAgentProfile`. `executionToLaunchResponse` drops the resulting `TaskExecution.AgentProfileID`, so the MCP handler cannot report it.
`create_task_kandev` does not need a code change. PR #2341 already aligns its stored and reported profile with workflow-default precedence.
## Backend
### Authoritative launch response
- Update `apps/backend/internal/orchestrator/session_launch.go`.
- Add `AgentProfileID` to `LaunchSessionResponse`.
- Copy `TaskExecution.AgentProfileID` in `executionToLaunchResponse`.
- Keep launch precedence and session persistence unchanged.
### MCP response and tool contract
- Update `apps/backend/internal/mcp/handlers/spawn_session.go`.
- Build `agent_profile_id` from `LaunchSessionResponse.AgentProfileID`.
- Update `apps/backend/internal/mcp/server/server.go`.
- State workflow profile precedence and the full success response shape.
## Tests
- **What:** The launch response carries the effective profile from `TaskExecution`.
**File:** `apps/backend/internal/orchestrator/session_launch_test.go`.
**How:** Pass a `TaskExecution` with an effective profile to `executionToLaunchResponse`.
- **What:** The MCP handler reports the launch result when it differs from the requested profile.
**File:** `apps/backend/internal/mcp/handlers/spawn_session_test.go`.
**How:** Use the real task service and SQLite repository for target authorization. Return a different effective profile from the fake launcher.
- **What:** The registered tool describes workflow precedence and the `agent_profile_id` response field.
**File:** `apps/backend/internal/mcp/server/spawn_session_tool_test.go`.
**How:** Inspect the registered tool description without adding tests to existing files above the test-file size limit.
- **What:** The existing `create_task_kandev` workflow-default regressions remain green.
**File:** `apps/backend/internal/mcp/handlers/handlers_test.go`.
**How:** Run the focused tests from PR #2341 without changing them.
## Public documentation
Update `docs/public/agent-communication.md`, which is the explanation page for task-agent messages and related MCP tools. Add the effective profile to both documented response shapes and explain workflow precedence.
No frontend or E2E change is required. The change affects an MCP response and its documentation, not a visual UI flow.
## Verification Results
- Task 01 red regressions failed as expected: the launch response omitted the
effective profile, the MCP handler returned the requested profile, and the
tool description omitted the effective response field.
- Task 01 exact verification passed: 6 tests across the orchestrator, MCP
handlers, and MCP server packages.
- Affected backend package verification passed: `go test ./internal/orchestrator
./internal/mcp/handlers ./internal/mcp/server -count=1`.
- Full backend verification passed: `make -C apps/backend test` ran
`CGO_ENABLED=1 go test -tags fts5 ./...` successfully.
- Backend lint passed: `make -C apps/backend lint` reported no issues.
- Task 02 public-doc verification passed: 60 validation tests and 41 published
pages validated.
- Formatting and whitespace checks passed: `gofmt -l` reported no files and
`git diff --check` reported no errors.
## Implementation Waves And Parallel Candidates
Wave 1:
- [x] [Task 01: Propagate the effective profile](task-01-propagate-effective-profile.md)
Wave 2:
- [x] [Task 02: Update public MCP documentation](task-02-update-public-docs.md)
Execution is sequential. The documentation task depends on the final response contract from Task 01.
## Risks and limits
- `LaunchSessionResponse` serves several launch intents. The new field is additive and comes from the existing execution record.
- `spawn_session_kandev` always uses `IntentStart`, so its successful response has an effective execution profile.
- This work does not change profile precedence. It only reports the result of existing precedence.
@@ -0,0 +1,84 @@
---
id: "01-propagate-effective-profile"
title: "Propagate the effective profile"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/spawn-session-effective-profile/spec.md"
---
# Task 01: Propagate the effective profile
## Intent
Make the synchronous launch result authoritative for the profile that `spawn_session_kandev` reports.
## Inputs
- The spec `What`, `API surface`, and `Scenarios` sections.
- The confirmed root cause in `plan.md`.
- The response propagation pattern in `executionToLaunchResponse`.
- The existing handler test setup in `spawn_session_test.go` and `message_task_test.go`.
## Acceptance
- `LaunchSessionResponse.AgentProfileID` equals the effective profile in `TaskExecution` for a successful start.
- `spawn_session_kandev` reports that effective profile, including workflow-default and step-pinned overrides.
- The tool description states the precedence and includes `agent_profile_id` in its response shape.
- Existing `create_task_kandev` workflow-default tests remain green.
## Files likely touched
- `apps/backend/internal/orchestrator/session_launch.go`
- `apps/backend/internal/orchestrator/session_launch_test.go`
- `apps/backend/internal/mcp/handlers/spawn_session.go`
- `apps/backend/internal/mcp/handlers/spawn_session_test.go`
- `apps/backend/internal/mcp/handlers/message_task_test.go`
- `apps/backend/internal/mcp/server/server.go`
- `apps/backend/internal/mcp/server/spawn_session_tool_test.go`
## TDD sequence
1. Add the launch-response and MCP-response regressions.
2. Run the focused command and make sure that the new regressions fail for the expected missing field or echoed value.
3. Add the response field and propagate the effective value.
4. Update the tool description and its contract test.
5. Run the same focused command and make sure that all selected tests pass.
## Verification
```bash
cd "$(git rev-parse --show-toplevel)/apps/backend" && go test ./internal/orchestrator ./internal/mcp/handlers ./internal/mcp/server -run 'TestExecutionToLaunchResponseReportsAgentProfile|TestHandleSpawnSessionReports(Effective|StepPinned)AgentProfile|TestSpawnSessionToolDescribesEffectiveAgentProfile|TestResolveMCPAutoStartConfig_WorkflowDefaultOutranksExplicit(OnUnpinnedStep|WhenStepOmitted)' -count=1
```
## Dependencies
None.
## Parallelism
`sequential`. This task changes the shared launch response contract and its MCP consumer.
## Output contract
Report the files changed, the red and green test results, blockers, and risks. Update this task and `plan.md` in the same conversation.
## Results
- Red: the launch-response and tool-description regressions failed because the
response omitted `agent_profile_id`; the handler regression failed because it
returned the requested profile instead of the effective launch result.
- Green: `go test ./internal/orchestrator ./internal/mcp/handlers
./internal/mcp/server -run 'TestExecutionToLaunchResponseReportsAgentProfile|TestHandleSpawnSessionReportsEffectiveAgentProfile|TestSpawnSessionToolDescribesEffectiveAgentProfile|TestResolveMCPAutoStartConfig_WorkflowDefaultOutranksExplicit(OnUnpinnedStep|WhenStepOmitted)' -count=1`
passed 5 tests in 3 packages.
- Fixup regression extension: the focused command with
`TestHandleSpawnSessionReportsStepPinnedAgentProfile` passed 6 tests in 3
packages.
- Full backend gate: `make -C apps/backend test` passed
`CGO_ENABLED=1 go test -tags fts5 ./...`.
- Backend lint: `make -C apps/backend lint` passed with no issues.
- Affected package suite: `go test ./internal/orchestrator
./internal/mcp/handlers ./internal/mcp/server -count=1` passed.
- Formatting and whitespace checks passed. No throwaway files or logs remain.
- External side effects: None.
@@ -0,0 +1,61 @@
---
id: "02-update-public-docs"
title: "Update public MCP documentation"
status: done
wave: 2
depends_on: ["01-propagate-effective-profile"]
plan: "plan.md"
spec: "../../specs/spawn-session-effective-profile/spec.md"
---
# Task 02: Update public MCP documentation
## Intent
Document the effective profile response on the public Agent Communication explanation page.
## Inputs
- The spec `API surface` and `Scenarios` sections.
- The final tool description from Task 01.
- The existing `spawn_session_kandev` sections in `docs/public/agent-communication.md`.
## Acceptance
- Both documented success shapes include `agent_profile_id`.
- The page states that the field is the effective profile after workflow profile resolution.
- The page does not imply that an explicit profile always launches on a workflow step.
- The public documentation checks pass.
## Files likely touched
- `docs/public/agent-communication.md`
## Verification
```bash
cd "$(git rev-parse --show-toplevel)" && rg -n "spawn_session_kandev|agent_profile_id" docs/public/agent-communication.md && node --test scripts/validate-public-docs.test.mjs && node scripts/validate-public-docs.mjs
```
## Dependencies
Task 01.
## Parallelism
`sequential`. The documented response must match the completed backend contract.
## Output contract
Report the changed page, its explanation classification, each command result, blockers, and risks. Update this task and `plan.md` in the same conversation.
## Results
- Updated `docs/public/agent-communication.md` with both documented
`spawn_session_kandev` success shapes and the effective-profile precedence.
- `rg -n "spawn_session_kandev|agent_profile_id"
docs/public/agent-communication.md` found the updated contract.
- `node --test scripts/validate-public-docs.test.mjs` passed all 60 tests.
- `node scripts/validate-public-docs.mjs` validated 41 published pages.
- Generated artifacts and temporary capture files: None. External side
effects: None.
+7 -3
View File
@@ -40,8 +40,12 @@ message_task_kandev(task_id="<full UUID>", prompt="<message text>")
By default, the message goes to the task's primary session. Advanced callers can pass
`session_id` to target a specific session that belongs to that task, including a sibling
session on their own task. `spawn_session_kandev` returns `{task_id, session_id, state}`
when an agent starts an additional session on an existing task; pass the `session_id` field to `message_task_kandev`.
session on their own task. `spawn_session_kandev` returns `{task_id, session_id, state, agent_profile_id}`
when an agent starts an additional session on an existing task; pass the `session_id` field to
`message_task_kandev`. The returned `agent_profile_id` is the effective profile after workflow
profile resolution. A pinned workflow-step profile wins first, followed by the workflow default
on an unpinned step. Without a workflow launch profile, an explicit profile wins, and the existing
inheritance rules apply when no profile is provided.
Delivery behaviour depends on the target session's state at the moment of the call:
@@ -239,7 +243,7 @@ These tools complement cross-task communication for common coordination patterns
| `list_task_sessions_kandev` | List a task's sessions to find the `session_id` for the two tools above |
| `list_related_tasks_kandev` | Discover parent / child / sibling / blocker task IDs |
| `create_task_kandev` | Delegate work to a new subtask; returns the new task's ID |
| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state}`; use the `session_id` field to message the new session directly |
| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state, agent_profile_id}`, where `agent_profile_id` is the effective profile after workflow resolution; use the `session_id` field to message the new session directly |
| `move_task_kandev` | Hand off a task to the next workflow step with an optional prompt for the receiving agent |
| `create_task_plan_kandev` | Record an agreed implementation plan (both tasks can create/update their own plans) |
| `get_task_plan_kandev` | Read a task's plan; useful before messaging to share a structured proposal |
+1
View File
@@ -89,6 +89,7 @@ Kandev's task model: documents, execution stages, labels, blocker escalation, su
| [parent-child-message-interrupt](tasks/parent-child-message-interrupt.md) | shipped |
| [parent-child-task-stop](tasks/parent-child-task-stop.md) | shipped |
| [mcp-task-agent-profile-default](tasks/mcp-task-agent-profile-default/spec.md) | shipped |
| [spawn-session-effective-profile](spawn-session-effective-profile/spec.md) | shipped |
| [runtime-state-publication-order](tasks/runtime-state-publication-order.md) | shipped |
| [agent-generated-titles](tasks/agent-generated-titles.md) | approved |
| [task-create-executor-default](tasks/task-create-executor-default.md) | approved |
@@ -0,0 +1,60 @@
---
status: shipped
created: 2026-08-12
owner: kandev
---
# Spawn Session Effective Agent Profile
## Why
Agents use `spawn_session_kandev` to start another session with a selected agent profile. The tool must report the profile that the new session uses.
Without this guarantee, a caller can receive confirmation for one profile while a workflow profile starts instead.
## What
- `spawn_session_kandev` reports the effective agent profile for each successful launch.
- The reported profile is the profile on the new session, after workflow profile resolution.
- A workflow step launch profile keeps its current precedence. A pinned step profile wins first. Otherwise, the workflow default wins on that step.
- Without a workflow launch profile, an explicit `agent_profile_id` keeps its current precedence.
- Without an explicit profile, the current inheritance rules remain unchanged.
- The tool description explains the precedence and the response field.
## API surface
The successful `spawn_session_kandev` response has this shape:
```json
{
"task_id": "string",
"session_id": "string",
"state": "string",
"agent_profile_id": "string"
}
```
`agent_profile_id` identifies the effective profile on the new session. It does not echo an earlier profile choice when workflow resolution selects another profile.
## Failure modes
- If the session launch fails, the tool returns the existing launch error and no success response.
- A successful session launch returns the nonempty effective profile from the launch result.
- A session name update can still fail without changing the successful launch result.
## Scenarios
- **GIVEN** an unpinned workflow step with workflow default A, **WHEN** a caller requests profile B, **THEN** the new session and response use profile A.
- **GIVEN** a workflow step pinned to profile A, **WHEN** a caller requests profile B, **THEN** the new session and response use profile A.
- **GIVEN** no workflow launch profile, **WHEN** a caller requests profile B, **THEN** the new session and response use profile B.
- **GIVEN** no explicit profile or workflow launch profile, **WHEN** a same-task session spawns another session, **THEN** the new session and response use the inherited profile.
## Out of scope
- Changing workflow, step, explicit, or inherited profile precedence.
- Changing `create_task_kandev`. Its workflow-default response behavior is already covered by the MCP-created task profile spec.
- Changing session persistence, executor selection, or profile validation.
## Implementation plan
See [`../../plans/spawn-session-effective-profile/plan.md`](../../plans/spawn-session-effective-profile/plan.md).