Refuse a symlinked board so each directory keeps its own - #28
Conversation
`sidecar init` writes nothing when the board file — or the `.sidecar/` home it sits in — is a symlink, and prints what to do instead. A link points two checkouts at one file, so every session's work piles into a single queue. The check uses Lstat, never Stat: a dangling link reports NotExist, and init would otherwise scaffold straight through it into the link's target. It runs on the path init was given, on a legacy root SIDECAR.md before migration renames it into `.sidecar/`, and on the viewer's own create prompt. Only the board and its `.sidecar/` home are checked, so a project directory behind a symlink still inits normally. The CLAUDE.md note carries the matching rule, so an agent doesn't create the link init refuses. Reading an existing symlinked board is unchanged — the viewer and its watcher still follow the link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZ6tbMtg6729spTZSRqbjw
|
Reviewed against The core is right. Three things worth a look.
|
…skips Move the symlink check in offerCreate below the terminal guard. A piped launch never prompts and never scaffolds, so the refusal had nothing to announce there — and its three lines landed in the primary buffer just before the viewer's alt screen hid them. Its test now asserts the silence when stdin isn't a terminal, and the refusal when it is. Say what else init leaves alone. A live symlinked board also skips the CLAUDE.md note and the reconcile hook, so re-running init upgrades nothing until the link is gone; the message and the README now state that instead of implying the board is the only thing untouched. The README also names the shared previous.md a linked .sidecar/ leaves behind, which is what makes `sidecar diff` report another session's changes as this one's. Name .sidecar/ in the CLAUDE.md rule. "Its directory" read as the project root for a custom board path — a directory the check never looks at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZ6tbMtg6729spTZSRqbjw
|
Three of the four are in
A live symlinked board blocks the upgrade path — intended, and staying. The point of the refusal is that the link itself is the problem; letting a re-run quietly upgrade the note and hook around it leaves the shared queue in place, which is what this PR exists to stop. What was wrong was the wording. The message now ends with "Remove the symlink, then run Shared Nit — the rule now names |
|
Solid, focused PR. The core reasoning is right: Three things, none blocking. a. A dangling legacy The new pre-check refuses on any symlink at
So a leftover broken To make the pre-check match what the code below it can actually do, gate the legacy call on b. The
The wait is also not obviously going to end: Cheapest fix is to let the caller decide — have c. Minor
Tests Good coverage, and |
What
sidecar initnow refuses a board reached through a symlink — the board file itself, or the.sidecar/home it sits in — and writes nothing. TheCLAUDE.mdnote carries the matching rule, so an agent doesn't create the link in the first place.A link points two checkouts at one file. Every session writing there lands in a single queue, and the board turns into noise.
How
symlinkedBoardPathusesLstat, neverStat. A dangling link reportsNotExist, so without itinitfalls through toscaffoldand writes the board at the link's target — in another directory.initwas given, a legacy rootSIDECAR.mdbefore migration renames it into.sidecar/(a symlink moves as a symlink), and the viewer's own create prompt..sidecar/home are checked. A project directory behind a symlink is ordinary and still inits normally.Tests
Seven new tests in
init_test.go: file symlink,.sidecar/directory symlink, dangling link, the legacy-migration hole, the viewer create path, the note carrying the rule, and a guard that a real board under a symlinked parent still inits. Full suite green.🤖 Generated with Claude Code
https://claude.ai/code/session_01MZ6tbMtg6729spTZSRqbjw