Commit Graph
393 Commits
Author SHA1 Message Date
rzen 0a01405ced The comments column gets a handle — the beside-mount divider is now user-draggable
Kanban.xcodeproj must be regenerated (xcodegen generate) before building.

CardWindowMetrics.commentsColumnWidth was a fixed figure between the body
column and the comments pane in the side-by-side layout; it is now only the
resting default. A new CommentsColumnDivider replaces the plain Divider()
between the two panes with a real HStack element carrying a resize-left-right
cursor and a drag gesture, clamped through a new pure seam,
CardWindowMetrics.clampedCommentsColumnWidth — never narrower than the
existing commentsMinimumWidth floor, never wide enough to push the body under
its own bodyMinimumWidth. The drag tracks live in memory
(commentsColumnWidthOverride) and writes AppPreferences.commentsColumnWidthKey
exactly once, on release, mirroring LaneResizeSession's live-track/write-once
split rather than hammering UserDefaults per tick.

Persistence is app-wide via @AppStorage, matching showComments and
commentsBesideBody — the pane's other two layout bits — rather than the
per-card BoardRegistry.cardWindowFrames: this is "how the pane is arranged,"
the same kind of fact those two already are, not a per-card window geometry.
Flagged on the card thread as a call worth owner review.

Six new unit tests cover the clamp's two floors, the degenerate case where a
container is too narrow for both, and its agreement with
CardWindowMetrics.minimumSize at the window's own floor.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 08:38:07 -04:00
rzen b0c134a896 A board leaves as one file and comes back as one — headings are lanes, rows are cards, position is the order
File ▸ Export ▸ writes the frontmost board as Obsidian Kanban Markdown, a
plain Markdown outline, or RFC 4180 CSV; File ▸ Import Board… reads any of
the three back into a fresh board, format detected rather than asked. Every
format encodes order as document position, so an export writes no ranks and
an import mints them in parse order on the ordinary create path. Lossy
exports post a warning-tone loss row naming the comments and attachments the
destination cannot carry. Convert-once: nothing watches, nothing merges.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 08:38:06 -04:00
rzen 242d013d8c The body editor yields a paste it cannot read — the screenshot's ⌘V reaches the card window's attachment branch
The editor holds the keyboard from the moment the card window opens, and
NSTextView answers an image-only pasteboard with a disabled Paste row —
so the window's image branch sat one responder below the keyboard and
could never be reached by it. The yield is by capability
(readablePasteboardTypes), the same shape as the editor's existing
refusal of file drops: any text flavor keeps the paste in the editor,
and a pasteboard the editor has no reading of passes paste: to the
responder behind it. Live-probed with real window-server ⌘V chords in
both windows.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 06:28:02 -04:00
rzen 297f12fa61 The pasteboard re-reads when ⌘ goes down — a screenshot no longer leaves ⌘V dead until the next app switch
Commands validate by conditional responder attachment, so the cached
imagePayload decided whether ⌘V had a responder at all — and the one
pasteboard writer that never deactivates the app, the screenshot hotkey,
changed the pasteboard without any checkpoint firing. Menu validation is
now two concrete moments: a menu beginning to track, and ⌘ going down,
whose beat before the letter is when the observation re-arms the
responder. One changeCount read per check, no timers.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 06:03:53 -04:00
rzen 9d30e14a4b The QuickLook extension stamps its build number like its host — an embedded appex must match CFBundleVersion for CFBundleVersion
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 05:52:29 -04:00
rzen 2f45201412 Paper trail for the second wave — share, Quick Look, pasted images, printed glyphs
Four changelog entries and a README that learns the share sheet, the Quick
Look extension, and the pasteboard's three image destinations.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 02:48:16 -04:00
rzen ca5d45156b A picture off the pasteboard becomes a card's file, its hero, or the board's backdrop
⌘V grows an image-data branch, below the app's own clipboard format and refused
outright while a file URL is on the pasteboard: a screenshot or a browser's Copy
Image lands as "Pasted Image.png" in the anchor card's attachments/, through the
very import path Finder file drops and ⇧⌘A take — one bracket, one Finder-style
collision ladder, one set of banners, and the same silence a drop's arrival has.
A card window's ⌘V pastes onto its own card; a focused text field still wins the
selector natively. A file-shaped flavor travels byte for byte, PNG preferred when
several are offered; TIFF and BMP are re-encoded to PNG, being interchange
encodings rather than files anyone wants in a folder.

The hero key gets the setter it was born owing: "Set as Hero" on any image row of
the attachment list, "Remove Hero" on the row that holds it, with menu-bar twins
so the context entry is nobody's only home. It writes as a restyle — one key, one
bracket, one invertible step on the window's own stack — and replaces rather than
refusing, because a card has one hero and the row that has it says Remove instead.

Edit ▸ Paste as Board Background is the same payload's other destination, taking
the existing background.image convention at its word: the picture into the board
folder as "Pasted Background.png", the colour subkey untouched, the generator's
overwrite-our-own-name rule inherited and its echo memo taught to tell the two
producers apart.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 02:43:53 -04:00
rzen b18f7ca609 Space in Finder opens the board — a Quick Look preview extension that outlines a .kanban package
A board is a folder, and a folder previews as a folder. `KanbanQuickLook.appex` gives it a
document's preview instead: the board's name, its tint and its symbol, then its lanes in display
order with each one's card count and its first few card titles.

The reading is `BoardOutline` (Kanban/Storage), deliberately not `BoardLoader.load`. The loader
throws on a half-broken board — right for opening one, wrong for pressing Space, where the honest
answer is the part that reads; it visits every card and lists `attachments/` and counts `comments/`
inside each; and it carries trash, tombstone migration and the defect stream, none of which renders.
This walk never throws and is capped at every level (`BoardOutlineLimits`): 12 lanes shown of at
most 100 considered, 6 card titles per lane of at most 200 parsed, counts by readdir-plus-stat up to
2000 per lane and never a parse. It re-derives nothing that decides *what* the answer is —
`FrontmatterDocument` parses, `IntegrityRules.isIdentityShaped` says what a lane or a card is,
`BoardLoader.directoryCandidates` supplies the stray tolerance, `Ranks` supplies display order,
`Palette` resolves colours — only *how far to look*.

The reply is HTML, the one data-based reply that reflows: a Quick Look panel is resized by the
user and a board outline is a wrapping row of columns, so a drawing block baked at a fixed
`contentSize` would be the wrong size a moment later. It gets vector text, its own scrolling and
light/dark for free. The board tint is a wash under the title and a lane's edge accent — never
under text, because a preview has none of `ContrastMath`'s ink-picking machinery and should not
grow one.

