feat(daemon): task agents work in a per-task worktree rooted at the task workspace - #3587
feat(daemon): task agents work in a per-task worktree rooted at the task workspace#3587lsm wants to merge 8 commits into
Conversation
…cit workspace Workspace-resolution at spawn previously treated an explicit task.workspace_path as an instruction to run the agent in-place in that repo, skipping worktree creation entirely. Now every spawn creates a task worktree rooted at the resolved task workspace (task workspace_path, else the space primary) and runs the agent inside it; explicit workspace selection only changes the repo the worktree branches from. The no-worktree-manager degenerate path still honors the task workspace in-place.
…idance resolveRepoRoot now reports whether the resolved root is a git repository, and the create-task-worktree pipeline halts with an actionable message before any git mutation when it is not. Previously a non-repo root degraded silently and surfaced later as a raw 'fatal: not a git repository' from git worktree add, which is what blocked every workspace-less task in spaces whose primary workspace is not a repo.
Space task agents running inside a task worktree previously got the
worktree as cwd but never received the Git Worktree Isolation system
prompt, because every gate keyed on session.worktree — the interactive
per-session worktree feature. SessionContext gains a taskWorktree
identity { worktreePath, branch, mainRepoPath } set at sub-session
creation and synced alongside workspace drift on restore, reuse,
rehydrate, migrate, and self-heal paths (with rollback), and the
query-options-builder gates now accept either source. Interactive
session.worktree still takes precedence when both exist.
Tasks could carry a workspace_path from creation but agents had no way to set or correct it afterwards; the manager-level support (guards: no active agent session, no existing worktree) was only reachable via RPC. The update_task tool now accepts a registered workspace label or path, resolves it through the same selection pipeline as task creation, and lists the registered workspaces on unknown selections.
…paces Task creation previously fell back to the space primary workspace silently, which in multi-workspace spaces routed worktrees (and before this series, agents) to the wrong repo or to a non-repo primary. A superpipe requirement helper now rejects task creation and workspace clearing with the list of registered workspaces whenever a space has more than one, while single-workspace spaces keep the primary default. Explicitly selecting any registered workspace, including the primary, remains valid. The guard covers the task manager (MCP create/update, RPC create/update), the worker-facing create_standalone_task tool, startWorkflowRun canonical tasks (change_plan inherits the previous run's workspace; spaceWorkflowRun.start accepts workspacePath), and goal-triggered creation paths.
task_schedules gains a workspace_path column (m225, guarded ALTER in the m219 pattern). create_scheduled_task accepts a registered workspace label or path resolved at creation time; when a schedule fires, the created task takes the schedule workspace, falling back to the goal pin, and the multi-workspace requirement guard rejects firing when neither exists. Unpinned goals in multi-workspace spaces now fail their check-ins loudly instead of silently routing to the primary.
…tion tasks Goal-triggered tasks, forge proposal tasks, and goal-automation review tasks all bypass SpaceTaskManager and created tasks with a NULL workspace even when their owning goal was pinned. All three paths now inherit the goal pin and run the multi-workspace requirement guard, so unpinned goals in multi-workspace spaces fail loudly at task creation (pin via update_goal workspace_path) instead of silently routing agents and worktrees to the primary workspace.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cca14eb08e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| spaces: this.deps.spaceRepo, | ||
| workspaces: new SpaceWorkspaceRepository(db), | ||
| spaceId: goal.spaceId, | ||
| hasExplicitSelection: isExplicitWorkspaceSelection(goal.workspacePath), |
There was a problem hiding this comment.
Preserve primary workspace selections for goal runs
When a goal in a multi-workspace space is explicitly pinned to the primary repository, resolveGoalWorkspacePath() normalizes that path to null, so this check treats the selection as absent and rejects every immediate or queued goal task. The same representation is consumed by the schedule, Forge, and automation guards, meaning the error's suggested update_goal workspace_path remedy cannot succeed when the desired workspace is primary; treat the goal's resolved primary value as a valid selection or persist selection state separately.
Useful? React with 👍 / 👎.
| const goalWorkspacePath = scope.spaceGoalId | ||
| ? (this.deps.goalService?.getGoal(scope.spaceGoalId)?.workspacePath ?? null) | ||
| : null; | ||
| if (this.deps.spaceRepo && this.deps.db) { | ||
| requireTaskWorkspaceSelectionSync({ | ||
| spaces: this.deps.spaceRepo, | ||
| workspaces: new SpaceWorkspaceRepository(this.deps.db), | ||
| spaceId: scope.spaceId, | ||
| hasExplicitSelection: isExplicitWorkspaceSelection(goalWorkspacePath), |
There was a problem hiding this comment.
Accept a workspace when materializing unlinked Forge proposals
For a Forge scope without a linked goal in a multi-workspace space, goalWorkspacePath is always null, so this guard unconditionally rejects create_task_from_forge_proposal. Neither CreateTaskFromProposalParams nor CreateTaskFromForgeProposalSchema provides a workspace argument, leaving users no way to satisfy the new requirement; add and resolve a workspace selection for this creation path.
Useful? React with 👍 / 👎.
| const goalWorkspacePath = deps.goalRepo.getById(goalId)?.workspacePath ?? null; | ||
| requireTaskWorkspaceSelectionSync({ | ||
| spaces: deps.spaceRepo, | ||
| workspaces: new SpaceWorkspaceRepository(deps.db), | ||
| spaceId: scope.spaceId, | ||
| hasExplicitSelection: isExplicitWorkspaceSelection(goalWorkspacePath), | ||
| }); |
There was a problem hiding this comment.
Validate workspace before persisting the Forge episode
When automation runs for an unpinned goal after a second workspace is registered, this validation occurs only inside createReviewTask, after episodeService.createFromEvidence() has already invoked the judge and persisted an episode, lessons, and proposals. The guard then throws before creating the review task or advancing the cursor, leaving orphaned Forge data and causing each job retry to repeat the expensive judge call and persist another copy; perform the workspace check before createFromEvidence().
Useful? React with 👍 / 👎.
| createdByAgent: input.createdByAgent, | ||
| createdBySession: input.createdBySession, | ||
| goalId, | ||
| workspacePath: input.workspacePath, |
There was a problem hiding this comment.
Validate schedule workspaces before enqueueing
When a space already has multiple workspaces, both the web schedule flow—which omits workspacePath entirely—and create_scheduled_task without its optional workspace still reach this code, persist a null-workspace schedule, enqueue its job, and report successful creation. The new fire-handler guard later rejects that job, so the schedule shown to the user never creates a task; enforce the selection requirement during schedule creation and expose/pass the workspace from the web flow.
Useful? React with 👍 / 👎.
| if (workspacePath && agentSession.getSessionData().workspacePath !== workspacePath) { | ||
| agentSession.updateMetadata({ workspacePath: workspacePath }); | ||
| const restoreContextPatch = this.taskWorktreeContextPatch(agentSession, space, task); |
There was a problem hiding this comment.
Backfill worktree context when paths already match
For a task-agent session created before this commit and restored after an upgrade, the persisted workspacePath already equals the persisted task-worktree path but its context has no taskWorktree. Because the new context patch is nested under the path-mismatch condition, it is never applied in that common restart case, and QueryOptionsBuilder consequently omits the new isolation prompt and worktree-specific additional directories; compute and persist the context patch independently of whether the path changed. The normal rehydration block around line 4106 has the same gating issue.
Useful? React with 👍 / 👎.
| previousCanonicalTask?.workspacePath | ||
| ? { workspacePath: previousCanonicalTask.workspacePath } | ||
| : {} |
There was a problem hiding this comment.
Carry primary workspace through workflow switches
When update_workflow_run switches a run whose canonical task is bound to the primary workspace in a multi-workspace space, that task's normalized workspacePath is null, so this conditional passes no workspace to startWorkflowRun(). The new task-selection guard then rejects the replacement run after the old run was already cancelled at line 1997, leaving the caller with a failed switch and no active original run; resolve the prior task through resolveTaskWorkspace and validate before cancellation.
Useful? React with 👍 / 👎.
| const scheduleWorkspacePath = schedule.workspacePath ?? goalForWorkspace?.workspacePath ?? null; | ||
| await requireTaskWorkspaceSelection({ |
There was a problem hiding this comment.
Keep scheduled workspace pins registered
When a schedule is pinned to a secondary workspace and that workspace is later removed, the workspace-removal guards do not count references from task_schedules. At fire time the stale path is non-empty, so this requirement check accepts it without verifying registration and the handler directly creates a task bound to a repository the space no longer owns; either block removal while schedules reference the workspace or revalidate the stored path before task creation.
Useful? React with 👍 / 👎.
| spaces: this.spaceRepo, | ||
| workspaces: this.workspaceRepo, | ||
| spaceId: this.spaceId, | ||
| hasExplicitSelection: isExplicitWorkspaceSelection(params.workspacePath), |
There was a problem hiding this comment.
Align the coordinator task schema with the new requirement
When a space coordinator wants to create a primary-repository task in a multi-workspace space, its CreateStandaloneTaskSchema still says to omit workspace to use the primary. Following that published tool contract now reaches this guard with no explicit selection and fails, while the node-agent schema was updated to say the field is required in this case; update the coordinator-facing schema so agents provide the primary label or path.
Useful? React with 👍 / 👎.
Non-git workspaces are a legitimate target (docs folders, scratch dirs): registration no longer requires a git repository root — any accessible, unclaimed directory registers, and space creation already only warned for non-git primaries. At spawn, a resolved workspace that is not a git repository runs the node agent directly in the workspace instead of attempting a worktree; createTaskWorktree keeps refusing non-git roots as a direct-caller guard with updated wording.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba79bb344b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| timeout: 30_000, | ||
| }).replace(/\n$/, ''); | ||
| return { commandCwd: topLevel || cwdRoot, dirKey: projectDirKey(commonDir) }; | ||
| return { commandCwd: topLevel || cwdRoot, dirKey: projectDirKey(commonDir), gitRepo: true }; |
There was a problem hiding this comment.
Preserve nested-directory workspace roots
When a registered workspace is a directory inside a Git repository, --show-toplevel replaces that selected directory with the outer repository root while marking it as eligible for isolation. The spawn path then runs git worktree add from that root, so the task session starts at the root of the entire repository rather than the registered directory and can edit files outside its intended workspace. The local help confirms that git worktree add ... <path> [<commit-ish>] checks out a worktree and has no source-subdirectory argument; either treat nested paths as direct non-Git workspaces or map the selected relative subdirectory into the created worktree.
Useful? React with 👍 / 👎.
Spawn previously ran agents in-place when a task had an explicit workspace_path and created worktrees only from the space primary — the exact inversion that blocked every workspace-less task in spaces with a non-repo primary and let explicit-workspace tasks share the user's checkout.
Tests: requirement/spawn/QOB/registration/fire-handler/proposal/goal-trigger unit tests updated or added; no behavior change for single-workspace git-primary spaces.