diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index ac316df2..334ab519 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -175,27 +175,27 @@ describe("worktree helpers", () => { it("rebinds same-repo workspace paths onto the current worktree root", () => { expect( rebindWorkspaceCwd({ - sourceRepoRoot: "/Users/nmurray/paperclip", - targetRepoRoot: "/Users/nmurray/paperclip-pr-432", - workspaceCwd: "/Users/nmurray/paperclip", + sourceRepoRoot: "/Users/example/paperclip", + targetRepoRoot: "/Users/example/paperclip-pr-432", + workspaceCwd: "/Users/example/paperclip", }), - ).toBe("/Users/nmurray/paperclip-pr-432"); + ).toBe("/Users/example/paperclip-pr-432"); expect( rebindWorkspaceCwd({ - sourceRepoRoot: "/Users/nmurray/paperclip", - targetRepoRoot: "/Users/nmurray/paperclip-pr-432", - workspaceCwd: "/Users/nmurray/paperclip/packages/db", + sourceRepoRoot: "/Users/example/paperclip", + targetRepoRoot: "/Users/example/paperclip-pr-432", + workspaceCwd: "/Users/example/paperclip/packages/db", }), - ).toBe("/Users/nmurray/paperclip-pr-432/packages/db"); + ).toBe("/Users/example/paperclip-pr-432/packages/db"); }); it("does not rebind paths outside the source repo root", () => { expect( rebindWorkspaceCwd({ - sourceRepoRoot: "/Users/nmurray/paperclip", - targetRepoRoot: "/Users/nmurray/paperclip-pr-432", - workspaceCwd: "/Users/nmurray/other-project", + sourceRepoRoot: "/Users/example/paperclip", + targetRepoRoot: "/Users/example/paperclip-pr-432", + workspaceCwd: "/Users/example/other-project", }), ).toBeNull(); }); diff --git a/doc/experimental/issue-worktree-support.md b/doc/experimental/issue-worktree-support.md new file mode 100644 index 00000000..8f05ff19 --- /dev/null +++ b/doc/experimental/issue-worktree-support.md @@ -0,0 +1,62 @@ +# Issue worktree support + +Status: experimental, runtime-only, not shipping as a user-facing feature yet. + +This branch contains the runtime and seeding work needed for issue-scoped worktrees: + +- project execution workspace policy support +- issue-level execution workspace settings +- git worktree realization for isolated issue execution +- optional command-based worktree provisioning +- seeded worktree fixes for secrets key compatibility +- seeded project workspace rebinding to the current git worktree + +We are intentionally not shipping the UI for this yet. The runtime code remains in place, but the main UI entrypoints are hard-gated off for now. + +## What works today + +- projects can carry execution workspace policy in the backend +- issues can carry execution workspace settings in the backend +- heartbeat execution can realize isolated git worktrees +- runtime can run a project-defined provision command inside the derived worktree +- seeded worktree instances can keep local-encrypted secrets working +- seeded worktree instances can rebind same-repo project workspace paths onto the current git worktree + +## Hidden UI entrypoints + +These are the current user-facing UI surfaces for the feature, now intentionally disabled: + +- project settings: + - `ui/src/components/ProjectProperties.tsx` + - execution workspace policy controls + - git worktree base ref / branch template / parent dir + - provision / teardown command inputs + +- issue creation: + - `ui/src/components/NewIssueDialog.tsx` + - isolated issue checkout toggle + - defaulting issue execution workspace settings from project policy + +- issue editing: + - `ui/src/components/IssueProperties.tsx` + - issue-level workspace mode toggle + - defaulting issue execution workspace settings when project changes + +- agent/runtime settings: + - `ui/src/adapters/runtime-json-fields.tsx` + - runtime services JSON field, which is part of the broader workspace-runtime support surface + +## Why the UI is hidden + +- the runtime behavior is still being validated +- the workflow and operator ergonomics are not final +- we do not want to expose a partially-baked user-facing feature in issues, projects, or settings + +## Re-enable plan + +When this is ready to ship: + +- re-enable the gated UI sections in the files above +- review wording and defaults for project and issue controls +- decide which agent/runtime settings should remain advanced-only +- add end-to-end product-level verification for the full UI workflow diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbc06553..d1dd1ddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@playwright/test': specifier: ^1.58.2 version: 1.58.2 - cross-env: - specifier: ^10.1.0 - version: 10.1.0 esbuild: specifier: ^0.27.3 version: 0.27.3 @@ -71,9 +68,6 @@ importers: drizzle-orm: specifier: 0.38.4 version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4) - embedded-postgres: - specifier: ^18.1.0-beta.16 - version: 18.1.0-beta.16 picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -327,9 +321,6 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 - cross-env: - specifier: ^10.1.0 - version: 10.1.0 supertest: specifier: ^7.0.0 version: 7.2.2 @@ -998,9 +989,6 @@ packages: cpu: [x64] os: [win32] - '@epic-web/invariant@1.0.0': - resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -3436,11 +3424,6 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-env@10.1.0: - resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} - engines: {node: '>=20'} - hasBin: true - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -6758,8 +6741,6 @@ snapshots: '@embedded-postgres/windows-x64@18.1.0-beta.16': optional: true - '@epic-web/invariant@1.0.0': {} - '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -9274,11 +9255,6 @@ snapshots: crelt@1.0.6: {} - cross-env@10.1.0: - dependencies: - '@epic-web/invariant': 1.0.0 - cross-spawn: 7.0.6 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/ui/src/adapters/runtime-json-fields.tsx b/ui/src/adapters/runtime-json-fields.tsx index 5cbc959b..fe552d74 100644 --- a/ui/src/adapters/runtime-json-fields.tsx +++ b/ui/src/adapters/runtime-json-fields.tsx @@ -2,6 +2,9 @@ import { useEffect, useState } from "react"; import type { AdapterConfigFieldsProps } from "./types"; import { Field, help } from "../components/agent-config-primitives"; +// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. +const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; + const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; @@ -57,6 +60,10 @@ export function RuntimeServicesJsonField({ config, mark, }: JsonFieldProps) { + if (!SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI) { + return null; + } + const existing = formatJsonObject(config.workspaceRuntime); const [draft, setDraft] = useState(existing); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 3aae4c8f..bf55a938 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -20,6 +20,9 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react"; import { AgentIcon } from "./AgentIconPicker"; +// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. +const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; + interface IssuePropertiesProps { issue: Issue; onUpdate: (data: Record) => void; @@ -179,7 +182,9 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp const currentProject = issue.projectId ? orderedProjects.find((project) => project.id === issue.projectId) ?? null : null; - const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null; + const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI + ? currentProject?.executionWorkspacePolicy ?? null + : null; const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled); const usesIsolatedExecutionWorkspace = issue.executionWorkspaceSettings?.mode === "isolated" ? true @@ -435,7 +440,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp onClick={() => { onUpdate({ projectId: p.id, - executionWorkspaceSettings: p.executionWorkspacePolicy?.enabled + executionWorkspaceSettings: SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && p.executionWorkspacePolicy?.enabled ? { mode: p.executionWorkspacePolicy.defaultMode === "isolated" ? "isolated" : "project_primary" } : null, }); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 06a2ec5e..9a9076bc 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -44,6 +44,8 @@ import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySel const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; +// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. +const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; /** Return black or white hex based on background luminance (WCAG perceptual weights). */ function getContrastTextColor(hexColor: string): string { @@ -426,7 +428,9 @@ export function NewIssueDialog() { chrome: assigneeChrome, }); const selectedProject = orderedProjects.find((project) => project.id === projectId); - const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy; + const executionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI + ? selectedProject?.executionWorkspacePolicy + : null; const executionWorkspaceSettings = executionWorkspacePolicy?.enabled ? { mode: useIsolatedExecutionWorkspace ? "isolated" : "project_primary", @@ -472,7 +476,9 @@ export function NewIssueDialog() { const currentPriority = priorities.find((p) => p.value === priority); const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId); const currentProject = orderedProjects.find((project) => project.id === projectId); - const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null; + const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI + ? currentProject?.executionWorkspacePolicy ?? null + : null; const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled); const assigneeOptionsTitle = assigneeAdapterType === "claude_local" @@ -514,7 +520,7 @@ export function NewIssueDialog() { const handleProjectChange = useCallback((nextProjectId: string) => { setProjectId(nextProjectId); const nextProject = orderedProjects.find((project) => project.id === nextProjectId); - const policy = nextProject?.executionWorkspacePolicy; + const policy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? nextProject?.executionWorkspacePolicy : null; executionWorkspaceDefaultProjectId.current = nextProjectId || null; setUseIsolatedExecutionWorkspace(Boolean(policy?.enabled && policy.defaultMode === "isolated")); }, [orderedProjects]); @@ -527,7 +533,11 @@ export function NewIssueDialog() { if (!project) return; executionWorkspaceDefaultProjectId.current = projectId; setUseIsolatedExecutionWorkspace( - Boolean(project.executionWorkspacePolicy?.enabled && project.executionWorkspacePolicy.defaultMode === "isolated"), + Boolean( + SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && + project.executionWorkspacePolicy?.enabled && + project.executionWorkspacePolicy.defaultMode === "isolated", + ), ); }, [newIssueOpen, orderedProjects, projectId]); const modelOverrideOptions = useMemo( diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index dd7e21dd..9237f5e3 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -26,6 +26,9 @@ const PROJECT_STATUSES = [ { value: "cancelled", label: "Cancelled" }, ]; +// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship. +const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false; + interface ProjectPropertiesProps { project: Project; onUpdate?: (data: Record) => void; @@ -707,6 +710,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )} + {SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && ( + <>
@@ -945,6 +950,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )}
+ + )}