Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions packages/guardrails/profile/plugins/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,26 +271,34 @@ async function yardadd(dir: string, id: string) {
const base = yard(dir)
const next = path.join(base, slug(id))
await mkdir(base, { recursive: true })

// Verify repository has commits
const head = await git(dir, ["rev-parse", "--verify", "HEAD"])
if (head.code !== 0) {
throw new Error("Cannot create worktree: repository has no commits. Create an initial commit first.")
}
const made = await git(dir, ["worktree", "add", "--detach", next, "HEAD"])
if (made.code !== 0) throw new Error(made.err || made.out || "Failed to create git worktree")
// Verify worktree actually has files (Issue #144: empty git init)
const files = await git(next, ["ls-files", "--cached"]).catch(() => ({ code: 1, out: "", err: "" }))
if (files.code !== 0 || !files.out.trim()) {
// Worktree might be empty — force checkout HEAD contents
const checkout = await git(next, ["checkout", "HEAD", "--", "."])
if (checkout.code !== 0) {
throw new Error(`Worktree created but checkout failed: ${checkout.err || checkout.out}`)
}
// Re-verify files are present
const recheck = await git(next, ["ls-files", "--cached"]).catch(() => ({ code: 1, out: "", err: "" }))
if (recheck.code !== 0 || !recheck.out.trim()) {
throw new Error("Worktree is still empty after checkout — cannot proceed with delegation")
}

// Step 1: Create worktree without checking out files (upstream pattern)
const made = await git(dir, ["worktree", "add", "--detach", "--no-checkout", next, "HEAD"])
if (made.code !== 0) {
await git(dir, ["worktree", "remove", "--force", next]).catch(() => {})
throw new Error(made.err || made.out || "Failed to create git worktree")
Comment on lines +282 to +285
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup on Step 1 failure isn’t guaranteed to remove a partially-created directory: git worktree remove only works for worktrees that were successfully registered, so if worktree add fails before registration you can still end up with a leftover next/ directory that will break subsequent retries. Consider adding a filesystem fallback (scoped to the yard(dir) base) to delete next when worktree remove fails/no-ops, and/or explicitly handle the “not a working tree” case.

Copilot uses AI. Check for mistakes.
}

// Step 2: Hard reset to populate working directory (upstream pattern)
const populated = await git(next, ["reset", "--hard"])
if (populated.code !== 0) {
await git(dir, ["worktree", "remove", "--force", next]).catch(() => {})
throw new Error(`Worktree created but population failed: ${populated.err || populated.out}`)
}

// Step 3: Verify files are actually present in the working directory
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Step 3 comment says this verifies files “present in the working directory”, but git ls-files --cached checks the index (tracked paths), not the filesystem contents. Either adjust the comment to match what’s being checked, or switch to a check that actually validates working tree files if that’s the intent.

Suggested change
// Step 3: Verify files are actually present in the working directory
// Step 3: Verify the worktree has tracked files in Git after reset

Copilot uses AI. Check for mistakes.
const check = await git(next, ["ls-files", "--cached"])
if (check.code !== 0 || !check.out.trim()) {
await git(dir, ["worktree", "remove", "--force", next]).catch(() => {})
throw new Error("Worktree is empty after reset --hard — cannot proceed with delegation")
}

return next
}

Expand Down
Loading