Skip to content

Releases: spscream/ai-floppy

v0.27.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 15:54
1a51b8c

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:

  • store runs once per machine, not once per working copy. A worktree
    needs no step of its own.
  • link is 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_dir is a fork of the corpus and is now
    treated as one. guard refuses while it stands, and store --migrate moves
    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 -A in 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 -s now has its exit status read. Every ok linked line in this
    plugin was printed unconditionally after an ln nobody 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, then ok 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, workplace and link all report the failure
    and stop now; ln's own message is left on stderr, because it names the errno.
  • workplace says 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 which store refused to touch it, which is how the
    order the tool suggested bricked a tree. It now stops with x no memory at … — there is nothing for the private scope to sit in, says to run the store
    verb first, and ends with Nothing was cloned, linked or written.
  • check and status judge the wiring with one piece of code. They had
    drifted: status carried 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 symlink against check: clean and pushed. The
    gate was the optimistic one of the pair, which is the wrong way round for a
    gate.
  • link stopped calling a correct worktree the wrong repository. Its
    refusal was x 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.log is excluded, not gitignored. Both language versions of
    the install guide said .gitignore covers it. It does not, deliberately:
    memory-heat.sh writes its ignore line into .git/info/exclude, and its
    header records why. The first release of the verb appended to .gitignore
    itself, so the first heat on 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 section init
    appends to a consumer's AGENTS.md, and the install guide with it, said the
    directory holds config and nothing else. workstatus runs
    .floppy/workstatus-project.sh when 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, and heat.log sits beside
    config.
  • A watched_files entry naming .floppy/run was typed by hand. No
    released init ever wrote one: every version from 0.11.0 to 0.26.0 writes
    watched_files commented 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.0 section of this file now carries it, with the measurement — the
    release page for v0.26.0 was published before that and is not rewritten. And
    init names 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/run is still in the tree,
    because dropping the entry while the file is tracked takes guard .floppy/run from rc 0 to rc 1. It edits nothing either way: .floppy/config
    is yours.

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 19:46
799e532

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:

  • init writes .floppy/config and nothing else. Your repository carries
    data, not code. Its refusal when the plugin had no shim/run is gone with
    the copy it guarded.
  • skills/init/SKILL.md drops its hand copy of the plugin search — 33
    lines that existed because init ran before .floppy/run existed. The test
    that executed that fenced block (tests/test-init-bootstrap.sh) goes with
    it. Prose cannot be executed, so tests/test-skills.sh resolves 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>/run and "one directory above" passing.
  • shim/run still ships, byte for byte. It is what a pre-0.26.0 consumer
    calls, and it cmps 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.

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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 22:53
f999cb2

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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 22:35
b25e1b8

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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 22:56
02246d4

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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 22:48
10ca3df

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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 13:27
d53c3be

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

Choose a tag to compare

@spscream spscream released this 13 Sep 13:25
4787097

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

Choose a tag to compare

@spscream spscream released this 13 Sep 13:25
95a2f75

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

Choose a tag to compare

@spscream spscream released this 08 Sep 12:39
37cc27e

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:

  • lint checks 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 no metadata.evidence and one had a name that did not match
    its file, so a [[link]] to it resolved by luck. It does not apply the
    index tree or quota.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 — start names the scope — not a pointer's.
  • commit peels 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.