The extension compiles `Kanban/Storage` whole, the `KanbanMobile` arrangement — the directory is
one unit in practice, so a narrower list is not on offer. `STORAGE_ONLY` is new: EchoLedger's
consumer sections speak the live store's vocabulary, and the phone's `#if os(macOS)` cannot exclude
them from a target that *is* macOS. Platform, and layer. Nothing else defines it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 02:37:10 -04:00
rzen da5d310673 File ▸ Share… stages a board as a zip and hands it to the system share sheet
Duplicate's own posture — a faithful copy, `.git` the sole exclusion, attachments/comments/trash
carried verbatim — staged to a temp directory (BoardShareStager, ditto-zipped via
DittoZipArchiver) and presented through NSSharingServicePicker (BoardSharePresentation), anchored
to the board window's toolbar or its center. Follows Duplicate/Save as Template's flush-then-
cancellable-copy sequence under the banner's in-progress row (ShareBoardCommand, AppCommands.swift),
menu-validated on focus alone rather than the read-only lock (a share is a read, Print's own
posture) with the one carve-out an open inline title editor still needs. WriteOperation gains
.shareBoard for the banner vocabulary; 11-command-nexus.md's File menu table gains the row.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 02:13:54 -04:00
rzen 05bbf78926 Printed symbols become real glyphs — template images resolved before they meet the PDF context
The owner's 2026-08-08 report ("SF symbols don't render well in the PDF output
of File ▸ Print…") photographed solid dark rectangles where the card icons
belong. The cause is not typography and not the renderer's layout: an
`NSImage(systemSymbolName:)` is a *template* image, a shape meant to be tinted
by the AppKit machinery that draws it. A print/PDF context has none of that
machinery, so the tint lands on the image's whole box instead of through its
coverage — a filled rectangle, measured at 1.000 ink coverage through a real
`NSPrintOperation`.

A second failure hid behind the first: a PDF context is a 1× device, so even a
non-template symbol rasterized at 72 ppi on the way onto the page (13 × 12
pixels for an 11 pt icon) and blurred at any zoom.

Both are the same mistake — leaving work for a context that cannot do it — so
`PrintSymbol` does the work first: the symbol is inked in the line's own colour
(resolved against the paper appearance, since a dynamic colour resolves at draw
time and this drawing happens long before the page exists), drawn into a bitmap
at eight times the point box, and handed over as ordinary non-template artwork.
The page now carries a 576 ppi glyph at 0.277 coverage. True vector was
measured and is not available: `NSSymbolImageRep` rasterizes into whatever
context draws it, the symbols are not reachable as font glyphs by name, and
re-wrapping the image in a PDF representation only embeds the same raster one
level down.

While in there, the attachment's baseline stops being a guess. It was
`font.descender * 0.5` — a constant that knew nothing about which symbol it was
placing, so every icon floated by a different amount. It is now the symbol's own
`alignmentRect`, which is Apple's metric for exactly this: the rect's height is
the font's cap height and its origin is the symbol's baseline within its box.

The forced light appearance moves to `PrintTypography.paper` because two places
now depend on it and must not drift: the page view pins it, and the symbol
raster draws under it.

Lane headings were checked and need nothing — `PrintLane` carries no icon, so
card meta lines are the only symbols a printed document has.

Tests drive the real pipeline: `PrintDocumentBuilder` → `PrintDocumentRenderer`
→ a real `NSPrintOperation` to PDF, then measure the ink on a sheet whose only
content is one icon. The coverage assertion fails at 1.000 on the shipped build.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 01:52:26 -04:00
rzen d16f10b058 The run's paper trail — nine changelog entries and the README catches up
README: the lane header's chrome loses its + and gains the collapse story,
card faces gain chips and the hero banner, keyboard navigation gains the
sticky row, the clipboard bullet gains Copy Link, the symbol grid becomes
three per-level shelves, and Printing gets the bullet it never had.
CHANGELOG: everything user-facing since the last documented commit, the
print feature and its entitlement fix folded into one honest entry.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 01:33:05 -04:00
rzen db863ba011 Three curated symbol vocabularies — boards, lanes, and cards each pick from their own shelf
`CuratedSymbols` was one flat ~65-glyph list serving every style-editor target alike. It is
now three level-specific sets — `boards` (project/container/identity), `lanes`
(workflow/stage/status), `cards` (work-item/content) — each ~30-40 entries, seeded from the
original list and `SymbolPickerCatalog.defaultSet`, reorganized by which level a glyph actually
reads as being about. Overlap is kept where a glyph genuinely fits everywhere (`flag`, `star`).

Wiring:
- `BoardInfoPopover`'s board-glyph `SymbolPicker` now passes `CuratedSymbols.availableBoards`
  instead of the picker's domain-agnostic default.
- The card sidebar's `SymbolPicker` (`CardSidebarSections`) now passes
  `CuratedSymbols.availableCards` instead of the old flat `available`.
- The style editor's own curated grid (`StyleEditorView`, the Style… popover's only remaining
  anchor) reads `CuratedSymbols.availableForStyleEditor(level:spansLevels:)`: a homogeneous
  target reads its own level's set, and a target that somehow spans more than one level (today
  unreachable — 04-interactions.md's cards-XOR-lanes rule keeps a live selection homogeneous)
  reads the three combined, via a new `BoardStore.styleTargetSpansLevels` seam that asks the
  question `styleLevel(of:)` deliberately collapses.
