Config reference
The file contains one key=value line for each setting. The dispatcher (<plugin>/scripts/run) reads it, and exports each value as a FLOPPY_* variable for the scripts.
All keys are optional. The table shows the value that each key has if the file does not contain it.
| key | default | what it controls |
|---|---|---|
memory_dir | .agent-memory | the directory of the memory of this repository |
memory_private_dir | private | the name of the private scope in the memory: facts about this project that the code repository must not carry, such as somebody else’s checkout or an access note. The workplace repository holds them, so other machines do read them. Facts about one machine go to machines/<name>/ of that repository instead. Only the name is a setting; the rule is not — committed memory must not link into this scope, and the check uses this key. The same rule covers common/, whose name is fixed rather than configurable: it is written into the store paths themselves, and a name settable in one of the two places would be a name that drifts |
public_repo | (not set) | the git URL of the repository that holds this project’s public memory when the code repository cannot. Set project_key also. Then run the store command one time for each machine. Not for each working copy: the memory is addressed by repository, and a worktree needs no step of its own |
private_repo | (not set) | the git URL of the repository that holds this project’s private memory: facts the team must not get. The workplace command wires it |
machine_key | (not set) | the name of this machine in the memory repositories, chosen by you. hostname is not used: on one of the author’s machines it is WIN-GVR0V5UPOD7. Only needed for a note that is true on one machine |
workplace_key | (not set) | the name of this workplace, when one private repository serves several of them. Only needed for a note that is true at one workplace |
project_key | (not set) | the name of this project in every memory repository it uses, and the name of its directory in agents_memory_dir. The scopes are public/projects/<key> (in public_repo) and private/projects/<key> (in private_repo) |
memory_project_key | (the value of project_key) | use a different key in public_repo only. Needed when the same project has two names in two repositories |
workplace_project_key | (the value of project_key) | the same, for private_repo |
agents_memory_dir | $HOME/agents_memory | holds one directory for each project, and the clones in .clones/. Each repository URL gets one clone. The name of the clone comes from the URL. floppy derives it; you do not set it. Two different repositories thus cannot use one clone directory. A clone from an earlier layout — under the parent directly, or at the parent itself — is used as it is, but only if its origin is the configured URL. See the example above |
memory_repo_dir | (derived) | replaces the derived checkout path of public_repo on this machine. Set it only if that checkout cannot be below the parent directory |
workplace_memory_dir | (derived) | the same replacement, for private_repo. Also the opt-out from the shared clone: several projects with one private_repo share one working tree by default, and this key gives one project a clone of its own. See “When two projects should not share one working tree” below |
memory_language | en | the language of the memory notes. No script uses this key. A session reads it from this file. It does not control the language of the answers to a human |
index_chars_max | 24500 | the maximum number of characters in the memory index. The value comes from the session loader of the agent application. That loader removes text above a limit and does not report the removed section. This is a fact about the application, not about your project. The limits for the corpus are in quota.lock |
note_stale_days | 180 | how long a note’s metadata.as_of may stand before lint names it. It warns and never fails: old and wrong are different things, and only a person who knows the area can tell them apart. Notes with no as_of are counted, not listed, so the field can arrive into a corpus that already exists. Lower it if your memory is mostly about a fast-moving dependency. The knowledge base in this repository ages a note after 90 days, but by a different mechanism: scripts/knowledge-rot-check.py --days, which never reads this file |
statuses_now | docs/statuses/NOW.md | the state file. start reads all of it. wrap keeps it correct |
statuses_now_chars_max | 12000 | the maximum number of characters in the state file. wrap-guard refuses a commit above this limit |
statuses_personal | <memory_dir>/<memory_private_dir>/machines/<machine>/NOW.md | the second state file: one person’s thread of work on one machine — what is half-done, where to resume. start reads it when it exists, wrap writes it. It is inside the memory, so commit sends it to the private store and never to this repository. The machine part comes from machine_key, or from hostname when that is not set. Leave the key unset unless the derived path is wrong: writing it here puts one machine’s path into a file every machine reads. It has no character limit, unlike statuses_now, because only the session that wrote it reads it |
statuses_regress_marks | (empty) | the words that mark a regression in the direction cell of a trend table, in your own language. Use a comma between them. wrap-guard then refuses to delete only the rows carrying one of these words. While the key is empty, no trend row may be deleted at all — safe, but it makes a rewritten file grow like an append-only one, because a one-time “done” row can never leave |
watched_dirs | docs | the directories, in addition to memory_dir, that wrap can commit. Use a comma between the names |
watched_files | AGENTS.md | the single files that wrap can commit. Patterns are permitted. Use a comma between the names |
commit_push | auto | the action after each commit. auto runs git pull --rebase, then pushes. never omits both. Use never if the repository has no remote, because auto fails there. To omit the push one time only, use --no-push |
private_repo and workplace_project_key have no default value. This is deliberate. With a default, a repository could write into the private memory of a different person.
Where the checkouts are
agents_memory_dir contains two things: one directory for each project, and a hidden .clones/ with one clone for each memory repository.
You open the project directories. floppy makes the clones.
An example. The configuration of one project is four lines:
project_key=acme
public_repo=git@example.com:team/notes-store.git
private_repo=git@example.com:workplace/agents-memory.git
agents_memory_dir=~/agents_memory
A value starting with ~/ or $HOME/ means your home directory — those two spellings only. The config is not a shell: ~user, other variables and anything mid-value stay literal. Since 0.24.1; before that, every path had to be written out absolute.
The result on disk is:
~/agents_memory/
acme/ <- the project, named by project_key
shared -> ../.clones/notes-store/public/projects/acme
private -> ../.clones/agents-memory/private/projects/acme
.clones/
notes-store/ <- clone of public_repo
agents-memory/ <- clone of private_repo
shared and private are symlinks. floppy makes them on each machine, and no repository contains them. They are relative, so you can move agents_memory_dir as one directory.
<memory_dir> in your repository resolves to ~/agents_memory/acme/shared, and <memory_dir>/private to ~/agents_memory/acme/private. Resolves, not points: since 0.27.0 there is no file in the repository doing the pointing. See “The memory is not in your working copy” below. These two addresses stay the same if a repository URL changes.
A second project uses the same two repositories in the same way. It gets its own directory ~/agents_memory/<other key>/, and its own scopes public/projects/<other key> and private/projects/<other key> inside the same two clones. There is one clone for each repository by default, not one for each project.
When two projects should not share one working tree
Projects sharing private_repo share its clone — and therefore one git working tree. Their scopes never overlap, but the tree state is one: a live session of one project holds its half-written status file dirty there at the moment another project’s wrap syncs. Since 0.24.0 commit stashes over the foreign dirt and restores it, and check counts this project’s files apart from another project’s — the shared default works. Opt out of sharing when you want a wall instead of a stash: point workplace_memory_dir at a path of this project’s own.
workplace_memory_dir=~/agents_memory/.clones/agents-memory--acme
On a machine that is already wired, the view and the cross-project link still resolve into the shared clone, and the verbs refuse to repoint wiring that resolves somewhere real — remove those two links first, then rewire:
git -C ~/agents_memory/.clones/agents-memory push # flush this project's leftovers first
rm ~/agents_memory/acme/private
rm <memory_dir>/common/private
bash <plugin>/scripts/run workplace
The cost is one more clone on disk. The remote stays one repository, the scopes do not move, and the other projects and the other machine see nothing changed.
If public_repo and private_repo hold the same URL, there is one clone, and both scopes are in it, beside each other.
Where the scopes are
The scopes are two directories beside each other:
public/projects/<key> in public_repo
private/projects/<key> in private_repo
public/common in public_repo — about no single project
private/common in private_repo — about no single project
Below any of them, a note that is not true everywhere goes one level deeper: workplaces/<workplace_key>/ or machines/<machine_key>/. A note that is true everywhere sits directly in the scope, which is the common case.
The two common scopes are the sibling of projects/<key>, for facts that are about no single project — an outside tool that was evaluated, a shell trap, what one machine has installed. store and workplace wire them beside the project’s own, as <memory_dir>/common/shared and <memory_dir>/common/private; a machine that ran only one of the two verbs gets only that half. Nothing in the committed index may point into them, for the same reason nothing may point into the private scope: the link is per machine, so it is dead for anyone who has not wired it. start names the scope instead, and lint fails on such a link.
The private scope is private to the project, and every machine of the workplace reads it — see docs/memory-model.md. Facts about ONE machine go to machines/<name>/ of the workplace repository.
These names are the current set and not the first. What the renames before them cost is in the lessons.
If your repository still uses the old names, the verb stops and prints the git mv commands. It does not move the notes itself. Two reasons: these notes can be the only copies, and a move done on one machine while the other machine still writes the old path forks the memory with no message anywhere. Update every machine first, then move the scopes one time.
Two memory repositories on one machine
A project can use store and workplace together. store moves all of the memory into a different repository. workplace attaches a shared scope at <memory_dir>/private. These can be two different repositories.
Two changes keep the two repositories apart:
- The checkout directory comes from the URL. Thus two URLs cannot give one directory.
- If a checkout is already there, the verb compares its
originwith the configured URL. If the two are different, the verb stops, and shows both.
The second change also finds a different problem: an unrelated repository at that path. Before 0.4.2 neither check existed, and the two verbs shared one directory in silence — the lessons have the measurement.
You do not need to move anything. If a checkout is already at the parent directory, floppy continues to use it, and the verb tells you so.
Memory in a different repository
Some repositories cannot hold agent notes with the code. Examples are a customer checkout that you do not own, and a policy that keeps the two apart.
In that condition, the memory goes into a store repository. Your code repository keeps one file: .floppy/config. It is a short list of settings. A review of it takes one minute.
To set this up during init, use the flags:
--memory-repo git@example.com:workplace/agents-memory.git --memory-key acme
To set it up later, put public_repo and project_key in .floppy/config. Then run:
bash <plugin>/scripts/run store # clone or pull, lay out the cache, verify a write
store runs one time for each machine. Not for each working copy. It is idempotent. To see the result without a change, run store --check.
There is no second step. link was one until 0.27.0; the memory directory of the agent application is now created for you, for whichever directory you are working in, the first time any verb runs there. It is announced when it happens.
The memory is not in your working copy
From 0.27.0 the memory of a project in a store is addressed by repository, not by working copy. memory_dir stays the name you type — .agent-memory/… is what you write in a file list, and what every report calls it — but no file or symbolic link of that name is created in the code repository at all. The notes are in agents_memory_dir/<project_key>/shared, which is a view into the store.
The reason is git worktree. A symbolic link in the working copy is ignored by git, by design, so git never carries it into a worktree. A worktree of a correctly configured repository therefore started with no memory, and every verb in it reported that the project had none. The key that finds the memory is project_key from .floppy/config, which git does carry, so every worktree of a repository computes the same one.
A symbolic link from an earlier version still works. Everything follows it. It is reported and never removed: removing it is your decision.
A repository whose memory_dir was pointed at a store by hand, with no public_repo and no project_key, has nothing to compose a cache path from. Its worktrees fall back to asking the main working copy of the same clone, which is where that symbolic link was made. That fallback needs git 2.31 or newer, for rev-parse --path-format=absolute. On an older git it does not engage, and a worktree of such a repository behaves as it did before 0.27.0: it reports no memory. The repair is the same in both cases — set public_repo and project_key and run store once.
If a real directory of notes is in that position, store stops and moves nothing. Those notes can be the only copies. To move them into the store, run:
bash <plugin>/scripts/run store --migrate
It prints every file before it moves it. If a name exists on both sides it stops and moves nothing at all, because which copy you meant to keep is not a question a script can answer. It deletes nothing, including the directory it empties. Nothing else calls this flag.
The last step of store is the important one. It writes a file through the view, and confirms that the file is in the store. All other steps can look correct while a write goes to a location that nobody publishes.
The configuration contains no key for “external” or “internal”. The layout follows from public_repo and project_key: both present means the memory of this project is in a store, and the address is computed from them. The configuration decides, not the file system. Whatever stands at memory_dir in a working copy is not an address and does not become one.
That is deliberate, and it was measured. Resolution used to fall back to the working copy whenever nothing stood at the store’s address, which reads as caution and is not: the skills of this plugin tell an agent to write .agent-memory/<file>, so one note from an agent that followed them created a directory there, took the address back, and put the repository into the exact condition this version removes — lint refusing to run, check printing MEMORY LINT COULD NOT RUN, and the whole corpus in the store invisible while nothing anywhere was red.
A real directory in that position is now a fork of the corpus, not a memory. guard refuses while it stands and names store --migrate; see above.
With a store, the wrap procedure closes two repositories:
guardasks the store for its changes, and reports them with the paths that you use.checkshows the notes that go out. The diff of the code repository cannot show them.commitcommits and pushes both repositories from one file list. If the store refuses the push,commitfails. It does not report “session closed” above notes that are not published.- If
memory_diris outside git,statusreports this condition. The memory then works for reading and writing, but nothing publishes it.
Know one disadvantage before you select this layout. Nobody reviews the memory with the code. In the in-repository layout, that review is free.
Caution: the incomplete condition is comfortable, and thus dangerous. The configuration names a store, and the machine never cloned it. Every path resolves; lint and check are quiet, because there is nothing there to be loud about; and a session reads an empty memory and believes the project has none. guard fails on it and names the verb that repairs it, and status reports the store in a section of its own, and thus shows a machine that omitted the setup. store --check answers the same question in one line, and answers it about this machine, not about this working copy.
quota.lock
This file is in the memory directory. It contains four limits:
chars_max— the total number of characters.note_chars_max— the characters in one note.pointers_max— the pointers in one index.pointer_line_max— the characters in one pointer line. The default is 170.
A fifth is optional and written once per half: half_chars_max.<half> bounds one half of the tree on its own, and half_chars_max.root covers the notes that sit directly in the memory directory. A half with no key of its own is not bounded, so a corpus that sets none behaves exactly as it did before the keys existed.
All of them are facts about this corpus. Thus they stay with the memory, and not in .floppy/config. One size limit is a fact about the agent application instead: index_chars_max, in the table above.
Every one of these ceilings warns before it refuses. At 96% of a ceiling lint prints a ! line naming it — the run still passes — and the corpus ceiling brings the per-half breakdown with it, so the line says which half grew. The exception is pointer_line_max: it bounds one line, and a line at 165 of 170 characters is not approaching anything, it is a line that fits.
The band exists because of who a hard stop lands on. A ceiling that only refuses stops whoever crosses it, and on a memory written from several machines that is routinely not whoever filled it. The ratchet below says a number may be raised only in the same commit as the notes that needed the room — so a session that meets a bare refusal has to either raise a ceiling it did not fill, or prune a half it did not write, and pruning another session’s notes is the one thing the wrap rite forbids outright. The warning reaches the session that is doing the filling, while the work of trimming is still its own.
The 96% is derived from each ceiling, not configured. Two numbers that have to be kept in a fixed relation are two chances to set them wrong, and nobody has a reason to want the warning at some other fraction.
The plugin does not supply a quota.lock file, and the file is never copied from one project to a different project. Its numbers must come from a measurement of the corpus of this project. A limit from a different project describes that project, and controls nothing here.
init therefore creates it in exactly one case: the repository already had notes when floppy arrived. Then there is a corpus to measure, and the numbers are this project’s own — chars_max at the measured total plus a tenth, pointers_max at the longest index found, and any note already over note_chars_max listed in grandfathered rather than failing the first run. On an empty memory init creates nothing: there is nothing to measure, and a ceiling invented for an empty directory bounds nothing.
Seeding at adoption is what a ratchet is for. It does not say how big this memory should be — it says how big it was on the day floppy arrived, so that every increase afterwards is a deliberate act visible in a diff. A project arriving already over some imported default would go red on its first run, and a linter that is red on day one is a linter that gets switched off.
init also prints what lint makes of an inherited corpus, grouped by kind with a count in front of each: ninety-four identical lines are the raw material of a report, not a report. It rewrites no note. A clean verdict comes with the linter’s warnings printed under it, because “nothing is wrong” and “nothing to do” are different reports — and one warning is created by adoption itself: pointers_max is seeded at the longest index found, which leaves that index at 100% of its own ceiling from the first run.
While the file is absent, the lint command gives a warning. It does not fail.