Failure paths for app-initiated git operations
Fills the gap: pull/push/switch/restore share the auto-committer's lock posture with a visible waiting state; clean failures are one-shot banner errors; the never-mutate rule exempts the app's own stamped leftovers, which it aborts and reports. Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
This commit is contained in:
@@ -7,7 +7,7 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board
|
||||
- **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.
|
||||
- **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. 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**: 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; stated here so it isn't rediscovered as a bug).
|
||||
- **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. 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**: auto-commit holds (the auth-pause posture, 07-sync-collab.md — pause, badge, explain, never hammer), Undo/Redo and the branch controls disable, 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, so finishing the operation in a terminal resumes the pipeline without ceremony.
|
||||
- **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. 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**: auto-commit holds (the auth-pause posture, 07-sync-collab.md — pause, badge, explain, never hammer), Undo/Redo and the branch controls disable, 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, 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. 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.
|
||||
- **Auto-commit**: every settled change (debounced past drag/typing churn) commits with a descriptive message ("Move card 'Fix login' to Doing"). Board undo sees **Edit sessions, not save ticks** (resettled from typing-settle granularity): the body editor's ~700 ms disk saves (05-card-window.md) keep the file crash-safe throughout a session but stay **uncommitted** — the body commit lands when the session ends, the **Edit→Preview flip being the effective Save button** (raw-source entry and window close end the session too). The committer **stages around open Edit sessions**: a board change committing mid-session excludes the session card's folder from staging, so a lane move never sweeps half-typed body text into its commit. Settled-tree events that cannot wait — a pull's flush-before-overwrite — commit the session's on-disk saves as-is (a mechanical exception; 07-sync-collab.md's same-card signpost covers the visible half); branch switch instead gates on explicit save-or-discard (Branch switching below). The cadence constraint below demands batching at least this coarse. **Board-window close and app quit flush the pipeline** — any pending editor save (05-card-window.md), then the pending auto-commit — before teardown; nothing settled is ever left unsaved or uncommitted by closing.
|
||||
- **Undo routing is by focus** — the platform's first-responder rule, stated here because two undo systems coexist. 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. With focus anywhere else, Edit ▸ Undo/Redo are git undo (and are disabled on boards without it). **No fall-through**: exhausting a focused editor's stack beeps; it never reaches board history.
|
||||
@@ -56,7 +56,7 @@ Agent and hand edits arrive through the watcher like any change and get auto-com
|
||||
|
||||
**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.)
|
||||
- **`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). 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.
|
||||
- **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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user