The guide learns the thread it was silent on, and grows a vendor-neutral twin
CLAUDE.md bumps to v14: a new Comments section teaches the comments/ mechanics (chronology-as-ordering, author vs modified-by, .draft/.trash as the app's, retract-by-follow-up), and a new "Use the thread: journal your work" section teaches conduct the guide never had — body is the spec, thread is the journal, post a plan cold-reader-ready on start, decisions as they're made, questions as comments, re-read before resuming, close with verification. comments/ leaves the reserved-tracker-keys list; the Layout diagram gains comments/ and AGENTS.md. The app now writes a byte-identical twin at AGENTS.md, the vendor-neutral name most non-Claude tools read — same lifecycle as CLAUDE.md, decided and healed independently per claimed name, both funneling a markerless foreign file to the single shared CLAUDE.user.md rescue. AgentGuide.install now returns [Displacement]; IntegrityRules.claimedRootNames and BoardStore.refreshAgentGuide cover both names; ChangeNarrator's guide-commit path check widens to both. DESIGN/08-agent-integration.md amends: the agent-guide section gains the Comments and Use-the-thread bullets and an AGENTS.md twin paragraph. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -4793,54 +4793,63 @@ public final class BoardStore: HealHost {
|
||||
|
||||
// MARK: - The agent guide
|
||||
|
||||
/// Brings the board root's `CLAUDE.md` up to the current guide version, or leaves it exactly as
|
||||
/// it is — the whole of 08-agent-integration.md ▸ The agent guide's scheduling. The rule itself
|
||||
/// is `AgentGuide.decide(_:)`, a pure function; this method is the I/O and the policy around it.
|
||||
/// Brings the board root's `CLAUDE.md` **and its `AGENTS.md` twin** up to the current guide
|
||||
/// version, or leaves either exactly as it is — the whole of 08-agent-integration.md ▸ The agent
|
||||
/// guide's scheduling. The rule itself is `AgentGuide.decide(_:)`, a pure function applied
|
||||
/// independently to each of `AgentGuide.targetFilenames`; this method is the I/O and the policy
|
||||
/// around both.
|
||||
///
|
||||
/// **Run on every successful reload**, beside the other scheduled heals, and once more at open
|
||||
/// (`BoardStoreRegistry.acquire`). That makes the guide *self-healing* rather than merely
|
||||
/// written-once: a foreign deletion, a downgrade to an older guide, a board restored from a
|
||||
/// template carrying a stale one — each heals on the next reload, without a single new signal.
|
||||
/// template carrying a stale one — each heals on the next reload, without a single new signal,
|
||||
/// and the two claimed names heal independently of each other (one can be current while the other
|
||||
/// is stale, deleted, or squatted).
|
||||
/// It also pre-wires the Pro-era bounce 06-history-undo.md acknowledges by name, where undoing
|
||||
/// an "Update agent guide (vN)" commit restores an older guide that the app immediately
|
||||
/// re-upgrades.
|
||||
///
|
||||
/// **The steady state is a read and a comparison** — one `lstat`, one small file read, one
|
||||
/// first-line parse — and no write at all. Nothing here touches the snapshot: the bytes land, the
|
||||
/// watcher notices, the reload applies, exactly like every other app write.
|
||||
/// **The steady state is a read and a comparison, twice over** — one `lstat`, one small file read,
|
||||
/// one first-line parse, per claimed name — and no write at all. Nothing here touches the
|
||||
/// snapshot: the bytes land, the watcher notices, the reload applies, exactly like every other
|
||||
/// app write.
|
||||
///
|
||||
/// ### The two skips, and the one displacement
|
||||
/// ### The two skips, and the one displacement — per file
|
||||
///
|
||||
/// `.skipUserFilenameTaken` is the standing exception (a rescue destination is not itself freed
|
||||
/// by a second displacement); `.displaceSquatterThenWrite` is the 2026-07-29 upgrade of the old
|
||||
/// untouchable-skip, and it *does* post — a node of the user's moved aside owes the same
|
||||
/// warning-tone notice `.trash`'s displacement does. Both decisions are re-made inside the write
|
||||
/// half, which is this heal's disk re-verify.
|
||||
/// half, which is this heal's disk re-verify — independently for `CLAUDE.md` and `AGENTS.md`, so
|
||||
/// one file's skip never silences the other's write.
|
||||
///
|
||||
/// ### The signature is the board root's picture
|
||||
/// ### The signature is the board root's picture, both names folded in
|
||||
///
|
||||
/// One failure, one row, then silence until something on disk actually changes — and the memo
|
||||
/// clears on success, which is what lets a foreign deletion of the guide be healed again
|
||||
/// clears on success, which is what lets a foreign deletion of either guide file be healed again
|
||||
/// immediately (the picture "missing" is restored, and a standing memo would make that deletion
|
||||
/// the one thing this could not heal).
|
||||
/// the one thing this could not heal). Each entry is filename-qualified, so a change confined to
|
||||
/// one claimed name — the other untouched — still changes the combined signature.
|
||||
public func refreshAgentGuide() {
|
||||
let root = rootURL
|
||||
let state = AgentGuide.inspect(atBoardRoot: root)
|
||||
let decision = AgentGuide.decide(state)
|
||||
// **A decision that writes nothing is no work at all**, and says so with an empty signature:
|
||||
// the engine's resting-clear then costs no bracket, which matters because a bracket schedules
|
||||
// a reload whether or not anything was written — a skip that opened one would tick forever.
|
||||
let signature: Set<String>
|
||||
switch decision {
|
||||
case .leaveAlone:
|
||||
signature = []
|
||||
case .skipUserFilenameTaken:
|
||||
// The ruling's own outcome (08 ▸ Ownership): a user-authored CLAUDE.md that cannot be
|
||||
// rescued keeps its name, and the guide simply does not exist on this board.
|
||||
Self.logger.debug("agent-guide refresh skipped — CLAUDE.md is not the app's and CLAUDE.user.md is taken")
|
||||
signature = []
|
||||
case .write, .displaceThenWrite, .displaceSquatterThenWrite:
|
||||
signature = [state.signature]
|
||||
// **A decision that writes nothing is no work at all**, and says so by contributing nothing to
|
||||
// the signature: the engine's resting-clear then costs no bracket, which matters because a
|
||||
// bracket schedules a reload whether or not anything was written — a skip that opened one
|
||||
// would tick forever.
|
||||
var signature: Set<String> = []
|
||||
for guideFilename in AgentGuide.targetFilenames {
|
||||
let state = AgentGuide.inspect(atBoardRoot: root, guideFilename: guideFilename)
|
||||
switch AgentGuide.decide(state) {
|
||||
case .leaveAlone:
|
||||
break
|
||||
case .skipUserFilenameTaken:
|
||||
// The ruling's own outcome (08 ▸ Ownership): user-authored content on a claimed guide
|
||||
// name that cannot be rescued keeps its name, and that guide simply does not exist on
|
||||
// this board.
|
||||
Self.logger.debug("agent-guide refresh skipped — \(guideFilename, privacy: .public) is not the app's and \(AgentGuide.userFilename, privacy: .public) is taken")
|
||||
case .write, .displaceThenWrite, .displaceSquatterThenWrite:
|
||||
signature.insert("\(guideFilename):\(state.signature)")
|
||||
}
|
||||
}
|
||||
|
||||
var displaced: [BannerCenter.Displacement] = []
|
||||
@@ -4849,13 +4858,15 @@ public final class BoardStore: HealHost {
|
||||
signature: signature,
|
||||
on: self
|
||||
) { () throws(BoardWriteError) -> Void in
|
||||
// One bracket over the displacement *and* the write: two files change, one app-mediated
|
||||
// reload lands, and (under Pro) one honestly-attributed commit records it.
|
||||
guard let moved = try AgentGuide.install(atBoardRoot: root) else { return }
|
||||
// The `CLAUDE.user.md` rescue is the settled, silent ownership rule; a squatter's
|
||||
// displacement is announced.
|
||||
guard !moved.wasUserContent else { return }
|
||||
displaced.append(BannerCenter.Displacement(name: moved.name, movedTo: moved.movedTo))
|
||||
// One bracket over every displacement *and* write across both claimed names: whatever
|
||||
// changes, one app-mediated reload lands, and (under Pro) one honestly-attributed commit
|
||||
// records it.
|
||||
for moved in try AgentGuide.install(atBoardRoot: root) {
|
||||
// The `CLAUDE.user.md` rescue is the settled, silent ownership rule; a squatter's
|
||||
// displacement is announced.
|
||||
guard !moved.wasUserContent else { continue }
|
||||
displaced.append(BannerCenter.Displacement(name: moved.name, movedTo: moved.movedTo))
|
||||
}
|
||||
} posting: {
|
||||
.displacedClaimedNames(displaced)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user