claude-code: say a memory conflict in the operator's plain words, and skip a look the state does not answer (issue 367)
mesh/merge-gate head 0096beeff999 merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 0096beeff999 merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request

The warning now opens with what the operator is needed for and names no path,
agent or session (ADR 0253's plain rule); clause references follow ADR 0343 as
reviewed.
This commit is contained in:
2026-10-13 16:01:16 +02:00
parent 6aa30eae66
commit 0096beeff9
2 changed files with 46 additions and 29 deletions
@@ -221,7 +221,7 @@ func carriedOnBoth(t *testing.T, state *memState, c *clock, a, b *memNode, file,
}
}
// §3, §4: a memory file written on one node appears unchanged on another, at any depth, and is carried once.
// §5, §6: a memory file written on one node appears unchanged on another, at any depth, and is carried once.
func TestAMemoryFileWrittenOnOneNodeAppearsUnchangedOnAnother(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
g14.write(t, project, "MEMORY.md", "- [state](state.md) — where things stand\n", c.now)
@@ -261,7 +261,7 @@ func TestAMemoryFileWrittenOnOneNodeAppearsUnchangedOnAnother(t *testing.T) {
}
}
// §1: only the memory folders are carried; a symbolic link, an oversized file and a secret's shape are not, and are
// §1, §2: only the memory folders are carried; a symbolic link, an oversized file and a secret's shape are not, and are
// listed with why.
func TestOnlyTheMemoryFoldersAreCarried(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
@@ -317,7 +317,7 @@ func TestOnlyTheMemoryFoldersAreCarried(t *testing.T) {
}
}
// §2: the operator's homes and an agent account's homes never take each other's memory.
// §4: the operator's homes and an agent account's homes never take each other's memory.
func TestHomesOfTheTwoKindsNeverTakeEachOthersMemory(t *testing.T) {
state, c := newMemState(), &clock{now: time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}
root := t.TempDir()
@@ -353,7 +353,7 @@ func TestHomesOfTheTwoKindsNeverTakeEachOthersMemory(t *testing.T) {
}
}
// §5, §6: a file changed on two nodes since it was carried is kept twice in one file on both, the newer first, with
// §7, §8: a file changed on two nodes since it was carried is kept twice in one file on both, the newer first, with
// one warning; it clears once the file is tidied.
func TestAFileChangedOnTwoNodesIsKeptTwiceTheNewerFirstWithAWarning(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
@@ -384,10 +384,21 @@ func TestAFileChangedOnTwoNodesIsKeptTwiceTheNewerFirstWithAWarning(t *testing.T
if len(notices) != 1 || notices[0]["severity"] != "warning" || notices[0]["clear"] != nil {
t.Fatalf("not one warning: %v", notices)
}
for _, field := range []string{"headline", "explanation", "summary"} {
if s, _ := notices[0][field].(string); strings.ContainsAny(s, "/~") || strings.Contains(s, ".md") {
t.Errorf("the warning's %s names a path: %q", field, s)
for _, field := range []string{"headline", "explanation", "summary", "resolved"} {
s, _ := notices[0][field].(string)
if s == "" || strings.ContainsAny(s, "/~:") && field != "explanation" || strings.Contains(s, ".md") {
t.Errorf("the warning's %s is empty or names a path: %q", field, s)
}
// The plain rule (ADR 0253 §3): it never sends the operator elsewhere.
if field != "summary" && (strings.Contains(strings.ToLower(s), "agent") || strings.Contains(strings.ToLower(s), "session")) {
t.Errorf("the warning's %s sends the operator elsewhere: %q", field, s)
}
}
if e, _ := notices[0]["explanation"].(string); !strings.HasPrefix(e, "Needs you: ") {
t.Errorf("the explanation does not open with its verdict: %q", e)
}
if h, _ := notices[0]["headline"].(string); len([]rune(h)) > 60 {
t.Errorf("the headline is longer than sixty characters: %q", h)
}
all := append(g14.copies(t), shanks.copies(t)...)
for _, v := range []string{"- base\n- learned on g14\n", "- base\n- learned on shanks\n"} {
@@ -430,7 +441,7 @@ func TestMergeVersionsPutsTheNewerFirst(t *testing.T) {
}
}
// §7: a removal moves the other node's unchanged file into the copies.
// §9: a removal moves the other node's unchanged file into the copies.
func TestARemovalMovesTheUnchangedFileIntoTheCopies(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "old.md", "an old note\n")
@@ -457,7 +468,7 @@ func TestARemovalMovesTheUnchangedFileIntoTheCopies(t *testing.T) {
}
}
// §7: a change outlives a removal, whichever reaches the state first.
// §9: a change outlives a removal, whichever reaches the state first.
func TestAChangeOutlivesARemoval(t *testing.T) {
for _, removalFirst := range []bool{true, false} {
state, c, g14, shanks := twoNodes(t)
@@ -484,7 +495,7 @@ func TestAChangeOutlivesARemoval(t *testing.T) {
}
}
// §7: a missing folder, a state that lost its keys and a deleted key remove nothing.
// §9: a missing folder, a state that lost its keys and a deleted key remove nothing.
func TestAMissingFolderOrAnAbsentKeyRemovesNothing(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
carriedOnBoth(t, state, c, g14, shanks, "MEMORY.md", "index\n")
@@ -515,7 +526,7 @@ func TestAMissingFolderOrAnAbsentKeyRemovesNothing(t *testing.T) {
}
}
// §8: on a first carrying, two nodes' different versions are both kept, and each node's own files reach the other.
// §10: on a first carrying, two nodes' different versions are both kept, and each node's own files reach the other.
func TestTheFirstCarryingKeepsEveryVersion(t *testing.T) {
state, c, g14, shanks := twoNodes(t)
g14.write(t, project, "MEMORY.md", "- laptop's index\n", c.now)