Build the auto-commit engine

Every settled change on a git-mode board commits, debounced 2s past
drag/typing churn, staged whole-root with .gitignore respected.
GitCommitOperation reaches the vendored libgit2 directly (same 1.9.2
pin SwiftGitX resolves — importable, not duplicated) for
signature-capable commits; add-git's config materialization is gone,
identity resolves at commit time (repo-local config, else derived
default) per the 2026-07-31 ruling in 06. CommitAttribution
classifies per file off EchoLedger receipts: user identity on
app-mediated windows, Lanework External <[email protected]>
on foreign, the modified-by refinement (<slug>@agents.lanework
.invalid) when every foreign file agrees, heal-marked receipts split
into their own commit — window split foreign → heal → user.
Edit-session granularity: ~700ms saves stay uncommitted, staging
excludes open session folders (closure-resolved so mid-session moves
stage around the new location), session end nudges the debounce so
each session lands exactly one body commit. Flush-before-overwrite
gates on known-foreign windows and commits synchronously ahead of
the write; close/quit flush the pipeline via CloseFlushCoordinator's
committerFlush. index.lock backs off briefly then re-debounces
silently; clean tree no-ops; genuine failures ride the standing
history-suspension banner and retry next debounce. Abnormal repo
states (detached HEAD, merge/rebase/cherry-pick in progress) hold
the engine with a 15s re-check; unborn HEAD commits "Initial board
state" whole-tree; dirty tree at open catches up through the same
engine. Message seam (CommitMessageComposing) ships interim — the
semantic composer is the next card.

Discovery diffs HEAD against an in-memory index with rename
detection (git status alone never pairs a bare mv), and a failed
survey reads as "could not look", never "nothing changed".

46 new tests / 8 suites, all real repositories via bundled libgit2.
2240 tests / 383 suites green; InertGitTests untouched.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 14:10:55 -04:00
parent 189af238a1
commit 3c07c26fda
17 changed files with 3009 additions and 60 deletions
+20
View File
@@ -268,6 +268,26 @@ public final class EchoLedger: Sendable {
receipts.withLock { $0[path]?.receipt }
}
/// **Every receipt the ledger holds right now, with its heal mark read, never consumed.**
///
/// Pro's auto-committer's one call (`GitAutoCommitter.harvest`), and it has to be a copy rather
/// than a read at commit time for an ordering reason worth stating here: receipts are *consumed*
/// by the landing reload that classifies them ("one write, one echo"), and the committer asks its
/// question a debounce later by which time the receipt for the user's own card edit is long
/// gone, and reading the live ledger would attribute the user's own work to `Lanework External`.
/// So the committer copies at the close of each write bracket, when a receipt describes a
/// completed write and nothing has yet had a chance to retire it, and re-applies the satisfaction
/// rule against disk itself (`CommitAttribution`).
///
/// Nothing is retired here, which is what makes this safe to call on every bracket: the
/// announcer's consumption still decides what speaks, and the committer's copy still decides what
/// each commit is authored by.
func outstandingEntries() -> [String: HarvestedReceipt] {
receipts.withLock { store in
store.mapValues { HarvestedReceipt(receipt: $0.receipt, isHeal: $0.isHeal) }
}
}
/// Whether the receipt at this path is heal-marked `false` for a path with no receipt at all,
/// which is the same shrug every other read here gives an unknown path.
public func isHeal(at url: URL) -> Bool {