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
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 `` 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
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
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
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
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
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
⌘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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
A trailing swipe on a board row opens Board Settings, whose first setting is location: iCloud or Local, with a confirmed move to the other side — destructive-styled only outbound, because leaving iCloud is the direction that sheds protection. The move is setUbiquitous against the real container and a coordinated move under the DEBUG stand-in; evacuation sweeps materialization first and refuses honestly while content is still downloading. The local home is the sandbox Documents folder, published to the Files app, so a local board is still a folder the user owns.
With a second home the iCloud wall softens (user-ruled 2026-08-08): the index always reaches ready, cloud unavailability becomes an inline notice with a retry, creates land locally when there is no account, and LANEWORK_FORCE_NO_ICLOUD makes that state reproducible in tests regardless of the machine's sign-in. Known gap, now user-reachable: backup remains iCloud-only, so local boards sit outside it.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Step 3 of strategy/01-git-excision.md: the popover strip is Info/Theme/Sync, the titlebar widget says the name alone, the card window's History section and its slot go, Board ▸ Pull/Push comes out with the RemoteCommands scaffold, and View ▸ History re-tags from the commit trail to the deferred foreign-change journal. The Sync placeholder re-annotates to the future ops-based sync service. Four git test suites leave with the surfaces they pinned (BoardGitSetup, BoardInfoPopover, BranchSwitch, GitUndo). The git engine still compiles underneath, unreferenced by UI. 2,890 tests green.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
Step 2 of strategy/01-git-excision.md: CommitMessageEngine and the composer seam relocate to a neutral module renamed away from commit vocabulary (ChangeNarrator, ChangeNarrationRequest, ChangeNarrating, SemanticChangeNarration, ChangeAuthorship), GitChangedPath extracts from GitCommitOperation as ChangedPath, and the one git tie severs — authorship's foreign case carries a display name, not a GitIdentity. The spec tests transplant as ChangeNarratorTests, alive until the journal work begins. 3,009 tests green.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
Under parallel-build load the six-turn settle let the first click land before the masonry's measurement passes did, on the empty-space layer instead of a card — which the dragless layer holds forever in a sterile queue, reading as a dead click. Bisect-verified environmental (same failure at every commit back to the suite's birth); with the settle, click → selection in 41 ms.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
The mobile app's first runtime coverage, and its first runtime, full stop: the suite launches the real app over a scratch directory seeded from rich-board.kanban via LANEWORK_LOCAL_ROOT, walks boards to lanes to cards to the card editor, retitles a card and polls the frontmatter on disk until the write lands, then taps through the Settings About section to see the changelog and license actually render. No iCloud account, no metadata query — the DEBUG override is the whole harness.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The About section lands last in the mobile Settings form, mirroring the Mac AboutBox voice: license link, copyright, and a version label that doubles as the changelog link. The changelog is the phone app's own — a separate product with its own 1.0, so the Mac CHANGELOG.md would narrate someone else's releases. The Update Build Info stamping phase rides along so the version line can carry a build number and date.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The dragless empty-space layer's clicks are held forever in a sterile synthetic queue (and injected micro-motion deadlocks AppKit's tracking loop), so the empty-space pins leave the suite; their truth is established on real event streams (2026-08-07). What remains pinned is the original defect's surface: instant card selection, double-click opens without a placeholder, right-click reaching its menu.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
research/08-competitive-analysis.md profiles the direct set (features, pricing, traction, matrices, ranked threats); strategy/01-git-excision.md carries the 2026-08-08 ruling to excise app-managed git, with keepers, harvest, delete list, and sequencing. Both indexed.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
A second product, not a second edition: dev.rzen.indie.KanbanMobile (iOS 26,
iPhone-only) compiles Kanban/Storage as source files, so a format change that
breaks the phone breaks this build the day it's made. Boards live in
iCloud.dev.rzen.indie.Kanban — named after the Mac bundle id so the Mac app can
adopt the container later without a migration; until then the folder is
"Lanework" in iCloud Drive and the Mac opens boards there through the open
panel.
EchoLedger grows #if os(macOS) gates around its three consumer surfaces
(verdicts/BoardDiff, harvest/HarvestedReceipt, comment retirement/CommentPath)
— the recording side BoardWriter stamps compiles on every platform, and the
gates are the seam a future phone verdict surface lands behind. AgentGuide
stays Mac-only.
The phone's watcher is NSMetadataQuery: BoardIndexStore (one query, package
UTI export makes a .kanban directory one item, equality-gated rescans,
download kicks per pass), BoardSession (materialization sweep before every
fail-fast walk, NSFileCoordinator brackets, perform{} = coordinated write then
awaited reload, ParseMemo threaded), CloudHome (off-main container resolution,
LANEWORK_LOCAL_ROOT DEBUG override for simulator work without an account).
Screens: Boards -> lanes -> cards -> card editor, value-routed by ItemID with
every screen re-reading the live snapshot; leading swipe moves a card via
confirmationDialog, trailing swipe sends it to .trash/; the editor commits
title through the Mac's canonical rename path and body through writeBody, with
drafts that survive reloads; attributes are the three typed style fields
(icon, iconColor, background) — labels is a reserved unknown-field key and
deliberately has no editor. Settings carries IndieBackup (backup root = the
container's Documents, restore rebuild = an index rescan, controller
constructed only once the home resolves).
Arbiter: KanbanMobile green for iOS Simulator, Kanban green for macOS, 3006
unit tests / 517 suites passed (PointerLatencyTests excluded — mid-rework
uncommitted in a parallel session).
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
BoardInfoTab gains .sync last in the strip, rendering BoardSyncTabView's one
honest caption. The tab claims the position where 07's remote half will
live without ruling where its setup surfaces land — that stays the open
Redesign card's question.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The titlebar widget becomes a two-line identity block: the board glyph at
22pt spanning both lines, the title over the branch (git-mode only, smaller
and secondary), the em-dash retired. New Branch… returns to the switch menu
behind a divider, revealing an inline name field — the pre-split shape. The
board settings sheet retires whole: add-git and commit identity render
inline in the Git tab's postures (BoardGitSetup.swift), the availability
rule collapses into BoardGitSetupSection.resolve, and Board ▸ Board
Settings… leaves the menu bar. Where 07's remote/credential setup surfaces
land is deliberately left open — filed on the Redesign board.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
Settings loses its Lanework Pro section and the About box its Pro line —
no surface names or sells Pro while the split is re-decided. The machinery
underneath (ProEntitlement, ProStorefront, ProSettingsSection, the
recorded session tier) stays compiled and tested, documented as dormant
since the pivot. CHANGELOG's two 2.0 entries and README's feature list
drop the Pro-gated framing while keeping the facts.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY