fix(server): skip execution-workspace inheritance from a terminal parent issue
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for work
> - Creating a child issue inherits the parent's execution workspace and
forces executionWorkspacePreference to reuse_existing
> - The inheritance source was never checked for status, so a child of a
cancelled or done issue bound itself to a worktree that was already
torn down or carried abandoned state
> - This pull request reads the source issue status and skips only the
execution-workspace hand-down for terminal sources; project and
project-workspace grouping still inherit
> - The benefit is that a child of a finished issue gets a fresh
workspace instead of inheriting a dead one
## Linked Issues or Issue Description
Refs #11464 — covers the terminal-status shape; the implicit live-parent
sharing noted in the issue comments is left as designed (children share
the live parent's worktree deliberately).
## What Changed
- server/src/services/issues.ts: getWorkspaceInheritanceIssue now reads
the source issue status, and the create path skips execution-workspace
inheritance when that status is done or cancelled. The project and
projectWorkspaceId hand-downs are unchanged.
- server/src/__tests__/issues-service.test.ts: a cancelled parent no
longer passes its executionWorkspaceId, preference, or settings to a
child created through parentId, while projectWorkspaceId still
inherits.
## Verification
- pnpm --filter @paperclipai/server vitest run
src/__tests__/issues-service.test.ts — 128 tests pass (embedded
Postgres).
## Risks
- Low risk. Only the terminal-source case changes; live parents behave
exactly as before, and an explicit executionWorkspaceId or
inheritExecutionWorkspaceFromIssueId caller override is untouched.
## Model Used
- Anthropic Claude — SWE-2 Max agent via Devin CLI, tool use and code
execution.
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
4042eb1c48
commit
e92cff7bfe
|
|
@ -3683,6 +3683,75 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("does not inherit the execution workspace from a cancelled parent issue", async () => {
|
||||
const companyId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
const parentIssueId = randomUUID();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
||||
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
companyId,
|
||||
name: "Workspace project",
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Primary workspace",
|
||||
});
|
||||
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Cancelled parent worktree",
|
||||
status: "archived",
|
||||
providerType: "git_worktree",
|
||||
closedAt: new Date(),
|
||||
});
|
||||
|
||||
await db.insert(issues).values({
|
||||
id: parentIssueId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
title: "Cancelled parent issue",
|
||||
status: "cancelled",
|
||||
priority: "medium",
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: {
|
||||
mode: "isolated_workspace",
|
||||
},
|
||||
});
|
||||
|
||||
const child = await svc.create(companyId, {
|
||||
parentId: parentIssueId,
|
||||
projectId,
|
||||
title: "Child issue",
|
||||
});
|
||||
|
||||
// Grouping still inherits; the dead worktree does not.
|
||||
expect(child.projectWorkspaceId).toBe(projectWorkspaceId);
|
||||
expect(child.executionWorkspaceId).toBeNull();
|
||||
expect(child.executionWorkspacePreference).toBeNull();
|
||||
});
|
||||
|
||||
it("createChild applies parent defaults, acceptance criteria, workspace inheritance, and optional parent blocker chaining", async () => {
|
||||
const companyId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
|
|
|
|||
|
|
@ -2678,6 +2678,7 @@ async function getWorkspaceInheritanceIssue(
|
|||
projectWorkspaceId: issues.projectWorkspaceId,
|
||||
executionWorkspaceId: issues.executionWorkspaceId,
|
||||
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
||||
status: issues.status,
|
||||
})
|
||||
.from(issues)
|
||||
.where(and(eq(issues.id, issueId), eq(issues.companyId, companyId)))
|
||||
|
|
@ -9954,11 +9955,18 @@ export function issueService(db: Db) {
|
|||
) {
|
||||
projectWorkspaceId = workspaceSource.projectWorkspaceId;
|
||||
}
|
||||
// A terminal source issue (done or cancelled) must not pass its
|
||||
// execution workspace down: the worktree is already torn down or
|
||||
// carries abandoned state, and `reuse_existing` would bind the new
|
||||
// issue to it. Grouping fields still inherit — the child remains
|
||||
// topically part of the same project — only the execution
|
||||
// worktree falls back to a fresh one.
|
||||
if (
|
||||
inheritsSourceProject &&
|
||||
isolatedWorkspacesEnabled &&
|
||||
!hasExplicitExecutionWorkspaceOverride &&
|
||||
workspaceSource.executionWorkspaceId
|
||||
workspaceSource.executionWorkspaceId &&
|
||||
!["done", "cancelled"].includes(workspaceSource.status)
|
||||
) {
|
||||
const sourceWorkspace = await tx
|
||||
.select({
|
||||
|
|
|
|||
Loading…
Reference in New Issue