The embedded guide catches up on its own — creation-time parity for both apps, an open-time refresh for the phone
The card's frozen spec recommended Option C (install the agent guide at board creation); the owner's follow-up comment extended that ruling to a second axis — embedded guidelines should update whenever a board opens if the on-disk version is older than Lanework's, which the Mac app already does via BoardStore.refreshAgentGuide()/runScheduledHeals(). This card implements both halves. - BoardWriter.createBoard now calls AgentGuide.install(atBoardRoot:) right after seedGitignoreIfAbsent, so every board — Mac- or phone-created, since KanbanMobile.BoardIndexStore.createBoard calls this same method — is born with a current-version CLAUDE.md, with no dependency on a later open. Routed through AgentGuide.install itself rather than a hand-rolled write, so never-downgrade, the CLAUDE.user.md rescue, squatter displacement, and the EchoLedger heal-attribution exclusion all carry over unchanged. - BoardSession (KanbanMobile) gains a private refreshAgentGuideOnce(), fired once from open() (already idempotent on the .idle phase), fire-and-forget through the same CoordinatedFileAccess.write bracket every phone write uses. Deliberately not a heal scheduler — a one-shot courtesy check at session open, silent on failure (logged, never surfaced to lastError or a banner), matching AgentGuide's own "nothing here is a user-facing event" posture. The type's doc comment now names this one exception while keeping "no heal scheduler" true. - project.yml: lifted the KanbanMobile target's AgentGuide.swift build exclusion (dating to the original mobile MVP, "agents work where the Mac app runs") — both changes above fail to compile on the phone without it, since the type simply wasn't in that module. Verified safe: AgentGuide.swift imports only Foundation, and its one upward dependency touches only EchoLedger's unconditional recording API, never the #if os(macOS)-gated consumer surfaces. Tests: KanbanTests/BoardWriterTests.swift gains createBoardInstallsTheCurrentAgentGuide, calling createBoard directly and asserting the guide lands at AgentGuide.version immediately — the card's own Done-when, and also the phone's creation-time coverage since it's the same call site. KanbanMobileUITests/AgentGuideUITests.swift covers the open-time refresh itself, the one piece only reachable end-to-end from a running KanbanMobile process (no mobile unit-test target exists): the bundle's fixture board already carries no CLAUDE.md, so tapping into it and polling disk proves the wiring with no fixture changes needed. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -9,6 +9,16 @@ import os
|
||||
/// snapshot on screen — and none of its machinery: no FSEvents, no echo verdicts, no heal scheduler,
|
||||
/// no git. The change signal here is the container's metadata query, relayed by `BoardIndexStore`.
|
||||
///
|
||||
/// **One narrow exception**: `open()` fires a single, one-shot `AgentGuide.install(atBoardRoot:)`
|
||||
/// alongside the first walk (`refreshAgentGuideOnce()`) — never repeated on `reload()` or
|
||||
/// `containerDidUpdate()`. This is the owner's 2026-08-09 ruling extending the Mac's open/reload
|
||||
/// guide refresh to the phone, answered as narrowly as that ruling allows: a board still has *no*
|
||||
/// heal scheduler here (nothing re-checks the guide on every foreign change the way
|
||||
/// `BoardStore.runScheduledHeals()` does), only a courtesy check the moment a session is opened.
|
||||
/// `BoardWriter.createBoard` (both platforms' only creation path) already installs a current guide
|
||||
/// at birth, so this exists for the boards that predate that guarantee or were last touched by an
|
||||
/// older build.
|
||||
///
|
||||
/// Minted and cached by `BoardIndexStore.session(forBoardAt:)`, never constructed directly by a
|
||||
/// screen: two screens looking at one board must share one snapshot.
|
||||
@MainActor
|
||||
@@ -96,6 +106,7 @@ final class BoardSession {
|
||||
/// Starts the first walk. Idempotent — a screen may call it on every appearance.
|
||||
func open() {
|
||||
guard case .idle = phase else { return }
|
||||
refreshAgentGuideOnce()
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -220,6 +231,57 @@ final class BoardSession {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The agent guide
|
||||
|
||||
/// The type doc's "one narrow exception": a single `AgentGuide.install(atBoardRoot:)` per
|
||||
/// session, fired from `open()` and never again — not a heal, not a scheduler, just the phone's
|
||||
/// answer to "should an already-old board catch up the moment somebody opens it".
|
||||
///
|
||||
/// **Fire-and-forget, deliberately not awaited by `open()`.** The guide is a courtesy an agent
|
||||
/// reads later, not something the board screen's first paint depends on, so it runs alongside
|
||||
/// `reload()` rather than gating it — the two detached tasks race, and neither waits on the
|
||||
/// other. It still goes through the same `CoordinatedFileAccess.write` bracket `perform(_:)`
|
||||
/// uses, because the ubiquity daemon is as much a second writer here as it is for any other
|
||||
/// phone write (`CoordinatedFileAccess`'s own doc comment).
|
||||
///
|
||||
/// **Silent, on `AgentGuide`'s own reasoning**: "nothing here is a user-facing event." A
|
||||
/// coordination refusal or a genuine `BoardWriteError` is logged and dropped — never written to
|
||||
/// `lastError` — because that property means *this session's own write failed*, and a courtesy
|
||||
/// guide refresh racing the daemon on session open is not the write a screen showing a spinner or
|
||||
/// a stale-snapshot notice is asking about. `AgentGuide.install` already re-verifies against disk
|
||||
/// before writing anything, so losing a race to a foreign fix — another device's session, an
|
||||
/// agent — is success, not a failure this call ever sees.
|
||||
private func refreshAgentGuideOnce() {
|
||||
let root = rootURL
|
||||
// `Task { }`, not a bare detached call: the I/O itself still runs off the main actor
|
||||
// (`Task.detached`, `perform(_:)`'s own shape), but logging a failure needs `Self.logger`,
|
||||
// which is main-actor-isolated because this whole type is — so the outer task hops back
|
||||
// after `.value` the same way `perform(_:)` does, instead of touching it from inside the
|
||||
// detached closure.
|
||||
Task {
|
||||
let outcome: Result<Void, BoardSessionError> = await Task.detached(priority: .utility) {
|
||||
let coordinated = CoordinatedFileAccess.write(itemAt: root) { resolved -> Result<Void, BoardWriteError> in
|
||||
do throws(BoardWriteError) {
|
||||
try AgentGuide.install(atBoardRoot: resolved)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
switch coordinated {
|
||||
case let .failure(failure):
|
||||
return .failure(.coordination(failure))
|
||||
case let .success(inner):
|
||||
return inner.mapError(BoardSessionError.write)
|
||||
}
|
||||
}.value
|
||||
|
||||
if case let .failure(failure) = outcome {
|
||||
Self.logger.error("agent guide refresh failed: \(failure.description, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The comment thread
|
||||
|
||||
/// Reads one card's comment thread — the phone's counterpart to the Mac card window reading
|
||||
|
||||
Reference in New Issue
Block a user