- `CuratedSymbols.combined` (the three sets' stable-order union) also replaces the old `.all`
  in `SymbolPickerCatalog`'s full-catalog fallback.
- `SymbolPickerCatalog.defaultSet` is kept as the fallback for a caller naming no level (a
  future saved-search picker, say) rather than retired.

DESIGN/03-board-ui.md and DESIGN/05-card-window.md's Styling/sidebar prose amended minimally
where they named "the curated set" as a single list.

Tests: three new/rewritten suites in KanbanTests/StyleModelTests.swift (set shape, availability,
overlap, `combined`, the style-editor level/span decision, the board-anchor width tripwire), one
new test in KanbanTests/StyleWriteTests.swift (`styleTargetSpansLevels`), and the old
single-list-pinning tests in KanbanTests/SymbolPickerTests.swift and KanbanTests/CardSidebarTests.swift
updated to the new set names. 2834 tests, 487 suites green (KanbanTests, arm64); one unrelated
flaky failure (RootRecoveryTests.vanishAndReturn under full-suite load) passed clean in isolation.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 01:26:36 -04:00
rzen c87616f3fb Comments join attachments on the card face — a quiet bubble-and-count chip, present-only
A card whose thread holds one comment or more now draws a second trailing chip beside the
paperclip: a secondary-tinted bubble glyph plus its count, shown only when the count is above
zero (design ruling 2026-08-09, card e729e30a). Same styling family as the attachments chip —
caption size, secondary tint, decorative and hidden outright from the accessibility tree — but
this one carries a visible count rather than staying icon-only, per the ruling's own "bubble-style
SF Symbol + count." It sits after the attachments chip at the row's trailing edge, in both the
live title row and the drag replica.

The count is a new `Card.commentCount` field the loader fills with a readdir over `comments/`'s
identity-shaped children that carry their own `index.md` — `BoardLoader.commentCount(in:)`, built
on the same `identityShapedChildren` predicate a trash entry's held-card count already uses. Never
a parse: `.draft` and `.trash/` are excluded for free, the same dot-prefixed hidden-entry skip
`CommentThread.load` documents for both, so the walk stays exactly the O(cards) shape
01-storage-format.md § Enhanced schema already commits to. Because the count rides inside the
`card: Card` parameter `CardFaceView` already takes — not a new parameter of its own — drawing the
chip costs nothing beyond a field read on an already-compared value: no new Observable read joins
the body, and the equatable gate already covers it via `Card`'s synthesized `Equatable`.

The one divergence from the comments pane's parsed count is documented rather than hidden: a
comment folder whose `index.md` exists but fails to parse is a `Stray` the thread read excludes by
opening and rejecting it, a cost this readdir does not pay. The face may then read one comment
high until that folder is fixed or removed — the trade the ruling's "cheap directory-entry count…
not a parse" asks for, over paying full parse cost on every card of every load. Every well-formed
comment, and every card with no malformed one, agrees with the pane exactly.

VoiceOver: `AccessibilityPhrases.cardValue` gains a `comments: Int` parameter, appended after
attachments and before the cut-pending phrase — the same left-to-right order the two chips draw
in, so a sighted read and a VoiceOver read never disagree about which comes first. The trashed
lane row's own call site (an opaque unit with no comments to speak of) passes `comments: 0`.

Docs: DESIGN/03-board-ui.md's card-face section describes both chips and retires the stale "closed
with no growth" sentence, honestly recording the 2026-08-09 growth (the hero banner landed hours
earlier, this chip after it) as exposure of facts the card already carries rather than a body
excerpt. DESIGN/10-accessibility.md's flattened-element sentence gains the comment count.
DESIGN/01-storage-format.md's Enhanced schema paragraph records the chip as shipped. WISHLIST #9
is marked shipped in place — not renumbered, since #10 and #11 are cross-referenced elsewhere.

Tests: CardCommentCountListingTests (BoardLoaderTests.swift) pins the readdir against a synthetic
tree — no comments/ folder, an empty one, non-identity-shaped and index-less strays excluded,
.draft/.trash/ excluded for free, agreement with CommentThread.load's parsed count in the
well-formed case, and the one documented divergence on a malformed index.md.
AccessibilityPhrasesTests covers cardValue's new parameter alone, alongside attachments, and
all three fragments together. ViewEquatableTests pins that a comment landing on a card is a gate
difference. BoardRenderPerformanceTests adds a render-cost guard: one comment added to one card
on a hosted 180-card board re-renders a handful of bodies, not the board.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 01:21:33 -04:00
rzen 9e6f4567df Print: entitle the sandbox, and stop the ⌘P chord from ever falling through
Root cause of the owner's repro (board window frontmost, File ▸ Print…
enabled, chosen from the menu, alert appears anyway): Kanban.entitlements
carried no com.apple.security.print key. The app is sandboxed, and a
sandboxed NSPrintOperation is denied by the sandbox with exactly this
wording — "This application does not support printing. Please contact
the application's developer." — regardless of which code path invokes
it. Fix: add the entitlement.

Alongside it, hardening for a separate, narrower failure mode that
happens to produce the identical alert text by a different mechanism:
PrintCommand used to disable itself over a window that published
neither a board nor a printable card (welcome, the template chooser,
Settings, the restore-bootstrap window, a card window whose board
hasn't joined). A disabled SwiftUI Button still owns its
.keyboardShortcut, so the unclaimed ⌘P chord fell through to AppKit's
own nil-target printDocument: action, whose stock failure is the same
system alert. The row now claims ⌘P unconditionally in every window;
scope resolves at the moment of the action instead (board, then card,
then a polite "Nothing to Print" / "Open a board or a card to print
it." refusal in the app's own voice). The boolean isEnabled(hasBoard:
hasPrintableCard:) becomes a three-way PrintCommand.resolveScope(...)
-> Scope pure function.

Also implements AppDelegate's application(_:printFiles:withSettings:
showPrintPanels:) — Finder's own File ▸ Print… / drag-to-printer /
print-and-open path was previously unhandled, its own separate route
to the same stock alert. PrintCoordinator.printFiles loads each path
headless through BoardLoader (no store, no window) and either prints
it or gives the same one-sentence refusal; the operation-building code
shared with the in-app path is factored out of run(_:) into
makeOperation(for:showsPrintPanel:) and runOperation(_:session:).

Docs: 11-command-nexus.md's Print row, PrintCommand's and
PrintCoordinator's doc comments, KanbanApp.swift's CommandGroup
comment, and project.yml's entitlements comment all narrate the
entitlement as the actual fix and the scope work as hardening beside
it.

Tests: PrintCommandValidationTests now exercises resolveScope's three
arms in place of the old boolean. A new PrintFinderResolutionTests
suite covers PrintCoordinator.resolveFinderPrint(atPath:) — the one
piece of the Finder half a test can drive without handing AppKit a
real print job — against a real board, an empty non-board folder, a
plain file, and an unsupported schema.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:46:16 -04:00
rzen ce92c24190 Hero image for cards — one of the card's own attachments, banded across its face
A card whose `hero:` names one of its own attachments draws that picture as a
banner across the full width of its plate, above the icon-and-title row,
aspect-fill cropped into a fixed 2.75 em band — 36pt at the standard body, and
em-scaled like every other figure the board draws, so it grows with the system
text size and with the board's zoom rather than shrinking against a title twice
its usual size. The figure sits deliberately under the 44pt a plain one-line
card is tall: a hero card should read as a card with a picture on it rather than
a picture with a caption, which is 03's standing rule that the title dominates.

The key's grammar is a **bare filename**, and that is what separates it from the
board background's `image` subkey rather than a nervousness about paths. A board
names a file anywhere under its root, so a path is that key's reading and where
it leads is the renderer's question. A card names one of the files it already
owns — the flat `attachments/` folder the app lists, relocates into, and carries
through every move, copy, trash and restore — so `hero: art/sketch.png` is not an
awkward spelling of a hero image, it is a value the key cannot mean. It therefore
has no reading at all: a value carrying a separator, or spelling `.`/`..`, or
empty, is malformed at the document layer, which renders it as absent and leaves
the coerce tier's trace, exactly as `width: 1.5` does. The bytes stay as written,
the resolver re-checks containment anyway, and the whole degrade family below
that — a name pointing at a missing file, an unreadable one, or one that is not
an image — ends the same way: no banner, no defect, nothing written.

