The phone joins the format — KanbanMobile MVP: shared storage verbatim over an iCloud container
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
This commit is contained in:
@@ -68,6 +68,8 @@ Lanework is in early development. This list tracks what has actually shipped and
|
||||
- **Branches and commit identity** — the board popover's git section is where a git board's branches live: the current branch is itself the picker, and beside the local branches it offers create-and-switch, which starts the new branch at the commit you are on. **Switching is never silent.** If any card window is holding unsaved keystrokes, an open Edit session whose crash-safe saves no commit has yet, or an open raw-source buffer, the switch stops and asks — **Save All**, **Discard**, or **Cancel** — for *every* open window, not just the ones the checkout would touch, because an unapplied raw buffer would otherwise write the whole pre-switch file onto the new branch's card later on. Save All ends each session with its normal commit and applies each raw buffer; a buffer that won't validate cancels the whole switch and puts you in front of the window that refused, nothing half-switched. Discard puts both the buffers and their uncommitted on-disk saves back to the last commit. With the sessions settled the pending auto-commit flushes onto the branch you are **leaving**, so the checkout runs on a genuinely settled tree and cannot fail dirty — and the checkout itself is git's *safe* one, never a force: work the app somehow didn't settle refuses the switch rather than being overwritten. The switch is bracketed like every wholesale operation — the watcher suspended, one full reload at the end, an in-progress row saying "Switching to 'main'…" — and if that final reload fails, the board locks read-only until a reload succeeds rather than letting you edit a snapshot of the branch you just left. Undo and redo do not survive the switch: the stack is discarded and reseeded from the new branch's own history, with redo empty, because replaying a restore from the old branch onto the new one would be wrong. When another program holds the repository's index, the switch waits and retries quietly, the row changing to say it is waiting for another writer's git lock and eventually naming the lock file — never a dialog, and the lock is never removed, because it isn't the app's. **An interrupted switch cleans up after itself**: before touching the repository the app writes down what it is about to do — in its own state, never in your board and never inside `.git` — so a crash or an unplugged volume mid-switch is recognized at the next open, rolled back to the state you were in, and announced ("A branch switch was interrupted — the previous state is restored"); an unfinished merge or rebase that *isn't* the app's is still left strictly alone. Beneath the branches sit the **commit-identity** fields: what you type there is written to the repository's own `.git/config`, so the setting is the file — portable to any git client, per board, and what the next commit is signed with. Leave a field empty and it shows the derived default (your account name, and `you@yourmac`) as a placeholder rather than filling it in, because a value the app wrote there would quietly outrank your own global git config for that board. A config edited from outside while the popover is open refreshes the fields you aren't typing in and leaves the one you are alone.
|
||||
- **A card's History** — the card window's sidebar gains a read-only **History** section on git boards: every commit that touched that card, newest first, each row its own semantic subject over a relative date and the author who made it — so an agent's work and your own read as one story ("Move card 'Fix login' to Doing · 2 days ago · Claude"). It follows the card by identity rather than by path, so moving between lanes — or into the trash and back — keeps one continuous trail. Read-only in this version: restoring a single old version stays a git-client job. The section is simply absent on boards without app-managed git — no placeholder, no greyed-out promise.
|
||||
|
||||
- **Lanework for iPhone (in development)** — a companion iOS app, second product rather than second edition (`dev.rzen.indie.KanbanMobile`), whose boards are the same `.kanban` folders in an iCloud Drive container (`iCloud.dev.rzen.indie.Kanban` — a folder the Mac's Finder shows as "Lanework", so the Mac app opens the same boards through the ordinary open panel today and can adopt the container natively later with no migration). It compiles the Mac app's storage engine *verbatim* — same loader, same writer, same byte-fidelity contract, so the two apps cannot drift on the format — and drives it from an `NSMetadataQuery` observer instead of FSEvents. The MVP surface: a Boards tab (boards → lanes → cards → card editor, with per-item download/materialization states honestly rendered), swipe actions to move a card between lanes or send it to `.trash/`, a card editor for title, body, and the typed style attributes (icon, icon color, background), and a Settings tab with local backup/restore of the whole container (IndieBackup). No git on the phone — the change signal is the container itself.
|
||||
|
||||
## Development
|
||||
|
||||
The Xcode project is generated — `project.yml` is the source of truth, not the `.xcodeproj`:
|
||||
@@ -86,7 +88,15 @@ xcodebuild build-for-testing -project Kanban.xcodeproj -scheme Kanban -destinati
|
||||
|
||||
macOS 26+, Swift 6 (strict concurrency), SwiftUI, sandboxed. Internal codename `Kanban` (target, scheme, bundle id `dev.rzen.indie.Kanban`); the app ships under the display name **Lanework**.
|
||||
|
||||
**One app target** (DESIGN/12-editions.md ▸ The target). `project.yml` declares exactly three: the app `Kanban`, the unit bundle `KanbanTests` hosted by it, and the UI bundle `KanbanUITests`; one scheme, `Kanban`, builds and tests all of them. The 2026-07-27 two-target edition split — a second `KanbanPro` application, its `LaneworkPro` scheme, a `KanbanProTests` bundle bound to it by `-module-alias`, and `scripts/verify-editions.sh` — was retired on 2026-07-30 in favour of one app with Lanework Pro as a subscription inside it. The `dev.rzen.indie.kanban-board` UTI and the three pasteboard types are exported once, by the one app that owns them.
|
||||
The iPhone app builds from the same project — `KanbanMobile` (iOS 26+, iPhone-only for now), sharing `Kanban/Storage` as source files:
|
||||
|
||||
```sh
|
||||
xcodebuild build -project Kanban.xcodeproj -scheme KanbanMobile -destination 'generic/platform=iOS Simulator'
|
||||
```
|
||||
|
||||
Simulator work without an iCloud account: launch with `LANEWORK_LOCAL_ROOT=<path>` in the environment (DEBUG builds only) and the app uses that folder in place of the ubiquity container.
|
||||
|
||||
**One *Mac* app target** (DESIGN/12-editions.md ▸ The target). `project.yml` declares the app `Kanban`, the unit bundle `KanbanTests` hosted by it, the UI bundle `KanbanUITests`, and the iPhone app `KanbanMobile` — a separate product, not a second Mac edition; one scheme per app. The 2026-07-27 two-target edition split — a second `KanbanPro` application, its `LaneworkPro` scheme, a `KanbanProTests` bundle bound to it by `-module-alias`, and `scripts/verify-editions.sh` — was retired on 2026-07-30 in favour of one app with Lanework Pro as a subscription inside it. The `dev.rzen.indie.kanban-board` UTI and the three pasteboard types are exported once, by the one app that owns them.
|
||||
|
||||
**StoreKit runs locally in development.** `Configuration.storekit` at the repo root declares the "Lanework Pro" subscription group and its monthly product (`dev.rzen.indie.kanban.pro.monthly`), and `project.yml` attaches it to the scheme's *run* action, so a ⌘R build shows a real price and completes a real purchase with no App Store Connect product and no sandbox account. It is a run-action setting only — builds, tests and `scripts/release.sh` are untouched, and the file is `buildPhase: none`, so it never reaches the shipped bundle. The same three strings live in App Store Connect (created by hand — RELEASE.md) and in `Kanban/Tier/ProProducts.swift`; a mismatch shows up in the app as "Can't reach the App Store right now."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user