fix(cli): stop worktree init --force from wiping repo worktrees/ (#6240)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Each working tree gets an isolated Paperclip instance; the CLI's
`paperclip worktree init` is what bootstraps that instance and writes
`<repo>/.paperclip/config.json` + `.env`
> - When `--force` was passed, the init path tried to "start clean" by
recursively removing the entire `<repo>/.paperclip/` directory before
rewriting those two files
> - But `<repo>/.paperclip/` also holds `worktrees/`, which contains
every repo-managed worktree checkout (70+ on this machine). The
recursive rm silently nuked all of them.
> - This PR narrows the `--force` reset so it only deletes the two files
it's about to rewrite (`config.json`, `.env`), instead of wiping the
whole `repoConfigDir`
> - It also adds a regression test that drops a sentinel file into
`<repoConfigDir>/worktrees/` and asserts it survives a `--force` init
> - The benefit is that `worktree init --force` becomes safe to run from
inside the main repo without destroying every sibling worktree checkout

## What Changed

- `cli/src/commands/worktree.ts`: in the `--force` branch of
`runWorktreeInit`, replace `rmSync(paths.repoConfigDir, { recursive:
true, force: true })` with targeted removals of `paths.configPath` and
`paths.envPath`. `paths.instanceRoot` removal is unchanged — that path
is per-instance and safe to wipe.
- `cli/src/__tests__/worktree.test.ts`: new regression test that seeds a
fake `worktrees/<name>/` checkout inside the repo's `.paperclip/` and
verifies `runWorktreeInit({ force: true, ... })` does not delete it.

## Verification

- `pnpm --filter @paperclip/cli test -- worktree` — the new regression
test fails on the old code and passes on the fix
- Manual: from a repo checkout, `npx paperclipai worktree init --force
…` no longer removes `<repo>/.paperclip/worktrees/`; only `config.json`
and `.env` are rewritten

## Risks

- Low. The change strictly narrows what `--force` removes. Any caller
that depended on `--force` also wiping unrelated files under
`<repo>/.paperclip/` (there shouldn't be any — it's documented as just
config + env) would see those files persist. `instanceRoot` cleanup is
unchanged.

## Model Used

- Claude (Anthropic), model `claude-opus-4-7`, ~200K context,
extended-thinking + tool-use enabled, run via the Paperclip
`claude_local` adapter.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — CLI-only fix)
- [x] I have updated relevant documentation to reflect my changes (no
doc surface affected)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley
2026-05-17 22:12:56 -07:00
committed by GitHub
parent 734385102c
commit 242a2c2f2b
2 changed files with 45 additions and 1 deletions
+39
View File
@@ -512,6 +512,45 @@ describe("worktree helpers", () => {
}
});
it("preserves repo-managed worktree checkouts when --force re-runs from the source repo", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-force-preserve-"));
const repoRoot = path.join(tempRoot, "repo");
const originalCwd = process.cwd();
try {
fs.mkdirSync(repoRoot, { recursive: true });
const repoConfigDir = path.join(repoRoot, ".paperclip");
fs.mkdirSync(repoConfigDir, { recursive: true });
fs.writeFileSync(path.join(repoConfigDir, "config.json"), "stale", "utf8");
fs.writeFileSync(path.join(repoConfigDir, ".env"), "STALE=1", "utf8");
// Simulate the repo-managed worktrees subfolder that holds every
// worktree checkout (the directory PAPA-358 reported as nuked).
const worktreesDir = path.join(repoConfigDir, "worktrees");
const checkoutDir = path.join(worktreesDir, "PAP-100-feature");
fs.mkdirSync(checkoutDir, { recursive: true });
const sentinelPath = path.join(checkoutDir, "sentinel.txt");
fs.writeFileSync(sentinelPath, "do-not-delete", "utf8");
process.chdir(repoRoot);
await worktreeInitCommand({
seed: false,
force: true,
fromConfig: path.join(tempRoot, "missing", "config.json"),
home: path.join(tempRoot, ".paperclip-worktrees"),
});
expect(fs.existsSync(sentinelPath)).toBe(true);
expect(fs.readFileSync(sentinelPath, "utf8")).toBe("do-not-delete");
expect(fs.existsSync(path.join(repoConfigDir, "config.json"))).toBe(true);
expect(fs.readFileSync(path.join(repoConfigDir, "config.json"), "utf8")).not.toBe("stale");
} finally {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
itEmbeddedPostgres(
"seeds authenticated users into minimally cloned worktree instances",
async () => {
+6 -1
View File
@@ -1385,7 +1385,12 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
}
if (opts.force) {
rmSync(paths.repoConfigDir, { recursive: true, force: true });
// Only remove the specific files we're about to rewrite, not the whole
// repoConfigDir — that directory can contain sibling state such as
// <repo>/.paperclip/worktrees/ holding every repo-managed worktree
// checkout, and a recursive rmSync here would nuke them all.
rmSync(paths.configPath, { force: true });
rmSync(paths.envPath, { force: true });
rmSync(paths.instanceRoot, { recursive: true, force: true });
}