Five of six findings were real. Verified each against the code rather than
taking them at face value; one was wrong and is noted below.
1. validate_examples.py carried a stale header from the spec-only phase --
"PARKED AS AN ASSET (deliberately not .py yet)... ON IMPLEMENTATION: rename
to skills/agent-memory/scripts/validate_examples.py". The file is already at
exactly that path. Rewritten to describe what it now is, keeping the
substantive part: why it exists, the seven check families, why it compares
the doc's algorithm by source text rather than exec()-ing a code fence, and
that nothing runs it automatically.
2. Missing shebang -- added. The same finding also claimed mode 100644; that
half is wrong, `git ls-files -s` shows 100755 for all five scripts.
3. hooks.json's description still opened "CONTRACT ONLY -- the referenced
scripts are not yet implemented", true of none of them now. Trimmed to keep
only the UserPromptSubmit provisionality (9.5 is genuinely still open) and
to record the measured latency alongside it.
4. The session-id fallback was the finding worth the most. `session_id` is the
right key -- engineering/security-guidance's shipped hook reads the same one
-- but the fallback was the CONSTANT "unknown-session", and sessions dedupe
by value. Had the key ever been absent, every session would collapse onto
one id, len(set(sessions)) would plateau at 1, and every claim would cap at
L1 forever with no error anywhere. Now falls back to the transcript's own
basename, which IS the session id. Verified end-to-end with session_id
omitted from the payload: the atom records the real session UUID.
5. Dead `now` parameter on _eligible_l1 -- removed.
6. marketplace.json metadata still said 104 agents / 120 slash commands,
pre-existing drift on a line this branch already edits. Trued up to 110/130.
Re-verified after: 69 checks 0 failures, SKILL.md 6/6 PASS, both blocking gates
still fire by name, check_paths 620 files clean, counters and plugin-json pass,
all 5 scripts --help, all 3 hooks parse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Turns DESIGN.md from a spec into a working plugin. Five stdlib scripts, three
hooks, agent, command, three references, plugin manifests.
The gates are the design:
L1 -> L2 >= 3 distinct sessions spanning >= 2 distinct calendar days
(`stated` = 2 sessions, day rule still applies; `verified` = 1
observation and is the only day-exempt path)
L2 -> L3 >= 2 distinct projects, >= 30 days, uncontested
Two gates refuse rather than guess. `redacted: true` blocks promotion on any
volume of evidence -- a durability-independent barrier, since a secret restated
across five sessions passes every recurrence gate; the flag firing means the
text was altered, a lexical filter finding one secret is not proof it found all
of them, and L2/L3 are committed to git. An open contradiction freezes both
claims, found by reverse join because the newer atom carries no flag.
All three hooks fail open: a broken memory system costs memory, never a session.
SessionEnd stages promotions to .memory/staged/ and never touches a CLAUDE.md;
only an explicit human adopt does, after backing both files up.
Verified, not asserted:
- all three pinned atom ids from DESIGN.md reproduce exactly
- both blocking gates demonstrated on sample input, named in the output
- end-to-end: two transcripts across two calendar days -> merged L1 atom ->
staged L2 promotion with the path prefix stripped
- reverse join blocks the unflagged newer atom
- cross-tier L2/L3 collision marked at injection time
- recall p50 29ms / p95 31ms / max 35ms spawn-to-exit, scoring itself 2-3ms
over 500 atoms -- interpreter cold start is the entire cost
- validate_examples.py 69 checks 0 failures; SKILL.md 6/6 PASS
- derive_counters --check, check_plugin_json --all, check_paths all clean
DESIGN.md 10.1's "+6" tool estimate corrected to +8 -- the delivered surface is
5 scripts + 3 hooks. README.md's deviations list is authoritative for that and
five other divergences from the pre-implementation spec.
Concept from TencentCloud/TencentDB-Agent-Memory (MIT). No upstream code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-44 review, three findings, all verified before fixing.
1. The schema's `redacted` description has always asserted "never
promoted to a committed tier without human review" -- a real gate that
DESIGN.md stated nowhere. Confirmed by grep. Added to 4.1 with the
reason the flag carries: redacted:true means the pass ALTERED the
claim, which is positive evidence the source was sensitive, and
redaction is lexical so finding one thing is not proof of finding
everything. Recurrence cannot substitute -- three sightings of a
scrubbed claim are three sightings of the same unresolved risk.
2. hooks.json did not quote ${CLAUDE_PLUGIN_ROOT} while both precedents
this PR cites do (handoff: python3 "${...}/hooks/session_start.py";
skillopt-sleep: "${...}/hooks/on-session-end.sh"). A path with a space
would break the command. Quoted all three.
3. 4.2.1 says "both sit at L1" but the schema marks only the older atom
(contested, contested_by), so the promotion gate cannot be a field
read on both sides. Specified the reverse join: blocked if own
`contested` is set OR own id appears in another atom's contested_by.
Deliberately not a mirrored `contests` field -- same fact in two
places, needing sync, with nothing able to say which copy is right.
Cheap by construction: 5.2 caps the store at 500 atoms and measured a
full pass at 2-3ms.
Did NOT add a schema->doc dangling-section-ref check. Wrote one, it
reported 3.1.1 and 4.1.2 as dangling, and both exist -- my heading regex
required a trailing period that sub-sections do not carry. Re-ran
correctly: zero dangling refs. A brittle checker for an empty class,
which I got wrong twice inside two minutes, is worse than no checker.
Verified: hooks.json parses; 69 checks, 0 failures; derive_counters.py
--check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-43 review found the Windows gap that 42 rounds missed. Verified it
is a hard break, not cosmetic: the schema's L1 pattern is
^~/\.claude/projects/[^/]+/[A-Za-z0-9._-]+\.jsonl#L[0-9]+$ -- literal ~/
and forward slashes -- so a path built from %USERPROFILE%\.claude\...
matches nothing and an extractor recording the OS path verbatim emits an
atom the schema rejects outright.
Resolved rather than added to 9, because it is mechanical: `source` and
`first_source` store a canonical form, not an observed one. The extractor
derives the ~/-relative forward-slash form from whatever the platform
handed it, the same way 3.1.1's promotion derives the stripped form
rather than storing what it saw -- recording is already a transform here.
Worth noting the blast radius: L2 and L3 are already platform-neutral,
since the stripped form is a bare <session>.jsonl#L<n> with no path.
Nothing committed is affected, only the gitignored L1. 3.1.1's
de-identification stripping bought portability for free.
Also stated first-run behaviour, which 5.4 skipped while specifying
concurrent writers in detail: a missing .memory/atoms.jsonl is the normal
initial state, read as an empty store and created on first write, with
recall returning nothing rather than failing.
Verified: 69 checks, 0 failures; derive_counters.py --check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-42 review: the "run the validator before editing" requirement lived
in 10.1 and the PR body. Someone opening DESIGN.md six months from now
reads neither -- and "a future editor who does not know the file exists"
is the exact failure the doc names. Now a callout at the top with the
command in it. Fourth time this review series has turned up a rule
stated somewhere other than where the person who needs it is looking.
Also sharpened what round 41 recorded about 9. The reviewer read "~700
lines of settled contract downstream of an unmeasured question" as a
reason to trim 4-5 until the extraction spike reports. Worth being
precise instead: a "no" on (2) does not make the promotion machinery
wrong -- recurrence counting, the tier caps and the contradiction
detector operate on atoms however they were produced, and none reference
the extraction method. It makes them unused. Content that would need
rework is worth deferring; content that would go unread is not, since
deleting reviewed text to re-derive it later costs more than leaving it.
Recorded as a sequencing lesson for the next spec this size rather than
a call to cut.
Verified: 69 checks, 0 failures; derive_counters.py --check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-41 review, two findings.
The 1 size claim went stale in a way worth being precise about. 85,875
bytes was correct at 67a1228, but merging dev moved this branch's base
and the file is now 90,312 -- and the parenthetical says "measured at
this branch's base", so it was false as written. Updated to 88 KB /
90,312, and noted that it grew 4,437 bytes while this PR was open, which
is 1's own thesis demonstrating itself on the file it is about. Round
29's principle still holds: the checker verifies the sentence's internal
consistency (method named, units agree), never a live comparison -- both
checks pass on the new figures.
Second, and the better finding: 9 listed six decisions as a flat set,
which understated that two of them decide whether there is a system at
all. (2) extraction-without-an-LLM gates everything downstream -- the
session gate, the tier caps, the contradiction detector are only as good
as what the extractor produces -- and (3) is answered by (2), since
9.3's 2-week trial IS the test of it. The other four are local: each
changes one mechanism and leaves the rest standing, including (5), whose
worst case deletes one hook while the tiering survives on SessionStart.
Now a table, with the cost stated plainly: ~700 lines of settled contract
sit downstream of a question nobody has measured.
Verified: 69 checks, 0 failures; derive_counters.py --check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-40 review made a point the spec had left to inference: the
recurrence gates are a durability filter, not a secrets filter. A secret
seen in three sessions across three days is exactly as much a secret as
one seen once, so the gates were never protecting CLAUDE.md from a leak
-- they only made one slower. The `verified` fast path removes even that,
taking a claim from a single sighting to a committed marker block with
6 rule 1's redaction pass as the sole barrier.
Stated in the fast-path block itself rather than left to a reader
combining 4.1 with 6 -- the third time this review has turned up a rule
living in one section while the surface an implementer builds from lives
in another. Two consequences named: redaction must not be shortcut on the
`verified` path ("a script confirmed it" says nothing about whether the
text holds a credential -- "the staging key sk-... works" is a plausible
verified claim), and a verified 1-observation atom carrying a secret is
the first behavioural test memory_promote.py should have, since it is the
shortest path in the system from raw transcript to committed file.
Verified: 69 checks, 0 failures; derive_counters.py --check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
The decision-driving §2 predated engineering/memory-engineering landing on
dev (#947) and never mentioned it. Adds the missing subsection: layer
distinction (it audits memory systems; this would be one), the namespace
fence both SKILL.mds must carry, a commitment that §5.1's eviction and
contradiction rules stay expressible in forgetting_policy_linter.py's F1/F4
form, and memory_cost_profiler.py as the §7 budget framework. Conclusion
renumbered 2.5 -> 2.6. Counters untouched by design (spec-only folder).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Bzm6Pafyxja6g4jUDPcei
Round-38 review noticed that the committed contract files already encode
an answer to some of 9's open decisions -- hooks.json wires
UserPromptSubmit even though 9.5's option (c) is to delete that hook.
The doc knew: 9.5 already says "if (c) wins, hooks.json must shrink too."
But that only helps a reader holding both files. Read on its own a
hooks.json says "these three hooks exist", which is precisely the wrong
impression, and a contract file is exactly the artifact someone reads on
its own before implementing. Same lesson as round 36's contested-tag gap,
one level up: a cross-reference is not a contract, and that applies
between files as much as between sections.
hooks.json's own description now marks the entry PROVISIONAL, names the
open decision, gives the reason (the budget is dominated by interpreter
cold-start, not by the script's work), and says plainly that listing the
hook is a contract for the shape it would take IF it survives -- not
evidence the decision was made. SessionStart and SessionEnd are marked
not provisional so the warning stays scoped.
Verified: hooks.json parses; 69 checks, 0 failures; derive_counters.py
--check unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-37 review. The substantive finding: 9.5's options (a) fits /
(b) raise the budget / (c) drop the hook all treat spawn cost as a
constant to tolerate -- while last round's measurement showed spawn cost
is the ONLY cost that matters (scan 2-3ms, interpreter ~20ms). None of
the three attacks the measured bottleneck. That follows directly from a
number this doc already carried, and I should have drawn it rather than
waiting for a reviewer to.
Added (d): a warm resident process holding the store in memory, with the
hook reduced to a socket round-trip. Named as a real option because it is
the only one that removes the dominant term, explicitly NOT as the
recommendation -- its costs land on this design's own properties. A
"never blocks, exit 0" hook gains a liveness dependency and must keep the
cold path anyway, so complexity is added to rather than swapped for what
(a)-(c) need; lifecycle and stale-socket handling are a second heuristic
alongside 5.4's; a resident process holding memory contents in RAM is a
different security surface; and stdlib-scripts-that-exit is this repo's
shape. Sequenced behind the busy-machine measurement: if 100ms holds
under load, a daemon buys latency nobody needed.
Also trimmed the three passages the review named as archive-not-spec.
One of them ("the sentence went stale twice (53 -> 57 -> 67)") was itself
stale at 69, which is the argument for cutting it rather than updating it.
Verified: 69 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-36 review, three real findings.
1. Session-id uniqueness was the one load-bearing claim in 3.1.1 asserted
rather than evidenced, while everything around it cites line numbers or
arithmetic. Measured it: transcript filenames on a live install are
RFC-4122 UUIDs (version 5, RFC-4122 variant), shown with the commands
that produced them. One install is enough to establish shape, not to
promise the scheme is stable -- so the more useful half of the fix is
the fallback the reviewer noted was missing. The glob now has defined
behaviour for all three outcomes, and the >= 2 row is the one that
matters: without it a naive implementation takes the first match and
attributes a claim to the wrong session, which is a *wrong* citation
rather than a missing one, and 6 rule 6 cares about that distinction.
2. 4.2 states the contested-rendering rule tier-agnostically, but 5.2 is
the contract user_prompt_submit.py actually gets built from and never
mentioned it. An implementer working strictly from 5.2 ships a recall
path that surfaces a contested claim as plain fact. Stated in both
places now -- cross-references are not a contract.
3. L1 had a stored cap (500 atoms); L2/L3 had only injection budgets, so
a marker block could grow without bound while every session saw a
silently truncated view -- the 1 failure this design exists to prevent,
one layer down. L2 caps at 60 atoms with overflow demoted to L1
(recoverable, re-promotable); L3 caps at 30 and refuses further
promotions instead of deleting, since "never auto-demoted" means the
cap blocks inflow rather than choosing what to lose.
Verified: 69 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-35 review called the 100ms recall budget the highest-risk
unverified assumption, and estimated interpreter cold start at 20-40ms.
9.5 has prescribed the measurement since it was written -- "time a no-op
python3 -c pass plus a 500-atom scoring pass at p50/p95" -- so I ran it
rather than citing an estimate.
n=40, Linux container, otherwise idle:
python3 -c pass p50 12.4 p95 30.8 max 36.0
spawn + 500 atoms + score + top-5 p50 23.2 p95 30.1 max 50.6
...of which in-script work p50 2.1 p95 3.0
This reframes the risk rather than settling it. The scoring pass is
~2-3ms, so the 500-atom cap is not the binding constraint and never was
-- cold start is essentially the whole cost, which makes the budget a
process-spawn question rather than an algorithmic one. 100ms holds with
~3x headroom here; the 50.6ms max shows a real tail that would widen
under load. Recorded as a floor, not the answer: 9.5 asks for a busy
machine and this was an idle container.
Also cut two asides the review named as review-thread artifact rather
than spec: the 3.1 paragraph on why a rationale cannot be a $comment
inside a fixture (5 lines -> 3, keeping the reason), and the validator's
8-line comment on not whitelisting $comment (-> 4, keeping the warning a
future editor needs).
Verified: 69 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-34 review, two actionable items.
5.1 had no internal-budget discussion while 5.2 argues at length that its
1s hook timeout is a backstop rather than the target -- an asymmetry a
reader can only resolve by guessing. Stated why it is deliberate: this
hook runs once per session rather than once per prompt, so a slow run
costs one startup instead of compounding; and its work is bounded by the
2KB/4KB byte caps rather than by a scan that grows with history, where
recall scores up to 500 atoms. Also noted that 9.5's cold-start finding
lands here too if it comes back bad.
Addressed the .py.txt parking as a *precedent* rather than defending this
instance. What makes it legitimate is not intent, which is unfalsifiable,
but that the file is not a tool: no plugin ships it, no skill owns it,
nothing invokes it, and counting it would make python_tools less accurate
rather than more. The abuse it could be mistaken for -- a real tool
renamed to keep a headline number down -- is separable by one question a
reviewer can ask: is anything supposed to run this? Pointed at 11's
audit/ option as the way to avoid the pattern entirely.
Not changed: hooks.json naming three scripts that do not exist. Checked
whether the hypothetical lint exists -- nothing in scripts/ or
.github/workflows/ references hooks.json at all -- so the file's own
"CONTRACT ONLY" description is the whole mitigation needed today.
Verified: 69 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-33 review. 4.1's L1->L2 gate has two clauses -- >= 3 sessions AND
>= 2 distinct calendar days (UTC) -- and family 4 only checked the count.
The days clause is the half that stops one long working day from minting
an L2 claim, which is the exact case 4.1 calls out for the `stated` fast
path, and it had neither a check nor a fixture exercising it.
Added it for L2 atoms whose confidence is not `verified` (4.1 names
`verified` as the only exempt path). Worth stating why the test is
sound rather than approximate: first_seen and last_seen bound every
observation, so date(first) != date(last) is equivalent to ">= 2 distinct
days", not a proxy for it. Same date means every observation fell inside
it; different dates means at least two were touched.
Verified by shrinking the L2 fixture's window to one day:
FAILED: atm_961f033d spans >= 2 distinct calendar days
FAILED: atm_961f033d/L2 promoted_at falls inside its own observation window
The second is family 7 catching a side effect of the same injection,
which is the intended overlap.
Also fixed the placement section's framing. It claimed "neither option is
endorsed here" and then introduced its bullets with "both cut against
staying here" -- a lean, contradicting the sentence above it, and the
third reviewer in a row to read it as one. Now two symmetric cost lists
with nothing weighed.
Verified: 68 -> 69 checks, 0 failures; derive_counters.py --check
unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-32 review. 4.1.1 step 3 enumerated nine of the schema's thirteen
required fields, so a memory_promote.py written literally against it
would emit a schema-invalid atom. The reviewer also caught that 4.1.3's
confidence-merge rule is scoped to "(5.3)" -- the same-tier SessionEnd
merge -- so it does not cover this cross-project one by implication.
Added a table for the four: `claim` from the earliest contributor (the
group normalizes identically; earliest is for determinism, since
normalization is lossy on case and punctuation), `confidence` = max()
across all contributors (4.1.3's principle, restated rather than
assumed), `redacted` = true if any is (over-claiming costs nothing,
under-claiming loses the signal), and `kind` must agree or the group is
ineligible -- `kind` is not in the hash key, so two projects classifying
the same sentence differently means the "same claim" premise is what is
shaky. Refusing keeps this section's one-directional property: L3
under-fires, it never mis-fires.
This step has now shipped three omissions (`source`,
`promoted_from_projects`, and these four), each caught by a human
reading the list against the schema. Added a check for the class: every
schema-required field must appear in 4.1.1. Re-injecting the omission
fails on ['kind', 'redacted'].
Two notes on that check, both found by verifying it rather than trusting
it. It first reported first_seen/last_seen/first_source as missing --
false, because norm_prose strips `_` as an italic marker, so the needles
had to be normalized the same way. And it is a substring test over the
section, so `confidence` and `claim` satisfy it via incidental mentions
elsewhere; that limit is now stated in the code rather than implied.
Verified: 67 -> 68 checks, 0 failures; derive_counters.py --check
unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-31 review spotted "other-project" sitting inert in the validator's
allow-list. It is inert for a reason worth fixing rather than tidying:
the check only scanned the path-embedded `-home-user-<slug>` form, so
`project` and `promoted_from_projects` -- where names appear bare -- were
never checked at all. That is the whole coverage gap, not a dead entry.
The consequence was the exact leak 6.5 exists to prevent. The rule came
from a fixture that named a private repo; a fixture naming one in
`project` rather than in a path would have passed clean. Verified by
injecting one:
FAILED: no unknown project names in fixtures, in paths or in fields
['acme-internal-private']
Now gathers names from both sources before comparing. Check count
unchanged at 67.
Verified: 67 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-30 review: ~70 lines of meta-discussion about where this file
should live stood between a reader and the tiering design. Moved to a
new 11 at the end, leaving a three-line pointer at the top. Old 11
(Attribution) renumbered to 12; the heading-order linter confirms the
sequence is still consistent.
The content is kept rather than dropped -- it is the record of an open
maintainer decision that three reviewers have now asked about -- but it
is not part of the design and should not be read first.
Verified: 67 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-29 review. Family 6 compared 1's cited byte count against the
CURRENT size of the repo-root CLAUDE.md -- a file this repo appends a
release note to on nearly every point release. The next unrelated PR
touching it would have turned this suite red for a reason with nothing
to do with this folder. 10.1's own thesis is that a checker must own its
ground truth; this one borrowed a moving one.
Snapshotting the byte count as a constant in the checker was the
reviewer's other suggestion and is not better: it puts the same number in
a second place that can drift from the first, with nothing able to say
which is wrong.
Replaced with two properties of the sentence itself, both of which the
doc controls: the claim must name `wc -c` and carry a real byte figure,
and its two units must agree (84 KB == 85,875 bytes). That still catches
the defect this check was written for -- 1 once read "~40 KB",
eyeballed, off by more than 2x -- verified by re-injecting it:
A (eyeballed, no byte figure): FAILED names `wc -c` and cites a byte figure
B (42 KB vs 85,875 bytes): FAILED KB and byte figures agree
Check count is unchanged at 67 (two removed, two added), so the
self-referential count assertion still holds. Also fixed a docstring in
_find() that still described the repo-root lookup this removes.
Verified: 67 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-28 review. Three changes, two held.
5.1 now carries the consequence 9.6 leaves live: L2 and L3 are injected
together and 4.2.1's detector cannot reach L3, so nothing upstream
guarantees they agree. Stated as a constraint on the hook -- never emit
two colliding claims as plain assertions -- rather than a mechanism, so
it holds under all three of 9.6's candidates. The reviewer is right that
retrofitting conflict-marking after session_start.py ships costs more
than honouring it in the first version; that does not require settling
9.6 itself, only refusing to ship the unmarked case.
AGENT_MEMORY_RECALL -> AGENT_MEMORY_USERPROMPTSUBMIT, in DESIGN.md and
hooks.json. The old justification (shorter; matches section 3's
vocabulary) traded away a property worth more: with all three vars
mirroring their hook name, a user who knows Claude Code's hook names can
derive all three without reading this doc. Three vars under two
conventions also invites the typo report the reviewer predicts.
Added the exact CI workflow snippet to 10.1. Still not wiring it into
ci-quality-gate.yml -- that runs on every PR in the repo, for a folder
9.3 permits deleting after a two-week trial -- but saying yes now costs
one paste rather than a design conversation.
Held: the placement decision, and the suggestion to split mechanical
rationale into references/. The second turns out to be blocked by the
first, which was worth measuring: a references/*.md under
engineering/agent-memory/ moves the references counter 746 -> 747, while
the same file under audit/ is pruned and free. The status header now
tables that alongside the parking hack and the double-relocation cost --
three open items resolving from one decision, which is the argument for
answering it before the implementation PR.
Verified: 67 checks, 0 failures; hooks.json parses; derive_counters.py
--check unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-27 review: DESIGN.md had accumulated its own review history inline
("round 3's fixture fix", "round 7 shipped two ids", "an earlier draft
violated..."), which reads as a PR changelog in a document an
implementer reads once. Fair, and I had been adding to it every round --
the previous commit put "until round 26" into 10.1.
Rewrote every such site to state the rule and the failure it prevents,
without the narration of who found it when. The rationale is what stops
a rule being re-broken; the round number is not. Two examples:
- 3.1's lifecycle callout now names the drift a "one lifecycle" claim
invites (two atoms that each validate alone and contradict each other
side by side) rather than reporting that a draft once did it.
- 10.1's property 1 now reads as a prohibition -- "it must not get this
property by exec-ing the fence" -- which is what a future editor needs,
instead of a note about when the exec was removed.
Same pass over the validator's comments, keeping the warnings (a future
editor of that file does need to know why $comment is not whitelisted)
and dropping the round counters.
Also stopped the status header reading as a lean toward audit/. Two
consecutive reviewers took "a better fit than this note originally
claimed" as an endorsement; it was meant as a correction to the note.
Now states plainly that neither option is endorsed and the decision is
the maintainer's, with the costs of each listed below it.
No rules, thresholds or contracts changed. Verified: 67 checks, 0
failures; derive_counters.py --check unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-26 review asked for a trust-boundary comment on published_normalize()'s
exec of a fenced block extracted from DESIGN.md. Removed the exec instead,
because the same review's other finding turns the comment into an
insufficient answer.
Findings 1 and 2 interact. Wiring the validator into CI -- correctly
observed to be possible today, since a temp .py copy is never seen by
derive_counters.py -- would have escalated the exec from "a maintainer
runs this on a branch they trust" to "any PR author executes arbitrary
code," because ci-quality-gate.yml triggers on pull_request and that
includes forks. The exec's safety depended on a fact outside the file,
and the obvious next improvement silently falsified it. A comment
documenting the trust boundary would have been read, agreed with, and
then invalidated by the very next commit.
The property the exec bought -- doc and fixtures cannot silently diverge
-- is preserved by comparing source text: the checker holds its own
normalize() and refuses to run if the doc's fenced block differs.
Verified by editing the doc's algorithm without updating the checker:
FATAL, both bodies printed. Fails closed on a cosmetic reformat too,
which is the safe direction.
Also corrected two claims this round proved wrong: 10.1 implied the .txt
parking blocks CI gating (it does not -- temp-copy works today, and the
placement decision does not gate it either), and the status header
inherited that error from round 25.
Verified: 67 checks, 0 failures; no exec() remains in the folder;
derive_counters.py --check unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-25 review, both findings verified against the tree.
Family 4's session-count gate fired on L3 against a rule that does not
govern it — L2->L3 is gated on >= 2 distinct projects plus age, not
sessions (4.1.1). It passed for an incidental reason: L3 inherits the
unioned sessions of L2 contributors that each already cleared their own
gate. Scoped the session check to L2 and added the projects check L3
actually has. Both verified to fail on injected defects.
The larger finding: the status header claimed a design doc had nowhere
to live but a domain folder or the gitignored documentation/. That was a
false binary. Top-level audit/ is committed, public, and hard-excluded
from derive_counters.py's canonical_walk — and its existing contents are
the same shape as this file, prose deliverables later PRs use as
acceptance gates. Recorded it with both consequences: the .py.txt
parking hack would be unnecessary there (verified — a .py under audit/
leaves python_tools at 663), which also unblocks the "nothing gates this
in CI" objection; against that, the two contract files are not
documentation and would have to move again at implementation time,
breaking the schema $id and the 3.1 link a second time.
Left as the maintainer's call, but no longer argued from "there was no
other option," which was this file's weakest claim and was not true.
Verified: 67 checks, 0 failures; derive_counters.py --check unchanged
(363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-24 review. One actionable finding, verified: 4.2 promised
contradiction handling "at L2/L3", but 4.2.1's detector groups atoms by
`project` and an L3 atom is scope=global with no `project` field — the
schema forbids one. Detection can never fire against L3, the one tier
that is always in context and never auto-demoted.
Scoped 4.2 to L1/L2 and split detection from rendering: the injected
`[contested]` tag stays tier-agnostic, since the schema permits
`contested` anywhere and a human can set it at adopt.
Added 9.6 rather than a loop change, because the L3 case is
underdetermined, not unbuilt. A project claim that differs from a global
one is either a correction to an over-generalised L3 atom or a
legitimate local exception, and no string-shape rule separates them.
Guessing "correction" is the expensive direction: it would tag the
persona tier as unreliable the first time any project deviates —
section 1's false-permanence failure, inverted. Records the live
consequence (5.1 injects L2 and L3 together, so two contradictory lines
can enter one context block unmarked) and three candidates, leaning
specificity-wins for v1.
Also: the "53 checks in six families" claim in 10.1 was stale by two
rounds and a reviewer quoted it back. Rather than correcting the number,
made the program own it — a final check asserts the doc cites the count
actually executed, so the sentence cannot go stale again. Documented the
working invocation in 10.1 too; the `.txt` parking means nothing in CI
gates this yet, and `python3 <(cat ...)` does not work because path
resolution walks up from __file__.
Verified: 69 checks, 0 failures; the count check fails when the doc
disagrees. derive_counters.py --check unchanged (363/89/663).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Round-23 review. All three findings verified against the tree first.
Fixture drift (3.1 L1 vs the schema's L2 example, captioned "one atom's
lifecycle read side by side"): the L1 snapshot carried the L2's
last_seen as its own first_seen, putting the first sighting three weeks
*after* the promotion it precedes. A second incoherence in the same pair
went unmentioned by the review and is arguably worse: the L1
back-pointer used the L2's `source` line (#L412) where its `first_source`
(#L77) belonged, which 3.1.1 forbids — promotion strips the path prefix
and nothing else, so a line number cannot drift across it. Both fixed by
anchoring the L1 fixture on the L2's first-sighting fields.
Added family 7 to the checker: within an atom, first_seen <= promoted_at
<= last_seen; across a lifecycle group, first_seen identical (4.1.1 takes
the min), last_seen/observations monotonic, sessions a superset, and
first_source surviving with only the prefix stripped. Families 1-6 all
passed the broken fixtures because each atom was independently
well-formed and nothing compared them — a prose claim that two fixtures
are one story imposes constraints no per-atom check can see. Verified by
re-injecting the old values: 2 failures, both named.
Manifest precedent was "5 for 5" and is 4 of 5. llm-wiki uses the same
on-disk nesting but declares the bare ["./skills"] — which IS one of root
CLAUDE.md's documented forms. That inverts the maintainer follow-up: the
question is which of two shapes the repo wants, not whether to bless an
undocumented fourth one, since a documented form already covers this
layout.
10.1 cited 644 -> 645 for the counter delta while a sibling file in the
same PR cited 663 -> 664. 663 is current; 644 predates book-to-skill.
Verified: 57 -> 68 checks, 0 failures; derive_counters.py --check
unchanged (363 skills, 89 plugins, 663 tools).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Three findings from the automated review, all contract-level:
- memory_schema.json: the tier=L3 -> promoted_from_projects conditional
was one-sided, so an L1/L2 atom carrying promoted_from_projects was
accepted. Added the else branch forbidding it outside L3 — the field
is not merely unnecessary there, it is meaningless, since scope is
still `project` and the array would claim cross-project evidence the
atom does not have.
- hooks.json: SessionStart declared no timeout and fell back to the
Claude Code default. Pinned to 5s. UserPromptSubmit keeps its 1s
backstop; SessionEnd stays async.
- DESIGN.md 4.1.1: state that the L2 -> L3 merge is lexical. normalize()
collapses whitespace/case/punctuation only, so two projects holding
the same rule in different words never merge. The failure is
one-directional — L3 under-fires, the claim stays live at L2 in each
project — and widening it needs an LLM or a per-user synonym table,
neither of which belongs in v1. Named in the same style as 4.2.1's
contradiction-detector limits.
Verified: assets/validate_examples.py.txt 57 checks / 0 failures; all
three schema examples still validate; an L1 atom carrying
promoted_from_projects is now rejected; derive_counters.py --check
passes unchanged (363 skills, 89 plugins, 663 tools).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Twenty-first review round. One fresh finding, and its sharp half is the
round-20 failure mode again in a new place.
validate_examples.py.txt derived BASE as dirname(dirname(__file__)) —
correct only at its current depth (assets/, two below the plugin root).
§10.1 moves it to skills/agent-memory/scripts/, four below, where that
form lands on the SKILL root instead. Verified by simulating both
layouts rather than reasoning about it:
- SCHEMA still resolved, by coincidence (assets/ and scripts/ become
siblings under the skill).
- DESIGN resolved to a path that does not exist and raised.
- The repo-root CLAUDE.md size check resolved to a nonexistent path and
was SKIPPED by its own `if os.path.exists(root)` guard — so the 84 KB
claim would stop being verified with the suite still printing green.
That last one is what makes this worth more than a path fix. Round 20 was
a whitelist that made a checker excuse the author's defect; this is a
guard that makes a checker excuse its own inability to run. Both report
success while checking nothing. A check that cannot locate its input has
FAILED, not passed, so the exists() guard is now an assertion.
BASE is anchored on a marker (walk up to DESIGN.md) instead of a fixed
dirname count, so the move cannot quietly disable anything. The repo root
is found by predicate — the directory holding BOTH CLAUDE.md and
scripts/derive_counters.py — because `.claude-plugin` alone is not a
repo-root marker: engineering/ carries one too, and anchoring on it lands
one level short. My first attempt did exactly that and the new assertion
caught it, which is the argument for the assertion.
Verified in three layouts: current (57 checks, 0 failures), the simulated
post-§10.1 move with links updated (57 checks, 0 failures — identical
count, so nothing is dropped by the move), and an orphaned copy with no
repo root above it (fails loudly, exit 1).
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Twentieth review round. One real defect, and the worse half is mine.
examples[0] embedded a $comment inside the atom object. `examples` entries
are INSTANCE data, and the schema declares additionalProperties: false
with no $comment in properties — so under any real validator (jsonschema,
ajv) that fixture fails the very schema it exists to demonstrate. The
distinction that makes this a bug rather than a style choice: $comment is
a SCHEMA keyword. It is legal at schema top level and inside the allOf
branches — where it still lives, untouched — and illegal in an instance.
The worse half: validate_examples.py.txt computed its allowed-property set
as `set(schema["properties"]) | {"$comment"}`. I wrote that whitelist to
make my own annotation pass. That is not a missing check; it is a check
deliberately weakened to tolerate the author's defect, which is strictly
worse than the round-8 happy-path gap — that one failed to look, this one
looked and was told to ignore what it saw. It also falsified the PR's own
testing claim, which said the examples validate against
additionalProperties when what they validated against was a checker built
to excuse them.
Fixed by removing the annotation from the fixture (option b) rather than
widening the schema, because instance data must be valid instance data.
The rationale it carried — why a third, L1 fixture exists at all: it is
the only thing exercising the L1 branch of the back-pointer conditional,
the one with PII consequences — now lives in §3.1 prose, where an
annotation about the fixtures belongs.
The workaround is gone from the checker, with a comment recording why it
must not come back. Verified both directions: re-injecting a $comment into
an example now fails "declares no unknown fields" (exit 1), and an
independent pass confirms all three fixtures carry zero undeclared keys.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Nineteenth review round, no blocking issues. Two acted on.
1. §4.1.1's "age >= 30 days" gate and §4.3's demotion both key off
promoted_at, but nothing required it — so an atom could reach L2
without recording when, never satisfy the age test, and sit
un-promotable forever without anything flagging it. Exactly the
silent-skip mode that made `redacted` required last round.
Now conditionally required at L2/L3. Deliberately NOT forbidden at L1,
which the reviewer's phrasing already got right and which matters more
than it looks: §4.3 demotes an expired L2 claim BACK to L1, and that
atom legitimately keeps the promoted_at from its earlier life —
forbidding it there would make every demoted atom invalid. The
asymmetry is documented in the field description rather than left for
someone to rediscover.
Added to the committed checker too, not just the schema (56 checks now,
was 54): a schema conditional nothing exercises is the same class of
gap as the rule it fixes. Verified — deleting promoted_at from the L2
fixture fails "committed tier records promoted_at", exit 1.
2. §8's citations were unverifiable to a reviewer without the upstream
repo. They are now checkable: the clone was read at commit
b44c6db5f5b1a011eed645efb1949840f99f961a (2026-08-05, tip of main at
inspection), and the Chinese source comment quoted in point 1 is
verbatim from MemoryProxy/src/agent-adapters/claude-code.ts lines 2-6.
Pinning the sha means the citation stays checkable even after upstream
moves.
Not acted on: the root CLAUDE.md pointer (same answer as last round — it
advertises the convention before it is ruled on, so it lands with the
maintainer's yes), and blessing the .py.txt parking pattern, which is the
same convention call and belongs in the same decision.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Eighteenth review round, no correctness bugs found. Two items acted on.
1. §6 rule 1 calls redaction "non-negotiable ... before any write", but
`redacted` was optional in the schema — so an atom that skipped the
pass entirely was schema-VALID, which is precisely the state the rule
forbids. The reviewer's framing is right: an invariant the schema
does not carry is a promise, not a guarantee.
`redacted` is now the 13th unconditionally required field. All four
fixtures already set it, so the rule is enforced immediately rather
than aspirationally, and memory_promote.py can now CHECK that
redaction ran instead of trusting every writer to remember. §3.1
separates it from the genuinely later-stage fields (contested,
contested_by, promoted_at) and says why it differs: those are set by
contradiction handling and promotion, whereas redaction has already
run by the time any atom exists on disk.
2. validate_examples.py.txt's published_normalize() did a bare
re.search(...).group(0) — a moved or renamed fence would raise a raw
AttributeError from inside a checker whose entire job is to fail
legibly. Now three explicit SystemExit paths (fence missing, fence
present but not valid Python, block ran but defined no normalize),
each naming the cause and the fix. Verified by renaming the function:
the checker prints the guidance and exits 1 rather than tracebacking.
Both regressions confirmed caught: dropping `redacted` from a fixture now
fails "has all required fields" (exit 1); the clean tree still passes 54
checks (exit 0).
Items 3 and 4 not acted on. 3 is the maintainer's convention call, now
raised by six reviewers. 4 (a pointer in root CLAUDE.md's Unreleased
section) is deliberately coupled to it — advertising this folder in the
repo's headline changelog would pre-empt the very decision I have been
declining to settle by commit, and would need reverting if the ruling
goes the other way.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Seventeenth review round, nothing blocking. One suggestion acted on, two
already-open items left open.
The reviewer's point on automating the example<->schema check is right,
and the sharpest version of it is one I had not weighed when declining
this twice on counter grounds: my verification logic has lived only in
throwaway shell heredocs, so it dies with the session that wrote it. The
next person editing this spec inherits nothing. Drift between DESIGN.md,
the schema and the fixtures has been the DOMINANT defect class across the
review — required-field drift, a tier the examples never exercised, ids
that stopped reproducing, headings inserted out of order, a confidence
value contradicting its own lifecycle narrative. Losing the checks that
found those is the real cost.
Added assets/validate_examples.py.txt — stdlib-only, 54 checks in six
families. Two properties make it more than a linter:
1. It EXECUTES the normalize() that DESIGN.md publishes rather than
reimplementing it, so doc and fixtures cannot silently disagree.
2. It is tested against INJECTED regressions, not just the happy path.
Four deliberate defects — an unstripped back-pointer (the OS-username
leak), a broken tier->scope pair, a wrong id, a confidence downgrade
— each make it exit 1; the clean tree exits 0. A checker that only
ever passes proves nothing, which is why the happy-path-only version
of this check missed the round-8 gap.
Parked as .txt, not .py, deliberately: this PR is spec-only and a .py
here is counted by derive_counters.py (measured 663 -> 664), producing a
counted tool belonging to no plugin in a folder that deliberately has no
SKILL.md. New §10.1 records that, names it the FIRST file the
implementation PR should land — before memory_extract.py, since
everything else is written against the contract it guards — and states
the reversal condition: if a spec-stage folder may carry tooling, it
becomes a .py and three counter files move, nothing else.
Suggestions 2 and 3 (the DESIGN.md-only convention; resolving §9.2 before
merge) are maintainer decisions already surfaced in the PR body. Five
reviewers have now independently raised the first.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Sixteenth review round. One real inconsistency, one scoping fix.
The §3.1 atom (atm_961f033d, L1) carries confidence "observed"; the same
id at L2 in the schema carries "stated" — and round 14 explicitly paired
those two as "one atom's lifecycle", so the pairing turned an
undocumented transition into a visible contradiction. Nothing in §4.1 or
§5.3's merge description said confidence could change at all.
This is not cosmetic: confidence selects both the session count (3/2/1)
and the distinct-days exemption, so WHEN it may change decides which gate
an atom is held to. Left unstated, two implementers would reasonably
build different machines.
Resolved by documenting the transition rather than flattening the
examples, because upgrading is correct on the merits — a claim the agent
inferred can later be stated outright or confirmed by a check, and
freezing confidence at extraction would hold the atom to a stricter gate
than its evidence warrants. New §4.1.3: total order observed < stated <
verified; merge takes max(existing, incoming); never downgrades, so a
weak re-observation cannot silently re-impose the slower gate; the gate
re-reads confidence at promotion time, not creation. §5.3's merge step
now lists it alongside observations and sessions.
Verification proves the example pair is a legal upgrade and that the L2
atom clears the gate its own confidence implies, rather than asserting
the prose is consistent.
Also scoped the fast paths to "(L1 → L2 only)" — L2 → L3 is gated on
distinct projects, not session count, so neither shortcut applies there.
Note: the heading-order linter added in round 13 caught me repeating the
round-13 mistake — §4.1.3 was inserted between 4.1.1 and 4.1.2. Fixed
before commit. That is the linter earning its keep one round after being
written.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Fifteenth review round, no correctness findings. Three concrete gaps,
all worth closing before implementation starts.
1. §5.4 said a writer that cannot acquire the lock "gives up and drops
its atoms, logging the loss" — and never said where. That is the one
place in the design where data disappears silently, so an unspecified
destination made the sentence decorative. Pinned to
.memory/errors.log (gitignored, 0600, capped 200 lines), added to the
§6 layout, and explicitly NOT stderr: SessionEnd is async, so its
stderr reaches nobody, which would make "logging the loss" a fiction.
/cs:memory status surfaces recent entries — a log nobody is pointed at
is the same as no log.
2. §10's planned tree moves assets/ under skills/agent-memory/, which
silently breaks two references: DESIGN.md's relative link to the
schema, and the schema's own $id. DESIGN.md stays at the plugin root
(it documents the plugin, not the skill), so the link lengthens rather
than staying put. Both now called out in a table with their post-move
values, matching how every other forward-looking wrinkle in this doc
is handled.
3. Open decision #2 has a repo-wide cost if it resolves toward an LLM
extractor that nobody had priced. Root CLAUDE.md's anti-patterns bullet
reads "one documented, opt-in exception" and names skillopt-sleep by
file; a second LLM-calling script makes that sentence false. The
implementation PR would have to amend that bullet rather than become a
silent second carve-out. Recorded as part of the cost of (b) — it is a
real argument for the rule-based path beyond recall.
Also stated in the Status header why a design doc lives under
engineering/ rather than the gitignored documentation/ folder (nothing
there is visible in a PR), while keeping the "is this a repeatable
pattern" question explicitly open for the maintainer.
Link checking now strips code spans before resolving relative links —
the previous version flagged the illustrative post-move path in item 2's
table as a dead link, which it is not.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Fourteenth review round, no correctness findings. Three clarity items,
all of which were real double-takes rather than nitpicks.
1. §5.4's 60s stale-break and 5s acquire timeout read as contradictory
("5 < 60, so how does stale-break ever fire?") because the doc never
said they answer different questions. Added a table: 60s is an mtime
AGE test — older than that, break immediately, no waiting; 5s is how
long a writer waits on a lock that is live and younger than 60s. The
stale path is checked first, not gated behind the wait.
2. AGENT_MEMORY_RECALL breaks the naming symmetry its two siblings keep
(they mirror SessionStart/SessionEnd exactly). Recorded why:
AGENT_MEMORY_USERPROMPTSUBMIT is the consistent name and is rejected
on ergonomics, since this is the variable a user reaches for most
often. Marked deliberate so a later round does not "fix" it.
3. atm_961f033d appears at tier L1 in DESIGN.md and tier L2 in the
schema, which reads like copy-paste. It is the atom's lifecycle: id
hashes claim+project with no tier component, so L1->L2 keeps it, and
stability is required because merge-on-re-observation keys on the id.
Writing that callout surfaced a defect in my own first draft of it: I
claimed promotion never mints a new id, but §4.1.1 step 3 does exactly
that at L2->L3, because dropping the project component changes the
hash input. Corrected before commit, and verification now PROVES both
halves by computing them (aid(claim, project) == the L1/L2 id;
aid(claim) != it) rather than asserting the prose is right.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Thirteenth review round. Three structural defects, all introduced by my
own earlier edits rather than present in the original draft.
1. §4.1.2 was placed physically BEFORE §4.1.1 — and §4.1.2's argument
cites the L2→L3 merge as the reason a state is unreachable, so a
reader met the citation before the section defining it. Reordered.
2. §4.1.1 and §4.1.2 were ### (h3), the same level as §4.1 itself,
while §3.1.1/§3.1.2 and §4.2.1 correctly use ####. Both bumped to
####. Both defects date to round 11, where the two sections were
spliced in without reconciling against the document's own structure.
3. "Hashing" was orphaned on its own line, residue from the round-7 edit
that replaced the text around it. Reattached.
The reviewer's framing is the right one: these undercut a document whose
pitch is self-checking rigor. So the fix is not just the three edits —
verification now includes a structural linter that derives expectations
from the numbering itself rather than checking known strings: heading
LEVEL must equal numbering depth + 1, and heading NUMBERS must ascend in
document order. Both would have failed on this state before the fix, and
they cover every section, not the three that happened to break.
Also the sixth round where a check failed on my own assertion rather than
the artifact (backticks in `scope`). Prose checks now normalize markdown
emphasis and code markers alongside case and whitespace, which is the
last of the three ways these assertions have been brittle.
Counters unchanged: 363 / 89.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Twelfth review round.
1. Section 1 and section 9.1 both claimed root CLAUDE.md is "~40 KB".
It is 85,875 bytes — 84 KB, more than 2x the stated figure. The
argument survives (the real number strengthens the bloat case), but
the error is the embarrassing kind for this doc specifically: section
2 opens "verified by reading the code, not the docs", and this was
eyeballed. Both sites now carry the measured figure with the method
(wc -c) so it is checkable rather than asserted, and verification
recomputes it from the file instead of matching a string.
2. The promotion table read "observations >= 3 across >= 3 distinct
sessions", presenting two conditions where there is one. The schema's
own observations description says the field is informational and the
gate counts sessions; since observations is always >= len(sessions),
the extra clause adds nothing except the risk that an implementer
gates on the wrong field — the exact confusion the sessions-as-a-set
rule exists to prevent. Row now names sessions as the gate and says
explicitly that observations is not it.
Also fifth round running where a verification check failed on my own
assertion rather than the artifact (this time case: "Informational" vs
"informational"). Prose checks now normalize case and whitespace once at
the top rather than being patched per-match.
Counters verified on the merged base: 363 / 89, unchanged by this PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Eleventh review round. Two genuine design gaps in the state machine, both
of which would have surfaced mid-implementation.
1. A tier=L1/scope=global atom was schema-legal but could never promote.
The scope field's own description said scope "is assigned at
extraction", so an extractor could mint one; but L1->L2 requires "same
project" and a global atom has no project field, while L2->L3 is a
merge over two or more L2 atoms. Such an atom would sit at L1 until it
expired at 90 days, silently, since nothing flagged it.
Closed by making scope a function of tier rather than a free choice:
L1 and L2 are project, global exists ONLY as the product of the L2->L3
merge. Enforced with a tier/scope conditional so a wrong extractor
fails validation instead of quietly producing orphans. This is also
correct on the merits — whether a claim is global is not knowable at
extraction; it becomes global by holding in a second project, which is
exactly what the merge represents. New section 4.1.2.
2. Section 4.1 gated L1->L2 on "no contradiction open", but 4.2 defined
contradiction handling only at L2/L3 — so the gate referenced a state
nothing produced. New section 4.2.1 defines detection at L1 with two
deterministic rules (explicit negation; same-subject different-value),
run at merge time within a project. States plainly what they do NOT
catch (semantic contradiction needs meaning, not string shape) and why
that is acceptable: the miss is bounded, since 4.2's L2/L3 handling
catches it one tier later and the human gate at adopt is what actually
holds. A narrow detector claiming completeness would be worse than one
that documents its edge.
Also: merged dev (branch was cut before #941) so the "adds nothing
countable" claim is verified against the real base — derive_counters
--check passes at 363 skills / 89 plugins, up from the 362/88 the branch
was cut at. Corrected the stale productivity/handoff evidence in section
10 (7 scripts + 2 hooks = 9 counted tools, not 5 + 2); the underlying
claim that hooks/*.py count was independently verified and is unchanged.
Section 1's skill count is now "360+" so it stops drifting with dev.
Counters verified on the merged base: 363 / 89, unchanged by this PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Tenth review round, no blocking findings. All three folded in.
1. The examples array held only L2 and L3 atoms, both using the stripped
back-pointer form — so the L1 branch of the tier conditional, the one
the $comment calls out as having PII consequences, was never exercised
by the schema's own self-test. DESIGN.md showed an L1 atom in prose,
but the schema read standalone never demonstrated the prefixed form.
My own verification had the same hole: it hand-evaluated L1 with a
synthetic case rather than a committed fixture, so nothing in the
repo pinned it.
Added a third example at tier L1 carrying the full <cwd-slug> path.
Chose a claim with kind='failure' so it also demonstrates a third
kind (the others were constraint and preference) rather than only
patching the coverage gap. Verification now asserts the examples span
all three tiers and both scope branches, and still rejects both an
unstripped L2 pointer and a prefix-less L1.
2. The L1->L2 row read "≥ 3 distinct sessions, ≥ 2 of them on distinct
days", which invites the wrong reading (2 of which sessions?). The
intent, stated precisely later in 4.1, is that the sessions SPAN two
days. Row now reads "spanning ≥ 2 distinct calendar days (UTC)",
matching the precision used elsewhere.
3. Recorded the stale-lock TOCTOU as a deliberate acceptance rather than
leaving it to be discovered. Two writers can both judge a lock stale;
the consequence is bounded by the design already there — each still
commits via os.replace, so the loser's atoms are lost, not corrupted,
and lost L1 candidates re-observe next session. A true mutex costs
portability (fcntl semantics vary across NFS and Windows) to buy
durability this tier does not need. Noted that it should not be
"fixed" without first showing the loss is observable.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Ninth review round, no blocking findings. Three of four folded in; the
fourth is a maintainer call I have deliberately not made.
1. Added section 3.1.2 — the invariants JSON Schema CANNOT enforce,
because it validates one atom at a time and a valid atom does not
imply a valid store: first_seen <= last_seen (no cross-field
comparison), id uniqueness across atoms.jsonl, and per-claim session
uniqueness across records (uniqueItems is within-array only). Each is
assigned an owner in memory_extract.py / memory_promote.py. Verified
the one thing the schema DOES enforce — within-atom sessions
uniqueness, which is what the durability gate actually depends on, so
the boundary is exact rather than hand-waved. Recorded because "the
schema validates" is easy to mistake for "the store is consistent".
2. Section 9.5 now says that if the cold-start measurement forces
outcome (c) — drop UserPromptSubmit — hooks/hooks.json must shrink
with it. Deleting the hook from the design alone would leave the
contract file asserting a hook the design no longer wants; a contract
must not outlive the decision that justified it.
3. Recorded the $id rationale as a $comment in the schema. Three
separate review rounds have now proposed conflicting changes to this
non-functional field (blob-vs-raw, dev-vs-main), so the reasoning is
written down to stop the churn: raw.githubusercontent because
github.com/blob/ serves HTML and breaks $ref dereferencing, and main
because $id identifies the published artifact — a dead link until the
dev -> main promotion lands is accepted deliberately over a URL that
goes stale the moment it does.
Not acted on: whether a DESIGN.md-only folder under a domain is a
pattern to keep or a one-off. Two reviewers have now raised it; it is a
repo-convention decision for the maintainer, not one to settle by
pushing a commit.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Eighth review round. Three findings, one of which closes a gap round 7
opened.
1. The tier-dependent back-pointer rule was prose-only. Round 7 argued
hard that stripping the ~/.claude/projects/<cwd-slug>/ prefix at
promotion is "not cosmetic" — an unstripped L2/L3 back-pointer commits
a contributor's OS username to a shared CLAUDE.md. But the schema left
source/first_source as bare {"type":"string"} with a description,
while the two OTHER promotion rules (project<->scope,
promoted_from_projects<->tier) were both machine-enforced
conditionals. The one rule with PII consequences was the one relying
on prose — which contradicts round 7's own framing that a contract
must be self-checking rather than self-asserting.
Added a tier-keyed conditional: at L1, source and first_source must
match ^~/\.claude/projects/[^/]+/...\.jsonl#L[0-9]+$; at L2/L3 they
must match ^[A-Za-z0-9._-]+\.jsonl#L[0-9]+$, which no path-prefixed
value can satisfy. memory_promote.py now fails loudly on a bad
promotion instead of leaking silently.
Verification hand-evaluates the conditional (jsonschema is not
available — stdlib-only repo) against three cases beyond the fixtures:
an unstripped L2 pointer is REJECTED, the stripped form is ACCEPTED,
and a prefix-less L1 is REJECTED so the local form stays
direct-openable. Asserting the rule fires is the point; asserting only
that the fixtures pass would have missed it.
2. Section 4.3 cited section 5.3 for the L3 cap. 5.3 is SessionEnd
capture; the cap ("Budget: 2 KB L3 + 4 KB L2", truncate by last_seen)
is in 5.1, SessionStart read. Corrected, and the check now confirms
the cited section actually contains the budget rather than trusting
the number.
3. The section 3.1.1 insertion split section 3.1's field bullet list,
leaving the `confidence` bullet abutting 3.1.1's closing paragraph
where it read as a stray continuation. Moved 3.1.1 to after the
complete list, so the list stays whole and the back-pointer deep-dive
is its own uninterrupted unit.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Seventh review round. Two real defects, both in the contract file.
1. The back-pointer format contradicted the de-identification rule it is
promoted through. source/first_source are required at EVERY tier, and
the format is ~/.claude/projects/<cwd-slug>/<session>.jsonl#L<line>
where <cwd-slug> is Claude Code's slugification of the ABSOLUTE working
directory — so on a real machine it reads -home-alice-work-... and
embeds the OS username. A strictly-compliant implementation of rule 4
("cite, don't invent") would therefore write a contributor's username
into a shared, git-tracked CLAUDE.md on the first promotion, violating
section 6's de-identification requirement. Rule 4 would have won,
being the more mechanical of the two.
Resolution: the format is now explicitly tier-dependent. L1
(gitignored) keeps the full local path; promotion into L2/L3 strips
the prefix to <session>.jsonl#L<line>. Nothing is lost — session ids
are globally unique, so the prefix is recoverable at read time by
globbing ~/.claude/projects/*/<session>.jsonl. Required at BOTH
promotion boundaries, since L1->L2 is the first crossing into
committed territory.
Round 3's fixture fix masked this: the placeholder read -home-user-,
which looks de-identified only because that machine's username is
literally "user". The L2/L3 fixtures now carry the portable form, so
the examples demonstrate the rule instead of hiding it.
2. Neither worked example id reproduced. atm_7f3a9c21 / atm_b41c8de0
were invented in round 1, before sha256 was pinned in round 3, and
nothing since re-derived them — in the file that calls itself the
contract. Recomputed to atm_961f033d / atm_12cc1dc2, and the exact
normalize() is now pinned in the doc (collapse whitespace, casefold,
strip trailing punctuation — order matters). Verification executes
that published function and re-derives every id, so the contract is
self-checking rather than self-asserting.
Also: stale-lock threshold quantified at 60s (the one unquantified
number in the doc), and the section 10 manifest form recorded as a
follow-up for root CLAUDE.md rather than silently changed here.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Sixth review round, one substantive nit.
Section 3.1 opened "Every field is mandatory", which is false: 12 of the
18 fields are unconditionally required, and the example directly beneath
it shows two that are not (project is conditional on scope, redacted is
set by a later stage). Left as-is this would push whoever writes
memory_extract.py to over-constrain the extractor — emitting fields at
extraction time that belong to redaction, contradiction handling, or
promotion, and emitting `project` on global-scoped atoms where the schema
forbids it outright.
Replaced with the true count plus an explicit accounting of why each of
the other six is conditional or deferred, and a note that the example
carries project/redacted because of what that particular atom is, not
because either is universal. Verification now cross-checks the prose
against the schema field by field rather than pattern-matching a phrase,
so the two cannot drift apart again.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Fifth review round. Three findings, all verified empirically rather than
reasoned about — two were wrong in ways that would have broken the
implementation PR's CI.
1. Section 10 claimed "tools +3", counting only scripts/*.py.
derive_counters.py counts EVERY .py outside repo-root scripts/, so the
three hooks/*.py count too. Verified by adding one file under hooks/
in this tree: python_tools moved 644 -> 645. productivity/handoff
confirms it independently — 5 scripts/ + 2 hooks/ files, documented
repo-wide as "7 stdlib-only Python tools". Corrected to +6, with the
evidence recorded so the implementation PR does not trip
derive_counters.py --check.
2. The planned layout put SKILL.md flat at the plugin root. Every
comparable agents+commands plugin nests it under skills/<name>/ —
skillopt-sleep, write-a-skill, agent-harness, handoff and llm-wiki are
5 for 5, and two of them are already cited in this doc for their hook
and staging patterns, so matching their directory shape too is the
consistent call. Tree corrected, and the plugin.json skills form
pinned to ["./skills/agent-memory"] to match.
3. Section 6 listed .memory/ as gitignored while marking adopted.log
inside it as committed, with no pattern shown. Not a contradiction but
it needed stating, because the obvious spelling does not work: a
directory-level `.memory/` ignore is never descended into, so a `!`
negation cannot re-include anything under it. Verified both spellings
in a scratch repo — `.memory/*` + negation tracks adopted.log,
`.memory/` + negation tracks nothing. Documented the working pattern
and why adopted.log is public by design (it records what already
cleared the L2/L3 bar into CLAUDE.md).
Also scoped the chmod non-negotiable to runtime-created files: git
tracks no POSIX mode beyond the executable bit, so a fresh checkout
materializes adopted.log at the cloner's umask and no in-repo
declaration changes that. Anything whose confidentiality depends on
mode bits must be gitignored, which is now stated as the reason
atoms.jsonl and staged/ are.
Counters unchanged (362 / 88) — still no SKILL.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Fourth review round. Five findings, all valid.
1. Section 4.1.1's L2->L3 merge assigned first_source but never source,
though the schema requires both — so memory_promote.py written against
this contract would have emitted atoms the schema rejects. Same class
as the promoted_from_projects gap from round 3. Merge now sets source
from the contributor with the latest last_seen, which also preserves
the field contract (first_source = oldest, source = newest) across the
merge boundary rather than only within one atom's history.
2. Concurrency was entirely unaddressed. SessionEnd is async and does a
read-modify-write on .memory/atoms.jsonl; two sessions on one repo
(several terminals, or worktrees) interleave and lose writes, and the
recall read can catch a partially-rewritten file. New section 5.4
reuses the repo's existing pattern rather than inventing one, citing
both precedents by line: agent-harness loop_controller.py:54-62 and
skillopt-sleep state.py:77. Writers take an exclusive lock then temp +
os.replace; readers take NO lock, because blocking UserPromptSubmit on
a lock held by an async SessionEnd would blow the 100ms budget for a
hook whose failure mode is meant to be "return nothing" — atomic
replacement is what makes lock-free reads safe. Writers that cannot
acquire within 5s drop their atoms and log it: losing one session's
candidates is recoverable, a wedged SessionEnd is not.
3. The aggregate collision claim was wrong. "Likelier than not across ~30
users" does not follow from a 0.29% per-file rate — 30 files give
8.4%, and even odds needs ~239. Replaced with the correct figures. The
conclusion (widen the id if the cap rises) was unaffected, but the
stated justification was false.
4. Fixtures used the real session id that generated this PR, which
section 6.5 — added last round precisely to stop fixture-realism
violations — forbids. All seven ids are now uniformly synthetic
(01SESSION<X>...), so none can be mistaken for real and the pattern is
assertable.
5. confidence:"verified" is the sharpest form of the extraction risk
section 9.2 only gestured at generally: hardest to assign lexically
(it requires recognising a check actually ran) yet lowest promotion
bar (1 observation, exempt from the distinct-days clause). Leaning
recorded: a rule-based extractor must never assign it — reserve it for
atoms minted by a tool that ran the check and can name it.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Third review round. Both findings valid; the first is sharper than the
report framed it.
1. The spec violated its OWN admission policy inside its own examples.
Section 6 requires committed content be "interpreted, de-identified,
non-confidential" — and the fixtures named a repo that appears nowhere
in this public tree and embedded its local transcript path. Two costs:
the section 6 analogy was unverifiable to any reader of this repo, and
a project name that was not ours to publish shipped in a committed
file. Fixtures are committed data; the policy binds them.
Fixed by genericizing the example project and restating the admission
analogy against llm-wiki, which is in this tree and checkable. Added
rule 6.5 making the policy explicitly binding on the spec's own
examples, since the underlying mistake was treating fixture data as
exempt — that is the part that would otherwise recur.
2. The id hash was unspecified. Section 4.1 said hash() without naming a
function, which for Python is actively dangerous: the builtin hash()
is salted per process for str, so ids would differ every run and
merging — the mechanism the whole durability gate rests on — would
silently never fire. Now pinned to stdlib hashlib.sha256 truncated to
8 hex, with the salting hazard called out.
Also replaced the unstated 32-bit id-space assumption with arithmetic:
birthday collision is 0.0029% at the 500-atom cap, 0.29% at 5000, so
raising the cap requires widening the id. A collision silently merges
two unrelated claims' durability counters, which is the same failure
project-scoping was added to prevent.
Verification now asserts fixtures carry no unknown project slugs, and
recomputes the collision table rather than trusting the numbers in prose.
Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Second review round. Two findings were real schema bugs that would have
corrupted promotion logic once memory_promote.py was written against it.
1. The schema could not represent the L2->L3 criterion it exists to gate.
Section 4.1 requires a claim hold at L2 in >= 2 distinct projects, but
`project` is a single string and the scope conditional FORBIDS it once
scope flips to "global" — so the multi-project evidence was discarded
at exactly the moment it stopped being an eligibility test and became
an audit trail. Added `promoted_from_projects`, required at L3 with a
minItems:2 conditional so the rule is enforced by the schema rather
than merely described in prose.
2. Atom identity was not project-scoped. `id` hashed claim text alone, so
two unrelated claims normalizing alike in different repos ("tests must
pass before merge") would collide and merge their `sessions` arrays
across projects — manufacturing false durability, since the L1->L2
gate requires sessions from the SAME project. Now
hash(claim + NUL + project) for project-scoped atoms.
This made a genuinely missing step visible: with project-scoped ids a
claim held in two projects is TWO atoms, so L2->L3 is a merge, not a
flag flip. That step was undocumented anywhere. New section 4.1.1
specifies it — group by the project-free hash, union sessions, sum
observations, min/max the timestamps, record contributors, retain the
contributing L2 atoms as the provenance chain.
3. `source` was a single string overwritten on every merge, so an
"anti-fabrication" field retained only the latest sighting and lost
the evidence that first justified the claim. Added `first_source`,
written once and never overwritten; both are now required.
Also fixed the $id ref dev -> main for a stable identifier. Deliberately
NOT switched to the github.com/blob/ form used by the repo's one other
schema: blob URLs serve HTML, not JSON, so that convention breaks any
tooling that resolves $id. Noted as pre-existing rather than propagated.
Verification now walks every JSON block in DESIGN.md plus the schema
examples, and asserts the L3 conditional is enforced rather than only
documented. Counters unchanged (362 / 88).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Six findings from automated review, all verified against the files before
acting. None architectural; all resolved in place.
1. Latency budget contradicted its own contract. DESIGN.md led with a
"hard 100 ms budget" while hooks.json set "timeout": 1 — the hook
timeout field is in SECONDS, so the contract permitted 10x the stated
budget. Split into two explicitly-named limits: a 100 ms internal
self-budget the script enforces against a monotonic clock, and the 1 s
hook timeout as a wedged-process backstop. States outright that
finishing under 1 s does not satisfy the spec.
2. The 100 ms budget was asserted with no mechanism to reach it. Bounded
the work: .memory/atoms.jsonl capped at 500 atoms with last_seen
eviction, single linear pass, bounded top-5 heap. Added open decision
9.5 requiring the budget be MEASURED before implementation, since
interpreter cold-start is the dominant cost and is not controllable
from inside the script — and naming "drop UserPromptSubmit entirely"
as an acceptable outcome. A recall hook that misses its budget every
prompt is worse than no recall hook.
3. The section 3.1 atom example omitted the required `tier` field, so a
reader could copy an invalid atom out of the doc that is meant to BE
the contract. Added; verification now parses every JSON block in
DESIGN.md, not only the schema's own examples, which is why this
drifted undetected.
4. Session ids were 8 chars in the schema examples and 24 in DESIGN.md.
Normalized to 24 everywhere; check asserts a single length across all
examples.
5. Schema $id was not a resolvable URL (GitHub blob path missing /blob/
<ref>/), which fails silently if tooling ever resolves it for $ref.
Now a raw.githubusercontent.com URL.
6. The "stated" fast path (2 sessions instead of 3) did not say whether
the >= 2-distinct-days clause survived, so one long working day could
have minted an L2 claim. Clause explicitly retained; "verified"
documented as the only exemption.
Counters unchanged (362 skills / 88 plugins) — still no SKILL.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
Design spec only — no SKILL.md, no plugin.json, no Python. Repo counters
are deliberately untouched (derive_counters counts skills by SKILL.md).
Derived from an inspection of TencentCloud/TencentDB-Agent-Memory (MIT).
Borrows two design ideas — the L0->L3 memory tiering and the
ownership/visibility model — and rejects its integration mechanism. No
code vendored.
The core idea: flat CLAUDE.md has exactly one injection policy (always,
in full), which causes bloat, staleness, and false permanence. Tiering
splits memory by durability and gives each tier its own retrieval policy:
L0 transcripts never injected, L1 atoms recalled on relevance, L2
injected per-project at SessionStart, L3 always in context under a cap.
Promotion is deterministic and recurrence-based (>= 3 distinct sessions
across >= 2 days for L1->L2), not importance-based, and requires a live
L0 back-pointer — keeping it stdlib-only per the no-LLM-in-scripts rule.
Includes a grounded overlap analysis against existing skills. Notably
skillopt-sleep already implements the L0 reader (harvest.py walks
~/.claude/projects/*/*.jsonl) and the protected-marker-block write; the
delta is tiering, prompt-time recall, and a durability gate. Spec
concludes agent-memory must be a separate self-contained plugin rather
than an extension, since skillopt-sleep is a vendored copy carrying 23
re-vendor deviations, and cross-skill imports are a repo anti-pattern.
Rejects MemoryProxy (ANTHROPIC_BASE_URL interception) on four grounds:
reverse-engineered CC internals, subscription-to-metered billing change,
raw conversation persistence incompatible with the repo's compliance
posture, and zero test coverage.
Files: DESIGN.md, hooks/hooks.json (contract), assets/memory_schema.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym