doc/plans/2026-03-10-workspace-strategy-and-git-worktrees.md
PAP-447 asks how Paperclip should support worktree-driven coding workflows for local coding agents without turning that into a universal product requirement.
The motivating use case is strong:
At the same time, we do not want to hard-code "every agent uses git worktrees" into Paperclip:
Paperclip should model execution workspaces, not worktrees.
More specifically:
This keeps the abstraction portable:
project workspace is the repo/project-level conceptexecution workspace is the runtime checkout/cwd for a rungit worktree is one strategy for creating that execution workspaceworkspace runtime services are long-lived processes or previews attached to that workspaceThis also keeps the abstraction valid for non-local adapters:
They should be treated as repo/project-scoped infrastructure, not agent identity.
The stable object is the project workspace. Agents come and go, ownership changes, and the same issue may be reassigned. A git worktree is a derived checkout of a repo workspace for a specific task or issue. The agent uses it, but should not own the abstraction.
If Paperclip makes worktrees agent-first, it will blur:
That makes reuse, reassignment, cleanup, and UI visibility harder.
By making execution workspace strategy opt-in at the adapter/config layer, not a global invariant.
Defaults should remain:
Then local coding agents can opt into a strategy like git_worktree.
By splitting responsibilities:
This avoids forcing a Claude-shaped or Codex-shaped model onto all adapters.
It also avoids forcing a host-filesystem model onto cloud agents. A cloud adapter may interpret the same requested strategy as:
The current technical model is directionally right, but the product surface needs clearer separation between:
Those should not be collapsed into one label in the UI.
For product/UI copy:
That gives Paperclip room to support:
without teaching users that "workspace" always means "git worktree on my machine".
The main place this should be configured is the project, not the agent form.
Reasoning:
So the project should own a setting like:
isolatedIssueCheckouts.enabled or equivalentand that should be the default driver for new issues in that project.
Even when a project supports isolated issue checkouts, not every issue should be forced into one.
Examples:
So the model should be:
This should not require showing advanced adapter config in normal issue creation flows.
The current raw runtime service JSON is too low-level as a primary UI for most local agents.
For claude_local and codex_local, the likely desired behavior is:
So the UI recommendation is:
Once Paperclip is creating isolated issue checkouts, it is implicitly touching a bigger workflow:
That means the product needs an explicit model for who owns PR creation and merge readiness.
At minimum there are two valid modes:
And likely three distinct decision points:
Those should not be buried inside adapter prompts. They are workflow policy.
A human operator may want a long-lived personal integration branch such as dotta and may not want every task to create a new branch/workspace dance.
That is a legitimate workflow and should be supported directly.
So Paperclip should distinguish:
This implies:
Projects should have a dedicated settings area for workspace automation.
Suggested structure:
Execution Workspaces
Enable isolated issue checkoutsDefault for new issuesCheckout implementationBranch and PR behaviorRuntime servicesCleanup behaviorFor a local git-backed project, the visible language can be more concrete:
Enable isolated issue checkoutsImplementation: Git worktreeFor remote or adapter-managed projects, the same section can instead say:
Implementation: Adapter-managed workspaceWhen creating an issue inside a project with execution workspace support enabled:
Use isolated issue checkoutIf the project does not support execution workspaces, do not show the control at all.
This keeps the default UI light while preserving control.
The agent form should not be the primary place where operators assemble worktree/runtime policy for common local agents.
Instead:
That means the common case becomes:
There should still be an advanced view for power users that shows:
But that should be treated like an expert/debugging surface, not the default mental model.
Suggested policy values:
shared_project_workspaceisolated_issue_checkoutadapter_managed_isolated_workspaceFor local git projects, isolated_issue_checkout may map to git_worktree.
Suggested project-level branch policy fields:
baseBranchbranchMode: issue_scoped | operator_branch | project_primarybranchTemplate for issue-scoped branchesoperatorPreferredBranch for human/operator workflowsThis allows:
Suggested project-level PR policy fields:
prMode: none | agent_may_open | agent_auto_open | approval_requiredautoPushOnDone: booleanrequireApprovalBeforeOpen: booleanrequireApprovalBeforeReady: booleandefaultBaseBranchThis keeps PR behavior explicit and governable.
Suggested project-level cleanup fields:
stopRuntimeServicesOnDoneremoveIsolatedCheckoutOnDoneremoveIsolatedCheckoutOnMergeddeleteIssueBranchOnMergedretainFailedWorkspaceForInspectionThese matter because workspace automation is not just setup. The cleanup path is part of the product.
Based on the concerns above, the UI should change in these ways:
claude_local and codex_localUse isolated issue checkout when the project has execution workspace support enabledThis changes the emphasis of the plan in a useful way:
It also clarifies that PR creation and cleanup are not optional side notes. They are core parts of the workspace automation product surface.
This section turns the product requirements above into a concrete implementation plan for the current codebase.
The runtime decision order should become:
That is the key architectural change. Today the implementation is too agent-config-centered for the desired UX.
Add a project-owned execution workspace policy object. Suggested shared shape:
type ProjectExecutionWorkspacePolicy = {
enabled: boolean;
defaultMode: "inherit_project_default" | "shared_project_workspace" | "isolated_issue_checkout";
implementation: "git_worktree" | "adapter_managed";
branchPolicy: {
baseBranch: string | null;
branchMode: "issue_scoped" | "operator_branch" | "project_primary";
branchTemplate: string | null;
operatorPreferredBranch: string | null;
};
pullRequestPolicy: {
mode: "none" | "agent_may_open" | "agent_auto_open" | "approval_required";
autoPushOnDone: boolean;
requireApprovalBeforeOpen: boolean;
requireApprovalBeforeReady: boolean;
defaultBaseBranch: string | null;
};
cleanupPolicy: {
stopRuntimeServicesOnDone: boolean;
removeExecutionWorkspaceOnDone: boolean;
removeExecutionWorkspaceOnMerged: boolean;
deleteIssueBranchOnMerged: boolean;
retainFailedWorkspaceForInspection: boolean;
};
runtimeServices: {
mode: "disabled" | "project_default";
services?: Array<Record<string, unknown>>;
};
};
Notes:
enabled controls whether the project exposes isolated issue checkout behavior at alldefaultMode controls issue creation defaultsimplementation stays generic enough for local or remote adaptersAdd issue-owned opt-in/override fields. Suggested shape:
type IssueExecutionWorkspaceSettings = {
mode?: "inherit_project_default" | "shared_project_workspace" | "isolated_issue_checkout";
branchOverride?: string | null;
pullRequestModeOverride?: "inherit" | "none" | "agent_may_open" | "agent_auto_open" | "approval_required";
};
This should usually be hidden behind simple UI:
Use isolated issue checkoutKeep agent-level workspace/runtime configuration, but reposition it as advanced override only.
Suggested semantics:
Files to change first:
packages/shared/src/types/project.tspackages/shared/src/validators/project.tsAdd:
executionWorkspacePolicy?: ProjectExecutionWorkspacePolicy | nullFiles to change:
packages/shared/src/types/issue.tspackages/shared/src/validators/issue.tsAdd:
executionWorkspaceSettings?: IssueExecutionWorkspaceSettings | nullIf we want these fields persisted directly on existing entities instead of living in opaque JSON:
packages/db/src/schema/projects.tspackages/db/src/schema/issues.tspackages/db/src/migrations/Recommended first cut:
projectsissuesThat minimizes schema churn while the product model is still moving.
Suggested columns:
projects.execution_workspace_policy jsonbissues.execution_workspace_settings jsonbFiles:
server/src/services/projects.tsserver/src/routes/projects.tsTasks:
Files:
server/src/services/issues.tsserver/src/routes/issues.tsTasks:
executionWorkspaceSettingsPrimary file:
server/src/services/heartbeat.tsCurrent behavior should be refactored so workspace resolution is based on:
Specific technical work:
Suggested internal helper:
type EffectiveExecutionWorkspaceDecision = {
mode: "shared_project_workspace" | "isolated_issue_checkout";
implementation: "git_worktree" | "adapter_managed" | "project_primary";
branchPolicy: {...};
pullRequestPolicy: {...};
cleanupPolicy: {...};
runtimeServices: {...};
};
Likely files:
ui/src/components/ProjectProperties.tsxui/src/pages/ui/src/api/projects.tsAdd a project-owned section:
Execution Workspaces
Important UX rule:
Likely files:
ui/src/pages/ui/src/api/issues.tsAdd:
Use isolated issue checkout toggle, only when project policy enables itDo not show:
in the default issue creation flow.
Files:
ui/src/adapters/local-workspace-runtime-fields.tsxui/src/adapters/codex-local/config-fields.tsxui/src/adapters/claude-local/config-fields.tsxTechnical direction:
Files:
packages/adapters/codex-local/src/ui/build-config.tspackages/adapters/claude-local/src/ui/build-config.tsTasks:
Files:
server/src/services/workspace-runtime.tsTasks:
This is not fully implemented today and should be treated as a separate orchestration layer.
Likely files:
server/src/services/heartbeat.tsNeeded decisions:
Suggested approach:
Likely files:
server/src/services/workspace-runtime.tsserver/src/services/heartbeat.tsNeeded behaviors:
To integrate these ideas without destabilizing the system, implement in this order:
This design shift is complete when all are true:
Paperclip already has the right foundation for a project-first model.
project_workspaces already exists in packages/db/src/schema/project_workspaces.tsProjectWorkspace type already includes cwd, repoUrl, and repoRef in packages/shared/src/types/project.tsdocs/api/goals-and-projects.mdCurrent run resolution already prefers:
See server/src/services/heartbeat.ts.
Both local coding adapters treat session continuity as cwd-bound:
packages/adapters/codex-local/src/server/execute.tspackages/adapters/claude-local/src/server/execute.tsThat means the clean insertion point is before adapter execution: resolve the final execution cwd first, then let the adapter run normally.
For server-spawned local adapters, Paperclip already injects a short-lived local JWT:
server/src/services/heartbeat.tspackages/adapters/codex-local/src/server/execute.tspackages/adapters/claude-local/src/server/execute.tsThe manual-local bootstrap path is still weaker in authenticated mode, but that is a related auth ergonomics problem, not a reason to make worktrees a core invariant.
The linked tool docs support a project-first, adapter-specific launch model.
Implication:
codex_local, Paperclip should usually create/select the checkout itself and then launch Codex inside that cwd--worktree / -wImplication:
claude_local can optionally use native --worktreeThis plan must explicitly account for the fact that many adapters are not local.
Examples:
codex_local and claude_localThese adapters do not all share the same capabilities:
Because of that, Paperclip should separate:
Paperclip should be able to express intentions such as:
Adapters should be free to map that intent into their own environment:
The important constraint is that the adapter reports back the realized execution workspace metadata in a normalized shape, even if the underlying implementation is not a git worktree.
Use three layers:
project workspaceexecution workspaceworkspace runtime servicesadapter sessionLong-lived repo anchor.
Examples:
./paperclipDerived runtime checkout for a specific issue/run.
Examples:
Long-lived or semi-long-lived processes associated with a workspace.
Examples:
These are not specific to Paperclip. They are a common property of working in a dev workspace, whether local or remote.
Claude/Codex conversation continuity and runtime state, which remains cwd-aware and should follow the execution workspace rather than define it.
Introduce a generic execution workspace strategy in adapter config.
Example shape:
{
"workspaceStrategy": {
"type": "project_primary"
}
}
Or:
{
"workspaceStrategy": {
"type": "git_worktree",
"baseRef": "origin/main",
"branchTemplate": "{{issue.identifier}}-{{slug}}",
"worktreeParentDir": ".paperclip/instances/default/worktrees/projects/{{project.id}}",
"cleanupPolicy": "on_merged",
"startDevServer": true,
"devServerCommand": "pnpm dev",
"devServerReadyUrlTemplate": "http://127.0.0.1:{{port}}/api/health"
}
}
Remote adapters may instead use shapes like:
{
"workspaceStrategy": {
"type": "isolated_checkout",
"provider": "adapter_managed",
"baseRef": "origin/main",
"branchTemplate": "{{issue.identifier}}-{{slug}}"
}
}
The important point is that git_worktree is a strategy value for adapters that can use it, not the universal contract.
Do not model this as a Paperclip-specific devServer flag.
Instead, model it as a generic list of workspace-attached runtime services.
Example shape:
{
"workspaceRuntime": {
"services": [
{
"name": "web",
"description": "Primary app server for this workspace",
"command": "pnpm dev",
"cwd": ".",
"env": {
"DATABASE_URL": "${workspace.env.DATABASE_URL}"
},
"port": {
"type": "auto"
},
"readiness": {
"type": "http",
"urlTemplate": "http://127.0.0.1:${port}/api/health"
},
"expose": {
"type": "url",
"urlTemplate": "http://127.0.0.1:${port}"
},
"reuseScope": "project_workspace",
"lifecycle": "shared",
"stopPolicy": {
"type": "idle_timeout",
"idleSeconds": 1800
}
}
]
}
}
This contract is intentionally generic:
command can start any workspace-attached process, not just a web serverPaperclip should distinguish between:
Examples:
pnpm dev{ pid, port, url }{ sandboxId, previewUrl }Paperclip should normalize the reported metadata without requiring every adapter to look like a host-local process.
Keep issue-level overrides possible through the existing assigneeAdapterOverrides shape in packages/shared/src/types/issue.ts.
Paperclip core should:
Paperclip core should not:
A shared server-side helper should handle local git mechanics:
{ cwd, branchName, url }This helper can be reused by:
codex_localclaude_localThis helper is intentionally for local adapters only. Remote adapters should not be forced through a host-local git helper.
In addition to the local git helper, Paperclip should define a generic runtime service manager contract.
Its job is to:
This manager should not be hard-coded to "dev servers". It should work for any long-lived workspace companion process.
The adapter should:
For example:
codex_local: run inside cwd, likely with --cd or process cwdclaude_local: run inside cwd, optionally use --worktree when it helpsFor runtime services:
Do not create a fully first-class worktrees table yet.
Start smaller by recording derived execution workspace metadata on runs, issues, or both.
Suggested fields to introduce:
executionWorkspaceStrategyexecutionWorkspaceCwdexecutionBranchNameexecutionWorkspaceStatusexecutionServiceRefsexecutionCleanupStatusThese can live first on heartbeat_runs.context_snapshot or adjacent run metadata, with an optional later move into a dedicated table if the UI and cleanup workflows justify it.
For runtime services specifically, Paperclip should eventually track normalized fields such as:
serviceNameserviceKindscopeTypescopeIdstatuscommandcwdenvFingerprintporturlproviderproviderRefstartedByRunIdownerAgentIdlastUsedAtstopPolicyhealthStatusThe first implementation can keep this in run metadata if needed, but the long-term shape is a generic runtime service registry rather than one-off server URL fields.
packages/shared.workspaceStrategy.typebaseRefbranchTemplateworktreeParentDircleanupPolicyuseProjectWorkspace flag working as a lower-level compatibility control.workspaceRuntime.services[] contract with:
Acceptance:
project_primary and git_worktreecontext.paperclipWorkspace for local adapters and into a generic execution-workspace intent payload for adapters that need structured remote realization.Primary touchpoints:
server/src/services/heartbeat.tsAcceptance:
git_worktree strategy:
Acceptance:
Rename this phase conceptually to workspace runtime service lifecycle.
Acceptance:
scopeType: project_workspace | execution_workspace | run | agentscopeIdserviceNamestatuscommand or provider metadatacwd if localenvFingerprintporturlprovider / providerRefownerAgentIdstartedByRunIdlastUsedAtstopPolicyreuseKey, for example:
projectWorkspaceId + serviceName + envFingerprintshared: reusable across runs, usually scoped to project_workspaceephemeral: tied to execution_workspace or runrun scope: stop when run endsexecution_workspace scope: stop when workspace is cleaned upproject_workspace scope: stop on idle timeout, explicit stop, or workspace removalagent scope: stop when ownership is transferred or agent policy requires itAcceptance:
codex_local to consume resolved execution workspace cwd.claude_local to consume resolved execution workspace cwd.--worktree, but keep the shared server-side checkout strategy as canonical for local adapters.Acceptance:
Acceptance:
manualon_doneon_mergedAcceptance:
This is related, but should be tracked separately from the workspace strategy work.
Needed improvement:
codexcoder or claudecoder locally without depending on an already-established browser-auth CLI contextThis should likely take the form of a local operator bootstrap flow, not a weakening of runtime auth boundaries.
codexcoder and claudecoder only.project_primary as the default for all existing agents.codex_local and claude_local without forcing a tool-specific abstraction into core.To keep this tractable, the first implementation should:
project_primary and git_worktreeThat is enough to validate the local product shape without prematurely freezing the wrong abstraction.
Follow-up expansion after that validation: