Keep worktree dev state isolated#4555
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2654583. Configure here.
ApprovabilityVerdict: Needs human review This PR changes runtime behavior by modifying how dev servers resolve their home directory, with worktree-local You can customize Macroscope's approvability policy. Learn more. |
A submodule's `.git` is also a file, so the file-vs-directory check alone classified every submodule as a linked worktree and handed it a worktree-local `.t3`. Read the `gitdir:` pointer and require it to land in `.git/worktrees` (compared as path segments, not a substring). Resolve cwd through a HostProcessWorkingDirectory reference rather than calling process.cwd() directly, so the precedence chain is testable. Add tests for the submodule and malformed-`.git`-file cases, plus end-to-end coverage of all four home-precedence outcomes, which had no test before. Document that worktree state lands in `.t3/userdata`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed and pushed fixes in 2a3ef8c. Findings1. Submodules were misclassified as worktrees (correctness bug)
Verified against a real submodule, the helper returned the submodule root, so 2.
3. Precedence chain had no end-to-end test
4. Stale doc claim
Verification
The core design is right and the ambient- |
The `.git/worktrees` segment check rejected worktrees whose common dir is not named `.git`. A worktree of a bare repo points at `<repo>.git/worktrees/<name>`, and $GIT_COMMON_DIR can be anything. Git always uses the `worktrees/<name>` tail, so match on that instead; `<git-dir>/modules/<name>` still fails it. A blank `--home-dir` also counted as a selection under `??`, skipping the worktree default and landing on the shared home — the outcome this precedence exists to prevent. Trim before choosing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/shared/src/devHome.ts`:
- Around line 28-33: Update pointsAtLinkedWorktree() to validate the .git
pointer file using only its first meaningful line, rejecting files with
arbitrary content before a gitdir: declaration instead of accepting later lines.
Preserve valid linked-worktree resolution and add a regression fixture covering
junk followed by a gitdir: line.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9b5bb3d2-5c90-4ecb-a211-9582b446b766
📒 Files selected for processing (9)
.agents/skills/test-t3-app/SKILL.mdAGENTS.mddocs/reference/scripts.mdpackages/shared/package.jsonpackages/shared/src/devHome.test.tspackages/shared/src/devHome.tspackages/shared/src/hostProcess.tsscripts/dev-runner.test.tsscripts/dev-runner.ts
| const gitdir = gitFileContents | ||
| .split(/\r?\n/) | ||
| .map((line) => line.trim()) | ||
| .find((line) => line.startsWith("gitdir:")) | ||
| ?.slice("gitdir:".length) | ||
| .trim(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate devHome =="
fd -a 'devHome\.' . | sed 's#^\./##'
echo "== diff/stat summary =="
git diff --stat || true
echo "== inspect packages/shared/src/devHome.ts =="
if [ -f packages/shared/src/devHome.ts ]; then
nl -ba packages/shared/src/devHome.ts | sed -n '1,140p'
fi
echo "== search devHome usages/tests =="
rg -n "devHome|linked worktree|worktrees|gitdir|\.t3" packages/shared -S || trueRepository: pingdotgg/t3code
Length of output: 390
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== packages/shared/src/devHome.ts =="
cat -n packages/shared/src/devHome.ts | sed -n '1,160p'
echo "== related tests =="
cat -n packages/shared/src/devHome.test.ts | sed -n '1,220p'
echo "== behavioral parser probe from source text =="
python3 - <<'PY'
raw = open('packages/shared/src/devHome.ts', encoding='utf-8').read()
print('finds split/find:', 'find((line) => line.startsWith("gitdir:"))' in raw)
print('contains .split limit:', re := __import__('re'); bool(re.search(r'split\([^;]*,\\s*1\\s*\)', raw)))
cases = {
'single': 'gitdir: /repo/path\n',
'junk_after': 'junk\n gitdir: /repo/path \n',
'junk_before': '\n junk\n gitdir: /repo/path \n',
'no_gitdir': 'junk\n',
'empty': '',
}
def current(contents):
gitdir = (
contents
.split('\n')
.map(lambda line: line.strip())
.find(lambda line: line.startswith('gitdir:'))
.removeprefix('gitdir:')
.strip()
if any(line.strip().startswith('gitdir:') for line in contents.split('\n')) else None
)
return gitdir
for name, value in cases.items():
print(name, repr(current(value)))
PYRepository: pingdotgg/t3code
Length of output: 10757
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== focused test command availability/state =="
command -v vp || true
command -v find || true
echo "== deterministic scan behavior for parser condition =="
python3 - <<'PY'
samples = [
("main .git file", "git\nrefs\n"),
("single valid pointer", "gitdir: /elsewhere/.git/worktrees/x\n"),
("malformed pointer with later valid line", "junk\n gitdir: /elsewhere/.git/worktrees/x \n"),
("valid pointer with junk line", "gitdir: /elsewhere/.git/worktrees/x\njunk\n"),
("submodule pointer", "gitdir: ../.git/modules/sub\n"),
("empty unreadable", ""),
]
def current_gitdir(contents):
gitdir = next(
(line.strip().removeprefix("gitdir:").strip()
for line in contents.split("\n") if line.strip().startswith("gitdir:")),
None,
)
if gitdir is None:
return None
segments = gitdir.replace("\\", "/").rstrip("/").split("/")
return None if len(segments) < 3 or segments[-2] != "worktrees" else True, gitdir
def proposed_gitdir(contents):
first = next(iter(contents.split("\n", 1)), "")
s = first.strip()
gitdir = s.removeprefix("gitdir:").strip() if s.startswith("gitdir:") else None
if gitdir is None:
return None
segments = gitdir.replace("\\", "/").rstrip("/").split("/")
return None if len(segments) < 3 or segments[-2] != "worktrees" else True, gitdir
print("name|current_worktree|current_gitdir|proposed_worktree|proposed_gitdir")
for name, contents in samples:
c = current_gitdir(contents)
p = proposed_gitdir(contents)
print(f"{name}|{c[0] if c[0] is not None else None}|{c[1]}|{p[0] if p[0] is not None else None}|{p[1]}")
PY
echo "== check package scripts/tests =="
cat -n package.json | sed -n '1,160p' 2>/dev/null || true
cat -n packages/shared/package.json | sed -n '1,160p' 2>/dev/null || trueRepository: pingdotgg/t3code
Length of output: 464
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic scan behavior for .git parser =="
python3 - <<'PY'
samples = [
("main .git file", "git\nrefs\n"),
("single valid pointer", "gitdir: /elsewhere/.git/worktrees/x\n"),
("malformed pointer with later valid line", "junk\n gitdir: /elsewhere/.git/worktrees/x \n"),
("valid pointer with junk line", "gitdir: /elsewhere/.git/worktrees/x\njunk\n"),
("submodule pointer", "gitdir: ../.git/modules/sub\n"),
("empty unreadable", ""),
]
def current_result(contents):
for line in contents.split("\n"):
s = line.strip()
if s.startswith("gitdir:"):
gitdir = s.removeprefix("gitdir:").strip()
segments = gitdir.replace("\\", "/").rstrip("/").split("/")
return (len(segments) >= 3 and segments[-2] == "worktrees"), gitdir
return None, None
def proposed_result(contents):
first = next(iter(contents.split("\n", 1)), "")
s = first.strip()
if not s.startswith("gitdir:"):
return None, None
gitdir = s.removeprefix("gitdir:").strip()
segments = gitdir.replace("\\", "/").rstrip("/").split("/")
return (len(segments) >= 3 and segments[-2] == "worktrees"), gitdir
print("name|current_worktree|current_gitdir|proposed_worktree|proposed_gitdir")
for name, contents in samples:
c = current_result(contents)
p = proposed_result(contents)
print(f"{name}|{c[0]}|{c[1]}|{p[0]}|{p[1]}")
PY
echo "== package.json scripts =="
if [ -f package.json ]; then
python3 - <<'PY'
import json
from pathlib import Path
for path in [Path("package.json"), Path("packages/shared/package.json")]:
if path.exists():
data = json.loads(path.read_text())
print(path)
print(json.dumps(data.get("scripts", {}), indent=2, sort_keys=True))
PY
fiRepository: pingdotgg/t3code
Length of output: 3358
Reject malformed .git pointer files.
pointsAtLinkedWorktree() accepts later gitdir: lines after arbitrary content, so .git with junk\ngitdir: /elsewhere/.git/worktrees/x can be treated as a linked worktree. Parse only the first pointer line and add a regression fixture.
Proposed fix
- const gitdir = gitFileContents
- .split(/\r?\n/)
- .map((line) => line.trim())
- .find((line) => line.startsWith("gitdir:"))
- ?.slice("gitdir:".length)
- .trim();
+ const [pointerLine = ""] = gitFileContents.split(/\r?\n/, 1);
+ const gitdir = pointerLine.trim().startsWith("gitdir:")
+ ? pointerLine.trim().slice("gitdir:".length).trim()
+ : undefined;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const gitdir = gitFileContents | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .find((line) => line.startsWith("gitdir:")) | |
| ?.slice("gitdir:".length) | |
| .trim(); | |
| const [pointerLine = ""] = gitFileContents.split(/\r?\n/, 1); | |
| const gitdir = pointerLine.trim().startsWith("gitdir:") | |
| ? pointerLine.trim().slice("gitdir:".length).trim() | |
| : undefined; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/shared/src/devHome.ts` around lines 28 - 33, Update
pointsAtLinkedWorktree() to validate the .git pointer file using only its first
meaningful line, rejecting files with arbitrary content before a gitdir:
declaration instead of accepting later lines. Preserve valid linked-worktree
resolution and add a regression fixture covering junk followed by a gitdir:
line.
Source: Coding guidelines

