Split git setup out of the popover into a board settings sheet

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 19:22:45 -04:00
parent 986347a95a
commit fecedab60d
6 changed files with 27 additions and 20 deletions
+5 -1
View File
@@ -55,7 +55,11 @@ The window-title widget opens the **board popover** — the one board-level surf
- **Board rename** (settled: this function stays in-app, unlike the pathfinder which dropped it with the inspector). Rename edits the board's frontmatter `title` only — the folder is never renamed by the app; the Finder document name is Finder's to change (01-storage-format.md's board-naming rule). A foreign rename landing while the popover is open resyncs the field from the snapshot only while the field is unfocused — a focused field keeps the user's keystrokes, the dirty-buffer courtesy applied here (settled). - **Board rename** (settled: this function stays in-app, unlike the pathfinder which dropped it with the inspector). Rename edits the board's frontmatter `title` only — the folder is never renamed by the app; the Finder document name is Finder's to change (01-storage-format.md's board-naming rule). A foreign rename landing while the popover is open resyncs the field from the snapshot only while the field is unfocused — a focused field keeps the user's keystrokes, the dirty-buffer courtesy applied here (settled).
- **Board styling** — the embedded style editor (Styling ▸ Controls above). - **Board styling** — the embedded style editor (Styling ▸ Controls above).
- **Git integration** — Pro tier surface, mode-aware (06-history-undo.md, 07-sync-collab.md; in the free tier this section is absent on ordinary boards and reduces to the contextual one-line Pro pointer on boards carrying an inert `.git` — 12-editions.md): on a mode-none board, the **add-git** action (opt-in init; on repo-nested boards replaced by the honest this-board-lives-inside-a-repository explanation — 06); on git boards, branch/source display, branch switching and creation, the commit-identity name/email fields (06), and **add/change remote** (a remote can be added or changed at any point — 07); for remote-backed boards additionally remote tracking (ahead/behind) with Pull/Push controls and the push-on-every-commit option. **Remote authentication surfaces inline here** (07 ▸ Remote authentication): credential fields on add/verify, the SSH key surface — the machine key with Copy + Verify, key import by paste or drag, the per-host key picker with removal of unreferenced imports, and confirm-gated machine-key regeneration — and the Authentication-needed badge state. - **Git at a glance** — Pro tier surface, mode-aware, *display and daily operations only* (re-ruled 2026-07-31 — setup moved to the board settings sheet below; in the free tier this section is absent on ordinary boards and reduces to the contextual one-line Pro pointer on boards carrying an inert `.git` — 12-editions.md): the posture lines (repo-nested explanation, unreadable-repo and paused states — 06), branch display with the **switch picker**, and on remote-backed boards remote tracking (ahead/behind) with **Pull/Push** controls and the status badges (Authentication needed, queued pushes, last error — the badge points at the sheet, capture happens there). A **Board Settings…** row opens the sheet — the popover's one setup affordance.
## Board settings sheet
**The setup home** (ruled 2026-07-31 — the popover/sheet split, 04-interactions.md's configuration carve-out): a board-scoped, titled, sectioned sheet on the board window, opened from the popover's Board Settings… row and from Board ▸ Board Settings… (11-command-nexus.md). It hosts everything setup-shaped: **add-git** (mode none; opt-in init — 06), **add/change remote** with the inline verify probe (07 ▸ Setup verifies right there), **credentials** — HTTPS username/token fields and the whole SSH surface (machine key Copy + Verify, key import by paste or **drag** — the sheet's stable frame is part of why it exists — the per-host key picker, unreferenced-import removal, confirm-gated machine-key regeneration), the TOFU first-connect confirm and mismatch block, **commit identity** name/email (06 — the visibility-scoped 2 s config re-read rides with the fields), **branch creation** (switching stays in the popover; create-and-switch runs 06's identical settle sequence from here), and **push-on-commit**. The mechanics that forced the split live comfortably here: confirmation alerts present over the sheet without dismissing the flow that owns them, network probes and their spinners survive focus changes, and typed-but-unverified credentials are never discarded by a stray click. Under the read-only lock the sheet's mutating controls disable in place (the Style-popover rule); every control is Tab-reachable and labeled (10-accessibility.md). Each control has exactly one home — the popover never duplicates a sheet control, the sheet never hosts the daily surface.
## Trash ## Trash
+1 -1
View File
@@ -54,7 +54,7 @@ Every command is a menu item. The full inventory — every command and action, i
- **⌥⌘↑/⌥⌘↓ sort within the lane** (the move-vs-jump question, resettled: *card* moves live on the ⌥⌘ chord, joining ⌥⌘←/⌥⌘→ lane width in a "⌥⌘ modifies" family; plain ⌥-arrows stay jumps; plain ⌘↑/⌘↓ are unassigned): the selected card(s) move one position within the lane — logical `order`, across interior masonry columns (10-accessibility.md's logical-order rule). A non-contiguous multi-selection **gathers on the first press**: the cards collect into a contiguous block anchored at the first selected card (first = lowest logical order; the rest follow in preserved relative order), and subsequent presses move the block one position. **Cards never change lanes by ⌘-arrow** (settled): inter-lane movement is drag or Cut/Paste (the clipboard rules above), so ⌥⌘↑/⌥⌘↓ disable when a card selection spans lanes and ⌘←/⌘→ are inert on card selections. With a **lane** selected, ⌘←/⌘→ move the lane one slot — closing 10-accessibility.md's lane-move defect — and ⌥⌘↑/⌥⌘↓ are inert. - **⌥⌘↑/⌥⌘↓ sort within the lane** (the move-vs-jump question, resettled: *card* moves live on the ⌥⌘ chord, joining ⌥⌘←/⌥⌘→ lane width in a "⌥⌘ modifies" family; plain ⌥-arrows stay jumps; plain ⌘↑/⌘↓ are unassigned): the selected card(s) move one position within the lane — logical `order`, across interior masonry columns (10-accessibility.md's logical-order rule). A non-contiguous multi-selection **gathers on the first press**: the cards collect into a contiguous block anchored at the first selected card (first = lowest logical order; the rest follow in preserved relative order), and subsequent presses move the block one position. **Cards never change lanes by ⌘-arrow** (settled): inter-lane movement is drag or Cut/Paste (the clipboard rules above), so ⌥⌘↑/⌥⌘↓ disable when a card selection spans lanes and ⌘←/⌘→ are inert on card selections. With a **lane** selected, ⌘←/⌘→ move the lane one slot — closing 10-accessibility.md's lane-move defect — and ⌥⌘↑/⌥⌘↓ are inert.
- **⌫/⌘⌫ delete** (resettled 2026-07-28; lanes rejoined 2026-07-29): on cards *and lanes*, a move into the trash (`.trash/`, top position — 03-board-ui.md; a lane travels subtree-intact, `kind: lane` stamped when absent, no dialog — recoverable now, so nothing needs confirming); on a **trash** selection the same chord deletes **permanently** (one Delete vocabulary, staged by place — confirmation per 03's recoverability rule, a lane's alert counting its cards, a mixed trash selection's alert counting both kinds). Selection moves to the deleted item's successor sibling, Finder-style (next card in the lane, next lane on the board; the last sibling's predecessor otherwise; empty container = nothing selected) — repeated ⌫ walks down a lane. **In the trash the successor walk is kind-blind** (ruled 2026-07-31): the next row of either kind, in the same all-rows order plain arrows walk — a successor is a fresh singleton selection, so the landing violates no grammar, and repeated ⌘⌫ empties a mixed trash without dead-ends, each delete confirm-gated per its kind. Deliberate deletes pick a successor; *external* vanishing never does (02-architecture.md's reload-survival rule: the selection just shrinks). **Put Back is retired with the tombstone model** (resettled 2026-07-28): File ▸ Delete is the chord's only owner — no twin menu items, no shared-equivalent routing; restore is drag-out or ⌘X/⌘V (The trash below). Plain ⌫ performs the same delete as fixed grammar (see Grammar above) — there is no Edit ▸ Delete item, so the two Delete-titled homes never collide for title-matched remapping. - **⌫/⌘⌫ delete** (resettled 2026-07-28; lanes rejoined 2026-07-29): on cards *and lanes*, a move into the trash (`.trash/`, top position — 03-board-ui.md; a lane travels subtree-intact, `kind: lane` stamped when absent, no dialog — recoverable now, so nothing needs confirming); on a **trash** selection the same chord deletes **permanently** (one Delete vocabulary, staged by place — confirmation per 03's recoverability rule, a lane's alert counting its cards, a mixed trash selection's alert counting both kinds). Selection moves to the deleted item's successor sibling, Finder-style (next card in the lane, next lane on the board; the last sibling's predecessor otherwise; empty container = nothing selected) — repeated ⌫ walks down a lane. **In the trash the successor walk is kind-blind** (ruled 2026-07-31): the next row of either kind, in the same all-rows order plain arrows walk — a successor is a fresh singleton selection, so the landing violates no grammar, and repeated ⌘⌫ empties a mixed trash without dead-ends, each delete confirm-gated per its kind. Deliberate deletes pick a successor; *external* vanishing never does (02-architecture.md's reload-survival rule: the selection just shrinks). **Put Back is retired with the tombstone model** (resettled 2026-07-28): File ▸ Delete is the chord's only owner — no twin menu items, no shared-equivalent routing; restore is drag-out or ⌘X/⌘V (The trash below). Plain ⌫ performs the same delete as fixed grammar (see Grammar above) — there is no Edit ▸ Delete item, so the two Delete-titled homes never collide for title-matched remapping.
- **Select All**: all visible cards on the board — filter-respecting, like every surface (Search below). **On the active trash side it selects the trash** (resettled 2026-07-28): with the trash visible and a non-empty trash selection, Select All selects all visible trash cards; in every other state, all visible live cards — the container boundary decides which "all" is meant (The trash below). - **Select All**: all visible cards on the board — filter-respecting, like every surface (Search below). **On the active trash side it selects the trash** (resettled 2026-07-28): with the trash visible and a non-empty trash selection, Select All selects all visible trash cards; in every other state, all visible live cards — the container boundary decides which "all" is meant (The trash below).
- **The contract's one carve-out is configuration** (settled): form-like git and board setup — add git, add/change remote, branch switching and creation, commit identity, credentials — lives in the board popover only, its committed home; its keyboard path is Board Info (⌘I) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). Recurring remote *operations* stay under the contract: Board ▸ Pull and Board ▸ Push are menu items (no default chord, remappable; validation enables them only on remote-backed boards — 07-sync-collab.md). - **The contract's one carve-out is configuration** (settled; containers re-ruled 2026-07-31 — the popover/sheet split): form-like configuration keeps exactly one home per control, split by weight across two committed homes. The **board popover** is the light surface — rename, styling, branch display and *switching*, ahead/behind with Pull/Push, the status badges; its keyboard path is Board Info (⌘I) plus Tab-reachable controls. **Setup lives in the board settings sheet** (03-board-ui.md ▸ Board settings sheet) — add git, add/change remote, credentials and the SSH surface, commit identity, branch *creation*, push-on-commit; its keyboard path is Board ▸ Board Settings… (11-command-nexus.md) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). The split's reasons are mechanical, not aesthetic: setup flows fire confirmation alerts, run inline network probes, and accept drag-in key import — acts that need a surface a stray click can't dismiss. Recurring remote *operations* stay under the contract: Board ▸ Pull and Board ▸ Push are menu items (no default chord, remappable; validation enables them only on remote-backed boards — 07-sync-collab.md).
- **⌘N target rule** (settled): with a card selected, the new card is created in that card's lane, immediately after it (paste-anchor consistency); with a lane selected, appended at its bottom (Return consistency); **a multi-selection anchors at its last member in flatten order** (settled — lane `order`, then card `order`, the multi-drag order; the same anchor serves paste): creation follows the last selected card, or appends to the last selected lane; the lane header's new-card button **overrides this rule** — the click names its target lane, selection notwithstanding (11-command-nexus.md ▸ Pointer grammar); with nothing selected — or a **trash** selection, which never anchors creation — the **last-active lane** — the lane that most recently held selection or a creation in this window session — falling back to the first lane. Title editor focused; same placeholder/abandon semantics as Return-creation. **Zero-lane board** (hand-made, or every lane deleted): card creation and card paste have no target — New Card, Return-creation, and Paste with a *card* payload disable via menu validation until a lane exists. New Lane (⇧⌘N) is one way in; Paste with a **lane** payload is the other — it stays enabled and lands at the board's right end (the lane-paste rule above), so cross-board structure transfer never needs a lane to exist first. - **⌘N target rule** (settled): with a card selected, the new card is created in that card's lane, immediately after it (paste-anchor consistency); with a lane selected, appended at its bottom (Return consistency); **a multi-selection anchors at its last member in flatten order** (settled — lane `order`, then card `order`, the multi-drag order; the same anchor serves paste): creation follows the last selected card, or appends to the last selected lane; the lane header's new-card button **overrides this rule** — the click names its target lane, selection notwithstanding (11-command-nexus.md ▸ Pointer grammar); with nothing selected — or a **trash** selection, which never anchors creation — the **last-active lane** — the lane that most recently held selection or a creation in this window session — falling back to the first lane. Title editor focused; same placeholder/abandon semantics as Return-creation. **Zero-lane board** (hand-made, or every lane deleted): card creation and card paste have no target — New Card, Return-creation, and Paste with a *card* payload disable via menu validation until a lane exists. New Lane (⇧⌘N) is one way in; Paste with a **lane** payload is the other — it stays enabled and lands at the board's right end (the lane-paste rule above), so cross-board structure transfer never needs a lane to exist first.
### The trash, keyboard-first (resettled 2026-07-28 — the materialized trash) ### The trash, keyboard-first (resettled 2026-07-28 — the materialized trash)
+8 -8
View File
@@ -2,13 +2,13 @@
**Tier scope: Lanework Pro** (12-editions.md). This doc is the git HistoryProvider; the free tier ships mode:none only, with macOS-native undo (13-native-undo.md) and the inert-`.git` posture (12). The Undo routing section below is tier-independent — both substrates dispatch through it. **Tier scope: Lanework Pro** (12-editions.md). This doc is the git HistoryProvider; the free tier ships mode:none only, with macOS-native undo (13-native-undo.md) and the inert-`.git` posture (12). The Undo routing section below is tier-independent — both substrates dispatch through it.
Git is the undo substrate — on boards that have git. **Git is opt-in per board (a pivot from the pathfinder, which auto-initialized every board): a board may be created without git, and git can be added later** (via the board popover; see 07-sync-collab.md's mode progression). A board without git binds the **native undo stack in every tier** (re-ruled 2026-07-31 — the provider follows the board, 13-native-undo.md; formerly no-undo under Pro, which made upgrading remove undo from mode-none boards); repo-nested boards remain the one no-undo case (text editors keep their standard typing undo everywhere; see Undo routing below). Deletes — card or lane — are recoverable on every board via the materialized trash (03-board-ui.md). Add-git swaps native → git mid-session, discarding the in-session native stack and seeding the git trail — the branch-switch discard-and-reseed precedent. On git-enabled boards, every settled change auto-commits; those mechanics are carried over from the pathfinder with their hard rules intact. Git is the undo substrate — on boards that have git. **Git is opt-in per board (a pivot from the pathfinder, which auto-initialized every board): a board may be created without git, and git can be added later** (via the board settings sheet — 03-board-ui.md; see 07-sync-collab.md's mode progression). A board without git binds the **native undo stack in every tier** (re-ruled 2026-07-31 — the provider follows the board, 13-native-undo.md; formerly no-undo under Pro, which made upgrading remove undo from mode-none boards); repo-nested boards remain the one no-undo case (text editors keep their standard typing undo everywhere; see Undo routing below). Deletes — card or lane — are recoverable on every board via the materialized trash (03-board-ui.md). Add-git swaps native → git mid-session, discarding the in-session native stack and seeding the git trail — the branch-switch discard-and-reseed precedent. On git-enabled boards, every settled change auto-commits; those mechanics are carried over from the pathfinder with their hard rules intact.
## Rules ## Rules
- **Opt-in init**: adding git to a board initializes a local repo at the board root. Bundled libgit2 — no git install required. No silent auto-init, ever. **The initial branch is `main`** (blessed 2026-07-31): the host's `init.defaultBranch` lives in config layers the sandbox can't read, so add-git sets it deterministically — git's modern default, the pathfinder's choice. - **Opt-in init**: adding git to a board initializes a local repo at the board root. Bundled libgit2 — no git install required. No silent auto-init, ever. **The initial branch is `main`** (blessed 2026-07-31): the host's `init.defaultBranch` lives in config layers the sandbox can't read, so add-git sets it deterministically — git's modern default, the pathfinder's choice.
- **Adoption**: a board whose root already contains `.git` opens **in git mode, silently** — adoption is not init. The no-silent-auto-init rule forbids *creating* a repository the user didn't ask for; recognizing one that exists is the opposite of that: the repo's presence *is* the opt-in (someone ran `git init` or `git clone`), and this is the primary way a second machine joins a shared board — clone in a terminal, open in the app (07-sync-collab.md's second entry arrow). All git-mode behavior applies from the first open: auto-commit, undo reseeded from the existing HEAD's first-parent ancestry, remote tracking if a remote is configured. - **Adoption**: a board whose root already contains `.git` opens **in git mode, silently** — adoption is not init. The no-silent-auto-init rule forbids *creating* a repository the user didn't ask for; recognizing one that exists is the opposite of that: the repo's presence *is* the opt-in (someone ran `git init` or `git clone`), and this is the primary way a second machine joins a shared board — clone in a terminal, open in the app (07-sync-collab.md's second entry arrow). All git-mode behavior applies from the first open: auto-commit, undo reseeded from the existing HEAD's first-parent ancestry, remote tracking if a remote is configured.
- **Detection is nearest-`.git`-wins**, checked at every board open: `.git` at the board root → git mode (adoption above); no `.git` at the root but one at any ancestor → repo-nested (below); neither → mode none. **Denial is not absence** (ruled 2026-07-31): the ancestor walk crosses paths above the board's sandbox grant, and a check the sandbox *refuses* (EACCES/EPERM) must never read as "no repo there" — detection distinguishes **clean none** (every ancestor answered not-found) from **unverifiable** (a check was denied); add-git is offered only on clean none, and unverifiable takes the repo-nested posture (conservative — the popover explains rather than offers). As hardening, add-git's create re-runs full detection and refuses unless it reads clean none, so the forbidden nested init is impossible even on a raced or stale read. Whether the shipped sandbox actually denies ancestor stats is an open empirical question (manual-verification list: a board deep inside an ungranted repo, and an ordinary board under an ungranted parent) — if it denies everywhere, every board would read unverifiable and this posture needs a data-informed revisit. A board can therefore change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects what it finds. **Open-time only, deliberately — for *discovery***: a `git init` under an open mode-none board takes effect at the next open — the running session keeps its mode, and the watcher does not scan for `.git` appearing (no mid-session mode flips from watching; stated here so it isn't rediscovered as a bug). The one deliberate mid-session transition is the app's own **add-git** (Opt-in init above): clicking it flips the open board into git mode immediately — the popover flows straight into the git controls, the first auto-commit follows — the rule forbids *discovered* flips, never commanded ones. - **Detection is nearest-`.git`-wins**, checked at every board open: `.git` at the board root → git mode (adoption above); no `.git` at the root but one at any ancestor → repo-nested (below); neither → mode none. **Denial is not absence** (ruled 2026-07-31): the ancestor walk crosses paths above the board's sandbox grant, and a check the sandbox *refuses* (EACCES/EPERM) must never read as "no repo there" — detection distinguishes **clean none** (every ancestor answered not-found) from **unverifiable** (a check was denied); add-git is offered only on clean none, and unverifiable takes the repo-nested posture (conservative — the popover explains rather than offers). As hardening, add-git's create re-runs full detection and refuses unless it reads clean none, so the forbidden nested init is impossible even on a raced or stale read. Whether the shipped sandbox actually denies ancestor stats is an open empirical question (manual-verification list: a board deep inside an ungranted repo, and an ordinary board under an ungranted parent) — if it denies everywhere, every board would read unverifiable and this posture needs a data-informed revisit. A board can therefore change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects what it finds. **Open-time only, deliberately — for *discovery***: a `git init` under an open mode-none board takes effect at the next open — the running session keeps its mode, and the watcher does not scan for `.git` appearing (no mid-session mode flips from watching; stated here so it isn't rediscovered as a bug). The one deliberate mid-session transition is the app's own **add-git** (Opt-in init above): clicking it flips the open board into git mode immediately — the settings sheet flows straight into the git sections, the first auto-commit follows — the rule forbids *discovered* flips, never commanded ones.
- **A `.git` that isn't a valid repository still reads as git mode — and fails loudly** (ruled 2026-07-31): detection is presence-shaped (any root `.git` entry, directory or worktree/submodule pointer file), so a corrupt or unopenable repo never falls to mode none — Add Git is never offered against an existing `.git`, whatever its condition (init into a repairable repo is exactly the never-mutate hazard). The board itself loads and edits normally — files are the board — but the failure is **loud**: a standing breakage-class banner at detection ("This board's git repository can't be read — history is paused; Lanework leaves the repository untouched"), announced per 10-accessibility.md, with the whole git surface paused (the abnormal-states posture below) and the popover's git section naming the state; the banner clears when a later open or reload finds the repo readable. Never a silent placeholder discovered only in the popover. - **A `.git` that isn't a valid repository still reads as git mode — and fails loudly** (ruled 2026-07-31): detection is presence-shaped (any root `.git` entry, directory or worktree/submodule pointer file), so a corrupt or unopenable repo never falls to mode none — Add Git is never offered against an existing `.git`, whatever its condition (init into a repairable repo is exactly the never-mutate hazard). The board itself loads and edits normally — files are the board — but the failure is **loud**: a standing breakage-class banner at detection ("This board's git repository can't be read — history is paused; Lanework leaves the repository untouched"), announced per 10-accessibility.md, with the whole git surface paused (the abnormal-states posture below) and the popover's git section naming the state; the banner clears when a later open or reload finds the repo readable. Never a silent placeholder discovered only in the popover.
- **Abnormal repo states** (settled; adoption never assumes a tidy clone): an **unborn HEAD** (`git init`, no commits yet) is normal git mode — the first auto-commit creates the root commit on the branch HEAD names, and the undo trail simply starts empty. **The root commit has its own subject** (settled): whenever the app creates a repo's first commit — immediately on the app's own add-git (init doesn't wait for the debounce; the board is protected from the moment git exists), or at the first settled change on an adopted unborn repo — it commits the whole tree as **"Initial board state"**, never a folded diff-from-empty: there is no last-committed snapshot to diff against, and forty Adds would bury the event. **The root commit is never split and is user-authored** (blessed 2026-07-31): it is a baseline, not a change-set — the event it records is the user's act of putting the board under git, adopted unborn repos' unwitnessed files included. A **detached HEAD**, or an **in-progress merge/rebase/cherry-pick** left by outside-the-app git (`MERGE_HEAD`, `rebase-merge`/`rebase-apply`, `CHERRY_PICK_HEAD` — pause states that load fine on a clean tree and are otherwise invisible), instead **pauses the git surface honestly — the *whole* surface, remote half included** (settled): auto-commit holds (the auth-pause posture, 07-sync-collab.md — pause, badge, explain, never hammer), Undo/Redo and the branch controls disable, **and Pull, Push, and push-on-commit hold with them** — with auto-commit held, 07's clean-tree-by-pull-time invariant is false, and a pull's rebase (or a rejected push's fetch→rebase→push) would run against a dirty tree carrying uncommitted edits, precisely what flush-before-overwrite exists to prevent; the ahead/behind badge keeps counting (a fetch is a read), and the popover's git section names the state plainly ("HEAD is detached — commits would belong to no branch"; "a merge is in progress") and says resolving it belongs to the tool that created it. Edits keep landing on disk — files are the board — and commit as one settled batch when the state clears. The app **never mutates repo state it didn't create** (no auto branch-at-HEAD, no `merge --abort`); the check runs at open and again before every flush — and, because a terminal's cleanup moves only files under `.git`, which the watcher never delivers, **a standing pause re-reads the repository state every 15 s** (blessed 2026-07-31; injectable cadence, only while paused, a handful of stats — not a retry, nothing is attempted) — so finishing the operation in a terminal resumes the pipeline without ceremony. **The one exemption is the app's own leftovers** (settled): every bracketed operation stamps its intent app-side (per-board registry) before touching the repo, so an interrupted app-run rebase or checkout is recognizable as Lanework's — finding a pause state with a matching stamp, the app **aborts its own unfinished operation** to restore the pre-operation state and says so via banner ("a branch switch was interrupted — the previous state is restored"), then clears the stamp — **on success only; a failed abort keeps the stamp** (blessed 2026-07-31): the stamp is the sole evidence the leftover is the app's, and clearing it on failure would demote the leftover to somebody-else's forever — the pause would then send the user to "the tool that created it," which was this app. Kept, the next open or paused-state re-read recognizes it and retries; recovery converges instead of orphaning. Abort discards nothing: fetched commits stay fetched, local commits are restored — the rebase's own no-loss accounting. Without a matching stamp the leftover is outside git's, and the pause-and-defer stance above holds unchanged. - **Abnormal repo states** (settled; adoption never assumes a tidy clone): an **unborn HEAD** (`git init`, no commits yet) is normal git mode — the first auto-commit creates the root commit on the branch HEAD names, and the undo trail simply starts empty. **The root commit has its own subject** (settled): whenever the app creates a repo's first commit — immediately on the app's own add-git (init doesn't wait for the debounce; the board is protected from the moment git exists), or at the first settled change on an adopted unborn repo — it commits the whole tree as **"Initial board state"**, never a folded diff-from-empty: there is no last-committed snapshot to diff against, and forty Adds would bury the event. **The root commit is never split and is user-authored** (blessed 2026-07-31): it is a baseline, not a change-set — the event it records is the user's act of putting the board under git, adopted unborn repos' unwitnessed files included. A **detached HEAD**, or an **in-progress merge/rebase/cherry-pick** left by outside-the-app git (`MERGE_HEAD`, `rebase-merge`/`rebase-apply`, `CHERRY_PICK_HEAD` — pause states that load fine on a clean tree and are otherwise invisible), instead **pauses the git surface honestly — the *whole* surface, remote half included** (settled): auto-commit holds (the auth-pause posture, 07-sync-collab.md — pause, badge, explain, never hammer), Undo/Redo and the branch controls disable, **and Pull, Push, and push-on-commit hold with them** — with auto-commit held, 07's clean-tree-by-pull-time invariant is false, and a pull's rebase (or a rejected push's fetch→rebase→push) would run against a dirty tree carrying uncommitted edits, precisely what flush-before-overwrite exists to prevent; the ahead/behind badge keeps counting (a fetch is a read), and the popover's git section names the state plainly ("HEAD is detached — commits would belong to no branch"; "a merge is in progress") and says resolving it belongs to the tool that created it. Edits keep landing on disk — files are the board — and commit as one settled batch when the state clears. The app **never mutates repo state it didn't create** (no auto branch-at-HEAD, no `merge --abort`); the check runs at open and again before every flush — and, because a terminal's cleanup moves only files under `.git`, which the watcher never delivers, **a standing pause re-reads the repository state every 15 s** (blessed 2026-07-31; injectable cadence, only while paused, a handful of stats — not a retry, nothing is attempted) — so finishing the operation in a terminal resumes the pipeline without ceremony. **The one exemption is the app's own leftovers** (settled): every bracketed operation stamps its intent app-side (per-board registry) before touching the repo, so an interrupted app-run rebase or checkout is recognizable as Lanework's — finding a pause state with a matching stamp, the app **aborts its own unfinished operation** to restore the pre-operation state and says so via banner ("a branch switch was interrupted — the previous state is restored"), then clears the stamp — **on success only; a failed abort keeps the stamp** (blessed 2026-07-31): the stamp is the sole evidence the leftover is the app's, and clearing it on failure would demote the leftover to somebody-else's forever — the pause would then send the user to "the tool that created it," which was this app. Kept, the next open or paused-state re-read recognizes it and retries; recovery converges instead of orphaning. Abort discards nothing: fetched commits stay fetched, local commits are restored — the rebase's own no-loss accounting. Without a matching stamp the leftover is outside git's, and the pause-and-defer stance above holds unchanged.
- **Boards nested inside an existing repository are left strictly alone** — git cannot be added to them (no nested repo, no commits into the user's repo), so they get **no undo** (settled; no app-managed undo journal, which would violate self-containment). The board popover's git section must say so honestly: not a hidden "add git" but a short explanation ("this board lives inside a repository; Lanework leaves it to that repository") — the option is absent because it *can't* apply, and the UI should teach that rather than look broken. - **Boards nested inside an existing repository are left strictly alone** — git cannot be added to them (no nested repo, no commits into the user's repo), so they get **no undo** (settled; no app-managed undo journal, which would violate self-containment). The board popover's git section must say so honestly: not a hidden "add git" but a short explanation ("this board lives inside a repository; Lanework leaves it to that repository") — the option is absent because it *can't* apply, and the UI should teach that rather than look broken.
@@ -21,11 +21,11 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board
- **Undo survives relaunch**: the undo stack reseeds from HEAD's first-parent ancestry on load; redo starts empty. In-session it behaves as classic dual stacks; after relaunch, past restore commits reappear as ordinary undoable steps. Deliberate: no sidecar state, nothing ever lost. Interaction with pull (07-sync-collab.md): a pull rebases unpushed local commits, so the in-session stack must remap onto the rewritten commits — the pre-rebase hashes are orphaned. A pleasant consequence of the reseed rule: the fetched remote commits sit in HEAD's first-parent ancestry, so after the next relaunch remote work becomes ordinary undoable steps too. - **Undo survives relaunch**: the undo stack reseeds from HEAD's first-parent ancestry on load; redo starts empty. In-session it behaves as classic dual stacks; after relaunch, past restore commits reappear as ordinary undoable steps. Deliberate: no sidecar state, nothing ever lost. Interaction with pull (07-sync-collab.md): a pull rebases unpushed local commits, so the in-session stack must remap onto the rewritten commits — the pre-rebase hashes are orphaned. A pleasant consequence of the reseed rule: the fetched remote commits sit in HEAD's first-parent ancestry, so after the next relaunch remote work becomes ordinary undoable steps too.
- **Heal commits are transparent to undo, in-session** (ruled 2026-07-29): heal-class commits — their paths known by the Writer's heal-marked receipts (Commit messages below) — never become undo steps: the stack pointer passes over them, and a restore materializing an older target **excludes paths whose divergence is heal work**, so a ⌘Z run never reverts a repair and never summons the scheduler (reverting one would re-arm the memo on the recreated defect signature, land a fresh heal commit, and — redo cleared — trap the run on an ever-renewing top; transparency dissolves the trap instead of suppressing the healer). The in-session qualifier is honest: receipts live in memory and the reseed is deliberately sidecar-free, so after relaunch old heal commits reappear as ordinary steps — undoing one recreates its defect and the scheduler re-heals within a reload, restore commit plus fresh heal commit, notice included. That **residual bounce is accepted family-wide** — the agent-guide quirk (Agent collaboration below) generalized to the relocation, the legacy migration, the displacement, and the remint — and it is **self-limiting to one bounce**: the fresh heal commit is in-session, transparent, and the undo run continues past it. Redo is symmetric. - **Heal commits are transparent to undo, in-session** (ruled 2026-07-29): heal-class commits — their paths known by the Writer's heal-marked receipts (Commit messages below) — never become undo steps: the stack pointer passes over them, and a restore materializing an older target **excludes paths whose divergence is heal work**, so a ⌘Z run never reverts a repair and never summons the scheduler (reverting one would re-arm the memo on the recreated defect signature, land a fresh heal commit, and — redo cleared — trap the run on an ever-renewing top; transparency dissolves the trap instead of suppressing the healer). The in-session qualifier is honest: receipts live in memory and the reseed is deliberately sidecar-free, so after relaunch old heal commits reappear as ordinary steps — undoing one recreates its defect and the scheduler re-heals within a reload, restore commit plus fresh heal commit, notice included. That **residual bounce is accepted family-wide** — the agent-guide quirk (Agent collaboration below) generalized to the relocation, the legacy migration, the displacement, and the remint — and it is **self-limiting to one bounce**: the fresh heal commit is in-session, transparent, and the undo run continues past it. Redo is symmetric.
- **Undo is board-local.** A cross-board move-out undone at the source resurrects the card even though it lives on in the destination — per-board histories cannot and must not mutate other boards. The resulting same-UUID fork across boards is legitimate (boards are independent identity namespaces); if the two ever meet through a move-in, the import boundary remints the arrival (01-storage-format.md's identity lifecycle). - **Undo is board-local.** A cross-board move-out undone at the source resurrects the card even though it lives on in the destination — per-board histories cannot and must not mutate other boards. The resulting same-UUID fork across boards is legitimate (boards are independent identity namespaces); if the two ever meet through a move-in, the import boundary remints the arrival (01-storage-format.md's identity lifecycle).
- The git surface lives in the **board popover** (03-board-ui.md): branch/source display, branch switching and creation, and the commit-identity name/email fields (see Interaction with external writers below) — alongside board rename and styling. - The git surface splits across the **board popover** and the **board settings sheet** (03-board-ui.md; the 2026-07-31 popover/sheet split): the popover carries the daily face — branch/source display, the switch picker, posture lines — alongside board rename and styling; the sheet carries setup — add-git, branch creation, the commit-identity name/email fields (see Interaction with external writers below), remote and credentials (07).
## Undo routing ## Undo routing
**Routing is by focus** — the platform's first-responder rule, its own section because two undo systems coexist and four docs cite the rule. While a text-editing surface is focused (card title field, body Edit mode, raw source, board inline rename), ⌘Z/⇧⌘Z are that editor's own **text undo** — standard, transient, session-scoped: leaving the editor (mode flip, focus loss, close) ends the session, and from then on that content's undo story is the git trail. Text undo works on **every** board — no-git and repo-nested included; "no undo/redo" above means board history, not typing. **Control-class text fields route the same way** (settled): the search field (04-interactions.md ▸ Search) and the popover's text fields (board rename, commit identity, credentials) own ⌘Z/⇧⌘Z as field-local text undo while focused — "board menu commands stay enabled" never hands Edit ▸ Undo to git while a text-bearing control has focus; a reflexive undo over a typo must never become a tree checkout. With focus outside every text-bearing surface — editor or control — Edit ▸ Undo/Redo are, **in a card window, that window's own session stack** (13-native-undo.md's two-level model, re-ruled 2026-07-31 — fine-grained window gestures, both tiers; the coarse close unit is the tier-split: one native board step, or one commit), and on board surfaces board history — git undo here (disabled on boards without it). **No fall-through**: exhausting a focused editor's — or the window's — stack beeps; it never reaches board history. **Routing is by focus** — the platform's first-responder rule, its own section because two undo systems coexist and four docs cite the rule. While a text-editing surface is focused (card title field, body Edit mode, raw source, board inline rename), ⌘Z/⇧⌘Z are that editor's own **text undo** — standard, transient, session-scoped: leaving the editor (mode flip, focus loss, close) ends the session, and from then on that content's undo story is the git trail. Text undo works on **every** board — no-git and repo-nested included; "no undo/redo" above means board history, not typing. **Control-class text fields route the same way** (settled): the search field (04-interactions.md ▸ Search), the popover's rename field, and the settings sheet's text fields (commit identity, credentials, remote URL) own ⌘Z/⇧⌘Z as field-local text undo while focused — "board menu commands stay enabled" never hands Edit ▸ Undo to git while a text-bearing control has focus; a reflexive undo over a typo must never become a tree checkout. With focus outside every text-bearing surface — editor or control — Edit ▸ Undo/Redo are, **in a card window, that window's own session stack** (13-native-undo.md's two-level model, re-ruled 2026-07-31 — fine-grained window gestures, both tiers; the coarse close unit is the tier-split: one native board step, or one commit), and on board surfaces board history — git undo here (disabled on boards without it). **No fall-through**: exhausting a focused editor's — or the window's — stack beeps; it never reaches board history.
## Commit messages ## Commit messages
@@ -46,9 +46,9 @@ The pathfinder's message engine carries over as the model — it is what earns t
## Branch switching ## Branch switching
Switching (or creating-and-switching) a branch from the board popover**create-and-switch runs the identical settle → flush → stamp → switch sequence, no at-HEAD fast path** (blessed 2026-07-31): "creating at HEAD can't change the tree" fails as a proof under concurrent writers (a foreign commit can land between the check and the create), and flushing pending work onto the branch it was made on is the honest cadence regardless — the occasional save-or-discard beat on a create buys one sequence with no special case: Switching a branch from the popover's picker (or creating-and-switching from the settings sheet — the 2026-07-31 split)**create-and-switch runs the identical settle → flush → stamp → switch sequence, no at-HEAD fast path** (blessed 2026-07-31): "creating at HEAD can't change the tree" fails as a proof under concurrent writers (a foreign commit can land between the check and the create), and flushing pending work onto the branch it was made on is the honest cadence regardless — the occasional save-or-discard beat on a create buys one sequence with no special case:
- **Settle the editors first — explicitly, never silently.** Branch switch neither silently commits nor silently abandons an open card-body Edit session: if any open card window has one (unsaved keystrokes, or on-disk ~700 ms saves the session hasn't committed — the mid-session state Auto-commit above deliberately leaves uncommitted), the switch presents a **save-or-discard step**: **Save All** ends every session with its normal commit (each card's Edit→Preview flip), **Discard** reverts buffers and uncommitted saves to HEAD, **Cancel** keeps the current branch and the sessions. **Open raw-source buffers are settled by the same step** (settled — an unsettled raw buffer is the worse hazard: its Apply later writes the *entire* pre-switch `index.md` byte-for-byte onto the new branch's card): Save All *applies* each raw buffer — and since Apply validates, a buffer that fails validation cancels the whole switch with focus on the offending window, nothing half-switched; Discard exits raw source without writing; Cancel keeps everything. External checkouts the app can't gate are the accepted last-writer-wins case, same as the Edit buffer (05-card-window.md's dirty-buffer rule; on git boards the overwritten version is a commit, one revert away). Silently flushing the commit alone would be wrong twice over: the tree can be clean precisely because a save hasn't landed, and a later debounced save would write old-branch text onto the new branch's card. With sessions settled, the pending auto-commit flushes (flush-before-overwrite above) and checkout runs on a truly settled tree: it cannot fail dirty, and no in-flight work is lost or dragged across branches. **The flush is Save-All-shaped** (blessed 2026-07-31): after Discard, the session's reverted bytes are *reconciled, never flushed* — the pending window for that folder drops, since disk again agrees with HEAD and committing the discarded saves would betray the button; pending changes elsewhere on the board still flush normally. (Inline title editors need no step of their own: reaching the popover's branch controls commits them — click-away commits, and board-scoped commands disable while one is focused — 04-interactions.md ▸ Grammar.) - **Settle the editors first — explicitly, never silently.** Branch switch neither silently commits nor silently abandons an open card-body Edit session: if any open card window has one (unsaved keystrokes, or on-disk ~700 ms saves the session hasn't committed — the mid-session state Auto-commit above deliberately leaves uncommitted), the switch presents a **save-or-discard step**: **Save All** ends every session with its normal commit (each card's Edit→Preview flip), **Discard** reverts buffers and uncommitted saves to HEAD, **Cancel** keeps the current branch and the sessions. **Open raw-source buffers are settled by the same step** (settled — an unsettled raw buffer is the worse hazard: its Apply later writes the *entire* pre-switch `index.md` byte-for-byte onto the new branch's card): Save All *applies* each raw buffer — and since Apply validates, a buffer that fails validation cancels the whole switch with focus on the offending window, nothing half-switched; Discard exits raw source without writing; Cancel keeps everything. External checkouts the app can't gate are the accepted last-writer-wins case, same as the Edit buffer (05-card-window.md's dirty-buffer rule; on git boards the overwritten version is a commit, one revert away). Silently flushing the commit alone would be wrong twice over: the tree can be clean precisely because a save hasn't landed, and a later debounced save would write old-branch text onto the new branch's card. With sessions settled, the pending auto-commit flushes (flush-before-overwrite above) and checkout runs on a truly settled tree: it cannot fail dirty, and no in-flight work is lost or dragged across branches. **The flush is Save-All-shaped** (blessed 2026-07-31): after Discard, the session's reverted bytes are *reconciled, never flushed* — the pending window for that folder drops, since disk again agrees with HEAD and committing the discarded saves would betray the button; pending changes elsewhere on the board still flush normally. (Inline title editors need no step of their own: reaching the popover's picker or the sheet's create control commits them — click-away commits, and board-scoped commands disable while one is focused — 04-interactions.md ▸ Grammar.)
- **The undo/redo stack does not survive a switch.** It is discarded and reseeded from the new HEAD's first-parent ancestry — the relaunch rule applied at switch time; redo starts empty. (Replaying a restore commit from the previous branch onto the new one would be wrong.) - **The undo/redo stack does not survive a switch.** It is discarded and reseeded from the new HEAD's first-parent ancestry — the relaunch rule applied at switch time; redo starts empty. (Replaying a restore commit from the previous branch onto the new one would be wrong.)
- **Everything remote-facing tracks the current branch**: ahead/behind, Pull/Push, and push-on-commit all operate against the current branch's upstream. On a branch with no upstream yet, the first push — manual or push-on-commit — **creates it on the remote quietly** (`push -u` semantics): creating a remote branch is non-destructive, and quiet is consistent with push-failures-never-nag (07-sync-collab.md). Genuine failures queue with the badge as usual. - **Everything remote-facing tracks the current branch**: ahead/behind, Pull/Push, and push-on-commit all operate against the current branch's upstream. On a branch with no upstream yet, the first push — manual or push-on-commit — **creates it on the remote quietly** (`push -u` semantics): creating a remote branch is non-destructive, and quiet is consistent with push-failures-never-nag (07-sync-collab.md). Genuine failures queue with the badge as usual.
- The switch itself is bracketed (02-architecture.md): watcher suspended, one full reload at the end. If that final reload fails, the board locks read-only until a successful reload — see 02's live-reload resilience; the on-screen snapshot is from the previous branch and must not be edited over the new one. - The switch itself is bracketed (02-architecture.md): watcher suspended, one full reload at the end. If that final reload fails, the board locks read-only until a successful reload — see 02's live-reload resilience; the on-screen snapshot is from the previous branch and must not be edited over the new one.
@@ -59,13 +59,13 @@ Agent and hand edits arrive through the watcher like any change and get auto-com
**Commit attribution is structural, not just a message convention.** The Writer/echo machinery (the **EchoLedger** — 02-architecture.md ▸ Components, where its matching rule and race cases are settled) lets the auto-committer classify every observed change, per file, as **app-mediated** (the user acting through the app) or **foreign** (anything else). User-driven commits carry the user's git identity; foreign changes are committed under the pinned synthetic author **`Lanework External <[email protected]>`** — so any git client can filter, log, and blame by origin. **The committer field is always the user's identity** (blessed 2026-07-31 — git's own `am`/cherry-pick convention: author = whose change, committer = who recorded it): every commit the app makes, foreign-authored included, records the user's app as its committer. The strings are API (users script against them; the `.invalid` TLD honestly marks a non-routable synthetic identity) — they change with the deliberateness of a schema change. **Commit attribution is structural, not just a message convention.** The Writer/echo machinery (the **EchoLedger** — 02-architecture.md ▸ Components, where its matching rule and race cases are settled) lets the auto-committer classify every observed change, per file, as **app-mediated** (the user acting through the app) or **foreign** (anything else). User-driven commits carry the user's git identity; foreign changes are committed under the pinned synthetic author **`Lanework External <[email protected]>`** — so any git client can filter, log, and blame by origin. **The committer field is always the user's identity** (blessed 2026-07-31 — git's own `am`/cherry-pick convention: author = whose change, committer = who recorded it): every commit the app makes, foreign-authored included, records the user's app as its committer. The strings are API (users script against them; the `.invalid` TLD honestly marks a non-routable synthetic identity) — they change with the deliberateness of a schema change.
**Where the user's git identity comes from** (no git install is assumed, and the sandbox doesn't read `~/.gitconfig` — honest limits, not bugs): **repo-local `.git/config` wins when present** — standard git semantics, readable in-sandbox because it lives under the board root, and the natural state of adopted/cloned boards. The board popover's git section exposes name/email fields that **write that repo-local config** — the setting *is* the file, portable to any git client, per-board by nature (work and personal boards can differ). **The fields re-read the config at 2 s while the popover is visible** (blessed 2026-07-31): the watcher never delivers `.git`, so no board event can carry a terminal-side config edit — the unfocused-resync courtesy needs its own signal, and a visibility-scoped poll is the 15 s paused-state re-read's shape at popover cadence (a focused field keeps its keystrokes; closing the popover stops the poll). **Writes append, reads take the last** (blessed 2026-07-31): the writer appends a plain `[user]` section and never edits existing sections or `[user "…"]` subsections (their semantics are tool-specific); the reader — like git itself — takes the last plain-section value, which is exactly what an append produces. The asymmetry lets the write always win without the writer ever reformatting what it didn't create — the frontmatter engine's never-reformat instinct applied to git config; the worst case is a slightly redundant file git reads correctly. Absent repo config, the **derived default** applies: the macOS account's full name plus `shortname@hostname` — git's own no-config fallback shape, zero ceremony. Commits pushed to a forge under the derived email won't link to a forge account; the popover fields are the fix when that matters. **The derived default is passed as an explicit per-commit signature, never written into repo config** (ruled 2026-07-31 — the signature-capable commit path gates the pro-m1 ship): repo config is the record of the user's popover edits and of adopted repos' own state, and an app-written identity there would outrank the user's global `~/.gitconfig` for their *own terminal commits* in that board. The build-time interim that materializes identity into a fresh repo's `.git/config` (SwiftGitX 0.4.0's signatureless commit + the sandbox's unreadable global config) is tolerated in-tree during pro-m1 construction and must die before release — the attribution rules above (per-commit author variation) require explicit signatures anyway. A debounce window containing both kinds is **split into two commits**, never mixed (flush-before-overwrite already orders them: foreign first, then the user's overwrite). Honest limit: the app distinguishes app-mediated from foreign, not human from agent — a hand edit in a text editor and an agent write look identical *unless the writer says otherwise via `modified-by` (below)*. Agents wanting precise attribution are encouraged (via the agent guide, 08-agent-integration.md) to commit their own changes; the app follows along. **Where the user's git identity comes from** (no git install is assumed, and the sandbox doesn't read `~/.gitconfig` — honest limits, not bugs): **repo-local `.git/config` wins when present** — standard git semantics, readable in-sandbox because it lives under the board root, and the natural state of adopted/cloned boards. The board settings sheet's identity section (03-board-ui.md — the 2026-07-31 split moved the fields out of the popover) exposes name/email fields that **write that repo-local config** — the setting *is* the file, portable to any git client, per-board by nature (work and personal boards can differ). **The fields re-read the config at 2 s while the sheet is visible** (blessed 2026-07-31): the watcher never delivers `.git`, so no board event can carry a terminal-side config edit — the unfocused-resync courtesy needs its own signal, and a visibility-scoped poll is the 15 s paused-state re-read's shape at sheet cadence (a focused field keeps its keystrokes; dismissing the sheet stops the poll). **Writes append, reads take the last** (blessed 2026-07-31): the writer appends a plain `[user]` section and never edits existing sections or `[user "…"]` subsections (their semantics are tool-specific); the reader — like git itself — takes the last plain-section value, which is exactly what an append produces. The asymmetry lets the write always win without the writer ever reformatting what it didn't create — the frontmatter engine's never-reformat instinct applied to git config; the worst case is a slightly redundant file git reads correctly. Absent repo config, the **derived default** applies: the macOS account's full name plus `shortname@hostname` — git's own no-config fallback shape, zero ceremony. Commits pushed to a forge under the derived email won't link to a forge account; the sheet's identity fields are the fix when that matters. **The derived default is passed as an explicit per-commit signature, never written into repo config** (ruled 2026-07-31 — the signature-capable commit path gates the pro-m1 ship): repo config is the record of the user's popover edits and of adopted repos' own state, and an app-written identity there would outrank the user's global `~/.gitconfig` for their *own terminal commits* in that board. The build-time interim that materializes identity into a fresh repo's `.git/config` (SwiftGitX 0.4.0's signatureless commit + the sandbox's unreadable global config) is tolerated in-tree during pro-m1 construction and must die before release — the attribution rules above (per-commit author variation) require explicit signatures anyway. A debounce window containing both kinds is **split into two commits**, never mixed (flush-before-overwrite already orders them: foreign first, then the user's overwrite). Honest limit: the app distinguishes app-mediated from foreign, not human from agent — a hand edit in a text editor and an agent write look identical *unless the writer says otherwise via `modified-by` (below)*. Agents wanting precise attribution are encouraged (via the agent guide, 08-agent-integration.md) to commit their own changes; the app follows along.
**`modified-by` refines foreign attribution** (the self-reported provenance key — 01-storage-format.md): when every file changed in a foreign debounce window carries the same `modified-by: X`, that commit is authored as **X** with the synthetic email `<slug>@agents.lanework.invalid` (display name verbatim, email local part slugified; the domain marks self-reported identity, distinct from both the user and the generic external author). Any disagreement between stamps, any unstamped changed file, or any true deletion in the window falls back to `Lanework External` — a deletion leaves no file to stamp. **A folder move is not a deletion**: items match by id across the whole board (Commit messages above — the same matching that reads a move as a move, not delete+add), so a moved card attributes by its stamp like any changed file. But a bare `mv` rewrites nothing — the moved `index.md` still carries whatever the app last wrote (no stamp) and demotes the window under the unstamped-file rule — so the agent guide teaches re-stamping on move (08-agent-integration.md). Same trust level as self-committing — it's what the writer claims, accepted as such; the stale-stamp hand-edit case (01) is the known misattribution edge. Self-committing remains the precise path; the stamp is the lightweight middle. **`modified-by` refines foreign attribution** (the self-reported provenance key — 01-storage-format.md): when every file changed in a foreign debounce window carries the same `modified-by: X`, that commit is authored as **X** with the synthetic email `<slug>@agents.lanework.invalid` (display name verbatim, email local part slugified; the domain marks self-reported identity, distinct from both the user and the generic external author). Any disagreement between stamps, any unstamped changed file, or any true deletion in the window falls back to `Lanework External` — a deletion leaves no file to stamp. **A folder move is not a deletion**: items match by id across the whole board (Commit messages above — the same matching that reads a move as a move, not delete+add), so a moved card attributes by its stamp like any changed file. But a bare `mv` rewrites nothing — the moved `index.md` still carries whatever the app last wrote (no stamp) and demotes the window under the unstamped-file rule — so the agent guide teaches re-stamping on move (08-agent-integration.md). Same trust level as self-committing — it's what the writer claims, accepted as such; the stale-stamp hand-edit case (01) is the known misattribution edge. Self-committing remains the precise path; the stamp is the lightweight middle.
**Two writers, one repository — the designed situation, not an edge case.** Self-committing agents mean the auto-committer shares the repo with concurrent `git` processes, and it must be graceful about it: **Two writers, one repository — the designed situation, not an edge case.** Self-committing agents mean the auto-committer shares the repo with concurrent `git` processes, and it must be graceful about it:
- **`index.lock` contention is never an error.** If the auto-committer finds the index locked (an agent's commit in flight), it backs off briefly and retries; if the lock persists, it simply re-debounces — the pending changes are still pending, and the next quiet moment commits them. No banner, no log-worthy failure: a held lock is another writer doing its job. (Genuine commit failures — disk full, repo corruption — are different: files stay safe on disk but history stops advancing; surfaced per 02-architecture.md ▸ Write-failure surfacing, retried on the next debounce.) **The same posture covers every app-initiated operation** (settled): pull, push, branch switch, and undo restore meeting a held lock wait and retry briefly, silently; contention outlasting the brief retry surfaces as a *waiting* state in the operation's in-progress banner row ("waiting for another writer's git lock"), retrying on its cadence — never an error dialog, never a hammer — and a wait that persists implausibly long names the lock path (a crashed writer's leftover is the user's to clear; the never-mutate rule's one exemption is the app's own leftovers, Abnormal repo states above). **The wait is bounded — 30 s, then a clean failure** (blessed 2026-07-31; injectable): a genuine writer finishes in seconds, so only a stale lock ever reaches the bound, and an eternal spinner holding the wholesale bracket — and with it the board — is worse than a failure that names the path to delete. The bound expiring is the clean-failure case below, tree untouched. An operation that fails *cleanly* — disk error, refused checkout; network and auth are 07-sync-collab.md's pause-and-badge story — surfaces as a one-shot banner failure naming the operation and the error, the tree left as it was; failure after the tree changed wholesale is instead 02-architecture.md's failed-final-reload lock. **Popover-anchored operations answer at the form first** (ruled 2026-07-31): add-git — and later popover-asked operations like verify-remote — fail into an inline caption in the popover's git section while the popover is open (the user asked from a form still under their eye; dismissing the popover dismisses the stale error, retry is right there, VoiceOver reads it from the focused surface); if the popover has closed before the answer arrives, the failure falls back to the one-shot banner above — inline is the primary surface, never a silence trap. The banner enumeration stays the posture for board-wholesale brackets that outlive any one surface — branch switch and undo restore included (blessed 2026-07-31: a switch's bounded lock wait can expire long after the popover dismissed). Which window's strip carries the row is 02-architecture.md's hosted-by-the-window-of-origin rule — the card window hosts its own strip, and a ⌘Z pressed there surfaces its failure there (re-homing to the board window if the card window closes first, per 02). - **`index.lock` contention is never an error.** If the auto-committer finds the index locked (an agent's commit in flight), it backs off briefly and retries; if the lock persists, it simply re-debounces — the pending changes are still pending, and the next quiet moment commits them. No banner, no log-worthy failure: a held lock is another writer doing its job. (Genuine commit failures — disk full, repo corruption — are different: files stay safe on disk but history stops advancing; surfaced per 02-architecture.md ▸ Write-failure surfacing, retried on the next debounce.) **The same posture covers every app-initiated operation** (settled): pull, push, branch switch, and undo restore meeting a held lock wait and retry briefly, silently; contention outlasting the brief retry surfaces as a *waiting* state in the operation's in-progress banner row ("waiting for another writer's git lock"), retrying on its cadence — never an error dialog, never a hammer — and a wait that persists implausibly long names the lock path (a crashed writer's leftover is the user's to clear; the never-mutate rule's one exemption is the app's own leftovers, Abnormal repo states above). **The wait is bounded — 30 s, then a clean failure** (blessed 2026-07-31; injectable): a genuine writer finishes in seconds, so only a stale lock ever reaches the bound, and an eternal spinner holding the wholesale bracket — and with it the board — is worse than a failure that names the path to delete. The bound expiring is the clean-failure case below, tree untouched. An operation that fails *cleanly* — disk error, refused checkout; network and auth are 07-sync-collab.md's pause-and-badge story — surfaces as a one-shot banner failure naming the operation and the error, the tree left as it was; failure after the tree changed wholesale is instead 02-architecture.md's failed-final-reload lock. **Form-anchored operations answer at the form first** (ruled 2026-07-31; container updated by the popover/sheet split — these forms live in the settings sheet): add-git — and later sheet-asked operations like verify-remote — fail into an inline caption in the sheet's relevant section while the sheet is up (the user asked from a form still under their eye; dismissing the sheet dismisses the stale error, retry is right there, VoiceOver reads it from the focused surface); if the sheet has been dismissed before the answer arrives, the failure falls back to the one-shot banner above — inline is the primary surface, never a silence trap. The banner enumeration stays the posture for board-wholesale brackets that outlive any one surface — branch switch and undo restore included (blessed 2026-07-31: a switch's bounded lock wait can expire long after the popover dismissed). Which window's strip carries the row is 02-architecture.md's hosted-by-the-window-of-origin rule — the card window hosts its own strip, and a ⌘Z pressed there surfaces its failure there (re-homing to the board window if the card window closes first, per 02).
- **A clean tree is the happy path, not a malfunction.** When the debounce fires and the tree has nothing to commit — the agent already committed its own work — the auto-committer no-ops silently. The agent's commit, under the agent's own authorship, *is* the record; that is precisely what the self-commit recommendation is for. - **A clean tree is the happy path, not a malfunction.** When the debounce fires and the tree has nothing to commit — the agent already committed its own work — the auto-committer no-ops silently. The agent's commit, under the agent's own authorship, *is* the record; that is precisely what the self-commit recommendation is for.
- **An agent's `git add -A` can sweep up the user's not-yet-committed app-mediated changes** under the agent's authorship, muddying structural attribution for that window. Accepted limit — the app cannot police another process's staging; the agent guide (08-agent-integration.md) tells agents to commit only their own paths, which keeps well-behaved agents honest. - **An agent's `git add -A` can sweep up the user's not-yet-committed app-mediated changes** under the agent's authorship, muddying structural attribution for that window. Accepted limit — the app cannot police another process's staging; the agent guide (08-agent-integration.md) tells agents to commit only their own paths, which keeps well-behaved agents honest.
+7 -7
View File
@@ -22,7 +22,7 @@ The board is a git repository (the 06-history-undo.md substrate — undo/redo, a
- Push/pull becomes a sharing mechanism between machines/people at file-level granularity; the fractal one-item-one-file design keeps conflicts rare and small (a reorder touches one file). - Push/pull becomes a sharing mechanism between machines/people at file-level granularity; the fractal one-item-one-file design keeps conflicts rare and small (a reorder touches one file).
- **Remote tracking lives in the board popover** (03-board-ui.md): ahead/behind indicator plus manual **Pull** and **Push** controls — which are also Board-menu items (no default chord, remappable — 11-command-nexus.md; the configuration carve-out is 04-interactions.md's). - **Remote tracking lives in the board popover** (03-board-ui.md): ahead/behind indicator plus manual **Pull** and **Push** controls — which are also Board-menu items (no default chord, remappable — 11-command-nexus.md; the configuration carve-out is 04-interactions.md's).
- **Which remote is the board's remote** (settled — adopted clones can carry several): resolution is git's own defaulting — the current branch's upstream remote; else `origin`; else the repo's sole remote. The popover names the remote it tracks, its change-remote control edits exactly that one, add-remote on a remote-less repo creates `origin`, and 06-history-undo.md's quiet first `push -u` targets the resolved remote (recording it as the upstream, which pins resolution thereafter). The unresolvable case — several remotes, no upstream, none named `origin` — is surfaced honestly: remote operations disable and the popover offers a one-time remote picker, whose choice becomes the branch's upstream on the next push. - **Which remote is the board's remote** (settled — adopted clones can carry several): resolution is git's own defaulting — the current branch's upstream remote; else `origin`; else the repo's sole remote. The settings sheet names the remote it tracks (the popover's tracking badge shows it at a glance — the 2026-07-31 popover/sheet split, 03-board-ui.md), its change-remote control edits exactly that one, add-remote on a remote-less repo creates `origin`, and 06-history-undo.md's quiet first `push -u` targets the resolved remote (recording it as the upstream, which pins resolution thereafter). The unresolvable case — several remotes, no upstream, none named `origin` — is surfaced honestly: remote operations disable and the sheet offers a one-time remote picker, whose choice becomes the branch's upstream on the next push.
- **Optional push-on-commit**: a per-board setting (stored app-side in the board registry — 02-architecture.md's per-board app state); when enabled, **every commit the app makes** is pushed immediately — auto-commits and undo/redo restore commits alike (06-history-undo.md's restores are commits like any other; a shared board never shows a phantom lag after an undo). - **Optional push-on-commit**: a per-board setting (stored app-side in the board registry — 02-architecture.md's per-board app state); when enabled, **every commit the app makes** is pushed immediately — auto-commits and undo/redo restore commits alike (06-history-undo.md's restores are commits like any other; a shared board never shows a phantom lag after an undo).
- **Push failures never nag.** A push rejected as non-fast-forward (another machine pushed first) triggers an automatic **fetch → rebase → push**, with bounded retries — the same rebase machinery as Pull, so it cannot block and cannot conflict. Manual Push behaves identically. Stated plainly: enabling push-on-commit implicitly accepts that remote commits may land in the live board whenever pushes race — consistent with the board's live-reload nature, but it should be learned from the design, not discovered. All other push failures stay quiet: pushes queue, the ahead/behind indicator in the board popover carries the pending count and the last error, and pushing resumes automatically on the next commit or manual Push. No modals, no per-commit errors. The one refinement: **authentication failures pause rather than retry** — see Remote authentication below. - **Push failures never nag.** A push rejected as non-fast-forward (another machine pushed first) triggers an automatic **fetch → rebase → push**, with bounded retries — the same rebase machinery as Pull, so it cannot block and cannot conflict. Manual Push behaves identically. Stated plainly: enabling push-on-commit implicitly accepts that remote commits may land in the live board whenever pushes race — consistent with the board's live-reload nature, but it should be learned from the design, not discovered. All other push failures stay quiet: pushes queue, the ahead/behind indicator in the board popover carries the pending count and the last error, and pushing resumes automatically on the next commit or manual Push. No modals, no per-commit errors. The one refinement: **authentication failures pause rather than retry** — see Remote authentication below.
- **There can be no conflicts — and no data loss.** Every edit becomes a commit before anything can overwrite it (auto-commit settles local changes; the tree is clean by the time a pull runs). The clean-tree premise is why **abnormal repo states pause the remote half too**: a detached HEAD or in-progress merge/rebase holds Pull, Push, and push-on-commit alongside auto-commit (06-history-undo.md ▸ Rules ▸ Abnormal repo states — the whole git surface pauses; the ahead/behind badge keeps counting, a fetch being a read). A pull — manual, or the automatic fetch-rebase after a rejected push — runs only at **interaction rest**: it queues behind an in-flight drag or open inline editor (the same settled-change notion the auto-commit debounce uses), flushes the pending auto-commit (06-history-undo.md's flush-before-overwrite), then runs bracketed (02-architecture.md's live-reload resilience) — never an error dialog, never a board yanked mid-drag. An open card-body **Edit session neither blocks a pull nor is interrupted by one**: the flush commits the session's on-disk saves as-is (06-history-undo.md's mechanical exception to session-granularity commits) and the rebase runs; when the pulled commits touch the very card being edited, the card window **signposts** the remote change (a transient banner, no modal, no merge UI) while the dirty buffer stays put and wins per 05-card-window.md — the losing remote version is a commit, one revert away. An open **raw-source buffer gets the same treatment** (settled): a pull neither blocks on it nor invalidates it — the bracket's write lock merely disables Apply while the pull runs — and the same-card signpost shows in source mode too (the banner strip is window furniture, not part of the swapped content area). A later Apply is last-writer-wins across the *whole file*, frontmatter included, with the overwritten pulled version a commit one revert away — on the same branch this is exactly the Edit-buffer race. (Branch switch and undo restore must gate raw buffers on save-or-discard instead — 06-history-undo.md ▸ Branch switching — because there a stale Apply would write onto a *different tree's* card, not merely race a newer version of the same one.) A pull fetches the remote's commits and **rebases local commits on top of them**; where a rebase hits a genuinely conflicting hunk, the **local side wins** — always, with no configuration. (This is the one deliberate history rewrite in the app, and it only ever touches unpushed local commits — see 06-history-undo.md's undo-never-rewrites rule.) Crucially, resolution discards nothing: the losing remote version survives intact in the fetched commits below, so an edit that "lost" the rebase is visible in any git client and one revert away. What may *appear* as data loss is always recoverable. No interactive merge UI, no conflict markers written by the app, sync never blocks. A pull that cannot start or fails cleanly follows 06-history-undo.md's app-initiated-operation posture — lock contention shows as a waiting state in the operation's banner row, clean failures as one-shot banner errors, an interrupted rebase is aborted-and-reported via the own-leftovers exemption; push alone keeps the richer queue-and-badge story (below). Conflict markers encountered in files (from git activity *outside* the app) fail fast only where they break parsing — markers in or around the frontmatter make the file the malformed-input case the loader rejects loudly with the offending path. Markers wholly inside a Markdown body are, honestly, valid input: they load fine and render as body text, and the app deliberately doesn't police body content to detect them (stated stance, not an oversight). (The old repo's AI-THINKING-merge-conflicts.md explored this territory; mine it when specifying the rebase mechanics.) - **There can be no conflicts — and no data loss.** Every edit becomes a commit before anything can overwrite it (auto-commit settles local changes; the tree is clean by the time a pull runs). The clean-tree premise is why **abnormal repo states pause the remote half too**: a detached HEAD or in-progress merge/rebase holds Pull, Push, and push-on-commit alongside auto-commit (06-history-undo.md ▸ Rules ▸ Abnormal repo states — the whole git surface pauses; the ahead/behind badge keeps counting, a fetch being a read). A pull — manual, or the automatic fetch-rebase after a rejected push — runs only at **interaction rest**: it queues behind an in-flight drag or open inline editor (the same settled-change notion the auto-commit debounce uses), flushes the pending auto-commit (06-history-undo.md's flush-before-overwrite), then runs bracketed (02-architecture.md's live-reload resilience) — never an error dialog, never a board yanked mid-drag. An open card-body **Edit session neither blocks a pull nor is interrupted by one**: the flush commits the session's on-disk saves as-is (06-history-undo.md's mechanical exception to session-granularity commits) and the rebase runs; when the pulled commits touch the very card being edited, the card window **signposts** the remote change (a transient banner, no modal, no merge UI) while the dirty buffer stays put and wins per 05-card-window.md — the losing remote version is a commit, one revert away. An open **raw-source buffer gets the same treatment** (settled): a pull neither blocks on it nor invalidates it — the bracket's write lock merely disables Apply while the pull runs — and the same-card signpost shows in source mode too (the banner strip is window furniture, not part of the swapped content area). A later Apply is last-writer-wins across the *whole file*, frontmatter included, with the overwritten pulled version a commit one revert away — on the same branch this is exactly the Edit-buffer race. (Branch switch and undo restore must gate raw buffers on save-or-discard instead — 06-history-undo.md ▸ Branch switching — because there a stale Apply would write onto a *different tree's* card, not merely race a newer version of the same one.) A pull fetches the remote's commits and **rebases local commits on top of them**; where a rebase hits a genuinely conflicting hunk, the **local side wins** — always, with no configuration. (This is the one deliberate history rewrite in the app, and it only ever touches unpushed local commits — see 06-history-undo.md's undo-never-rewrites rule.) Crucially, resolution discards nothing: the losing remote version survives intact in the fetched commits below, so an edit that "lost" the rebase is visible in any git client and one revert away. What may *appear* as data loss is always recoverable. No interactive merge UI, no conflict markers written by the app, sync never blocks. A pull that cannot start or fails cleanly follows 06-history-undo.md's app-initiated-operation posture — lock contention shows as a waiting state in the operation's banner row, clean failures as one-shot banner errors, an interrupted rebase is aborted-and-reported via the own-leftovers exemption; push alone keeps the richer queue-and-badge story (below). Conflict markers encountered in files (from git activity *outside* the app) fail fast only where they break parsing — markers in or around the frontmatter make the file the malformed-input case the loader rejects loudly with the offending path. Markers wholly inside a Markdown body are, honestly, valid input: they load fine and render as body text, and the app deliberately doesn't police body content to detect them (stated stance, not an oversight). (The old repo's AI-THINKING-merge-conflicts.md explored this territory; mine it when specifying the rebase mechanics.)
@@ -32,13 +32,13 @@ The board is a git repository (the 06-history-undo.md substrate — undo/redo, a
Everything above assumes credentials exist; this is where they come from. Constraints first, stated as honest limits: a sandboxed app with bundled libgit2 cannot read `~/.ssh` (no entitlement grants it — silent access to every key is exactly what the sandbox exists to prevent), cannot reach `ssh-agent` (a unix socket outside the container; this also rules out 1Password/Secretive/YubiKey agents), cannot run the user's credential helpers, and gains nothing by shelling out (children inherit the sandbox). The network-client entitlement is assumed. Auth is therefore app-native, and **the Keychain is the only credential store** — credentials never live in board files or repo config. That is the deliberate inversion of files-are-truth: secrets are the one thing that must never be a file in the board. Everything above assumes credentials exist; this is where they come from. Constraints first, stated as honest limits: a sandboxed app with bundled libgit2 cannot read `~/.ssh` (no entitlement grants it — silent access to every key is exactly what the sandbox exists to prevent), cannot reach `ssh-agent` (a unix socket outside the container; this also rules out 1Password/Secretive/YubiKey agents), cannot run the user's credential helpers, and gains nothing by shelling out (children inherit the sandbox). The network-client entitlement is assumed. Auth is therefore app-native, and **the Keychain is the only credential store** — credentials never live in board files or repo config. That is the deliberate inversion of files-are-truth: secrets are the one thing that must never be a file in the board.
- **Transports: HTTPS and SSH, both Keychain-backed.** - **Transports: HTTPS and SSH, both Keychain-backed.**
- **HTTPS (primary)**: username + token (forge PATs; plain basic auth for generic hosts), stored as a Keychain internet password keyed by **host + username** — git's own scoping model, shared across boards: one GitHub token serves every board, and two accounts on one host coexist as two usernames. **Which username a board uses is the remote URL's business** (git's own answer, and the HTTPS analogue of the SSH per-host table): a username in the URL (`https://alice@host/…`) selects the Keychain item `host + alice`, and the popover's credential capture stamps the entered username into the remote URL in repo config — the URL is the assignment record, no app-side state (the secret itself stays in the Keychain). A URL naming no username resolves to the host's sole stored username; when a host has several, the popover's username field becomes a picker and saving stamps the choice into the URL, while background operations treat the ambiguity as **Authentication needed** (pause and badge, never guess — the same posture as auth failure). - **HTTPS (primary)**: username + token (forge PATs; plain basic auth for generic hosts), stored as a Keychain internet password keyed by **host + username** — git's own scoping model, shared across boards: one GitHub token serves every board, and two accounts on one host coexist as two usernames. **Which username a board uses is the remote URL's business** (git's own answer, and the HTTPS analogue of the SSH per-host table): a username in the URL (`https://alice@host/…`) selects the Keychain item `host + alice`, and the settings sheet's credential capture stamps the entered username into the remote URL in repo config — the URL is the assignment record, no app-side state (the secret itself stays in the Keychain). A URL naming no username resolves to the host's sole stored username; when a host has several, the sheet's username field becomes a picker and saving stamps the choice into the URL, while background operations treat the ambiguity as **Authentication needed** (pause and badge, never guess — the same posture as auth failure).
- **SSH — Keychain-resident keys, never key files.** Each Mac has a **Lanework key**: an app-generated ed25519 keypair whose private half lives as an ACL-protected Keychain item and is handed to libssh2 from memory — it never exists on disk. The board popover shows the public key with a Copy affordance; the user adds it to their forge like any machine key. An **existing key imports by paste or drag** (a one-time read under user intent): copied into the Keychain — passphrase entered once at import, stored under Keychain protection thereafter — and the original file is never referenced again. Per-machine identity, per-Mac revocable on the forge — the ssh-idiomatic shape. (Secure Enclave-backed keys — non-exportable, custom sign callback, P-256 — are a possible later hardening, not v1.) - **SSH — Keychain-resident keys, never key files.** Each Mac has a **Lanework key**: an app-generated ed25519 keypair whose private half lives as an ACL-protected Keychain item and is handed to libssh2 from memory — it never exists on disk. The board settings sheet shows the public key with a Copy affordance; the user adds it to their forge like any machine key. An **existing key imports by paste or drag** (a one-time read under user intent): copied into the Keychain — passphrase entered once at import, stored under Keychain protection thereafter — and the original file is never referenced again. Per-machine identity, per-Mac revocable on the forge — the ssh-idiomatic shape. (Secure Enclave-backed keys — non-exportable, custom sign callback, P-256 — are a possible later hardening, not v1.)
- **Key scope: app-level objects, per-host assignment.** Keys are never board state — the machine key plus any imports live app-wide (Keychain), and each SSH host maps to one of them: default the machine key; importing a key during a host's setup assigns it to that host. A "host" is `hostname[:port]` parsed from the remote URL — the same endpoint identity the TOFU fingerprint store uses (OpenSSH's own `[host]:port` convention); the URL's username (`git@`) disambiguates nothing and stays out of it. The assignment table holds **only overrides** — no entry means the machine key, so the default costs zero records and removing an override self-heals to it. The popover's key picker is labeled per-host ("key for github.com"), which teaches the one cross-board consequence: switching a host's key switches it for every board on that host — the same rotate-once-follow-everywhere behavior as HTTPS tokens. Housekeeping stays small: an import referenced by no host row can be removed; the machine key only regenerates (confirm-gated — it invalidates the old public half on every forge), and that is the entire rotation story. The board popover is only the surface — it shows the key for *that remote's host*, the way the commit-identity fields front repo-local config. Known limit, accepted: two accounts on the *same* host can't be told apart by key (forges bind key→account globally; git's own answer is ssh-config aliases, which live in files the sandbox can't read) — a per-remote key override joins the wishlist if it ever bites. - **Key scope: app-level objects, per-host assignment.** Keys are never board state — the machine key plus any imports live app-wide (Keychain), and each SSH host maps to one of them: default the machine key; importing a key during a host's setup assigns it to that host. A "host" is `hostname[:port]` parsed from the remote URL — the same endpoint identity the TOFU fingerprint store uses (OpenSSH's own `[host]:port` convention); the URL's username (`git@`) disambiguates nothing and stays out of it. The assignment table holds **only overrides** — no entry means the machine key, so the default costs zero records and removing an override self-heals to it. The sheet's key picker is labeled per-host ("key for github.com"), which teaches the one cross-board consequence: switching a host's key switches it for every board on that host — the same rotate-once-follow-everywhere behavior as HTTPS tokens. Housekeeping stays small: an import referenced by no host row can be removed; the machine key only regenerates (confirm-gated — it invalidates the old public half on every forge), and that is the entire rotation story. The board settings sheet is only the surface — it shows the key for *that remote's host*, the way the commit-identity fields front repo-local config. Known limit, accepted: two accounts on the *same* host can't be told apart by key (forges bind key→account globally; git's own answer is ssh-config aliases, which live in files the sandbox can't read) — a per-remote key override joins the wishlist if it ever bites.
- **Host verification is trust-on-first-use**: with no `~/.ssh/known_hosts` readable, the first connection to an SSH host confirms its fingerprint with the user; accepted fingerprints live app-side (02-architecture.md's app-wide state home, host-scoped). A later mismatch **hard-blocks with an explanation** — that mismatch is the attack the check exists for. - **Host verification is trust-on-first-use**: with no `~/.ssh/known_hosts` readable, the first connection to an SSH host confirms its fingerprint with the user; accepted fingerprints live app-side (02-architecture.md's app-wide state home, host-scoped). A later mismatch **hard-blocks with an explanation** — that mismatch is the attack the check exists for.
- **Setup verifies right there.** Adding or changing a remote (board popover — 03-board-ui.md) probes with authentication immediately (ls-remote): missing or rejected credentials surface **inline in the popover** — HTTPS shows username + token fields with a forge-appropriate hint; SSH shows the machine key to copy plus Verify. The user leaves the popover with a remote that demonstrably works, or knowingly not. Boards adopted from a terminal clone (whose auth lives outside the sandbox and can't be reused) hit the same inline flow at the first in-app operation that needs credentials. - **Setup verifies right there.** Adding or changing a remote (board settings sheet — 03-board-ui.md, the 2026-07-31 popover/sheet split) probes with authentication immediately (ls-remote): missing or rejected credentials surface **inline in the sheet** — HTTPS shows username + token fields with a forge-appropriate hint; SSH shows the machine key to copy plus Verify. The user leaves the sheet with a remote that demonstrably works, or knowingly not. Boards adopted from a terminal clone (whose auth lives outside the sandbox and can't be reused) hit the same inline flow at the first in-app operation that needs credentials.
- **Auth failures pause; they never nag and never hammer.** A push or pull rejected for authentication (expired token, revoked key) is not retried — a dead credential cannot succeed, and hammering invites rate limits and lockouts. The push queue pauses and the popover badge switches to a distinct **Authentication needed** state carrying the error; the popover presents the same inline fields, prefilled where possible. Updating the credential (or fixing forge-side and hitting Verify) resumes the queue. Network failures keep the quiet auto-resume above — only auth pauses. - **Auth failures pause; they never nag and never hammer.** A push or pull rejected for authentication (expired token, revoked key) is not retried — a dead credential cannot succeed, and hammering invites rate limits and lockouts. The push queue pauses and the popover badge switches to a distinct **Authentication needed** state carrying the error (the badge points at the settings sheet); the sheet presents the same inline fields, prefilled where possible. Updating the credential (or fixing forge-side and hitting Verify) resumes the queue. Network failures keep the quiet auto-resume above — only auth pauses.
- **Background operations never prompt.** Push-on-commit and the automatic fetch-rebase-push stay silent through auth trouble (badge only); credential capture happens exclusively in the popover, where the user already is when it matters (manual Pull/Push live there too). - **Background operations never prompt.** Push-on-commit and the automatic fetch-rebase-push stay silent through auth trouble (badge only); credential capture happens exclusively in the settings sheet, one click behind the badge that says it's needed (manual Pull/Push stay in the popover — daily operations, the split's other half).
## iCloud Drive — not supported (decided) ## iCloud Drive — not supported (decided)
+2 -1
View File
@@ -38,6 +38,7 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na
- **Welcome window**: recents rows are elements labeled "⟨name⟩, ⟨location⟩, N lanes, M cards" (name, icon, and counts all registry-cached — 02-architecture.md; the row never reads a board's files); row actions (Open / Reveal in Finder / Forget) via context menu; unavailable rows say so ("unavailable — board not found"). - **Welcome window**: recents rows are elements labeled "⟨name⟩, ⟨location⟩, N lanes, M cards" (name, icon, and counts all registry-cached — 02-architecture.md; the row never reads a board's files); row actions (Open / Reveal in Finder / Forget) via context menu; unavailable rows say so ("unavailable — board not found").
- **Template chooser**: templates are elements labeled by title; the mini per-lane previews are decorative and hidden from the tree. - **Template chooser**: templates are elements labeled by title; the mini per-lane previews are decorative and hidden from the tree.
- **Board popover**: labeled controls throughout; the ahead/behind indicator's information — counts, queued pushes, last error — must be readable as text, never conveyed by color or shape alone. - **Board popover**: labeled controls throughout; the ahead/behind indicator's information — counts, queued pushes, last error — must be readable as text, never conveyed by color or shape alone.
- **Board settings sheet** (03 — the 2026-07-31 popover/sheet split): titled and sectioned with headers VoiceOver can navigate by; labeled controls throughout; inline probe/verify outcomes announced from the focused section; every control Tab-reachable under Full Keyboard Access — the sheet exists partly *because* Tab-walking two dozen controls in an untitled popover failed this bar.
- **Style editor** (card sidebar section, board popover, Style… popover — 03-board-ui.md ▸ Styling ▸ Controls): grids are arrow-navigable, every well Tab-reachable and labeled by name (palette color, symbol name; leading wells "None" / "Default"); the current value is stated by trait, and a batch selection's mixed state reads as "mixed", never conveyed by highlight alone. - **Style editor** (card sidebar section, board popover, Style… popover — 03-board-ui.md ▸ Styling ▸ Controls): grids are arrow-navigable, every well Tab-reachable and labeled by name (palette color, symbol name; leading wells "None" / "Default"); the current value is stated by trait, and a batch selection's mixed state reads as "mixed", never conveyed by highlight alone.
## Text scaling & visual accommodations ## Text scaling & visual accommodations
@@ -50,7 +51,7 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na
## Verification ## Verification
- **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`) runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit, raw source), welcome, template chooser, board popover. - **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`) runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit, raw source), welcome, template chooser, board popover, board settings sheet.
- **A manual VoiceOver smoke script** lives with the test plan: create lane → create card → rename → cut/paste to another lane → external edit lands (announcement heard) → delete → restore from the trash (⌘X/⌘V) → Empty Trash. Run per release; it is the canonical "does the board actually work blind" check. - **A manual VoiceOver smoke script** lives with the test plan: create lane → create card → rename → cut/paste to another lane → external edit lands (announcement heard) → delete → restore from the trash (⌘X/⌘V) → Empty Trash. Run per release; it is the canonical "does the board actually work blind" check.
## Changes from Kanban ## Changes from Kanban
+4 -2
View File
@@ -10,7 +10,7 @@ The single source of truth for **every command and action the app can perform**
| **M** | Menu command, effectively fixed | Undo/Redo only: NSUndoManager rewrites their titles dynamically, which defeats title-matched remapping (04). | | **M** | Menu command, effectively fixed | Undo/Redo only: NSUndoManager rewrites their titles dynamically, which defeats title-matched remapping (04). |
| **G** | Fixed grammar key | Platform grammar, deliberately not remappable — Finder's own Return/arrows aren't either (04 ▸ Grammar). | | **G** | Fixed grammar key | Platform grammar, deliberately not remappable — Finder's own Return/arrows aren't either (04 ▸ Grammar). |
| **P** | Pointer grammar | Clicks, drags, modifiers — not customizable. | | **P** | Pointer grammar | Clicks, drags, modifiers — not customizable. |
| **C** | Configuration control | Form-like controls (board popover, chooser, welcome); the keyboard path is reachability (Board Info ⌘I + Tab-reachable controls), not bindings — 04's configuration carve-out. | | **C** | Configuration control | Form-like controls (board popover, board settings sheet, chooser, welcome); the keyboard path is reachability (Board Info ⌘I / Board ▸ Board Settings… + Tab-reachable controls), not bindings — 04's configuration carve-out, containers per the 2026-07-31 popover/sheet split. |
**Toolbar presence is a separate axis**, orthogonal to the classes: toolbar items mirror menu commands, their presence per window is user-customizable (Customize Toolbar — 03-board-ui.md ▸ Toolbar; defaults and catalogs live there), and a toolbar is never a function's only home. Labels match menu titles except Undo/Redo, whose toolbar labels stay static (03). **Toolbar presence is a separate axis**, orthogonal to the classes: toolbar items mirror menu commands, their presence per window is user-customizable (Customize Toolbar — 03-board-ui.md ▸ Toolbar; defaults and catalogs live there), and a toolbar is never a function's only home. Labels match menu titles except Undo/Redo, whose toolbar labels stay static (03).
@@ -45,6 +45,7 @@ The single source of truth for **every command and action the app can perform**
| Board | Move Left / Move Right | ⌘← / ⌘→ | Lane selection only (one slot; never into the trash) — cards cross lanes by drag or Cut/Paste, not ⌘-arrows; disabled while any text control is focused (04 ▸ Grammar, caret-chords rule) | | Board | Move Left / Move Right | ⌘← / ⌘→ | Lane selection only (one slot; never into the trash) — cards cross lanes by drag or Cut/Paste, not ⌘-arrows; disabled while any text control is focused (04 ▸ Grammar, caret-chords rule) |
| Board | Increase Lane Width / Decrease Lane Width (the stepper's re-divide semantics, never the window's size — 03 ▸ Lane) | ⌥⌘→ / ⌥⌘← | Selected lane(s) — batches over a multi-lane selection like style (03 ▸ Lane); disabled while any text control is focused (04 ▸ Grammar, caret-chords rule) | | Board | Increase Lane Width / Decrease Lane Width (the stepper's re-divide semantics, never the window's size — 03 ▸ Lane) | ⌥⌘→ / ⌥⌘← | Selected lane(s) — batches over a multi-lane selection like style (03 ▸ Lane); disabled while any text control is focused (04 ▸ Grammar, caret-chords rule) |
| Board | Pull / Push | — (no default) | Remote-backed boards only (07); disabled during 06's abnormal-state pause (the whole git surface holds) and on an unresolvable remote (07's one-time remote picker case); popover twins exist | | Board | Pull / Push | — (no default) | Remote-backed boards only (07); disabled during 06's abnormal-state pause (the whole git surface holds) and on an unresolvable remote (07's one-time remote picker case); popover twins exist |
| Board | Board Settings… | — (no default) | Opens the board settings sheet (03 — the 2026-07-31 popover/sheet split); popover row twins it |
| View | Show Trash (checkmark toggle) | — (no default) | Board window — ⇧⌘T is deliberately left to the system's Show Tab Bar: window tabbing stays enabled (settled; see Standard macOS furniture), so the chord is the system's; assign one via the remapping mechanism if wanted (04 ▸ Configurable bindings) | | View | Show Trash (checkmark toggle) | — (no default) | Board window — ⇧⌘T is deliberately left to the system's Show Tab Bar: window tabbing stays enabled (settled; see Standard macOS furniture), so the chord is the system's; assign one via the remapping mechanism if wanted (04 ▸ Configurable bindings) |
| View | Edit Body (checkmark toggle) | ⌘E | Card window; disabled while Raw Source is active | | View | Edit Body (checkmark toggle) | ⌘E | Card window; disabled while Raw Source is active |
| View | Show Comments (checkmark toggle) | — (no default) | Card window; app-wide, persisted across restarts (re-ruled 2026-07-29 — no content-derived auto-show; 05 ▸ The comments column) | | View | Show Comments (checkmark toggle) | — (no default) | Card window; app-wide, persisted across restarts (re-ruled 2026-07-29 — no content-derived auto-show; 05 ▸ The comments column) |
@@ -100,7 +101,8 @@ Context menus are the per-item action inventory VoiceOver reads (10 ▸ The boar
## Configuration controls (C) ## Configuration controls (C)
- **Board popover** (Board Info ⌘I — 03 ▸ Board popover): board rename; embedded style editor; add-git (mode none) / repo-nested explanation (06); branch display, switch, create; commit-identity name/email (06); add/change remote, credential fields, the SSH key surface — machine key Copy + Verify, key import by paste or drag, the per-host key picker, removal of an unreferenced import, confirm-gated machine-key regeneration — Authentication-needed state (07); ahead/behind with Pull/Push buttons and the push-on-commit toggle. - **Board popover** (Board Info ⌘I — 03 ▸ Board popover; light surface per the 2026-07-31 popover/sheet split): board rename; embedded style editor; posture lines (repo-nested explanation, unreadable/paused states — 06); branch display and switch picker; ahead/behind with Pull/Push buttons and the status badges; the Board Settings… row.
- **Board settings sheet** (Board ▸ Board Settings…, or the popover row — 03 ▸ Board settings sheet): add-git (mode none, 06); branch create; commit-identity name/email (06); add/change remote with inline verify, credential fields, the SSH key surface — machine key Copy + Verify, key import by paste or drag, the per-host key picker, removal of an unreferenced import, confirm-gated machine-key regeneration — the Authentication-needed capture and TOFU confirms (07); the push-on-commit toggle.
- **Style editor** (three anchors — 03 ▸ Styling ▸ Controls): grids arrow-navigable, every well Tab-reachable. - **Style editor** (three anchors — 03 ▸ Styling ▸ Controls): grids arrow-navigable, every well Tab-reachable.
- **Template chooser** (09): template selection; Reveal in Finder for the user store. - **Template chooser** (09): template selection; Reveal in Finder for the user store.
- **Welcome** (03): recents list; Forget. - **Welcome** (03): recents list; Forget.