Skip to content

Answer the Ubuntu install feedback: init, service install, show, --redact, subagent labels, 0.2.0 - #30

Open
doronp wants to merge 13 commits into
mainfrom
feat/install-feedback
Open

doronp wants to merge 13 commits into
mainfrom
feat/install-feedback

Conversation

@doronp

@doronp doronp commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Answers the feedback from a fresh install on an Ubuntu 24.04 VM: one commit per item, then a 0.2.0 bump and two review-fix commits. The commits can be split into separate PRs if you prefer.

The items

# Feedback Commit What changed
1 Python 3.12 blocked the documented pipx route 59865e1 docs The floor stays at 3.13 (DESIGN §1 locks it). Every install command now names --python 3.13, and pipx moves to a footnote. A docs test pins every documented install line to the floor in pyproject.toml.
2 Setup means writing TOML by hand beffcf2 feat gitmemory init {claude-code,pi} appends the standard [[watch]] table and never overwrites, because config.toml also holds push opt-ins. It validates through the watcher's own parser, writes atomically, and does nothing on a second run. Refusals exit 1.
3a Nothing keeps the watcher running b39449a feat gitmemory service install writes a systemd --user unit or a LaunchAgent, refuses to overwrite one, and prints the enable, check, log, linger and remove lines. It never runs systemctl or launchctl itself.
3b Nothing says the watcher isn't running 5c6a874 feat recall warns on stderr when hook records have waited more than an hour. Time since the last capture can't tell an idle machine from a dead watcher; a waiting record can, because every watcher pass drains the spool.
4 No way to open an address dba5db7 feat gitmemory show <agent>/<session>/gNN@<offset> [--context N] prints the turn from the store. The address is validated by the store's own identity rule and never becomes a path.
5 Subagent transcripts swamp the results 656136c fix A real bug: latest-user counted a subagent's dispatch prompt as the user's own words, so it could outrank the user's actual rule. That is fixed. Subagent hits are now labelled subagent:<role>/<kind>, and docs/watching.md gives a main-sessions-only pattern recipe with its costs. No new exclude key.
6 recall output skips the redaction gate b9a5f57 feat Opt-in recall --redact / show --redact, or GITMEMORY_REDACT=1, masks the push gate's credential shapes in what is printed. This is masking, not a gate, and the docs say what it does not catch. DESIGN.md:212 gets a dated amendment.
7a +391995999B 3bb0aff feat +392.0 MB (391995999 B). size= and the commit subject stay exact.
7b The store takes about 1.75× 0434189 docs New "How much disk it takes" section in docs/watching.md: why it's above 1×, a measured table, and the one real store at 1.75×.
7c Plugin and CLI versions skew 56327cf feat, 7a10f22 gitmemory --version. The skill names the release it was written for (a test pins it to pyproject) and reads invalid choice / unrecognized arguments as "the CLI is older". 7a10f22 bumps to 0.2.0 in lockstep, so installed plugins actually receive this round's skill.

Owner decisions: docs-only Python fix, a write-only service command, opt-in redaction, one PR. The rest are defaults; docs/reviews/install-feedback-round.md lists them.

Review

  • Per item: an independent Opus reviewer tried to break each commit before integration, and a fixer folded its verified findings in.
  • Kimi K3 cross-review, as a pair programmer, of each item commit. 53 findings: 22 fixed, 6 declined, 25 refuted. Each refutation came from a live probe or a code read. All five "high" findings were artefacts of reviewing a commit alone. Fixes are in b7e3521.
  • Final gate: an Opus review of the whole branch through five lenses (security, end to end, docs, CI from a fresh clone, simplicity), with two refute-by-default skeptics per finding, plus a final Kimi pass on the fix commit and on the composed SKILL.md. Fixes are in f0ebb41, including a real show --redact crash on a lone UTF-16 surrogate.
  • Full mutation pass over all 662 rows: 646 caught by their own test. The other 16 behave identically on main under the same setup, so none is from this branch.

Every review finding and its disposition is in docs/reviews/install-feedback-round.md.

