Changelog
Why this file exists, in one line: plugin update compares version strings and copies nothing while the version is unchanged, so the version here is load-bearing, and a consumer needs to know whether an update is worth taking.
One column mattered more than the rest and is called out per release:
Refresh
.floppy/run? — until 0.26.0,.floppy/runwas a copy in your repository, not a link, and no plugin update ever touched it. When a release changed the shim, updating the plugin was only half the job:cp "$FLOPPY_ROOT/shim/run" .floppy/run. Since 0.2.1 the shim noticed this itself and printed the command; before that it was silent.Since 0.14.0 the answer was usually no: the copy held only the search for the plugin, and verbs, config keys and defaults arrived with
plugin update. Since 0.26.0 there is nothing to refresh — the skills call the plugin’s dispatcher by path andinitputs no runner in your repository. The line is kept in every entry because a repository created before 0.26.0 still carries its copy, and the answer for it is still worth stating.
Dates are the day the version was tagged in .claude-plugin/plugin.json.
0.27.0 — 2026-09-26
Refresh .floppy/run: no. The shim is untouched. scripts/run did change, and that copy arrives with plugin update.
A worktree of a correctly wired repository had no memory
git worktree add produced a checkout with no memory in it at all. The memory was reached through a symbolic link at <repo>/<memory_dir>, gitignored, and git carries neither ignored files nor symbolic links 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 computed now, not stored. A repository whose .floppy/config carries public_repo and project_key resolves its memory to <agents_memory_dir>/<project_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 address. memory_dir stays the name you type — .agent-memory/… is what you write in a file list and what every report calls it.
What follows from that:
storeruns once per machine, not once per working copy. A worktree needs no step of its own.linkis no longer a step. The agent application’s own pointer is<claude-config>/projects/<encoded cwd>/memory, one per working directory, so it cannot be addressed by repository. The config parser creates it on any verb, announces it once, and skips it for--check. The verb still ships, for a pointer you want to rewire by hand.- The configuration decides, not the file system. Resolution no longer falls back to the working copy when nothing stands at the store’s address. That fallback read as caution and was not: this plugin’s own skills 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 at
memory_diris a fork of the corpus and is now treated as one.guardrefuses while it stands, andstore --migratemoves it — printing every file before it moves it, refusing the whole run on a name that exists on both sides or on a private scope it has no right to publish, and deleting nothing, not even the directory it empties. - The fence between the two repositories holds. The public scope commits from the public store and the private one from the workplace store;
git add -Ain a code worktree stages no memory path at all.
A symbolic link an earlier store left in your working copy costs you nothing: it points at exactly the address that is computed now, so the configuration gives it the answer it already had. It is not followed, though — nothing in a working copy is an address any more, so a link that was repointed somewhere else by hand is ignored from this release on, and the notes behind it stop being read. Nothing deletes such a link; what to do with it is yours to decide.
A repository whose memory_dir was pointed at a store by hand, with no public_repo and no project_key, has nothing to compose an address from: its worktrees fall back to asking the main working copy of the same clone, which needs git 2.31 or newer for rev-parse --path-format=absolute. On an older git that fallback does not engage and such a worktree behaves as it did before this release, reporting no memory. The repair is the same in both cases — set public_repo and project_key, then run store once.
Four defects that work uncovered on the way
ln -snow has its exit status read. Everyok linkedline in this plugin was printed unconditionally after anlnnobody checked. In a git worktree whose target directory did not exist, the truth and the lie came out two lines apart on two streams:ln: failed to create symbolic link …: No such file or directory, thenok linked: .agent-memory/private -> …. It was the only place in the whole verb matrix where this tool said yes about something that had not happened.store,workplaceandlinkall report the failure and stop now;ln’s own message is left on stderr, because it names the errno.workplacesays which verb comes first, and refuses before it clones. Run with no memory for the private scope to sit in, it used to create a real directory there — after whichstorerefused to touch it, which is how the order the tool suggested bricked a tree. It now stops withx no memory at … — there is nothing for the private scope to sit in, says to run thestoreverb first, and ends withNothing was cloned, linked or written.checkandstatusjudge the wiring with one piece of code. They had drifted:statuscarried an arm for a private scope that is not a symbolic link, and the gate never had it. Measured one second apart in a worktree of this plugin’s own repository —status: repository exists, but .agent-memory/private is not a symlinkagainstcheck: clean and pushed. The gate was the optimistic one of the pair, which is the wrong way round for a gate.linkstopped calling a correct worktree the wrong repository. Its refusal wasx no <path>/.agent-memory — wrong repository, rc 2, for a worktree wired exactly as intended. It names what is actually missing now, and the verb that makes it.
init wrote where CDPATH pointed, not where you pointed it
An exported CDPATH with a relative entry makes cd print the directory it found on stdout, and that print landed inside the command substitution that resolves --repo. With CDPATH=.:<dir> and a relative --repo target, the path came out two lines long: init created a directory whose name ends in a newline beside the target, printed ok at every step, exited 0 — and the repository it had been pointed at kept nothing but its .git.
The unset CDPATH was already in the file, seven lines below where it had to be: above the cd that finds the plugin, rather than above the one that resolves the argument. --repo . and an absolute --repo are both immune, because cd consults CDPATH for neither, which is why every call the suite and the init skill make missed this.
The dispatcher stops inventing a plugin root
scripts/run roots itself in its own path and then sourced the config parser out of that root without asking whether it was there. . on a missing file prints one raw No such file or directory, and with set -u but no -e the dispatcher carried on: called through a symbolic link that points at that file alone rather than at a checkout, env printed a FLOPPY_ROOT naming no plugin and exited 0 — an invented root wearing a success. It now names the resolved root, names what is missing in it, and says the two things that can mean: an incomplete install, or a call that came through a link to the dispatcher alone, which carries none of the plugin with it.
Three documentation claims that measured false
.floppy/heat.logis excluded, not gitignored. Both language versions of the install guide said.gitignorecovers it. It does not, deliberately:memory-heat.shwrites its ignore line into.git/info/exclude, and its header records why. The first release of the verb appended to.gitignoreitself, so the firstheaton every machine left that file modified — andwrap’s guard refuses to commit it, so a wrap that named the file stopped on a change the human had not made. The log is machine-local, and so is the rule that hides it..floppy/does hold code — just never the plugin’s. The sectioninitappends to a consumer’sAGENTS.md, and the install guide with it, said the directory holdsconfigand nothing else.workstatusruns.floppy/workstatus-project.shwhen a project defines one, which is an executable in that directory. Both now say what is true: the plugin puts no code there, that hook is the project’s own, andheat.logsits besideconfig.- A
watched_filesentry naming.floppy/runwas typed by hand. No releasedinitever wrote one: every version from 0.11.0 to 0.26.0 writeswatched_filescommented out and without the runner in it. The 0.26.0 migration notes said nothing about the entry at all, which left a reader who had followed the migration to the letter with a line they could not date. The0.26.0section of this file now carries it, with the measurement — the release page forv0.26.0was published before that and is not rewritten. Andinitnames the entry when it sees one — read with the config parser’s own key pattern and matched between commas, so an indented or spaced copy is found and.floppy/runner.mdis not — splitting its advice by whether.floppy/runis still in the tree, because dropping the entry while the file is tracked takesguard .floppy/runfrom rc 0 to rc 1. It edits nothing either way:.floppy/configis yours.
0.26.0 — 2026-09-25
Refresh .floppy/run: no — there is nothing left to refresh. A repository that already carries a copy keeps working; a repository created from here on never gets one.
The runner leaves your repository
Every init before this release copied shim/run into the consumer as .floppy/run, and every skill, document and status note named that path. The copy existed for one reason: a skill had no way to say where the plugin was, so something inside the repository had to search for it.
It does have a way. A harness states the skill’s own base directory above the skill text — Base directory for this skill: <plugin>/skills/workstatus — and the plugin directory is two levels above that (measured in Claude Code on 2026-09-22, on this plugin and one other). So the skills now call bash <plugin>/scripts/run <verb> directly, and scripts/run derives FLOPPY_ROOT from its own ${BASH_SOURCE[0]} instead of being handed it. A direct call needs no floppy variable at all: bash <plugin>/scripts/run status runs with nothing set but HOME, which the config parser has always read.
What follows from that:
initwrites.floppy/configand nothing else. Your repository carries data, not code. Its refusal when the plugin had noshim/runis gone with the copy it guarded.skills/init/SKILL.mddrops its hand copy of the plugin search — 33 lines that existed becauseinitran before.floppy/runexisted. The test that executed that fenced block (tests/test-init-bootstrap.sh) goes with it. Prose cannot be executed, sotests/test-skills.shresolves it instead: no skill names.floppy/run, a skill using<plugin>says where it comes from and how far above the base directory it sits, and every<plugin>/…path a skill names has to exist in this checkout. The last two were added after a review showed<plugin>/runand “one directory above” passing.shim/runstill ships, byte for byte. It is what a pre-0.26.0 consumer calls, and itcmps itself against the plugin’s copy — so any edit here would tell every one of those repositories that their copy is stale. It is deliberately left untouched.
Migrating a repository you already have
Nothing breaks if you do nothing: .floppy/run still finds the plugin and still runs the verb. Nothing in the plugin calls it any more, so when you want it gone:
git rm .floppy/run
In the same commit, correct the line in AGENTS.md that names .floppy/run as the entry point — the verbs are run from the plugin now: bash <plugin>/scripts/run <verb>, where <plugin> is two directories above the base directory the harness states. A later init prints a reminder when it sees the old line, and prints one more when it finds the leftover file.
And check watched_files while you are in there. git rm takes the file; it does not touch .floppy/config. No released init ever wrote that entry — every version writes watched_files commented out and without the runner in it — so this is only for a config somebody edited by hand, as this repository’s own was until this release. If yours reads watched_files=…,.floppy/run,…, what to do depends on the file:
- Dropped the runner too. The entry now names nothing. Nothing breaks — measured on a repository carrying it with no
.floppy/runin the tree,guardandcheckboth run clean — but the path comes back at you in the scope lineguardends with, “nothing else changed under ….floppy/run…”, so the summary claims a file that is not there. Drop the entry. - Keeping the runner. Leave the entry alone. It is the only reason
wrapmay commit an edit to that file: drop it while the file is still tracked andguard .floppy/rungoes from rc 0 to rc 1, “outside.agent-memorydocsAGENTS.md”.
init says which of the two you are in, and edits nothing either way, because .floppy/config is yours.
The command a human typed
There isn’t one, and that is the measurement: the operator of the six repositories that use floppy reports never having typed .floppy/run by hand. The surface is dropped rather than replaced; no AI_FLOPPY_HOME recipe and no cache path with a version in its last segment is invented to stand in for it.
0.25.1 — 2026-09-18
Refresh .floppy/run: no. The shim is untouched.
One repository may hold both scopes, and the gates now know it
A project that cannot commit notes next to its code — a fork kept for upstream pull requests, a checkout the team does not own — can point public_repo and private_repo at the same repository: the scopes do not collide, because they sit at public/projects/<key> and private/projects/<key> inside it. store, workplace and link all wire that layout and all report success.
The checks did not cover it. FLOPPY_PRIVATE_STORE was derived by asking whether the private scope’s repository differs from the memory store, which here it does not, so the variable was blanked, wrap-guard’s scope scan returned on its first line, and each of fifteen changed files came back as not changed: wrong path, or the edit was lost — the message for a typo. The reader spent several iterations checking their own paths before running env, which was the one place the truth was. Reported from a consumer checkout and reproduced here.
What decides coverage is containment, not repository identity: the memory scan reaches what lies under the memory scope, and a sibling prefix is not under it. The neighbouring derivation for common/ had always used containment and says so in its own comment.
Two consequences travel with the fix:
- One repository is one commit. With both scopes in one clone,
commitwould otherwise write two commits carrying the same message, push twice into it, and print two sections naming the same path. The scopes stay separate everywhere they are derived and translated; they merge where the unit stops being a scope and becomes a repository. - An unscanned scope says so. The scan returns silently on an empty store, so any future blanking would again wear the message for a typo. A file in a private scope that no store covers is now told that, with the path it resolves to and where to look.
A bash condition in a note is not a [[link]]
The link scan took everything between [[ and ]], so [[ -n "$x" ]] in a note — inline or inside a fenced block — was read as a slug and resolved to nothing. A hard error, so it took check and the commit with it, and the only way out was to reword prose about shell code. A slug never contains whitespace; a bash condition always does, and that is the whole discriminator. All 41 links in the corpus that found this are slug-shaped, and a misspelled slug is still a slug: the positive control in tests/test-memory-lint.sh pins that a dangling [[slug]] still fails.
0.25.0 — 2026-09-18
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor by the 0.21.0 rule: no file layout moved and no migration is needed, but a rite reads differently — wrap’s skill is a tenth shorter and the evidence behind its rules now sits beside it rather than inside it.
wrap stops carrying its evidence through the hot path
Section 5 held about 750 words of measurement: the 48-run turn study, the quadratic-cost arithmetic, the superseded explanation of the fold. All of it is load-bearing when a rule is questioned and dead weight when the rite is merely run — and the rite is run every session. It moves to skills/wrap/measurements.md next to the skill, referenced by name rather than @-imported, so it is read when a rule is being changed and not before. The rules themselves stay inline with their numbers. 3082 → 2744 words.
The rewrite-once rule gets a price, because the bare count never argued
The rule carried “2.6 edit turns per run on that one file” and no consequence. A count alone argues for nothing, and this one has never held: re-measured across five repositories a week later it is 2.7, the same before the mid-session capture convention as after. What was missing is what a turn there costs — a median 32k base-equivalent tokens, almost all of it re-reading the context rather than writing the text. That is what makes patching the expensive option even though each call looks small: two patching turns cost 64k where one full rewrite costs 49k with the whole file in its output, and at the p90 of six turns the gap is 143k.
knowledge-rot-check.py gets the timezone slack the other two linters had
Both CI legs went red on verified_on is in the future: the date was written at UTC+3 in the evening while the runners were still on the previous UTC day. The same off-by-one was measured on 2026-09-05 and fixed then in memory-lint.sh and translation-check.py — this third linter was missed.
In the same pass, the knowledge note on the Opus 5 delegation line was re-verified against Claude Code 2.1.267. Its recheck grepped the binary for a whole 2.1.232 literal, which the release reworded, so the gate failed while the behaviour was unchanged; it now greps the sentence’s middle, which is what survives a rewording.
The field survey becomes a page of the site
docs/comparison.md and its Russian translation — floppy among eight memory systems — join the page table in scripts/site-build.sh.
0.24.1 — 2026-09-14
Refresh .floppy/run: no. The shim is untouched.
A leading ~ or $HOME in a path value means the user’s home
cfg_get returned every value verbatim, and 0.24.0’s own recipe nearly shipped workplace_memory_dir=$HOME/... — git would have cloned into a directory literally named $HOME under the repository. The parser now expands a leading ~/ or $HOME/ (and the two bare forms) to the user’s home; nothing else — the config is not a shell, so ~user, other variables and anything mid-value stay literal. Beyond fixing the trap, this lets a committed config name a path that is true on every machine and carries no username into a public repository. tests/test-memory-dirs.sh scenario 11 pins both spellings and both failure shapes.
0.24.0 — 2026-09-14
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor by the 0.21.0 rule: no file layout moved and no migration is needed, but a rite behaves differently — commit’s sync no longer fails on a dirty file it does not own, and check reads a shared clone differently.
The sync stashes over dirt it does not own (autostash)
git pull --rebase refuses on any unstaged change to a tracked file, even with nothing to rebase — and nothing anywhere set the rebase.autoStash the comments relied on. Measured 2026-09-13 on the owner’s machine: several projects share one clone of the workplace repository, a live session of one project held its status file modified there, and another project’s wrap died at the sync step — commit landed, push lost, the other machine blind. All three sync sites in commit now pass -c rebase.autoStash=true: the workplace clone (foreign projects’ dirt), the memory store (a crashed session’s half-written note), and the project repository itself (product code a session deliberately leaves uncommitted). The stash round-trips the dirt; nothing of it is committed or pushed.
check counts a shared clone in two piles
The workplace section counted every dirty file in the clone and said “name them in your file list and commit closes them too” — an invitation to commit another project’s half-written work. It now counts this project’s own paths (its scope and private/common/) apart from everything else, and says of the rest: another project’s session owns them; leave them.
One project can opt out of the shared clone
The default stays one clone per repository URL. workplace_memory_dir — which always won over the derived path — is now the documented and tested way to give one project a clone of its own when several projects share private_repo on one machine: isolation of the working tree without moving any other project or machine. The recipe, including migrating an already-wired machine, is in docs/guide/config.md under “When two projects should not share one working tree”, and tests/test-memory-dirs.sh pins both halves of it.
0.23.0 — 2026-09-13
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor by the 0.21.0 rule: two rites are new, a verb is new, and the ignore mechanism moved — none of it is a fix to a released behaviour, because none of it was released before this version. There is no migration to do: the first heat call wires its own ignore line, and a memory written the old way is still correct.
The heat verb and lint’s cold-note report (#70)
bash .floppy/run heat <slug>... appends one YYYY-MM-DD <slug> line per opened note to .floppy/heat.log; the start rite instructs sessions to call it, and lint gains a note heat section naming the notes no session has ever reported opening — a reporter, never a gate, because the log is a self-reported floor (the 2026-09-09 benchmark measured self-report as an under-count) and cold-but-correct notes earn their keep by existing. The log is per-checkout working data: machine-local, rotated past 5000 lines, outside every quota.
The ignore line lives in .git/info/exclude, not the consumer’s .gitignore. The first cut edited .gitignore, and this repository’s own review measured the consequence the same day: the tree goes permanently dirty on a file wrap’s guard refuses to commit. The exclude file is machine-local like the log itself, so no repository churn at all.
The consolidate rite (#69)
The quota ratchet knew two answers when a ceiling nears — raise the number or prune stale notes — and this repository’s own corpus measured both running out: three chars_max raises in one week with an honest pruning pass finding nothing. The new rite is the third answer: read one half whole (logging the reads via heat), gather evidence per note (as_of, the cold list, wikilink overlap), and propose merges, rewrites and deletions in a numbered list — a proposer, never a gate; every change waits for the human’s yes, and an empty-handed pass is a valid outcome the next raise commit can cite.
Review fixes that shipped inside the same release
The pre-release review (10 findings, 2026-09-13) landed here rather than in a patch after: a stale FLOPPY_REPO is now a loud error instead of a log written into the wrong repository; a slug with whitespace or a leading dash is rejected at write time instead of becoming a permanently-cold line lint can never match; a rotation that cannot complete is named instead of swallowed; a heat log holding only a newline reads as empty instead of reporting every note cold “since :”; and the READMEs and tests/test-docs.sh count six skills from the directory listing instead of a hand-kept five.
0.22.0 — 2026-09-13
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor for the same reason 0.21.0 was: no verb, config key or file layout moved, but a rite’s prose changes how writing a note behaves, and prose is the product here. There is no migration to do — a memory written the old way is still correct.
A new note pulls a revision of its wikilink neighbours
agent-memory’s one-fact-per-file check only looked backwards — does a note already cover this ground. It now has a second half: before saving, open the notes the new one [[links]] to, and where the new fact refines or contradicts one of them, fold it in or rewrite that note instead of leaving a near-duplicate sibling.
Write time is the cheap moment for that merge — the session still holds the whole picture. The motivating measurement is this repository’s own quota week of 2026-09-05..09: chars_max raised three times while an honest pruning pass found nothing stale to drop. On a corpus that grows live, merging at write time is the only pruning there is. The mechanism half of the same problem — a consolidation rite proposed against the 96% quota warning — is #69, not in this release.
init’s bootstrap search resolves all six ways again
The fenced block in skills/init/SKILL.md — the plugin search init runs before .floppy/run exists — carried only four of the shim’s six resolution branches: it stopped at the Claude Code cache. A Cursor user whose harness had not set CURSOR_PLUGIN_ROOT was told “plugin not found” for a plugin the shim itself would have resolved through the local symlink. The block now duplicates the whole has_scripts chain — harness variables, AI_FLOPPY_HOME, Claude cache (sort -V), Cursor local symlink, Cursor cache (ls -dt) — and tests/test-init-bootstrap.sh extracts and runs each branch, so the shim and the skill drift apart loudly rather than silently.
0.21.0 — 2026-09-09
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor rather than patch because the rites behave differently, even though no verb, config key or file layout moved. The prose is the product here, and a consumer reading “patch” would expect a fix rather than a changed ritual. There is no migration to do: a memory written the old way is still correct, and nothing about a note’s shape, its index or its quota changed.
A note is written when the fact appears, not collected at wrap
wrap used to do the selecting: at the end of a session it re-read what had happened, decided which facts earned a note, and wrote them. That is the single most expensive moment to do that thinking. Every turn resends the whole window, the window only grows, and closing is where it is largest — so the same judgement costs more there than anywhere else it could have been made.
It is also where the reasoning is thinnest. A rejected option carries its argument only while the conversation still holds it; what survives a summary is the decision, not the argument, so the next session proposes the same option again and pays for the same refusal twice.
So selection moved to the moment the fact appears. agent-memory now names the five moments that produce a note — an option rejected, a measurement landed, a number that turned out to mean something else, a tool trap, a frozen decision — and says explicitly what is not one: finishing a task, making a commit, a green suite. Those are in the git log.
start carries the trigger, because it is the only rite loaded early enough to be read before the first of those moments arrives. wrap is loaded at the end, and agent-memory is loaded when a note is already being written — too late to be the thing that prompts one.
wrap keeps a selection step, narrowed to what only became clear at the end, and it does not re-open what the session already wrote. Its closing report now covers both halves — what was written during the session and what the rite added — because a report listing only the second understates the memory the session actually kept.
Why this needs no new lock
A note file collides with nothing: its name is unique, so two sessions writing two notes write two files. Its index pointer is a single anchored line, and an insert applies against whatever the index holds at that moment, so two sessions inserting different pointers both land. Neither needs the wrap lock.
The whole-file rewrite is the opposite, and it stays in wrap under the lock — rewriting an index, or either status file, is where a second writer silently drops the first one’s work. That asymmetry is why the status file did not move with the notes.
One rule follows from lint and is worth stating plainly: the pointer is written with the note, never after it. A note no index points at is a hard error, not a warning — nobody will find it.
What this release does not claim
The cost argument above is arithmetic over the turn measurement already recorded in wrap, not a new measurement. What nobody has counted is how many of a wrap’s turns go to selecting facts rather than to checking and committing. The skills say so where they make the argument.
Nothing in the test suite covers this change. It is prose, and the guards check skill frontmatter, config keys and the site’s page table — all of which stay green whether or not the prose is right. Review was the only check, and the design document says so rather than implying a rigour that is not there.
Design and plan are in docs/specs/2026-09-09-status-written-as-the-session-runs-design.md and docs/plans/2026-09-09-status-as-the-session-runs-pr1.md; the latter records the hook mechanisms that were checked and not adopted.
0.20.0 — 2026-09-08
Refresh .floppy/run: yes — the first release since 0.14.0 that says so, and the change is one line. The shim’s last line was exec bash, which throws away an interpreter chosen on the command line: /bin/bash .floppy/run status ran the shim on /bin/bash and every verb under whatever bash PATH offered first. It is now exec "${BASH:-bash}". The plugin search is untouched, so an un-refreshed copy still finds the plugin and still works — defer this one if you never name an interpreter yourself.
cp "$FLOPPY_ROOT/shim/run" .floppy/run
Minor rather than patch because there is a new scope with new wiring, and init writes a second .gitignore rule. There is no migration to do: a memory without common/ lints, wraps and commits exactly as before, and the verbs create the scope on the next store / workplace.
The cross-project scope is wired, not just documented
common/ — the subject-level sibling of projects/<key>, for facts about no single project — has been in the memory model since 0.7.0 and was created, linked or read by no verb. It was documentation of a thing that did not exist. On this repository fifteen such notes sat in a private store where no session could reach them.
store and workplace now wire it as two links under the memory directory:
| path | wired by | holds |
|---|---|---|
<memory_dir>/common/shared | store | facts about no one project that the team may read |
<memory_dir>/common/private | workplace | the same, kept off the team’s repository |
Two links and not one, because the two namespaces live in two different repositories. Either verb alone leaves a usable half — a machine that ran only workplace gets only common/private, which is the ordinary case and not an error.
common/ gets no view directory under agents_memory_dir, unlike every other scope. The symmetric shape — one <views>/common/ holding both leaves — assumes one store per namespace, and the public namespace has one store per project; the suite’s own two-store fixture, which exists for init and not for this, failed on it the first time it ran. The links go straight into each clone.
Three gates had to be taught the scope separately, because each routes by path under its own pathspec, and each dropped it in silence:
lintchecks the per-note invariants there and nothing else. A note’s frontmatter is a property of the note, true wherever it lives, and these notes had never been checked by anything — measured on the day the scope was wired, 8 of 15 carried nometadata.evidenceand one had anamethat did not match its file, so a[[link]]to it resolved by luck. It does not apply the index tree orquota.lock: those ceilings are measurements of one project’s corpus, and a corpus several projects write to would be wearing a borrowed cap.- A committed note may not link into
common/, the rule that already held for the private scope and for the same reason: both are wired per machine, so a relative link into either is dead for anyone who has not wired it. Routing is the reader’s job —startnames the scope — not a pointer’s. commitpeels the two leaves off before the project arm claims them and sends each to the repository it actually resolves into. A leaf pointing somewhere else is refused by name rather than committed to the wrong repository: notes going to a repository the session never reported writing to is the failure worth a hard stop.
lint’s clean line now names the foreign corpora rather than counting them into silence — clean: 20 notes, 20 pointers across 1 indexes (also checked 15 in common/). A green run reporting “20 notes” while quietly checking 35 teaches the reader that the number is the whole memory.
status --flow reports stale translations
Where a repository keeps <stem>.<lang>.md documents carrying the translation marker on line 1 — the HTML comment naming of=, blob= and on=, whose one authority is translation-check.py --list — the flow report now says which have drifted from the source they name. The section prints only where the checker actually lists something, so a repository with no translations sees no heading — the same rule as the worktree line.
The shell asks; translation-check.py --list answers. The pre-gate in workstatus.sh is deliberately loose and decides one thing only, whether starting python is worth it. It uses ? and never a bracket range: a range is matched through LC_COLLATE, and on the macOS runner — collation aAbBcC… — [a-z] also matched an uppercase tag, so guide.RU.md counted as a translation and turned the macos-bash-3-2 leg red. A single-character wildcard has no endpoints for a locale to reorder.
The guard stopped advising a command that would refuse
wrap-guard.sh’s memory-wiring error told everyone to run bash .floppy/run store. In a repository with no public_repo and no project_key that verb refuses, so the advice was a dead end: a consumer followed it on 2026-09-08, hit the refusal, and reported the tool as inapplicable. The message now branches on whether those keys are set, and points at init where they are not.
Also in this release, not shipped to consumers
The user documentation left README.md. The guide, the config reference and the skills prose are under docs/guide/ in both languages, the front page is a landing with the two install commands, and the archaeology — the plugin-cache post-mortem, the 0.5.0 rename, the 0.4.2 two-repositories incident — is in docs/lessons.md. Nothing was rewritten in either language; review verified the moved lines byte-for-byte.
That move broke four guards at once, all of them assuming exactly one directory level, and one of them shipped two live 404s past six per-task reviews because the guard written for that class was blind to its own form. The site’s page table is now asserted in both directions, so a row whose document was never built fails the suite instead of printing ok.
tests/run.sh pins its interpreter by putting a directory holding one bash — a symlink to the interpreter it was started with — at the front of PATH. That covers ~180 bare-bash call sites across 19 test files, and everything added later, in one place. The claim that the macOS job ran the scripts under bash 5 was retracted: measured on the runner, that image carries one bash and it is 3.2.57.
0.19.0 — 2026-09-06
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor rather than patch because there is a new config key, note_stale_days, and a new note field, metadata.as_of. Nothing breaks and there is no migration to do: the field is optional and stays optional, an undated note is counted rather than named, and a corpus that never adds one lints exactly as it did before. status prints one line fewer in the case where it used to be wrong.
status kept naming branches that had already been deleted
The -- origin section reads remote-tracking refs, and git fetch without --prune only learns what appeared — never what went away. A remote-tracking ref outlives the branch it names, so a branch deleted on merge stayed in the report until something else happened to prune it.
Measured, minutes after two pull requests merged with --delete-branch: both of their branches were still listed as live, ten minutes stale. Where every change lands as a pull request that deletes its branch — which is what a protected default branch makes ordinary — that is wrong after every merge, and wrong in the direction that invents work. A branch named there reads as something still open.
The fetch now prunes. It removes remote-tracking refs under origin only; local branches are untouched. The regression test runs against a real bare remote, because a sandbox with no origin cannot tell a fetch that prunes from one that does not.
A note said what kind of claim it held and never said when
metadata.evidence records where a claim came from. Nothing recorded when it was last true — and two of its four values were already asking for a date: read is documented as “true until something runs and says otherwise”, and decided as the case where a date and an author apply instead of evidence. Neither had anywhere to put one.
git log is not that date. It moves on a reformat, a renamed link, a fixed typo, and it drifts in the direction that makes an old claim look freshly checked.
metadata:
type: project
evidence: measured
as_of: 2026-09-06 # optional
lint gains a -- note dates section with three behaviours, and the split between them is the whole design:
| state | what happens |
|---|---|
no as_of | counted in one line, never named, never a failure |
| unusable — malformed, or dated more than a day ahead | fails the run |
older than note_stale_days (default 180) | named, and the run still passes |
Undated is not an error, and that is load-bearing. The field arrives into corpora that already exist, on machines whose owners did not ask for it. A check that reddens somebody’s memory the day they update the plugin is a check they switch off — and the four that were already earning their place go with it.
Age warns and never fails, for a different reason: old and wrong are different things. A note recording a frozen decision is fine at three years; a note about a dependency’s behaviour is suspect at three months, and no threshold distinguishes them. A gate would also teach the cheap way out — bump the date without re-checking anything — which converts a stale note into a fresh-looking one and destroys the only signal the field carries. Lower note_stale_days if your memory is mostly about a fast-moving dependency.
One day of slack on a future date, and that tolerance is the measured part. A note written at 22:0x UTC from a UTC+3 clock is dated ahead of a UTC runner; the same off-by-one had already turned both CI legs red on this repository’s knowledge base, named the base rather than the offending note, and healed itself after midnight — the signature everyone reads as flakiness. No timezone is more than a day from UTC, so the slack costs nothing and removes a self-healing red. A date a month out is still a typo worth catching.
Day arithmetic is computed rather than asked of date: -d is GNU and -v is BSD, so anything built on either is green on one CI leg and dead on the other.
Also in this release, not shipped to consumers
The macOS temp-path question is settled by measurement: <b> in /var/folders/<a>/<b>/T/ is fixed by the runner image, not drawn per machine. Twenty runners in one dispatch returned two components, each tied to a kernel version 20 out of 20 — 25.5.0 carrying _ (6 runners), 25.6.0 without (14), so the same commit passes or fails by which image it lands on. The 30% is a rollout mix on one day, not a property of macOS, and it drops to zero when 25.5.0 retires — leaving the defect intact and the tests green. This changes tests.yml and the knowledge base in this repository; nothing in the plugin.
0.18.0 — 2026-09-06
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor rather than patch because there is a new config key, statuses_personal, and commit behaves differently on a branch the remote has never seen. Nothing breaks: the new key is optional and derived when unset, the file it names is optional too, and a repository that never writes one behaves exactly as before. No migration to do.
All three fixes came out of closing one session on 2026-09-06, the day the pull-request model went in. That model is what turned each of them from an edge case into the ordinary path.
Every wrap on a protected branch failed at its last step
commit ended with pull --rebase then push. On a branch with no upstream the pull has nothing to rebase against, fails with “no tracking information”, and takes the whole tail with it. While wrap ran on the default branch — which has an upstream — this was an edge case. Once main refuses a direct push, every wrap runs on a branch created for it, and such a branch has no upstream on its first commit. So it was every close, not an occasional one.
With no upstream the pull is skipped and the push is -u origin HEAD: there is nothing to rebase against on a branch the remote has never seen, and setting the upstream is what a first push is for. Where an upstream exists nothing changes — the pull there guards the two-machine case, which is the reason it exists.
The push output is now read, because its failure has two kinds. A push refused by a branch rule used to print push failed (network/VPN?) Retry: git push, which is advice that cannot work — and it was the first thing a consumer saw after protecting their own default branch. It now names the rule and prints the recipe:
push refused by a branch rule (GH013), not by the network.
main is protected; the commit is made and safe locally. Move it to a branch:
git switch -c wrap-2026-09-06
git push -u origin HEAD
gh pr create --fill
commit still does not create the branch itself. These scripts decline to guess, and moving someone off the branch they were on is a guess.
The wrap lock protected the clone, not the memory
The lock lived in git rev-parse --git-dir, on the reasoning that each worktree has its own “because each worktree carries its own memory copy”. Measured, both halves: a linked worktree’s --git-dir really is .git/worktrees/<name>, and in the store layout the second half is false. There memory_dir is a gitignored symlink into a store shared by every checkout on the machine — so two worktrees took two independent locks and wrote the same notes.
The lock now follows the resource. With a store it lives in that store’s git directory, named for the project key, so every session on the machine writing those notes serialises while another project sharing the store is not blocked. With the memory inside the repository the old placement was already right and is unchanged. acquire and status print a covers: line.
Two machines are covered by nothing, and the wrap skill now says so instead of implying otherwise. It also stops claiming git does not arbitrate: across machines it does, by refusing to merge two rewrites of the current-state file. A conflict on every overlapping wrap is loud, not silent, and a reader who expects silence watches for the wrong failure.
The status is two files, split by who the fact is true for
The current-state file was the only artefact every wrap rewrote whole. A note collides with nothing by construction and an index merges; that one file conflicted on any overlap, and the pull-request model sharpened it — two threads of work now meet as a merge conflict in it on every overlapping branch.
Most of what forced those rewrites was not project state. It was one person’s thread of work: what they were mid-way through, what is half-done, where to resume. That changes every session and is shared with nobody.
statuses_now | statuses_personal (new) | |
|---|---|---|
| true for | the project | one person, one machine |
| holds | frozen decisions, what is red, what waits on the human, metrics and their marks | the thread of work: what is unfinished, where to pick it up |
| lives | docs/statuses/NOW.md, committed with the code | the private scope, machines/<name>/NOW.md |
| contention | rare — it changes rarely | none — no other machine writes that path |
The new path is derived, not a constant: it follows memory_dir and memory_private_dir and takes its machine part from machine_key, or from hostname when that is unset. init writes it only as a commented example, with the reason — setting the key puts one machine’s path into a config every machine reads.
Being inside the memory it needs no new plumbing: commit sends it to the private store and never to the code repository. memory-lint stops reading it as a malformed note, excluded by path (machines/*/<leaf>) so that a second machine’s copy arriving through the sync is skipped too, at a name this machine cannot compute — and only that one filename, so a genuine note filed under machines/ still lints. start reads it after the project file when it exists; absence is the ordinary state and status says nothing rather than warning.
This narrows how often two sessions collide. It does not close the two-machine case, and agent-memory says that where it describes the three status genres.
0.17.0 — 2026-09-06
Refresh .floppy/run: no. The shim is untouched — no commit in this release reaches shim/run.
Minor rather than patch because init changes what it writes into a consumer’s config. Nothing breaks: project_key was already read, so a config written by this release works with older plugins, and a config carrying the old memory_project_key keeps working with this one. init never rewrites an existing config, so this affects newly initialised repositories only.
init wrote the narrow key
init wrote memory_project_key. That is the per-scope override; project_key is the default of both memory_project_key and workplace_project_key, so writing the narrow one left the workplace scope needing a second key of its own, free to drift from the first. This repository’s own config had project_key set by hand, with the reason written beside it — init now writes what that comment says.
init wrote an ignore rule the store layout already covers
In the store layout memory_dir is a symlink and store ignores it whole. init added /<memory_dir>/<private> on top of that, which git already refuses as part of the subtree. Harmless, and invisible for the same reason most of these are: the file looks configured. init now asks git check-ignore first.
A repository initialised before this release still has both lines. store now says so rather than removing it —
note /.agent-memory/private in .gitignore is covered by /.agent-memory and can be removed
— because this is your .gitignore, a line in it may have been written by hand, and one line removed by hand is cheaper than a rule about when a script may delete from a file it does not own.
The release that did not happen
Between 0.14.0 and 0.16.2, five versions bumped .claude-plugin/plugin.json and nothing else: no tag, no release, and both marketplace manifests plus the Cursor plugin manifest sat at 0.14.0 the whole time. Since the marketplace manifest is what an update compares, plugin update had nothing to copy — the two link fixes of 0.16.1 and 0.16.2 never reached anyone until 0.16.2 was released by hand on 2026-09-05.
Nothing was red and nothing could have been: each commit looked complete on its own, and the omission is only visible against files the diff does not touch. tests/test-release.sh now asserts the three versions agree, and asserts the fourth manifest’s version conditionally — demanding its absence would fail the commit that legitimately adds one.
The knowledge base is re-checked on a schedule
knowledge-recheck.py and knowledge-rot-check.py already ran on every push through the test suite. Every trigger there is a repository event, while these notes are claims about Claude Code releases, macOS, Docker and vendor feature flags — none of which move when somebody pushes. A weekly workflow now runs both on Linux and macOS, because a note names the platforms its claim holds for and the runner skips the rest; a Linux-only schedule reported the macOS-only note as skipped forever and called that green. A failure opens one issue and comments on it thereafter.
Worth knowing what it covers: 3 of 12 notes. Eight carry nothing executable — by design, a claim taken from vendor documentation has nothing to run — and one more needs a claude install the runners do not have. Measured on the first scheduled run: macOS checks three notes, Linux two.
The macOS job was red at random, and was not flaky
tests/test-memory-link.sh computed the harness’s project directory itself with tr '/.' '--' — two characters, while link has folded three since 0.16.1. On any temporary path containing _ the test addressed a directory --check no longer looks at. macOS TMPDIR is /var/folders/<random>/T/ and that component carries an underscore some of the time; Linux TMPDIR is /tmp, which is why only one platform ever showed it.
The test now builds the state instead of addressing it — wire the repository, find the link the script made, replace it with a directory — so it no longer contains the encoding rule in any form. The lesson is written up in docs/lessons.md, including the part worth reading: the comment that excused the recomputation was written deliberately, in the release that fixed the same class of bug, and survived nine hours.
Contributing
main now refuses a direct push. Changes land through pull requests, checked on both platforms before merge, with no bypass for anyone including the owner. No approval is required, so a pull request can be self-merged; what the rule buys is that the checks run before the change is on main, and that work arriving from a session other than the one working here is visible as a change rather than as history. See AGENTS.md.
Also in this release: a knowledge note was retracted rather than aged out — prometheus-exporter-carries-only-sessions measured a one-request session and blamed the exporter, and is replaced by prometheus-metrics-need-a-second-request.
0.16.2 — 2026-09-05
Refresh .floppy/run: no. The fix is in link, again — the same file as 0.16.1, a different defect, found by standing the external layout up rather than by reading it.
link compared one resolved path against one unresolved one. It took readlink -f of the harness symlink — fully resolved — and compared it to memory_dir as written. In the ordinary layout those agree, because memory_dir is a real directory. With store it is a symlink into another repository, so the two could never be equal, and the entire external layout had:
linkrefusing, with “the symlink points elsewhere”, the machine it had just wired itself;link --checkthat could never go green;statusreportingmemory is not wired on this machineagainst wiring that worked — a write through it reached the store, which is the thing that actually matters and the thing nobody was checking.
Both sides are resolved now. cd && pwd -P rather than readlink -f, matching the rest of these scripts and not depending on a readlink that only grew -f on recent macOS; a dangling link falls back to readlink and is still reported rather than crashing. A link pointing at a genuinely different directory is still refused — resolving both sides is not the same as comparing nothing, and there is a test for exactly that.
Two of this release’s regression tests are for the second run rather than the first. store ends by printing next: bash .floppy/run link, and status asks the same question afterwards, so the second call is the one a consumer actually makes — and it was the one that failed.
0.16.1 — 2026-09-05
Refresh .floppy/run: no. The fix is in link.
link encoded the checkout path as tr '/.' '--', and Claude Code folds a third character the same way: _. So a repository whose directory name carries an underscore got a project directory of its own that the harness never opens.
Nothing failed. link created the directory it had computed and reported success; --check agreed, because it asks this same line; status reported the wiring as present. This is precisely the second silent failure mode the script’s own header names — “the project directory is computed correctly, but Claude Code encodes the path differently, same result” — and it had been live against that description the whole time.
Measured 2026-09-05 from the harness’s own transcripts, which record the cwd a session actually ran in:
-home-amalaev-work-agents-harness cwd=/home/amalaev/work/agents_harness
-home-amalaev-work-ai-floppy cwd=/home/amalaev/work/ai_floppy
-home-amalaev--local-bin cwd=/home/amalaev/.local/bin
Two of that machine’s three consumers were affected, one across fifteen sessions. Case is not folded — /tmp/consensus-5Ob9Z2 keeps its capitals in the harness’s own directory name — so this stays a tr of three characters rather than becoming a general slug.
What it costs in practice is the memory not reaching the session, not notes being lost: writes go through the repository, and the stray directory only holds wiring. On the machine this was found on, one project had an empty directory there and the other had a hand-written stub saying “the memory moved into the repository, do not write here” — someone had hit this and worked around it without the cause being named.
If your checkout path contains _, re-run bash .floppy/run link after updating. It will refuse if a real directory stands where the symlink belongs — that is the guard working; look at what is in that directory, move anything real, then run it again. The stale directory under ~/.claude/projects with the underscore in its name can be removed once the new one is wired.
The regression test asserts the resulting directory name rather than recomputing the rule. The existing test at tests/test-memory-link.sh:30 recomputes it on purpose — it only needs to agree with the script — and that is exactly why nothing here was red: a check that restates the rule agrees with any rule, including a wrong one.
0.16.0 — 2026-09-05
Refresh .floppy/run: no. The change is entirely inside the plugin.
Every ceiling memory-lint enforces now warns at 96% before it refuses at 100%. Until this release exactly one of them did — the index, through a derived IDX_WARN — and chars_max, half_chars_max.<half>, note_chars_max and pointers_max went from silence straight to a failed run.
Reported from a consumer corpus of 158 notes across four halves (#1). Its flow half sat at 72694 of 75000 — 96.9% while lint printed clean: 158 notes, 166 pointers across 8 indexes. The budget had been set the same day at +10% over a 68311-character measurement: 66% of that headroom went in one day, four commits, with nothing said.
The band is not cosmetic, because of who a bare refusal lands on. It stops whichever session crosses a ceiling, and on a memory written from several machines that is routinely not the session that filled it. The ratchet says a number may be raised only in the same commit as the notes that needed the room — so that session must either raise a ceiling it did not fill, or prune a half it did not write, and pruning a neighbouring session’s notes is the one thing the wrap rite forbids outright. A warning reaches the session doing the filling, while trimming is still its own work.
- One fraction, shared by all five ceilings and derived from each, rather than a knob per ceiling: the argument the index section already made — two numbers kept in a fixed relation are two chances to set them wrong — does not get better when repeated five times.
- The per-half breakdown now prints with the corpus warning as well as the corpus failure. Naming which half grew is the whole point of the per-half tally, and “the memory is nearly full” without it sends the reader to measure by hand.
pointer_line_maxdeliberately has no band. It bounds one line, and a line at 165 of 170 characters is not approaching anything — it is a line that fits. The other five bound something that accumulates.
Warnings do not fail a run: every x in this release’s output was an x before it, and rc is unchanged for every corpus that was passing or failing. What changes is that a passing run can now be talkative. Two cases are worth expecting on the first run after the update:
- A corpus seeded by
inithaspointers_maxat the longest index it found, which is 100% of that ceiling by construction — so the index that set the number now says so. That is accurate: the next pointer added to that half fails. Plan the sub-index split, or raise the number deliberately. chars_maxseeded byinitcarries a tenth of headroom, so a freshly adopted corpus sits at ~91% and stays quiet.
And because of the first of those, init now prints the linter’s ! lines beside its verdict instead of only the verdict. It reported memory-lint is clean and dropped them — true, and not the same report as “nothing to do”. The warning that adoption itself creates was landing on the one run an adopter reads line by line, and being swallowed there.
0.15.1 — 2026-09-05
Refresh .floppy/run: no. The shim is unchanged; the fix is in init.
-
initwrote the wrong name into.gitignore. The line it appended was/<memory_dir>/local— the name the private scope carried before 0.6.0, and the onememory-workplace.shnow migrates away from — while the symlink that script actually creates is/<memory_dir>/<memory_private_dir>, defaultprivate. So a freshly initialised repository ignored a path nothing creates, and on the firstworkplacethe real symlink came out untracked: either committed into the code repository, which is the one thing the ignore exists to prevent, or tripping over the guard on every wrap.Nothing reported it, in either direction. The
.gitignorelooks configured, and the test suite asserted the wrong name back — with an exact-line assertion, which made it look rigorous.initnow takes the leaf frommemory_private_dirwhen the config already carries the key and defaults toprivateotherwise, and a regression test refuses the pre-0.6.0 name outright.A repository initialised before this release has the wrong line already. Re-running
initdoes not remove it —initnever deletes: fix it by hand, or add the right line beside it. Check withgit check-ignore -v <memory_dir>/private; on effectssdk, the first consumer, the line had already been corrected by hand. -
README said
init“adds the machine-local memory directory to.gitignore”. Nothing about that scope is machine-local — a hundred lines below, the same file explains that this is exactly why the name was dropped in 0.7.0.
0.15.0 — 2026-09-05
Refresh .floppy/run: no. The change is entirely inside the plugin.
memory-lint now tallies the corpus by half, and quota.lock accepts optional half_chars_max.<half>=N keys (half_chars_max.root for notes sitting directly in the memory directory). A half with no key is unbounded, so a corpus that sets none behaves exactly as before; when the corpus-wide chars_max trips, the failure now prints the breakdown beside it whether or not any per-half key is set.
What prompted it was a measurement that also says what not to build. On a consumer repository, across 42 sessions of transcripts, a session reads the root index (7 336 characters, loaded every time), one half index (3 311–7 010), and a median of 8 notes — about 40 000 characters of a 485 000-character corpus. The rest never enters the window. So the corpus-wide cap is not a proxy for context cost: the index tree already keeps cold notes out for free, and a “cold storage tier” underneath it would save nothing that is being paid.
What the cap does do is force pruning — and that is where a single number fails. That corpus is worked from two machines, its halves were 3.4x apart in size, and the half that grows is not the one whose session hits the ceiling: the cap was raised three times in eleven days, each time by a session adding a legitimate note to a different half. A per-half budget puts the ceiling where the growth is.
One caution from the same measurement, worth repeating because it nearly became an argument for the tier that is not being built: 49 of that corpus’s 156 notes had never been opened in any local transcript, which reads as a third of the memory being dead weight. Forty of the forty-nine belonged to the half worked on the other machine, whose transcripts are not on the machine doing the counting. “Never used” measured on one machine is a question about coverage, not a fact about the corpus.
No new knob for the always-loaded index: index_chars_max in .floppy/config already caps it, and that number is a fact about the harness rather than about any corpus.
0.14.0 — 2026-08-26
Refresh .floppy/run: yes, once — and much less often after that. The shim in a consumer repository is now a stub: it finds the plugin and execs it. The verb table and the config parser moved into the plugin (scripts/run, scripts/lib-config.sh), where plugin update delivers them.
Asked what the shim is even for, the honest answer had two halves and only one of them justified a copy in someone else’s repository. Measured over the 24 commits that have touched shim/run: 14 changed the config parser, 12 the plugin resolver, 6 the verb table — and at least 7 of the 24 changed only the parser or the table. Every one of those obliged every consumer to re-copy a file for something a plugin update could have carried. Worse, that class is the silent one: a stale parser does not announce itself, it keeps applying the old default. The resolver cannot move — code that finds the plugin cannot live in the plugin — but its failures are loud, and now it is all that travels.
The staleness therefore reverses direction, and improves:
- A new verb needs no refresh anywhere. It appears in the plugin, and every repository that has ever run
initcan call it. This was the case that motivated the unknown-verb message in the first place, back whenparitywas added; it cannot happen again. - A new config key or default needs no refresh either.
- A plugin older than the stub is now a full stop, not a partial one. A pre-0.14.0 plugin has no dispatcher, so no verb runs against it, including verbs whose scripts it does have. Before, only the missing verb failed. The refusal names what is missing, which side is old, and the resolved plugin root. This is the release’s one deliberate loss, and it is asserted in
tests/test-shim.shrather than left to be discovered.
Also, from the same measurement session:
CURSOR_PLUGIN_ROOTis now consulted, right afterCLAUDE_PLUGIN_ROOTand before any cache guess. Cursor exports it the way Claude Code exports its own (measured 2026-08-26 against a cross-harness plugin that branches on the two by name); floppy never tried it. On the owner’s machine~/.cursor/plugins/cache/floppy/floppy/exists and is empty, so the cache branch resolves nothing there and only the local symlink saves the run — an answer from the harness beats three guesses.init’s bootstrap search learns the same variable.- The stale-copy hint now names this file by its own absolute path rather than one derived from the repository root, which the stub no longer resolves.
tests/test-docs.sh read the config keys out of shim/run to check the README documents them. After the move that grep matched nothing and the loop asserted nothing — green, and checking nothing. It reads scripts/lib-config.sh now and fails if the key list ever comes back empty.
482 asserts, up from 479.
0.13.0 — 2026-08-26
Refresh .floppy/run: yes (the verb table lost an entry and the config parser lost a key). parity is gone, with everything around it — the --scaffold generator that 0.12.0 added the same day, the commands_dir config key, and the localized commands section of check.
The feature was built on an assumption nobody had checked. The premise was that a team which works in its own language needs the rite in that language, so the plugin must guard two copies of every procedure against drift. Asked what part of a command file a person actually reads, the answer turned out to be: the command name, the description in the picker, and argument-hint. The body is a prompt. It is loaded into the model’s context on invocation and is never shown to anyone. Claude Code’s own documentation also states that custom commands and skills are now the same thing: .claude/commands/wrap.md and .claude/skills/wrap/SKILL.md both produce /wrap and behave alike.
So the work the plugin was automating — translating hundreds of lines of prose — produced no text a user would ever see, and created the second copy that parity then existed to police. Removing the copy removes the drift, the check, and the generator together.
bash .floppy/run parityis no longer a verb. An old shim that still has it will report a missing script rather than fail obscurely; refresh the copy.commands_diris no longer read from.floppy/config. A line left in the file is ignored, as any unknown key is.checkno longer has alocalized commandssection, and thewrapskill no longer lists a drifted command among the things to fix before committing.scripts/parity.shandtests/test-parity.share deleted. The two shim tests that usedparityas their example verb now usestatus.
If localized rites are ever wanted again, the cheap version is the one this release did not have to build: translate description and argument-hint, which is the whole of what a person sees, and leave the body alone.
479 asserts, down from 536.
0.12.0 — 2026-08-26
Refresh .floppy/run: no (the shim is unchanged; the new behaviour is a flag on a verb it already dispatches). parity can now write the files it checks, and the README says how to write them by hand.
Asked how to create .claude/commands/wrap.md, this repository had no answer. parity had guarded localized command files since 0.9.0 and the README explained the check at length, but nothing anywhere described the artifact: a repository was told its two copies must not drift, and never told how to make the second copy. The only working example lived in tests/test-parity.sh.
bash .floppy/run parity --scaffoldwrites one skeleton per rite intocommands_dir: the numbered headings and everybash .floppy/runcall, verbatim, with the English prose replaced by a marker that says how many lines were dropped and where to read them. It never writes over a file that exists — a translation ofwrap.mdis hours of work and lives in exactly the path the generator wants.- The generator is in
scripts/parity.sh, next to the checker, not beside it. The two have to agree on one thing — what in a rite is load-bearing — and two files that must agree are the arrangementparityexists to police. - Calls that appear only inline in prose are the case that makes this more than a copy: dropping that paragraph would drop the call. They are re-emitted under the step whose prose mentioned them; calls outside any step get a section of their own.
- The generated file passes
paritybefore a word of it is translated, and the test asserts exactly that round trip. A generator whose output starts red teaches its reader that the check is wrong. - README: a new subsection on making the command files — all three rites, not one; the numbers of the
## N.headings and thebash .floppy/runlines survive translation, the titles and the argument placeholders do not.tests/test-docs.shnow asserts the subsection is there.
Also: the site test now checks the changelog against the version in plugin.json, so a release that moves the version and forgets this file goes red instead of publishing a site whose newest entry is the release before it.
536 asserts, up from 481.
0.11.0 — 2026-08-25
Refresh .floppy/run: yes (the shim derives one more path). commit now closes a third repository — the workplace one — where before it refused to commit into it at all.
Reported from a Linux machine: /wrap could not commit a note written into the private memory. Reproduced here, in both commit_push modes, so the mode was never the cause:
x .agent-memory/private/private-fact.md — not changed: wrong path, or the
edit was lost
(a parallel session may have written between check and commit)
- The gates knew two shapes and this was the third.
FLOPPY_MEMORY_EXTERNALcovers the whole memory being foreign; the common shape is the memory living in the code repository with only<memory_dir>/<private_dir>symlinked into the workplace repository. Neitherwrap-guard.shnorwrap-commit.shcontained the word. So the guard asked THIS repository’sgit statusabout a path this repository ignores by design, was told nothing had changed, and refused — while the note sat on disk andworkplacehad just reported that a write through the link lands in the workplace repository. - The diagnosis it printed was wrong twice over: the path was right, the edit was not lost, and no parallel session was involved. A gate that refuses is fine; a gate that refuses with a false reason sends the next hour somewhere else.
commitnow stages, commits and pushes those files in the workplace repository, the way it already did for a store, and leaves the code repository untouched. A call naming files of both kinds closes both.- The
checkline that said workplace changes must be committed “there separately” now says to name them in the file list instead. It was true until this release and would have sent the human to commit by hand what the next call does — including notes this session never claimed.
481 asserts, up from 466.
0.10.0 — 2026-08-25
Refresh .floppy/run: yes (the shim now refuses a variable that names a plugin which is not there). Expect a red lint on the first run in a project that has a workplace memory repository: notes there have never been checked, and now they are.
- The shim stops instead of searching on when
AI_FLOPPY_HOMEis set to a directory holding noscripts/*.sh. A cache path is a guess and skipping a wrong guess in silence is right; a variable somebody set is a statement about where the plugin is, and skipping THAT in silence is how a call ends up against another copy. Measured while fixing 0.9.1: the variable was pointed at a directory with no plugin, the search fell through to the Claude Code cache, an older copy answered, and a script that had just been fixed was reported as still broken — with nothing in the output naming the copy that ran.CLAUDE_PLUGIN_ROOTis treated differently on purpose: the harness sets it per plugin, so a call from inside another plugin’s skill can carry that plugin’s root through nobody’s mistake. That one warns, names the root used instead, and carries on. - The memory linter reaches the private scope — the symlink into the workplace repository, which every one of its queries used to exclude by path. It gets the per-note invariants: frontmatter,
name/description,metadata.type,metadata.evidence, slug uniqueness,[[link]]resolution. Measured on the first consumer’s store the moment it ran: three notes, three with nometadata.evidence, invisible because nothing reached them. - What the private scope deliberately does NOT get: the index rules, because the scope is flat by design and its README is prose rather than pointers — demanding
MEMORY.mdandINDEX.mdthere would report every note an orphan on the first run; and the quota, because those numbers live in the committed memory’squota.lockand are facts about that corpus. Borrowing them here is the same borrowed cap this project refuses elsewhere.
466 asserts, up from 452.
0.9.1 — 2026-08-25
Refresh .floppy/run: no. The shim is unchanged; the fixes are in the scripts it calls.
checkno longer reports a red memory linter with no reason. The linter has three outcomes, and the code knew two: exit 2 is a refusal before any checking — no memory layout in this repository, or a scope name it cannot use — and it prints a sentence rather than ` xlines. Those lines were what got counted, so the human sawMEMORY LINT IS RED, 0 problem(s)` and nothing else. It stays red, because a wrap that cannot read the memory should not proceed quietly, but it now prints the linter’s own sentence. This is the message a person meets when the wrong project is active in a harness holding several, so throwing it away was expensive.commitno longer dies on a file this session deleted withgit rm. Reported from a second session.git rmstages the deletion, which takes the path out of both the worktree and the index, andgit add -- <that path>then fails with “did not match any files” — killing the whole commit. Measured while fixing:git add -A -- <path>fails identically, so widening the add is not the fix. An already-staged deletion is simply left alone; it is in the index already, which is what the add was for. A path that matches nothing anywhere stays in the list and still fails, so a typo in the file list is as loud as before.tests/run.shruns the files in parallel, one job per core by default. Measured on a 12-core mac: 71.8s serially, 20.7s parallel, identical results and identical output order. Sixteen jobs measured slower than twelve, so the default is the core count and not more.FLOPPY_TEST_JOBS=1restores the serial run, whose output streams live — which is what you want while chasing one failure.
452 asserts, nine of them added here.
0.9.0 — 2026-08-25
Refresh .floppy/run: yes. The shim reads one new config key.
statuses_regress_marks — the words that mark a regression in the direction cell of a trend table, in the consumer’s own language. With the key set, wrap-guard protects only the rows carrying one of those words; any other row may be deleted.
Why: the guard was stricter than the rule it enforces. Both AGENTS.md and docs/statuses/README.md of the first consumer say “a metric marked worse is not deleted”, and the reason given is that a vanished bad number is how a regression hides. The guard protected every row instead, which made each one immortal — including one-time “done” facts that can never move again. A rewritten state file then grows exactly like the append-only journal it was split away from: the first consumer hit its character cap with 24 of 40 process rows being finished facts rather than live indicators.
An unset key keeps the old behaviour, so no consumer loses a guard by updating. The words are configured rather than built in because this file already learned that matching one language’s word breaks every other consumer.
0.8.0 — 2026-08-25
Refresh .floppy/run: yes (the config parser no longer reads three keys). Breaking, in name only: a config that still spells a key the old way is now read as if the key were absent.
- The compatibility fallbacks for
memory_repo,workplace_repo, andmemory_local_dirare gone.public_repo,private_repo, andmemory_private_dirare the keys. The fallbacks were kept so that a config written before 0.6.0 or 0.7.0 would keep working; checked before removing them, and the only.floppy/configoutside this repository already spells every key the new way, so the compatibility was documenting keys nobody has. - The messages that named a removed key now name the current one:
storeasks forpublic_repo,workplaceasks forprivate_repo, and the linter’s refusal of a scope name with regex metacharacters saysmemory_private_dir. A message that names a key the parser ignores is worse than no message. - The config template that
initwrites offers the current names too. - README: the key table had a blank line inside it, which split it in two and left everything from
workplace_project_keydown rendering as raw pipes rather than a table. The paired keys sit next to each other again. - README: the
project_keyrow described the scopes asprojects/<key>/sharedandprojects/<key>/private, which 0.7.0 replaced; and the example config, the checkout tree, and two sentences still used the pre-0.7.0 repository key names.
437 asserts, down from 440: the three that vanished asserted README documents memory_repo, workplace_repo, and memory_local_dir.
0.7.1 — 2026-08-25
Refresh .floppy/run: no. Needed while migrating to 0.7.0.
- A view left pointing at a scope that a release moved is repointed, not refused. 0.5.1 did this for the link inside the consumer repository and stopped there; the view is the same kind of thing — a symlink this verb created, with nothing behind it once the scope moved — and refusing it made the 0.7.0 migration wait on a manual
rmon every machine. Found one command into that migration. A view that still resolves somewhere else is refused as before, because that one may belong to another store.
440 asserts, up from 434.
0.7.0 — 2026-08-25
Refresh .floppy/run: yes (four new keys). The scopes moved under a namespace directory — the old ones are refused with the git mv printed.
The model is written down now: docs/memory-model.md. Three questions per note, independent — who may read it (a repository boundary), what it is about (projects/<key> or common), and where it is true (everywhere, one workplace, one machine). The layout had been renamed three times in a day because that model existed only in whoever was editing.
- Scopes are
public/projects/<key>andprivate/projects/<key>. The leaf that repeated the audience (shared,private) is gone. The namespace directory is always present, including in a repository that holds only one: the alternative makes the path depend on whether two config URLs are equal. - New keys
public_repoandprivate_repo, replacingmemory_repoandworkplace_repo, which are still read. The old pair named an audience with a validity word and made two independent questions look like one axis. - New keys
machine_keyandworkplace_key. They name the validity directoriesmachines/<machine>/andworkplaces/<place>/inside a scope, for a note that is not true everywhere — the cell no earlier layout could express. Both are optional and nothing creates the directories yet. - Migration is refused, not performed, exactly as in 0.5.0 and 0.6.0: the verb prints the
git mv. Wiring left pointing at an earlier scope is repointed without a manualrm.
0.6.2 — 2026-08-25
Refresh .floppy/run: no. Take it on any machine that has migrated to private, or status lies to it on every call.
statusspelled the private scope in, and 0.6.0 turned that into a permanent false alarm. The section looked for<memory_dir>/localwhile the wiring, correctly, was<memory_dir>/private: a properly wired machine was told to runworkplaceon every call, and a genuinely broken link under the new name printed nothing at all. Found by reading the report on a machine that had just migrated — no test failed, because no test asserted the section against a wired repository. The name now comes from the environment, with the pre-0.6.0 variable read as a fallback so a repository sitting behind an unrefreshed shim is judged by the name that shim actually uses.- The linter’s fallback for the same variable said
localwherememory-workplace.shsaidprivate. Under an unrefreshed shim the rule “committed memory must not link into the private scope” therefore guarded a directory that no longer existed — the way of applying to nothing that this variable was introduced in 0.4.0 to prevent. Both now end inprivate.
430 asserts, up from 423. The first draft of the new ones passed against the unfixed code: the sandbox had no .floppy/run, so every “does not say X” assert was satisfied by a run that died before printing anything. They now assert the section printed before asserting what it says.
0.6.1 — 2026-08-25
Refresh .floppy/run: no.
- The rename left the old
locallink behind, pointing at a path thegit mvhad emptied — two links in the memory directory, one of them broken, on every machine that migrates.workplacenow removes it, but only while it dangles: there is nothing behind a dangling symlink this verb created itself. A link under the old name that still resolves points at something real and is left alone. - The refusal named the layout by the version it predates (“before 0.5.0”) while refusing a 0.5.1 layout, and the commit line it suggested still spelled the old scope. Both were read on a live migration a minute after the release that made them wrong.
423 asserts, up from 415.
0.6.0 — 2026-08-25
Refresh .floppy/run: yes (new key name). The private scope is renamed — migrate it the same way as 0.5.0, and read why first.
localbecameprivate, in the scope (projects/<key>/private), in the link inside your repository (<memory_dir>/private), in the view, and in the config key (memory_private_dir;memory_local_dirstill works). The name was left over from the days when that directory really was machine-local and gitignored. It has been neither since it became a symlink into a repository shared by a workplace’s machines — and the name kept saying otherwise, convincingly enough that the owner of the corpus read it as “facts about this machine” and asked where those go. They go tomachines/<name>/of the workplace repository, which is synced like everything else: that scope answers “where is this true”, not “who may see it”.- The old scope is refused with the
git mvprinted, exactly as in 0.5.0 and for the same reason. A link left pointing at any earlier scope of the same repository is repointed without a manualrm(0.5.1).
415 asserts, up from 414.
0.5.1 — 2026-08-25
Refresh .floppy/run: no. Take it before migrating a second machine.
- Wiring left by a pre-0.5.0 run is repointed, not refused. After the scope move,
<memory_dir>/localstill pointed at the old scope of the same repository, and the verb stopped with “the symlink points elsewhere” — so every machine needed a manualrmbefore it could be rewired. That link is wiring: it holds no content, and this verb recreates it on each machine anyway. A link into a different repository is still refused, because that one may be another workplace and this verb does not guess. readlink -fleftmemory-workplace.sh. BSD readlink on macOS has no-f, so that comparison had never worked there — it was reached only when the link already disagreed, which is why no test and no machine had shown it.
414 asserts, up from 406.
0.5.0 — 2026-08-25
Refresh .floppy/run: yes (new key, and the shim resolves the new layout). Scope names changed — read the migration note below before updating.
The layout is now keyed by project, not by repository. What a person opens is agents_memory_dir/<key>/, holding shared and local; the clones moved into agents_memory_dir/.clones/<repository>/, one per URL as before. A flat parent mixed two alphabets — project names beside repository names — and telling them apart needed the config open beside the terminal.
- New key
project_key. One key names this project in every memory repository it uses and names its directory in the view.memory_project_keyandworkplace_project_keystay as overrides, for the project whose scope really is named differently in two repositories. - The scopes are siblings now:
projects/<key>/sharedin the store,projects/<key>/localin the workplace repository. They wereprojects/<key>/memoryandprojects/<key>itself, so with one repository serving both roles the second contained the first — private notes and shared memory in one tree, and--migrate-localwalking a corpus that was not its own. N projects were already separated by<key>; what confused them was the nesting. - The old scope names are refused, not migrated. The verb stops and prints the
git mvcommands. Moving somebody’s notes unasked is what this plugin refuses to do everywhere else, and doing it on ONE machine is worse than not doing it: until the other machine also runs 0.5.0, one writes the old path while the other reads the new one and the memory forks with nothing red anywhere. Update every machine first, then move the scopes once. <memory_dir>and<memory_dir>/localpoint at the view, not into a clone, so a repository URL can change without rewiring every worktree. The write probe at the end of each verb proves the whole chain, so the extra hop is checked rather than trusted.- The view links are relative when the clone sits under the same parent:
agents_memory_dircan be moved as one directory. They are never committed — in the adopted legacy layout, where the parent itself is a checkout, the containing repository is told to ignore them. initlearned--agents-memory-dir, and now wires the store by calling the shim rather than the store script with a hand-built environment. That copy of the path resolution had already fallen behind the shim’s.- The nesting refusal walks up to the nearest existing ancestor. With clones under
.clones/, testing only the immediate parent would have missed the case and cloned into the repository below it.
406 asserts, up from 405.
0.4.2 — 2026-08-25
Refresh .floppy/run: yes (the shim resolves the checkout paths, and one new config key).
Two defects, both found by asking what happens when memory_repo_dir and workplace_memory_dir name one directory, and both measured before they were fixed.
- Notes could be published to the wrong repository, with
okon every line. The configured URL was read only on the clone path. With a checkout already at that directory, the second verb skipped its clone, never compared the remote, wired the link, and reported “a write through the link lands in the workplace repository” — while the notes landed in the store, wherecommitwould have pushed them. Now: the checkout directory is derived from the URL under one parent (agents_memory_dir), so two repositories cannot collide; and any existing checkout has itsorigincompared with the configured URL, with a refusal naming both. That second check also catches an unrelated repository sitting at the path, which never needed a collision. - The wiring symlink was committed into the store. With a store,
local/is created inside the store’s working tree, holding an absolute path: on a machine whose checkout lives elsewhere it is a dangling link, and the pathprojects/<key>/memory/local/memory/local/...recurses without limit. It is per-machine wiring, and every machine runs the verb anyway, so the store is now told to ignore it. - New key
agents_memory_dir(default$HOME/agents_memory), with a worked example of the resulting tree in the README — the layout was described in words only, and “the parent” named nothing a reader could point at.memory_repo_dirandworkplace_memory_dirremain as per-machine overrides. Nothing to migrate: a checkout already sitting at the parent is adopted once itsoriginmatches, and the verb says so. - A clone that would land inside another checkout is refused, with the two commands that undo the nesting.
scripts/lib-checkout.shnow holds the half of the two verbs that was duplicated — and had already drifted.
405 asserts, up from 377.
0.4.1 — 2026-08-25
Refresh .floppy/run: yes (the install hints it prints name the repository, and the repository was renamed).
- The repository is
spscream/ai-floppy, wasspscream/ai_floppy. GitHub redirects the old name for both git and the web, so an existing marketplace entry and any command someone already wrote keep working; what would not have kept working is the hint a failing shim prints, which would have named a repository nobody can find. Nothing else moved: the plugin and the marketplace are still bothfloppy,floppy@floppyinstalls as before, andAI_FLOPPY_HOMEkeeps its underscore — it names an environment variable, not the repository. - No behaviour changed anywhere. This version exists so that
plugin updatecopies the corrected shim at all: it compares version strings, and under an unchanged one it would report “already at the latest version” over a shim still printing the old name.
0.4.0 — 2026-08-25
Refresh .floppy/run: yes (new verb, four new config keys).
- New verb
store: wires memory hosted in another repository — clone or pull, link, add the ignore line, and then the step that actually proves it, writing through the link and confirming the file appears in the store. Idempotent, per machine and per worktree,--checkreports without changing. It refuses rather than guesses when a real directory sits where the symlink belongs: those notes may be the only copies of something. initlearned--memory-repo/--memory-key/--memory-repo-dir, and writes the index through the new symlink so it lands in the store. Half the pair is refused rather than half-applied. Without the flags nothing changes: the generated config documents the option commented out, because a project pointed at a store it never chose would write its notes into somebody else’s repository.- New guard, and it catches a state that was comfortable to live in: a memory directory that is gitignored and inside this repository. That is the shape a half-done external setup takes — ignore line added, symlink never created — and in it notes are written and read normally while nothing will ever commit them. Previously the failure surfaced at the end of the session wearing the wrong name (“not changed: wrong path, or the edit was lost”).
memory_local_dir(defaultlocal) names the machine-local scope instead of the linter spelling it in. Not cosmetic: the rule “committed memory must not link into that scope” is what protects against links dead on a second machine, and under any other name it had been silently applying to nothing. A name with regex metacharacters is refused rather than matched wrongly.
377 asserts, up from 328.
0.3.0 — 2026-08-25
Refresh .floppy/run: yes (new config key).
- The memory size caps left
memory-lint.sh, into two homes rather than one.index_chars_maxis a fact about the harness — its session loader truncates the memory index past a limit of its own, measured at ~24986 characters on Claude Code — so it is a.floppy/configkey with the number shipped as a default.pointer_line_maxis a fact about your corpus, the same kind aspointers_maxbeside it, so it joinsquota.lock(default 170). Asking a project to measure the first one would be asking it to measure somebody else’s tool. - The index-size warning threshold is no longer configurable at all. It had to stay in a fixed relation to the cap, which is two chances to set it wrong; derived at 96%.
- Both new values are rejected by name when non-numeric, and fall back to the default rather than comparing against garbage.
0.2.3 — 2026-08-25
Refresh .floppy/run: no. But if your memory is external (0.2.2), take this release: without it the memory gate silently passes.
- Fixed, and it was serious:
memory-lint.shwalked the memory with plainfind, which does not descend into a symlinked starting point. With memory hosted in another repository the memory directory is a symlink, so every check iterated over nothing and reportedclean: 0 notes— and that lint iswrap-commit’s first gate, so the whole memory gate was dead in that layout. tests/run.shdispatched each test through a barebash, resolved via PATH. On macOS a newer bash normally sits ahead of/bin/bash, so a run meant to prove bash 3.2 compatibility re-tested bash 5. It now uses$BASHand prints which interpreter it is using.- CI added: the suite runs on ubuntu (bash 5) and on macOS with
/bin/bashexplicitly, which is the job that actually earns its minutes.
0.2.2 — 2026-08-25
Refresh .floppy/run: yes (the shim derives the new layout).
- Memory can live in a repository other than the code’s — for a checkout you do not own, or a policy keeping notes and code apart. Point
memory_dirat a symlink into another git repository and gitignore it;guardasks that repository what changed and answers in the paths you typed,checkshows the notes going out,commitcommits and pushes both from one file list, andstatusreports the store so a clean tree stops meaning “nothing pending”. - The layout is derived from where
memory_dirresolves, never declared: a flag in the config would disagree with the filesystem exactly when a symlink failed to be created. - A store that cannot be pushed fails
commitloudly instead of printing “session closed” over unpublished notes. Amemory_diroutside git altogether is named as such — it reads and writes fine and publishes nothing. - Setup is still manual (clone, symlink, ignore line).
initdoes not do it yet.
0.2.1 — 2026-08-25
Refresh .floppy/run: yes — and this is the release that makes future refreshes visible.
- The shim compares itself against the plugin’s copy on every call and prints one line on stderr when they differ, with a ready
cp. Most of the ways a shim goes stale are silent: a corrected config parser or search order just keeps doing the old thing. - A warning, not a gate — an old shim usually still works, and refusing to run would turn a nudge into an outage on whichever machine pulled first.
- The hint is a plain
cpand not arefreshverb, because a shim old enough to need refreshing is too old to know the verb that would do it. For the same reason this guard only starts working after one manual refresh.
0.2.0 — 2026-08-25
Refresh .floppy/run: yes (new verb).
- New verb
parity: compares localized command files against the English skills they translate — the set ofbash .floppy/run <verb>calls and the sequence of numbered headings, and deliberately nothing about wording, section titles or length.wrap’scheckgates on it. - Both stale-copy failures around the shim used to be mute and now name which side is behind: an unknown verb (this copy is old) and a verb whose script is missing (this machine’s plugin is old).
- New config key
commands_dir(default.claude/commands).
0.1.1 — 2026-08-25
Refresh .floppy/run: yes.
- The shim refuses to resolve into a plugin directory that holds no
scripts/*.sh. A cache from a moment whenscripts/was empty was being treated as an install, and every verb died with “No such file or directory” pointing inside the cache. - Version bumped for its own sake as well:
plugin updatehad been reporting “already at the latest version” and copying nothing.
0.1.0 — 2026-08-25
First release. The session rite extracted from the project it grew in: the shim, eight verbs, five skills, memory conventions, and the guards — file-list gate, memory linter, per-session lock — that keep the rite honest.