An unopenable repository fails loudly — the standing row, the paused surface, the honest heal

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-06 18:44:12 -04:00
parent 52df210284
commit 0d8ecdb78b
20 changed files with 938 additions and 42 deletions
+52 -5
View File
@@ -194,6 +194,22 @@ public final class GitAutoCommitter {
@ObservationIgnored
public var reportRecovery: (@MainActor () -> Void)?
/// **The repository became unreadable, or readable again** the standing breakage banner's
/// raise and heal (06-history-undo.md Rules, the corrupt-`.git` loud failure, ruled
/// 2026-07-31: "a standing breakage-class banner at detection the banner clears when a later
/// open or reload finds the repo readable").
///
/// Called on the *transition only*, with the new answer so a board that stands unreadable for
/// an hour posts one row rather than one per 15 s re-read, and a repository repaired in a
/// terminal heals the row on the first re-read that opens it.
///
/// Separate from `reportFailure` because the two conditions are different rows saying different
/// things: a failed commit is "history stopped advancing, here is the error" (a retry away),
/// while this is "there is no repository the app can read at all". Wired by
/// `AppModel.beginSession` to `BoardStore.noteRepositoryUnreadable(_:)`.
@ObservationIgnored
public var reportRepositoryUnreadable: (@MainActor (Bool) -> Void)?
/// **What a flush landed, and which of it was heal work** the undo stack's in-session ear
/// (06-history-undo.md Rules The stack is HEAD's first-parent ancestry, live; Heal commits
/// are transparent to undo, in-session).
@@ -459,9 +475,36 @@ public final class GitAutoCommitter {
/// and it read it in order to write.
public func refreshPause() async {
let root = boardRoot
pause = await Task.detached(priority: .userInitiated) {
let read = await Task.detached(priority: .userInitiated) {
GitCommitOperation.reading(at: root).pause
}.value
setPause(read)
}
/// **The detection-time probe's answer, seeded before anything has been attempted**
/// (06-history-undo.md Rules, the corrupt-`.git` loud failure: "a standing breakage-class
/// banner **at detection**").
///
/// The engine's ordinary way of learning a pause is to try to commit and be held, which is the
/// right cadence for committing and far too late for this one: the ruling's whole point is that
/// the failure is loud at the open rather than discovered a debounce later or, worse, only in
/// the popover. `HistoryStore`'s composition probes (`GitRepository.canOpen`) and calls this.
///
/// It is deliberately the *same* state a held flush would have reached, not a parallel flag: one
/// pause, one surface, and the first re-read either confirms it or heals it.
public func noteRepositoryUnreadable() {
setPause(.unreadable)
}
/// The one place `pause` is assigned, so the raise-and-heal seam cannot be forgotten by a path
/// that sets it (`reportRepositoryUnreadable`). Fires on the transition only entering
/// `.unreadable` from anything else, or leaving it for anything else, `nil` included.
private func setPause(_ new: GitRepositoryPause?) {
let was = pause == .unreadable
pause = new
let now = new == .unreadable
guard was != now else { return }
reportRepositoryUnreadable?(now)
}
/// Whether a card window's folder is currently staged around the stage-around rule, made
@@ -881,7 +924,7 @@ public final class GitAutoCommitter {
switch outcome {
case let .committed(landed):
let oids = landed.map(\.oid)
pause = nil
setPause(nil)
lastFailure = nil
commitCount += oids.count
lastCommitOIDs = oids
@@ -899,7 +942,7 @@ public final class GitAutoCommitter {
case .nothingToCommit:
// **The happy path, not a malfunction** (06): an agent committed its own work, or the
// whole window was staged around. Silent, and the window closes either way.
pause = nil
setPause(nil)
lastFailure = nil
dropHarvestOutsideOpenSessions()
holdsForeignChanges = false
@@ -913,12 +956,16 @@ public final class GitAutoCommitter {
arm()
case let .held(reason):
pause = reason
setPause(reason)
Self.logger.notice("auto-commit held: \(reason.rawValue, privacy: .public)")
// **The standing pause's own re-read** (06 Rules Abnormal repo states, blessed
// 2026-07-31) and the mid-session healing path for the unreadable repository too: the
// watcher never delivers `.git`, so a repository repaired in a terminal has no other way
// to be noticed before the next open.
arm(after: holdRecheckInterval)
case let .failed(failure):
pause = nil
setPause(nil)
lastFailure = failure
Self.logger.error("auto-commit failed: \(failure.description, privacy: .public)")
reportFailure?(failure)