| 1 | # π€ Calendar |
| 2 | |
| 3 | A calendar for one team, on one instance. Alongside `oficina/docs` and |
| 4 | `oficina/sheets`, on the same platform, in the same shape. |
| 5 | |
| 6 | This file is binding. Where it disagrees with a general skill, this file |
| 7 | wins. Where it disagrees with the design spec, the spec wins and this |
| 8 | file is wrong β fix it in the same commit. |
| 9 | |
| 10 | **The design:** `docs/superpowers/specs/2026-09-02-calendar-design.md`. |
| 11 | The raw material it came from, including the rejected alternatives, is |
| 12 | `docs/superpowers/brainstorms/2026-09-02-calendar-brainstorm.md`. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## The one rule |
| 17 | |
| 18 | > **An event's contents are sealed at rest. The bucket never holds a |
| 19 | > plaintext title, place or note.** |
| 20 | |
| 21 | The falsifiable form: **publish this database in full and the worst |
| 22 | anyone learns is the shape of the team's time** β when they are busy, |
| 23 | for how long, how often, and in which zones β **and who is on the team. |
| 24 | Never what any event is, where it is, or what was written about it.** |
| 25 | |
| 26 | `TestTheDatabaseNeverHoldsAPlaintextTitle` enforces it from the first |
| 27 | commit, and it greps the raw SQLite bytes and the WAL rather than |
| 28 | asking SQLite: a column that was never sealed looks identical through |
| 29 | Go and completely different on disk. It checkpoints first, or it would |
| 30 | pass by reading a file the data has not reached. |
| 31 | |
| 32 | ### What this rule does NOT cover, said here so it is never read as covering it |
| 33 | |
| 34 | 1. **Times are plaintext, and free/busy is a real leak.** Every view is |
| 35 | a range query, `NextDue` reads `MIN(due_at)` before any request, and |
| 36 | the platform's own timer registry holds reminder instants anyway. |
| 37 | Sealing the instants would claim a property the estate does not |
| 38 | have. A published database gives up the team's whole free/busy. |
| 39 | 2. **A reminder email carries the title** through the relay and into a |
| 40 | mailbox. That is delivery, not storage, and it is the product |
| 41 | working β but the relay's logs are not ours. |
| 42 | 3. **A subscription feed puts plaintext in whoever's account it was |
| 43 | pasted into.** The share page says so before the button, not after. |
| 44 | 4. **A compromised live host reads everything.** We never claim "we |
| 45 | cannot read your data." |
| 46 | 5. **A PUBLISHED booking page is a live free/busy oracle for that host, |
| 47 | to anyone, without the database.** Every calendar the host can see |
| 48 | shapes the answer. This is a NEW disclosure, not the one above, and |
| 49 | the Publish button says so in a sentence before it is pressed. |
| 50 | |
| 51 | --- |
| 52 | |
| 53 | ## Settled decisions |
| 54 | |
| 55 | | # | Ruling | Rejected | Who, when | |
| 56 | |---|---|---|---| |
| 57 | | 1 | **Store what the person said**: wall-clock time plus IANA zone is the record; the UTC columns are a derived index | Storing the instant as truth β a tzdata update then moves a meeting and nobody can say what was meant | This session, 2026-09-02 | |
| 58 | | 2 | *(Superseded 2026-09-29: the suite's theme is estilo's Calma β estilo AGENTS.md row 8 β so Calendar now serves `theme.ForApp("calendar")`, written to static/theme.css by scripts/writeestilo and pinned by TestThemeIsEstilosForCalendar.)* Pin rastrillo v0.23.0 and take theme `day` directly from `ui.ThemeCSS` | Consuming `estilo`, which is `day` byte-for-byte today and pins v0.22.0 β adopting it buys nothing and drags us back a release ("the two-pins trap", estilo's own AGENTS.md) | This session, 2026-09-02 | |
| 59 | | 3 | This app's own components take `cal-` | `rst-` is the framework's and a future release may claim a name; `estilo-` is the suite's, and a month grid is not a suite component until a second app copies one | This session, 2026-09-02 | |
| 60 | | 4 | The recurrence subset is the six a picker offers; anything else is stored verbatim, flagged, and not expanded | Refusing a rule we cannot expand β an importer that eats a rule it does not understand eats data | This session, 2026-09-02 | |
| 61 | | 5 | Reminders are per PERSON, not per event | Per-event reminders β two people on one meeting want different warnings, and the fix needs a person column anyway | This session, 2026-09-02 | |
| 62 | | 6 | Delivery is at-least-once; every ambiguous case resolves towards sending | At-most-once β a reminder that never arrives is worse than one that arrives twice | This session, 2026-09-02 | |
| 63 | | 7 | Local sign-in is a magic link, with no passkey second factor | Passkeys β `passkey.Config` derives its relying party from this instance's host, so one person in three team instances enrols three credentials. Same ruling docs made | This session, 2026-09-02 | |
| 64 | | 7a | Local sign-in uses Keymail for an address whose domain runs Keymail, and the magic link for every other address β chosen by the domain alone, never by the allowlist (an oracle otherwise). The Keymail hop is a continuation page, never a 303 out of the form POST (Safari applies form-action to it) | Forcing every address to a magic link | Paul, 2026-10-06 ("I do want it to use keymail") | |
| 65 | | 8 | A new event defaults to the person's PERSONAL calendar | Defaulting to the team calendar β the cost of forgetting to change a dropdown should fall on the side of privacy | This session, 2026-09-02 | |
| 66 | | H1 | **Optional Home sign-in connects to an existing person after fresh local proof and explicit confirmation; local sign-in remains available** | Treating the Home subject as an email or allocating a new person | User-approved suite connection, 2026-09-10 | |
| 67 | | B1 | A booking is confirmed by a BUTTON on the page the mailed link opens β a same-origin POST β never on submit and never on GET | Instant confirmation; confirming on the GET, which every link-scanning mail gateway would press | Booking spec Β§1, 2026-09-07 | |
| 68 | | B2 | Everything the host can see is busy β every calendar they hold a grant on | Personal only | Booking spec Β§1, 2026-09-07 | |
| 69 | | B4 | The public identifier is a chosen plaintext `handle` plus a GENERATED page code | A chosen slug β prose joined to events | Booking spec Β§1, 2026-09-07 | |
| 70 | | B5 | Booking mail is the ONE outbound to a non-team address, and only to an address the recipient typed in order to receive it | Emailing quick-add attendees "while we're at it" | Booking spec Β§1, 2026-09-07 | |
| 71 | | B8 | A held slot is a tentative EVENT | A holds table the slot query consults | Booking spec Β§1, 2026-09-07 | |
| 72 | | B11 | The EVENT's `sequence` is the one revision counter, bumped as `sequence + 1`; every booking-side write names the revision it expects | A booking sequence; a stale-read `+1` | Booking spec Β§1, 2026-09-07 | |
| 73 | | B12 | Hold and reschedule serialise on a process mutex AND take SQLite's write lock before the final busy read, inside the transaction that inserts | The mutex alone; a busy read before `BEGIN` | Booking spec Β§1, 2026-09-07 | |
| 74 | | B13 | Mail is a queue table, one row per (booking, kind, recipient, revision), claimed with an identity and a lease, acknowledged conditionally on that identity | A marker column | Booking spec Β§1, 2026-09-07 | |
| 75 | | B14 | The management token is 32 random bytes, hex, stored SEALED as well as hashed | Hash only, as feeds do | Booking spec Β§1, 2026-09-07 | |
| 76 | | B15 | A hold expires at the EARLIER of thirty minutes and the meeting's start; confirmation requires now < start | Thirty minutes flat | Booking spec Β§1, 2026-09-07 | |
| 77 | | B16 | No sender error is "definite"; nothing is freed because a send failed | Marking holds failed | Booking spec Β§1, 2026-09-07 | |
| 78 | | B17 | A slot is the same length on the wall clock and the elapsed clock, or it is not offered | Round-tripping the start only | Booking spec Β§1, 2026-09-07 | |
| 79 | | B18 | A booked event's scheduling β start, end, tz, all_day, kind, rrule β belongs to the booking flow; the host's form shows them disabled and a save that changes any is refused | Invariant checks on host-supplied times | Booking spec Β§1, 2026-09-07 | |
| 80 | | B19 | Mail says the CURRENT state; a transition deletes every unsent row the new state makes false | Historical snapshots per row | Booking spec Β§1, 2026-09-07 | |
| 81 | | B20 | The booker sees the page's title and description AS THEY WERE at hold time, snapshotted sealed onto the booking | Live page text | Booking spec Β§1, 2026-09-07 | |
| 82 | | B21 | Reschedule writes `tz` = the host's current zone atomically with both local strings and both UTC columns | Leaving `tz` as it was | Booking spec Β§1, 2026-09-07 | |
| 83 | | B22 | Both queues carry a random `claim_id`; every completion mutation is conditional on it and built from the row the claim returned; reminders retry through `retry_at`, never `due_at`; `ErrMailBusy` defers without counting an attempt | Claim by timestamp; retry written into `due_at` | Booking spec Β§1, 2026-09-07 | |
| 84 | | B23 | Availability FAILS CLOSED: a busy series that could not be expanded to completion, or whose rule this app cannot parse, is a 503, never "free" | Trusting `Expand`'s slice | Booking spec Β§1, 2026-09-07 | |
| 85 | | B24 | Reschedule repairs the event's reminder queue rows INSIDE its transaction | After commit | Booking spec Β§1, 2026-09-07 | |
| 86 | | B25 | A booked event's form carries its scheduling snapshot as hidden inputs, including the event's own `tz`, with the controls disabled | Disabled controls alone, which submit nothing | Booking spec Β§1, 2026-09-07 | |
| 87 | | B26 | Reschedule enumerates the event's reminder RULES and upserts a queue row for each | Rebuilding the rows that exist | Booking spec Β§1, 2026-09-07 | |
| 88 | |
| 89 | Don't relitigate one in a drive-by. |
| 90 | |
| 91 | --- |
| 92 | |
| 93 | ## Mechanical rules |
| 94 | |
| 95 | Rules here are enforced by tests, not by memory. If you find yourself |
| 96 | relying on remembering one, make it a test instead. |
| 97 | |
| 98 | **Time** |
| 99 | - **The record is `start_local` + `tz`. The UTC columns are an index.** |
| 100 | Anything that writes a UTC column without deriving it from the local |
| 101 | pair is a bug. |
| 102 | - **DST has no correct answer and Go does not promise one.** `clock.go` |
| 103 | documents what actually happens and `clock_test.go` pins it to real |
| 104 | unix numbers. The gap and the fold resolve DIFFERENTLY β the gap takes |
| 105 | the pre-transition offset, the fold takes the second occurrence. Do |
| 106 | not reason about one from the other. |
| 107 | - **A day is 23, 24 or 25 hours.** Use `DayBounds`, never `Add(24h)`. |
| 108 | - **`Zone()` takes bounded input on public routes:** `CommonZones` plus |
| 109 | the host's own zone. Its cache holds successes only. |
| 110 | - **A booking slot is the same length on the wall clock and the elapsed |
| 111 | clock, or it is not offered.** Both ends round-trip through `ToUTC`. |
| 112 | - **Every range is half-open.** An inclusive end is how a one-day event |
| 113 | shows on two days and how a week query double-counts its boundary. |
| 114 | - **All-day events are DATES.** Never a datetime at UTC midnight, which |
| 115 | puts a birthday on the wrong day west of Greenwich. |
| 116 | |
| 117 | **Recurrence** |
| 118 | - **COUNT counts GENERATED occurrences; EXDATE removes afterwards.** |
| 119 | - **`Expand` may stop early.** Anything that decides FREE time reads |
| 120 | `EventsInRangeStrict` and fails closed on `ErrIncompleteExpansion`. |
| 121 | - **A day span is a difference of CIVIL dates, never elapsed hours over |
| 122 | 24.** And advancement carries the series' ORIGINAL clock onto each |
| 123 | new date β never the previous occurrence's resolved time. |
| 124 | - **A named date a month does not have is NO occurrence** β never |
| 125 | clamped. The 31st, 29 February, the fifth Friday. |
| 126 | - **Overrides are keyed by the occurrence's ORIGINAL local time and |
| 127 | rendered at their new one.** The range query is two passes because of |
| 128 | it. Skip the second and a moved occurrence vanishes; skip the dedupe |
| 129 | and it appears twice. |
| 130 | - Expansion iterates in **wall clock**. Stepping a UTC instant moves a |
| 131 | 09:00 standup by an hour for half the year. |
| 132 | |
| 133 | **Data** |
| 134 | - Every scoped read goes through a grant join. **A row that isn't yours |
| 135 | 404s, never 403s** β including for a write, so status codes cannot |
| 136 | enumerate the team's calendars. |
| 137 | - **Booker name, address, note and TOKEN are sealed; the address has |
| 138 | the keyed fingerprint. A page's public code is generated, never a |
| 139 | chosen slug β a slug is prose. Booking windows are plaintext: |
| 140 | shape-of-time.** |
| 141 | - **Every booking transition is a conditional UPDATE naming the state |
| 142 | and revision it expects, checking RowsAffected.** Zero rows is |
| 143 | "someone moved it first", never a retry. |
| 144 | - **A booked event's start, end, tz, all_day, kind and rrule belong to |
| 145 | the booking flow.** A host edit that changes any of them is refused; |
| 146 | the form shows them disabled with the stored values hidden beneath. |
| 147 | - **Never bind a request body onto a model.** Updates take an explicit |
| 148 | column allowlist; the columns absent from it (`calendar_id`, |
| 149 | `created_by`, `series_id`) are exactly the ones a hidden field must |
| 150 | not move. |
| 151 | - **Never `sessions.UserID`** β under the magic-link plugin the Subject |
| 152 | is an address, so it returns `(0, false)` and every query silently |
| 153 | scopes to person 0: a calendar that looks empty rather than broken. |
| 154 | Use `auth.From(r)`, and `requirePerson` for the id. |
| 155 | - **A Home session subject is opaque.** Resolve its explicitly linked person ID |
| 156 | before any grant lookup. Never pass it to `EnsurePersonSetup` or use it as an |
| 157 | address. Recheck current admission and retain AUTOINCREMENT person IDs without |
| 158 | reuse or reassignment. Home proof storage uses a separate stable connection key. |
| 159 | - Migrations are additive-only. Never edit one that has shipped. |
| 160 | |
| 161 | **Ticks** |
| 162 | - **A `time.Ticker` is not a scheduler.** The instance hibernates. |
| 163 | - **`NextDue` is the earliest CLAIMABLE instant** across reminders, hold |
| 164 | expiries, owed booking mail and IP-forgets. New scheduled work goes |
| 165 | here, never in a goroutine. Cheap work runs before delivery; |
| 166 | reminders get the first half of the delivery budget. |
| 167 | - **Every queue row has a random `claim_id`;** every completion |
| 168 | mutation is conditional on it and checks RowsAffected, and a payload |
| 169 | is built from the row the claim returned, never from the candidate |
| 170 | read. A reminder's `due_at` is the occurrence minus the offset and is |
| 171 | never written by a retry β `retry_at` is. Booking mail's `due_at` is |
| 172 | retry eligibility only. |
| 173 | - **No sender error is definite.** Never free a resource because a send |
| 174 | "failed". Every send goes through `boundedSender`; `ErrMailBusy` is a |
| 175 | deferral, not an attempt. |
| 176 | - **A reschedule repairs the event's queue rows in the same transaction |
| 177 | as the move.** |
| 178 | - **The tick route is mounted OUTSIDE `csrf.Protect`.** `csrf.Protect` |
| 179 | refuses any POST with no `Sec-Fetch-Site`, `Origin` or `Referer`, and |
| 180 | the scheduler sends none; inside the protected router a tick 403s |
| 181 | forever and the platform reads 4xx as "do not retry". It fails |
| 182 | silently and permanently. `TestTheTickRouteIsNotBehindCSRF` stands in |
| 183 | front of it and asserts WHICH refusal answered. |
| 184 | - **Work inside the request.** 202-plus-a-goroutine is a reliable way to |
| 185 | be hibernated mid-send. |
| 186 | - **Ticks are expensive.** `CARLOS_HIBERNATE_IDLE` defaults to 15 |
| 187 | minutes, so each tick buys a quarter hour of billed residency. The |
| 188 | declared schedule is 6h and the precision comes from `NextDue`. |
| 189 | |
| 190 | **Frontend** |
| 191 | - Write the **`rst-*` attribute grammar** (`rst-btn="primary"`), never |
| 192 | the class spelling. Adopt per screen, never per element. |
| 193 | - This app's own components take **`cal-`**. |
| 194 | - **No affordance may require script.** The scriptless page is the |
| 195 | product; `datetime.js` and any later enhancement sit on top of a |
| 196 | working floor. |
| 197 | - **Colour never carries meaning alone.** Every chip prints its own |
| 198 | time and title; cancelled says "Cancelled"; past is written in the |
| 199 | past tense; today has three signals. |
| 200 | - Light/dark is three states and **auto is the ABSENCE of |
| 201 | `data-theme`**. The attribute takes the light/dark SCHEME, never |
| 202 | rastrillo's theme NAME. |
| 203 | - **Quick add has ONE parser and it is the Go one.** The live pane is a |
| 204 | fetch to `/events/quick/parse`. Never add a second parser in |
| 205 | JavaScript: two readings of "next Tuesday at half three" means the |
| 206 | time shown is not the time saved. |
| 207 | - **Attendees are recorded, never invited.** Nothing emails them, every |
| 208 | surface that names one says so, and their addresses are sealed with a |
| 209 | KEYED fingerprint for lookup β an unkeyed hash of an address is that |
| 210 | address in a different alphabet. |
| 211 | - **Except a BOOKER, who typed their address to be mailed.** Booking |
| 212 | mail is the one outbound to a non-team address. |
| 213 | |
| 214 | **Browser** |
| 215 | - `make browser` is the painted-output gate: Playwright, run from |
| 216 | `TestQuickAddInARealBrowser`. It is NOT in `make ci` β the one gate |
| 217 | everybody runs must not need a toolchain the app does not use. |
| 218 | - It runs the feature twice, the second time with **JavaScript |
| 219 | disabled**. That pass is the only proof the scriptless floor is real. |
| 220 | - `make browser` also runs `TestNoZoomOnTap`, through chromedp: every |
| 221 | text control on a touch screen is 16px or more, or iOS zooms the page |
| 222 | on tap. chromedp drives when `RASTRILLO_CHROME` names a Chromium (the |
| 223 | Makefile finds the Playwright cache's on a Mac); it parked at |
| 224 | `about:blank` without one. `TestBrowserWalk` drives now, but fails |
| 225 | at its first sign-in (no link is mailed after its form submit), and |
| 226 | is not in `make browser`; do not assume it ran. |
| 227 | - **A control styled with `font: inherit` under a selector heavier than |
| 228 | a bare `input`** loses tokens.css's 16px-on-touch rule. Restate |
| 229 | `font-size: max(1rem, 1em)` for it under `(pointer: coarse)`, as |
| 230 | calendar.css does for the quick-add box. |
| 231 | |
| 232 | **rastrillo** |
| 233 | - **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path, |
| 234 | `github.com/carlosframework/rastrillo`, stops at v0.23.0: code |
| 235 | importing it gets no fix, and its own checks cannot tell (Calendar |
| 236 | zoomed iOS on every tap for a month that way). `TestNoPreMoveRastrillo` |
| 237 | allows one import of it, native.go's bridge to rastrillo/native's |
| 238 | server, until that server moves; `native_exchange_test.go` proves the |
| 239 | bridge. The CLI is pinned as a `tool` in go.mod: `go tool rastrillo β¦`, |
| 240 | never a `rastrillo` from PATH. |
| 241 | - **Run `go tool rastrillo doctor` on every rastrillo upgrade**, and |
| 242 | `--fix` to re-copy what drifted (tokens.css, select.js, datetime.js). |
| 243 | It leaves `vendoredIsMine` alone: rastrillo.js (estilo's shim, without |
| 244 | the old busy rule) and theme.css come from estilo through |
| 245 | `go run ./scripts/writeestilo`, and calendar.js is Calendar's OWN |
| 246 | script, not rastrillo's month-grid overlay of the same name. If |
| 247 | `--fix` ever writes rastrillo.js, re-run writeestilo: the old shim |
| 248 | must never load beside busy.js (TestBusyFilesAreEstilos). busy.js is |
| 249 | estilo's too, and shell.css/shell.js are rastrillo's console shell, |
| 250 | which Calendar does not draw. **While go.mod still requires the old |
| 251 | path, doctor compares nothing and `--fix` refuses**: copy the drifted |
| 252 | files out of the new module's `ui/` by hand, and |
| 253 | TestVendoredAssetsMatchTheLibrary says which. |
| 254 | - **An emailed link confirms before it is spent.** GET /auth/verify draws |
| 255 | the door with one Sign in button (`handlers.renderConfirm`) and spends |
| 256 | nothing, so a mail scanner cannot burn it; the POST redeems. Both are |
| 257 | mounted in both modes. A test that signs in opens the link, reads the |
| 258 | token off the door and POSTs it (`SignInForTest`, `openLink`/`postLink`). |
| 259 | - **Inline style attributes need the CSP's `'unsafe-inline'`.** |
| 260 | rastrillo's default style-src refuses them; the day and week grids |
| 261 | place chips and slots with them and every chip carries `--cal-hue`, so |
| 262 | `ContentSecurityPolicy` (serving.go) widens style-src alone. |
| 263 | `TestProductionCSPIsTheDefaultBarStyles` keeps every other directive |
| 264 | at rastrillo's default, and `TestPlacedChipsSurviveTheProductionCSP` |
| 265 | (`make browser`) is the one browser check that sees the real policy. |
| 266 | A CONSTANT style belongs in calendar.css, never in an attribute. |
| 267 | |
| 268 | **Logs** |
| 269 | - **No title, place, note or address in any log line, error string or |
| 270 | metric label.** `carlos logs` is platform-held storage we cannot grep |
| 271 | afterwards. Errors carry a stage name and a code. |
| 272 | |
| 273 | **Deploys** |
| 274 | - One binary, `CGO_ENABLED=0`, cgo-free SQLite. |
| 275 | - **`carlos deploy`, never `carlos ship` alone.** `ship` leaves the |
| 276 | release unpromoted and nothing serving it. |
| 277 | - **`make release`, never a bare `go build`** β it stamps |
| 278 | `rastrillo.BuildVersion` and refuses a dirty tree. |
| 279 | - **A 200 is not verification, and neither is `X-Carlos-Version`.** That |
| 280 | header is the router's adopted release. `GET /api/version` is served |
| 281 | by the process. Measured here at ~110 seconds behind the promote. |
| 282 | - **`stable` needs a passkey step-up in the console**, and so does |
| 283 | moving an instance between `stable` and `edge`. This app runs on |
| 284 | `edge`, like docs. Create an instance on the channel you want. |
| 285 | |
| 286 | --- |
| 287 | |
| 288 | ## Shipping cadence |
| 289 | |
| 290 | Worktree per piece of work, branch per piece of work, never on `main`. |
| 291 | Squash-merge only when the work is done, it is approved, and CI is |
| 292 | actually green β checked, not assumed. Review on a deployed canary, |
| 293 | never localhost. |
| 294 | |
| 295 | `make ci` is the one gate: vet, gofmt, tests, migration check. The same |
| 296 | definition CI runs. Run it before every push. |
| 297 | |
| 298 | ## Review |
| 299 | |
| 300 | Every spec and every branch is reviewed by the highest-capability model |
| 301 | available, discovered rather than hardcoded. Record which model |
| 302 | reviewed. A review by a weaker model than the author is not a review. |
| 303 | |
| 304 | ## Authorship |
| 305 | |
| 306 | π€ / π¨ markers, `Co-Authored-By:` trailers. AI is a guest with a name |
| 307 | tag: opt-in, disclosed, and every path works with it switched off. |
| 308 | |
| 309 | Startup verifies the configured data key against `meta.sealed_key_check_v1`, or existing encrypted calendar names on first adoption. A refusal requires restoring the correct key/database pair. Never delete or replace the sentinel to make an unreadable database start. |
| 310 | |
| 311 | ## Work in a worktree, and catch main up first |
| 312 | |
| 313 | Take a worktree for the work. Do not work in the shared checkout. |
| 314 | |
| 315 | git -C <repo> fetch origin |
| 316 | git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main |
| 317 | |
| 318 | Both halves matter, for different reasons. |
| 319 | |
| 320 | **The worktree**, because several sessions share this machine and some |
| 321 | share this repository, and a checkout has one HEAD, one index and one |
| 322 | working tree between all of them. A branch switch under somebody else's |
| 323 | uncommitted work destroys it with nothing on screen to say so, and two |
| 324 | sessions editing the same paths disagree quietly instead of conflicting |
| 325 | loudly. A worktree gives the work its own HEAD and its own files, and |
| 326 | costs a directory. |
| 327 | |
| 328 | **From `origin/main`, after a fetch** β not from whatever the checkout |
| 329 | happens to be sitting on. A branch cut from a stale main is a merge |
| 330 | conflict scheduled for later, against files somebody else has renamed in |
| 331 | the meantime. A branch cut from another task's branch is worse: it |
| 332 | carries that task's commits into yours, where they are reviewed as |
| 333 | though you wrote them, and the two can only be separated by hand. |
| 334 | |
| 335 | Remove it when the work has landed β `git worktree remove` β so that |
| 336 | `git worktree list` keeps saying something true. |
| 337 | |
| 338 | Written down because of 2026-09-17, in `meet`. The shared checkout was |
| 339 | sitting on one task's branch while holding a *different* task's feature |
| 340 | uncommitted on disk β some four hundred lines, a new migration among |
| 341 | them β put there by a session nobody else knew was working in that tree. |
| 342 | Two other sessions were meanwhile coordinating over the same checkout by |
| 343 | asking each other not to touch it, which is the arrangement a worktree |
| 344 | makes unnecessary. Nothing was lost. Nothing about the arrangement would |
| 345 | have said so if it had been, and by then the same feature existed twice, |
| 346 | once on each side of a rename, because the two sessions had branched |
| 347 | from different starting points. |
| 348 | |