Tests

  • 1,134 tests offline (from 1,037) and 662 mutation rows (from 606). ruff, the full suite, and the corpus-on suite are green locally on macOS.
  • The Linux service path is tested only by monkeypatching sys.platform. It has not run against a real systemd host.

Changes in visible behaviour

  • watch and capture print sizes in a new format. A script parsing +NB needs updating.
  • recall's role column reads subagent:user/text for subagent turns.
  • The default policy no longer ranks a subagent's prompt as the user's words.

Left open

  • After merge: tag v0.2.0. The CHANGELOG compare links and the pinning advice in hook/README.md refer to it.
  • Pre-existing on main, not fixed here: the push gate misses a \b-anchored key that comes right after a JSON \n or \t escape. Nothing is sent today. The unpushed egress branch's decoded-view scanning appears to target this.
  • Pre-existing on main: 16 mutation rows don't bite their intended test offline. Some need the corpora, the dashboard rows need serve, and the rest also fail on main.
  • SECURITY.md's supported-versions table still reads 0.1.x.
  • docs/assets/demo.gif still shows the old +NB format.
  • bench: full-history no-retrieval control arm on LongMemEval-S (closes #16) #17 is red on its README test count, and this PR moves that count again, so whichever lands second rebases. The local egress branch will conflict in __main__.py, redact.py and the README pins.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U

doronp and others added 13 commits October 18, 2026 13:44
The README's install line did not install on Ubuntu 24.04, whose own
Python is 3.12, and the same line with `--python 3.13` did. pyproject
requires >=3.13, but no install command said so. Left to choose, uv can
settle on the older interpreter and refuse (it does with Python downloads
off), and pipx always installs into the Python it runs under.

Every install command with a source argument now names the floor: the
README quickstart and [serve] example, the skill's setup step,
hook/README.md, the plugin description, and the derive extra's error
hint. pipx survives as a README footnote that names a 3.13 interpreter.
Where uv fetches Python only when asked, the README says to run
`uv python install 3.13` first; where it never does, to put a 3.13 on
PATH, because `uv python install` is refused there too. The floor itself
is unchanged; DESIGN.md section 1 stays as locked.

test_every_documented_install_names_the_python_floor reads
requires-python with tomllib and fails on any tracked install line with
a source that lacks the floor, spelled `--python X.Y`, `--python=X.Y` or
uv's `-p X.Y` (pipx: `pythonX.Y`). A command ends at its code span, a
`)` or `#`, the end of a line not continued by a backslash, or the next
command on the same line, and the test checks that scanner on those
cases before it scans. Prose with no source, such as hook/README.md's
"if you installed with `uv tool install`", is not a command and is
skipped. The README's test counts move by one, and docs/REPRODUCE.md's
expected counts, stale since 1,027, now read what a checkout collects:
1038 offline, 1363 with the corpora, 328 conformance cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
The plugin and the CLI are installed separately, and nothing makes them one
release: Claude Code keeps the plugin at the commit its marketplace was
cloned at until its version changes, while `uv tool install` takes whatever
commit it is given. A skew has shipped already, silently: v0.1.0's recall
prints no date column and ranks by relevance, the skill decides which of two
lines is current by date, and both say 0.1.0.

- `gitmemory --version` prints `gitmemory <__version__>` through argparse's
  version action (the constant had no reader). Every CLI before it exits 2
  with "the following arguments are required: cmd", measured under Python
  3.12, 3.13 and 3.14.
- The recall skill names the release it is written for (0.1.0, today's
  pyproject version), runs --version once per session and says when the CLI
  is older. Its first "When it fails" bullet reads exit 2 with `invalid
  choice`, or `unrecognized arguments` naming a flag, as an older CLI: say
  which subcommand or flag is missing and never retry without it, so a later
  safety flag cannot degrade into its absence. `unrecognized arguments`
  naming only plain words is a topic left unquoted, not an older CLI: quote
  it and search again. --version joins the read-only allow-list, and its
  call joins the echoes: every session that uses the skill now records one,
  and on a query about versions those records outrank the real hits at the
  default -k 10.
- The skill-commands test accepts `gitmemory --version` with no subcommand
  and looks for the flag in the top-level help. A new test pins the release
  the skill names to pyproject and __version__, the --version output, the
  argparse phrases the skill quotes, each got from the CLI, the unquoted-topic
  case, and the --version echo.
- hook/README.md says how to pin both to one tag, with the install line
  naming --python 3.13 like every other. CONTRIBUTING.md gets the bump rule,
  `uv lock` included: uv.lock records the project version, and
  `uv lock --check` fails after a pyproject-only bump. It also says to move
  CHANGELOG's [Unreleased] and to tag the bump `v<version>`, which the pin
  advice names.
- The new test moves the README's counts and docs/REPRODUCE.md's expected
  counts by one, to what a checkout now collects: 1039 offline, 1364 with
  the corpora, 328 conformance cases.

No version bump here; the release commit makes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
A subagent's first turn is user-role text that the parent agent wrote:
its dispatch prompt. The default policy counted every user/text block as
the user's own words, so a dispatch prompt that was newer than the
user's rule, and matched it, led recall ahead of that rule. That is the
forgetful-agent failure latest-user exists to prevent, arriving through
a subagent. The index already kept the per-turn isSidechain flag in
`turns`; neither Hit nor search() could see it.

- Hit.is_sidechain, read in search() by a primary-key lookup on turns.
  No schema bump and no reindex, and search() order is unchanged, so no
  benchmark number can move. Timed within noise on 120k turns.
- latest_user no longer admits a sidechain turn as the user's words.
  derive._prose still does; changing it would move the secondary-set
  numbers, and the docstring says so.
- recall prints subagent:<role>/<kind> for such a turn, in the same
  column, so split()[3] is still role/kind and main-session lines are
  unchanged. The mark is per turn, so old-format sidechain lines inside
  a main file are labelled too. USAGE.md gives a main_only policy that
  drops them at read time.
- docs/watching.md states the pattern's glob semantics and adds a
  main-sessions-only recipe, pattern = "*/*.jsonl", with its costs:
  those turns are never captured, are lost once Claude Code cleans up
  old transcripts, and drop out of dashboard spend. The brief survives
  in the parent, and so does the report unless a background agent's or
  workflow's session ended first; a workflow's journal is left out. The
  recipe must replace the default table, since a second table on the
  same root still finds every subagent. A doc test runs the documented
  block and pins both claims. No exclude key: the existing pattern key
  already says this.
- Three mutation rows, so 606 becomes 609 negative controls. On top of
  the install and --version commits, the suite is 1,043 tests offline
  (1,368 with the corpora).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
Setup meant writing TOML by hand: no command wrote config.toml, and with
none the watcher logs "watching nothing". `gitmemory init claude-code`
(or `pi`) now adds that agent's documented root to the config.

Watch roots still have no default. daemon.ROOTS is read only by init,
and only because the user named the agent; load_watches reads the file
and nothing else.

It appends and never changes a byte already there, because config.toml
also holds the [remote.*] push opt-ins and a rewrite would revoke one.
Same agent and root already watched: exit 0, nothing written. It
refuses with exit 1, writing nothing, when the file is a symlink, has
another hard link (the rename would leave the other name holding the
old bytes), or is not a regular file; is not valid TOML; has a `watch`
a [[watch]] table cannot join; already watches the root under another
agent (discover gives a shared root's files to the first watch); or
holds a table the watcher would skip. That last check runs the
watcher's own rules, split out of load_watches as watches_from, so the
refusal is the watcher's sentence and cannot drift from it. Usage
errors and a config it could not read stay exit 2: an unreadable file
is never taken for a missing one and replaced.

Since init now prints the watcher's sentence as its reason, the store
check gets one per direction: a root inside the store "is inside the
store" rather than "contains the store itself", which was wrong there.

The write goes through store._write_atomic, so a running watcher reads
the old file or the new one. A killed init can leave
config.toml.tmp.<pid>.<tid>, a full copy of the file, at the store
root, and the watcher's `git add --all` would commit it, so
/config.toml.tmp.* is gitignored.

Docs: README quickstart and command table, docs/watching.md,
docs/USAGE.md, hook/README.md, ARCHITECTURE.md. Skill setup step 2
tells the user to run init, after the older-CLI bullet and the
`--python 3.13` install line already there, and init joins the commands
the skill must not run. The plugin description adds the configure step
beside the pinned install command. The doc test reads the table under
watching.md's "## Configure what to watch", which ends before the
main-sessions-only recipe's own toml block. Eight mutation rows
(609 -> 617); suite 1,043 -> 1,063 offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
… the watcher

The watcher is the only thing that captures, and nothing kept it running:
the docs said "no installer" and left the unit to the user. A hand-written
unit falls into three traps, each reproduced while designing this. The
interpreter's realpath is outside the venv and cannot import gitmemory.
`python -m` from a unit's working directory ($HOME) runs a stray
~/gitmemory.py instead of the package. And a user service sees neither the
shell's GITMEMORY_HOME nor its PATH, so it watches another store or misses
git.

`gitmemory service install` (new module service.py) writes the facts only
the running process has into one file: sys.executable as given, with
`-P -m gitmemory`, an explicit `--home <store>`, and PATH from git's
directory (made absolute) plus /usr/bin:/bin. It writes a systemd --user
unit on Linux, under $XDG_CONFIG_HOME only when that is absolute and
otherwise ~/.config, and a LaunchAgent plist on macOS. It refuses (exit 1,
O_EXCL) to overwrite an existing file, in the one-line `refusing: ...`
shape `gitmemory init` uses, through the same helper. It exits 2 on
another platform,
with no git, when it cannot tell which Python is running, and for a store
path with a line break, which a unit cannot hold and a plist would turn
from \r into \n. It prints the start, check, log, upgrade and remove
commands and never runs systemctl, launchctl or loginctl, because enabling
it is the user's step, as installing the hook is. The removal is
`stop; rm`, not `stop && rm`: `launchctl bootout` of a job written and
never started exits 3, and the refusal's way out has to remove the file
anyway.

Tests run the file's own command line under the manager's conditions (cwd
$HOME holding a decoy gitmemory.py, env HOME plus the file's PATH) for both
backends, and run the printed removal against stand-in managers that fail.
The unit's quoting is read back strictly, as systemd would, so an
undoubled %h or $HOME or an unescaped backslash fails the test. Six
mutation rows (O_EXCL, -P, realpath, the % and $ doubling, the removal's
`;`) are caught by them. Docs: watching.md "Keep it running", USAGE
step 2, README table, ARCHITECTURE (whose CLI list now names init and
service), the skill's setup steps (install, `gitmemory init`, then
`gitmemory service install`) and its do-not-run list, a dated DESIGN.md
amendment naming the systemd user unit, CHANGELOG, and the pinned counts,
re-measured on top of install, version, subagents and init.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
recall checked nothing about capture, so a stopped watcher was invisible
from it: every search answered from the store as it was when the watcher
stopped, with an empty stderr. The time since the last capture cannot tell
that from an idle machine, because a running watcher on an idle machine
captures nothing either. The spool can: every watcher pass drains every
complete record, covered or not, so a record an hour old means no pass has
run on that store since it arrived.

daemon.STALE_RECORD and a read-only daemon.uncollected() count the spool's
complete records and the age of the oldest, by drain_spool's own rule (a
dot-name is a hook still writing). A directory is not counted either: no
pass can unlink one, so it would warn for ever with a watcher running.
recall prints one stderr line before the results when that age passes the
threshold, naming the store, so a hook and a watcher on different stores
show up too. Exit code unchanged, and the subagent label on result lines
is untouched. Skipped under --db, whose index names no store, and silent
on any store the hook does not ring: capture-only stores, tests and bench.

The skill quotes the line and tells the user to start `gitmemory watch`
and keep it running with `gitmemory service install`; it runs neither. It
does not rule out a reindex, which still finds what the watcher captured
before it stopped. docs/watching.md names the other causes: a service that
is installed but not started or failing, one whose --home is another store,
and a watcher that runs passes less often than hourly. Three tests, seven
negative controls (623 -> 630), and the README and REPRODUCE.md counts,
measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
recall stops each turn at 160 characters and hands back an address, but
nothing resolved one: the rest of the turn was segment arithmetic done by
hand under raw/, which the recall skill spelled out for the agent.

`gitmemory show <agent>/<session>/gNN@<offset> [-C N | --context N]` reads
the store, not the index, so it needs no index and a stale or tampered one
cannot choose what it prints. It parses the generation the way `index` does,
so a turn cut by a segment boundary comes back whole, and an offset inside a
turn resolves to that turn with its own start in the header. A subagent's
turn is headed subagent:<role>, because its user text is the parent agent's
brief, not the user's words. recall and show now share one _date() helper,
and recall's line keeps its subagent: label in the role column.

The address is input. Its names go through store.identity() before
anything is read and are only compared with what sessions() found, so it
never becomes a path; `[0-9]` keeps int() from reading Arabic-Indic digits.
Errors name the address as it was typed, masked, since a session id can be
a token: identity() lowercases, and a lowercased AWS key id is one no
detector matches. Exit 2 for a malformed address or not a store, 1 for an
address the store does not hold or two manifests claiming it. Only header
lines start in column 0: tool names are whitespace-collapsed and text is
split on "\n" only, which closes the two header forgeries the prototype
reproduced. -C rejects a negative count, which plain type=int turned into
empty output and exit 0.

Eight mutation rows cover the guards (638 rows). README, USAGE (with a full
example address), the recall skill (show first; for a CLI older than show,
tell the user as the older-CLI rule says and read raw/ by hand; show in the
read-only list and the --home sentence; and a captured show result listed as
an echo, since it holds the whole turn dated later than the turn), DESIGN
2.8, ARCHITECTURE and CHANGELOG are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
…al shapes

Through the plugin's skill, recall and show output goes to the model
provider, and both printed transcript text as written: an AKIA key, a
private key, a session id that is a token. The redaction gate stands at
push only, on purpose, because a store on the owner's own disk is not an
egress. So this is opt-in masking, not a gate: `--redact` on recall or
show, or GITMEMORY_REDACT set to anything but empty or 0, masks both
tiers' matches in the printed text and in the address. It refuses
nothing, and the default is unchanged.

redact.mask() is safe_path's span masking with one widening:
private_key_block matches only the BEGIN line, so masking the match printed
the base64 key under a masked header. Its span now runs to the END line, or
to the end of the text. Only a header no earlier span covers searches for
its END, and the END pattern's runs are bounded: a page of headers with no
END, or one END line that never closes, made the widening quadratic (a 2 MB
tool_result took recall --redact 41 s; it now takes 0.28 s). Spans are
joined once rather than with `bytes +=`. safe_path delegates to mask().

The index stores records.safe_text's spelling of a control or invisible
character (``, `​`, `\x7f`), and each spelling ends in a word
character, so a key right after one lost the `\b` its detector needs while
the push gate, reading raw bytes, still found it. mask() also scans a copy
with each spelling blanked to as many spaces and masks the union.

recall masks after the whitespace collapse, which can join a PEM header a
newline split, and before the 160-character cut, because a key straddling
the cut leaves a prefix no detector matches (graph._label measured both).
The masked address drops the marker's space, so the line keeps its columns,
and the subagent mark stays in the role's field.

show masks each block whole before it splits it into lines, because a
private key's body is the lines under its header, and masks the header's
address and a tool's name too. It keeps the line breaks recall flattens, so
it asks mask() for a third scan, `collapse=True`: the blanked copy with
every whitespace run one space, its matches mapped back to the text's own
offsets. recall does not ask for it, since its text is flattened already.

The flag lives in a _redact_flag helper that both commands take. conftest
clears GITMEMORY_REDACT, as the docs tell the owner to make it standing in
settings.json.

The skill passes --redact to recall and show whenever GITMEMORY_REDACT is
set and not 0, so an older CLI fails with an argparse error instead of
printing unmasked, and while masking it never reads a turn from raw/, and
says a masked address is not one show can look up. SECURITY.md and
USAGE.md list what it does not cover. DESIGN.md's "exactly two boundaries"
gets a dated amendment, and its stale detector count (nine and three) is
corrected to twelve and four.

Twelve tests and sixteen mutation rows (recall: cut, collapse, PEM
widening, address and its one column, env parsing, the spelled-control scan
and its union, the linear widening, the bounded END pattern; show: whole
blocks, address, its one field, tool name, the collapsed scan and its
offset mapping), each caught by its intended test. README: 1,119 tests,
654 negative controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
`watch` logged `captured 470 +43025169B commit=...` and `capture` printed
`<manifest>  +6202B  size=11550  g00`, so a person had to count digits to
know how much a pass took. Both now say the amount through
`daemon.size_text()`: `+392.0 MB (391995999 B)`, or `+N B` under 1000
bytes. The units are decimal, so the rounded figure and the exact count in
parentheses can be checked against each other by moving a point, and a
script doing byte arithmetic still has the count.

`size=` stays plain bytes: it is the end of `[0, size)`, the coordinate
`verify` and every `@offset` are stated in, not an amount. The git commit
subject keeps `+NB`, because it is the durable record and the history
already written says it that way; the watch-line test pins that too.

Three mutation rows, each caught by its named test (657 in all): the
watch line and the capture line reverted to `+NB`, and the 999.95 guard
loosened so 999950 B reads "1000.0 kB".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
A store came to 1.75x what it had captured (684 MB for 392 MB), and no
document said why. ARCHITECTURE.md said the store "holds each byte the
agent wrote exactly once", and DESIGN.md's first consequence of appending
was "Zero duplication", which are true of raw/ only: .git keeps a second,
zlib-compressed copy (gitrepo.init says so), and index/ holds the
searchable text a third time, uncompressed, beside its FTS5 postings.

docs/watching.md gets a "How much disk it takes" section: budget about
twice what you capture, what each directory costs, a gzip one-liner that
estimates .git for a store holding what is under the Claude Code root now
(and what that count leaves out), and the measurements behind the advice
(du -sk on synthetic 470-transcript corpora, 2.3x for prose and code, 3.4x
for base64 tool output, 1.2-1.5x for the part that cannot be rebuilt). It
says index/ is the part to delete for space, because gitmemory index
rebuilds it from the store, and that recall and dashboard refuse until it
is rebuilt. It also says a rebuild needs about twice index/'s usual space
while it runs: index.build writes the new database beside the old one and
renames it into place, measured at 1.92x of capture steady and 3.82x just
before the rename. So when space is tight, delete index/ first. The one
real store is a reported data point, not proof of why real transcripts
index smaller. The README points at the section, ARCHITECTURE.md scopes
"exactly once" to raw/, and DESIGN.md gets a dated bracketed amendment.

The ratios are not pinned in tests: they move with block size, git
version, gc timing and corpus. The new doc test pins the advice instead.
Deleting index/ is safe only while the store's .gitignore ignores index/
and tracks raw/ and sessions/. It also checks that the README's,
ARCHITECTURE.md's and DESIGN.md's anchors resolve to a watching.md
heading, reading headings with fenced blocks cut out so a `# comment` in
a recipe cannot stand in for one.

On this branch the section also points at the main-sessions-only pattern
as the way to keep subagent and workflow transcripts out of the store, with
its cost, and says that `show` reads the store and keeps working while
index/ is gone. The new doc test moves the README count to 1,129 and the
corpus arithmetic to 1454 and 1457; docs/REPRODUCE.md's collect lines are
re-measured to match. No mutation row is added, so 657 stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
Claude Code keeps an installed plugin's copy until the plugin's version
changes, so the recall skill, gitmemory show, --redact and the other
changes under skills/ and hooks/ since 0.1.0 reach nobody who installed
the plugin until the version moves. This bumps it in the five places
CONTRIBUTING names, in one commit: pyproject.toml, uv.lock,
__version__, .claude-plugin/plugin.json and the release the recall
skill says it is written for. The tests pin the first four equal.

CHANGELOG's [Unreleased] becomes [0.2.0] - 2026-10-18 with a compare
link from v0.1.0, and [Unreleased] now starts at v0.2.0. The CHANGELOG
and hook/README.md text that said the skill would arrive with the next
release now says, in the past tense, why 0.1.0 installs lacked it, and
that 0.2.0's bump brings it on update.

DESIGN.md 2.8's "As built" said "the CLI only"; it now names the
plugin's recall skill too, as a search the agent runs rather than an
injection, so the no-injection statement stays true. The plugin.json
description is cut from 620 to 449 characters, keeping the install
command at --python 3.13, which the install-line test requires.

Not tagged and not pushed: tag v0.2.0 once this is on main, since
hook/README.md tells users to pin both installs to the tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
Kimi K3 reviewed each item commit of the round on its own, against main.
Every finding was then re-checked against the integrated branch, with a
live probe where one could run. Of 53, 21 led to a change, 6 are true
and declined, and 26 do not hold; several of those came from seeing one
commit without the rest. docs/reviews/install-feedback-round.md records
each one with its evidence.

The one behavioural fix: two `gitmemory init`s at once read the same
config.toml and the later rename dropped the other's table, while both
said "added" (46 of 60 concurrent pairs, measured). init now re-reads the
file under a lock in .locks/ just before the rename and refuses, exit 1,
when it moved; 0 losses in 60 since. Both reads go through one helper,
because the re-read alone also refused an unreadable file, and the
existing control for that guard had stopped biting. One new negative
control (658), and watching.md and the CHANGELOG say what is and is not
covered.

User-visible wording: the stale-spool line now says "N hook record(s)
uncollected, the oldest for <age>", since the age was only the oldest
record's (code, skill, USAGE, watching.md and tests together), and the
--redact help says empty is off as well as 0.

Tests that now hold what they claimed: the install-floor scan reads a
command split across code spans; REPRODUCE.md's counts are held to the
README's (a new test; with init's, the suite is 1,131); the disk section's delete
advice is checked against the store's .gitignore, and CHANGELOG's anchors
are checked too; the init temp test asks git about the writer's real temp;
the PEM-header linearity test counts searches instead of timing them; a
key inside a PEM body is pinned; the _ago 2-hour and 2-day edges are
pinned; a daemon test no longer depends on /tmp/x being absent.