What
.t3directory.--home-dirauthoritative while preventing an ambientT3CODE_HOMEfrom selecting the installed app’s live data.Why
A dev command launched from a linked worktree could otherwise inherit the shared application home and write development state into the database used by the installed app.
Impact
This is the foundational slice of the dev-server overhaul. It changes only worktree state isolation and is the base for the two follow-up PRs.
Verification
vp test run packages/shared/src/devHome.test.ts scripts/dev-runner.test.ts(32 tests)vp run --filter=@t3tools/shared typecheckNote
Isolate dev state to the linked git worktree's
.t3directory by defaultresolveGitWorktreePathandresolveWorktreeT3Homeutilities inpackages/shared/src/devHome.tsto detect when the process runs inside a linked git worktree and derive its local.t3path.scripts/dev-runner.tsto apply a new precedence order: explicit--home-dir> worktree-local<worktree>/.t3> ambientT3CODE_HOME. Blank--home-diris treated as unset.T3CODE_HOMEfallback from the--home-dirCLI flag; absence of the flag now delegates to the new precedence logic.AGENTS.mdto document the new precedence rules, including the note that submodules are not worktrees.T3CODE_HOMEunless--home-diris absent and no worktree.t3is detected.Macroscope summarized 6404fc1.
Note
Medium Risk
Changes which SQLite database dev commands use in worktrees (behavioral shift away from shared/live userdata); logic is well-tested but mis-detection could still point at the wrong home.
Overview
Linked git worktrees now get an isolated dev data directory at
<worktree>/.t3(userdata under<worktree>/.t3/userdata) when you runvp run dev, so feature work does not inherit the installed app’s shared home.The dev runner resolves home in order: explicit
--home-dir(whitespace-only counts as unset), then worktree.t3, then ambientT3CODE_HOME. Worktree detection walks up from cwd, treats a.gitfile whosegitdir:ends inworktrees/<name>as a linked worktree (not main checkout, submodules, or invalid pointers), and is implemented in new@t3tools/shared/devHomehelpers with tests.createDevRunnerEnvno longer merges ambientT3CODE_HOMEwhen no explicit base dir is passed; precedence is applied inrunDevRunnerWithInputusing injectableHostProcessWorkingDirectory. Agent docs andtest-t3-app/ scripts reference reflect the new default and pairing/SQLite paths for worktree-local state.Reviewed by Cursor Bugbot for commit 6404fc1. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
.t3directory by default.--home-dir, worktree.t3, then ambient configuration.Documentation
Tests