fix(arc-0d80.06mq9p): server-authoritative CWD resolution for git worktrees - #37
Merged
Conversation
ResolveProjectByPath now does longest-prefix component-wise matching with SQL ESCAPE handling. Hand-written query in workspaces.go (sqlc 1.30.0 cannot parse ESCAPE); SQL lifted to a package-level const for auditability.
Swap s.store.ResolveProjectByPath for s.resolveProjectForPath so that linked git worktrees outside a registered workspace prefix are accepted. Add three behavior tests covering prefix match, linked-worktree resolution via DetectMainRepo, and cross-project rejection.
… + escaping) - Add GIT_CONFIG_NOSYSTEM=1 and GIT_CONFIG_GLOBAL=/dev/null to mustRun helper to prevent system-level git config interference on CI - Add explicit t.Cleanup for git worktree removal in LinkedWorktreeOutsideRegistered test - Use fmt.Sprintf with %q for proper JSON path escaping instead of string concatenation
Switch resolveProject handler to use s.resolveProjectForPath so the GET /projects/resolve endpoint gains worktree-aware resolution. Update mockWPStore.ResolveProjectByPath to do longest-prefix match mirroring the real storage semantics, and add TestResolveProject_GitWorktree.
Collapse resolveFromServer to a single ResolveProjectByPath call. Delete resolveBySubdirectory, detectGitMainWorktree, and isSubdirectory which are now dead code — the server handles all path resolution including exact match, longest-prefix match, and git worktree detection.
The new resolveFromServer contract always returns a non-nil error on miss (per spec). Checks for empty projectID after a nil-error path were unreachable dead code. Simplify both call sites and update the resolveProject doc comment to reflect server-side delegation. - main.go line 216: Remove unreachable serverWsID != "" clause - ai.go line 145: Remove unreachable resolvedProjectID == "" clause - main.go line 197: Update doc comment for resolveProject
The LIKE prefix match in ResolveProjectByPath was using raw w.path values concatenated into the pattern, meaning _, %, and \ in workspace paths acted as SQL wildcards and caused false-positive prefix matches. Fix by wrapping w.path in triple-nested REPLACE calls (\ first, then %, then _) so the column value is treated as a literal substring under ESCAPE '\'. Three new regression tests lock in the protection for each metacharacter.
Extend DetectMainRepo to handle the case where a linked worktree's .git pointer resolves to a bare repo (e.g. repo.git/worktrees/<name>) rather than a normal repo (.git/worktrees/<name>). Validates the bare-repo case by checking for the presence of HEAD and objects/ before returning the bare repo path as the canonical main-repo path.
Consolidates three near-identical git test helper implementations across gitfs and api packages into a single hardened package with consistent CI-safety env vars (GIT_CONFIG_NOSYSTEM, GIT_CONFIG_GLOBAL=/dev/null). Removes all duplicated mustRun/initRepo/addWorktree/mustRunGit functions and their redundant t.Cleanup worktree-removal registrations.
- gofmt: trim trailing newline in ai_sessions_test.go - gofumpt: group adjacent var decls in worktree_test.go - gosec G306: tighten test fixture file perms 0o644 → 0o600 - gosec G703: nolint with justification on isBareRepo path joins (dir is a validated absolute gitdir; leaves are constants) - mnd: extract dirPerm const in gittest for 0o755 MkdirAll - go.mod: promote fatih/color to direct (go mod tidy)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
arc-0d80.06mq9p:arc ai session start --stdin(Claude Code SessionStart hook) failed withCWD ... does not resolve to a known projectwhen invoked from a git worktree.internal/gitfspackage (pure-Go worktree detection, nogitbinary or third-party library at runtime). Newinternal/testutil/gittestshared test helpers.Root cause
The CLI's
resolveFromServerhad 4 fallback stages (exact / normalized / git-main-worktree / subdirectory). The server'screateAISessionrevalidated CWDs using only stage 1 (exact match). So a worktree CWD would resolve client-side viadetectGitMainWorktree, then get rejected when the client POSTed the raw worktree path back to the server.Approach
Server becomes authoritative for path resolution (consistent with the existing
workspace_paths.hostnamecolumn that already implies local-per-host scoping):internal/gitfspackage — pure-Go parser for.gitpointer files. Handles normal repos AND bare-repo worktrees. ~30 lines of runtime code, no dependencies.(storage.Storage).ResolveProjectByPathfrom exact-match to component-wise longest-prefix match, with literal_/%/\characters in registered paths properly escaped via SQLREPLACE(ESCAPE '\\'alone wasn't sufficient —w.pathis interpolated, not bound).(*Server).resolveProjectForPathhelper — two-stage resolver (storage prefix match, thengitfs.DetectMainRepofallback for worktrees outside any registered prefix).createAISessionandGET /projects/resolveto the new helper.resolveFromServerto a single server call. Deleted ~88 lines of now-redundant client-side resolution logic.Notable details
ResolveProjectByPathis hand-written (not sqlc) because sqlc 1.30.0 cannot parse SQLite'sESCAPEclause. Documented precedent in CLAUDE.md (ListIssues,GetLabelsForIssues).w.path(the column), not the bound input — the metacharacter source is the registered path, not user input. Without this, a registered path like/home/john_doe/projwould falsely match/home/johnXdoe/projXfile.DetectMainReporecognizes both<workdir>/.git/worktrees/<name>and<repo>.git/worktrees/<name>layouts. Validates the latter by checking forHEADandobjects/.ARC_SERVER=https://remote/for path-related features was already broken by the existinghostnamecolumn scoping; this PR doesn't change that, but documents it via behavior.Test plan
internal/gitfs— 11 tests including bare-repo worktree, malformed.git, relative gitdir, deep subdirectory walkinternal/storage/sqlite/workspaces— 6 tests including longest-prefix-wins, component-boundary rejection, AND negative assertions that_/%in registered paths do NOT act as wildcards (lock the ESCAPE protection)internal/api/ai_sessions_test.go—TestCreateAISession_LinkedWorktreeOutsideRegistereddoes realgit init+git worktree addand verifies end-to-end resolution.TestCreateAISession_RejectCrossProjectconfirms cross-project drift is still rejected (422).internal/api/workspace_paths_test.go—TestResolveProject_GitWorktreeexercises the public REST endpoint against a real linked worktree.make test) green.arc ai session start --stdinfrom any worktree no longer emits the old error. Tests prove the fix in code; activation is a deployment step.Adversarial review pass
After the initial epic landed, an adversarial code review caught two real bugs and one coverage gap that the orthodox per-task review missed:
ESCAPE '\\'clause was performative — it declared\\as the pattern escape character but didn't actually escape_/%/\\in the column value. Fixed via SQLREPLACEchain onw.path. New negative-assertion tests lock it down.DetectMainReposilently failed for bare-repo worktrees. Now supported.GIT_CONFIG_NOSYSTEM/GIT_CONFIG_GLOBALset in two of three). Consolidated intointernal/testutil/gittest.T1's history was squashed (5 commits → 1) to remove a noisy buggy/revert/reapply sandwich; backup branch retained locally.
Files
Closes arc-0d80.06mq9p.