Skip to content

feat(daemon): task agents work in a per-task worktree rooted at the task workspace - #3587

Open
lsm wants to merge 8 commits into
devfrom
feat/task-worktree-per-workspace
Open

feat(daemon): task agents work in a per-task worktree rooted at the task workspace#3587
lsm wants to merge 8 commits into
devfrom
feat/task-worktree-per-workspace

Conversation

@lsm

@lsm lsm commented Sep 3, 2026

Copy link
Copy Markdown
Owner

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.

  • spawn creates a task worktree from the resolved workspace (explicit task workspace, else primary); non-git workspaces are supported and used directly by the agent with no worktree
  • workspace registration accepts any accessible unclaimed directory (git or not); space creation already only warned for non-git primaries
  • task agents get the Git Worktree Isolation system prompt via session.context.taskWorktree (interactive session.worktree unchanged, still takes precedence)
  • update_task and the worker create_standalone_task tool accept a workspace; scheduled tasks carry one (m225)
  • multi-workspace spaces require an explicit workspace at task creation (manager, RPC, MCP, worker tool, run start, schedule fire) — single-workspace spaces keep the primary default
  • goal/forge/automation task creation inherits the goal pin and runs the same requirement guard

Tests: requirement/spawn/QOB/registration/fire-handler/proposal/goal-trigger unit tests updated or added; no behavior change for single-workspace git-primary spaces.

lsm added 7 commits September 2, 2026 21:52
…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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T03:05:40.314969Z ba79bb3 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +370 to +378
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +310 to +316
const goalWorkspacePath = deps.goalRepo.getById(goalId)?.workspacePath ?? null;
requireTaskWorkspaceSelectionSync({
spaces: deps.spaceRepo,
workspaces: new SpaceWorkspaceRepository(deps.db),
spaceId: scope.spaceId,
hasExplicitSelection: isExplicitWorkspaceSelection(goalWorkspacePath),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines 2436 to +2437
if (workspacePath && agentSession.getSessionData().workspacePath !== workspacePath) {
agentSession.updateMetadata({ workspacePath: workspacePath });
const restoreContextPatch = this.taskWorktreeContextPatch(agentSession, space, task);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +2007 to +2009
previousCanonicalTask?.workspacePath
? { workspacePath: previousCanonicalTask.workspacePath }
: {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +161 to +162
const scheduleWorkspacePath = schedule.workspacePath ?? goalForWorkspace?.workspacePath ?? null;
await requireTaskWorkspaceSelection({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant