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:
+189
-48
@@ -38,16 +38,39 @@ import Foundation
|
||||
/// `BoardStore.performWrite` posts every `BoardWriteError` it sees. The scheduling — when this is
|
||||
/// consulted, and why it is safe to consult on every reload — lives at
|
||||
/// `BoardStore.refreshAgentGuide()`.
|
||||
///
|
||||
/// **`CLAUDE.md` has a twin: `AGENTS.md`** (ruled 2026-08-09, card dc1314bb) — byte-for-byte the same
|
||||
/// guide, at a second board-root name, written and upgraded in lockstep. Every promise above —
|
||||
/// never-downgrade, the markerless-content rescue, squatter displacement — applies to each name
|
||||
/// independently; `install(atBoardRoot:)` is simply called once per name. See `agentsFilename` and
|
||||
/// `targetFilenames` below for why a duplicate file rather than a symlink, and `install`'s doc for how
|
||||
/// one call keeps both in lockstep without two bodies of prose to let drift.
|
||||
enum AgentGuide {
|
||||
|
||||
// MARK: - The two claimed names
|
||||
// MARK: - The claimed names
|
||||
|
||||
/// The app-owned guide, and one of the board-root names the loader already claims
|
||||
/// (`BoardLoader.reservedRootNames`) so that neither file is ever read as a stray.
|
||||
static let filename = "CLAUDE.md"
|
||||
|
||||
/// **The `AGENTS.md` twin** (08-agent-integration.md ▸ The agent guide; ruled 2026-08-09, card
|
||||
/// dc1314bb) — byte-for-byte the same content as `filename`, written and upgraded in lockstep by
|
||||
/// the same `install(atBoardRoot:)`. The vendor-neutral name most non-Claude tools read; `CLAUDE.md`
|
||||
/// stays for Claude's own pickup. **Deliberately a duplicate, never a symlink**: iCloud Drive's
|
||||
/// symlink handling is unreliable, and rewriting both from the one literal on every install makes
|
||||
/// drift impossible by construction — there is no second body of prose to let drift.
|
||||
static let agentsFilename = "AGENTS.md"
|
||||
|
||||
/// The two guide filenames `install(atBoardRoot:)` maintains, in the order it processes them —
|
||||
/// `filename` first, so that a simultaneous foreign claim on both funnels its one rescue through
|
||||
/// `filename`'s decision before `agentsFilename`'s is even inspected (see `install`'s doc).
|
||||
static let targetFilenames: [String] = [filename, agentsFilename]
|
||||
|
||||
/// The user's extension point (08 ▸ `CLAUDE.user.md`) — and the rescue destination for a
|
||||
/// markerless `CLAUDE.md`. The app writes this name exactly once per board, if ever.
|
||||
/// markerless `CLAUDE.md` **or** a markerless `AGENTS.md`. The app writes this name at most once
|
||||
/// per board: a single shared destination for "content that was somebody's before either claimed
|
||||
/// name was the app's," never a second `AGENTS.user.md` — the rescue is conceptually the user's
|
||||
/// guide content, singular, regardless of which claimed name it was sitting on.
|
||||
static let userFilename = "CLAUDE.user.md"
|
||||
|
||||
/// The guide the app ships. **v4 was the pathfinder's**, and real boards carry it; v5 is the
|
||||
@@ -101,8 +124,27 @@ enum AgentGuide {
|
||||
/// an agent writing the key *is* how a hero image gets set. One clause beside the other card keys
|
||||
/// in Frontmatter, spelling the grammar the reading enforces — a bare filename, never a path —
|
||||
/// because a path is exactly what an agent that has just written `` into a
|
||||
/// body will reach for.
|
||||
static let version = 13
|
||||
/// body will reach for. **v14 teaches the comment thread — mechanics and conduct — and ships the
|
||||
/// `AGENTS.md` twin** (08-agent-integration.md ▸ The agent guide, ▸ Use the thread; 01-storage-
|
||||
/// format.md § Enhanced schema; filed 2026-08-08 off a live session that journaled an
|
||||
/// implementation card by deriving every comments rule from 01 by hand, because the guide never
|
||||
/// taught them). Comments shipped after v11 rewrote Git for the excision and left `comments/` on
|
||||
/// the reserved-keys list — telling agents not to use a surface the app had since built: the
|
||||
/// thread column, `.draft`, and `comments/.trash/` are all live. Two new sections close the gap.
|
||||
/// **Comments** teaches the mechanics — the folder shape, chronology as the ordering, `author`
|
||||
/// surviving app writes where `modified-by` doesn't, `.draft` and `.trash` as the app's own,
|
||||
/// retraction by follow-up rather than rewrite, `modified` differing from `created` as the whole
|
||||
/// of "edited." **Use the thread: journal your work** teaches the conduct the mechanics section
|
||||
/// never could: the card body is the spec, the thread is the journal, and a cold reader — the
|
||||
/// user checking in later, another session picking the card back up — should be able to
|
||||
/// reconstruct the plan and every decision from the thread alone; per-board process stays
|
||||
/// `CLAUDE.user.md`'s. `comments/` itself leaves the reserved-keys line, which now names only the
|
||||
/// tracker-integration keys still unclaimed. Separately, the app now writes a second guide file,
|
||||
/// `AGENTS.md`, byte-for-byte identical to this one and carrying the same marker — the
|
||||
/// vendor-neutral filename most non-Claude tools read, kept a deliberate duplicate rather than a
|
||||
/// symlink (iCloud Drive's unreliable symlink handling), rewritten in lockstep by the same
|
||||
/// `install(atBoardRoot:)` so drift is impossible by construction rather than by discipline.
|
||||
static let version = 14
|
||||
|
||||
// MARK: - The version marker
|
||||
|
||||
@@ -125,8 +167,8 @@ enum AgentGuide {
|
||||
|
||||
// MARK: - The decision
|
||||
|
||||
/// What is sitting on `CLAUDE.md`, as the filesystem answers it — no policy, so the rule below
|
||||
/// can be a pure function of it.
|
||||
/// What is sitting on one claimed guide name (`CLAUDE.md` or `AGENTS.md`), as the filesystem
|
||||
/// answers it — no policy, so the rule below can be a pure function of it.
|
||||
enum Existing: Equatable {
|
||||
/// Nothing at that path (or nothing this process can read there — the same non-event).
|
||||
case missing
|
||||
@@ -149,7 +191,8 @@ enum AgentGuide {
|
||||
case squatted
|
||||
}
|
||||
|
||||
/// The board root's two claimed names, read once — the input to `decide(_:)`.
|
||||
/// One claimed guide name's picture, plus the shared rescue name's freedom — the input to
|
||||
/// `decide(_:)`. `inspect(atBoardRoot:guideFilename:)` builds one of these per guide filename.
|
||||
struct State: Equatable {
|
||||
var existing: Existing
|
||||
|
||||
@@ -221,17 +264,22 @@ enum AgentGuide {
|
||||
|
||||
// MARK: - Reading the board root
|
||||
|
||||
/// Reads the state of the two claimed names. Purely a read — it creates nothing, and it is
|
||||
/// cheap enough (one `lstat`, plus a small file read only when there is a file to read) to run
|
||||
/// on every reload.
|
||||
/// Reads the state of one guide filename (`filename` or `agentsFilename`) plus the shared rescue
|
||||
/// name's freedom. Purely a read — it creates nothing, and it is cheap enough (one `lstat`, plus a
|
||||
/// small file read only when there is a file to read) to run on every reload, for both names.
|
||||
///
|
||||
/// **A live disk read, not a memoized one** — called once per guide filename, sequentially, inside
|
||||
/// `install`, so a rescue the first call just performed (`filename` moved to `userFilename`) is
|
||||
/// reflected in `userFilenameIsFree` by the time the second call inspects `agentsFilename`, with no
|
||||
/// stale picture to reconcile.
|
||||
///
|
||||
/// **`lstat` semantics throughout, never `fileExists`** (`IntegrityRules.node(at:)`): a
|
||||
/// **dangling** symlink is a node that is *there* — it holds the name, and it is displaced as a
|
||||
/// link rather than followed — while `fileExists` follows the link, finds nothing, and would
|
||||
/// call the name free.
|
||||
static func inspect(atBoardRoot root: URL) -> State {
|
||||
static func inspect(atBoardRoot root: URL, guideFilename: String) -> State {
|
||||
State(
|
||||
existing: existingNode(at: root.appendingPathComponent(filename)),
|
||||
existing: existingNode(at: root.appendingPathComponent(guideFilename)),
|
||||
userFilenameIsFree: IntegrityRules.node(at: root.appendingPathComponent(userFilename)) == nil
|
||||
)
|
||||
}
|
||||
@@ -249,30 +297,56 @@ enum AgentGuide {
|
||||
|
||||
// MARK: - Writing it
|
||||
|
||||
/// Puts the current guide at the board root, first moving a displaced `CLAUDE.md` out of the way
|
||||
/// when the decision called for it.
|
||||
/// Puts the current guide at the board root — **at both claimed names** (ruled 2026-08-09,
|
||||
/// card dc1314bb) — first moving a displaced file out of the way wherever a decision calls for it.
|
||||
///
|
||||
/// **Called inside `BoardStore.performWrite`**, so both halves ride one watcher bracket: the
|
||||
/// rescue and the guide land as a single app-mediated reload rather than as two foreign-looking
|
||||
/// events.
|
||||
/// **One call, up to two independent decisions, one identical body.** `targetFilenames` is
|
||||
/// processed in order, `filename` then `agentsFilename`: each gets its own `inspect` (a fresh disk
|
||||
/// read) and its own `decide`, exactly as if the other name did not exist, and each that needs a
|
||||
/// write gets the same `content` — the twin promise is upheld by writing one literal to two paths,
|
||||
/// never by copying one file onto the other. **The sequencing is what makes the shared rescue name
|
||||
/// safe**: if both `CLAUDE.md` and `AGENTS.md` are markerless simultaneously, `filename`'s rescue
|
||||
/// to `userFilename` runs first and actually lands on disk before `agentsFilename` is even
|
||||
/// inspected, so its `decide` sees `userFilename` correctly as taken and falls to
|
||||
/// `.skipUserFilenameTaken` — never a lost race, because there is no concurrency here to race.
|
||||
///
|
||||
/// The move is `FileManager.moveItem` and nothing else: it preserves the bytes exactly — the
|
||||
/// displaced file may not even be UTF-8 — and it **fails rather than overwrite** if
|
||||
/// `CLAUDE.user.md` appeared between the decision and this call, which is what makes the
|
||||
/// "user content is never destroyed" promise hold against a race rather than merely against a
|
||||
/// stale read.
|
||||
/// **Called inside `BoardStore.performWrite`**, so every half rides one watcher bracket: up to two
|
||||
/// rescues and up to two guide writes land as a single app-mediated reload rather than as four
|
||||
/// foreign-looking events.
|
||||
///
|
||||
/// **It re-verifies against disk** (01-storage-format.md § Validation and healing: "every
|
||||
/// scheduled heal re-verifies its defect against disk at write time and no-ops when it is
|
||||
/// gone"): the board root is re-inspected here, and a guide that has become current since the
|
||||
/// decision — an agent wrote it, another window healed it first — returns `nil` rather than
|
||||
/// rewriting a file that no longer needs it. Losing the race to a foreign fix is success.
|
||||
/// Each move is `FileManager.moveItem` and nothing else: it preserves the bytes exactly — the
|
||||
/// displaced file may not even be UTF-8 — and it **fails rather than overwrite** if `userFilename`
|
||||
/// appeared between that file's decision and this call, which is what makes the "user content is
|
||||
/// never destroyed" promise hold against a race rather than merely against a stale read.
|
||||
///
|
||||
/// - Returns: what this call displaced, or `nil` when it wrote nothing at all.
|
||||
/// **Each write re-verifies against disk** (01-storage-format.md § Validation and healing: "every
|
||||
/// scheduled heal re-verifies its defect against disk at write time and no-ops when it is gone"):
|
||||
/// the board root is re-inspected per file, and a guide that has become current since its decision
|
||||
/// — an agent wrote it, another window healed it first — contributes nothing to the result rather
|
||||
/// than rewriting a file that no longer needs it. Losing the race to a foreign fix is success, and
|
||||
/// the two files' races are independent: one can win while the other loses.
|
||||
///
|
||||
/// - Returns: what this call displaced, one entry per file that needed one — empty when it wrote
|
||||
/// nothing at all.
|
||||
@discardableResult
|
||||
static func install(atBoardRoot root: URL) throws(BoardWriteError) -> Displacement? {
|
||||
let guideURL = root.appendingPathComponent(filename)
|
||||
let decision = decide(inspect(atBoardRoot: root))
|
||||
static func install(atBoardRoot root: URL) throws(BoardWriteError) -> [Displacement] {
|
||||
var displacements: [Displacement] = []
|
||||
for guideFilename in targetFilenames {
|
||||
if let displaced = try installOne(guideFilename: guideFilename, atBoardRoot: root) {
|
||||
displacements.append(displaced)
|
||||
}
|
||||
}
|
||||
return displacements
|
||||
}
|
||||
|
||||
/// One claimed name's whole install — `install(atBoardRoot:)`'s per-file body, factored out so the
|
||||
/// loop above is the only place that knows there are two.
|
||||
private static func installOne(
|
||||
guideFilename: String,
|
||||
atBoardRoot root: URL
|
||||
) throws(BoardWriteError) -> Displacement? {
|
||||
let guideURL = root.appendingPathComponent(guideFilename)
|
||||
let decision = decide(inspect(atBoardRoot: root, guideFilename: guideFilename))
|
||||
var displaced: Displacement?
|
||||
|
||||
switch decision {
|
||||
@@ -283,25 +357,25 @@ enum AgentGuide {
|
||||
case .write:
|
||||
break
|
||||
case .displaceThenWrite:
|
||||
// The rescue, not a squatter: a markerless `CLAUDE.md` is user *content*, and it has a
|
||||
// designated destination (08-agent-integration.md ▸ Ownership).
|
||||
// The rescue, not a squatter: markerless content on a claimed name is user *content*, and
|
||||
// it has a designated destination (08-agent-integration.md ▸ Ownership).
|
||||
do {
|
||||
try FileManager.default.moveItem(at: guideURL, to: root.appendingPathComponent(userFilename))
|
||||
} catch {
|
||||
throw BoardWriteError(
|
||||
operation: .agentGuide,
|
||||
path: guideURL.path,
|
||||
reason: .io(message: "could not move the existing \(filename) aside to \(userFilename): \(error.localizedDescription)")
|
||||
reason: .io(message: "could not move the existing \(guideFilename) aside to \(userFilename): \(error.localizedDescription)")
|
||||
)
|
||||
}
|
||||
EchoLedger.current?.recordMove(from: guideURL, to: root.appendingPathComponent(userFilename))
|
||||
displaced = Displacement(name: filename, movedTo: userFilename, wasUserContent: true)
|
||||
displaced = Displacement(name: guideFilename, movedTo: userFilename, wasUserContent: true)
|
||||
case .displaceSquatterThenWrite:
|
||||
// The claimed-name displacement (ruled 2026-07-29): a folder or symlink on the app's own
|
||||
// name, moved aside by the Finder ladder and never destroyed.
|
||||
guard let freed = try BoardWriter.displaceClaimedName(
|
||||
ClaimedNameSquatter(
|
||||
name: filename,
|
||||
name: guideFilename,
|
||||
found: IntegrityRules.node(at: guideURL) ?? .directory,
|
||||
expected: .file
|
||||
),
|
||||
@@ -311,7 +385,7 @@ enum AgentGuide {
|
||||
// anyway. Nothing displaced, nothing written.
|
||||
return nil
|
||||
}
|
||||
displaced = Displacement(name: filename, movedTo: freed, wasUserContent: false)
|
||||
displaced = Displacement(name: guideFilename, movedTo: freed, wasUserContent: false)
|
||||
}
|
||||
|
||||
try BoardWriter.atomicReplace(text: content, at: guideURL, operation: .agentGuide)
|
||||
@@ -327,14 +401,16 @@ enum AgentGuide {
|
||||
/// owns is now under a different name — and only the tone differs: the `CLAUDE.user.md` rescue
|
||||
/// is the settled, silent ownership rule (08-agent-integration.md), while a squatter's
|
||||
/// displacement gets the relocation-style warning-tone notice (01-storage-format.md § Fractal
|
||||
/// layout ▸ Rules, ruled 2026-07-29).
|
||||
/// layout ▸ Rules, ruled 2026-07-29). `name` names whichever claimed name — `CLAUDE.md` or
|
||||
/// `AGENTS.md` — this particular displacement was about; a call to `install` that touches both can
|
||||
/// produce two of these, one per name.
|
||||
struct Displacement: Sendable, Equatable {
|
||||
/// The claimed name that was freed.
|
||||
let name: String
|
||||
/// The name the displaced node now has.
|
||||
let movedTo: String
|
||||
/// Whether this was the markerless-`CLAUDE.md` rescue (silent) rather than a squatter's
|
||||
/// displacement (announced).
|
||||
/// Whether this was the markerless-guide rescue (silent) rather than a squatter's displacement
|
||||
/// (announced).
|
||||
let wasUserContent: Bool
|
||||
}
|
||||
|
||||
@@ -352,7 +428,7 @@ enum AgentGuide {
|
||||
/// The marker interpolates `version` rather than spelling the number twice: the constant and the
|
||||
/// first line cannot drift apart, and a bump is one edit.
|
||||
private static let guideBody = """
|
||||
<!-- lanework-agent-guide v\(version) — created and kept up to date by the Lanework app. Don't edit this file: it is overwritten on upgrades. Board-specific instructions live in CLAUDE.user.md (see below), which the app never touches. -->
|
||||
<!-- lanework-agent-guide v\(version) — created and kept up to date by the Lanework app. This guide is written at two names, CLAUDE.md and AGENTS.md, kept byte-identical. Don't edit either file: both are overwritten on upgrades. Board-specific instructions live in CLAUDE.user.md (see below), which the app never touches. -->
|
||||
|
||||
# This folder is a Lanework kanban board
|
||||
|
||||
@@ -370,12 +446,14 @@ enum AgentGuide {
|
||||
<board>/ this folder (the board)
|
||||
├── index.md board title + settings; body = board description
|
||||
├── CLAUDE.md this guide (app-maintained)
|
||||
├── AGENTS.md byte-identical twin of CLAUDE.md (app-maintained)
|
||||
├── .trash/ deleted cards and lanes (app-managed — see Deleting)
|
||||
├── <uuid>/ a LANE
|
||||
│ ├── index.md lane title + order; body = lane notes/policy
|
||||
│ ├── <uuid>/ a CARD
|
||||
│ │ ├── index.md card title + order; body = the card's content
|
||||
│ │ └── attachments/ the card's files (flat, top-level)
|
||||
│ │ ├── attachments/ the card's files (flat, top-level)
|
||||
│ │ └── comments/ the card's comment thread (see Comments)
|
||||
│ └── <uuid>/ another card
|
||||
└── <uuid>/ another lane
|
||||
```
|
||||
@@ -444,8 +522,9 @@ enum AgentGuide {
|
||||
custom metadata (`project:`, `tags:`, `claimed-by:` …) is safe to add and
|
||||
survives every app rewrite. Reserved for Lanework's upcoming tracker sync —
|
||||
preserved but not rendered, don't repurpose them: the card keys `labels`,
|
||||
`assignees`, `due`, the `remote` key (cards and board), `remote-state`
|
||||
(lanes), and a card-level `comments/` folder.
|
||||
`assignees`, `due`, the `remote` key (cards and board), and `remote-state`
|
||||
(lanes). `comments/` is **not** on this list — it's a shipped feature with
|
||||
its own section below, not a reserved name.
|
||||
|
||||
## Stamping your work: `modified-by`
|
||||
|
||||
@@ -570,6 +649,37 @@ enum AgentGuide {
|
||||
- Subfolders under `attachments/` are tolerated but the app never creates
|
||||
or lists them — keep attachments top-level.
|
||||
|
||||
## Comments
|
||||
|
||||
Every card has a comment thread at `comments/` inside the card folder —
|
||||
a lightweight, chronological log beside the card itself. The card body
|
||||
is the spec; the thread is where work on it gets journaled (see Use the
|
||||
thread below).
|
||||
|
||||
- A comment is `comments/<lowercase-uuid>/index.md`: frontmatter
|
||||
`schema: 1`, `kind: comment`, `author`, `created`, `modified`. **No
|
||||
`title`, no `order`.** A comment may carry its own `attachments/`,
|
||||
one level down from the card's — the same rules as Attachments above
|
||||
apply there too.
|
||||
- **Chronology is the ordering**: the thread sorts by `created`
|
||||
ascending, so the stamp *is* the position. Write real current UTC
|
||||
(`date -u +%FT%TZ`); give a burst of several comments distinct
|
||||
seconds — ties fall back to folder-name order.
|
||||
- **`author` is self-reported and survives app writes** — the
|
||||
deliberate contrast with `modified-by` above, which the app clears on
|
||||
every write of its own. Write your name into `author` once; it
|
||||
sticks through every later app-mediated rewrite of the card or board.
|
||||
- **`comments/.draft` and `comments/.trash` are the app's** — the
|
||||
unposted composer draft and the undo backing store. Never write into
|
||||
either, and never "delete" a comment by moving it there yourself:
|
||||
content no live undo step owns is swept as residue at the next
|
||||
window open. To retract a comment, post a follow-up saying so — never
|
||||
rewrite or remove an existing one.
|
||||
- A comment counts as **edited** when `modified` differs from
|
||||
`created` — that's the whole rule, no separate flag. Fixing a typo
|
||||
is fine; once the conversation has moved past something you wrote,
|
||||
post a follow-up instead of rewriting it.
|
||||
|
||||
## Hard rules (the app fails loudly on violations)
|
||||
|
||||
- Frontmatter must parse as YAML. The board's own `index.md` must carry
|
||||
@@ -611,9 +721,40 @@ enum AgentGuide {
|
||||
- **Commit your own changes, with clear messages** — nothing else will
|
||||
commit them for you, and a semantic message ("Move card 'Fix login' to
|
||||
Doing") is the history the user will actually read.
|
||||
- **Leave the app-maintained files to the app** — this guide and the
|
||||
seeded `.gitignore` are rewritten by Lanework when they need to be;
|
||||
don't edit or delete them, and don't commit changes to the user's other
|
||||
files that you didn't make.
|
||||
- **Leave the app-maintained files to the app** — this guide (both
|
||||
`CLAUDE.md` and its `AGENTS.md` twin) and the seeded `.gitignore` are
|
||||
rewritten by Lanework when they need to be; don't edit or delete
|
||||
them, and don't commit changes to the user's other files that you
|
||||
didn't make.
|
||||
|
||||
## Use the thread: journal your work
|
||||
|
||||
The card's body is the spec; its comment thread (see Comments above)
|
||||
is the journal. Edit the body when scope, constraints, or done-when
|
||||
change. Everything else — progress, in-the-moment thinking, questions —
|
||||
belongs in the thread.
|
||||
|
||||
- **Starting work on a card**: post a comment with your plan and every
|
||||
decision already made, rejected alternatives included, written for a
|
||||
reader with none of your context — the user checking in later,
|
||||
another agent, or your own future session picking the card back up
|
||||
cold.
|
||||
- **While working**: record decisions as you make them, not
|
||||
reconstructed afterward. Outcomes live elsewhere — commits, diffs —
|
||||
so the thread's job is the *why*, the routes you didn't take, and
|
||||
any limitation you knowingly accepted.
|
||||
- **Questions**: post them as comments. The app narrates arrivals by
|
||||
path shape ("Comment on '⟨card⟩'"), so a posted question genuinely
|
||||
reaches the user live. Answers come back as later comments —
|
||||
**re-read the whole thread before resuming any card**, not just the
|
||||
last entry.
|
||||
- **Finishing**: close with verification evidence — what you ran, what
|
||||
it showed — and name anything you did differently from what the
|
||||
card asked.
|
||||
- **Comments never stamp the card**: posting one leaves the card's own
|
||||
`modified` and `modified-by` alone, the same as the app's own posts.
|
||||
|
||||
Per-board process — lane cadence, commit conventions, anything specific
|
||||
to this board — belongs in `CLAUDE.user.md`, not here.
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -215,9 +215,15 @@ public enum IntegrityRules: Sendable {
|
||||
/// `CLAUDE.md`'s squatter is displaced by the **agent guide's** own heal, which already owns
|
||||
/// that file's whole decision (`AgentGuide.Decision.displaceSquatterThenWrite`); `.trash`'s is
|
||||
/// its own scheduled heal, because nothing else ever writes that name.
|
||||
///
|
||||
/// **`AGENTS.md` joined 2026-08-09** (card dc1314bb, 08-agent-integration.md ▸ The agent guide):
|
||||
/// the byte-identical twin of `CLAUDE.md`, claimed and displaced on exactly the same terms — its
|
||||
/// squatter is the agent guide's own heal too (`AgentGuide.install` runs the same decision against
|
||||
/// both names), so it is not answered by `squattedClaimedName(atBoardRoot:)` below either.
|
||||
public static let claimedRootNames: [ClaimedName] = [
|
||||
ClaimedName(name: trashFolderName, expected: .directory, displacesSquatters: true),
|
||||
ClaimedName(name: "CLAUDE.md", expected: .file, displacesSquatters: true),
|
||||
ClaimedName(name: "AGENTS.md", expected: .file, displacesSquatters: true),
|
||||
ClaimedName(name: "CLAUDE.user.md", expected: .file, displacesSquatters: false),
|
||||
ClaimedName(name: gitignoreFileName, expected: .file, displacesSquatters: false),
|
||||
]
|
||||
@@ -291,9 +297,9 @@ public enum IntegrityRules: Sendable {
|
||||
/// (01-storage-format.md § Fractal layout ▸ Rules, ruled 2026-07-29).
|
||||
///
|
||||
/// Only names whose `displacesSquatters` is `true` can produce one, and only `.trash` is
|
||||
/// answered here: `CLAUDE.md`'s squatter is the agent guide's, detected by `AgentGuide.inspect`
|
||||
/// in the same read that decides everything else about that file, so answering it twice would be
|
||||
/// two mechanisms racing to displace one node.
|
||||
/// answered here: `CLAUDE.md` and its `AGENTS.md` twin's squatters are the agent guide's,
|
||||
/// detected by `AgentGuide.inspect` in the same read that decides everything else about each
|
||||
/// file, so answering either here too would be two mechanisms racing to displace one node.
|
||||
public static func squattedClaimedName(atBoardRoot root: URL) -> ClaimedNameSquatter? {
|
||||
guard let claimed = claimedRootNames.first(where: { $0.name == trashFolderName }),
|
||||
let found = node(at: root.appendingPathComponent(claimed.name)),
|
||||
|
||||
Reference in New Issue
Block a user