Address the memory by repository, so a worktree has one - #96
Merged
Merged
Conversation
A git worktree of a correctly wired repository arrived with no memory. The memory was reached through <repo>/<memory_dir>, a gitignored symlink, and git carries neither ignored files nor symlinks into a worktree — so `lint` exited 2 with "this repository does not use this memory layout", `check` printed MEMORY LINT COULD NOT RUN, and `commit` stopped at "memory lint is red" before it ever reached the file-list gate. The whole corpus was on the machine, complete, and unreachable from the tree the session was working in. The address is now computed, not stored. A repository whose .floppy/config carries public_repo and project_key resolves its memory to <agents_memory_dir>/<key>/shared, and nothing of that name is created in a working copy at all. project_key is tracked, so every worktree of a repository computes the same one; `store` runs once per machine and a worktree needs no step of its own. The configuration decides, not the file system. An earlier draft fell back to the working copy whenever nothing stood at the store's address, which reads as caution and is not: the skills of this plugin tell an agent to write .agent-memory/<file>, so one note from an agent that followed them recreated the directory, took the address back, and put the repository into exactly the state above. A real directory there is now a fork of the corpus: `wrap-guard` refuses while it stands, and `store --migrate` moves it, printing every file first and refusing the whole run on a collision or on a private scope it has no right to publish. The harness's own pointer is not addressed by repository and cannot be: it is <claude-config>/projects/<encoded cwd>/memory, one per working directory. That half is made automatically by the config parser on any verb, announced once, and skipped for --check. Two repositories keep the fence: the public scope commits from the public store and the private one from the workplace store; `git add -A` in a code worktree stages no memory path at all. scripts/run and scripts/init.sh are touched: `run` exports FLOPPY_AUTOLINK=0 for --check, and `init` lays the memory out at the store address instead of writing MEMORY.md into an ignored directory in the tree.
spscream
force-pushed
the
fix/am/memory-in-worktrees
branch
from
September 25, 2026 20:00
63ffe14 to
2797b89
Compare
spscream
added a commit
that referenced
this pull request
Sep 26, 2026
…you point it (#98) Bump the three version-carrying manifests from 0.26.0 to 0.27.0 and add the CHANGELOG section the release workflow extracts as the release body. Minor rather than patch: #96 changes behaviour a consumer relies on. The memory of a project in a store is addressed by repository, so `store` runs once per machine instead of once per working copy and `link` stops being a setup step. Four guide pages already on main say "since 0.27.0" in ten places. No code: only the version, the changelog entry, and the Cursor plugin manifest that tests/test-release.sh asserts against the plugin manifest.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A
git worktreeof a correctly wired repository arrived with no memory.The memory was reached through
<repo>/<memory_dir>— a gitignored symboliclink — and git carries neither ignored paths nor that link into a worktree. So
in every worktree:
commitnever reached the file-list gate. The corpus was on the machine,complete, and unreachable from the tree the session was working in. The
memory storesection ofcheckandguarddid not appear at all, so nothingin the output said the memory existed elsewhere.
What changed
The address is computed, not stored. A repository whose
.floppy/configcarries
public_repoand a key resolves its memory to<agents_memory_dir>/<key>/shared. No file and no symbolic link of that name iscreated in a working copy at all.
storeruns once per machine; a worktreeneeds no step of its own.
The key is
project_keyfrom.floppy/config. Three derived identifierswere measured against a real worktree and its checkout, and all three agree from
both sides —
rev-parse --path-format=absolute --git-common-dir,remote get-url origin, and the root commit. All three were rejected anyway, for whatthey do when the world moves rather than for what they answer today: the
git-common-dir path moves with the checkout, so a
mvof the repository orphansthe memory;
originis empty in a repository with no remote, which is everytest sandbox and many clones; the root commit does not exist until the first
commit, which is the state
initruns in.project_keyis tracked, so everyworktree of a repository computes the same one, and it survives both a move and
a change of remote.
The configuration decides, not the file system. The first draft resolved
into the cache only when nothing stood at
memory_dir, which reads as cautionand is not: the skills of this same plugin tell an agent to write
.agent-memory/<file>, and one note from an agent that followed them recreatedthe directory, took the address back, and reproduced the failure above. The same
trap is reachable without any mistake, through the plugin's own
statuses_personaldefault. A real directory in that position is now a fork ofthe corpus:
wrap-guardrefuses while it stands and names the verb that movesit.
Migration is mandatory, explicit, and never automatic.
store --migrateprints every file before it moves any, refuses the whole run on a name that
exists on both sides, refuses outright when
<memory_dir>/privateor<memory_dir>/commonis a real directory — those notes belong to anotherrepository, and moving them into the public store would publish them — and
deletes nothing, including the directory it empties. Nothing calls this flag:
not a verb, not a rite, not
init.The harness pointer is made for you. The session loader reads
<config>/projects/<encoded cwd>/memory, one per working directory, and thatpath is the harness's to choose. Addressing by repository cannot close that
half, so the config parser creates the pointer on any verb, announces it once on
stderr, repairs it when it dangles, and is skipped entirely for
--check.A hand-wired repository is not left behind. One with a
memory_dirpointedat a store by hand has no
public_repoto compose a cache path from; itsworktrees ask the main working copy of the same clone, which is where that link
was made. That fallback needs git 2.31 and is documented as such.
Two repositories, two scopes. The public scope commits from the public
store and the private one from the workplace store. Measured from a fresh
worktree:
FLOPPY_MEMORY_STOREandFLOPPY_PRIVATE_STOREare two distinctclones, and
git add -Ain the code worktree stages zero memory paths.How verified
git worktree addfrommain, zero manual verbs run in it, thenlint→check→guard→commit. All four are green,checkprints thememory storesection, andcommitpasses the file-list gate and commits. The same four verbs againstthe parent commit, on the same worktree shape, reproduce the four failures
quoted at the top.
/bin/bash.knowledge-recheck.py: 9 passed, 0 failed, 3 skipped, 14 notmachine-checkable.
translation-check.py: clean.tests/test-worktree-memory.sh(37 assertions) runs thewhole rite from a real
git worktree addfixture with no manual verbs, andcarries a case per defect found in review.
tests/test-memory-migrate.sh(45)covers the migration, including the private-scope refusal and a name with
spaces.
tests/test-wrap-lock.shno longer hand-creates the symbolic link areal worktree does not have — that line was the coverage hole this defect
lived in.
assertion and no other.
tests/lib.shnow unsetsAI_FLOPPY_HOME,CLAUDE_PLUGIN_ROOTandCURSOR_PLUGIN_ROOT. With it exported in the shell,init's own subcallreached the developer's checkout rather than the fixture, and a test asserting
the old symbolic link passed against code that no longer creates one.
What is out, and what was risked
release-adjacent files, and this would be a guaranteed conflict for no gate:
tests/test-release.shasserts only that the three manifests agree with eachother. Whoever ships this should bump them together and answer "Refresh
.floppy/run?" — the answer is no, the shim is unchanged.scripts/runandscripts/init.share touched, though a parallel taskowns them. Two lines each and neither is avoidable:
runis the only placethe arguments are visible, so it is where
--checkcan exempt itself fromcreating a pointer; and
initwroteMEMORY.mdinto an ignored directory inthe tree, which is the stranded corpus this change exists to prevent.
wrap-guard's un-ignored-memory check keeps its narrowing. Restoring itto fire unconditionally would print "nothing will ever commit these notes",
which is now false — the notes go to the cache and commit from the store. The
dangerous state it was aimed at is caught by the new stranded-directory gate
instead.
link from an earlier release still resolves and everything follows it; it is
reported and left standing. A real directory is refused, not emptied. The
principle in
memory-store.sh— this script does not decide the fate ofmemory — is intact.