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:
2026-08-09 08:38:07 -04:00
parent 0fe92bdf38
commit 0c4aad01a0
9 changed files with 488 additions and 101 deletions
+46 -35
View File
@@ -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)
}