That last promise is about *height* as much as about ink, so the band is given no
height at all until a picture has actually decoded. A card whose hero cannot be
drawn lays out identically to a card with no key, structurally rather than by a
branch somebody has to remember; the price is one settle per hero as a board
opens, and none after that. Everything else the face draws is attached outside
the new stack and is untouched by it — the accent stripe still runs the plate's
full leading edge across the band's corner, the selection and file-hover strokes
still ring the whole plate, the cut and drag dims still cover it, and the drop
model still registers the plate's real height, so a hero card is simply a taller
card the masonry already understands. The trash draws it too, by the one-face
rule.

Decoding is ImageIO's downsampling path off the main actor at a quarter of the
backdrop's pixel budget (`BoardBackdrop.decode` gained the limit as a parameter
rather than being copied), and the results live in one app-wide, deliberately
non-observable cache keyed on path plus the file's date and size. Non-observable
because a tracked write there would invalidate every hero face on the board,
which is the O(board) invalidation this view was rebuilt once already to shed;
each face holds its own picture in view state and seeds it from the cache, which
is also what lets the drag replica — whose preview builder is non-escaping and
cannot await anything — carry the band at the face's real height. Taking a stamp
twice from one URL value turned out to answer with the first read's date and size
however many times the bytes had changed, so `stamp(of:)` now drops its cached
resource values first; noticing a replacement is the only thing a stamp is for.

The face takes the resolved URL as a compared input rather than resolving it, for
selected-ness's reason one axis over: resolving needs the card's folder, which a
face does not know, and finding it from the snapshot would be a board walk per
face. The lane and the trash column each know their own container and compute it
once for the whole strip.

There is no in-app setter this version — the key is written by hand or by an
agent, which is why the guide bumps to v13 with a clause spelling the grammar out
beside the other card keys, and why `attachments/` gets the one-line pointer an
agent that has just written `![](attachments/x.png)` will need. "Set as Hero"
from the attachment row is future work, as is the card window and print, which
draw the same model and show no banner today.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:41:15 -04:00
rzen f9f284cac9 Keyboard ←/→ keep their place — a sticky ordinal that outlives the clamp
Lateral card navigation was pure geometry: the nearest drawn frame in the
direction. That loses the walk in the card's own title — stepping from a
10-card lane's 8th card into a 3-card lane clamps to its 3rd, and coming
back out, "the nearest frame at that height" is the 3rd card's height. The
information the user was walking at stopped being on screen, so no rule
over rectangles could have recovered it.

So it is remembered instead. `TransientBoardState.lateralOrdinal` holds the
1-based position a run of ←/→ started from, counted over the cards the board
is showing, and `NavigationMath.lateralHop` lands each hop on
`min(ordinal, target lane's count)` of the next lane that is showing cards —
collapsed and query-emptied lanes hopped over on `firstCard`'s rule rather
than by the accident of registering no frames. 8th → 3rd → 8th.

Every reset comes from one funnel and needs no enumeration anywhere: the
ordinal is a defaulted `nil` parameter on `select`, so a click, a marquee, a
↑/↓ step, an ⌥-jump, a ⌫ successor, a lane-domain arrow and the reload's
focus recovery all end the run by saying nothing. `resolve` adds the one
rule a value referencing no item can need — the ordinal never outlives the
head it was counted from — while a reload that leaves the cursor standing
leaves the run standing too.

A wide lane's interior masonry columns keep their spatial step and carry the
ordinal through untouched: a column hop is not a lane hop, and stickiness is
lane-granular over the logical order. With no lane in the direction the
geometry has the last word, which is how → still reaches the shown trash.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:28:05 -04:00
rzen eb950ee881 Remove the lane header's "+" new-card control
The lane header no longer draws a new-card button. Every other
creation path is untouched: File > New Card (Cmd-N), Return on a
selected lane, and double-click on lane empty space.

- LaneView: drop the button and its overlay slot, leaving the
  collapse chevron as the header's one piece of trailing chrome.
- NewCardTarget: drop the button's click-names-target carve-out over
  the Cmd-N target rule (Return on a selected lane is now the only
  direct-target path left).
- AccessibilityPhrases: drop the button's spoken label.
- BoardMetrics: drop its width reserve; laneHeaderTrailingReserve now
  reduces to the collapse chevron's own reserve.
- DESIGN/03, 04, 10, 11: update the lane title-bar inventory, the
  Cmd-N override clause and search-clearing mechanism list, the
  accessibility tree-shape sentence, and the pointer-grammar row that
  described the button.
- Tests: drop the two pinned accessibility-label tests and the
  newCardButtonReserve assertions; retarget the truncation-headroom
  test at the now-solo laneHeaderTrailingReserve; fix stale doc-quote
  comments and the manual accessibility-verification checklist.

Full KanbanTests suite: 2807 tests, 482 suites, all passing.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:28:05 -04:00
rzen b09c4bd5c0 Copy Link — a card context-menu row that puts the card folder on the pasteboard
Card 737a949f: "Add an option to card context menu to copy a link to
the card folder." Implements the design ruling verbatim.

- New context-menu row "Copy Link" (CardFaceView.boardMenu, board
  side only — trash cards are excluded, matching "sole selected live
  card"). Writes the clicked card's folder in one pasteboard item
  carrying two representations: the file:// URL under .fileURL and
  the plain absolute path under .string (FolderLinkPasteboard.swift).
  Enabled on a sole selected live card; disabled on a multi-selection
  and wherever edit-shaped actions already disable, per the ruling.
  Also exposed as a VoiceOver custom action alongside its siblings.
- Menu-bar twin: Board ▸ Copy Link (BoardCommands.swift,
  CopyLinkCommand), no default chord — the every-function-a-menu-item
  contract in DESIGN/11-command-nexus.md is still current, so this is
  the twin that contract calls for, homed the way Open Card/Rename/
  Style… already are.
- DESIGN/11-command-nexus.md: new Board-menu row and an updated Card
  context-menu row.
- Tests (KanbanTests/CopyLinkTests.swift): the target predicate's
  enablement (sole card / multi-selection / lane / trash / empty /
  inline-editing), the pasteboard write's exact bytes via a fake
  pasteboard (both representations, exact folder URL), and a
  disabled-target no-op.

Caught and fixed during self-review: an early version read the
context menu's widened-selection helper (targetIDs) inside the
Copy Link row's .disabled(...), which reads store.selection. Since
.contextMenu's content closure is evaluated on every ordinary body
pass (not only when the menu opens), that resubscribed every card
face on the board to every selection change — the exact O(board)
regression RENDER-INSTRUMENTATION.md's isSelected/selectedCount
split exists to prevent, caught by BoardRenderPerformanceTests and
MarqueeRenderCostTests. Fixed by reading the already-hoisted,
non-Observable `selectedCount` parameter instead, which answers the
same "how many ride along" question at zero extra subscription cost.

xcodegen generate, the Kanban scheme build, and the full KanbanTests
suite (2816 tests) are clean.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:28:05 -04:00
rzen bab456c08d Collapsible lanes — frontmatter-backed slim strips outside the width division
A lane folds to a fixed slim vertical strip carrying its glyph, its card-count
badge and its title turned on its side, and the strip is deliberately not part
of the window's division: the expanded lanes' units divide what is left once
each folded strip's fixed width has come off the top, so folding a lane is a
re-divide trigger of the Show/Hide Trash family — the window never moves and
the siblings grow into what the lane gave up.

The state is a first-class lane frontmatter key, `collapsed: true`, and
document state exactly as `width` is: the files are the board, so an agent
folds a lane by writing one key. Absent means expanded, expanding removes the
key rather than writing `false` (the remove-at-default family beside a
one-unit `width`, the empty rename's `title` and the None well's
`background`), and the lane's `width` rides along untouched so expanding
restores the lane the user had. The read is `width`'s leniency one type over —
a boolean scalar or a quoted boolean word reads as itself, everything else has
no reading at all and renders as expanded, bytes preserved either way.

Toggling is the header's always-visible collapse chevron, the lane context
menu's single Collapse Lane / Expand Lane row, and a plain click anywhere on
the strip; a modified click on the strip stays the ordinary selection grammar,
so a folded lane is still selectable by pointer. The title reads bottom-up and
is justified to the top of the room below the strip's chrome (owner ruling
2026-08-08), truncating against the strip's own height.

