Agent guide v6 - warn against lane-glob moves

A real agent incident (2026-07-29): moving cards with mv <lane>/* swept
the lane's own index.md along with the card folders, overwriting the
destination lane's identity file and leaving the source lane index-less
(the Implementation board briefly lost its lane titles; restored from
its auto-commit history). Both guide generations taught the correct
named-folder form but never said why it is load-bearing.

v6 adds the one-folder-at-a-time bullet to Moving and reordering:
a lane folder holds its own index.md beside its cards, so a glob
sweeps the identity file with them. Boards heal to v6 on their next
open per the first-line marker rule. The guide tests now derive
their expected marker and current/newer versions from
AgentGuide.version instead of a hardcoded literal, so the next bump
cannot silently break them.

1669 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-29 14:32:31 -04:00
parent 62c47a2209
commit 0d846c634e
2 changed files with 17 additions and 9 deletions
+9 -2
View File
@@ -44,8 +44,11 @@ enum AgentGuide {
/// The guide the app ships. **v4 was the pathfinder's**, and real boards carry it; v5 is the /// The guide the app ships. **v4 was the pathfinder's**, and real boards carry it; v5 is the
/// rewrite's guide (lanes, `.trash/`, `attachments/`, `modified-by`, the `CLAUDE.user.md` /// rewrite's guide (lanes, `.trash/`, `attachments/`, `modified-by`, the `CLAUDE.user.md`
/// pointer) and supersedes it on the next open. /// pointer) and supersedes it on the next open. v6 adds the one-folder-at-a-time move warning:
static let version = 5 /// a real agent incident (2026-07-29) showed `mv <lane>/*` sweeping the lane's own `index.md`
/// along with the cards and destroying the destination lane's identity the guide now says
/// *why* the named-folder form is load-bearing, not just what to type.
static let version = 6
// MARK: - The version marker // MARK: - The version marker
@@ -327,6 +330,10 @@ enum AgentGuide {
- Move to another lane: `mv <laneA>/<card-uuid> <laneB>/` — the folder move - Move to another lane: `mv <laneA>/<card-uuid> <laneB>/` — the folder move
IS the move. Then set the card's `order` to place it among the IS the move. Then set the card's `order` to place it among the
destination's cards, update `modified`, and re-stamp `modified-by`. destination's cards, update `modified`, and re-stamp `modified-by`.
- Move folders **one at a time, by name** — never `mv <laneA>/* <laneB>/`.
A lane folder holds its own `index.md` beside its cards, so a glob
sweeps the lane's identity file along with them and overwrites the
destination lane's.
- Reorder within a lane: rewrite only that card's `order`. - Reorder within a lane: rewrite only that card's `order`.
## Editing and deleting ## Editing and deleting
+8 -7
View File
@@ -95,7 +95,7 @@ struct AgentGuideMarkerTests {
@Test("The guide the app ships carries the current marker in its first line") @Test("The guide the app ships carries the current marker in its first line")
func shippedGuideCarriesTheMarker() { func shippedGuideCarriesTheMarker() {
let firstLine = String(AgentGuide.content.prefix { !$0.isNewline }) let firstLine = String(AgentGuide.content.prefix { !$0.isNewline })
#expect(firstLine.contains("lanework-agent-guide v5")) #expect(firstLine.contains("lanework-agent-guide v\(AgentGuide.version)"))
#expect(AgentGuide.installedVersion(of: AgentGuide.content) == AgentGuide.version) #expect(AgentGuide.installedVersion(of: AgentGuide.content) == AgentGuide.version)
// Files the app creates end with LF (01-storage-format.md § Encoding and line endings). // Files the app creates end with LF (01-storage-format.md § Encoding and line endings).
#expect(AgentGuide.content.hasSuffix("\n")) #expect(AgentGuide.content.hasSuffix("\n"))
@@ -152,8 +152,8 @@ struct AgentGuideDecisionTests {
/// "Never downgraded" a newer app version may have written it. /// "Never downgraded" a newer app version may have written it.
@Test("Current and newer are left alone") @Test("Current and newer are left alone")
func currentAndNewerAreLeftAlone() { func currentAndNewerAreLeftAlone() {
#expect(AgentGuide.decide(state(.file(text: guideText(version: 5)))) == .leaveAlone) #expect(AgentGuide.decide(state(.file(text: guideText(version: AgentGuide.version)))) == .leaveAlone)
#expect(AgentGuide.decide(state(.file(text: guideText(version: 6)))) == .leaveAlone) #expect(AgentGuide.decide(state(.file(text: guideText(version: AgentGuide.version + 1)))) == .leaveAlone)
#expect(AgentGuide.decide(state(.file(text: AgentGuide.content))) == .leaveAlone) #expect(AgentGuide.decide(state(.file(text: AgentGuide.content))) == .leaveAlone)
} }
@@ -200,7 +200,7 @@ struct AgentGuideStoreTests {
store.refreshAgentGuide() store.refreshAgentGuide()
let written = try String(decoding: fixture.data(AgentGuide.filename), as: UTF8.self) let written = try String(decoding: fixture.data(AgentGuide.filename), as: UTF8.self)
#expect(written.prefix { !$0.isNewline }.contains("lanework-agent-guide v5")) #expect(written.prefix { !$0.isNewline }.contains("lanework-agent-guide v\(AgentGuide.version)"))
#expect(written == AgentGuide.content) #expect(written == AgentGuide.content)
#expect(!fixture.exists(AgentGuide.userFilename)) #expect(!fixture.exists(AgentGuide.userFilename))
#expect(brackets.begins == 1, "one bracket — one app-mediated reload, one commit") #expect(brackets.begins == 1, "one bracket — one app-mediated reload, one commit")
@@ -232,9 +232,10 @@ struct AgentGuideStoreTests {
func currentGuideIsNotRewritten() throws { func currentGuideIsNotRewritten() throws {
let fixture = try makeBoard() let fixture = try makeBoard()
defer { fixture.tearDown() } defer { fixture.tearDown() }
// Deliberately *not* the app's own text: a v5 marker is the whole gate, and a file carrying // Deliberately *not* the app's own text: a current marker is the whole gate, and a file
// one must survive verbatim even when its body differs from what this version would write. // carrying one must survive verbatim even when its body differs from what this version
let url = try writeRoot(AgentGuide.filename, Data(guideText(version: 5).utf8), in: fixture) // would write.
let url = try writeRoot(AgentGuide.filename, Data(guideText(version: AgentGuide.version).utf8), in: fixture)
let before = try stat(url) let before = try stat(url)
let store = try BoardStore(rootURL: fixture.root) let store = try BoardStore(rootURL: fixture.root)
let brackets = GuideBracketLog() let brackets = GuideBracketLog()