diff --git a/apps/backend/internal/orchestrator/executor/executor_execute.go b/apps/backend/internal/orchestrator/executor/executor_execute.go index 9fd14b19ea..cb5ab253b1 100644 --- a/apps/backend/internal/orchestrator/executor/executor_execute.go +++ b/apps/backend/internal/orchestrator/executor/executor_execute.go @@ -2175,27 +2175,17 @@ func (e *Executor) injectHandoverIfNeeded(ctx context.Context, taskID, currentSe // to the task root via filepath.Dir would diverge hot vs cold cwd and break // resume with -32002 Resource not found. // -// resp.WorktreePath here already mirrors what executor_standalone.go writes -// into metadata["worktree_path"] (= req.WorkspacePath from the env preparer), -// which is also what becomes cmd.Dir of the agent process. So persisting it -// as-is keeps a single source of truth. +// resp.WorkspacePath is the lifecycle execution's actual working directory and +// is therefore authoritative. WorktreePath remains a compatibility fallback +// for lifecycle responses that only expose the preparer's legacy metadata. func computeWorkspacePath(req *LaunchAgentRequest, resp *LaunchAgentResponse) string { - // SSH materializes repositories on the remote host. RepositoryPath still - // identifies the source checkout on this host, so persisting it would make a - // later sibling attach to a different remote directory. The lifecycle - // response is the canonical remote task-directory handle. - if req.ExecutorType == string(models.ExecutorTypeSSH) && resp.WorkspacePath != "" { + if resp.WorkspacePath != "" { return resp.WorkspacePath } if resp.WorktreePath != "" { return resp.WorktreePath } - if req.RepositoryPath != "" { - return req.RepositoryPath - } - // Quick-chat sessions have no worktree/repo but the lifecycle manager - // creates a workspace directory — use it as fallback. - return resp.WorkspacePath + return req.RepositoryPath } // persistTaskEnvironment creates or updates the task environment record after a successful launch. diff --git a/apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go b/apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go index 6d4b0dc095..faf47f9f76 100644 --- a/apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go +++ b/apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go @@ -52,6 +52,7 @@ func TestResolveTaskEnvWorkspacePath(t *testing.T) { } }) + // @covers AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.3 t.Run("no worktree falls back to repository path", func(t *testing.T) { req := &LaunchAgentRequest{RepositoryPath: "/repos/myrepo"} resp := &LaunchAgentResponse{} // no WorktreePath @@ -60,6 +61,15 @@ func TestResolveTaskEnvWorkspacePath(t *testing.T) { } }) + // @covers AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.1 + t.Run("recovered execution keeps lifecycle workspace", func(t *testing.T) { + req := &LaunchAgentRequest{RepositoryPath: "/source/kandev"} + resp := &LaunchAgentResponse{WorkspacePath: "/tasks/task-1/kandev"} + if got := computeWorkspacePath(req, resp); got != "/tasks/task-1/kandev" { + t.Fatalf("recovered workspace: want lifecycle workspace, got %q", got) + } + }) + t.Run("SSH keeps the remote task directory over the host repository path", func(t *testing.T) { req := &LaunchAgentRequest{ ExecutorType: string(models.ExecutorTypeSSH), diff --git a/apps/web/components/task/file-browser-toolbar.tsx b/apps/web/components/task/file-browser-toolbar.tsx index 111c6b5e4f..85f78c3d93 100644 --- a/apps/web/components/task/file-browser-toolbar.tsx +++ b/apps/web/components/task/file-browser-toolbar.tsx @@ -239,7 +239,10 @@ export function FileBrowserToolbar({ {displayPath ? ( - + {displayPath} diff --git a/apps/web/e2e/tests/session/session-resume.spec.ts b/apps/web/e2e/tests/session/session-resume.spec.ts index 0a512de494..32f26f65e9 100644 --- a/apps/web/e2e/tests/session/session-resume.spec.ts +++ b/apps/web/e2e/tests/session/session-resume.spec.ts @@ -148,6 +148,71 @@ test.describe("Session resume (ACP mode)", () => { // 10. The agent should respond to the new prompt await session.expectChatResponseVisible("simple mock response", 1, { timeout: 30_000 }); }); + + // @covers AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.2 + test("preserves the worktree Files path after backend restart", async ({ + testPage, + apiClient, + seedData, + backend, + }) => { + test.setTimeout(120_000); + + const task = await apiClient.createTaskWithAgent( + seedData.workspaceId, + "Worktree Path Resume Task", + seedData.agentProfileId, + { + description: "/e2e:simple-message", + workflow_id: seedData.workflowId, + workflow_step_id: seedData.startStepId, + repository_ids: [seedData.repositoryId], + executor_profile_id: seedData.worktreeExecutorProfileId, + }, + ); + const sessionId = task.session_id; + if (!sessionId) throw new Error("createTaskWithAgent did not return a session_id"); + + const session = await openTaskSession(testPage, "Worktree Path Resume Task"); + await session.waitForChatIdle({ timeout: 30_000 }); + await waitForSessionState(apiClient, { + taskId: task.id, + sessionId, + expectedState: "WAITING_FOR_INPUT", + message: "Initial worktree session did not settle before restart", + timeout: 30_000, + }); + + const { sessions } = await apiClient.listTaskSessions(task.id); + const initialSession = sessions.find((candidate) => candidate.id === sessionId); + const expectedWorkspacePath = initialSession?.workspace_path ?? initialSession?.worktree_path; + if (!expectedWorkspacePath) throw new Error("Worktree session did not expose a workspace path"); + const expectedDisplayPath = expectedWorkspacePath.replace(/^\/(?:Users|home)\/[^/]+\//, "~/"); + + await session.clickTab("Files"); + const visibleWorkspacePath = session.files.getByTestId("file-browser-workspace-path"); + await expect(visibleWorkspacePath).toHaveText(expectedDisplayPath, { timeout: 30_000 }); + + // Keeping Files active makes reload reconstruct the workspace-only execution + // before automatic session resume promotes and persists that execution. + await backend.restart(); + await testPage.reload(); + await expect(session.files).toBeVisible({ timeout: 30_000 }); + + await session.clickSessionChatTab(); + await session.waitForLoad(); + await session.waitForChatIdle({ timeout: 60_000 }); + await expect(session.chat.getByText("Resumed agent Mock", { exact: false })).toBeVisible({ + timeout: 15_000, + }); + + // Reload once promotion has persisted the task environment. The boot payload + // must project the promoted execution's canonical workspace, not stale UI state. + await testPage.reload(); + await session.waitForLoad(); + await session.clickTab("Files"); + await expect(visibleWorkspacePath).toHaveText(expectedDisplayPath, { timeout: 30_000 }); + }); }); // --------------------------------------------------------------------------- diff --git a/docs/plans/recovered-workspace-path-persistence/plan.md b/docs/plans/recovered-workspace-path-persistence/plan.md new file mode 100644 index 0000000000..1298434e6e --- /dev/null +++ b/docs/plans/recovered-workspace-path-persistence/plan.md @@ -0,0 +1,136 @@ +--- +created: 2026-08-30 +status: done +requirements: + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001 + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002 +system_design: + - ../../specs/tasks/system-design/additional-session-workspace-reuse.md +legacy_specs: [] +--- + +# Implementation Plan: Recovered Workspace Path Persistence + +## Overview + +Preserve a Worktree task's actual execution workspace when backend restart +recovery creates a workspace-only execution and later promotes it during agent +resume. The work starts with a focused failing precedence regression, applies +the smallest orchestrator correction, and then proves the user-visible Files +path through the existing restart E2E flow. + +The confirmed failure is: + +1. Restart recovery reconstructs an execution at the correct worktree path. +2. Workspace-only promotion returns that path as `WorkspacePath`, but the + recovered metadata has no `WorktreePath`. +3. `computeWorkspacePath` prefers the request's source `RepositoryPath` over + the response's actual `WorkspacePath`. +4. `persistTaskEnvironment` overwrites the correct environment path with the + source checkout. +5. Session reads prefer the environment path, so Files displays the source + checkout even though agentctl still runs in the worktree. + +## Scope + +### In scope + +- Treat the lifecycle response's execution workspace as authoritative over + source repository input. +- Preserve legacy fallbacks for responses without an execution workspace. +- Add a focused Go regression that fails under the current precedence. +- Extend the existing session-restart E2E coverage to assert that the Files + path remains the original Worktree task path after automatic resume. +- Add only the stable selector needed for that UI assertion. + +### Out of scope + +- Database schema changes or startup migrations. +- Files layout, navigation, touch behavior, or responsive composition changes. +- Reconstructing missing worktree inventory from filesystem state. +- Changing the environment-first task-session API projection. +- Direct mutation of a live user's database as part of the code change. + +## Technical approach + +### Workspace-path precedence + +Update `computeWorkspacePath` in +`apps/backend/internal/orchestrator/executor/executor_execute.go` so a non-empty +`LaunchAgentResponse.WorkspacePath` wins. Retain `WorktreePath` as the fallback +for legacy lifecycle responses and `LaunchAgentRequest.RepositoryPath` as the +last repository-backed compatibility fallback. + +The response workspace is `AgentExecution.WorkspacePath`, which is the actual +agentctl working directory surfaced by `backendapp.lifecycleAdapter`. This +keeps fresh launch, workspace-only promotion, SSH recovery, and quick-chat +recovery on one authority rule. + +### Regression boundaries + +Add a subtest to +`apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go` +with a source repository request, an empty response `WorktreePath`, and a +correct response `WorkspacePath`. It must expect the response workspace and +fail before the production correction. + +Extend +`apps/web/e2e/tests/session/session-resume.spec.ts` with a Worktree-executor +scenario that records the original session worktree, keeps Files active, +restarts the backend, waits for automatic resume, and asserts the visible Files +workspace path remains unchanged. Add a stable path selector in +`apps/web/components/task/file-browser-toolbar.tsx`; no presentation behavior +changes. + +## Tests + +- `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.1`: the unit regression + proves recovered execution workspace precedence over the source checkout. +- `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.2`: the restart E2E proves + the Files projection remains the canonical Worktree path after promotion. +- `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.3`: existing repository + fallback coverage remains green when the response has neither workspace nor + worktree path. + +## E2E tests + +Add `preserves the worktree Files path after backend restart` to +`apps/web/e2e/tests/session/session-resume.spec.ts` in the `chromium` project. +It uses the existing `backend.restart()` fixture and session recovery page +object patterns. The shared Files component supplies the same normalized path +to desktop and phone; because this fix does not alter layout or interaction, +no new mobile-only scenario is required. + +## Work orders + +- [x] [Task 01: Preserve recovered workspace path](task-01-preserve-recovered-workspace-path.md) + +## Verification results + +- RED: the new `recovered execution keeps lifecycle workspace` subtest failed + because `computeWorkspacePath` returned `/source/kandev` instead of + `/tasks/task-1/kandev`. +- RED: Chromium collected the named restart scenario and failed on the absent + `file-browser-workspace-path` selector before the selector was added. A + DOM-coupled prototype already kept the path in the normal E2E timing; the Go + regression is the defect-specific precedence gate. +- `cd apps && pnpm install --frozen-lockfile` passed. +- `cd apps/backend && go test ./internal/orchestrator/executor -run '^TestResolveTaskEnvWorkspacePath$'` + passed with 7 tests. +- `cd apps/backend && go test ./internal/orchestrator/executor` passed with 527 + tests. +- `cd apps/web && pnpm e2e:run tests/session/session-resume.spec.ts -- --grep 'preserves the worktree Files path after backend restart'` + collected and passed 1 Chromium test. +- `cd apps/web && pnpm exec prettier --check components/task/file-browser-toolbar.tsx e2e/tests/session/session-resume.spec.ts` + passed. +- `cd apps/web && pnpm run typecheck` passed. +- `python3 scripts/lint-spec-files.py --all` and `git diff --check` passed. + +## Risks + +- `WorkspacePath` and `WorktreePath` historically overlap. The test matrix must + preserve legacy responses that supply only `WorktreePath`. +- The E2E stimulus must open Files before restart so reload reconstructs the + workspace-only execution before automatic resume promotes it. +- Previously corrupted rows are repaired only after a successful resume under + the corrected binary and only while canonical worktree inventory remains. diff --git a/docs/plans/recovered-workspace-path-persistence/task-01-preserve-recovered-workspace-path.md b/docs/plans/recovered-workspace-path-persistence/task-01-preserve-recovered-workspace-path.md new file mode 100644 index 0000000000..21757d1bac --- /dev/null +++ b/docs/plans/recovered-workspace-path-persistence/task-01-preserve-recovered-workspace-path.md @@ -0,0 +1,121 @@ +--- +id: "01-preserve-recovered-workspace-path" +title: "Preserve recovered workspace path" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001 + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002 +acceptance_criteria: + - AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.1 + - AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.2 + - AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.3 +system_design: + - ../../specs/tasks/system-design/additional-session-workspace-reuse.md +--- + +# Task 01: Preserve Recovered Workspace Path + +## Summary + +Make the lifecycle execution workspace authoritative when launch or resume +persistence selects the task environment path. Prove the correction with a +failing unit regression and the existing browser-level backend-restart flow. + +## In scope + +- Add the recovered workspace-only precedence case to + `TestResolveTaskEnvWorkspacePath` and confirm RED. +- Change `computeWorkspacePath` to prefer the lifecycle execution workspace, + while retaining worktree-only and source-repository fallbacks. +- Add a stable selector for the visible Files workspace path. +- Add a Worktree-executor restart/resume scenario to the existing session + resume Playwright specification. +- Record RED and final command results in this work order and `plan.md`. + +## Out of scope + +- Schema or data migrations. +- Lifecycle promotion redesign or new worktree metadata requirements. +- Frontend state derivation, layout, mobile composition, or copy changes. +- Filesystem-based repair of missing canonical worktree inventory. +- Manual repair of the reported live task. + +## Acceptance + +- A response with the recovered execution workspace and no worktree metadata + persists that workspace instead of the request's source repository. +- Existing worktree-only, SSH, quick-chat, and empty-response fallbacks retain + their intended paths. +- After backend restart and automatic resume, the Files path shown for a + Worktree task is unchanged from its original canonical worktree. + +## Verification + +```bash +cd apps && pnpm install --frozen-lockfile +cd apps/backend && go test ./internal/orchestrator/executor -run '^TestResolveTaskEnvWorkspacePath$' +cd apps/backend && go test ./internal/orchestrator/executor +cd apps/web && pnpm e2e:run tests/session/session-resume.spec.ts -- --grep 'preserves the worktree Files path after backend restart' +``` + +The new unit subtest and E2E assertion must fail for the diagnosed path mismatch +before the production correction and pass afterward. Confirm Playwright +collects and executes the named `chromium` test. + +## Files likely touched + +- `apps/backend/internal/orchestrator/executor/executor_execute.go` +- `apps/backend/internal/orchestrator/executor/executor_execute_workspace_path_test.go` +- `apps/web/components/task/file-browser-toolbar.tsx` +- `apps/web/e2e/tests/session/session-resume.spec.ts` +- `docs/plans/recovered-workspace-path-persistence/plan.md` +- `docs/plans/recovered-workspace-path-persistence/task-01-preserve-recovered-workspace-path.md` + +## Dependencies + +None. + +## Risks + +- An E2E test that reloads into Chat instead of Files may miss the + workspace-only reconstruction and fail to exercise the production sequence. +- Adding a selector must not change the Files toolbar's responsive structure or + accessible name. + +## Parallelism + +`sequential` + +## Inputs + +- `REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002` and its acceptance + criteria. +- `docs/specs/tasks/system-design/additional-session-workspace-reuse.md`. +- `docs/decisions/2026-08-08-task-owned-worktree-lifetime.md`. +- `computeWorkspacePath`, `backendapp.lifecycleAdapter.LaunchAgent`, and + `lifecycle.Manager.promoteWorkspaceExecution`. +- Existing restart patterns in + `apps/web/e2e/tests/session/session-resume.spec.ts`. + +## Results + +- Made `LaunchAgentResponse.WorkspacePath` authoritative, with + `WorktreePath` and `RepositoryPath` retained as legacy fallbacks. +- Added the defect-specific Go regression for a recovered execution that has + no worktree metadata and confirmed it failed before the production change. +- Added the Files path selector and a Chromium restart/resume test that checks + the original worktree path before restart and after promotion plus a fresh + boot-payload reload. +- The first E2E draft used a DOM relationship and stayed green before the + backend correction under normal fixture timing. The final E2E RED was the + missing stable selector; the Go test remains the direct regression for the + response-precedence defect. +- All verification commands in this work order passed. The executor package + reported 527 passing tests, and the named Chromium run reported 1 passing + test. +- No mobile-only test was added because the change does not alter Files layout, + controls, touch behavior, or responsive composition; both desktop and mobile + render the same normalized path component. diff --git a/docs/specs/tasks/README.md b/docs/specs/tasks/README.md index 3fb5aced52..22b82c8ac3 100644 --- a/docs/specs/tasks/README.md +++ b/docs/specs/tasks/README.md @@ -109,6 +109,7 @@ signals, and task-scoped scheduling contracts. ### System design +- [Additional Session Workspace Reuse](system-design/additional-session-workspace-reuse.md) - [Attach Workspace Sources](system-design/attach-workspace-sources.md) - [Quick Chat Agent Titles](system-design/quick-chat-agent-titles.md) - [Quick Chat Session Resumption](system-design/quick-chat-session-resumption.md) diff --git a/docs/specs/tasks/requirements/additional-session-workspace-reuse.md b/docs/specs/tasks/requirements/additional-session-workspace-reuse.md index 622c7ed01d..f4b019ce57 100644 --- a/docs/specs/tasks/requirements/additional-session-workspace-reuse.md +++ b/docs/specs/tasks/requirements/additional-session-workspace-reuse.md @@ -2,6 +2,7 @@ status: draft system: tasks created: 2026-08-19 +updated: 2026-08-30 owners: - kandev --- @@ -34,6 +35,24 @@ preserving independent session runtime state. reuse shall fail with a typed, recoverable API error without creating a session or replacement workspace. +### REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002: Canonical Workspace Identity Continuity + +**Intent:** Keep the task's effective workspace identity stable when Kandev +reconstructs or resumes its runtime. + +#### Acceptance criteria + +- **AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.1:** When Kandev recovers + or resumes a task with a ready canonical environment, the persisted and + projected workspace path shall remain the materialized workspace used by the + recovered runtime. +- **AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.2:** When the task reloads + after recovery, Files and later attached sessions shall resolve the same + canonical workspace; the repository's source checkout shall not replace it. +- **AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002.3:** When no materialized + or recovered runtime workspace exists, a legacy repository-backed session + can continue to use its source checkout as a compatibility fallback. + ## Migrated source detail ## Why @@ -111,3 +130,4 @@ an optional session name remains best effort after a successful launch. - A trusted filesystem read-only agent mode. - Automatic workspace repair, reset, branch switching, or replacement during session spawn. +- Reconstructing a missing physical worktree from filesystem guesses. diff --git a/docs/specs/tasks/system-design/additional-session-workspace-reuse.md b/docs/specs/tasks/system-design/additional-session-workspace-reuse.md new file mode 100644 index 0000000000..0595f1ecd5 --- /dev/null +++ b/docs/specs/tasks/system-design/additional-session-workspace-reuse.md @@ -0,0 +1,107 @@ +--- +status: current +system: tasks +requirements: + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001 + - REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002 +created: 2026-08-30 +owners: + - kandev +--- + +# Additional Session Workspace Reuse System Design + +## Purpose and boundaries + +The task system owns the canonical task environment, its physical worktree +inventory, and the execution lifecycle that reuses those resources. The +workspace system supplies repository and worktree resources, while the agent +runtime creates the process that consumes the resolved task workspace. + +This design preserves one effective workspace identity across initial launch, +additional-session attachment, backend restart, workspace-only reconstruction, +and agent promotion or resume. It follows +[ADR-2026-08-08-task-owned-worktree-lifetime](../../../decisions/2026-08-08-task-owned-worktree-lifetime.md). + +## Requirement mapping + +| Requirement | Design section | +| --- | --- | +| `REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001` | [Canonical environment and inventory](#canonical-environment-and-inventory) | +| `REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002` | [Recovery and path authority](#recovery-and-path-authority) | + +## Canonical environment and inventory + +`task_environments` owns the task-level execution environment. +`task_environment_repos` owns the ordered physical repository and worktree +inventory. `task_sessions.task_environment_id` attaches a session to that +environment; an execution ID or repository source path is not workspace +identity. + +The initial materializer publishes a ready environment and complete repository +inventory. Additional sessions validate and attach to that environment without +running repository preparation or creating another physical owner. Missing, +creating, inconsistent, or unsupported environments fail through the existing +typed reuse errors. + +## Recovery and path authority + +`task/service.GetWorkspaceInfoForSession` reconstructs the runtime workspace +from the canonical environment-repository inventory. For a single repository, +the effective workspace is its worktree. For multiple repositories, it is their +task root. The lifecycle manager uses that value as the recovered +`AgentExecution.WorkspacePath` and as the agentctl working directory. + +The lifecycle adapter returns both the execution workspace and any optional +worktree metadata. Workspace-only reconstruction can legitimately return the +correct `WorkspacePath` without a `worktree_path` metadata key when the +execution is promoted to an agent execution. + +The orchestrator persists an effective workspace using this authority order: + +1. The lifecycle response's non-empty `WorkspacePath`, which is the actual + execution working directory. +2. The response's non-empty `WorktreePath` for legacy adapters that do not + expose an execution workspace. +3. The request's `RepositoryPath` only when no materialized runtime workspace + is available. + +The source repository path is preparation input. It must not overwrite a +materialized task workspace reported by the lifecycle response. + +## Persistence and projection + +After successful launch or resume, the orchestrator refreshes +`task_environments.workspace_path` with the authoritative effective workspace. +No schema migration is required. A successful resume under this contract also +self-heals an environment path written incorrectly by the former fallback +order, provided the canonical worktree inventory remains valid. + +Task-session reads project the linked environment workspace before the legacy +session-local path. The Files panel and other workspace consumers use that +effective `workspace_path`, with `worktree_path` retained as the primary +repository and compatibility fallback. + +## Failure and recovery + +- An empty execution workspace and empty worktree path preserve the existing + source-repository fallback for legacy or non-materialized local sessions. +- A recovered execution workspace always wins over source checkout input, + including when optional worktree metadata is absent. +- Missing or ambiguous physical inventory fails closed. This path does not + inspect the filesystem to guess a replacement owner. +- Persistence failure follows the existing launch/resume rollback and task + environment failure handling. + +## Verification + +The orchestrator unit boundary covers all workspace-path precedence cases, +including workspace-only recovery with a source repository present. A session +resume Playwright scenario uses the Worktree executor, restarts the backend, +allows automatic promotion/resume, and verifies that the Files path remains +the original worktree path. + +The Files path component is shared by desktop and phone layouts. This repair +changes backend state normalization, not layout, navigation, scrolling, or +touch behavior, so the focused desktop recovery scenario plus existing shared +component coverage provides mobile parity.