While folded the lane draws no cards at all, which is what makes every
exclusion true by construction rather than by a guard per gesture: no card
face means no marquee target and no navigation frame, and no registered grid
means the masonry's drop zones have nothing to resolve against. What did need
code is the half that names absolute destinations — the option-arrow jumps and
the arrow seed scan past a folded lane, the lane domain's down-arrow is inert
on one, and New Card skips it (a selection inside one falls through to the
last-active lane, the stale selection's rule). A drop on the strip appends at
the lane's end, cards and Finder files alike, with an accent edge standing in
for the shadow the strip has no masonry to open; there is no hover-to-auto-
expand yet. Lane reorder works on the strip, and a dragged folded lane carries
its fold, so its shadow and its replica are the strip rather than its units.

The write is `writeLaneWidths` clause for clause — one `updateIndex` bracket,
the same stamp behaviour, the same three do-nothing paths — with two new
`WriteOperation` cases and two new undo verbs rather than one of each, because
a banner or an Edit-menu row that said "resize" after Collapse Lane would name
a control the user never touched.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:53:10 -04:00
rzen 57542177c1 The undo rows stop hearing their own echo — a text manager answers canRedo by posting the checkpoint that re-invalidated the row, forever
Diagnosed by sampling a live frozen instance: with a text surface focused,
RedoMenuRow's body reads the routed manager's title, NSUndoManager.canRedo
posts NSUndoManagerCheckpoint synchronously, UndoCommandTicker bumps its
observed revision mid-body, and SwiftUI schedules the re-evaluation whose
own read posts the next checkpoint — the main thread never returns to the
event loop (~99% CPU, app frozen). Board-routed reads never echo, because
BoardUndoManager's overrides answer from the provider without posting —
which is why the board-only live probe (21/21) never met the loop.

The rows now derive title and enablement inside
UndoCommandTicker.silencingReadEchoes, a synchronous main-actor window in
which bump() drops what arrives: a read cannot change the state it reads,
so the echo carries no information and dropping it loses nothing. Genuine
checkpoints — registration closing a group, a crossing — still land.

Three regression tests pin the mechanism, including the asymmetry that
made the redo side the fuel: canUndo answers silently, canRedo posts.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 22:50:00 -04:00
rzen 7651e40318 Print boards and cards with configurable components and named print profiles
⌘P had no story: KanbanApp removed the platform's Print row outright on
11-command-nexus.md's "No Print story in v1 (⌘P unused)" line. That line
retires. File ▸ Print… now prints the board in front — lanes left to right,
each lane's cards top to bottom, as a linear document rather than a picture
of the strip — or, from a card window, that card. The trash is unreachable
by construction: it is a sibling container of `lanes`, not a lane.

The rules live in a pure layer nothing AppKit can reach. `PrintOptions` is
one Codable value carrying the printing card's five bullets — which
components (title, icon+labels line, rendered body, comments off by default
with either reading order), page breaks, one base face and size every other
size derives from, and a toggleable running head and foot. `PrintSource` is
what is being printed, frozen at ⌘P so the panel's repeated relayouts and a
board reloading underneath cannot disagree. `PrintDocumentBuilder` turns the
pair into a block list, which is where every decision a rendered page hides
becomes something a test can hold: component order, comment ordering, and
page-break markers that are markers rather than whitespace. Empty is empty
all the way up — a card with nothing to print consumes no page break, and a
lane whose cards all dropped out takes its heading with it.

A page break is a pagination fact, not a spacing one. TextKit has no
page-break character, so `PrintDocumentView` splits the document into
sections at its breaks and flows each into as many page-sized text
containers as it needs: a container boundary *is* a sheet boundary, at any
paper size with any margins. Bodies come from the app's one Markdown pass —
`BodyMarkup.parse` into `BodyMarkupRenderer` — re-faced run by run so the
chosen family reaches the text and fixed-pitch code keeps its own, and drawn
under a forced light appearance so the card window's dynamic label colours
do not print white.

Options ride in the print panel's own accessory rather than a pre-flight
sheet of ours, which buys the system's live preview of the real paginated
document; the preview refreshes through one KVO revision counter rather than
thirteen mirrored properties. Profiles persist app-side in UserDefaults,
never in board files — a print profile is how this user likes to read, not
what a board is (`BoardZoomStore`'s argument). A name is a profile's
identity, folded case-insensitively; "Last Used" is reserved in every
spelling, kept out of the stored list, and captured when an operation
actually ran, so a cancelled print rewrites nothing. Both decoders are
total: one unrecognized key must not cost a user every profile they saved.

DESIGN/11-command-nexus.md gains the Print row and loses the sentence
saying it would never have one.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:42:11 -04:00
rzen cdc91d669d The embedded guide catches up on its own — creation-time parity for both apps, an open-time refresh for the phone
The card's frozen spec recommended Option C (install the agent guide at
board creation); the owner's follow-up comment extended that ruling to a
second axis — embedded guidelines should update whenever a board opens if
the on-disk version is older than Lanework's, which the Mac app already
does via BoardStore.refreshAgentGuide()/runScheduledHeals(). This card
implements both halves.

- BoardWriter.createBoard now calls AgentGuide.install(atBoardRoot:)
  right after seedGitignoreIfAbsent, so every board — Mac- or
  phone-created, since KanbanMobile.BoardIndexStore.createBoard calls
  this same method — is born with a current-version CLAUDE.md, with no
  dependency on a later open. Routed through AgentGuide.install itself
  rather than a hand-rolled write, so never-downgrade, the
  CLAUDE.user.md rescue, squatter displacement, and the EchoLedger
  heal-attribution exclusion all carry over unchanged.

- BoardSession (KanbanMobile) gains a private refreshAgentGuideOnce(),
  fired once from open() (already idempotent on the .idle phase),
  fire-and-forget through the same CoordinatedFileAccess.write bracket
  every phone write uses. Deliberately not a heal scheduler — a
  one-shot courtesy check at session open, silent on failure (logged,
  never surfaced to lastError or a banner), matching AgentGuide's own
  "nothing here is a user-facing event" posture. The type's doc comment
  now names this one exception while keeping "no heal scheduler" true.

- project.yml: lifted the KanbanMobile target's AgentGuide.swift build
  exclusion (dating to the original mobile MVP, "agents work where the
  Mac app runs") — both changes above fail to compile on the phone
  without it, since the type simply wasn't in that module. Verified
  safe: AgentGuide.swift imports only Foundation, and its one upward
  dependency touches only EchoLedger's unconditional recording API,
  never the #if os(macOS)-gated consumer surfaces.

Tests: KanbanTests/BoardWriterTests.swift gains
createBoardInstallsTheCurrentAgentGuide, calling createBoard directly
and asserting the guide lands at AgentGuide.version immediately — the
card's own Done-when, and also the phone's creation-time coverage since
it's the same call site. KanbanMobileUITests/AgentGuideUITests.swift
covers the open-time refresh itself, the one piece only reachable
end-to-end from a running KanbanMobile process (no mobile unit-test
target exists): the bundle's fixture board already carries no
CLAUDE.md, so tapping into it and polling disk proves the wiring with
no fixture changes needed.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:22:02 -04:00
rzen 05ae703989 replace symbol grid in card window sidebar with a compact symbol picker
The card window sidebar's Style section embedded StyleEditorView symbols-only
(showsBackground: false, showsSymbols: true), drawing the curated grid whole
inside the sidebar's own scroll view — a permanently open 5-7 row grid ahead
of every other section. Replace it with the reusable compact SymbolPicker
(already used by BoardInfoPopover for the board's glyph): a single well at
rest, the grid only inside its own popover.

- CardStyleSection no longer instantiates StyleEditorView at all. A new
  "Symbol" row mirrors the existing "Background" row's inspector shape
  (caption leading, control trailing), wiring SymbolPicker's onSelect/
  onSelectColor to icon/iconColor through StyleCommand.apply(...on: undo),
  the exact funnel the background combo already rides — so the card window's
  undo session semantics (13-native-undo.md) are unchanged.
- The picker is fed CuratedSymbols.available rather than its own smaller
  general-purpose default, so a card's curated vocabulary doesn't shrink.
- StyleEditorLayout.sidebar and showsBackground stay in StyleEditor.swift
  (still correct, still tested) rather than being cut as dead code — a
  separate, larger cleanup this card doesn't make (recorded on the card).
- DESIGN/03-board-ui.md and DESIGN/05-card-window.md: updated the sentences
  describing the sidebar hosting the style editor's symbol grid to describe
  the compact picker instead.
- Tests: CardSessionUndoTests gains a symbol/tint analogue of the existing
  background-combo session-routing test; CardSidebarTests gains a curated-set
  tripwire and an updated file-header note.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:14:40 -04:00
rzen 898facebe0 The storage spec admits comments shipped — 01's Enhanced schema heading drops 'feature out of scope'
Comments have been live since the 2026-07/08 build-out (thread column, composer,
.draft, comments/.trash on Mac and mobile), but 01-storage-format.md's Enhanced
schema heading and opening clause still called the feature out of scope. Surfaced
by the done-when audit of the Pipeline card 'Revisit comments — available in all
versions, not teams-only'. Heading, intro, and the comments bullet's parenthetical
now state shipped reality; tracker keys (labels/assignees/due, remote/remote-state)
stay reserved as before.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:06:08 -04:00
rzen 9a52b795b2 The drag learns the stepper's trick — past the screen's edge, lane growth re-divides instead of stopping
The right-edge drag's growth was capped at the screen's visible frame,
because each snap tick grows the window; on a window near the screen edge
that left a lane stuck at a tick or two of headroom. Settled 2026-08-08
(03-board-ui.md § Lane, superseding the pathfinder's hard stop): at the
screen the window pins and each further tick re-divides the fixed strip
width across one more unit — siblings compress, the stepper's mechanism
arriving under the drag's fingers. The regimes meet with no pixel jump
(the re-divided standard at the fit IS the frozen standard, by the
exact-fill identity), shrinking mirrors the way back, the rubber band
moves to the strip's own capacity, and a window with no headroom at all —
full screen included — re-divides from the very first snap.

New pure arithmetic in LaneLayoutMath (pinnedStripWidth, resizeStandard,
resizeMaxUnits, resizeWindowDelta, snappedUnits over per-count slots);
LaneResizeSession splits the tick across the regimes and derives its
standard from the live count; the handle and BoardView hand the session
the strip's whole divide. 2709 unit tests green (+11).

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 21:36:56 -04:00
rzen 05b1a787de The sync workstream gets its charter — strategy/02: anchors, the eleven-area issue map, drilling order with the one-way doors named 2026-08-08 21:12:58 -04:00
rzen 51d7b4b4bf The owners say so out loud — LSHandlerRank: Owner on both apps' board-type claims
Implied already by exporting the UTI; explicit so Lanework outranks any
third-party claimant of dev.rzen.indie.kanban-board on either platform.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 20:54:11 -04:00
rzen cb25ba94fe The product takes back its name — Lanework.app returns as the built bundle, the module keeps the Kanban codename
PRODUCT_NAME: Lanework and PRODUCT_MODULE_NAME: Kanban were the old repo's
arrangement and fell out in the git excision, leaving Debug builds to register
themselves with Launch Services as Kanban.app. KanbanTests points TEST_HOST at
the real product, since XcodeGen derives it from the target name and would miss.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 20:18:55 -04:00
rzen 1fd19dfb12 The undo command surface rebuilds — app-owned rows and explicit toolbar targets over FocusedValues
Edit ▸ Undo/Redo become the app's own replaced rows and the board toolbar
pair takes explicit targets, both reading the focused session's
BoardUndoManager through FocusedValues.undoStack (board windows publish the
session's manager, card windows their own) — the nil-target route died with
the SwiftUI window latch, 13-native-undo.md ▸ Rules ▸ command surface,
re-ruled 2026-08-08. The rows enact the routing predicate themselves: text
focus routes ⌘Z to the first responder's own manager, title and enablement
included, re-derived at fire time with a beep for the stale window.
NativeHistoryProvider turns @Observable so both surfaces re-derive on stack
changes; a checkpoint-notification ticker covers plain text managers.
.responderAction leaves ToolbarItemSpec with its only user;
windowWillReturnUndoManager stays wired for AppKit's own asks.

Live-probed on the fixture board (21/21): the row retitles to "Undo Add
Lane" and crosses via real ⌘Z key events, ⇧⌘Z redoes via a window-server
chord, the toolbar pair validates and fires, search-field and body-editor
⌘Z stay text undo with board stacks untouched, and a card window crosses
its own stack with no fall-through. 2698 unit tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 18:55:34 -04:00
rzen 78c32776d4 The phone reads the thread — comments arrive on the card's read view, posting and editing behind transactional sheets
The comment thread renders read-only under the card's body (author line, Markdown body through CardBodyView, read-only paperclip rows), read outside the snapshot and re-read on every walk landing via BoardSession.snapshotGeneration. Add Comment posts through the Mac composer's own draft-then-rename bracket — seeding from the card's single synced draft so a thought started on the Mac finishes here — and each row's context menu opens the same sheet in edit mode. Both commit on their trailing button or not at all: the phone's transactional model, dirty-Cancel confirmation and swipe-dismiss disabled while dirty included. UI-tested end to end with disk assertions.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 18:18:56 -04:00
rzen 2516c4ba5d The phone learns to read whole blocks — CardBodyView renders BodyMarkup's full tree, and the title falls in line
The read view's inline-only AttributedString shim gives way to a real
block renderer over BodyMarkup — the same parse the Mac Preview trusts,
whose swift-markdown dependency project.yml said was already riding
along for exactly this surface. Headings, nested lists with static task
checkboxes, quotes, GFM tables with per-column alignment and clamped
colspans, sideways-scrolling code blocks, literal HTML, dividers, and
tappable absolute links (relative ones stay prose — no folder to
resolve against). The title block sheds its inner padding so its
leading edge sits flush with the body; the tint now grows outward via
a negative background inset instead of pushing the text in. One rich
fixture card body enriched to exercise every block kind; Mac fixture
round-trip suites verified green against it.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 15:22:36 -04:00
rzen a8e8dd13d4 The undo command surface re-rules — app-owned rows over FocusedValues, the window latch on record
NSWindow latches its own empty undo manager during SwiftUI window creation,
before any delegate of ours can answer, so the nil-target route 13 assumed
never reaches the session's manager. Ruled today: Edit ▸ Undo/Redo become
the app's own replaced rows and the toolbar pair takes explicit targets,
both reading the focused BoardUndoManager; the routing predicate relocates
to the command layer unchanged; the delegate hook stays wired, unrelied-on.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 14:49:33 -04:00
rzen c4591ead2c Cards open to be read — the mobile detail screen turns read-only, editing moves behind a transactional Save
CardDetailScreen renders title, inline-Markdown body, and a quiet dates
footer; all writing moves to the new CardEditScreen, a full-screen cover
with segmented Details/Body panes. Drafts commit in one perform bracket
on Save only — Cancel guards dirty drafts with a discard confirmation,
and a failed write keeps the sheet and its drafts and raises an alert
instead of dismissing. CardAttributesSection becomes a pure
binding-driven editor with no write path of its own. The UI test walk
crosses the new split, with body-pane and discard coverage, and a
deterministic replaceAllText helper retires the flaky ⌘A select-all.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 14:47:50 -04:00
rzen 28abaac2d9 The lane and card lists give up their pickers — rank order is the only order below the boards
The sort control stays on the board list alone; lanes and cards return
to the arrangement the ranks state, long-press reorder now ungated
because the rows always show the order a drag would rewrite.
ItemSortOrder leaves with the pickers that needed it.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 13:41:09 -04:00
rzen 2d919b8131 The prose catches up with the one-version app — tier qualifiers retired, the entitlement comment carried over
The excision's follow-up sweep: present-tense prose that still implied an edition axis now reads
correctly under one version ("in every tier" clauses dropped or turned substrate-shaped, the
announcer's "every free-tier bracket today" is "every bracket today" — nothing passes a phrase),
and forward-looking promises pinned to the mooted pro-m1/pro-m2 milestones now name the thing
itself (the change narrator in Kanban/Changes/, the foreign-change journal successor) or fall to
past tense. Kanban.entitlements' network-client comment sheds its "dormant until Pro ships"
framing for the pivot's own reasoning: the key stays because the sync capability to come needs
it regardless. Untouched on purpose: the storage layer's coerce/tolerate/refuse tiers, the
chooser's bundled/user/keyless tiers, verbatim design-doc quotations, and genuine past-tense
record. One dangling reference repaired en route: EndToEndVerification.md cited the long-renamed
InertGitTests. Comment-only throughout; 2,686 unit tests green, unchanged.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 13:40:13 -04:00
rzen df986566e5 Rows learn to be carried — long-press reorder for lanes and cards, manual mode only
An onMove on both lists, live only while the sort picker says Manual:
the drag mints a midpoint rank between its landing neighbours through
BoardWriter.moveItem's same-parent reorder path, and when a gap is
exhausted the whole container renumbers to a fresh 1024 ladder in one
perform bracket. Neither path stamps modified, so a reorder never
disturbs the Recent sort. UI suite 4/4 in the simulator.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 13:28:24 -04:00
rzen e1f89d9cf9 The subscription machinery leaves the code — Kanban/Tier excised, StoreKit wiring unwound
The 2026-08-08 one-version ruling (12-editions.md ▸ PIVOT 2026-08-08) carried out: Kanban/Tier/
deleted wholesale (Tier, ProEntitlement, ProProducts, ProStorefront, the never-rendered
ProSettingsSection) with TierTests and Configuration.storekit, whose project.yml resource entry
and scheme storeKitConfiguration go with it. AppModel loses the entitlement, the currentTier
seam, BoardSession.tier, and the purchase flow's reopenOpenBoards (its only caller was the
storefront); AppDelegate's launch keeps only the appearance application. The three tests
pinning the recorded tier and the reopen are deleted with their subject. The network-client
entitlement stays — the sync capability to come needs it regardless — and the HistoryProviding
seam stands untouched. 2,686 unit tests green (2,707 minus the 21 that tested what left).

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 13:18:54 -04:00
rzen 871083e5ca The tab bar retires — boards are the app, settings behind the gear, recency the default order
The boards stack becomes the root screen: Settings moves from its own
tab to a sheet behind a leading gearshape button, the sort pickers trade
their text segments for symbols (hand.draw, textformat.abc, clock) with
the words kept for accessibility, and every list now opens sorted by
most recent change. UI tests re-aimed at the gear button and Done-button
scoping fixed for the sheet-over-sheet nav bars — 4/4 green in the
simulator.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 13:07:44 -04:00
rzen 53be63bb03 The tier axis goes quiet — one version, everything free, Pro deferred to the sync era
User-ruled 2026-08-08, after the git excision: the base/Pro split is mooted for now. The charter's anchors collapse to a single version (monetization workstream parked, the generosity tension resolved by construction), 12-editions.md carries the pivot note naming what the companion excision removes (Kanban/Tier/, TierTests, Configuration.storekit and its wiring; the network-client entitlement stays for the future sync service), and the README's tiers bullet becomes the one-version statement. Pro returns with the iPhone + ops-based sync capability as a fresh design pass — a reintroduction, not a resurrection. The code excision itself is handed off.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 13:00:44 -04:00
rzen 4e1cbd1da9 The sort picker climbs to the navigation bar — principal slot, centered
Same three conditional pickers, now riding the nav bar's center instead
of the bottom bar, width-capped so the segments don't sprawl.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 12:47:36 -04:00
rzen 4c179f63fc The plan closes its own loop — executed 2026-08-08, steps 1 through 8, each committed green
Step 8's verification record: 2,707 unit tests green at close (the golden fixture suite among them, untouched — no git fixtures ever existed); the UI test targets compile; the keeper behaviors — a board beside a real .git opened, watched, and healed byte-identically — are pinned by GitAgnosticStorageTests, FolderWatcherTests, and the gitignore/relocation suites, all green. The accessibility audit could not run to a verdict in this environment (its AX sweep flagged another app's fullscreen video element — the same failure on surfaces the excision never touched); the popover audit's own screenshot shows the strip as ruled: Info, Theme, Sync.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 12:43:25 -04:00
rzen 49b2f4db6e The sort control leaves the list — three pickers settle into the bottom bar
Same segments, same persistence; the picker now rides each screen's
toolbar as a bottom-bar item shown only when there are rows to reorder,
instead of masquerading as the first row of the list it sorts.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 12:42:44 -04:00
rzen e8791b239d The phone's rows learn their faces and two more orders — icons everywhere, lanes and cards sortable
Boards, lanes, and cards all show their frontmatter icon leading the
row, tinted through the palette with a secondary fallback; the board
scanner now reads icon and iconColor from the same one-file parse as
the title. Lane and card lists gain the segmented Manual/Name/Recent
control — display-only sorting layered over the rank order, never
rewriting what's on disk.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 12:34:10 -04:00
rzen 445d035a83 The paper agrees with the code — guide v11, README, DESIGN re-rulings, and the adjudicated sweep
Step 7 of strategy/01-git-excision.md, the companions. The agent guide bumps to v11: the Git section teaches repo-resident etiquette alone (stage only your own paths, commit your own changes, leave app-maintained files to the app) — existing boards heal to the new text on next open. README re-anchors: the four git feature bullets out, tiers say the complete Mac experience is free, and one bullet states the format's git-friendliness promise. The changelog drops the never-shipped git entries. DESIGN re-rules: 06 retired with Undo routing migrated to 13 (now the sole substrate's doc, seam kept open), 07 retired as written pending the ops-service workstream, 14 retired as superseded record, 12 carries the second pivot note, the index reflects all of it; the charter gets a pointer note (the anchors' full re-ruling stays with the user). InertGitTests renames to GitAgnosticStorageTests — the excision restores its original claim app-wide. And the sweep: ~70 comment sites across 36 files adjudicated against the keeper list, every present-tense description of the excised machinery made past tense or repointed, keepers untouched. 2,707 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 12:31:27 -04:00
rzen 8cf1457814 The phone's list learns a second order — sort boards by name or by recency
A segmented control above the mobile board list, remembered across
launches: Name keeps the index's stable title order, Recent sorts by
content-change date descending with undated boards last and ties held
stable against refresh reshuffling.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 12:21:35 -04:00
rzen 09a21cf5a0 The last thread cut — SwiftGitX and libgit2 leave the project
Step 6 of strategy/01-git-excision.md, the proving step: the two packages and their rationale blocks (including the stale never-runs-in-the-free-app note) come out of project.yml, the project regenerates, and the build resolves clean — nothing living referenced libgit2. Resurrection pins (SwiftGitX 0.4.0, libgit2 1.9.2) are recorded in the plan and at the pre-git-excision tag. 2,707 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 11:41:27 -04:00
rzen edaf5be706 The stack comes out — Kanban/Git/ deleted wholesale, ten thousand lines into history
Step 5 of strategy/01-git-excision.md: the seventeen dead engine files and the five remaining git test suites go (InertGitTests stays — the naming footgun is a Storage keeper). Two rescues ride ahead of the delete: HarvestedReceipt relocates to EchoLedger (the harvest surface outlives its git consumer; foundation for the deferred journal), and commentTimestamps joins the narrator it always served. The provider-swap purge test re-expresses over a git-free fake; the duplicate-id ladder keeps every pure historyRank pin and loses only the two ranker-driven ones. Resurrection point: tag pre-git-excision. 2,707 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 11:38:16 -04:00
rzen adbc5ddd89 Backup bows out of the phone — the zipped-share future replaces it, later
IndieBackup comes out whole: the package, the Backup directory, the Settings section, the .kanbanbackup document type and its onOpenURL restore path. Settings is now the About section alone. The Files-app keys stay — they serve local-board visibility, not backups. Board sharing as a zipped archive is the planned replacement and is deliberately not started here.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 11:25:53 -04:00
rzen cdba512512 The seams unbind — the provider is always native, and the git stack compiles dead
Step 4 of strategy/01-git-excision.md, the entangled one: AppModel's makeHistoryProvider collapses to the native provider (the seam stays injectable per the reversibility posture), the session's git state and its wiring go (wireGitUndo, wireBranchSwitching, the card-session staging threading), BoardStore sheds commitSeam and the identity-history ranker (the loader's nil-safe rung now tops out at birth date — today's no-git behavior), SessionSettleGate keeps the gate and inherits the path utility it borrowed, BoardRegistry drops the persisted operation stamp (decode-safe), and the git banner family leaves BannerCenter with its announcer and accessibility phrases. One missed harvest tie severed (the narrator's root subject is its own now). Nothing outside Kanban/Git/ references the stack — proven by sweep. 2,855 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 11:25:41 -04:00
rzen f6a24132b6 The phone borrows the Mac's face — the desktop icon master becomes the iOS app icon
The Mac set's 1024 master is full-bleed square artwork already, so it carries over whole: alpha-flattened (App Store icons must be opaque) into a single-size iOS AppIcon set, and the target points at it.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-08-08 10:56:39 -04:00