Docs and skill: show's echo rule matches the header's shape; the skill
says to search again with -k 50 when subagent lines crowd the results,
and not to list sessions/ for a masked address; USAGE says the service
starts the watcher at every login and names linger; "about twice" the
capture is bounded for transcripts that compress poorly; the stale suite
sizes in mutate_index's docstrings are dated rather than re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
Kimi K3's final pass, over the fix commit and the skill as it now reads
whole, and an Opus gate of five lenses over the whole branch, with two
refute-by-default skeptics per finding, raised 33 findings between them.
This answers them. The record is the "Final gate" section of
docs/reviews/install-feedback-round.md, which also corrects show-7 and
moves service-3 to fixed.

Four changes of behaviour, each with a test that failed first and a new
mutation row:

- show --redact no longer stops with exit 2 partway through a turn that
  holds a lone surrogate. mask() encodes with surrogatepass, as records
  hashes; surrogateescape round-trips only U+DC80-U+DCFF.
- recall --redact masks as show does, with the mask's own collapse, and
  flattens and cuts after it. str.split() broke a value at a no-break
  space before the mask saw it, so recall printed what show and the gate
  flag. A header a line break split is still joined.
- show refuses a masked address with a line of its own, still exit 2, not
  with "as recall prints it" about what recall printed.
- The printed systemd removal ends with daemon-reload.

Two simplifications: init refuses in the except of its re-parse and drops
`have`, and uncollected() loses a clock nothing passed.

Docs: the --redact caveat list and the settings.json snippet are kept in
USAGE.md only, and SECURITY.md and hook/README.md link them. The list now
says the store lowercases a session id, so a credential-shaped one prints
unmasked, and that a key split across two blocks is masked to the end of
the first. The --redact sample comes from its own transcript. While
masking, the skill reads nothing under the store by hand; it puts -- before
a topic that starts with -, and states each rule once, with one retry,
-k 50. REPRODUCE 5b sets HOME as the suite does, so a generic account
collects what the README says. The CHANGELOG, DESIGN.md, watching.md and
the docs index follow. A2, the push gate missing a key right after a JSON
escape, is on main and left for its own issue.

Pins move to 1,134 tests and 662 negative controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPBLmJD2q1PVxdkWx8C16U
Signed-off-by: doronp <3586743+doronp@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant