updating paths

This commit is contained in:
Dotta
2026-03-10 14:43:09 -05:00
parent 859c82aa12
commit b0b7ec779a
7 changed files with 108 additions and 41 deletions

View File

@@ -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();
});

View File

@@ -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

24
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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);

View File

@@ -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<string, unknown>) => 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,
});

View File

@@ -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<InlineEntityOption[]>(

View File

@@ -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<string, unknown>) => void;
@@ -707,6 +710,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
)}
</div>
{SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && (
<>
<Separator className="my-4" />
<div className="py-1.5 space-y-2">
@@ -945,6 +950,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
)}
</div>
</div>
</>
)}
</div>
</div>