Paste's anchor rules never accounted for a tombstoned selection, and a
literal reading filed a pasted lane beside a trashed card's on-disk lane.
Settled: ⌘V stays enabled and behaves as with nothing selected (card
payload to the last-active lane, lane payload to the board's right end).
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The fixed grammar keys were formally live over the board while the search
field had focus, letting a reflexive Return fire rename/create and destroy
the query. Settled: every key acts on the field (no pass-throughs), Return
no-ops, Tab moves focus to the filtered board with the query intact, and
Escape stages — clear query, then exit field, with board-focus Escape
clearing an active search before its clear-selection meaning.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Accept liberally, emit conservatively (764a4d4): the gate is 8-4-4-4-12
hex in any case and any UUID version — uuidgen and UUID().uuidString
print uppercase, and a strict lowercase gate would silently stray an
agent's standard-tool card. Identity comparison is UUID-value equality
everywhere: ItemID keeps its byte-faithful rawValue but equates and
hashes on the lowercased canonical form, and the writer's
import-boundary collision check canonicalizes, so a same-UUID arrival
spelled in another case remints instead of slipping past. The app still
mints only lowercase v4 and never renames to canonicalize.
Full suite 342 tests in 63 suites green. Two findings filed.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Every lane command keyed off "a selected lane" with no drag-free way to
create one. Settled: ⌥↑ with the lane's first card selected selects the
lane (↓/⌥↓ descend back; ⌥↑ then inert), an empty selection seeds at the
first lane's first card, and a plain click on the lane title bar selects
with the drag surface engaging only on movement.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The row's name and icon implicitly required reading each recent board's
root index.md at welcome time — the hang the counts rule was written to
avoid. Settled: title/icon/iconColor live in the registry record; they
refresh whenever an open board's reload changes them (in-app renames land
instantly, foreign renames of open boards ride along), with closed-board
renames stale until next open like the counts.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The banner surface as one vocabulary (Kanban/LiveStore/BannerCenter,
Kanban/UI/BannerStripView): a pure precedence rule — in-progress pinned
above the collapse (ratified mid-build), lock > breakage > one-shot
write failures > commit+attachment, signposts last — with all
user-facing phrasing owned here via exhaustive switches over the closed
WriteOperation enum; free-form English survives only in diagnostics.
performWrite posts its failures before rethrowing, so no one-shot can
bypass the strip; refusals under lock post nothing.
The lock vocabulary completes: vanishedRoot and unwritableLocation join
bracketedReloadFailed, each with its own clearing rule (unwritable
clears only on a reconciling reload's writability re-probe). The
registry now owns root recovery: bookmark re-resolution absorbs renames
transparently, a dead root locks read-only and re-arms FSEvents on the
gone path so the root's return round-trips back through rootChanged,
re-minting and re-keying on the way. DirtyBufferGuard is the one modal
moment, retry / save a copy / discard, no fourth button.
36 new tests; full suite 333 tests in 62 suites green. Five findings
filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Both whole-board copies were absent from the lock's command enumeration,
staying enabled while the source tree was gone, unknown, or unwritable and
while the lock suppressed their required close flush. Settled: Duplicate
disables in every lock state; Save as Template disables except under the
unwritable-location lock, where reading the board into Application Support
is a legitimate copy-out with a vacuous flush precondition.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The stacking order covered only error classes, letting an in-progress row
collapse behind "+N more" — the sole explanation of a bracket's write lock
and a deferred close. Settled: in-progress rows pin above everything,
exempt from and uncounted by the collapse; the passive remote-change
signpost ranks below all error classes and may collapse.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The trash's newest-first order is load-bearing for keyboard input, but
same-second multi-deletes and 01's blessed-unparseable timestamps had no
ordering. Settled: ties break by folder name ascending; date-unknown
entries sort as oldest, after all dated entries; lane entries interleave
by their own stamp.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
01 said "lowercase UUIDv4" without defining the test, and the strict
reading made every uppercase uuidgen/UUID().uuidString folder a silently
skipped stray. Settled: 8-4-4-4-12 hex in any case and any version is an
identity; comparison is UUID-value equality (case-spellings are one
identity, matching default-APFS behavior); the app emits lowercase v4
only and never renames to canonicalize.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The registry recorded recency but not which boards were open at quit.
Settled: each record carries an open-now flag set on open and cleared on
user-initiated close only — quit teardown leaves it standing, so there is
no at-quit write and crash recovery falls out for free. Restoration
reopens flagged records by lastOpened order; the preference only gates
consulting the set.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
10 recovered focus "to the card's lane" even in its own lane-tombstone
case, where that lane renders nowhere. Settled: recovery walks up then
sideways — the lane at the vanished lane's position (next by order, else
previous), board container only when no lanes remain, never into the
hidden trash; the announcement names the lane, not the card.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
10 promised toggleable-without-pointer, 11's normative inventory had the
toggle as pointer-only, and 02's lock disabled it "via menu validation"
with no menu item to validate. Settled: checkboxes and links are real
controls in the focus/accessibility tree — FKA + Space and VO-Space toggle
them; in-content controls are content, not commands, so 04's menu-item
contract and 11's existence rule scope to commands; under the read-only
lock the controls disable in place.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
03's Put Back cascade ("restores the lane too") was unreachable dead text
under 01's hide-the-subtree consumer rule and 02's effective liveness.
Settled: the lane's single trash entry subsumes everything beneath it; its
count is what Put Back returns to the board; lane Put Back splits by flag
(hidden-with-lane cards return, individually tombstoned ones reappear as
trash rows), making that recovery deliberately two steps.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
04's drag rule called a within-board lane duplicate unsupported while the
clipboard rules quietly permitted it via same-board lane paste. Settled in
favor of the clipboard: the duplicate is supported there (fresh GUIDs,
tombstones stripped, no menu-validation special case) and merely not
available by drag — ⌥ stays ignored on lane drags so the gesture remains a
clean reorder.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
01's table promised an update on every app write, which raw-source Apply
(byte-for-byte contract) and attachment operations (never touch index.md;
load-bearing for 03's Add-Attachment-during-raw-source rule) cannot honor.
Settled: modified updates only on app writes that rewrite the item's
index.md; both exceptions stated as deliberate, and the stale-readout
consequence on 05's Modified line accepted with attachment history living
where the files do.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
06 and 10 consumed a per-file app-vs-foreign classifier that 02's component
inventory never named and whose layering prose seemed to deny. Settled: a
new EchoLedger component records each BoardWriter operation's expected
outcome (content hash / absence / move pair); final-content matching
classifies each observed file, deciding both races (byte-identical foreign
write → app-mediated, accepted; same-window foreign overwrite → foreign,
last writer wins). Ledger is in-memory and per-store; launch catch-up is
all-foreign; bracketed operations bypass it. "Trusts its own writes no more
than anyone else's" is scoped to rendering; BoardWriter's no-hidden-state
bullet stands.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
WriteOperation replaces free-form operation strings throughout
BoardWriter, per the settled rule in 02 § Write-failure surfacing: the
banner layer will switch exhaustively over it, so a new operation
without a banner rendering is a compile-time hole. Titles enrich at the
two points a document read makes them known (updateIndex, and the
move/copy pre-flight), so failures after the read name the item;
purge never reads and stays title-less. Free-form English survives
only in the diagnostic reason.
Full suite 300 tests in 56 suites green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
The parallel design session settled the tombstoned-ancestor question:
a card counts as trashed if its own flag or its lane's says so, so
tombstoning a lane ejects its cards from a live selection — they render
nowhere once the lane collapses to a single trash entry, and nothing
invisible may stay selected, drag-included, or pending-cut. One-line
predicate change in Selection.resolved(against:), plus the test.
Full BoardStore suite green (15 tests).
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Two registries (Kanban/LiveStore/): BoardStoreRegistry shares one live
store and one started, fully wired watcher per open board across its
windows — keyed by file identity (fileResourceIdentifier), never path,
refcounted to order teardown; release matches by store identity so a
board renamed while open can't leak its watcher. BoardRegistry persists
app-private per-board state in Application Support as diff-stable JSON:
records anchored by security-scoped bookmarks, recents = the registry
sorted by lastOpened (counts registry-cached, never scanned), graceful
orphaning with Forget, corrupt files quarantined aside, and files-first
verified — the board tree is untouched byte-for-byte. Timestamps use
ISO8601DateFormatter with fractional seconds: the FormatStyle variant
truncates-then-rounds and drifts a millisecond per round trip.
16 registry tests; full suite 297 tests in 56 suites green. Four
findings filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Tombstoning a card's lane ejects the card from selection, drag
membership, and the pending cut - liveness is ancestor-walked, since
the card renders nowhere once 03 collapses the lane to a trash entry.
BoardStore's bullet now says it coalesces reloads (one walk in flight)
while the debounce lives in FolderWatcher, matching m3 as built.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The next-reload-or-lock expectation arms on every bracket exit, thrown
operations included - a mid-flight failure is when the tree's state is
least known, so the mandatory final reload runs regardless.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
The parallel design session resolved the m3 watcher findings; two
changed behavior: .git internals are now filtered at any depth (a
nested clone or submodule is a stray whose internals never render),
and a root change landing mid-bracket is owned by the root-change
path — endBracket() skips its mandatory reload once a .rootChanged
tore the stream down, cleared when a fresh stream attaches. The skip
is scoped by an explicit flag, not by stream absence: a watcher whose
stream never came up still delivers post-bracket reloads, since
distrust of FSEvents delivery is the mandatory reload's whole reason.
Two new watcher tests; full suite 281 tests in 54 suites green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Per-board @Observable MainActor hub (Kanban/LiveStore/): watcher signals
drive off-main tree walks with a generation guard, single-flight
coalescing (strongest pending origin, watcher's merge rule), and the
resilience contract — a failed reload never replaces a good snapshot,
per-file breakage never locks editing, and a wholesale operation
(performWholesale) arms a reload-must-succeed-or-lock floor so a failed
post-bracket reload flips the board read-only until a good reload heals
it. Selection is a pure UUID-set value re-resolved on every swap;
liveness flips eject. performWrite brackets the watcher so Writer
round-trips come back app-mediated.
14 store tests; full suite 279 tests in 54 suites green. Three findings
filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Contradictions: v1 gains a minimal Settings pane (⌘,) housing the
restore-at-launch toggle; Window ▸ Welcome to Lanework inventoried in 11.
Gaps: close/quit waits for in-flight bracketed and copy-shaped operations;
pull extends the Edit-session treatment to open raw-source buffers
(signpost + last-writer-wins Apply); File ▸ Duplicate keeps tombstones
(01's "as-is" softened to per-flow exclusions). Minor: Undo routing
promoted to a real heading in 06; 11's dead "10 ▸ Actions" citation
repointed. m3 watcher findings: root-gone cancels armed debounced
delivery; mid-bracket root changes owned by the root-change path;
.git filtered at any depth; banked mid-bracket reconciles and the
200 ms debounce ratified.
Debounced FSEvents watcher over the board tree (Kanban/LiveStore/):
trailing 200ms debounce coalesces bursts; nestable brackets suppress
delivery and close with exactly one app-mediated reload; origins merge
reconciling > appMediated > foreign; missed-events flags degrade to a
reconciling reload; WatchRoot vanish tears the stream down (streams die
and are recreated — reattach() covers rename re-resolution); board-root
.git churn is filtered against the symlink-resolved root. The FSEvents
release callback must be top-level and non-isolated — a MainActor
closure traps in dispatch_assert_queue during stream deallocation.
15 watcher tests (4 consecutive green runs); full suite 265 tests green.
Five design findings filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Four minor design-review findings resolved: appended frontmatter lines
adopt the file's prevailing line ending; empty-but-not-blank frontmatter
joins the readable-but-uneditable shapes; an interrupted renumber's
per-file partial is accepted; the write-failure operation becomes a
closed enum carrying the item title.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
- Appended frontmatter lines adopt the file's prevailing line ending
(read off the opening delimiter) — one stamp never produces a
mixed-ending file.
- Empty-but-not-blank frontmatter ({}, null, ~) joins the
readable-but-uneditable shapes: zero keys load fine, but appending
after that text would be unparseable YAML.
- Renumber is atomic per file, not per lane — an interrupted rescale
is accepted the same way as the shared-board burst: valid floats,
deterministic order, next renumber completes the job.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
The executable spec for BoardWriter's cross-cutting guarantees, in a
dedicated WriteFidelityTests.swift: minimal-touch verified down to
sibling mtimes across a seven-operation session; renumber pinned as
the sole exception (whole lane rewritten, board root / other lanes /
tombstones untouched); delete→restore byte-precise (only the modified
line differs, zero tombstone residue); unknown-key order preserved
with keys interleaved among schema-owned ones; and a composite
building a board purely through the writer, hand-editing a file like
an agent would, then moving/copying/deleting/renumbering to a
zero-warning reload. Guarantees already pinned by the per-operation
suites (atomicity, move/copy identity, body round-trip) are
referenced, not duplicated. Shared fixtures promoted to
WriterTestSupport.swift. README gains the write-side feature bullet.
5 new tests; 250 total green. Closes milestone m2-storage-write.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
importAttachments lands files into a card's attachments/ (created on
first import — the one folder the app ever makes there), never
overwriting and never refusing a name: collisions auto-rename
Finder-style, counting up from 2, with a same-named subfolder
blocking a name exactly like a file. Sources are validated before any
copy; a failed copy removes the partial destination and throws naming
the source file; a multi-file batch stops at the first failure with
earlier files staying landed. listAttachments is the flat surface:
top-level regular files only, Finder-sorted, subfolders and hidden
entries excluded and untouched; a missing attachments/ lists empty.
11 new unit tests; 245 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
deleteItem writes deleted: <now> in place through updateIndex — the
folder never moves, nothing beneath it is touched (hiding the subtree
is the renderer's ancestor walk, not a stored flag). restoreItem
removes the key — position-perfect by construction, every duplicate
occurrence taken so a hand-written twin cannot resurrect the
tombstone. purgeItem physically removes the tree; an already-missing
folder is success (a Finder deletion converges on the same end
state), and no prior tombstone is required (Delete Immediately skips
the stage by design). Board-root deletion is structurally unreachable
via the UUID-shape guard shared with move/copy. Neither delete nor
restore polices liveness — re-deleting refreshes the timestamp,
restoring a live item is a harmless stamped rewrite.
14 new unit tests; 234 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
moveItem: physical folder move, UUID and created travel unchanged;
exactly one file rewritten (the moved root's order, stamped). Import
boundary detected by resolved board-root comparison; on a cross-board
move-in, arriving UUIDs colliding with any identity in the destination
board (tombstones included) are reminted per folder at the finest
grain — folder rename only, file bytes untouched, every repair
reported in MoveResult. A colliding root moves straight to its minted
name. A same-parent move degrades to a plain reorder, self excluded
from the appended-rank scan.
copyItem: whole-tree copy minting fresh UUIDs at every depth; .fork
keeps created while stamping modified and clearing modified-by, .born
(template instantiation) stamps created fresh too. Nested uneditable
or unreadable files copy byte-verbatim rather than blocking the
gesture; the root must be rewritable. All-or-nothing at the
destination — any failure removes the partial tree.
24 new unit tests; 220 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
createBoard fills or mints a .kanban (or extension-less) folder and
refuses to clobber an existing board; createLane/createCard mint fresh
lowercase-UUIDv4 folders and append at max+1024 among visible siblings
via the same strict scan renumber uses (extracted, shared). New files
carry schema/title?/order?/created/modified — created==modified, no
modified-by — written LF through the same atomic temp+rename path.
The editability pre-flight is now scoped to operations that rewrite
siblings: renumber refuses on an uneditable sibling, a create beside
one proceeds. Schema constant unified on BoardLoader.supportedSchema.
Info.plist declares the .kanban document type (UTI conforming to
com.apple.package) so Finder treats a board as one document.
20 new unit tests; 196 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
The single point through which every mutation becomes a filesystem
operation: read fresh from disk (strict byte-faithful UTF-8), refuse
readable-but-uneditable frontmatter shapes, apply the edit through the
span engine, stamp modified / clear modified-by, then temp-file+rename
atomically. Renumber-visible-children is the sole minimal-touch
exception, tombstones untouched. Structured BoardWriteError carries
operation + path + reason for the future banner surface.
Alongside, three edges the card surfaced:
- The loader now decodes byte-faithfully too, so a BOM'd or non-UTF-8
index.md is rejected at load per the encoding contract, instead of
loading via NSString's silent BOM strip and then refusing every write.
- An appended frontmatter line adopts the file's prevailing line ending
(a CRLF file stays uniformly CRLF when a stamp first lands in it).
- Empty-but-not-blank frontmatter ({}, null, ~) is detected as
uneditable — appending after it would be unparseable YAML.
30 new unit tests; 176 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
- 10 cites 11 for Add Attachment and the lane-width menu items
- 04 uses ⌘/⇧ symbols for click modifiers; card moves scoped to the
⌥⌘ chord; ../WISHLIST.md path fixed
- 03 says stepper (plus the menu pair) in the editing-surfaces table
- 06 says one commit per debounce window
- 03/10 Reduce Motion lists aligned at five items; rubber-band
disambiguated as the lane-resize feedback
- 11 requires the trash shown for Empty Trash
- 02 rewords and-peers
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: the rename editor follows its card by UUID through
foreign moves, discards silently when the target vanishes or flips
liveness, and a mid-search rename re-runs the predicate like any edit
— creation stays the search filter's only carve-out.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: wake/activation reconciling reloads, MustScanSubDirs and
overflow degrade to a full reload, dead streams are recreated at the
re-resolved root. Stale boards structurally excluded.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: eager staging at copy time, at most the current copy
kept (sweep at launch and each copy against pasteboard references,
surviving relaunch), and attachment-less fallback pastes surface a
one-shot banner naming the loss.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: resting zones recompute per snapshot (frozen inputs are
the dragged items' sizes and the pointer); proposals re-validate by
liveness with release-cancels-when-invalid; an emptied drag cancels
itself, partial vanishing drops survivors.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: on root-gone signals the bookmark re-resolves first —
success absorbs the rename transparently (watcher re-attach, URL and
window-key re-derive, full reload); only an unresolvable bookmark is
the vanished-root lock.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: Save All applies each raw buffer (a validation failure
cancels the switch, focus on the offender), Discard exits without
writing, Cancel keeps everything. External checkouts remain accepted
last-writer-wins per the dirty-buffer rule.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
18 golden fixture boards as real on-disk trees under Fixtures/ — ten
valid/tolerated cases (rich board, interrupted create, non-UUID strays,
stray files, tombstones, tie-breaks, unknown-key order, coercion,
last-wins duplicates, board-level deleted) and eight fail-fast cases,
each pinned to its exact error reason and path; the rich board's whole
tree round-trips byte-identically. Pins two contract subtleties now
recorded in the design: tombstone flags are not inherited (renderers
walk ancestors) and last-wins resolves before order strictness. README
gains the storage-foundation feature entry. 146 tests total.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Fills the gap: pull/push/switch/restore share the auto-committer's lock
posture with a visible waiting state; clean failures are one-shot
banner errors; the never-mutate rule exempts the app's own stamped
leftovers, which it aborts and reports.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: foreign commits — auto-committed agent work and agent
self-commits — push onto the in-session stack as steps, with a
pre-flight HEAD re-sync before every undo/redo. One rule in-session
and across relaunch; arriving commits clear redo.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: the lock spans every window sharing the store — Edit
entry, raw-source Apply, attachments, sidebar actions, checkbox toggles
disable; drags out offer copy only. Open Edit buffers stay typable with
saves suspended; held text resolves by lock cause (switch/restore
settle first, post-pull buffer-wins, returned root saves normally).
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
Fills the gap: restores materialize only the current-vs-target diff, so
untouched Edit sessions are unaffected; a touching restore reuses the
branch-switch Save All / Discard / Cancel step. Redo symmetric;
raw-source buffers follow the branch-switch settle treatment.
Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb