Repository navigation
Releases: spscream/ai-floppy
Release list
v0.27.0
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 thestore
verb 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.gitignore
itself, so the firstheaton every machine left that file modified — and
wrap'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 sectioninit
appends 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 beside
config.- A
watched_filesentry naming.floppy/runwas typed by hand. No
releasedinitever wrote one: every version from 0.11.0 to 0.26.0 writes
watched_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. The
0.26.0section of this file now carries it, with the measurement — the
release page forv0.26.0was published before that and is not rewritten. And
initnames 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.md
is 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/config
is yours.
v0.26.0
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/runIn 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.
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.
v0.25.1
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,
commit
would 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.
v0.25.0
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.
v0.24.1
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.
v0.24.0
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.
v0.23.0
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.
v0.22.0
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.
v0.21.0
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.
v0.20.0
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/runMinor 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.