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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user